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主机的性能主要受两个因素影响:
- 数据宽度:更宽的数据总线意味着更高的吞吐量。但要注意,FPGA的布线资源有限,过宽的数据总线可能导致时序问题。
- 流水线深度: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主机与从机之间的数据流和握手信号。主机内部包含数据源、控制逻辑、握手状态机和输出FIFO。数据通过TDATA总线传输,TLAST标记包结束,TVALID和TREADY负责握手控制。
可可精品资料库