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 握手机制:谁等谁?

握手有三种情况,我画个图你就明白了:

CLK T0 T1 T2 T3 T4 TVALID TREADY(提前) ← 从设备一直就绪,数据在T0就传输 TREADY(延迟) ← 从设备在T1才就绪,数据在T1传输 ✓ ✓ ✓

看到了吗?数据只在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协议其实不复杂,但细节决定成败。我个人觉得,只要记住三点:

  1. 握手看TVALID+TREADY,两者同时为高才算数
  2. 包边界靠TLAST,最后一个beat别忘了拉高
  3. 字节对齐靠TKEEP,哪个字节有效就置1哪个位

TUSER是个好东西,别浪费了。用它来带一些控制信息,能让你的设计灵活很多。嗯,今天就到这儿,下次咱们聊聊怎么用AXI4-Stream来对接高速收发器的内部接口。