3. AXI4-Stream协议详解:信号定义、握手机制、数据包格式、字节对齐与TUSER/TKEEP
AXI4-Stream,说白了就是高速数据搬运的“高速公路”。它不像AXI4-Full那样有地址、读写通道那么复杂,它只干一件事:把数据从A点推到B点,而且可以无限流下去。我当年第一次接触它时,觉得这玩意儿太简单了,结果在字节对齐上栽了个跟头……嗯,咱们今天就把这些坑一个个填平。
3.1 信号定义:最少需要几根线?
AXI4-Stream最核心的信号只有三根:
| 信号名 | 方向 | 作用 |
|---|---|---|
| TVALID | 主→从 | 主设备说:“数据准备好了,你拿走吧!” |
| TREADY | 从→主 | 从设备说:“我准备好了,你发吧!” |
| TDATA | 主→从 | 真正的数据总线,宽度可以是8、16、32、64、128……任意字节的整数倍 |
你想想看,就这三根线,已经能完成最基本的流式传输了。但实际项目中,我们几乎不会只用这三根——因为还有TLAST、TKEEP、TUSER、TSTRB、TID、TDEST等等。我个人习惯,最少会带上TLAST和TKEEP,否则很多场景根本没法用。
核心原则:TVALID和TREADY同时为高时,一个数据传输完成。这叫“一次握手”。
3.2 握手机制:谁等谁?
握手有三种情况,我画个图你就明白了:
看到了吗?数据只在TVALID和TREADY同时为高的时钟沿传输。从设备可以提前拉高TREADY(表示“我随时能收”),也可以等数据来了再拉高(表示“我还没准备好”)。
我的经验:在高速收发器场景中,我建议从设备尽量提前拉高TREADY。因为一旦TVALID已经拉高,从设备再反应就慢了一个周期,容易造成反压。我曾经在一个10Gbps的链路上吃过这个亏,后来改成“TREADY常高,只在FIFO快满时才拉低”,吞吐率直接拉满。
3.3 数据包格式:TLAST的妙用
流式数据总得有头有尾吧?TLAST就是干这个的。
- TLAST=0:当前数据只是包中间的一部分,后面还有
- TLAST=1:当前数据是包的最后一个beat,传完这个就结束了
举个例子,一个64字节的包,数据总线是32位(4字节),那么需要16个beat传完。前15个beat的TLAST=0,最后一个beat的TLAST=1。
注意:TLAST必须和最后一个数据的TVALID同时有效。我曾经见过有人把TLAST晚了一个周期拉高,结果接收端把下一个包的第一个beat当成了上一个包的结尾……嗯,调试了两天才发现。
3.4 字节对齐:TKEEP的真相
TKEEP是很多新手搞不清楚的信号。说白了,它就是个“字节使能”。
假设数据总线是64位(8字节),但你的包可能不是8字节的整数倍。比如一个34字节的包,最后一个beat只有2字节有效,剩下的6字节是垃圾。这时候TKEEP就派上用场了:
// 64位数据总线,最后一个beat
// 数据:0x12345678_9ABCDEF0 (8字节)
// 但只有低2字节有效
TDATA = 0x12345678_9ABCDEF0
TKEEP = 0b00000011 // 只有bit0和bit1为1,表示byte0和byte1有效
TLAST = 1 // 这是最后一个beat
TKEEP的每一位对应一个字节:
- TKEEP[n]=1:对应的字节是有效数据
- TKEEP[n]=0:对应的字节是填充数据,接收端应该忽略
关键区别:TKEEP和TSTRB容易混淆。TSTRB是“写使能”,用于指示哪些字节需要被写入;TKEEP是“有效指示”,用于指示哪些字节是真正的数据。在大多数高速收发器场景中,我们只用TKEEP,不用TSTRB。
3.5 TUSER:你的“私人定制”通道
TUSER是AXI4-Stream里最灵活的信号。它不参与数据传输,但可以携带任何你想带的“元信息”。
我在项目中常用TUSER来干这些事:
- 包起始标记:在第一个beat的TUSER里打上标记,接收端知道“新包来了”
- 错误标志:如果数据在传输过程中发生了CRC错误,可以在TUSER里置位
- 通道号:多个逻辑通道复用时,用TUSER区分
- 时间戳:记录数据进入系统的时间
举个例子,一个典型的以太网帧在AXI4-Stream上的表现:
// 第一个beat:帧头
TDATA = {目的MAC, 源MAC, 类型/长度}
TUSER = 0x01 // bit0=1表示这是帧起始
TKEEP = 0xFF // 所有字节都有效
TLAST = 0
// 中间beat:数据载荷
TDATA = {payload bytes}
TUSER = 0x00
TKEEP = 0xFF
TLAST = 0
// 最后一个beat:帧尾
TDATA = {最后几个字节 + FCS}
TUSER = 0x02 // bit1=1表示这是帧结束
TKEEP = 0x0F // 只有低4字节有效
TLAST = 1
我的习惯:TUSER的位宽我一般设成和数据总线宽度相同(或者至少32位)。这样每个字节都能对应一个标记位,用起来特别顺手。曾经有个项目TUSER只设了8位,结果后面要加功能时发现不够用,只能重新改RTL……嗯,教训深刻。
3.6 完整握手时序示例
咱们来看一个完整的传输过程,假设数据总线32位,传输一个10字节的包:
时钟周期 | TVALID | TREADY | TDATA | TKEEP | TLAST | TUSER
---------|--------|--------|-------------|-------|-------|------
T0 | 1 | 1 | 0xAABBCCDD | 1111 | 0 | 0x01 ← 第一个beat,4字节
T1 | 1 | 1 | 0x11223344 | 1111 | 0 | 0x00 ← 第二个beat,4字节
T2 | 1 | 1 | 0x5566XXXX | 0011 | 1 | 0x02 ← 最后一个beat,只有2字节有效
T3 | 0 | 1 | XXXXXXXXXX | XXXX | X | XX ← 空闲周期
这个包总共10字节,分3个beat传完。注意最后一个beat的TKEEP=0b0011,表示只有低2字节有效。TUSER在第一个beat标记了包起始(0x01),最后一个beat标记了包结束(0x02)。
避坑指南:我曾经在调试时发现接收端收到的数据总是错位。查了半天,原来是发送端在最后一个beat的TKEEP设置错了——它把高字节使能了,但实际数据在低字节。记住:TKEEP必须和TDATA中的有效字节位置严格对应,不能想当然。
3.7 总结一下
AXI4-Stream协议其实不复杂,但细节决定成败。我个人觉得,只要记住三点:
- 握手看TVALID+TREADY,两者同时为高才算数
- 包边界靠TLAST,最后一个beat别忘了拉高
- 字节对齐靠TKEEP,哪个字节有效就置1哪个位
TUSER是个好东西,别浪费了。用它来带一些控制信息,能让你的设计灵活很多。嗯,今天就到这儿,下次咱们聊聊怎么用AXI4-Stream来对接高速收发器的内部接口。