4、Aurora用户接口:流接口(AXI4-Stream)与帧接口详解

好,咱们进入正题。Aurora协议的用户接口,说白了就是FPGA内部逻辑跟Aurora核之间怎么“说话”。这部分我当年刚接触时也绕了不少弯路,今天给你掰开揉碎讲清楚。

Aurora核提供了两种用户接口:流接口(AXI4-Stream)帧接口(Framing Interface)。你可能会问,为什么搞两个?嗯,这得从应用场景说起。

4.1 流接口(AXI4-Stream)

流接口,顾名思义,就是数据像水流一样,源源不断地送出去。它基于AXI4-Stream标准,但做了一些简化。我个人习惯把它叫做“无头无尾”的接口——没有包的概念,只有字节流。

4.1.1 信号定义

先看核心信号,我列个表,你对照着看:

信号名 方向 位宽 说明
s_axi_tx_tdata 输入 用户可配(2/4/8字节) 发送数据总线
s_axi_tx_tvalid 输入 1 发送数据有效
s_axi_tx_tready 输出 1 接收端准备好
s_axi_tx_tkeep 输入 位宽/8 字节使能(流接口可选)
s_axi_tx_tlast 输入 1 帧结束标志(流接口可选)
m_axi_rx_tdata 输出 用户可配 接收数据总线
m_axi_rx_tvalid 输出 1 接收数据有效
m_axi_rx_tready 输入 1 用户准备好接收

这里有个关键点:tready和tvalid的握手关系。我曾经在项目里吃过亏——以为只要tvalid拉高,数据就一定能送出去。其实不是的,必须tvalid和tready同时为高,数据才算真正传输。

核心规则:AXI4-Stream的传输发生在tvalid和tready同时为高的时钟沿。数据不会丢失,但可能会被插入等待周期。

4.1.2 流接口的典型用法

流接口最适合什么场景?我告诉你,连续数据流。比如ADC采样数据、视频流、或者你只是想把一堆数据从A点搬到B点,不需要关心数据边界。

看个简单的发送代码片段:

// 流接口发送示例
always @(posedge clk) begin
    if (rst) begin
        s_axi_tx_tvalid <= 1'b0;
        s_axi_tx_tdata  <= 64'd0;
    end else begin
        // 当有数据要发送且Aurora核准备好时
        if (tx_data_valid && s_axi_tx_tready) begin
            s_axi_tx_tvalid <= 1'b1;
            s_axi_tx_tdata  <= tx_data;
        end else if (s_axi_tx_tready) begin
            // 没有数据时,拉低valid
            s_axi_tx_tvalid <= 1'b0;
        end
    end
end

你想想看,这个代码里最容易被忽略的是什么?是tready的下降沿。当Aurora核内部FIFO快满时,它会拉低tready。这时候如果你还傻等着tvalid一直高,数据就卡在那了。我建议你加个状态机来管理。

个人技巧:我习惯在发送端加一个深度为16的异步FIFO,把用户数据先缓存起来。这样即使Aurora核偶尔反压,也不会丢数据。这个FIFO的几乎满信号可以反压给上游。

4.2 帧接口(Framing Interface)

帧接口就不一样了。它是有“边界”的接口。每个传输单元都是一个完整的帧,有起始、有结束。说白了,它更适合包协议的场景。

4.2.1 帧接口的信号差异

帧接口比流接口多了几个关键信号:

信号名 说明
s_axi_tx_tsof 帧起始(Start of Frame)
s_axi_tx_teof 帧结束(End of Frame)
m_axi_rx_tsof 接收帧起始
m_axi_rx_teof 接收帧结束

嗯,这里要注意:帧接口的tlast信号含义不同。在流接口里tlast只是可选的边界指示,但在帧接口里,teof是必须的。没有teof,接收端就不知道帧什么时候结束。

4.2.2 帧格式与对齐

帧接口的数据传输有个特点:数据必须按字节对齐。什么意思?就是说你发送的帧长度必须是字节的整数倍。我曾经遇到一个坑——发送端凑了个奇数长度的帧,结果接收端解析时总是错位。

避坑指南:我曾经在一个雷达数据采集项目里,因为帧长度不是4字节对齐,导致接收端DMA搬运数据时总是少几个字节。后来我强制在帧尾补了填充字节,问题才解决。记住:帧长度最好是数据总线位宽的整数倍

4.2.3 帧接口的时序

看一个典型的帧发送时序:

// 帧接口发送状态机示例
localparam IDLE    = 2'd0;
localparam SEND    = 2'd1;
localparam WAIT_EOF = 2'd2;

reg [1:0] state, next_state;

always @(posedge clk) begin
    if (rst)
        state <= IDLE;
    else
        state <= next_state;
end

always @(*) begin
    next_state = state;
    case (state)
        IDLE: begin
            if (tx_frame_valid && s_axi_tx_tready)
                next_state = SEND;
        end
        SEND: begin
            if (s_axi_tx_tready && s_axi_tx_teof)
                next_state = IDLE;
            else if (!s_axi_tx_tready)
                next_state = WAIT_EOF;
        end
        WAIT_EOF: begin
            if (s_axi_tx_tready)
                next_state = SEND;
        end
    endcase
end

这个状态机里,我特意加了WAIT_EOF状态。为什么?因为当tready被拉低时,你不能丢掉当前帧的teof标志。必须等tready恢复后,再送出teof。这个细节,很多初学者会忽略。

4.3 流接口 vs 帧接口:怎么选?

好,现在你知道了两种接口的区别。那实际项目里怎么选?我给你几个判断标准:

  • 选流接口:数据是连续流,没有包边界。比如视频像素流、连续采样数据。或者你上层协议自己管理包边界,不需要Aurora核帮你做帧定界。
  • 选帧接口:数据是离散的包,每个包有独立含义。比如以太网帧、自定义命令包。帧接口的tsof/teof可以帮你自动生成帧定界符。

我个人经验是:能用帧接口就别用流接口。为什么?因为帧接口的边界信息更完整,接收端恢复数据时更容易。我在一个光纤通信项目里,一开始用流接口,结果接收端总是要自己猜帧边界,后来换成帧接口,代码量直接减半。

重要提醒:帧接口的tkeep信号在帧接口模式下是必须使用的。它指示了最后一拍数据中哪些字节是有效的。如果你不用tkeep,接收端会认为所有字节都有效,可能导致数据错位。

4.4 两种接口的共享设计技巧

有些场景下,你可能需要同时支持两种接口。比如你的IP核要兼容不同用户的需求。这时候怎么办?

我建议你做一个接口转换桥。把帧接口的tsof/teof映射成流接口的tlast,或者反过来。核心逻辑就一句话:

// 帧接口转流接口示例
assign s_axi_tx_tlast = s_axi_tx_teof;  // 帧结束映射为流结束

但要注意:帧接口的tsof在流接口里没有对应信号。如果你需要帧起始信息,就得自己加一个sideband信号。我一般会在数据总线的最高位带一个sof标志位,接收端解析时提取出来。

我的习惯:在设计用户逻辑时,我统一使用帧接口。即使数据是连续的,我也人为地按固定长度(比如1024字节)切分成帧。这样接收端处理起来更规整,调试也方便。你可以试试这个思路。

好了,关于流接口和帧接口,核心内容就这些。记住一句话:流接口是水管,帧接口是包裹。选哪个,看你寄的是自来水还是快递。下一节我们讲时钟补偿和通道绑定,那才是Aurora真正让人头疼的地方。