十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

重新审视Vivado HLS:从C到RTL的硬件设计实践

重新审视Vivado HLS:从C到RTL的硬件设计实践 从 2020 年开始我把大量的 FPGA 开发时间从 RTL 转移到了 Vivado HLS 上期间经历了几次从这工具不能用到这工具真香的心态循环。最近又把旧工程翻出来重新梳理了一遍结合这半年做的新项目我发现很多关于 HLS 的判断都需要修正——所以有了这篇 Revisiting Vivado HLS 的记录。我会把 HLS 的定位变化、综合机制里容易被误解的部分、实际项目的完整流程以及我在调试过程中踩过的坑一起写出来给正在犹豫要不要入坑的朋友一些参考。1. 为什么重新审视HLS 的定位这几年发生了明显变化1.1 从 RTL 思维到 C 思维换个角度看硬件设计有人说 HLS 就是把 C 编译成 Verilog这个说法害了一批人。如果只是翻译那工具生成的 RTL 质量肯定不如手工设计。HLS 真正做的事情是从 C/C 描述中提取数据流和控制流再根据你设定的时钟周期和优化指令去调度这些运算并绑定到具体的硬件资源上。这里有个很关键的思维转变写 RTL 时你脑子里必须有明确的时序图状态机数据通路写 HLS 时你脑子里要有的是算法流程并行度访存模型。换句话说RTL 是告诉硬件每一步怎么走HLS 是告诉工具最终要算什么至于中间怎么调度工具会替你决定一部分。这和当年从汇编转向 C 语言很像。汇编程序员总觉得 C 编译器生成的代码不够干净但工程规模大到一定程度C 带来的开发效率是手写汇编无法比拟的。HLS 和 RTL 的关系也类似小模块、强时序约束的场景RTL 依然是王道但算法复杂、参数频繁调整的场景HLS 的迭代速度确实有明显的优势。1.2 Vivado HLS 到 Vitis HLS工具本身也在迭代很多资料还是老版本开口就是Vivado HLS 2018.3 怎么样实际上 Xilinx现在算 AMD在 2019.2 版本之后就把独立的 Vivado HLS 整合到了 Vitis 统一开发环境里改名为 Vitis HLS。也就是说如果你现在装的是 2020.1 以上的 Vivado在启动界面里看到的 HLS 工具已经是 Vitis HLS 了。这个变化不只是改了个名字。Vitis HLS 的编译器前端用到了更新的 LLVM 版本对 C 标准的支持更好支持 OpenCL、HLS 库函数也更加丰富。最直观的感受是同样一段带有复杂模板和 lambda 表达式的 C 代码老版本 Vivado HLS 直接报错新版本 Vitis HLS 能正常综合。所以如果你在网上搜到 2018、2019 年的教程先确认一下它讲的界面和命令是否适用于你当前版本。版本差异导致的工程兼容性问题我在后面会专门讲。另外安装环节也经常有人卡住特别是 Windows 下安装 2020.2 之后的版本文件非常大下载时间长安装过程中还容易遇到winpcap 安装失败或license 文件无法加载这类问题。我的建议是在 AMD 官网注册后直接下载离线安装包别用网页安装器否则中断重来特别痛苦安装路径不要带中文和空格尽量放在纯英文目录下。1.3 哪些项目适合 HLS哪些不适合经过这几年实践我总结了一个比较务实的判断标准不一定绝对但可以作为参考维度适合 HLS不适合 HLS算法类型信号处理、图像处理、AI 推理、通信基带复杂状态机、自定义总线协议、精确时序控制数据特征流式数据、规则访存随机访问、强依赖动态分支迭代频率算法参数频繁调整接口/时序一旦确定基本不动性能需求吞吐量为主latency 次之极端时序要求、超低延迟团队背景软件工程师主导算法硬件工程师经验丰富且人手充足我见过做得最顺的 HLS 项目是一个图像 ISP 流水线算法从 MATLAB 原型直接移植到 C然后加 HLS 指令调优化整个软硬件迭代周期压缩了一大半。但我同样见过把 HLS 用在 DDR 控制器调优上的翻车案例折腾了两个月性能都上不去最后回到 RTL 一周解决。别迷信工具也别一棒子打死要看你手里到底是什么类型的问题。2. HLS 综合机制拆解它是怎么把 C 变成硬件的2.1 C/C 到 RTL 的映射过程很多人好奇 HLS 综合的时候内部到底干了什么。简单说分成三步调度scheduling、绑定binding、控制逻辑提取control logic extraction。调度解决的是某个运算在第几个时钟周期执行的问题。比如一段循环里有一条乘法和一条加法如果目标时钟频率比较宽松工具可能让它们在同一个周期并行执行如果时钟频率很紧就必须拆成两个周期。这个过程类似于工厂排产要考虑每台设备什么时候空闲、哪个工序必须先完成。绑定解决的是某个运算用哪种硬件资源实现的问题。乘法可能用 DSP48E也可能用 LUT 拼出来的乘法器数据可能存在 BRAM也可能存在触发器阵列。工具会根据资源约束和性能目标自动选择。最后工具把所有状态机的控制逻辑补全生成 RTL。所以你们在 Vivado 里看到 HLS 导出的 RTL 有一大堆 FSM 和握手信号就是这个阶段产生的。我用一个做菜的例子来类比RTL 开发是你亲自给每个厨师排班规定谁切菜、谁掌勺、谁洗碗每一步都在什么时间做HLS 开发是你只告诉餐厅经理今天要出 100 道宫保鸡丁经理自己去安排火力大小、灶台分配、人员调度。效率高不高取决于经理对厨房的了解程度——也就是你对 HLS 指令的理解程度。2.2 优化指令不是万能的HLS 最常用的指令无非这么几个PIPELINE、UNROLL、ARRAY_PARTITION、DATAFLOW、INTERFACE。但很多人包括当年的我以为加上 PIPELINE 就能自动提升吞吐量结果综合完一看latency 没降多少资源倒是涨了一倍。这里有一个关键原因内存带宽瓶颈。拿 FIR 滤波器举例如果你对系数数组不进行 ARRAY_PARTITION那么所有访问都得通过这个数组的 BRAM 端口来读写一个时钟周期最多读两个数据双端口 BRAM。这时候就算你给内部循环加了 PIPELINE但由于访存端口不够数据喂不进来流水线照样跑空。就像餐厅里厨师再快传菜窗口一次只能出一盘菜后面全堵着。所以正确做法是先分析算法里哪些数据访问是并行瓶颈针对性地做 ARRAY_PARTITION 或 RESHAPE让数据能并行读出来再上 PIPELINE。我在后面实践部分会给出可运行的例子。另外DATAFLOW 指令也不是能随便加的。它用在多个 function 之间有数据流传递的场景可以让函数之间像流水线一样重叠执行。但如果函数之间是带反馈的循环依赖或者数据传递需要完整数组同步那 DATAFLOW 可能反而导致错误需要通过 FIFO 方式重新设计接口。2.3 接口综合是 HLS 最容易踩雷的地方接口综合Interface Synthesis是 HLS 和普通 C 编译器差异最大的部分。普通 C 程序的函数参数只是内存地址或者值但 HLS 里每个顶层函数参数都会被映射成具体的硬件端口协议。最常见的三种ap_none裸端口、ap_vld/ap_ack握手信号、m_axiAXI 主接口。如果你写了一个顶层函数参数是指针类型并且没有显式指定 INTERFACE 指令工具会默认帮你生成 m_axi 接口——即使你只打算把这个数组放到片上 BRAM 里。这会导致综合出来的 RTL 资源占用巨大因为你挂上了一个完整的 AXI 总线主设备。我调过好几个类似的工程资源莫名其妙的爆炸最后发现就是接口类型选错。所以每次写顶层函数前先明确你的数据从哪来、到哪去从 PL 端寄存器访问用 s_axilite从 DDR 通过 DMA用 m_axi从流式接口进来用 axis。不同接口的握手逻辑和时序差别很大最稳妥的办法是一开始就写清楚 INTERFACE pragma而不是依赖工具默认值。还有个常见现象Vivado 综合端口名字被优化意味着什么其实这是因为 HLS 或综合器判断某个端口没有参与实际逻辑或者被常量折叠、被优化掉了。解决办法通常是在 HLS 里加上 ap_ovld 或者把端口定义为 volatile让工具认为它有副作用在 Vivado 里则可以给信号加 keep 或 DONT_TOUCH 属性。注意端口被优化掉不一定是坏事只要它确实没有接入逻辑优化反而是正常的。真正要小心的是你期望它暴露出来做调试结果它消失了那就要检查是不是方向写反了。3. 一个可复现的实践把一段 C 算法变成 Vivado 里的 IP3.1 环境与工程准备我用的是 Vivado/Vitis HLS 2022.2 版本开发板是 Zynq UltraScale ZCU104。如果你手头板子不同没关系Part 选择可以改成你自己的器件不影响工程结构。打开 Vitis HLS老版本界面叫 Vivado HLS入口略有差异Create New Project给工程起名fir_hls_prj路径选纯英文目录。指定顶层函数fir_filter。添加源文件fir.cpp和测试文件fir_tb.cpp。选择器件ZCU104 对应xczu7ev-ffvc1156-2-e或者直接搜索型号。创建完成后先跑一遍 C Simulation确认算法功能正确。这一步常见的问题是如果你用的器件型号是 Zynq UltraScale 系列Vitis HLS 的器件数据库更新很频繁老版本 Vitis HLS 可能识别不了新器件直接换个新版本即可不用浪费时间折腾。3.2 编写可综合 C 代码的注意事项HLS 不是万能的并不是所有 C/C 代码都能综合成硬件。我建议从源头上避开这些写法动态内存分配malloc/new、递归、系统调用printf 在综合时会忽略但在仿真时可用最好加#ifndef __SYNTHESIS__宏包住、浮点动态精度操作、多级指针。这些在 RTL 里没有对应物工具要么报错要么综合出来的资源异常。下面是一个经典 FIR 滤波器的 HLS 可综合代码#define N 128 #define TAPS 32 void fir_filter(const int input[N], int output[N], const int coeff[TAPS]) { #pragma HLS INTERFACE m_axi depthN portinput offsetslave #pragma HLS INTERFACE m_axi depthN portoutput offsetslave #pragma HLS INTERFACE s_axilite portreturn static int shift_reg[TAPS]; #pragma HLS ARRAY_PARTITION variableshift_reg typecomplete for (int i 0; i N; i) { #pragma HLS PIPELINE shift_reg[0] input[i]; for (int t TAPS - 1; t 0; t--) { shift_reg[t] shift_reg[t - 1]; } int acc 0; for (int t 0; t TAPS; t) { acc shift_reg[t] * coeff[t]; } output[i] acc; } }这段代码有几个点需要注意shift_reg是内部数组代表硬件里的移位寄存器。我加了#pragma HLS ARRAY_PARTITION typecomplete把它拆成一个个独立的寄存器避免访问端口冲突。input、output用了m_axi接口因为数据量是 128 个 int不适合全部放到 BRAM 里作为寄存器访问走 AXI 从 DDR 读取更合理。s_axilite portreturn用于控制模块启停方便软件通过寄存器控制。如果要进一步加速可以把内层累加循环#pragma HLS UNROLL但要注意乘法器数量是否足够。32 个乘加运算同时并行在 ZCU104 上资源没有问题换小器件就要小心。3.3 综合、C/RTL 协同仿真与导出写完代码后依次执行C Simulation用fir_tb.cpp里的测试数据验证功能。这一步很快先确保算法逻辑对。C Synthesis综合生成 RTL。这个过程会输出调度结果、资源使用估计、latency 和 interval 等信息。C/RTL Co-simulation把综合后的 RTL 放到仿真器里和 C 测试平台联合跑一遍。这一步比 C Simulation 慢得多但能验证接口时序是否正确。Export RTL导出成 Vivado 可用的 IP 核。Vitis HLS 2022.2 支持导出成.xci文件或者打包成.zip。在 C/RTL 协同仿真时我遇到过好几次xsim 43-3294 signal exception_access_violation received这个报错看起来很吓人实际上多数是仿真器编译产生的临时文件不干净或者代码里有一段非法指针。解决方法是先清理工程里的sim目录重新跑仿真如果还不行把协仿真的dump trace选项关掉减少仿真器负担。导出 IP 后在 Vivado 工程里通过Add IP把生成的.xci添加进去连接时钟、复位和 AXI 接口。如果你只是测试可以给s_axilite接口挂一个 AXI GPIO 或者直接连到 Zynq PS 的 M_AXI_GP 口上用 SDK/Vitis 软件写寄存器触发。3.4 导入到 Vitis 工程时的同步问题热词里有人问重新修改 Vivado 程序并且导出新的比特流到 Vitis 工程里面注意事项这个确实容易踩坑。当你改完 HLS 生成的 IP或者改了 Vivado 里的 RTL重新综合生成比特流后一定要在 Vivado 里重新Export Hardware生成新的.xsa文件老版本是.hdf然后再导入到 Vitis 工程。如果你只是把工程里的比特流直接覆盖掉而不重新生成.xsaVitis 里的硬件描述文件还是旧的寄存器地址映射可能对不上最终启动时程序跑飞或者寄存器读写无效。我一开始就吃过这个亏改完 RTL 直接替换了 bit 文件结果 Vitis 里加载的驱动基地址还是老的调试了半天才发现是硬件描述文件没同步。另一个注意点是导出 IP 时最好把所有优化的 HLS 工程和 Vivado 工程放在同一个工作区里管理用相对路径引用。如果工程被移动了位置最好在 HLS 里重新Export RTL否则 Vivado 会找不到 IP 的源文件一旦清理缓存问题就暴露了。4. 实测中遇到的那些坑完整排查链路4.1 浮点运算导致性能落差一个真实的 latency 异常有一个做雷达信号处理的工程算法里大量使用 float 类型做乘法累加。综合后看报告latency 高得离谱一个 64 点的复数乘法累加竟然要 2000 多个时钟周期。我排了很久才意识到问题float 运算在 HLS 里综合出来的不是简单乘法器而是浮点运算 IP 核比如floating-point multiplier、floating-point adder。这些 IP 核本身有几十个时钟周期的延迟再叠加流水线依赖整个循环的迭代间隔II就拉得很长。排查过程如下先看综合报告里的Loop部分定位到具体是哪个循环耗时最长。把那个循环里的运算换成ap_fixed16,6定点类型重新综合对比。结果发现同样的算法用定点类型后 latency 从 2000 降到 200 左右资源还少了一半。所以如果你的算法对精度要求不是那么苛刻尽量用定点数。HLS 的ap_fixed类型设计得很好舍入、饱和这些模式都可以配置大部分信号处理场景完全够用。浮点留到需要动态范围非常大的场景再用但要想清楚代价。4.2 数组分区之后反而变慢带宽与布线的一个平衡我在另一个图像卷积核加速工程里发现ARRAY_PARTITION加多了综合时间变长时序还变差了。这就是内存带宽提升了但布线拥塞了的典型情况。把所有数组都按complete分区意味着每个元素都变成一个独立的寄存器读写端口无限多理论上并行度最高。但 FPGA 的布线资源是有限的寄存器之间距离太远信号传播延迟就大反而把时钟频率拖下来。而且综合器面对海量寄存器连接关系运行时间会指数级上升。我的解决思路是用typecyclic factor2或者typeblock factor4这种部分分区方式只对最外层循环里真正需要并行访问的维度做分区。例如卷积分两个方向只需要横向或者纵向并行就只拆对应的维度其他维度保持块状存储。综合时间、布线拥塞和性能之间要有一个性价比取舍不是拆得越狠越好。4.3 协同仿真通过但上板失败一个接口协议问题这是最让人头疼的一类问题C/RTL 协同仿真明明全对但下到板子上跑起来数据就是不对。我当时遇到的具体情况是一个 AXI4-Lite 寄存器接口的 HLS IP软件通过 PS 写寄存器IP 计算完后再把结果写回寄存器。仿真全过上板后在寄存器的某个 bit 上读到随机值。排查链路如下先用 Vivado 的 Hardware Manager 抓取s_axilite总线上的读写时序发现 PS 发出的地址和 HLS 期望的地址差了一个偏移。回到 Vitis 工程查看xparameters.h里的寄存器基地址发现基地址和 Vivado 里 Address Editor 设置的地址不一致。为什么不一致因为我重新导出了硬件描述文件但 Vitis 工程里的platform没有同步更新还在用旧地址。所以这里我强烈建议遇到上板不一致的问题先查接口地址映射再查数据通路。很多人一上来就怀疑 HLS 代码逻辑有问题实际上大部分问题出在系统集成环节。同时在 HLS 代码里对于调试想看的中间量可以用单独的寄存器接口暴露出来否则你只能通过对最终结果反推效率太低。4.4 安装与版本兼容性问题的若干琐碎经验Vivado 安装到一半无反应多数是磁盘空间不够或者安装包缓存路径有问题。我习惯给安装器设置一个单独的临时目录并且确认可用空间在 100GB 以上否则很容易中断。vivado 2020.2 综合失败Messages 没有错误信息这个诡异问题遇到过一次原因是工程路径里有父目录权限不够导致综合器写不了中间文件。把整个工程移动到用户目录下重新编译即可。2018.3 工程拿到 2022.2 打开低版本 IP 核需要升级有些自定义 IP 没有兼容版本时会直接报错。建议升级前对原工程做备份逐批升级 IP不要一次性全量升级。HLS 里用到了 C11/14 特性老版本编译器报with the -stdc0x or -stdgnu0x compiler相关错误只需要在 Vitis HLS 的编译选项里加上-stdc11即可新版本 2021.1 以上默认就支持 C17省事很多。vitis hls 界面没有找到某个指令不同版本对 pragma 名称有微调比如老版本的#pragma HLS interface新版本也支持但推荐用向导添加避免手写拼错。5. HLS 与 RTL 的边界什么时候应该放弃 HLS5.1 时序收敛是绕不开的一关HLS 生成的 RTL 普遍比手工 RTL 更容易出现时序问题特别是当你的设计目标频率在 300MHz 以上时。原因在于 HLS 的调度器通常更关注吞吐量和资源占用对物理布局的感知能力有限生成的逻辑可能分散在 FPGA 的不同区域导致走线延迟过大。如果你在 HLS 综合报告里看到预估的 frequency 不达标不要指望进入 Vivado 后 Pipelining 能拯救一切那是白费功夫。尽早检查代码结构是不是关键路径上有太长的组合逻辑链是不是加法器的位宽太大是不是可以把多个乘法分成两级HLS 的#pragma HLS LATENCY也可以用来给工具指定一个期望的延迟让它按这个目标重新调度有时候会有奇效。但也存在一些 HLS 怎么调都满足不了的场景比如你需要一个接口时序非常苛刻的 DDR 控制器或者一个需要精确到纳秒级的自定义协议响应。这种场景下老老实实用 RTL 写状态机才是正路。5.2 控制密集型逻辑HLS 的短板还是 RTL 的主场HLS 的 C 代码里你很容易写出一堆 if-else、switch-case以及基于某个信号值跳转的复杂状态。这些逻辑映射到 RTL 往往会变成巨大的状态集合综合后 FSM 极其庞大时序和资源都难以保证。而且这类控制逻辑很难通过加指令来优化因为瓶颈在决策顺序和依赖关系不在并行度。相比之下RTL 状态机写起来虽然啰嗦但每个状态、每个跳转条件是显式的做时序约束、定位 bug 都很直接。我自己总结的经验是如果一段算法里 80% 以上是数学运算适合 HLS如果 50% 以上是条件分支和协议状态跳转建议 RTL 或者至少把控制部分拆出来用 RTL 做。5.3 混合方案HLS 做计算RTL 做控制大多数实际工程里最优解往往不是全 HLS或全 RTL而是混合方案。我之前做过一个数据采集与处理系统数据采集、DMA 搬运、命令解析用 RTL 实现保证接口时序的确定性核心的压缩和滤波算法用 HLS 实现方便算法工程师快速迭代参数。HLS IP 通过 AXI4-Stream 接口和 RTL 部分连接两边各自维护风险隔离得很好。如果你打算这么干接口一定要设计得简单清晰HLS 那边尽量用流式接口axis不要暴露太多寄存器控制位。控制层面全部交给 RTL 或 CPU 软件HLS 只负责数据进、数据出这样出问题时定位范围会小很多。6. 复盘后的几点实际建议如果重新让我选工具我不会再纠结HLS 能不能取代 RTL这种问题。工具只是工具重要的是项目里哪个环节最痛。算法开发周期长用 HLS 加速迭代接口时序复杂到焦头烂额用 RTL 稳扎稳打。两者配合效果远好于押注单一方案。从实际效率角度看HLS 对我个人最大的收益不是免写 RTL而是算法和硬件能对表MATLAB/Python 里验证过的算法移植到 HLS 几乎是同一套逻辑沟通成本低了很多。这个价值在算法和硬件由不同团队负责的项目里尤其明显。最后分享一个小技巧也是我现在做每个 HLS 工程都会执行的动作在项目初始化时就写一个性能预算表把目标时钟、延迟、资源上限、接口协议全部列清楚然后挂在工程根目录下。每轮综合后立刻对照一旦某项超标马上就能定位到是哪一次修改引入的。这个习惯帮我省了无数次从头翻 git log的麻烦。如果你正准备开始一个新的 HLS 项目我建议从这一步做起。
返回列表