2. AXI4-Stream协议详解:协议握手规范、数据包结构、TUSER/TLAST信号、实战:搭建一个简单的AXIS数据发生器
大家好,欢迎来到第二章。上一章我们聊了AXI总线的整体架构,今天咱们聚焦到AXI4-Stream——这个在图像处理领域用得最多的接口。说实话,我刚开始接触图像加速时,总觉得AXI4-Stream不就是个数据流嘛,能有多复杂?结果第一次调通就踩了坑。嗯,咱们今天就把这个协议彻底讲透。
2.1 协议握手规范:VALID与READY的舞蹈
AXI4-Stream的核心,说白了就是一对握手信号:TVALID和TREADY。发送方说“我有数据”,接收方说“我准备好了”,一拍即合,数据就传过去了。
握手有三种情况:
- TVALID先有效,TREADY后有效:发送方先准备好,等接收方。这是最常见的场景。
- TREADY先有效,TVALID后有效:接收方提前说“我随时可以收”,发送方准备好就发。我习惯在接收端用这种模式,能减少一个时钟周期的延迟。
- 同时有效:一拍搞定,效率最高。
关键规则:一旦TVALID拉高,发送方就不能再改变数据,直到握手成功。我曾经在调试时忽略了这一点,导致数据错位,查了整整一下午。
握手时序图如下:
个人技巧:我写握手逻辑时,习惯把TVALID和TREADY的时序单独画出来。你想想看,如果两个信号同时变化,很容易出现亚稳态。所以我会在接收端加一级寄存器打拍,虽然多一个周期延迟,但稳定性好很多。
2.2 数据包结构:TDATA、TKEEP、TSTRB
AXI4-Stream的数据总线是TDATA,宽度可以是8、16、32、64、128位等。图像处理中常用64位或128位,因为一个像素通常是8位或16位,宽总线能一次传多个像素。
这里有个容易忽略的信号——TKEEP。它指示哪些字节是有效的。比如TDATA是32位(4字节),TKEEP就是4位。如果TKEEP=4'b1111,说明4个字节都有效;如果TKEEP=4'b0011,只有低2个字节有效。
至于TSTRB,它表示哪些字节是数据而非填充。说实话,我在图像处理项目中很少用TSTRB,因为图像数据基本都是连续的。但如果你做的是数据包拼接,TSTRB就很有用了。
| 信号 | 宽度 | 作用 | 我常用的场景 |
|---|---|---|---|
| TDATA | 8/16/32/64/128/...位 | 数据总线 | 传输像素值 |
| TKEEP | TDATA宽度/8 | 字节有效指示 | 处理非对齐数据 |
| TSTRB | TDATA宽度/8 | 字节类型指示 | 几乎不用 |
2.3 TUSER与TLAST:图像帧的边界标记
图像处理中,TUSER和TLAST是灵魂信号。
- TLAST:标记一行的结束。比如一张1920x1080的图像,每传完1920个像素,TLAST就拉高一次。我刚开始做时,以为TLAST只标记最后一行的结束,结果图像全乱了。
- TUSER:用户自定义信号。我习惯用它标记帧的开始。比如TUSER拉高时,表示新一帧的第一个像素来了。这样接收端就能准确知道帧边界。
避坑指南:我曾经在一个项目中,TUSER和TLAST的时序没对齐,导致接收端把上一帧的最后一个像素当成下一帧的第一个像素。图像出现了“撕裂”效果。后来我强制要求TUSER在帧的第一个像素时拉高,TLAST在行最后一个像素时拉高,两者互不干扰。
2.4 实战:搭建一个简单的AXIS数据发生器
理论讲完了,咱们动手写个代码。这个数据发生器会生成一个简单的图像数据流:每帧4行,每行4个像素,像素值从0递增。
module axis_data_gen #(
parameter DATA_WIDTH = 32 // 每个时钟传4个像素(每个像素8位)
)(
input wire clk,
input wire rst_n,
// AXIS 输出
output reg m_axis_tvalid,
input wire m_axis_tready,
output reg [DATA_WIDTH-1:0] m_axis_tdata,
output reg m_axis_tlast,
output reg m_axis_tuser
);
// 内部计数器
reg [3:0] pixel_cnt; // 行内像素计数(0-3)
reg [1:0] line_cnt; // 行计数(0-3)
// 数据生成逻辑
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_tuser <= 1'b0;
pixel_cnt <= 4'd0;
line_cnt <= 2'd0;
end else begin
// 握手成功时更新数据
if (m_axis_tvalid && m_axis_tready) begin
// 生成4个像素值(每个8位)
m_axis_tdata <= {8'(pixel_cnt*4+3), 8'(pixel_cnt*4+2),
8'(pixel_cnt*4+1), 8'(pixel_cnt*4+0)};
// 行结束标记
if (pixel_cnt == 4'd3) begin
m_axis_tlast <= 1'b1;
pixel_cnt <= 4'd0;
line_cnt <= line_cnt + 1'b1;
end else begin
m_axis_tlast <= 1'b0;
pixel_cnt <= pixel_cnt + 1'b1;
end
// 帧开始标记(第一行第一个像素)
if (line_cnt == 2'd0 && pixel_cnt == 4'd0) begin
m_axis_tuser <= 1'b1;
end else begin
m_axis_tuser <= 1'b0;
end
// 始终有效
m_axis_tvalid <= 1'b1;
end else begin
// 初始状态或背压时保持
m_axis_tvalid <= 1'b1;
end
end
end
endmodule
调试心得:我写这个模块时,习惯在仿真中先检查TUSER和TLAST的时序。你想想看,如果TUSER在帧中间意外拉高,接收端就会误判帧边界。所以我会在仿真脚本里加一句断言:assert property (@(posedge clk) disable iff (!rst_n) $rose(m_axis_tuser) |-> (line_cnt==0 && pixel_cnt==0));
这个发生器虽然简单,但涵盖了AXI4-Stream的核心要素:握手、数据、行结束、帧开始。你可以把它作为图像处理流水线的起点,后面接上滤波、缩放等模块。
好了,这一章就到这里。记住,AXI4-Stream的握手是基础,TUSER和TLAST是图像处理的关键。下一章我们会用这个发生器来驱动一个真实的图像处理IP,到时候你就知道这些信号有多重要了。
本章要点回顾:
- 握手规则:TVALID和TREADY必须同时有效才能传输
- 数据包结构:TDATA传数据,TKEEP指示有效字节
- TUSER标记帧开始,TLAST标记行结束
- 实战中注意时序对齐,避免帧边界错位
可可精品资料库