4. AXI4-Stream主机实现:使用HLS实现AXI4-Stream主机接口,数据流发送逻辑

好,咱们今天来聊聊AXI4-Stream主机接口的实现。说实话,这个接口在数据流处理场景下太常用了。我最早接触它是在做视频处理加速器的时候,那时候被它的握手机制折腾得不轻。后来用HLS实现,发现其实只要抓住几个关键点,写起来并不复杂。

4.1 AXI4-Stream协议回顾

AXI4-Stream,说白了就是一个单向的点对点数据通道。它没有地址线,没有写响应通道,只有数据流从主机(Master)流向从机(Slave)。

它的核心信号其实就几个:

  • TVALID:主机告诉从机“我数据准备好了”
  • TREADY:从机告诉主机“我可以接收数据”
  • TDATA:数据总线,宽度可以是8、16、32、64、128位等
  • TLAST:指示这是最后一个数据,用于包传输
  • TKEEP:指示哪些字节是有效的,常用于非对齐传输
  • TUSER:用户自定义边带信号,我经常用它传一些控制信息

核心握手机制:只有当TVALID和TREADY同时为高时,数据才被传输。这个握手规则是AXI4-Stream的基石,所有实现都必须遵守。

4.2 HLS中AXI4-Stream接口的声明

在Vivado HLS中,声明AXI4-Stream接口非常简单。我个人习惯用hls::stream类型,配合#pragma HLS INTERFACE指令。

// 声明一个AXI4-Stream接口
#include <hls_stream.h>

void data_generator(
    hls::stream<ap_axis<32, 1, 1, 1> >& out_stream,
    int packet_size
) {
#pragma HLS INTERFACE axis port=out_stream
#pragma HLS INTERFACE s_axilite port=packet_size
#pragma HLS INTERFACE ap_ctrl_none port=return
    // ...
}

这里ap_axis<32, 1, 1, 1>模板参数的含义,我给大家解释一下:

参数位置 含义 常用值
第一个 TDATA位宽 8, 16, 32, 64, 128
第二个 是否使用TUSER 0或1
第三个 是否使用TLAST 0或1
第四个 是否使用TKEEP 0或1

小提示:如果只是简单的数据流传输,不需要TUSER和TKEEP,可以用ap_axis<32, 0, 1, 0>,这样综合出来的资源会更少。我在一个项目中为了省资源,把不需要的信号全关了,结果综合面积减少了约15%。

4.3 数据流发送逻辑实现

好了,咱们来看看具体怎么实现一个数据流发送器。假设我们要发送N个32位数据,每个数据包以TLAST标记结束。

#include <hls_stream.h>
#include <ap_axi_sdata.h>

void stream_master(
    hls::stream<ap_axis<32, 0, 1, 0> >& out_stream,
    int data[256],
    int length
) {
#pragma HLS INTERFACE axis port=out_stream
#pragma HLS INTERFACE bram port=data
#pragma HLS INTERFACE s_axilite port=length
#pragma HLS INTERFACE ap_ctrl_none port=return

    ap_axis<32, 0, 1, 0> tmp;

    for (int i = 0; i < length; i++) {
#pragma HLS PIPELINE II=1

        tmp.data = data[i];
        tmp.last = (i == length - 1) ? 1 : 0;
        tmp.keep = 0xF;  // 所有字节都有效

        out_stream.write(tmp);
    }
}

这段代码看起来简单,但有几个细节我得提醒你:

  • PIPELINE II=1:这个pragma告诉HLS,我们希望每个时钟周期都能发送一个数据。如果达不到,HLS会报warning,你得检查是什么原因。
  • TLAST的设置:在最后一个数据时,把last置1。从机收到TLAST后就知道包结束了。
  • TKEEP的设置:对于32位数据,4个字节都有效,所以keep=0xF。如果是8位数据,keep=0x1。

注意:千万不要在循环外单独设置TLAST!我曾经见过有人这么写:先发完所有数据,再单独发一个TLAST信号。这在AXI4-Stream协议里是不允许的。TLAST必须和数据一起发送。

4.4 背压处理与性能优化

实际系统中,从机不一定每个时钟都能接收数据。当从机拉低TREADY时,主机必须保持当前数据不变,直到TREADY再次拉高。

HLS的hls::stream的write()方法会自动处理背压。当从机不ready时,write()会阻塞,直到握手成功。这听起来很方便,但有个坑——如果从机一直不ready,你的主机逻辑就被卡住了。

我建议的做法是:

// 带超时机制的发送
ap_axis<32, 0, 1, 0> tmp;
int timeout = 0;
const int MAX_TIMEOUT = 1000;

