第四章:Verilog模块封装为Vitis内核

好,咱们今天聊点硬核的。前面几章我们把Vitis的开发环境搭好了,也跑通了第一个Hello World。但说实话,那个Hello World用的是纯C/C++写的,跟咱们做FPGA的Verilog好像还隔着一层纱。这一章,我就带你把这层纱捅破——把Verilog模块封装成Vitis内核。

为什么要封装?说白了,就是让软件工程师也能调用你的硬件加速器。你写了一个牛逼的FFT模块,怎么让CPU调用它?通过Vitis内核。你写了一个图像处理流水线,怎么集成到Xilinx的平台上?还是通过Vitis内核。所以,这个技能是软硬件协同开发的基石。

核心概念:Vitis内核本质上是一个被封装好的硬件加速单元,它通过标准接口(AXI)与系统其他部分通信。你的Verilog模块只要套上这个“壳”,就能被Vitis编译器识别、调度。

4.1 理解接口协议:AXI4-Stream, AXI4-Lite, AXI4-Memory Mapped

在开始封装之前,咱们得先搞清楚Vitis内核用到的三种接口协议。我当年刚接触时也被这些名字搞晕过,其实没那么复杂。

4.1.1 AXI4-Stream

这个接口最简单,就是数据流。没有地址,没有握手信号?不对,有握手。它只有三个核心信号:tvalid、tready、tdata。发送方拉高tvalid表示数据有效,接收方拉高tready表示可以接收。两者都高时,一个数据就传输完成了。

我在项目中遇到过一个问题:两个模块之间用AXI4-Stream通信,结果数据老是丢包。查了半天,发现是tready信号处理不对。接收方在内部FIFO快满时提前拉低了tready,但发送方没有及时响应。嗯,这里要注意:tready的拉低必须保证当前传输完成后才生效,不能半路截断。

个人经验:AXI4-Stream适合做流水线处理。比如视频数据一行一行地流进来,你做一个边缘检测,处理完再流出去。这种场景下,Stream接口比Memory Mapped高效得多,因为它不需要地址开销。

4.1.2 AXI4-Lite

这个接口是轻量级的Memory Mapped接口。它支持单个数据的读写,适合配置寄存器。比如你想让硬件加速器换个参数,就用AXI4-Lite写一个寄存器。

AXI4-Lite的信号比Stream多:有地址、写数据、写选通、写响应、读数据、读响应等等。但别怕,Vitis Kernel Wrapper会帮你生成大部分逻辑,你只需要关注用户逻辑部分。

你想想看,为什么不用全功能的AXI4?因为AXI4-Lite面积小、时序好。配置寄存器这种低频操作,用AXI4-Lite完全够用。我见过有人非要用AXI4-Full去配寄存器,结果把时序搞得一塌糊涂。

4.1.3 AXI4-Memory Mapped(AXI4-Full)

这个接口是完整版的Memory Mapped,支持突发传输。如果你的内核需要从DDR里搬一大块数据进来处理,处理完再写回去,那就得用这个接口。

AXI4-Full的信号最多,但功能也最强。它支持多个outstanding传输,支持乱序返回。不过说实话,在Vitis内核里,我们通常不直接操作这些信号——Vitis Kernel Wrapper会帮你封装好,你只需要关心数据地址和长度就行。

接口类型 适用场景 复杂度 带宽
AXI4-Stream 数据流处理(视频、音频、网络) 低 高
AXI4-Lite 寄存器配置、状态查询 中 低
AXI4-Memory Mapped 大数据块读写(DDR访问) 高 极高

4.2 Vitis Kernel Wrapper:给你的Verilog模块穿件“外套”

好,现在咱们来实战。假设你有一个Verilog模块,功能很简单:输入两个32位数,输出它们的和。咱们把它封装成Vitis内核。

首先,你得写一个顶层Wrapper。这个Wrapper的作用就是把你的Verilog模块和AXI接口连接起来。Vitis提供了一个模板,但我建议你手写一遍,这样才能理解里面的门道。

