资讯动态

Vitis HLS入门指南:从C代码到FPGA RTL与IP核实践

发布时间:2026/10/28 4:31:36 来源:尧图企业网站定制
简介面向FPGA高层次综合开发者的《Vitis高层次综合用户指南》中文译本对应UG1399 v2022.22022年12月7日发布适合HLS初学者、算法加速工程师以及需要把C/C代码落到可综合硬件上的设计者。全文分七部分HLS概念与优势、HLS编程指南FPGA编程三大范例、控制驱动与数据驱动的任务级并行、循环与流水线优化、Vitis HLS的环境配置与工程创建、命令行选项、C语言测试平台驱动、预置库函数与模板以及把既有代码与IP移植到HLS环境的策略目录层级清晰可按设计进程逐节查阅。整包为1个PDF文档约16.41MB保留原版章节编号与图表便于检索、批注和打印对照。已有735人学习下载可作为日常工具书遇到并行化、接口综合或移植问题时按目录直达对应章节结合示例与综合报告理解工具行为少走弯路。1. 先搞懂一件事HLS 到底把 C 代码变成了什么第一次打开这份 Vitis HLS 用户指南我猜你和我当初一样翻两页就懵了满屏的 pipeline、unroll、m_axi 全是没见过的词而你要做的却是 FPGA 上的硬件电路。先说句能让你安心的实话高层次综合HLS不是让你绕开硬件知识而是给你换一种描述硬件的语言——用 C/C 写逻辑让工具替你生成寄存器传输级RTL代码。你可以把它想成一个懂硬件的翻译官。你写的是顺序执行的软件语句它读完之后帮你把这些语句排成能在电路里同时跑的流水线最后吐出一份可以直接打包成 IP 核的硬件设计。这份 Vitis HLS 用户指南UG1399v2022.2就是这位翻译官的使用说明书。它适合两类人刚接触 FPGA、不想一上来就啃 Verilog 的新手以及写过软件、想把算法搬上硬件的工程师。2. 为什么值得学写 C 和写 Verilog 差在哪里很多人对 FPGA 的第一印象是“难”。难在哪难在你得用一个时钟周期一个时钟周期的方式去描述电路这个信号在上升沿采样、那个状态机要跳转到第几态、握手信号拉高之后几个周期才能读数据。写一个简单的图像滤波几千行 Verilog 打底是常事而且改一个参数就可能推倒重来。HLS 换了个思路你描述“算什么”工具负责“怎么排电路”。这不是偷懒而是把重复的、机械的电路搭建工作交给工具让你把精力放在算法和数据流上。但这里必须先泼一盆冷水——HLS 不是银弹它有自己的脾气写惯软件的人第一次上手十有八九会翻车。所以下面这几节我先把“为什么值得学”和“学的时候要注意什么”一起讲清楚。2.1 FPGA 编程的三大范例顺序、数据并行、任务并行指南的第 1 章把 FPGA 编程归纳成三大范例这个划分非常关键因为它决定了你写 C 代码时的思维方式。第一种是顺序执行也就是控制驱动。你的代码一行一行往下走硬件里对应的是状态机做完第一步再做第二步。这是软件工程师最熟悉的模式也是最容易上手的起点。代价是并行度低如果你的算法里有很多可以同时做的事用纯顺序的方式写就浪费了 FPGA 的并行能力。第二种是数据并行。典型场景是一个循环里对一大堆数据做同样的运算比如给一张图的每个像素乘一个系数。硬件里可以复制多份运算单元让多个像素同时算。在 HLS 里控制这件事的手段就是循环展开unroll和流水打拍pipeline后面第 4 章会细讲。第三种是任务并行也叫数据驱动的任务级并行。一个流程里有多个独立的处理阶段比如“读数据 → 处理 → 写数据”每一段各干各的段与段之间用 FIFO 一样的通道传数据。指南里专门讲了数据驱动和控制驱动两种任务模型还讲了怎么混用。新手最容易犯的错是想在一开始就把三种范例全用上。我的建议很朴素先用顺序的方式把功能写对、跑通 C 仿真再考虑给最耗时的那个循环加流水最后才去想任务级并行。这个顺序和指南第 1 章“组合三种范例”的思路是一致的。2.2 换语言到底省了什么效率、可读性、优化空间指南在“高层次综合的优势”一节里列了几条我挑三条对新手最有实感的讲。开发效率。同样一个矩阵乘Verilog 要手写乘加单元阵列、地址生成、握手时序C 版本可能就三层循环。省下来的时间不是一点半点尤其是你还要反复调参数、改位宽、试不同的并行度的时候改 C 代码比改 RTL 快一个数量级。可读性和可维护性。这一点在团队协作里体现得最明显。一个算法工程师看得懂 C未必看得懂你写的状态机。用 C 描述算法算法本身成了设计文档后人接手时不用先反向推理你的电路意图。优化空间。HLS 工具会自动做流水线、资源分配、位宽推断这些事而且它会给你一份详细报告告诉你每个循环的启动间隔是多少、瓶颈在哪。这份报告的价值很高——它把“为什么我的设计跑不快”这个玄学问题变成了可以逐条排查的清单。我在第一次看到调度查看器里那条长长的、被拉开的时序条时才真正明白自己那段代码慢在哪。但优势的另一面就是约束。既然工具要替你推断电路那它就必须能“看懂”你的代码。凡是它看不懂的写法——动态申请内存、递归调用、函数指针乱飞——它都会直接报错。这不是工具的缺陷而是这类工具的边界你得先接受这个边界。2.3 这份指南的七个部分对应你的学习顺序整本指南分成七大部分HLS 简介、HLS 编程指南、使用 Vitis HLS、命令行、C 语言驱动、HLS 库、HLS 移植。新手看到这个目录容易犯选择困难我的建议是不要按目录顺序从头读到尾。前两部分简介 编程指南是概念地基值得认真读但不必一次读完。第三部分“使用 Vitis HLS”是操作手册是你真正动手时会反复翻的部分从创建工作、设置时钟频率到跑 C 仿真、做综合、看报告全在这里。第四、五部分偏向自动化流程和验证等你手工跑通一遍之后再回来看会轻松很多。第六、七部分的库和移植属于你有了第一个能跑的工程之后的进阶内容。如果你只有一小时那就读第 1 章简介然后直接跳到第 12 章“创建新的 Vitis HLS 工程”把工具跑起来。概念这种东西光看是记不住的得让它在屏幕上动起来。3. 第一个例子怎么跑从一段向量加法到看见综合报告这一章是全文最“手把手”的部分。我选了一个最朴素的例子两个数组逐元素相加。它足够简单简单到你能一眼看懂功能又足够典型典型到能把建工程、写顶层函数、加接口约束、跑仿真、做综合这条完整链路走一遍。3.1 起步前的两件事环境配置和启动方式Vitis HLS 有两种用法图形界面和命令行。新手先用图形界面因为综合报告、调度查看器这些可视化工具都在界面里看图比看日志快得多。命令行适合你后面做批量回归现在不用急。启动之前要先配置环境。Linux 下通常是执行安装目录里的环境脚本让工具链的路径进到当前终端# 进入工具安装目录执行环境配置脚本 # 具体路径按你自己的安装位置替换不要照抄 source 安装目录/settings64.sh # 配置完成后验证一下能打印出版本信息就说明环境对了 vitis_hls -versionWindows 下更省事直接从开始菜单找到 Vitis HLS 的启动项点开就行。这里有个血泪经验如果你在终端里启动工具一定要在同一个终端里先 source 环境脚本否则工具会报找不到器件库或者找不到编译器。我第一次装完就踩了这个坑折腾了半小时才发现是新开的终端没继承环境变量。界面打开后你会看到一个工程浏览区、一个代码编辑区和一个流程导航区Flow Navigator。流程导航区那一排按钮——C 仿真、综合、协同仿真、导出——就是你接下来要按顺序点的东西。先记住这个布局别的先不用管。3.2 建工程源文件、测试激励、顶层函数怎么放新建工程时有几样东西要准备好缺一样后面都会卡住。第一是源文件。注意HLS 综合的是“函数”不是main。所以你的源文件里应该是一个可以被别人调用的函数而不是一个自己跑起来的程序。这个函数叫顶层函数工具会从它开始往下综合。第二是头文件。把函数声明、常量定义放进去源文件和测试激励都 include 它避免两边的接口对不上。第三是测试激励文件。这是新手最容易忽略的东西。测试激励就是一个带main的 C 文件它负责造数据、调你的顶层函数、比对结果。没有它你只能做综合不能做 C 仿真也就失去了在写硬件之前先把功能验证掉的机会。第四是顶层函数名。工具需要知道从哪个函数开始综合这个在工程设置里指定命令行里对应set_top。建工程时还会让你选一个解决方案名solution。这个概念要提前说清一个工程下可以挂多个解决方案每个解决方案有自己的一套约束和编译指示共用同一份源文件。你后面想试“时钟 10ns 和 5ns 分别是什么结果”“加流水和不加流水差多少”就是靠建多个解决方案来对比的不用复制一堆工程。这个设计非常实用指南第 16 章讲“创建额外解决方案”说的就是这个。3.3 写第一个能综合的函数先看顶层函数。这段代码不长但每一条编译指示pragma都有明确作用// vec_add.h #ifndef VEC_ADD_H #define VEC_ADD_H #define N 64 void vec_add(const int a[N], const int b[N], int c[N]); #endif// vec_add.cpp #include vec_add.h void vec_add(const int a[N], const int b[N], int c[N]) { // 三个数组各走一个 m_axi 接口bundle 名字不同表示分成三组总线 #pragma HLS INTERFACE m_axi porta offsetslave bundlegmem0 #pragma HLS INTERFACE m_axi portb offsetslave bundlegmem1 #pragma HLS INTERFACE m_axi portc offsetslave bundlegmem2 // 控制寄存器走 s_axiliteCPU 通过它读写参数、启动和查询状态 #pragma HLS INTERFACE s_axilite portreturn bundlectrl for (int i 0; i N; i) { // 让每次迭代都能在上一个迭代还没结束时启动目标启动间隔 1 个时钟 #pragma HLS PIPELINE II1 c[i] a[i] b[i]; } }逐条解释一下。#pragma HLS INTERFACE这一组决定的是“这个函数对外长什么样”也就是它变成 IP 核之后外部怎么访问它。m_axi表示数组走 AXI 主口直接对内存读写offsetslave表示地址偏移由控制寄存器给出而不是硬编码在电路里这样同一份硬件可以处理不同地址的数据bundlegmem0/1/2表示把三个数组分到三组独立的总线上避免它们抢同一个端口。s_axilite是控制接口CPU 通过它启动这个 IP、读状态。PIPELINE II1是给循环的意思是希望每 1 个时钟就能启动一次新的迭代。再看测试激励// vec_add_tb.cpp #include iostream #include vec_add.h int main() { int a[N], b[N], c[N]; // 造一组简单数据方便人工核对 for (int i 0; i N; i) { a[i] i; b[i] 2 * i; } vec_add(a, b, c); // 逐元素比对任何一处不对都算失败 int err 0; for (int i 0; i N; i) { if (c[i] ! 3 * i) { std::cout mismatch at i : c[i] std::endl; err; } } std::cout (err 0 ? PASS : FAIL) std::endl; return err; }这份激励的关键在于它返回错误数。很多新手写的测试文件最后只 print 一句“跑完了”工具根本不知道成功还是失败后面做自动协同仿真的时候就没法判断。养成习惯main返回非零表示失败。3.4 三步走C 仿真、综合、协同仿真如果你喜欢命令行把下面这段存成run_hls.tcl然后用vitis_hls -f run_hls.tcl跑起来。图形界面里的操作和这段脚本是一一对应的看懂脚本也就看懂了界面在干什么。# 创建一个工程-reset 表示如果已存在就清空重建 open_project -reset prj_vec_add # 指定从哪个函数开始综合 set_top vec_add # 添加源文件和测试激励 add_files vec_add.cpp add_files -tb vec_add_tb.cpp # 创建一个解决方案指定流程目标 open_solution -reset solution1 -flow_target vivado # 器件型号按你自己板子上的填这里只是示例 set_part {xc7z020clg400-1} # 目标时钟周期 10ns即 100MHz create_clock -period 10 -name default # 第一步C 语言仿真先确认功能对不对 csim_design # 第二步综合把 C 变成 RTL 并生成报告 csynth_design # 第三步C/RTL 协同仿真验证综合出来的电路行为是否和 C 一致 cosim_design # 最后导出成 IP供上层系统调用 export_design -format ip_catalog几个参数值得单独说。set_part是器件型号写错了会直接报错去查你开发板的型号不要照抄别人的。create_clock -period 10里的 10 是纳秒你要 200MHz 就写 5。这个数字不是许愿工具会尽力去满足做不到的时候会在报告里告诉你差多少。csim_design跑的是纯软件仿真速度快用来抓逻辑错误。csynth_design是真正的综合它输出 RTL 和一份综合汇总里面有几样东西必看资源占用LUT、寄存器、DSP、BRAM 各用了多少、时序估计目标时钟和估计时钟、以及每个循环的启动间隔 II。cosim_design最慢但它能抓住“C 仿真过了、综合出来的电路却不对”这类问题——这种情况在指针、未初始化变量、边界处理上并不罕见。提示第一次跑把cosim_design留着别删。哪怕它慢它也是你确认“我写的 C 和生成的电路是同一个东西”的唯一手段。综合跑完之后界面里会有几个查看器。调度查看器把每个操作放在时间轴上你能直观看到哪一步占了几个周期、哪里被卡住了。函数调用图给你整体结构。数据流查看器和时间线轨迹查看器在你看任务级并行的时候特别有用。新手先看调度查看器和综合汇总就够了别一上来被一堆图吓住。4. 三个绕不开的概念循环、数组、函数跑通第一个例子之后你会发现真正的难点不在工具操作而在“我这么写工具会给我生成什么电路”。这一章讲三个最基础也最容易出问题的东西循环、数组、函数。指南把它们分别放在第 3、4、5 章我建议连着读因为它们在真实设计里是缠在一起的。4.1 循环流水打拍和展开到底在改什么循环是 HLS 里最值得花时间的部分因为你算法里 90% 的时间都花在循环上。先说流水打拍pipeline。默认情况下一个循环的每次迭代是串行的第 1 次迭代完全做完才开始第 2 次。加流水之后第 2 次迭代可以在第 1 次还没结束时就开始只要它们用的硬件资源不冲突。衡量指标叫启动间隔 IIinitiation intervalII1 表示每个时钟都能送进一个新数据这是很多场景下的理想值。再说展开unroll。展开是另一回事它把循环体复制多份让多次迭代真正并行执行。一个 4 次迭代的循环完全展开电路里就有 4 份运算单元同时干活。代价是资源翻倍。所以展开不是越多越好你得看自己的资源预算。这两者的区别新手经常搞混。我的记法是流水是让活儿错开时间做展开是让活儿同时做。流水省的是时间展开花的是资源。// 流水写法资源基本不变吞吐提上来 for (int i 0; i N; i) { #pragma HLS PIPELINE II1 c[i] a[i] * b[i] c[i]; } // 展开写法并行度上去运算单元也翻倍 for (int i 0; i 4; i) { #pragma HLS UNROLL c[i] a[i] * b[i] c[i]; }除了这两个指南还讲了循环合并和嵌套循环处理。循环合并是把相邻的、边界相同的两个循环合成一个减少循环本身的开销但合并之后流水可能会受更多约束不一定总是变快。嵌套循环的常见做法是给内层加流水、外层保持顺序因为外层流水往往会导致资源爆炸。指南里还专门讲了变量循环边界的问题——如果你的循环次数是运行时才知道的工具没法完全展开很多优化就用不了这是新手写代码时特别容易忽略的一点。跑完综合记得回头看报告里的 II。如果某个循环的 II 是 5 而不是 1说明有资源冲突通常就是下一节要说的数组访问问题。4.2 数组软件里的一行硬件里是一块存储器在软件里写int a[64]你觉得它就是 64 个格子随便访问。在硬件里不是。默认情况下这个数组会被映射成一块双端口 BRAM也就是每个时钟最多只能读两次、写两次。你的循环如果在一个周期里要访问 3 个不同的元素那就撞墙了II 直接掉下来。解决办法是数组分区array partition。把一个数组拆成多块小的或者按元素拆开成独立的寄存器。常见三种拆法完全拆分complete每个元素一个存储单元、按块拆block、交叉拆cyclic。拆得越狠并行访问能力越强占用资源也越多。int a[N]; // 完全拆开每个元素独立存储适合并行度要求高、元素数不多的场景 #pragma HLS ARRAY_PARTITION variablea typecomplete dim1 // 按块拆成 4 份折中方案元素多的时候更实用 #pragma HLS ARRAY_PARTITION variablea typeblock factor4 dim1数组还有几个新手常踩的点。接口上的数组顶层函数的数组参数如果不指定接口工具会给你一个默认接口很可能和你板子上的总线对不上导出的 IP 接不进去。数组初始化在声明时给了初值的数组综合出来是 ROM这是省资源的好办法。数组复位给数组加复位会导致每个元素都要有复位逻辑资源开销很大能用初始化解决的就别用复位。4.3 函数内联、流水、例化三选一函数在硬件里对应一块电路。当一个函数被多处调用时工具默认会共用同一份电路——这省资源但同一时刻只能服务一个调用者。如果你想让它同时服务多个调用者就得例化多份。反过来如果一个函数很小、调用很频繁函数调用本身的开销保存现场、跳转就很划不来这时候用**内联inline**把函数体直接展开到调用处省掉调用开销。代价是代码体积变大如果这个函数在很多地方被调用电路可能膨胀。函数也可以加流水。给函数加流水意味着这个函数可以连续接收新的一组输入而不必等上一组完全算完。这个用法在流式处理里很常见。// 小函数内联掉调用开销 inline int square(int x) { return x * x; } // 大函数允许流水连续接收新数据 void process(int in, int out) { #pragma HLS PIPELINE II1 out square(in) 1; }选择原则很简单调用点多、函数体小就内联需要同时处理多路数据就例化数据是流式进来的就加流水。指南第 5 章和第 10 章里讲的“C 类中展开循环”“静态变量对流水的影响”都是这些原则在具体场景下的延伸等你能跑通例子之后再回头读会顺很多。4.4 数据类型为什么不用 int 而用 ap_int硬件里位宽就是资源。你写一个int就是 32 位如果你的数据实际只用到 8 位那 24 位就是白扔的。在 FPGA 上这种浪费会直接体现在 LUT 和寄存器的占用上。所以 HLS 提供了任意精度类型ap_intW表示 W 位有符号整数ap_uintW是无符号版本还有ap_fixed表示定点小数。用ap_int8代替int处理 8 位数据资源能省下一大截。#include ap_int.h // 只用 8 位就声明 8 位别用 int 浪费 24 位 void scale(ap_uint8 in, ap_uint8 out) { out in * 2; }要注意的是位宽推算得自己心里有数。两个 8 位数相乘结果是 16 位如果你还用 8 位接就会溢出。指南第 6 章讲得很细包括标准类型、复合类型、任意精度类型、指针、矢量类型和位宽传输新手先把ap_int和ap_uint用熟就够。另外指针也是新手容易栽的地方用指针做随机访问工具分析不出访存模式流水就做不出来甚至直接综合失败。5. 新手避坑四个把综合结果搞崩的常见操作这一章列的四个坑是我见过新手最容易踩、也最难自己定位的。每条我都按“现象 → 原因 → 解决”写你遇到问题时可以对照着排查。5.1 现象综合直接报错提示不支持某构造原因你用了 HLS 不支持的 C/C 构造。指南第 7 章专门列了一份清单包括系统调用、动态内存分配malloc/new、递归函数、虚函数、以及某些未定义行为。这些在软件里天经地义在硬件里没法映射——硬件没有操作系统也没有可以随时增长的堆。解决把动态内存改成静态数组数组大小在编译期就定下来把递归改写成循环加显式栈把系统调用打印、文件读写从综合的函数里挪出去只留在测试激励里。第 7 章那份清单值得在动手前扫一遍能省很多试错的功夫。5.2 现象综合能过但某个循环的 II 一直是 5 以上怎么加流水都不降原因多半是数组访问冲突。默认的双端口 BRAM 一个周期只能支持有限的访问次数你的循环在一个周期里要读好几个不同下标的元素硬件做不到只能把迭代拉开。也可能是循环里存在跨迭代的依赖比如后一次迭代要读前一次写的结果。解决先看调度查看器找出是哪几个操作在抢资源。如果是数组访问考虑加ARRAY_PARTITION如果是依赖看看能不能把依赖拆开或者接受这个 II。还有一种情况是循环边界是变量工具没法展开那就把边界固定下来。5.3 现象C 仿真通过了功能看起来没问题但资源占用高得离谱原因位宽写得太大或者数组没有分区导致工具给你推断了一块巨大的存储器。还有一种隐蔽情况是给数组加了复位每个元素都要独立的复位逻辑。解决把能降到ap_int8的数据类型都降下来检查每个数组的存储实现该分区的分区该做成 ROM 的用初始化列表做 ROM去掉不必要的数组复位。综合汇总里的资源数字要养成习惯去看别只看“综合成功”四个字就完事。5.4 现象C 仿真过、综合也过上板之后结果就是不对原因C 仿真只验证了算法逻辑它不知道综合出来的电路长什么样。典型问题包括未初始化变量、指针别名、以及边界情况处理不一致。这类问题在纯 C 环境里可能刚好“运气好”跑对了硬件里就没有这个运气。解决跑 C/RTL 协同仿真。它会用你写的测试激励去驱动生成出来的 RTL把结果和 C 模型对比。这一步慢但它是唯一能抓住这类问题的手段。另外协同仿真过不了的时候去看时间线轨迹通常能定位到是哪个接口的时序对不上。注意把“C 仿真通过”当成“设计完成”是新手最贵的错觉。三个验证步骤各有各的职责一个都不能省。6. 下一步学什么从跑通例子到能自己调优的路线跑通第一个例子只是拿到了入场券。接下来怎么走我给你一条我验证过、也踩过坑的路线分四个阶段每个阶段都有明确的“算过关”的标准别凭感觉判断自己学没学会。阶段要练的事对应指南部分怎么算过关第二阶段给循环加流水、加展开、加数组分区观察 II 和资源的变化第 3、4 章 第 10 章能把一个 II5 的循环优化到 II1并说清代价第三阶段自己定接口m_axi、s_axilite、axis 各用一次第 8、9 章导出的 IP 能被上层系统正确调用第四阶段建多个解决方案做对比实验用命令行批量跑第 16 章 命令行部分能一次跑出三组参数的结果并列表对比第五阶段移植一段现成的软件算法让它能综合、能协同仿真第 6、7 章 移植部分协同仿真通过资源在预算内第二阶段最值得投入时间。找一个你自己写过的、带嵌套循环的小算法比如卷积或者排序把它搬进来然后一组一组地改先只加流水记录 II 和资源再加数组分区再记录再试着展开。三组数据摆在一起你对“哪种写法花多少资源换多少性能”就有直觉了。这个直觉比读十遍文档都管用。第三阶段开始接触接口。新手容易在这里卡住因为接口选错后面怎么调都不对。我的建议是先照着指南第 8 章的示例抄一遍把三种接口各跑通一次然后再去理解存储器布局模型那些细节。第四阶段是效率的分水岭。学会用多个解决方案做对比之后你就不再靠猜了。同一份代码改一个参数建一个新 solution跑完把报告里的数字抄到一张表里谁快谁慢一目了然。最后说个我自己的教训。我刚开始学的时候最大的毛病是“改完就跑综合”从来不回头看报告里的 II 和资源数字看到“综合成功”就以为万事大吉结果上板一测性能差得离谱再回头一点点翻报告前后浪费的时间够我把整个流程重走三遍。从那以后我每次改完代码都强制自己先把综合汇总里的资源占用和每个循环的 II 抄下来和上一版对比确认变化符合预期再去跑协同仿真。这个习惯听起来笨但它把“玄学调优”变成了可追踪的实验记录。希望这条路线能帮你少走点弯路也希望你早点跑出第一个自己的 IP 核。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