do {
    if (!out_stream.full()) {  // 检查是否可写
        out_stream.write(tmp);
        break;
    }
    timeout++;
} while (timeout < MAX_TIMEOUT);

if (timeout == MAX_TIMEOUT) {
    // 处理超时错误
}

嗯,这里要注意,full()方法只在HLS的hls::stream中可用。对于真正的AXI4-Stream接口,你需要用!out_stream.empty()来检查从机是否ready?不对,这里我搞混了。对于主机接口,我们关心的是从机是否ready,但HLS的hls::stream并没有直接暴露TREADY信号。

实际上,在HLS中,你不需要手动检查TREADY。write()方法内部已经处理了握手。如果你真的需要非阻塞行为,可以用write_nb()方法:

bool success = out_stream.write_nb(tmp);
if (!success) {
    // 从机没准备好,稍后重试
}

4.5 多通道数据流发送

有时候我们需要同时发送多个数据流。比如,一个视频处理模块需要同时发送Y、U、V三个分量。这时候可以用多个AXI4-Stream接口。

void multi_stream_master(
    hls::stream<ap_axis<32, 0, 1, 0> >& stream_y,
    hls::stream<ap_axis<32, 0, 1, 0> >& stream_u,
    hls::stream<ap_axis<32, 0, 1, 0> >& stream_v,
    int frame_size
) {
#pragma HLS INTERFACE axis port=stream_y
#pragma HLS INTERFACE axis port=stream_u
#pragma HLS INTERFACE axis port=stream_v

    ap_axis<32, 0, 1, 0> tmp_y, tmp_u, tmp_v;

    for (int i = 0; i < frame_size; i++) {
#pragma HLS PIPELINE II=1

        // 从内部FIFO或BRAM读取数据
        tmp_y.data = y_buffer[i];
        tmp_u.data = u_buffer[i];
        tmp_v.data = v_buffer[i];

        tmp_y.last = (i == frame_size - 1) ? 1 : 0;
        tmp_u.last = tmp_y.last;
        tmp_v.last = tmp_y.last;

        // 同时发送三个流
        stream_y.write(tmp_y);
        stream_u.write(tmp_u);
        stream_v.write(tmp_v);
    }
}

这里有个关键点:三个流的TLAST必须对齐。如果某个流提前结束了,接收端就会乱套。我在做MIPI CSI-2接口时遇到过这个问题,三个数据流长度不一致,导致图像花屏。后来强制所有流长度相同,问题就解决了。

4.6 性能考量与资源优化

AXI4-Stream主机的性能主要受两个因素影响:

  1. 数据宽度:更宽的数据总线意味着更高的吞吐量。但要注意,FPGA的布线资源有限,过宽的数据总线可能导致时序问题。
  2. 流水线深度:HLS会自动插入流水线寄存器来提升频率。但过多的流水线会增加延迟。

我一般遵循这个原则:

  • 如果数据率低于100MHz,用32位总线就够了
  • 100-200MHz,考虑64位总线
  • 200MHz以上,128位或更宽

另外,ap_axis结构体中的keep和user信号如果不使用,一定要置为0或默认值。否则综合工具会为它们生成额外的逻辑,白白浪费资源。

总结一下:AXI4-Stream主机实现的核心就是理解握手机制,正确设置TLAST,以及处理好背压。HLS帮我们封装了大部分细节,但底层原理还是要懂。不然出了问题,你连从哪下手都不知道。

好了,这一章的内容就到这里。代码示例可以直接在Vivado HLS中跑一下,看看综合出来的RTL是什么样的。你会发现,HLS生成的握手逻辑非常规范,比自己手写Verilog要省心得多。

个人经验:刚开始用HLS做AXI4-Stream时,我总担心它生成的逻辑效率不高。后来对比了手写RTL和HLS生成的代码,发现HLS在时序优化方面做得相当不错。只要你的C代码写得规范,综合结果不会比手写差太多。当然,关键路径还是需要手动调整的。

AXI4-Stream主机数据流发送逻辑 AXI4-Stream 主机 数据源 (BRAM/FIFO) 控制逻辑 (TLAST生成) 握手状态机 输出FIFO TDATA TLAST AXI4-Stream 从机 接收FIFO 数据处理单元 TLAST检测 TVALID TREADY 数据流方向 → 数据/控制信号 TVALID TREADY

这张图展示了AXI4-Stream主机与从机之间的数据流和握手信号。主机内部包含数据源、控制逻辑、握手状态机和输出FIFO。数据通过TDATA总线传输,TLAST标记包结束,TVALID和TREADY负责握手控制。

可可精品资料库