// 这是你的原始Verilog模块
module adder (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [31:0] a,
    input  wire [31:0] b,
    output reg  [31:0] sum
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            sum <= 32'd0;
        else
            sum <= a + b;
    end
endmodule

然后,你需要写一个Kernel Wrapper。这个Wrapper会包含AXI4-Lite接口,用来接收a和b的值,并输出sum。

// Vitis Kernel Wrapper
module adder_kernel (
    // AXI4-Lite 从接口
    input  wire        s_axi_control_aclk,
    input  wire        s_axi_control_aresetn,
    input  wire [5:0]  s_axi_control_awaddr,
    input  wire        s_axi_control_awvalid,
    output wire        s_axi_control_awready,
    input  wire [31:0] s_axi_control_wdata,
    input  wire [3:0]  s_axi_control_wstrb,
    input  wire        s_axi_control_wvalid,
    output wire        s_axi_control_wready,
    output wire [1:0]  s_axi_control_bresp,
    output wire        s_axi_control_bvalid,
    input  wire        s_axi_control_bready,
    input  wire [5:0]  s_axi_control_araddr,
    input  wire        s_axi_control_arvalid,
    output wire        s_axi_control_arready,
    output wire [31:0] s_axi_control_rdata,
    output wire [1:0]  s_axi_control_rresp,
    output wire        s_axi_control_rvalid,
    input  wire        s_axi_control_rready
);

    // 内部寄存器
    reg [31:0] reg_a, reg_b, reg_sum;
    reg [31:0] reg_control;  // bit0: ap_start, bit1: ap_done

    // 这里省略了AXI4-Lite的地址译码逻辑
    // 实际项目中,你可以用Xilinx提供的axi_lite_ipif或者手写

    // 实例化你的Verilog模块
    adder u_adder (
        .clk   (s_axi_control_aclk),
        .rst_n (s_axi_control_aresetn),
        .a     (reg_a),
        .b     (reg_b),
        .sum   (reg_sum)
    );

    // 控制逻辑:当ap_start拉高时,开始计算
    always @(posedge s_axi_control_aclk) begin
        if (!s_axi_control_aresetn) begin
            reg_control <= 32'd0;
        end else begin
            // 软件写0x00地址的bit0来启动
            // 计算完成后,硬件置位bit1表示完成
        end
    end

endmodule

注意:上面的代码是简化版,实际项目中你需要处理AXI4-Lite的地址译码、握手信号、以及ap_start/ap_done/ap_idle/ap_ready这些控制信号。Vitis内核的启动协议是固定的:软件写ap_start=1,硬件开始工作,完成后拉高ap_done,软件读到ap_done=1就知道计算完成了。

4.3 RTL内核向导使用

手写Wrapper太麻烦?别急,Vitis提供了RTL内核向导。这个工具可以自动生成Wrapper代码,你只需要提供你的Verilog模块的接口描述。

具体步骤是这样的:

  1. 在Vitis IDE中,选择File → New → RTL Kernel
  2. 填写你的模块名、接口信息(输入输出、位宽、方向)
  3. 选择接口类型(AXI4-Lite、AXI4-Stream、AXI4-Memory Mapped)
  4. 点击Finish,向导会生成一个完整的Kernel工程

我曾经用这个向导做过一个视频缩放内核。我的Verilog模块有3个Stream接口(输入视频、输出视频、配置参数),向导自动生成了所有AXI接口逻辑,我只用把核心算法填进去就行。嗯,省了不少事。

但要注意,向导生成的代码有时候不是最优的。比如它默认的FIFO深度可能不够,或者握手逻辑过于保守。我建议你把生成的代码当作模板,然后根据实际需求优化。

4.4 知识体系结构图

下面这张图展示了Verilog模块封装为Vitis内核的完整流程和接口关系:

Verilog模块封装为Vitis内核 - 知识体系 你的Verilog模块 (核心算法逻辑) 输入/输出端口 时钟/复位 实例化 Vitis Kernel Wrapper AXI接口逻辑 控制寄存器(ap_start/done) 地址译码 握手信号处理 数据通路 AXI4-Lite AXI4-Stream AXI4-Memory Mapped RTL内核向导 自动生成Wrapper代码 Vitis编译器 编译链接生成.xclbin 封装流程:Verilog模块 → 添加Wrapper → 选择接口类型 → 编译生成内核 三种接口可组合使用,例如:AXI4-Lite配参数 + AXI4-Stream传数据

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 时钟域问题:你的Verilog模块和AXI接口可能不在同一个时钟域。我曾经直接把AXI时钟接到模块上,结果时序跑不过。后来加了异步FIFO才解决。
  • 复位极性:Vitis默认使用高电平复位,但很多FPGA工程师习惯低电平复位。记得检查你的模块复位极性是否匹配。
  • ap_start/ap_done协议:这个协议是Vitis内核的命根子。软件通过写ap_start启动硬件,硬件通过拉高ap_done通知软件。如果这个协议没实现好,你的内核就永远跑不起来。
  • 接口位宽对齐:AXI4-Lite的数据位宽是32位,如果你的内部寄存器是64位,需要做位宽转换。我见过有人直接把64位寄存器映射到32位总线上,结果数据被截断了。

我的建议:第一次做封装时,先用最简单的加法器练手。把整个流程跑通——从Verilog模块到Wrapper,再到Vitis编译,最后用host程序调用。这个流程通了,后面再复杂的模块也只是接口数量的增加。

好了,这一章的内容就到这里。记住,封装的核心就是给你的Verilog模块穿上一件AXI接口的外套。这件外套好不好看不要紧,关键是合身——接口协议要对、时序要过、控制逻辑要完整。下一章,我们会深入讲解如何用Vitis编译和调试这个内核。

可可精品资料库