4、AXI4-Stream接口实战:HLS中实现AXIS数据流的输入输出,TLAST、TKEEP信号处理

AXI4-Stream,简称AXIS,是AXI家族里最轻量级的协议。它没有地址线,没有写数据通道和读数据通道的区分,说白了就是一条单向的数据管道。我在做视频处理加速器的时候,几乎天天跟它打交道。今天我们就来聊聊,在HLS里怎么用好AXIS接口,尤其是TLAST和TKEEP这两个信号。

4.1 为什么需要AXIS?

你想想看,在数据流处理场景下,比如图像滤波、FFT、FIR滤波器,数据是一拍一拍连续进来的。如果用AXI Full或者AXI Lite,每次传输都要带地址,效率太低了。AXIS就干一件事:主设备发数据,从设备收数据,中间没有任何握手之外的额外开销。

我个人习惯把AXIS比作「传送带」。数据包就像箱子,一个接一个往前送。TVALID和TREADY就是传送带的启停开关。TLAST告诉你这是最后一个箱子。TKEEP则告诉你箱子里哪些格子是有效的。

核心要点: AXIS只关心数据流,不关心数据存在哪里。它是最适合HLS高层次综合的接口类型之一。

4.2 HLS中的AXIS接口声明

在Vivado HLS中,声明一个AXIS接口非常简单。用hls::stream模板类,配合#pragma HLS INTERFACE指令就行。

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

// 定义一个32位数据 + 1位TLAST + 1位TKEEP的AXIS结构
typedef ap_axis<32, 1, 1, 1> axis_pkt;

void axis_example(hls::stream<axis_pkt> &in_stream,
                  hls::stream<axis_pkt> &out_stream) {
#pragma HLS INTERFACE axis port=in_stream
#pragma HLS INTERFACE axis port=out_stream
#pragma HLS INTERFACE s_axilite port=return

    axis_pkt tmp;
    // 从输入流读数据
    in_stream.read(tmp);
    // 处理数据(这里只是简单透传)
    tmp.data = tmp.data + 1;
    // 写到输出流
    out_stream.write(tmp);
}

这里要注意,ap_axis模板的参数依次是:数据位宽、TUSER位宽、TID位宽、TDEST位宽。我一般只用前两个参数,后面两个设成1就够了,除非你有特殊的路由需求。

4.3 TLAST信号:包尾标记

TLAST是AXIS里最容易出问题的地方。它的作用就是告诉接收端:「哥们,这个数据是包的最后一个了,后面没了。」

我在项目中遇到过这样一个坑:一个视频行缓冲模块,每行有1920个像素。我忘了在最后一个像素置位TLAST,结果下游的DMA控制器一直等下一拍数据,整个系统就卡死了。从那以后,我每次写AXIS逻辑,第一件事就是检查TLAST有没有在正确的位置拉高。

避坑指南: 我曾经因为TLAST没置位,导致仿真通过但上板死锁。记住:TLAST必须与最后一个有效数据对齐,不能早也不能晚。

在HLS中处理TLAST,通常的做法是用一个计数器。比如处理一个长度为N的包:

void packet_process(hls::stream<axis_pkt> &in, hls::stream<axis_pkt> &out,
                    int packet_len) {
#pragma HLS INTERFACE axis port=in
#pragma HLS INTERFACE axis port=out
#pragma HLS INTERFACE s_axilite port=packet_len
#pragma HLS INTERFACE s_axilite port=return

    axis_pkt pkt;
    for (int i = 0; i < packet_len; i++) {
#pragma HLS PIPELINE II=1
        in.read(pkt);
        // 处理数据...
        pkt.data = pkt.data * 2;
        // 判断是否是最后一个数据
        if (i == packet_len - 1) {
            pkt.last = 1;  // 置位TLAST
        } else {
            pkt.last = 0;
        }
        out.write(pkt);
    }
}

嗯,这里要注意:pkt.last就是TLAST信号。在ap_axis结构体里,它被封装成了一个成员变量。你直接赋值就行,HLS综合工具会自动把它映射到硬件TLAST线上。

4.4 TKEEP信号:字节使能

TKEEP这个信号,说白了就是「字节掩码」。它告诉接收端,数据总线上的哪些字节是有效的。比如你的数据总线是64位(8字节),但最后一个数据可能只有3个字节有效。这时候TKEEP就是8'b00000111,表示低3个字节有效。

