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 配置系数的模块,则用寄存器接口接收参数。
五、基本编码规范:让综合结果可预测
除了语法边界,编码风格也直接影响 HLS 的综合质量。以下规范建议在项目初期就统一执行:
- 循环边界尽量为常量:
for (int i = 0; i < N; i++)中的N用const或模板参数定义,便于 HLS 展开和流水。 - 避免跨迭代依赖:如果循环体内第
i次迭代依赖第i-1次的结果,HLS 无法并行展开,会退化为串行状态机。此时应显式使用#pragma HLS dependence或重构算法。 - 数组访问用常量索引:
data[i]中i为循环变量时,HLS 可以推断出存储器结构;若i来自运行时输入,则可能生成多路选择器或无法综合。 - 接口参数显式声明:使用
#pragma HLS interface明确指定ap_memory、axis、s_axilite等接口类型,避免 HLS 自动推断出不符合系统要求的端口。 - 位宽按需定义:累加器、计数器、地址位宽都应根据实际范围选择
ap_int/ap_uint,不要一律用int。 - 函数粒度适中:每个 HLS 函数对应一个硬件模块,函数过大导致资源紧张,过小则接口开销占比高。建议按数据流阶段划分函数。
总结来说,Quartus HLS 的 C++ 子集围绕“编译期可确定”这一核心原则展开。写代码时,应始终问自己:这个变量、这个分支、这次循环,在硬件里对应什么?如果答案不明确,就需要调整写法或显式添加编译指示。掌握这些边界和规范,才能在后续的流水线、展开和接口优化中有的放矢。