第四章: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模块的接口描述。
具体步骤是这样的:
- 在Vitis IDE中,选择File → New → RTL Kernel
- 填写你的模块名、接口信息(输入输出、位宽、方向)
- 选择接口类型(AXI4-Lite、AXI4-Stream、AXI4-Memory Mapped)
- 点击Finish,向导会生成一个完整的Kernel工程
我曾经用这个向导做过一个视频缩放内核。我的Verilog模块有3个Stream接口(输入视频、输出视频、配置参数),向导自动生成了所有AXI接口逻辑,我只用把核心算法填进去就行。嗯,省了不少事。
但要注意,向导生成的代码有时候不是最优的。比如它默认的FIFO深度可能不够,或者握手逻辑过于保守。我建议你把生成的代码当作模板,然后根据实际需求优化。
4.4 知识体系结构图
下面这张图展示了Verilog模块封装为Vitis内核的完整流程和接口关系:
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编译和调试这个内核。