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 三种接口的对比总结

我画了一张图,帮你快速理解这三种接口的关系:

HLS接口类型对比 ap_fifo 数据 + valid/ready 无地址,无包边界 硬件:FIFO 适合:流式数据 开销:低 ap_hs 地址 + 数据 + 握手 支持随机访问 硬件:RAM接口 适合:控制/配置 开销:中 axis 数据 + TLAST/TKEEP 支持包边界 硬件:Stream 适合:高速数据通路 开销:高 复杂度与功能递增 →

从这张图你可以看到,三种接口从左到右,功能越来越强,但硬件开销也越来越大。我个人建议:能用ap_fifo就别用axis,除非你真的需要TLAST。

3.6 实战中的选择策略

好了,理论讲完了,咱们来点实际的。你在做HLS设计时,到底该怎么选?

  1. 先看数据流模式:如果是顺序流式,ap_fifo或axis;如果是随机访问,ap_hs或ap_memory。
  2. 再看是否需要包边界:需要TLAST?那就axis。不需要?ap_fifo更省资源。
  3. 最后看接口对接:如果你的IP要接到AXI Interconnect上,那axis是必须的。如果只是模块间直连,ap_fifo完全够用。

我的个人经验:在早期的原型验证阶段,我习惯先用ap_fifo把功能调通。等性能优化阶段,再根据瓶颈把关键路径换成axis。这样调试效率高很多。

嗯,关于接口类型,今天就聊这么多。记住一句话:接口类型决定了你的数据通路能跑多快、占多少资源。选对了,事半功倍;选错了,后面改起来很痛苦。

可可精品资料库