我刚开始做AXIS设计时,总觉得TKEEP可有可无。直到有一次做UDP包处理,包尾长度不固定,我才发现TKEEP的重要性。没有TKEEP,你没法告诉下游哪些字节是真正的数据,哪些是填充的垃圾。

TKEEP位宽 数据总线位宽 每个TKEEP位对应的字节
4位 32位 每1位对应1个字节
8位 64位 每1位对应1个字节
16位 128位 每1位对应1个字节

在HLS中处理TKEEP,代码也很直接:

void process_with_tkeep(hls::stream<axis_pkt> &in, hls::stream<axis_pkt> &out) {
#pragma HLS INTERFACE axis port=in
#pragma HLS INTERFACE axis port=out
#pragma HLS INTERFACE s_axilite port=return

    axis_pkt pkt;
    in.read(pkt);

    // 假设我们只保留低16位数据,高16位清零
    // 同时TKEEP只使能低2个字节
    pkt.data = pkt.data & 0x0000FFFF;
    pkt.keep = 0x03;  // 二进制 0011,表示低2字节有效

    out.write(pkt);
}
小技巧: 如果你确定数据总是对齐的(比如总是32位对齐),可以把TKEEP设成全1。但如果你做的是包处理、串口通信这类非对齐数据,TKEEP就必须认真处理。

4.5 TLAST与TKEEP的协同工作

这两个信号经常一起出现。典型的场景是:一个数据包的最后一次传输,数据可能不满一整拍。这时候TLAST拉高,TKEEP指示哪些字节是有效的。

举个例子,一个UDP包长63字节,数据总线是64位(8字节)。前7拍每拍8字节,共56字节。最后一拍只有7字节。那么最后一拍的TLAST=1,TKEEP=8'b01111111(或者0x7F)。

我在做网络加速器时,就专门写了一个模块来处理这种「包尾对齐」的逻辑。核心思想就是:

  1. 计算剩余字节数
  2. 根据剩余字节数生成TKEEP掩码
  3. 在最后一拍同时置位TLAST和正确的TKEEP
// 生成包尾TKEEP的示例代码
ap_uint<8> gen_tkeep(int remaining_bytes) {
    ap_uint<8> tkeep = 0;
    for (int i = 0; i < remaining_bytes; i++) {
#pragma HLS UNROLL
        tkeep[i] = 1;
    }
    return tkeep;
}

4.6 实战中的常见问题

我总结几个实际项目中容易踩的坑:

  • TLAST没对齐: TLAST必须和最后一个有效数据的TVALID同时有效。如果TLAST早一拍或晚一拍,下游模块就会乱掉。
  • TKEEP全1的惯性思维: 很多人习惯把TKEEP设成全1,结果遇到非对齐数据就出问题。记住:TKEEP不是可选项,是必须认真对待的信号。
  • 忽略TREADY反压: AXIS的握手是双向的。你的模块必须能处理TREADY拉低的情况。在HLS里,hls::stream的read()和write()会自动处理反压,但如果你用了read_nb()(非阻塞读),就要自己判断返回值。
重要提醒: 我曾经在HLS里用read_nb()写了一个数据分发器,结果因为没检查返回值,导致数据丢失。仿真时数据量小没发现,上板跑大数据量就出错了。所以,除非你非常清楚自己在做什么,否则尽量用阻塞式的read()和write()。

4.7 本章知识体系

下面这张图展示了AXIS接口在HLS中的核心知识结构,从接口声明到TLAST/TKEEP处理,再到实战注意事项:

AXI4-Stream HLS实战知识体系 AXIS接口实战 接口声明 TLAST包尾标记 TKEEP字节使能 hls::stream + ap_axis #pragma HLS INTERFACE axis 计数器判断包尾 pkt.last = 1 置位 与TVALID对齐 字节掩码生成 非对齐数据处理 包尾协同工作 避坑:TLAST对齐 | TKEEP非全1 | 阻塞读写

好了,关于AXIS接口的TLAST和TKEEP处理,今天就聊到这里。这两个信号看似简单,但用不好真的会出大问题。我建议你在做设计时,先画一个时序图,把TLAST、TKEEP、TVALID、TREADY的波形画清楚,再动手写代码。这样能避免很多后期调试的麻烦。

可可精品资料库