Vitis HLS入门到进阶:用C/C++描述硬件电路,从FIR滤波器优化看懂流水线与数组分区

发布时间:2026/10/7 5:24:51
Vitis HLS入门到进阶:用C/C++描述硬件电路,从FIR滤波器优化看懂流水线与数组分区 如果你做过 FPGA 开发大概率听说过Vitis HLS这个名字。它最核心的作用是让你用 C/C 描述算法再自动综合成 RTL 电路省掉一部分手写 Verilog 的重复劳动。我在刚接触时也抱着怀疑态度C 这种顺序语言怎么描述硬件后来踩了不少坑才理解它并不是把 C“翻译”成 Verilog而是用 C 的语义去描述并行电路的行为。这篇文章会从工具认知、环境搭建、接口与存储的底层逻辑、一个 FIR 滤波器实例的优化演进一直讲到调试技巧和进阶方向。适合两类人看一类是有 C/C 基础、想往 FPGA 加速方向转的软件工程师另一类是写过 Verilog、想提升算法原型验证效率的硬件工程师。看完不敢说让你立刻成为 HLS 高手但至少能帮你少走一半弯路。1. Vitis HLS 入门先搞懂它到底在做什么1.1 它不是一个“自动翻译机”而是一套“硬件建模语言”很多人刚开始学 Vitis HLS都会拿一个 C 函数去综合然后期待工具把它变成和手写 Verilog 一样漂亮的 RTL。实际结果通常是能跑但资源、时序、吞吐都一般。这不是工具不行而是使用姿势不对。Vitis HLS 里的 C/C 不是软件语言而是一种硬件描述语言。同样一个for循环在软件里是迭代执行在 HLS 综合后可以变成完全展开的并行电路也可以变成共享资源的流水线结构。for循环的次数、迭代间隔IIIteration Interval、数据依赖都会决定最终电路长什么样。从这个角度看写 HLS 更像是在“用 C 的语法做硬件架构设计”而不是在“写软件逻辑”。理解这一点后很多表象就说得通了为什么随便写个三重循环综合后 latency 高得离谱为什么加了一个#pragma HLS pipeline后资源暴涨为什么数组不加 partition综合器偏偏把它放进了 BRAM 而不是寄存器这些问题背后都是“硬件电路”在起作用不是 C 语义里“变量存到内存里”的直觉。学 HLS 的第一步是忘掉“函数调用”与“内存读写”的软件习惯重新建立“模块端口”“寄存器数组”“读写时序”的硬件观。最有效的练习方式就是先从单函数、单循环、小数组开始反复读综合报告里的 Schedule View 和资源利用率看工具把你的代码映射成了什么结构。看多了就会慢慢形成“这段代码会综合成什么电路”的直觉。1.2 哪些人适合学 Vitis HLS哪些场景不要硬用Vitis HLS 适合的场景比较集中算法验证迭代快、数据处理流程清晰、并行度可以在数组和循环级别体现。典型应用包括图像预处理、信号滤波、通信编解码、AI 算子加速等这些场景往往是“数据流 计算密集”非常适合用高层综合做快速实现。不适合的场景也很明确极致的时序调优、非常规的硬件接口时序、复杂的多时钟域交互、需要手工控制布线位置的设计这些仍然是 RTL 的强项。HLS 不是来取代 Verilog 的它更像是在“算法探索”和“RTL 实现”之间加了一个高效中间层。你可以先用 C/C 快速验证算法再让 HLS 生成初版 RTL最后针对瓶颈模块手工优化或直接手写替换这种组合拳在实际项目中非常实用。还有一个常见误区是拿 HLS 写通用处理器软件逻辑比如链表、动态内存分配、递归调用。这些东西在 HLS 里要么效率极低要么根本不可综合。所以判断标准很简单你的数据是不是规则的、流式的、计算密集的如果是HLS 会很好用如果不是建议继续用 RTL 或处理器方案。2. 环境准备与第一个可综合工程2.1 工具版本和安装方案怎么选Vitis HLS 的发展有个分水岭2020.1 版本之前叫 Vivado HLS之后统一改称 Vitis HLS并整合进 Vitis 统一工具套件里。现在网上的教程一大半是早几年的看到vivado_hls命令不要慌我们现在通常在 Vitis 环境下用vitis_hls启动用法基本一致但工程文件格式和部分报告界面有变化。安装时建议直接下载 Vitis 一体化安装包里面会带 Vivado 和 Vitis HLS。需要注意几个实操要点第一安装路径不要有中文和空格否则后面跑 C/RTL 联合仿真时xsim 那套工具经常莫名找不到库第二如果机器配置一般只勾选需要的器件支持就可以没必要全装第三Windows 下能跑但我强烈建议在 Linux 环境下做综合和仿真。实测同一个工程Linux 下的综合速度快不少xsim 的稳定性也明显更好。实验室机器上 Linux个人电脑 Windows 学习的话就多存盘多容忍一些莫名奇妙的报错。Vitis HLS 的 license 跟着套装走一般你下载了 Vitis 或 Vivado HLx 版本就能正常使用。如果你只是学习用官方免费的 WebPACK 版本也能创建工程、跑 C 仿真和部分综合只是器件范围受限。入门期完全够用。2.2 新建工程、编写第一个 C 函数并跑通 C 仿真纸上谈兵半天还是要实际建一个工程。我用一个最简单的 8 阶 FIR 滤波器做例子既可以跑通流程后面优化章节又能直接用同一份代码对比效果。先建一个目录比如fir_hls_demo里面放三个文件fir.h、fir.cpp、fir_tb.cpp。// fir.h #ifndef FIR_H #define FIR_H #define FIR_LEN 8 typedef int data_t; typedef int acc_t; void fir(data_t *y, data_t x); #endif// fir.cpp #include fir.h static const data_t coeff[FIR_LEN] {1, 2, 3, 4, 4, 3, 2, 1}; void fir(data_t *y, data_t x) { static data_t shift_reg[FIR_LEN]; acc_t acc 0; for (int i FIR_LEN - 1; i 0; i--) { shift_reg[i] shift_reg[i - 1]; } shift_reg[0] x; for (int j 0; j FIR_LEN; j) { acc shift_reg[j] * coeff[j]; } *y acc; }// fir_tb.cpp #include fir.h #include stdio.h int main() { data_t out; data_t input[8] {0, 1, 2, 3, 4, 5, 6, 7}; for (int i 0; i 8; i) { fir(out, input[i]); printf(out[%d] %d\n, i, out); } return 0; }打开 Vitis HLS新建工程把fir.cpp设为设计源文件fir_tb.cpp设为测试平台顶层函数选择fir。先把时钟周期设成 10ns器件可以先随便选一个比如 xc7z020后面综合时再调。点 C Simulation能看到 8 次调用的输出结果这个阶段 C 仿真的本质就是用本机编译器跑一遍 C 代码目的是验证算法逻辑本身不涉及任何硬件时序。跑通 C 仿真后再点 C Synthesis综合完成后会生成报告。这会儿还没做任何优化报告里的 Latency 通常不会太好但这就是我们的起点。先别急着改代码先看看界面里的 Schedule Viewer 和资源利用率对“同一段 C 代码在硬件上长什么样”有个初步印象。后面的优化都是从这个版本出发逐步展开的。3. 接口与存储的底层逻辑从 pragma 看硬件3.1 接口综合函数参数如何变成 AXI 协议很多初学者第一次被 HLS 劝退都是卡在 interface pragma 上。其实接口综合没那么玄它要解决的是一个问题C 函数在做 RTL 模块时参数对应的输入输出端口应该遵循什么握手协议和总线协议。先看标量参数。顶层函数的data_t x默认情况下可能被综合成一组数据线外加 valid/ack 握手信号也可能是最简单的一根数据线加一个使能。在没有显式约束时工具会按默认策略选一种默认策略在不同版本间还有过调整。所以我的建议是只要是顶层函数就显式写 interface pragma别赌工具默认行为。下面这张表是几种常用接口模式的选择参考接口模式典型使用场景大致信号形态注意事项ap_none / ap_ovld / ap_vld标量输入输出单拍数据交换数据线、valid、ack握手开销小适合低频配置类端口ap_fifo流式数据读写配合 hls::stream 使用din/dout、empty/full天然匹配数据流几乎不用关心握手细节ap_memory数组参数映射到片上 BRAMaddress、ce、we、d、q适合小数据量内部存储不跨片外传输s_axilite寄存器配置通道常用于 SoC 控制AXI-Lite 总线由 ARM 端读写配置延迟大但易用m_axi大块数据搬移访问片外 DDRAXI Master 总线支持突发传输配套 hls::stream 才能高效搬运我一般在原型验证阶段如果数据量小就直接用 ap_memory 把数组放到 BRAM如果算法是流水式处理连续数据流就用 ap_fifo 搭配hls::stream如果设计最终要挂到 Zynq 的 ARM 上就用 s_axilite 做控制口用 m_axi 做大块数据通道。这几个模式在不同场景下几乎覆盖了 80% 的日常需求。回到 FIR 这个例子后面做优化时我会给x和y显式加接口 pragma避免综合器自动生成的端口形式和自己预期不一致。一个实用的经验是C 仿真通过不代表综合后的 RTL 接口就是你想要的看综合报告里的 Interface 摘要逐项确认每个端口的方向、协议、位宽这才是靠谱的检查方式。3.2 数组和存储资源BRAM、寄存器、分块策略C 语言里的数组到硬件里最常见的映射是 BRAM。BRAM 的好处是密度高、容量大缺点是端口数量有限。一个双端口 BRAM 每个周期最多支持两次读或写访问。这个限制一旦遇到并行读多个数据的情况就会成为性能瓶颈。Vitis HLS 提供了#pragma HLS array_partition来改变数组的存储布局。它的本质是把一个 BRAM 数组拆成多个物理存储块让一次可以并行访问更多数据。分区方式有三种cyclic 是轮流分配block 是连续切块complete 是彻底展开成寄存器数组。我自己的使用经验是小数组比如 FIR 的 shift_reg长度低于 16 或 32直接 complete一口气展开成寄存器读写完全并行中等数组看访问模式选 cyclic 或 block大数组不要轻易 complete否则资源会爆炸BRAM 变成一堆 LUT/FF成本非常高。还有一个很容易忽略的问题是数组在函数参数里的处理。顶层函数的数组参数综合器默认会生成一组 memory 接口信号访问时序是“地址-读数据”两拍甚至更晚。这意味着如果你在 C 代码里连续循环读取数组综合器要按 BRAM 的时序安排读写受端口数量和读延迟影响循环的 II 很难做到 1。解决思路通常是两条一是对数组做 partition增加并行访问能力二是换成hls::stream流式接口把“内存读取”转换为“流式消费”配合 dataflow 可以把整个计算的流水线彻底打通。这里补充一个判断小技巧当你在综合报告里看到某个循环的 II 远大于 1或者有 Bank Conflict 警告时优先检查这个循环访问了哪些数组这些数组有没有被 partition访问顺序是否和存储分布冲突。大多数性能问题最终都能追溯到“存储端口争用”而不是“计算资源不够”。4. 性能优化一个 FIR 滤波器的三版演进4.1 第一版先跑通再看报告回到 FIR 的例子。第一版代码不加任何性能优化直接综合。在 10ns 时钟、默认器件下我本地实测的一个结果是 Latency 大约在 40 拍左右主要时间花在两个循环上一个是 8 次的移位循环一个是 8 次的乘加循环。如果不展开这两个循环是串行执行的每轮迭代还要等上一次写回完成整体自然慢。看报告时别只看 Latency还要注意 Interval。FIR 这种结构本质上每个采样点调用一次如果是流式处理我们真正关心的是两次调用之间最短能隔多少拍也就是 Interval。Latency 表示“单次调用的总耗时”Interval 表示“能多快开始下一次调用”。在很多流水式的 HLS 设计里Interval 比 Latency 更重要。第一版综合完可以先在资源报告里看看用了几个 BRAM、几个 DSP。一般这个阶段资源都很少因为工具默认会共享乘法器移位循环也慢吞吞地复用同一个存储端口。这说明什么说明代码还有大量优化空间瓶颈不在资源而在“没有并行起来”。4.2 第二版循环流水线与数组分块第二版优化目标是把乘加循环的流水线拉起来。在fir.cpp的乘加循环体内加上// fir.cpp 第二版只展示核心改动 void fir(data_t *y, data_t x) { static data_t shift_reg[FIR_LEN]; acc_t acc 0; for (int i FIR_LEN - 1; i 0; i--) { shift_reg[i] shift_reg[i - 1]; } shift_reg[0] x; for (int j 0; j FIR_LEN; j) { #pragma HLS pipeline II 1 acc shift_reg[j] * coeff[j]; } *y acc; }#pragma HLS pipeline II1的意思是让这个循环的每一次迭代间隔 1 拍就启动一次新的计算。理想情况下8 次迭代只需要“预填充几拍 8 拍”就能完成整个循环。但这里有个隐藏问题shift_reg[j]存放在静态数组里如果综合器把它放进了 BRAM读端口数量有限流水线可能没法在一个周期内把需要的数据都取出来。而且之前的移位循环也在同一个周期写shift_reg读写端口的竞争会让 II 达不到 1。所以第二版还需要对shift_reg做数组分块把它从 BRAM 拆散成多个小存储块甚至直接展开。我是在函数体头部加了一段static data_t shift_reg[FIR_LEN]; #pragma HLS array_partition variable shift_reg complete加上之后shift_reg的每个元素会变成独立的寄存器移位循环和乘加循环的读写访问都能并行存储端口的瓶颈就解除了。这一版综合后我刚才那个例子里 Latency 降到了大约 20 拍以内整体改善非常明显。做这一步时要注意array_partition complete只适合小数组。如果哪天你把一个 1024 深度的数组也 complete 了资源报表会教你做人。一般超过 32 长度的数组优先考虑 cyclic 或 block 方式。另外pragma 的位置要写对数组定义之后、使用之前作用域内才有效。4.3 第三版循环展开用空间换时间第二版已经把流水线跑起来了但乘加循环本身还是串行累加。8 次乘法虽然可以流水但每次累加都依赖上一次的结果这是一个典型的循环携带依赖loop-carried dependency它决定了整个循环的最终延迟至少是乘法链加上加法链的深度。第三版的思路是把循环直接展开让 8 个乘法并行执行再用加法树把结果合起来。在乘加循环里加一个 unroll pragmafor (int j 0; j FIR_LEN; j) { #pragma HLS pipeline II 1 #pragma HLS unroll factor 8 acc shift_reg[j] * coeff[j]; }当循环次数是常数 8且 factor 设成 8 时这个循环会被完全展开。综合器会生成 8 路并行的乘法器再把结果通过加法树相加。付出的代价是 DSP 或乘法器资源明显增加。在这次示例里Latency 进一步降到了 10 拍以内。这里要给个务实提醒循环展开后资源涨了不代表性能一定翻倍。如果加法链太长综合器会自动做加法树优化但位宽越大、DSP 级联越深时序可能变差。实际项目中我通常不会一上来就 unroll 一个很大的循环而是先看这个循环是不是瓶颈再决定展开多少。比如循环 64 次可以先试factor16看性能和资源的平衡点在哪里。展开因子太大DSP 占用高布线压力大综合时间也会明显变长。经过这三版演进FIR 这一个简单函数的 Latency从我实测的三四十拍降到了个位数Interval 也大幅缩短。这个案例很直观地展示出 HLS 优化的两条主线一个是“让计算流水起来”对应 pipeline一个是“让数据取用并行起来”对应 partition 和 unroll。这两个方向几乎适用于所有以循环为核心的 HLS 设计。5. 调试与常见问题排查实录5.1 仿真通过、综合报错多半是接口问题我在实际项目里最常遇到的情况是C 仿真完全正常一到 C Synthesis 或者 C/RTL 联合仿真就出问题。排查一圈后十次里有七八次都是顶层接口定义不清晰造成的。举个例子如果你把fir的y这个指针参数当成普通变量直接传进去综合器可能会自动生成一组 memory 接口包含地址线、数据线、读写使能。可是你的 testbench 还是按 C 函数调用的方式传入out联合仿真时工具生成的 RTL 波形里外部的“调用者”根本不会按 memory 时序去和模块握手结果自然对不上。解决办法就是显式指定接口。像 FIR 这种单采样点输入、单输出值的结构我一般会这样约束#pragma HLS interface mode ap_vld port x #pragma HLS interface mode ap_vld port y如果数据是流式连续处理用hls::stream接口更合适对应的接口模式是 ap_fifo。如果你想对接 Zynq ARM 端那就用 s_axilite 和 m_axi。关键不是记住每个模式对应什么信号时序而是要在综合报告里检查“综合器实际生成的接口”确认和你的预期一致。排接口问题时我还有个习惯先跑一次 C Synthesis打开报告里的 Interface 标签看每个端口有没有握手信号、位宽对不对、方向对不对。把这里确认清楚至少能解决一半的联合仿真问题。5.2 常见错误速查表与排查思路我把这几年实际踩过、帮别人排查过的典型问题整理成一张表按出现频率排序现象可能原因处理思路C 仿真正常综合后功能不对接口握手时序与预期不一致显式添加 interface pragma检查综合接口报告pipeline 后 II 达不到 1循环间数据依赖、数组 Bank 冲突查看 Dependency/Bank Conflict 警告做 partition综合 BRAM 占用突然爆炸大数组被 complete 展开大数组改用 cyclic/block或默认 memoryC/RTL 联合仿真总是超时握手信号没有等 validtestbench 等待 ap_done/ap_vld流式接口改用 hls::stream浮点运算结果偏差大浮点单元流水级不同、精度被放大改用 ap_fixed验证时用容差比较综合时间异常缓慢大量无约束的循环展开、超大位宽运算缩小 unroll factor缩小数据类型位宽xsim 报找不到动态库安装路径含空格或非英文字符重装到纯英文路径在 Vitis 终端里启动流程这里面我想多说一句浮点的问题。HLS 完全支持 float 和 double但代价非常高一个浮点加法器在 FPGA 上要占几百个 LUT流水线深度也大。如果你的算法可以做定点化尽量转用ap_fixed定点数性能差距经常是数量级的。转换也不要盲目先估算数据的动态范围和精度需求比如ap_fixed32, 10表示总位宽 32 位整数部分 10 位剩下 22 位小数具体每位的含义要按算法自己推。定点化会引入量化误差这是正常的只要在可接受范围内就行。排查这些问题时工具自带的几个 viewer 非常有用。Schedule Viewer 能直观看到每个周期的资源占用和流水情况Dataflow Viewer 能看hls::stream流式设计里各个阶段的并行度。别嫌麻烦学会看这几个窗口比瞎试 pragma 高效得多。6. 进阶路线与工程落地建议6.1 从教程到项目学完基础后往哪个方向深入如果你已经能把手上的小程序综合成性能可接受的 RTL下一步就可以往更贴近工程的方向走了。我建议按以下顺序渐进式练习先做图像处理算法比如 Sobel 边缘检测、均值滤波这些算法天然是二维数组和窗口滑动刚好能把循环优化、数组 partition、行缓冲这些概念全部串起来再做一个流式数据处理的例子比如 FIR 的流式版本用hls::stream连续处理数据帧体会 dataflow 和乒乓操作的配合最后可以尝试把一个算法封装成 Vivado IP挂到 Zynq 的 AXI 总线上通过 ARM 端传数据跑一次完整的软硬件协同流程。这个过程中有两个知识点值得重点钻研。一个是hls::stream配合 dataflow 的用法。hls::stream本质上是一个带握手信号的 FIFO它让模块之间不用关心对方什么时候读写只要数据流能对上就行。#pragma HLS dataflow可以让多个函数之间自动形成流水线并行执行代价是每个函数之间可能需要乒乓缓冲或 FIFO资源会有所增加。另一个是 m_axi 接口的高效写法。当你需要从 DDR 搬大块数据时能不能发出连续突发直接决定带宽利用率。做到这一点需要保证访问地址连续、数据位宽对齐并尽量避免每次只读一个数据就切到其他地址。还有一个容易被忽略的点HLS 生成的 RTL 和手写 RTL 一样需要做时序收敛检查。如果某个模块在 HLS 里综合报告的时序很好但放进完整工程后时序崩了十有八九是布局布线阶段的拥塞问题或跨时钟域处理没有做对。建议在 HLS 阶段就把时钟约束、输入输出延迟约束写清楚不要一路默认到 Vivado 里才发现问题。6.2 我在实际项目中的三点体会最后按惯例聊点接地气的经验。第一先写“不可综合的算法模型”再改写成“可综合风格”。我在做图像算法时一开始根本不加 pragma先在 C 里把功能验证好甚至用 OpenCV 对照结果确认算法无误后再一点点替换成可综合的数据结构每一步都重新跑 C 仿真和 RTL 联合仿真。这样能极大减少“算法错了还是综合器错了”的纠结。第二优化循环之前先看 Dependency 报告。我一开始特别喜欢在循环里乱加 pipeline 和 unroll结果资源翻倍性能却没提升。后来养成习惯先看卡脖子的依赖在哪是循环间数据依赖还是数组访问冲突再对症下药。很多时候加一个array_partition的效果比盲目展开循环好得多。第三不要迷信 HLS 能完全替代 RTL 工程师。一个项目中HLS 最适合用来做复杂的算法模块和快速原型但底层的高速接口、精细的时钟管理、关键路径的时序修正仍然需要 RTL 或对 RTL 有足够理解的人来把控。最好的组合是“HLS 负责算法RTL 负责接口二者互补”。学会 Vitis HLS不是为了丢掉 Verilog而是让整个团队在算法到硬件这条路上走得更快。从一个 FIR 滤波器开始到完整的软硬件系统这条路每往前走一步都会遇到新问题但也因此几乎每个项目都能优化出惊喜。如果你正在学 HLS建议现在就把手上的小函数综合一遍看看报告加个 pragma再综合一遍亲手感受一下“同一段 C 代码不同的硬件架构”带来的差异。这一步迈出去后面的路就顺了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询