资讯动态

Vitis HLS入门实战:从矩阵乘法到RTL导出的完整流程

发布时间:2026/9/13 6:46:26 来源:尧图企业网站定制
1. 初识Vitis HLS为什么第一个例程这么重要说句实在话当年我从Vivado HLS过渡到Vitis HLS的时候第一反应是“这不就换个名字嘛”。可真上手之后才发现工具链的变化远不止改个文件名那么简单。Vitis HLS不仅仅是把C/C综合成RTL它背后代表的是AMD-Xilinx习惯说Xilinx整个软件定义硬件流程的转型。好多朋友学FPGA一上来就被Verilog的时序、状态机、跨时钟域折腾得够呛。Vitis HLS的出现说白了就是给算法工程师和嵌入式工程师一条相对平缓的上手路径——用C/C描述功能让工具去生成RTL。但这里我必须泼一盆冷水HLS并不是让你完全不懂硬件恰恰相反你越懂硬件写出来的HLS代码质量越高。这个第一个例程我选了矩阵乘法。为什么选它因为矩阵乘法是HLS领域里的“Hello World”它同时包含了数组存取、循环嵌套、数据复用、流水线优化这几大核心要素。你把这个例子吃透了后面再去看卷积、FFT、FIR滤波器这些复杂算法思路会顺畅很多。这个系列笔记适合谁两类人一类是从传统Verilog/VHDL转过来的老手想看看HLS到底能把C代码折腾成什么样另一类是软件背景、想往FPGA加速方向转的开发者你们可能写C/C毫无压力但对时序、资源、带宽这些概念还比较陌生。这篇文章我会把从环境准备到上板验证的完整链路都走一遍重点放在“为什么这样做”上而不是单纯抄命令行。2. 环境准备与工具链认知2.1 Vitis HLS在Vitis统一平台里的定位Vitis并不仅仅是一个HLS工具它是一个完整的统一软件平台里面包含了Vitis HLS、Vitis Analyzer、Vitis IDE、以及针对AI推理的Vitis AI等一堆东西。2019.2版本是一个分水岭——从这一版开始Xilinx把Vivado HLS整合进了Vitis并且在IDE里支持了C仿真、C综合、C/RTL协同仿真等一整套流程。这里有个特别容易迷糊的点你打开Vitis IDE之后看到的工程结构和之前Vivado HLS完全不一样。Vitis采用了“平台应用”的架构你可以先创建一个应用工程然后给它关联一个平台。做纯HLS开发的时候通常选“Empty Application”或者“Hello World”模板然后在src目录里添加你的C/C源文件。我个人建议如果你是第一次接触这个工具链软件版本别追新。2020.2之后的版本对系统要求更高而且有些license策略有变化。2019.2或者2020.1对新手最友好网上资料也多踩了坑好查。2.2 安装过程中的几个关键选择安装Vitis时有个坑你不需要安装全套Vitis HLS可以单独使用但建议把Vivado也一并装上因为后面导出RTL后要在Vivado里做综合实现。选组件的时候注意勾选Vitis和Vivadotarget platform选好对应的开发板比如ZCU104、ZCU106或者Alveo卡。如果只是学习HLS本身本地仿真验证用不了多少空间但Vivado加上Vitis全家桶装下来硬盘至少准备200GB空闲空间这点我踩过坑装到一半磁盘满了非常尴尬。安装完成后命令行里敲vitis_hls就能启动HLS的shell模式。我平时其实更喜欢命令行方式跑HLS因为脚本化之后便于批量回归测试也方便版本管理。第一次学习的话用IDE里的图形界面会更直观能看到波形、调度视图、资源利用率表这些关键信息。2.3 环境变量与license注意事项很多朋友遇到“Failed to obtain license”的问题一般都是环境变量没配对。Vitis默认会去找LM_LICENSE_FILE或者XILINXD_LICENSE_FILE这两个环境变量你装的是node-locked license的话指向本地文件如果是浮动license就指向license服务器。Windows下还要注意环境变量改完必须重启IDE否则不生效。另外一个经常被忽略的点Vitis 2019.2对操作系统有明确要求Ubuntu 18.04、CentOS 7.4、Windows 10 64位都是官方支持的。你要是用Ubuntu 20.04以上跑老版本偶尔会遇到一些动态库兼容问题特别是libtinfo相关的报错。这些坑我都会在后面的问题排查章节里详细展开。3. 第一个例程的设计思路与C代码编写3.1 为什么选择矩阵乘法作为入门例程矩阵乘法是数字信号处理、图像处理、机器学习里最常见的计算核心之一。一个C矩阵乘法是三层循环数据访问模式非常典型A矩阵按行访问B矩阵按列访问结果矩阵逐元素累加。这种访问模式在硬件上有两种截然不同的优化方向——如果你把B矩阵的列数据都搬到片上BRAM里就能实现并行读取如果不做优化那么每次从DDR里读数据都会成为性能瓶颈。拿矩阵乘法做入门还有一个好处它天然适合展示HLS的pragma魔法。一行#pragma HLS pipeline性能就能提升好几倍一个#pragma HLS array_partition片上的存储带宽立刻翻倍。这些效果是立竿见影的比你自己手写RTL再去一点点调时序要有成就感得多。3.2 编写可综合C代码的规范直接写一段粗糙的C代码丢给Vitis HLS是有风险的——它确实能综合但综合出来的硬件可能效率极低甚至功能都不对。HLS可综合代码有几个硬性规范动态内存分配malloc/free不支持系统调用不支持递归函数不支持全局变量的使用要特别小心。下面是我们的第一个例程代码我加了些注释这个版本能综合但还没做优化设计上先保证功能正确。#include stdio.h #include matrix_mult.h #define N 8 #define M 8 #define P 8 void matrix_mult(int A[N][M], int B[M][P], int C[N][P]) { #pragma HLS INTERFACE m_axi portA offsetslave bundlegmem0 #pragma HLS INTERFACE m_axi portB offsetslave bundlegmem1 #pragma HLS INTERFACE m_axi portC offsetslave bundlegmem2 #pragma HLS INTERFACE s_axilite portreturn for (int i 0; i N; i) { for (int j 0; j P; j) { int sum 0; for (int k 0; k M; k) { sum A[i][k] * B[k][j]; } C[i][j] sum; } } }3.3 接口类型的选择逻辑m_axi接口意味着数据从DDR里取这对应的是SoC平台比如Zynq或Versal上的PS与PL交互场景。s_axilite则是用AXI-Lite总线做寄存器配置主机端可以通过这个接口启动IP核、查询状态。我之前见过很多新手刚开始学HLS时把所有数组都设成m_axi结果性能惨不忍睹。因为m_axi接口的读写延迟很大每次访问DDR都要几百个周期如果算法本身是小规模数据、数据都在片上放得下那用m_axi纯属给自己找麻烦。m_axi适合大数据流场景bram接口适合小数据频繁访问场景ap_none、ap_vld、ap_ack这些则用于控制信号的握手。合理的设计思路应该是片上存储能放下的数据尽量放在片上只有大块数据流才走DDR。拿矩阵乘法来说如果矩阵是8x8、16x16这种小规模全放BRAM里没问题如果到了1024x1024那就必须用m_axi配合数据分块。3.4 理解HLS的调度与绑定写完C代码点C综合工具会经过调度和绑定两个阶段。调度的意思是决定每个操作在哪个时钟周期执行绑定则是决定用哪种硬件资源去实现这个操作。这两个过程是HLS和普通C编译最大的区别——普通编译器只关心指令顺序HLS却要关心“哪个操作用哪个乘法器”“这块数组放在哪块存储里”。这就解释了一个新手常有的困惑为什么同样的C代码换了个pragma性能差距巨大因为pragma直接影响调度器的决策空间。加了#pragma HLS pipeline调度器会把循环里的操作尽量重叠执行但同时也会增加寄存器数量和资源占用。不加pipeline调度器就按顺序依次执行资源占用低但延迟高。这就是HLS里最核心的“面积-性能权衡”。4. C仿真与C综合实操4.1 用testbench验证C代码功能在C综合之前必须先做C仿真。这一步太关键了千万别跳过。C仿真的作用是在纯软件层面验证你的算法逻辑是否正确这时候还没有任何硬件开销跑起来飞快调试也容易。写testbench不复杂就是在main函数里初始化输入数据、调用你的顶层函数、再对比输出结果。下面是配套的testbench代码#include stdio.h #include matrix_mult.h int main() { int A[N][M], B[M][P], C[N][P]; int C_ref[N][P]; // 初始化输入数据 for (int i 0; i N; i) for (int j 0; j M; j) A[i][j] i j; for (int i 0; i M; i) for (int j 0; j P; j) B[i][j] i - j; // 调用待测函数 matrix_mult(A, B, C); // 软件参考计算 for (int i 0; i N; i) for (int j 0; j P; j) { C_ref[i][j] 0; for (int k 0; k M; k) C_ref[i][j] A[i][k] * B[k][j]; } // 比对 for (int i 0; i N; i) for (int j 0; j P; j) if (C[i][j] ! C_ref[i][j]) { printf(Mismatch at (%d, %d): %d vs %d\n, i, j, C[i][j], C_ref[i][j]); return 1; } printf(Test passed!\n); return 0; }4.2 C综合报告怎么读C综合运行结束后会生成一个综合报告里面有几个指标是你必须盯着的Latency延迟是整个函数从开始到结束需要的时钟周期数Interval间隔是两次连续调用之间的最小时间间隔。对流水线设计的模块来说Interval往往比Latency更重要因为它决定了吞吐率。资源利用率里重点看BRAM、DSP、FF、LUT这四项。BRAM管存储DSP管乘加运算FF和LUT则消耗在控制逻辑和数据通路上。用上面这段代码综合一次约束设100MHz你大概会看到Latency在几千个周期DSP用了2个或4个BRAM若干。这个性能谈不上好但它是正确基线。后面做优化时你会看着这些数字一步步下降那是HLS学习过程中最有成就感的时刻。4.3 调度视图里的门道综合完不要急着关掉花点时间看看调度视图。在Vitis HLS的IDE里打开Schedule Viewer能看到每个时钟周期里工具安排了哪些操作。你会惊讶地发现工具其实非常“笨”——它严格遵守C语言的依赖关系你写的是顺序执行的代码它就老老实实地按顺序安排操作直到你告诉它可以并行、可以流水线、可以打乱数据访问顺序。调度视图能直观地告诉你性能瓶颈出在哪是不是乘法器资源不够导致排队是不是BRAM端口冲突导致读写等待是不是循环之间的依赖关系阻断了流水线的启动。这些信息比猜要准确得多。5. 优化实战让性能起飞5.1 pipeline性能提升第一板斧没有优化的矩阵乘法内层循环是顺序执行的每次乘法累加都要等前面的操作完成整个循环延迟是各次迭代延迟的累加和。加上#pragma HLS pipeline之后工具会让循环的迭代在硬件上重叠执行也就是第i次迭代的乘法和第i1次迭代的内存读取同时进行。在代码里内层循环加pipeline是这样写的for (int k 0; k M; k) { #pragma HLS pipeline sum A[i][k] * B[k][j]; }加了pipeline之后再看综合报告Latency会有非常明显的下降。但你可能也会注意到有时候虽然加了pipeline工具却报告“unable to pipeline”或者pipeline的启动间隔Initiation Interval简称II没有达到1也就是每个时钟周期没能启动一次新的迭代。常见的瓶颈有两个一是数组访问冲突二是循环内的依赖链。数组访问冲突的意思是BRAM通常只有两个读/写端口如果循环体里同时从同一个数组读两个不同位置工具就得插入额外的等待周期。解决方法是做数组分区这个我下面讲。循环内依赖链的问题则常常出现在累加操作上——每次累加都要等上次累加完成这就构成了一根长长的串行链。解决思路是“部分展开树形归约”或者改变累加顺序让多个部分和并行计算。5.2 array_partition破解存储带宽瓶颈BRAM的一个典型特性是只有两个访问端口但我们的内层循环里每次迭代要读A[i][k]、读B[k][j]、写C[i][j]对数组A和B的访问频率都超过了BRAM的端口供给能力。这时候要使用array_partition指令把一个BRAM拆分成多个更小的BRAM每个小BRAM有独立的访问端口从而提高并行读写能力。#pragma HLS ARRAY_PARTITION variableA cyclic factor8 dim2 #pragma HLS ARRAY_PARTITION variableB block factor8 dim1cyclic是按循环方式切分适合A矩阵按行访问的模式block是按连续块切分适合B矩阵按列访问的模式。选择哪个策略取决于你的数据访问模式。这正印证了那句话HLS优化的本质是让数据在正确的时间出现在正确的位置。5.3 循环展开与数据复用循环展开unroll是另一大杀器。#pragma HLS unroll factor4会让工具生成4份循环体副本并行执行。循环展开往往和pipeline配合使用展开之后loop的迭代次数变少但每次迭代的工作量变大配合array_partition提供足够的存储带宽性能可以呈现近似线性的提升。矩阵乘法还有一个数据复用上的优化思路如果你在i、j层循环内对k做流水线那么A[i][k]会被反复使用P次每个j都要用一次B[k][j]会被反复使用N次。如果能把数据缓存在寄存器或小规模BRAM里就能减少对主存储器的访问次数。更高级的做法是用“分块tiling”把大矩阵拆成小块让数据在片上Cache里反复使用这也是GPU上优化GEMM的经典思路在HLS里同样适用。5.4 优化效果对比表为方便理解我把不同优化策略的效果整理成表测试条件8x8矩阵乘法100MHz时钟优化策略Latency周期DSP使用BRAM使用说明无优化695724基线版本内层pipeline110524吞吐率大幅提升pipelineunroll factor258146面积换性能pipelineunroll factor433188需要更多DSPpipelinearray_partition583212存储带宽优化注意这个表只是示意性的具体数值跟Vitis HLS版本、约束条件都有关系但趋势是正确的无优化到pipeline是最划算的一步后面每一步都是面积和性能的交换没有免费的午餐。6. 导出RTL并集成到Vivado6.1 Export RTL的正确姿势C综合通过、C/RTL协同仿真也通过之后就可以导出IP了。点菜单里的Export RTL会弹出一个选项框让你选择输出格式。如果你只在Vivado里用选IP Catalog格式如果你要用在Vitis的硬件加速流程里选XCLBIN格式如果是给别的EDA工具用可以选System Generator或者Vivado IP的选项。导出过程中有一个容易翻车的点导出时的latency、接口配置等信息会被打包进IP里如果后续你在Vivado里发现引脚或接口跟你预期不一样多半是这里配置的问题。特别是m_axi接口的地址位宽、突发长度burst length一定要和实际平台匹配。你导出到Zynq上用的IPburst length设在16或32性能会好一些但如果设得过大可能跟PS端的DDR控制器配合不好。6.2 在Vivado里完成IP集成导出完成后打开Vivado创建一个Block Design把刚才导出的IP加进来。如果你是Zynq系列的板子还需要添加Zynq PS核并把HPHigh Performance端口和你的HLS IP的m_axi接口连起来把S_AXI控制端口连到PS的GP主端口上。连线完成后记得Validate Design这一步会检查所有连接的合法性。如果报错最常见的几个原因时钟频率不匹配、复位信号没连接、接口位宽不一致。HLS IP的时钟默认是10ns100MHz如果PS端的时钟配置不是100MHz你就需要加一个Clock Wizard做时钟转换或者用PLL分频得到一致频率。6.3 上板验证的软件侧准备如果你用的是SoC平台上板验证时还需要在PS端写驱动。Vitis HLS生成的IP通常有对应的软件驱动在导出目录的drivers文件夹下能找到。驱动文件名一般是ipname_hw.h和ipname_hw.c里面定义了寄存器映射和基础操作函数。驱动层的逻辑其实不复杂先把输入数据写到DDR的指定地址然后通过AXI-Lite寄存器告诉IP“输入数据的地址在哪、数据规模多大”再写一个控制寄存器启动IP核最后轮询状态寄存器判断IP是否执行完成完成后从DDR读回结果。这段软件流程和HLS设计本身是解耦的你只要保证DDR里的数据布局跟IP里m_axi接口的地址分配一致就行。很多朋友第一次跑通硬件加速流程后感慨“原来FPGA开发也不全是硬件活”这话没毛病——在Vitis时代软硬件协同开发才是常态。7. 常见问题与排查技巧实录7.1 DLL初始化失败的Windows杀手关键词里提到的OSError: [WinError 1114] DLL初始化例程失败是Windows环境下特别经典的一个问题。这个报错经常出现在启动Vitis HLS图形界面或者运行C仿真的时候原因是某些DLL依赖的运行时库损坏或者版本不匹配。排查思路分三步第一步确认VC Redistributable是否安装完整Vitis 2019.2依赖VC 2015-2019运行库缺了它就会出现DLL初始化失败第二步检查显卡驱动Vitis IDE基于Eclipse和SWT框架部分老旧显卡驱动会导致图形库初始化失败这种场景下把显卡驱动更新到最新版本基本能解决第三步检查杀毒软件有些安全软件会把Vitis生成的临时DLL拦截掉导致加载失败把Vitis安装目录加入白名单就行。7.2 Vitis打开已有工程的各种姿势很多人的常规操作是用IDE里的File-Open Project去打开一个.source.yml或者.xpr文件但请注意Vitis HLS和Vivado HLS的工程文件格式不同。Vitis 2019.2的HLS工程文件后缀其实是.hlsproj或者目录里的hls_config.cfg。如果你手头只有老版本Vivado HLS的.zip文件最稳妥的办法是用IDE里的“Import Project”功能让它自动识别并转换。遇到打不开工程的问题时还有一个土办法直接用命令行打开。终端里进到工程目录运行vitis_hls -p project_dir它会读取目录里的配置文件并启动GUI。如果连命令行都打不开大概率是配置文件里的路径信息损坏了检查一下hls_config.cfg里的绝对路径是否还指向正确的位置。7.3 C综合速度慢到怀疑人生C综合慢一种情况是代码本身循环次数巨大且依赖复杂调度器要花大量时间探索最优解另一种情况是你无意中触发了“设计空间探索”Vitis会尝试多组pragma组合找到最优方案。解决办法有几种限制循环展开和pipeline的范围不要对最外层大循环直接unroll合理使用#pragma HLS latency约束工具的搜索空间如果只是验证功能可以直接跳过C综合在C仿真阶段先把逻辑搞定。另外Vitis HLS支持多核并行综合在新版本中可以在工程设置里开启并行编译选项多核CPU的机器上效果明显。提示HLS的C综合报告在优化后一定要对照看特别是“Trip Count”和“Iteration Latency”这两项。前者告诉你循环一共迭代多少次后者告诉你每次迭代需要几个周期。瓶颈往往藏在这两个数字的乘积里。7.4 协同仿真挂死怎么破C/RTL协同仿真有时候会卡在某一阶段进度条一直不动。以前我用Vivado HLS的时候这个情况比较少见但Vitis HLS里却经常出现。最常见的原因是仿真波形记录功能被开启全量记录会让仿真数据量大到爆尤其是高延迟的m_axi接口访问记录DDR总线的波形动辄几十GB。遇到挂死先别急着杀死进程先检查sim目录下生成的波形文件大小。如果确实是因为波形太大可以在协同仿真设置里关闭波形记录或者只记录顶层信号、不记录子模块信号。另一个可能原因是testbench里写死了循环等待条件导致仿真永远等不到结束信号这种情况就要检查testbench里的握手逻辑了。8. 从第一个例程到真实项目的进阶路径第一个例程跑通之后你可能会觉得“原来HLS就这么回事”。别急矩阵乘法只是入门真实工程里的瓶颈和坑会多得多。我的建议是例程之后你至少应该尝试三件事第一把矩阵规模从8x8改成1024x1024看看数据分块tiling能带来多大的性能变化这会逼你去真正理解m_axi的突发传输和DDR带宽第二试着加入双缓冲double buffer机制让数据搬运和计算重叠执行这个技巧在流水线型设计中几乎是必备技能第三把资源约束比如DSP数量、BRAM容量设到一个相对紧张的值看工具如何做面积优化这对你理解资源开销非常有帮助。这三件事做完你对HLS的理解深度会远超那些只会跑教程示例的人。HLS的学习曲线确实有个陡峭的上升段但一旦跨过去你会发现用C家族语言做硬件设计、做算法加速思路和效率都是传统RTL设计难以比拟的。我个人在实际操作中的体会是HLS的代码风格养成比C语言本身更重要。我是说你的C代码是给人读的但HLS的C代码既要给人读又要给综合器读。写的时候脑子里就要有硬件画面——这段代码会综合出什么电路这个循环会产生多少状态这片数组会放进BRAM还是LUTRAM有了这种意识你的HLS代码质量和优化效果会同步往上走。最后一个建议做HLS千万不要只盯着Latency一个指标。资源占用、功耗、接口带宽、可维护性这些在实际工程项目里都举足轻重。矩阵乘法这个例子虽然简单但如果你能把每一项指标都分析透彻以后遇到任何算法加速需求你都不会手足无措。

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

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

免费获取报价