3. HLS数据通路基础:ap_fifo、ap_hs、axis接口类型
各位同学,今天我们来聊聊HLS里最基础也最绕不开的话题——接口类型。说实话,我刚接触Vivado HLS那会儿,看到ap_fifo、ap_hs、axis这几个名字,第一反应是:这不就是几个握手协议嘛,有啥好学的?结果第一次做项目就被狠狠教育了一顿。
嗯,咱们今天就把这三个接口类型彻底讲透。你想想看,数据通路说白了就是数据怎么从一个模块流到另一个模块。而接口类型,就是定义这个流动规则的。
3.1 为什么需要不同的接口类型?
先问个问题:如果你的设计里只有一个函数,输入输出都是简单的int,那HLS默认会生成什么?答案是ap_none——也就是没有握手信号,数据直接连进去。这在纯组合逻辑里没问题,但一旦涉及流水线、多周期操作,你就需要告诉HLS:数据什么时候有效,什么时候可以读走。
我个人习惯把接口类型分成三类:
- ap_fifo:先进先出,适合流式数据
- ap_hs:握手信号,适合控制密集型
- axis:AXI-Stream,适合高速数据通路
说白了,它们都是「带控制信号的接口」,只是控制信号的复杂度和协议规范不同。
3.2 ap_fifo:最简单的流接口
ap_fifo是我个人用得最多的接口类型。为什么?因为它简单。只有两个控制信号:
- valid:数据有效
- ready:接收方准备好
没有地址,没有ID,没有last信号。数据就是一个接一个地流进去。
核心特点:
- 单向数据流,没有反压机制(除了ready)
- 适合生产者-消费者模型
- 硬件实现就是FIFO,深度可配置
来看一个例子:
// ap_fifo接口示例
void fifo_example(
hls::stream<int>& input_fifo, // ap_fifo输入
hls::stream<int>& output_fifo // ap_fifo输出
) {
#pragma HLS INTERFACE ap_fifo port=input_fifo
#pragma HLS INTERFACE ap_fifo port=output_fifo
int data = input_fifo.read(); // 阻塞读
int result = data * 2;
output_fifo.write(result); // 阻塞写
}
我在项目中遇到过一个问题:用ap_fifo做数据缓存时,如果读写速率不匹配,FIFO会满或者空。这时候如果你用阻塞读写,整个流水线就卡住了。所以后来我习惯加上非阻塞的read_nb和write_nb,配合empty()和full()判断。
小技巧:ap_fifo的深度默认是2,如果你需要更大的缓冲,记得用DEPTH参数:#pragma HLS INTERFACE ap_fifo port=input_fifo depth=16
3.3 ap_hs:带握手的通用接口
ap_hs比ap_fifo多了什么?多了地址和读写控制。说白了,它就是一个带握手信号的RAM接口。
控制信号包括:
- address:地址总线
- ce:时钟使能
- we:写使能
- data:数据总线
- valid/ready:握手信号
为什么需要ap_hs?因为有些场景下,你需要随机访问数据,而不是顺序流式。比如一个查找表,或者一个配置寄存器。
// ap_hs接口示例
void hs_example(
int* input, // ap_hs输入
int* output // ap_hs输出
) {
#pragma HLS INTERFACE ap_hs port=input
#pragma HLS INTERFACE ap_hs port=output
// 这里input和output会被综合成带握手信号的RAM接口
int temp = *input;
*output = temp + 1;
}
嗯,这里要注意:ap_hs的地址是自动生成的。如果你需要手动控制地址,那就得用ap_memory或者bram接口了。
我曾经踩过的坑:ap_hs接口在综合时,如果数据位宽不是8的倍数,HLS会做位宽对齐。比如你定义一个int[3]的数组,实际硬件接口会变成4个int的宽度。这会导致资源浪费。所以,我建议数据位宽尽量对齐到8、16、32、64这些标准宽度。
3.4 axis:AXI-Stream高速接口
终于到了axis。这是AXI协议家族里最轻量、最高效的接口。为什么?因为它去掉了地址,只保留数据流和控制信号。
axis的信号包括:
| 信号 | 宽度 | 说明 |
|---|---|---|
| TDATA | 8/16/32/64/128/... | 数据总线 |
| TVALID | 1 | 发送方数据有效 |
| TREADY | 1 | 接收方准备好 |
| TLAST | 1 | 数据包结束标志 |
| TKEEP | TDATA/8 | 字节使能 |
| TSTRB | TDATA/8 | 字节选通(很少用) |
| TID | 可配置 | 数据流ID |
| TDEST | 可配置 | 目的地标识 |
| TUSER | 可配置 | 用户自定义信号 |
axis最强大的地方在于:它支持背靠背传输,没有地址开销。你想想看,在AXI4-Full里,每次传输都要发地址,地址通道和数据通道是分开的。而axis直接把数据打包成流,一拍一个数据,效率极高。
// axis接口示例
#include <ap_axi_sdata.h>
void axis_example(
hls::stream<ap_axis<32,1,1,1>>& input,
hls::stream<ap_axis<32,1,1,1>>& output
) {
#pragma HLS INTERFACE axis port=input
#pragma HLS INTERFACE axis port=output
ap_axis<32,1,1,1> data_in = input.read();
ap_axis<32,1,1,1> data_out;
data_out.data = data_in.data * 2;
data_out.last = data_in.last; // 传递TLAST信号
data_out.keep = 0xF; // 所有字节有效
output.write(data_out);
}
axis vs ap_fifo 怎么选?
- 如果你的数据是简单的流式,没有包的概念,用ap_fifo就够了
- 如果你需要TLAST标记包边界,或者需要TKEEP做字节使能,那就必须用axis
- axis的硬件开销比ap_fifo大,因为多了TLAST、TKEEP等信号
3.5 三种接口的对比总结
我画了一张图,帮你快速理解这三种接口的关系:
从这张图你可以看到,三种接口从左到右,功能越来越强,但硬件开销也越来越大。我个人建议:能用ap_fifo就别用axis,除非你真的需要TLAST。
3.6 实战中的选择策略
好了,理论讲完了,咱们来点实际的。你在做HLS设计时,到底该怎么选?
- 先看数据流模式:如果是顺序流式,ap_fifo或axis;如果是随机访问,ap_hs或ap_memory。
- 再看是否需要包边界:需要TLAST?那就axis。不需要?ap_fifo更省资源。
- 最后看接口对接:如果你的IP要接到AXI Interconnect上,那axis是必须的。如果只是模块间直连,ap_fifo完全够用。
我的个人经验:在早期的原型验证阶段,我习惯先用ap_fifo把功能调通。等性能优化阶段,再根据瓶颈把关键路径换成axis。这样调试效率高很多。
嗯,关于接口类型,今天就聊这么多。记住一句话:接口类型决定了你的数据通路能跑多快、占多少资源。选对了,事半功倍;选错了,后面改起来很痛苦。
可可精品资料库