3. AXI4-Stream协议详解:TVALID/TREADY握手协议、数据包格式、KEEP与STRB信号
各位同学,今天我们来啃一块硬骨头——AXI4-Stream协议。说实话,这个协议在高速数据传输场景下太常见了,尤其是做SerDes接口设计,几乎天天跟它打交道。我刚开始接触时也觉得信号挺多,但用熟了就会发现,它其实就三板斧:握手、数据包、控制信号。
3.1 TVALID/TREADY握手协议——数据流动的节拍器
AXI4-Stream最核心的机制就是握手。说白了,就是发送方和接收方之间的一种默契——你准备好了吗?我准备好了吗?好,咱们一起走。
握手信号就两个:
- TVALID:发送方说「我有数据要给你」
- TREADY:接收方说「我准备好收了」
数据什么时候才算真正传输成功?当TVALID和TREADY同时为高的那个时钟上升沿。这个点很重要,我见过不少新手在这里栽跟头。
核心规则:数据只在TVALID和TREADY同时为高的时钟周期传输。缺一个都不行。
握手有三种常见场景,我画个图帮大家理解:
你看这个图,T0时刻TVALID先拉高了,但TREADY还没准备好,数据D0就一直等着。直到T1时刻TREADY也拉高,数据才传过去。这就是典型的「发送方先就绪」场景。
个人经验:我建议在设计时,如果接收方可以立即接收,尽量让TREADY保持高电平。这样能减少握手延迟,提高吞吐量。但要注意,如果接收方FIFO快满了,必须及时拉低TREADY,否则数据会丢失。
3.2 数据包格式——TLAST与TKEEP的配合
AXI4-Stream的数据是以包(Packet)为单位传输的。一个包可能包含多个数据传输周期。怎么知道一个包结束了?靠TLAST信号。
TLAST在包的最后一个数据周期拉高。这个信号在SerDes接口中特别重要——比如你要传输一个以太网帧,或者一个PCIe TLP,都需要明确知道包边界。
我曾经遇到过一个坑:某个项目中,发送端在包结束时忘了拉高TLAST,导致接收端一直等数据,整个链路卡死了。排查了两天才找到原因,后来我就在代码里加了断言检查——TLAST必须在包结束时为高,否则报错。
3.3 KEEP与STRB信号——字节级别的控制
这两个信号容易混淆,我当年也花了不少时间才搞明白。
| 信号 | 全称 | 作用 | 使用场景 |
|---|---|---|---|
| TKEEP | Byte Keep | 指示哪些字节是有效数据 | 数据流中夹杂无效字节时 |
| TSTRB | Byte Strobe | 指示哪些字节有有效校验 | 需要字节级写使能时 |
说白了,TKEEP控制数据有效性,TSTRB控制校验有效性。两者都是位宽为数据宽度/8的位向量。
举个例子,假设数据总线是32位(4字节),TKEEP=4'b1011,表示第0、1、3字节有效,第2字节无效。接收端看到TKEEP为0的字节,应该忽略该字节的数据。
注意:TKEEP和TSTRB不是互斥的。它们可以同时使用,也可以只用其中一个。在SerDes接口中,我通常只用TKEEP,因为数据校验一般在链路层处理,不需要TSTRB。
3.4 实战中的握手设计要点
嗯,这里我要多说几句。实际项目中,握手设计有几个容易出问题的地方:
- 死锁问题:发送方等接收方就绪,接收方等发送方就绪,两边都卡死了。我建议在设计中加入超时机制,或者确保至少有一方不依赖对方信号。
- 背压处理:接收方处理不过来时,必须能拉低TREADY。但拉低TREADY时,发送方必须保持TVALID和数据不变。这个规则一定要遵守。
- 跨时钟域:如果TVALID和TREADY来自不同时钟域,需要做同步处理。我一般用两级触发器同步,或者用异步FIFO。
核心原则:TVALID不能依赖TREADY,但TREADY可以依赖TVALID。也就是说,发送方不能等接收方说「我准备好了」才把数据准备好,而应该先准备好数据再等接收方。
3.5 一个简单的Verilog示例
最后,我给大家看一个简单的AXI4-Stream发送端代码框架。这个代码我实际用过,稍作修改就能用在SerDes接口中。
module axis_sender (
input wire clk,
input wire rst_n,
// AXI4-Stream 接口
output reg m_axis_tvalid,
input wire m_axis_tready,
output reg [31:0] m_axis_tdata,
output reg m_axis_tlast,
output reg [3:0] m_axis_tkeep,
// 内部数据接口
input wire data_valid,
input wire [31:0] data_in,
input wire data_last
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
m_axis_tvalid <= 1'b0;
m_axis_tdata <= 32'd0;
m_axis_tlast <= 1'b0;
m_axis_tkeep <= 4'hF;
end else begin
// 当数据有效且握手成功时,更新输出
if (data_valid && (!m_axis_tvalid || (m_axis_tvalid && m_axis_tready))) begin
m_axis_tvalid <= 1'b1;
m_axis_tdata <= data_in;
m_axis_tlast <= data_last;
m_axis_tkeep <= 4'hF; // 默认所有字节有效
end else if (m_axis_tvalid && m_axis_tready) begin
// 握手成功,如果没有新数据,拉低TVALID
m_axis_tvalid <= 1'b0;
end
end
end
endmodule
这段代码的核心逻辑是:当有数据要发送时,拉高TVALID并输出数据。如果接收方还没准备好(TREADY为低),数据保持不变。直到握手成功,才准备发送下一个数据。
避坑指南:我曾经在代码里忘了处理「TVALID为高但TREADY为低时数据不能变」这个规则,结果接收方收到了错误的数据。后来我加了一个条件判断——只要TVALID为高且TREADY为低,数据就不能更新。这个bug花了我一整天调试,大家引以为戒。
好了,AXI4-Stream协议的核心内容就这些。TVALID/TREADY握手是基础,TLAST定义包边界,TKEEP/TSTRB控制字节粒度。把这些搞清楚了,SerDes接口的数据通路设计就成功了一半。
可可精品资料库