下面我将从不同场景出发,详细讲解如何在 Vivado HLS 中传递参数,并提供代码示例。

核心概念:参数传递的本质
在 HLS 中,参数传递的本质是在 C/C++/SystemC 代码中定义可配置的常量,当您使用 hls::directive 将函数综合为 IP 核时,这些常量可以被“提升”为 IP 核的可配置参数,在生成 IP 后,您可以在 Vivado Block Design 中或通过 Tcl 脚本为这些参数赋值,从而改变 IP 核的行为,而无需重新综合。
顶层接口参数(最常用)
这是最常见的场景,用于配置 IP 核的接口,例如数据位宽、FIFO 深度等,这些参数通常通过 #pragma HLS INTERFACE 指令与顶层函数的输入/输出参数关联。
配置数据位宽
假设您有一个处理像素的函数,您可能希望它既能处理 8-bit 的灰度图,也能处理 24-bit 的彩色图,您可以将数据类型定义为参数。
C++ 代码 (top_function.h):

#ifndef TOP_FUNCTION_H
#define TOP_FUNCTION_H
#include <ap_int.h> // 使用 Xilinx 的任意精度整数类型
// 使用模板来定义数据位宽参数
template <int DATA_WIDTH>
void process_data(
const ap_int<DATA_WIDTH> in_data, // 输入数据
ap_int<DATA_WIDTH> out_data // 输出数据
) {
// HLS 指令:将 in_data 映射为 AXI-Stream 接口的 'tdata' 信号
// 并指定其位宽为 DATA_WIDTH
#pragma HLS INTERFACE axis port=in_data bundle=DATA_BUS
#pragma HLS INTERFACE axis port=out_data bundle=DATA_BUS
// 内部逻辑示例:简单地将数据加1
out_data = in_data + 1;
}
#endif // TOP_FUNCTION_H
如何使用:
在您的顶层测试文件中,您可以实例化这个模板函数,并指定具体的 DATA_WIDTH。
#include "top_function.h"
void top(ap_uint<8> in_data_8, ap_uint<8> &out_data_8,
ap_uint<24> in_data_24, ap_uint<24> &out_data_24) {
// 实例化一个处理 8-bit 数据的版本
process_data<8>(in_data_8, out_data_8);
// 实例化一个处理 24-bit 数据的版本
process_data<24>(in_data_24, out_data_24);
}
综合结果:
Vivado HLS 会为 process_data<8> 和 process_data<24> 分别生成一个 IP 核,这两个 IP 核的 AXI-Stream 接口 tdata 的位宽分别为 8 和 24。
配置 FIFO 深度
当您使用 m_axi 接口访问外部内存(如 DDR)时,您可能需要调整 AXI-Stream 到 M_AXI 的 FIFO 深度以优化性能。
C++ 代码:
#include <ap_axi_sdata.h>
#include <hls_stream.h>
#define DATA_WIDTH 32
void memory_access(hls::stream<ap_axiu<DATA_WIDTH, 0, 0, 0> >& in_stream,
int* out_buffer) {
// ...
ap_axiu<DATA_WIDTH, 0, 0, 0> data;
// 指令:创建一个从 AXI-Stream 到 M_AXI 的 FIFO
// 并将其深度参数化
#pragma HLS STREAM variable=in_stream depth=FIFO_DEPTH
#pragma HLS INTERFACE m_axi port=out_buffer offset=slave bundle=gmem
for (int i = 0; i < 1024; i++) {
in_stream.read(data);
out_buffer[i] = data.data;
}
}
如何传递参数:
您不能直接在 #pragma 中使用变量,标准做法是使用宏定义。
- 在命令行或 Tcl 脚本中定义宏:
# 在 Vivado HLS Tcl 控制台中 set_param hls.directive.m_axi_fifo_depth 32 # 或者更直接的方式是在综合命令中添加 -DFIFO_DEPTH=32
- 在 C++ 代码中:
// 在文件开头 #ifndef FIFO_DEPTH #define FIFO_DEPTH 16 // 默认值 #endif
内部算法参数
有时,参数用于控制算法内部的行为,例如滤波器大小、查找表大小、循环次数等。
使用 #define 宏定义
这是最简单直接的方法。
C++ 代码:
#include <ap_int.h>
// 定义滤波器大小为参数
#define FILTER_SIZE 3
void filter_image(const ap_int<8> input_image[512][512],
ap_int<8> output_image[512][512]) {
// 将 FILTER_SIZE 提升为 HLS 参数,这样它就可以在 IP 核配置时修改
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS PIPELINE II=1
for (int i = 0; i < 512; i++) {
for (int j = 0; j < 512; j++) {
// ... 使用 FILTER_SIZE 进行卷积运算 ...
}
}
}
如何传递参数:
在综合命令中通过 -D 选项覆盖宏定义。
# 在命令行中 vivado_hls -f script.tcl -DFILTER_SIZE=5
或者在 Vivado HLS GUI 的 "Solution Settings" -> "Compile Options" -> "Other Flags" 中添加 -DFILTER_SIZE=5。
使用 hls::param (推荐用于内部参数)
Vivado HLS 提供了 hls::param 类,专门用于创建 HLS 参数,它比宏定义更结构化,并且可以在 HLS 工具中被更好地识别和管理。
C++ 代码:
#include <ap_int.h>
#include <hls/param.h> // 包含 hls::param
// 使用 hls::param 定义一个整数参数
const int hls_param filter_size = 3;
void filter_image_with_param(const ap_int<8> input_image[512][512],
ap_int<8> output_image[512][512]) {
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS PIPELINE II=1
// hls::param 对象可以直接在循环边界等地方使用
for (int i = 0; i < 512; i++) {
for (int j = 0; j < 512; j++) {
// ...
}
}
}
如何传递参数:
hls::param 的参数值通常通过 Tcl 脚本进行配置,在综合脚本中,您可以使用 set_param 命令。
Tcl 脚本 (script.tcl):
# 打开项目
open_project -force my_project
set_top filter_image_with_param
# 添加源文件
add_files filter_image.cpp
# 创建解决方案
create_solution -flow_target vivado
set_part {xc7z020-clg400-1} # 根据你的FPGA选择
create_clock -period 10
# --- 关键部分:设置参数 ---
# 设置名为 "filter_size" 的 hls::param 参数的值为 5
set_param hls.param.filter_size 5
# 运行综合
csim_design -clean
csynth_design
export_design -format ip_catalog -output ./my_ip
close_project
运行 vivado_hls -f script.tcl 后,生成的 IP 核将使用 filter_size = 5 进行综合。
通过 Vivado GUI 传递参数
对于宏定义参数,您可以直接在 Vivado HLS GUI 中配置,非常方便。
- 打开项目并添加源文件:像往常一样创建项目并添加您的 C++ 文件。
- 转到 "Solution Settings":在左侧导航栏中,选择您的解决方案,然后点击 "Solution Settings"。
- 修改 "Compile Options":
- 在 "Compile Options" 标签页下,找到 "Other Flags" 字段。
- 在这里添加您的宏定义,
-DFILTER_SIZE=5或-DDATA_WIDTH=64。
- 运行综合:点击 "Run C Synthesis",HLS 将会使用您定义的参数值进行综合。
最佳实践与总结
-
区分参数类型:
- 接口参数:使用
#pragma HLS INTERFACE和模板(如<int DATA_WIDTH>)。 - 内部算法参数:优先使用
hls::param,因为它更结构化;对于简单场景,可以使用#define。
- 接口参数:使用
-
命名清晰:为您的参数使用有意义的名称,如
AXI_DATA_WIDTH,MEM_DEPTH,FILTER_KERNEL_SIZE。 -
提供默认值:在 C++ 代码中,为您的参数(特别是
hls::param和#define)提供合理的默认值,这样即使不通过命令行或脚本传参,代码也能正确编译和综合。 -
文档化:在您的代码注释中说明每个参数的用途和有效范围,方便其他开发者(和未来的您)理解。
-
Tcl 脚本自动化:对于需要频繁更改参数并快速迭代验证的场景,编写 Tcl 脚本是最高效的方式,它可以实现一键配置、综合和导出。
通过灵活运用这些参数传递技术,您可以创建出高度可配置、可复用的 IP 核,从而极大地加速您的 FPGA 硬件开发流程。
