4、Aurora用户接口:流接口(AXI4-Stream)与帧接口详解
好,咱们进入正题。Aurora协议的用户接口,说白了就是FPGA内部逻辑跟Aurora核之间怎么“说话”。这部分我当年刚接触时也绕了不少弯路,今天给你掰开揉碎讲清楚。
Aurora核提供了两种用户接口:流接口(AXI4-Stream)和帧接口(Framing Interface)。你可能会问,为什么搞两个?嗯,这得从应用场景说起。
4.1 流接口(AXI4-Stream)
流接口,顾名思义,就是数据像水流一样,源源不断地送出去。它基于AXI4-Stream标准,但做了一些简化。我个人习惯把它叫做“无头无尾”的接口——没有包的概念,只有字节流。
4.1.1 信号定义
先看核心信号,我列个表,你对照着看:
| 信号名 | 方向 | 位宽 | 说明 |
|---|---|---|---|
| s_axi_tx_tdata | 输入 | 用户可配(2/4/8字节) | 发送数据总线 |
| s_axi_tx_tvalid | 输入 | 1 | 发送数据有效 |
| s_axi_tx_tready | 输出 | 1 | 接收端准备好 |
| s_axi_tx_tkeep | 输入 | 位宽/8 | 字节使能(流接口可选) |
| s_axi_tx_tlast | 输入 | 1 | 帧结束标志(流接口可选) |
| m_axi_rx_tdata | 输出 | 用户可配 | 接收数据总线 |
| m_axi_rx_tvalid | 输出 | 1 | 接收数据有效 |
| m_axi_rx_tready | 输入 | 1 | 用户准备好接收 |
这里有个关键点:tready和tvalid的握手关系。我曾经在项目里吃过亏——以为只要tvalid拉高,数据就一定能送出去。其实不是的,必须tvalid和tready同时为高,数据才算真正传输。
核心规则:AXI4-Stream的传输发生在tvalid和tready同时为高的时钟沿。数据不会丢失,但可能会被插入等待周期。
4.1.2 流接口的典型用法
流接口最适合什么场景?我告诉你,连续数据流。比如ADC采样数据、视频流、或者你只是想把一堆数据从A点搬到B点,不需要关心数据边界。
看个简单的发送代码片段:
// 流接口发送示例
always @(posedge clk) begin
if (rst) begin
s_axi_tx_tvalid <= 1'b0;
s_axi_tx_tdata <= 64'd0;
end else begin
// 当有数据要发送且Aurora核准备好时
if (tx_data_valid && s_axi_tx_tready) begin
s_axi_tx_tvalid <= 1'b1;
s_axi_tx_tdata <= tx_data;
end else if (s_axi_tx_tready) begin
// 没有数据时,拉低valid
s_axi_tx_tvalid <= 1'b0;
end
end
end
你想想看,这个代码里最容易被忽略的是什么?是tready的下降沿。当Aurora核内部FIFO快满时,它会拉低tready。这时候如果你还傻等着tvalid一直高,数据就卡在那了。我建议你加个状态机来管理。
个人技巧:我习惯在发送端加一个深度为16的异步FIFO,把用户数据先缓存起来。这样即使Aurora核偶尔反压,也不会丢数据。这个FIFO的几乎满信号可以反压给上游。
4.2 帧接口(Framing Interface)
帧接口就不一样了。它是有“边界”的接口。每个传输单元都是一个完整的帧,有起始、有结束。说白了,它更适合包协议的场景。
4.2.1 帧接口的信号差异
帧接口比流接口多了几个关键信号:
| 信号名 | 说明 |
|---|---|
| s_axi_tx_tsof | 帧起始(Start of Frame) |
| s_axi_tx_teof | 帧结束(End of Frame) |
| m_axi_rx_tsof | 接收帧起始 |
| m_axi_rx_teof | 接收帧结束 |
嗯,这里要注意:帧接口的tlast信号含义不同。在流接口里tlast只是可选的边界指示,但在帧接口里,teof是必须的。没有teof,接收端就不知道帧什么时候结束。
4.2.2 帧格式与对齐
帧接口的数据传输有个特点:数据必须按字节对齐。什么意思?就是说你发送的帧长度必须是字节的整数倍。我曾经遇到一个坑——发送端凑了个奇数长度的帧,结果接收端解析时总是错位。
避坑指南:我曾经在一个雷达数据采集项目里,因为帧长度不是4字节对齐,导致接收端DMA搬运数据时总是少几个字节。后来我强制在帧尾补了填充字节,问题才解决。记住:帧长度最好是数据总线位宽的整数倍。
4.2.3 帧接口的时序
看一个典型的帧发送时序:
// 帧接口发送状态机示例
localparam IDLE = 2'd0;
localparam SEND = 2'd1;
localparam WAIT_EOF = 2'd2;
reg [1:0] state, next_state;
always @(posedge clk) begin
if (rst)
state <= IDLE;
else
state <= next_state;
end
always @(*) begin
next_state = state;
case (state)
IDLE: begin
if (tx_frame_valid && s_axi_tx_tready)
next_state = SEND;
end
SEND: begin
if (s_axi_tx_tready && s_axi_tx_teof)
next_state = IDLE;
else if (!s_axi_tx_tready)
next_state = WAIT_EOF;
end
WAIT_EOF: begin
if (s_axi_tx_tready)
next_state = SEND;
end
endcase
end
这个状态机里,我特意加了WAIT_EOF状态。为什么?因为当tready被拉低时,你不能丢掉当前帧的teof标志。必须等tready恢复后,再送出teof。这个细节,很多初学者会忽略。
4.3 流接口 vs 帧接口:怎么选?
好,现在你知道了两种接口的区别。那实际项目里怎么选?我给你几个判断标准:
- 选流接口:数据是连续流,没有包边界。比如视频像素流、连续采样数据。或者你上层协议自己管理包边界,不需要Aurora核帮你做帧定界。
- 选帧接口:数据是离散的包,每个包有独立含义。比如以太网帧、自定义命令包。帧接口的tsof/teof可以帮你自动生成帧定界符。
我个人经验是:能用帧接口就别用流接口。为什么?因为帧接口的边界信息更完整,接收端恢复数据时更容易。我在一个光纤通信项目里,一开始用流接口,结果接收端总是要自己猜帧边界,后来换成帧接口,代码量直接减半。
重要提醒:帧接口的tkeep信号在帧接口模式下是必须使用的。它指示了最后一拍数据中哪些字节是有效的。如果你不用tkeep,接收端会认为所有字节都有效,可能导致数据错位。
4.4 两种接口的共享设计技巧
有些场景下,你可能需要同时支持两种接口。比如你的IP核要兼容不同用户的需求。这时候怎么办?
我建议你做一个接口转换桥。把帧接口的tsof/teof映射成流接口的tlast,或者反过来。核心逻辑就一句话:
// 帧接口转流接口示例
assign s_axi_tx_tlast = s_axi_tx_teof; // 帧结束映射为流结束
但要注意:帧接口的tsof在流接口里没有对应信号。如果你需要帧起始信息,就得自己加一个sideband信号。我一般会在数据总线的最高位带一个sof标志位,接收端解析时提取出来。
我的习惯:在设计用户逻辑时,我统一使用帧接口。即使数据是连续的,我也人为地按固定长度(比如1024字节)切分成帧。这样接收端处理起来更规整,调试也方便。你可以试试这个思路。
好了,关于流接口和帧接口,核心内容就这些。记住一句话:流接口是水管,帧接口是包裹。选哪个,看你寄的是自来水还是快递。下一节我们讲时钟补偿和通道绑定,那才是Aurora真正让人头疼的地方。