2. AXI4-Stream协议详解:协议握手规范、数据包结构、TUSER/TLAST信号、实战:搭建一个简单的AXIS数据发生器

大家好,欢迎来到第二章。上一章我们聊了AXI总线的整体架构,今天咱们聚焦到AXI4-Stream——这个在图像处理领域用得最多的接口。说实话,我刚开始接触图像加速时,总觉得AXI4-Stream不就是个数据流嘛,能有多复杂?结果第一次调通就踩了坑。嗯,咱们今天就把这个协议彻底讲透。

2.1 协议握手规范:VALID与READY的舞蹈

AXI4-Stream的核心,说白了就是一对握手信号:TVALID和TREADY。发送方说“我有数据”,接收方说“我准备好了”,一拍即合,数据就传过去了。

握手有三种情况:

  • TVALID先有效,TREADY后有效:发送方先准备好,等接收方。这是最常见的场景。
  • TREADY先有效,TVALID后有效:接收方提前说“我随时可以收”,发送方准备好就发。我习惯在接收端用这种模式,能减少一个时钟周期的延迟。
  • 同时有效:一拍搞定,效率最高。

关键规则:一旦TVALID拉高,发送方就不能再改变数据,直到握手成功。我曾经在调试时忽略了这一点,导致数据错位,查了整整一下午。

握手时序图如下:

时钟周期 CLK TVALID TREADY TDATA 握手成功 ✓ T1 T2 T3 T4

个人技巧:我写握手逻辑时,习惯把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标记行结束
  • 实战中注意时序对齐,避免帧边界错位

可可精品资料库