资讯详情

Vitis HLS入门指南:FPGA算法加速从C到RTL的高层次综合实践

📅 2026/10/5 11:37:40 | 华诺云谱 👁 阅读
Vitis HLS入门指南:FPGA算法加速从C到RTL的高层次综合实践
最近连续被好几个做FPGA的朋友问到同一个问题Vitis HLS到底值不值得学还是老老实实写Verilog就行问的人多了我想干脆整理一个系列教程出来从最基础的概述开始把Vitis HLS这件事彻底讲透。今天这篇是第一讲主要解决三个问题Vitis HLS是什么、为什么需要它、怎么把它跑通第一个完整流程。如果你正在考虑用FPGA做算法加速或者刚接触Xilinx的开发环境这篇文章应该能帮你少走不少弯路。1. Vitis HLS到底是干什么的1.1 先说说FPGA开发的老大难FPGA开发有一个绕不开的痛点用Verilog或VHDL描述算法实在太慢了。不是说语法有多难而是硬件描述语言的思维方式本质是在描述“电路怎么搭”而不是描述“算法怎么算”。你写一个FIR滤波器RTL代码里一半以上的时间都在处理状态机、计数器、地址生成、读写时序真正的滤波算法逻辑反而被淹没在大片细节里。到了产品迭代阶段就更头疼。今天要换个滤波器系数明天要改矩阵维度后天要调整帧格式每改一次都要同步改testbench、改地址逻辑、改时序约束稍微不注意就会引入新的时序问题。我在实际项目里见过不少团队一个算法模块在RTL上反复调试前前后后拖了几个月性能和资源倒是达标了但市场窗口也错过了。Vitis HLS解决的就是这个问题。它允许你用C、C或System C来描述算法然后由工具自动把高级语言综合成RTL代码。你写的是算法逻辑工具帮你处理调度、状态机、接口协议这些硬件细节。这样一来算法工程师也能直接参与FPGA开发不需要先花半年时间补RTL的课。1.2 它和“用C写硬件”到底是不是一回事很多刚接触的朋友有个误解以为Vitis HLS是“把C语言直接变成电路”。一句话就能说明白不是。C语言描述的是行为综合工具做的是把行为翻译成逻辑电路这个过程叫高层次综合High-Level Synthesis。如果你写过软件可以这样类比GCC把C语言编译成汇编指令Vitis HLS把C语言“编译”成Verilog/VHDL。但和软件编译有个本质区别——综合器不只是翻译语法它还要考虑硬件特有的问题组合逻辑能不能在一个时钟周期内跑完、多个操作之间怎么调度、数据要存在寄存器里还是BRAM里、乘法是用DSP还是用LUT拼。这些都是综合器自动决定的但最终产出的RTL是真实的、可上板的电路不是仿真模型。还有一个容易混淆的概念Vitis HLS和Vitis整个平台的关系。Vitis是Xilinx统一软件平台的名字里面包含嵌入式开发、AI推理、加速库、以及我们这里要讲的Vitis HLS。HLS只是其中负责“把C综合成硬件”的那一部分。实际项目中你用HLS生成IP核然后拿到Vivado里去布局布线最后生成bitstream下载到板卡这是一条完整链路。1.3 什么人适合用什么人别硬上我这些年接触过的用户用Vitis HLS用得最顺的主要是三类人。第一类是算法工程师。通信领域的DPD数字预失真、CFR波峰因数降低图像领域的滤波、特征提取这类算法用RTL从头写一遍调试周期按月算用C/C建模验证后再交给HLS综合快得多。第二类是软件背景出身的开发者想用FPGA做硬件加速但不想从头啃RTL。第三类是资深RTL工程师他们多数不靠HLS做最终交付而是用它做快速原型验证算法确定后再手写RTL做性能优化。反过来说如果你只是做简单的接口逻辑、控制逻辑或者做一个状态机就能搞定的模块直接用RTL更划算。这种场景下HLS反而显得笨重——综合出来的电路可能比手写RTL多消耗不少资源时序也不一定更好。我的看法一直是RTL和HLS不是替代关系而是分工协作。复杂算法用HLS加速迭代关键接口和时序敏感的模块用手写RTL把控这才是工程上比较务实的做法。2. Vitis HLS在后台干了些什么2.1 调度和绑定把C代码变成流水线作业表理解Vitis HLS的底层机制有助于你写出更容易被优化的代码。综合过程的核心是两步调度Scheduling和绑定Binding。调度解决的是“每个时钟周期干什么”。举个例子表达式sum a[i] * b[i]乘法器延时3纳秒加法器延时1纳秒时钟约束是5纳秒。调度器会决定这个乘加操作放在一个时钟周期内完成还是拆成两个周期。如果寄存器到寄存器之间的组合逻辑路径太长5纳秒跑不完调度器会自动拆成多周期保证满足时序约束。绑定解决的是“用什么硬件资源干这件事”。同样是乘法运算可以用DSP48E硬核实现也可以用LUT拼一个乘法器累加结果可以放在寄存器里也可以放在BRAM里。绑定器会根据你的资源约束和性能目标做选择默认逻辑是“用尽量少的资源满足给定时钟约束”。用做饭来类比调度就是安排工序——先烧水还是先切菜这口锅什么时候用来炖汤、什么时候用来炒菜绑定就是决定用到哪些锅碗瓢盆——灶台只有一个还是有两个有没有烤箱。调度做得好一顿饭出菜快绑定做得好厨房资源不浪费。2.2 接口综合所有参数最后都会变成引脚和总线很多人在HLS上栽跟头不是栽在算法本身而是栽在接口上。Vitis HLS的顶层函数会被综合成一个硬件模块函数的每个参数都要被映射成物理端口或总线协议这个过程叫接口综合Interface Synthesis。默认情况下函数参数的处理方式有固定规则标量参数默认变成寄存器端口数组参数默认映射到BRAM接口指针参数会被当成存储器接口或者流接口处理。此外顶层模块还会自动生成时钟、复位、以及控制握手信号比如ap_start、ap_done、ap_idle、ap_ready这些信号组成ap_ctrl_hs控制协议。在接口综合里几个概念必须搞懂ap_none表示端口没有握手信号数据一直有效ap_vld和ap_ack是带有效和应答的握手方式适合数据非连续的场景ap_memory是BRAM接口有地址、数据、读写使能信号axis是AXI4-Stream接口适合高速数据流m_axi是AXI4-Master接口可以主动从DDR里搬数据。实际工程中配置寄存器通常用AXI4-Lite数据通路通常用AXI4-Stream或者AXI4-Master数组运算模块内部用BRAM接口就够了。接口设计的重要性怎么强调都不过分。很多HLS模块综合出来性能很好但接到整个系统里吞吐上不去最典型的原因就是接口选错了。数据从一个接口进来是逐拍握手从另一个接口变成突发传输整体性能可能差出好几倍。2.3 可综合的C代码子集不是所有C都能上FPGAVitis HLS能综合的代码是C/C的一个子集不是完整语言标准。这一点最好提前建立一个清晰认识否则你会被各种综合报错折磨。可以综合的包括固定边界的循环、数组、算术运算、逻辑运算、条件分支、函数调用、结构体和类。这些在数字信号处理、图像处理、矩阵运算的算法描述里占了绝大部分。不可以综合的包括动态内存分配malloc/free/new/delete、递归、动态大小的循环边界、基于堆的指针操作、系统调用printf、fopen、虚函数、异常处理等。原因也好理解硬件电路必须在编译期确定所有资源的大小和连接关系可变大小的内存分配在电路里根本没有对应的东西。所以用HLS写代码第一条原则是在写C代码的初期就要按硬件思维来约束自己。数组大小必须编译期可确定循环边界尽量用#define定义指针用法尽量简单直接。我见过不少人把PC上的算法工程直接丢给HLS结果改代码的时间比重写RTL还长这就是没搞清楚可综合子集这个概念。3. 环境准备选型、安装、第一个Hello工程3.1 Vitis HLS和Vivado HLS该选哪个网上搜资料的时候很多人会看到Vivado HLS和Vitis HLS两个名字容易搞混。简单梳理一下在2019.2之前Xilinx的工具叫Vivado HLS是一个独立工具界面2020.1开始Xilinx将工具整合进Vitis统一软件平台名字改成了Vitis HLS。两者核心能力基本一致都是做高层次综合但界面、命令行、工程文件格式有一些区别。Vitis HLS加入了针对Versal等新器件的支持同时集成了更多Vitis平台相关的流程比如可以直接生成适合Vitis加速卡使用的内核。Vivado HLS则停留在旧版本不再增加新功能。对比项Vivado HLSVitis HLS所属版本2019.2及更早2020.1及之后界面独立HLS GUIVitis统一IDE或命令行命令行工具vivado_hlsvitis_hls新器件支持不支持Versal支持Versal工程兼容性老工程不直接兼容Vivado HLS工程新项目我建议直接用Vitis HLS没必要在新的开发环境上重新熟悉一遍旧工具。另外多说一句很多人一上来就翻Xilinx选型手册琢磨该买哪块板卡实际上在做HLS阶段你只需要对目标器件的资源规模有个大概概念就行。开发过程中先用Vitis HLS跑综合看资源报告再根据报告决定具体器件型号这是更高效的做法。3.2 安装与License那些坑安装Vitis HLS第一件事要明白它不是独立安装包而是Vitis软件平台的一部分。你去Xilinx官网下载Vitis安装时可以选择安装哪些组件HLS是默认包含的。整个Vitis安装包体积非常庞大安装后占用磁盘空间可以从几十GB到上百GB建议准备一个至少500GB空闲的SSD并且预留充足时间。安装完成后Linux下需要source设置脚本通常是/tools/Xilinx/Vitis/2023.1/settings64.shWindows下则直接打开Vitis HLS命令提示符。很多新人遇到的第一个坑是明明装了Vivado结果在命令行里输入vitis_hls提示找不到命令原因就是没有source环境变量或者只单独装了Vivado而没有装Vitis。License方面Vitis HLS和Vivado共用一套license。评估版可以跑小规模设计但综合大一点的工程容易被限制。建议需要长期开发的朋友直接申请企业版license通过环境变量XILINXD_LICENSE_FILE指向license文件。还有一点容易忽略Vitis HLS的仿真器对license的校验很严格如果license的feature不全CSIM能跑但综合会报错这个问题排查起来比较迷惑。另外经常有人碰到USB下载器驱动在Windows下加载失败的问题。严格说这跟HLS没有关系它影响的是后续FPGA上板调试环节。最常见的解决办法是换一个已签名的旧版本驱动或者手动指定驱动路径。遇到驱动报错别慌先把驱动卸干净再重装多半能解决。3.3 用Tcl脚本快速建一个Hello工程创建一个Vitis HLS工程有两种方式图形界面和Tcl脚本。图形界面流程直观适合学习Tcl脚本可重复、可自动化适合实际开发。这里给你一个最小可用的Tcl脚本从创建工程到运行仿真一步到位open_project matrix_mult_prj add_files matrix_mult.cpp add_files -tb matrix_mult_tb.cpp set_top matrix_mult open_solution solution1 -flow_target vivado set_part {xczu9eg-ffvb1156-2-e} create_clock -period 10 -name clk csim_design csynth_design cosim_design export_design -format ip_catalog解释几个关键命令open_project创建工程目录add_files添加源文件和testbenchset_top指定顶层函数open_solution创建一个综合方案set_part设置目标器件create_clock设置时钟约束为10纳秒也就是100MHz然后依次执行C仿真、综合、协同仿真和导出IP。把上面脚本保存成run.tcl在命令行里执行vitis_hls -f run.tcl就能看到完整流程自动跑起来。我习惯在项目一开始就把Tcl脚本建好每轮优化只需修改代码再重新执行一遍脚本比在GUI里反复点菜单高效得多。4. 跑通一次完整流程从C代码到IP核4.1 先写一个能综合的矩阵乘法理论讲再多不如动手跑一遍。这里用一个8×8矩阵乘法作为示例这是HLS入门最常见的例子麻雀虽小但五脏俱全能清楚看到循环怎么被综合、资源怎么被消耗。#define N 8 void matrix_mult(int A[N][N], int B[N][N], int C[N][N]) { for (int i 0; i N; i) { for (int j 0; j N; j) { int sum 0; for (int k 0; k N; k) { sum A[i][k] * B[k][j]; } C[i][j] sum; } } }注意几个对HLS友好的设计N用宏定义直接写在编译期数组维度固定循环边界固定累加逻辑就是一层乘加。这样写是为了让综合器能够确定所有资源的边界。如果你把N改成从外部输入或者用malloc动态分配数组综合器直接就报错了。这是写HLS代码和写软件代码之间一个非常基本但重要的思维转变。4.2 C仿真先证明算法是对的写testbench这一步很多人想省掉我强烈建议不要省。你的C代码本身如果逻辑就是错的那后面综合出来的硬件也一定是错的而且硬件上调试错误比C代码调试麻烦十倍。testbench不用写得太复杂核心是准备输入数据、调顶层函数、比对结果。比如可以初始化几个矩阵调用一次matrix_mult然后用循环检查结果是否正确。如果涉及浮点数比对要有一定的误差容限。Vitis HLS里跑C仿真就叫csim_design也可以通过GUI里的“Run C Simulation”按钮执行。这一步能通过算法层面的正确性才算有保障。我在实际项目中testbench通常会被复用很多次C仿真用一遍C/RTL协同仿真又用一遍。所以写testbench时稍微花点心思输入输出做成可配置的后面能省不少事。4.3 综合报告怎么看C仿真通过后执行综合csynth_design等进度条走完打开综合报告。报告里有几个指标需要重点盯。第一个是Latency也就是端到端延迟表示从模块开始工作到输出最后一位数据的时钟周期数。第二个是Interval也叫启动间隔表示模块处理完一次任务后能开始下一次任务的最短间隔。Interval直接反映了吞吐能力数值越小吞吐越高工程优化时最关注的往往就是这个指标。第三个是资源用量包括LUT、FF、BRAM、DSP四种基本资源报告里会给出使用数量和占器件总资源的百分比。以这个8×8矩阵乘法为例不做任何优化时三层循环完全串行综合后的Latency可能达到上万周期DSP可能只用1个资源看着很省但性能就很感人。这是普遍现象不用慌。综合报告里有各个循环的耗时分析顺着报告往下看能定位到到底是哪一层循环耗时最多优化时有的放矢。另外注意一个细节综合报告里给出的时序和资源是基于逻辑综合的估算不代表最终布局布线后的实际结果。最终是否收敛还是要去Vivado里跑到place和route之后才能确定。这个“估算”和“实际”之间的落差也是很多新手容易被误导的地方。4.4 导出IP并在Vivado中使用综合完成并且报告符合预期后执行导出IP这一步。在Tcl脚本里对应export_design -format ip_catalog。导出的产物是一个Vivado IP核会以.zip文件形式打包包含配套的元数据和例化模板。Vitis HLS也支持导出成其他格式比如System Generator、Vivado IP目录或者只导出RTL实际项目中导出为IP Catalog格式是最常用的方式。到这一步Vitis HLS的任务就结束了剩下的活在Vivado里完成。新建一个Vivado工程在IP Catalog里右键选择“Add Repository”把HLS导出的IP目录加进去就能在工程里例化这个自定义IP。例化之后给IP连接时钟、复位配置接口的连接方式然后跑综合、布局布线最终生成bitstream。这里提醒一句在Vivado里如果看到IP上的端口和你预期不一致比如少了某个握手信号先回头看HLS里的接口设置而不是急着改Vivado工程。5. 初学必知的三个优化手段5.1 PIPELINE让循环流水线化默认综合下循环是串行执行的第一次迭代全部跑完才开始第二次迭代。如果每次迭代需要10个周期循环执行8次总时间就是80个周期。PIPELINE优化做的事情是让下一次迭代不用等上一次完全结束提前开始执行就像工厂流水线一样上一道工序刚做完下一道工序马上接上。用法很简单在循环体内部加一行pragma#pragma HLS pipeline II1这里的II是Initiation Interval的缩写表示两次迭代开始之间的间隔周期数。II1是最理想的情况意味着每个时钟周期都能启动一次新的迭代吞吐最大化。但能不能达到II1取决于循环体内有没有数据依赖。典型的反例是累加操作sum a[i]下一次迭代必须等上一次的sum更新完成这种循环依赖会把II拉大可能变成2或者3。这在FIR滤波器、累加器这类代码里非常常见。PIPELINE最大的好处是“加一行代码就能提吞吐”代价是有时侯会增加一些寄存器资源。我把这个当作HLS优化的第一步先流水化再看还有没有瓶颈。5.2 UNROLL用面积换吞吐PIPELINE是让迭代重叠UNROLL是让多个迭代并行执行。默认情况下循环每次迭代复用同一套硬件资源UNROLL之后循环体被复制多份同时处理多个数据。#pragma HLS unroll factor4factor4表示把循环展开4份4次迭代同时执行。如果数组足够大还可以配上限压榨整套硬件并行度让所有迭代同时执行。代价很明显循环体复制多少份资源就翻大约多少倍乘法器数量、寄存器数量、内部存储带宽都会一起涨上去。实际使用中PIPELINE和UNROLL经常搭配使用。我的经验是先PIPELINE把吞吐瓶颈暴露出来如果单个迭代内部的时延已经压到极限再用UNROLL增加并行度。不过UNROLL有个隐藏瓶颈——数组的读端口。BRAM通常只有两个读端口如果循环展开后同时需要读4个数据存储器带宽可能成为新瓶颈这时候要考虑数组分块array partition或者改用更宽的存储结构。这个问题的隐蔽性很强综合报告里性能上不去时优先查存储器冲突。5.3 INTERFACE解决数据进出瓶颈很多新手的优化顺序搞反了一上来就拼命优化循环和算术逻辑最后发现整体性能还是上不去原因是数据搬运卡住了。HLS模块不可能凭空造数据上层模块和DDR要把数据送进来处理完再搬出去。如果接口的吞吐量小于计算模块的处理能力再优化的算法也只是个跑不快的引擎。最典型的场景是数组参数默认是BRAM接口而BRAM一个周期只能读一个地址、写一个地址。如果内层循环一次需要同时读多个数组元素BRAM端口就会成为热点限制吞吐。这时候可以把数据接口改成AXI4-Stream让数据连续流式进入计算完了再流式输出吞吐会大幅提升。接口优化在代码层面就是一组pragma#pragma HLS INTERFACE modeaxis portA #pragma HLS INTERFACE modeaxis portB #pragma HLS INTERFACE modeaxis portC #pragma HLS INTERFACE modeap_ctrl_none portreturn这段配置把三个矩阵端口都定义为AXI4-Stream并且关闭了控制握手协议ap_ctrl_none让模块变成一个纯粹的流式处理单元可以一直不停地接收数据、输出结果。做接口设计时有个原则直到数据格式和接口协议功能逻辑先把数据组织好再交给计算模块。这是HLS开发中非常关键的系统思维。6. 常见问题排查与避坑经验6.1 综合报错速查表跑HLS工程时会遇到一些非常典型的报错这里整理成速查表方便你遇到问题时对照排查。报错信息常见原因解决办法unable to determine loop bounds循环边界不是编译期常量用#define定义边界或改成固定次数循环array ... local top-level interface not supported顶层数组参数缺接口约束添加INTERFACE pragma指定接口synthesis directive not recognizedpragma拼写或位置不对检查拼写把pragma放在循环体或函数体内部timing constraint not met时钟约束过紧或关键路径过长加PIPELINE或放宽时钟周期float point operations consume too much resources浮点运算开销较大考虑改用定点数或在浮点IP核和资源之间找平衡Cannot find file ...源文件路径不对检查add_files路径和环境变量看到报错先去定位是代码可综合性问题还是pragma问题还是资源约束问题然后再动手改。不要一报错就重写整个工程HLS的报错信息其实已经把线索给得比较清楚了。6.2 从Vivado HLS迁移到Vitis HLS的注意点如果你之前用的是Vivado HLS现在换到Vitis HLS有几个地方需要特别留意。第一旧工程文件不能直接打开。Vitis HLS的工程格式和Vivado HLS不同需要在Vitis HLS里重新创建工程再把源文件、testbench、约束文件添加进去。第二命令行变了从vivado_hls变成vitis_hls脚本里的命令名要同步更新。第三部分pragma的写法在新版本里有调整或扩展编译if报错时不要死磕旧语法先去查对应版本的文档。还有一个比较容易踩的坑Vitis HLS的默认代码风格检查比旧版更严格一些在旧版里能通过的模糊写法新版会直接报warning甚至error。迁移的时候多留意编译输出的警告信息尤其是关于位宽和符号的警告这类问题在RTL里会放大成很难查的bug。6.3 我的几条实战心得踩过不少坑之后总结几条真心建议。第一条先跑通再优化。我第一次拿HLS做项目时一上来就想着把性能调到极致结果花了一周调循环展开和流水后来发现算法本身有一个边界条件是错的所有优化白做。先把最简单的版本完整跑通从C仿真一直到上板验证再开始优化。这条顺序千万不要颠倒。第二条报告里数字都是“预估值”。综合报告里看到的Latency和资源是逻辑综合阶段的结果最后布局布线可能会差出10%到20%。所以在HLS阶段做优化时目标值要留有余量不要卡着极限设计。我第一次做项目时把资源用到了器件的98%结果在Vivado里布局一直失败后来才知道HLS的资源估算和实际布局有差距必须留出充足的余量。第三条接口设计第一算法优化第二。一个HLS模块能不能高效工作首先看数据进出是否顺畅其次才看内部计算是否高效。我在给团队做评审时第一眼永远是看顶层接口设计接口协议选对了后面怎么优化都不会太差接口选错了内部优化做得再漂亮整体吞吐也会被卡死。我个人从RTL转HLS时一开始是有偏见的总觉得用C写出来的硬件不够“正宗”。但实际做了几个项目之后发现HLS最大的价值根本不是替代RTL而是让人能更快地把一个算法想法变成可上板验证的硬件原型。尤其是在算法迭代阶段HLS可以把几周的RTL开发时间压缩到几天这种效率提升对产品节奏的影响非常大。这篇是第一讲后面我会把Vitis HLS的安装配置、真实项目案例、循环优化策略、以及与Vivado的联合调试逐个展开。下一讲我们从最基础的Vitis HLS环境搭建开始把那些安装配置和环境变量的问题一次性讲透。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