4. FPGA加速入门:HLS开发流程、流水线设计、定点数优化

FPGA 加速,很多人一听就觉得门槛高。得写 Verilog,得懂时序,还得会看波形图。

说实话,我早年也是这么想的。直到我接触了 HLS(High-Level Synthesis,高层次综合),才发现原来用 C/C++ 也能写 FPGA 逻辑。当然,不是说 HLS 能解决所有问题,但它确实让算法工程师和软件出身的人,有了快速上手 FPGA 的路径。

这一章,我就带你走一遍 HLS 的核心流程。重点聊聊三个东西:开发流程、流水线设计、定点数优化。这三板斧砍下来,你基本就能把一段 C 代码,变成一块能跑在板子上的加速逻辑。

4.1 HLS 开发流程:从 C 到 RTL

HLS 的本质,是把 C/C++ 描述的算法,自动转换成硬件描述语言(VHDL/Verilog)。你想想看,这相当于让编译器帮你写 RTL 代码。

但别高兴太早。编译器没那么聪明,它需要你给它“提词”——也就是用 pragma 指令或者 directive 来指导它。

一个典型的 HLS 开发流程,分五步走:

  1. 算法建模:用 C/C++ 写出功能正确的算法。这一步跟写软件没区别。
  2. C 仿真验证:在 PC 上跑一遍,确保逻辑对。这一步很快,几秒钟就出结果。
  3. 综合(Synthesis):HLS 工具把 C 代码转成 RTL。这一步会生成报告,告诉你用了多少资源、能跑多快。
  4. 协同仿真(Co-simulation):把生成的 RTL 和你的测试激励一起仿真。这一步比较慢,但必须做。
  5. 导出 IP:把生成的 RTL 打包成 IP 核,供 Vivado 等工具调用。

核心要点:HLS 不是“一键生成”。它更像一个翻译官,你需要告诉它“这里要并行”、“那里要流水线”、“这个变量用寄存器”。

我个人习惯,在写 HLS 代码之前,会先画一个数据流图。把输入、输出、中间计算节点都标清楚。这样写 pragma 的时候,心里才有底。

4.2 流水线设计:让数据“流”起来

流水线,是 FPGA 加速的灵魂。没有流水线,你的硬件就跟单核 CPU 一样,一条指令一条指令地跑,浪费了 FPGA 的并行能力。

举个例子。假设你要计算 y = a * b + c。如果不做流水线,硬件会先算乘法,等乘法结果出来,再算加法。这中间,乘法器在忙的时候,加法器在闲着。

做了流水线之后呢?乘法器算完第一组数据,立刻把结果传给加法器,同时乘法器开始算第二组数据。这样一来,乘法器和加法器都在满负荷工作。

在 HLS 里,开启流水线非常简单,加一行 pragma 就行:

// 对循环体做流水线
for (int i = 0; i < N; i++) {
#pragma HLS PIPELINE II=1
    out[i] = a[i] * b[i] + c[i];
}

这里的 II=1 是关键。II 是 Initiation Interval(启动间隔),意思是每隔 1 个时钟周期,就能启动一次新的计算。II=1 是流水线的最高境界,意味着你的硬件达到了“每个周期出一个结果”的吞吐率。

避坑指南:我曾经在一个项目中,为了追求 II=1,把代码改得面目全非。结果综合出来资源爆了,时序也跑不过。后来我学乖了,II 值不是越小越好,要根据你的数据依赖和资源情况来定。II=2 或者 II=3,有时候反而是最优解。

为什么会这样?因为流水线有“气泡”。如果循环体内部有数据依赖,比如当前迭代需要用到上一次迭代的结果,那流水线就必须停下来等。这时候,II 就没办法做到 1。

所以,写 HLS 代码的时候,要尽量消除循环体内的数据依赖。把能提前算的,都提到循环外面去。

4.3 定点数优化:算得快,还省资源

FPGA 做浮点运算,代价很高。一个单精度浮点乘法器,要占几百个 LUT 和 DSP。而一个定点乘法器,可能只需要几十个 LUT。

说白了,能用定点,就别用浮点。这是 FPGA 加速的铁律。

HLS 里,定点数用 ap_fixed<W, I> 来表示。W 是总位宽,I 是整数部分位宽。小数部分位宽就是 W - I。

举个例子:

#include <ap_fixed.h>

// 定义一个 16 位定点数,8 位整数,8 位小数
typedef ap_fixed<16, 8> fixed16_t;

fixed16_t a = 3.25;  // 实际存储为 3.25 * 2^8 = 832
fixed16_t b = 1.5;
fixed16_t c = a * b; // 结果也是定点数

定点数的好处,不仅仅是省资源。它还能让你的设计跑在更高的频率上。因为位宽小了,组合逻辑的路径就短了,时序自然就好。

但定点数也有坑。最大的坑就是 精度丢失 和 溢出。

问题 原因 解决办法
精度丢失 小数位不够,截断导致误差 增加小数位宽,或者做四舍五入
溢出 整数位不够,结果超出表示范围 增加整数位宽,或者做饱和截断
乘法位宽膨胀 两个 W 位定点数相乘,结果需要 2W 位 及时截断,或者用 ap_fixed 的缩放模式

注意:定点数的位宽选择,一定要做仿真验证。我见过有人凭感觉选了 16 位,结果跑出来的结果跟浮点版本差了 10%。后来一查,是小数位不够,累积误差太大了。所以,先仿真,再综合,这个顺序不能乱。

我个人习惯,在做定点数优化时,会先写一个 Python 脚本。用 Python 的 numpy 模拟定点数的行为,把位宽、截断模式、饱和模式都试一遍。找到最优的位宽组合后,再移植到 HLS 代码里。这样能省很多综合时间。

4.4 知识结构总览

下面这张图,把 HLS 开发、流水线、定点数优化的核心关系画出来了。你可以把它当作本章的“地图”。

FPGA HLS 加速核心知识结构 FPGA HLS 加速核心知识结构 HLS 开发流程 流水线设计 定点数优化 资源与性能 PIPELINE pragma II 值优化 消除数据依赖 ap_fixed<W, I> 位宽与精度权衡 溢出与截断处理 LUT / DSP / BRAM 时序收敛 吞吐率 vs 延迟

从这张图你能看到,HLS 开发流程是骨架,流水线设计和定点数优化是两条腿。资源与性能是最终的目标。三者缺一不可。

4.5 实战建议:从一个小例子开始

如果你刚接触 HLS,我建议你别一上来就搞复杂的神经网络。先拿一个 FIR 滤波器或者矩阵乘法练手。

步骤很简单:

  • 先写 C 代码,跑通功能。
  • 加 PIPELINE pragma,看 II 值能不能到 1。
  • 把浮点改成定点,对比精度和资源。
  • 最后综合,看时序报告。

嗯,这里要注意。HLS 工具生成的报告,有时候会“骗人”。它说能跑 200MHz,但放到 Vivado 里布局布线后,可能只能跑 150MHz。所以,一定要做布局布线后的时序仿真,别只看 HLS 的报告。

我记得有一次,HLS 报告说资源用了 30%,结果布局布线后,BRAM 爆了。原因是 HLS 把数组综合成了分布式 RAM,而 Vivado 把它映射到了 BRAM 上。从那以后,我每次都会在综合后,手动检查一下资源映射情况。

一句话总结:HLS 是工具,不是魔法。你越懂硬件,HLS 给你的回报就越大。流水线让数据流起来,定点数让资源省下来。这两件事做好了,你的 FPGA 加速就成功了一大半。