3. HLS核心语法与C++子集

Quartus HLS 并不是一个“什么 C++ 都能综合”的通用编译器,它接受的是一个经过严格裁剪的 C++ 子集。理解这个子集的边界,比记住某条语法本身更重要:写进去的代码必须能被映射成确定的硬件结构——寄存器、组合逻辑、存储器和接口协议。凡是无法在编译期确定硬件形态的写法,通常都会被拒绝或产生不可预期的结果。

一、支持的 C++ 特性:以“可静态展开”为原则

Quartus HLS 支持 C++ 的基本控制流与函数抽象,但要求这些结构在综合阶段能够被完全展开或静态确定。具体来说:

  • 基本类型与运算:int、short、char、bool、float、double 以及 ap_int<N>、ap_uint<N>、ap_fixed<W,I> 等任意精度类型均可使用。算术、比较、位运算、移位、三目运算符都支持。
  • 控制流:if/else、for、while、switch/case、break、continue 都支持,但循环边界最好在编译期可确定,否则会生成较大的状态机或无法流水。
  • 函数与模板:普通函数、内联函数、函数重载、模板函数、模板类都支持。模板参数在实例化时确定,因此常用于参数化位宽和数组深度。
  • 数组与结构体:定长数组、结构体、struct 嵌套、typedef 都支持。数组会被映射为片上存储器或寄存器组,深度和位宽直接影响资源。
  • 类与命名空间:支持类、成员函数、构造函数、命名空间,但类通常用于封装算法模块,不建议使用虚函数和多态。

这些特性的共同点是:编译期可以确定每个变量、每个分支、每次循环迭代对应的硬件资源。例如一个 for (int i = 0; i < 8; i++) 的循环,如果循环体内没有跨迭代依赖,HLS 可以将其完全展开为 8 份并行逻辑,从而在一个时钟周期内完成 8 次运算。

二、不支持或需谨慎使用的特性

以下特性在 Quartus HLS 中通常不被支持,或者虽然语法上能通过,但综合结果与软件语义不一致:

  • 动态内存分配:new、delete、malloc、free 无法映射为硬件,必须用定长数组或 hls::stream 替代。
  • 虚函数与运行时多态:虚函数表需要运行时查表,硬件中无法高效实现,应改用模板或函数重载在编译期确定。
  • 递归:递归深度在编译期无法确定,会生成不可综合的调用栈,应改为迭代或显式展开。
  • 异常处理:try/catch/throw 没有对应的硬件语义,应通过返回值或状态标志传递错误。
  • 标准库容器:std::vector、std::map、std::string 等依赖动态内存和复杂运行时,不能直接使用。
  • 指针与动态索引:指针运算、指针数组、运行时决定的数组下标,会导致无法确定访问哪个存储单元,应改为编译期常量索引或 hls::stream 的顺序访问。
  • 浮点除法和三角函数:虽然 float 支持,但除法、sin、cos 等会消耗大量 DSP 和逻辑资源,建议用定点数或查表替代。
特别注意:printf、cout 等 I/O 语句在综合时会被忽略,只能用于 C 仿真阶段调试,不能作为硬件行为的一部分。

三、数据类型:位宽决定资源

在 HLS 中,数据类型的选择直接决定硬件资源。默认的 int 是 32 位,如果实际只需要 8 位,就会浪费 24 位寄存器或布线资源。因此,推荐使用任意精度类型:

类型位宽典型用途
ap_uint<8>8 位无符号像素值、小范围计数器
ap_int<16>16 位有符号音频采样、传感器数据
ap_fixed<16,8>16 位定点,8 位小数滤波系数、增益控制
ap_uint<1>1 位布尔标志、使能信号

使用 ap_fixed<16,8> 表示范围约 [-128, 127.996],精度为 1/256 ≈ 0.0039。相比 float 的 32 位,定点数在乘法器中可以节省大量 DSP 资源,且延迟更可预测。

四、接口类型:决定模块如何与外界通信

Quartus HLS 通过接口类型将 C++ 函数参数映射为硬件端口。常见的接口类型包括:

  • 寄存器接口:标量参数(如 int gain)默认映射为寄存器,由主机通过 Avalon-MM 或 AXI-Lite 写入。
  • 存储器接口:数组参数(如 int data[256])映射为 Avalon-MM Master 或 AXI4 Master,HLS 自动生成地址、读使能、写使能等信号。
  • 流接口:hls::stream<T> 映射为 AXI4-Stream,适合连续数据流处理,支持 read() 和 write() 阻塞式访问。
  • 无接口:hls::axis 等打包类型可携带 last、keep 等边带信号。

接口选择直接影响系统集成方式。例如,图像处理流水线通常用 hls::stream 连接多个 HLS 模块,形成无地址的流式数据通路;而需要 CPU 配置系数的模块,则用寄存器接口接收参数。

Quartus HLS 接口映射与数据流 主机 / CPU Avalon-MM / AXI-Lite 寄存器接口 gain, threshold, mode HLS 算法模块 ap_int / ap_fixed 运算 流水线 / 展开 / 状态机 hls::stream 读写 数组 → 片上存储器 存储器接口 AXI4 Master / DDR 流接口 AXI4-Stream 配置 参数 读写 数据流 ✓ 支持 • 基本类型 / ap_int / ap_fixed • if/for/while/switch、模板、结构体 • 定长数组、hls::stream、类与命名空间 ✗ 不支持 / 慎用 • new/delete/malloc、虚函数、递归 • try/catch、std::vector/map/string • 指针动态索引、浮点除法/三角函数

五、基本编码规范:让综合结果可预测

除了语法边界,编码风格也直接影响 HLS 的综合质量。以下规范建议在项目初期就统一执行:

  1. 循环边界尽量为常量:for (int i = 0; i < N; i++) 中的 N 用 const 或模板参数定义,便于 HLS 展开和流水。
  2. 避免跨迭代依赖:如果循环体内第 i 次迭代依赖第 i-1 次的结果,HLS 无法并行展开,会退化为串行状态机。此时应显式使用 #pragma HLS dependence 或重构算法。
  3. 数组访问用常量索引:data[i] 中 i 为循环变量时,HLS 可以推断出存储器结构;若 i 来自运行时输入,则可能生成多路选择器或无法综合。
  4. 接口参数显式声明:使用 #pragma HLS interface 明确指定 ap_memory、axis、s_axilite 等接口类型,避免 HLS 自动推断出不符合系统要求的端口。
  5. 位宽按需定义:累加器、计数器、地址位宽都应根据实际范围选择 ap_int/ap_uint,不要一律用 int。
  6. 函数粒度适中:每个 HLS 函数对应一个硬件模块,函数过大导致资源紧张,过小则接口开销占比高。建议按数据流阶段划分函数。
一个实用的检查方法:在 C 仿真通过后,先看 HLS 报告的“循环是否被展开”“数组是否被映射为 BRAM”“接口是否符合预期”,再决定是否进入综合。语法正确不等于硬件正确,接口和循环结构才是决定性能的关键。

总结来说,Quartus HLS 的 C++ 子集围绕“编译期可确定”这一核心原则展开。写代码时,应始终问自己:这个变量、这个分支、这次循环,在硬件里对应什么?如果答案不明确,就需要调整写法或显式添加编译指示。掌握这些边界和规范,才能在后续的流水线、展开和接口优化中有的放矢。