3. C语言基础回顾与HLS扩展:数据类型、指针、数组、结构体、HLS特有的任意精度类型

好,咱们进入第三章。这一章我打算带大家把C语言在HLS中的那些“变味”的地方捋一遍。你可能会想:“C语言我熟啊,有什么好讲的?”

嗯,别急。我当年第一次用HLS写一个FIR滤波器,直接把int类型往上一扔,结果综合出来的资源量吓死人。后来才明白,在FPGA的世界里,位宽就是生命。你多用1个bit,可能就多占一堆LUT和DSP。

所以这一章,咱们重点聊聊:数据类型怎么选、指针怎么用才不坑、数组怎么映射成BRAM、结构体怎么打包,以及HLS最核心的武器——任意精度类型。

3.1 数据类型:别再只用int了

C语言的标准数据类型,在HLS里都能用。但问题是,它们太“胖”了。

  • char:8位,适合小范围计数。
  • short:16位,适合中等精度。
  • int:32位,默认类型,但资源消耗大。
  • long long:64位,慎用,综合出来面积很大。
  • float/double:浮点型,HLS支持,但会消耗大量DSP和LUT。我建议能用定点就别用浮点。

核心原则:能用多窄就用多窄。别图省事直接用int,除非你真的需要32位精度。

举个例子,你写一个计数器,最大值是100。用int就是32位,但用unsigned char只需要8位。综合出来,资源能省3/4。

3.2 指针:小心使用,别乱飞

指针在C语言里是神器,但在HLS里,它是个“双刃剑”。

HLS对指针的支持有限。它不支持动态分配(malloc/free),也不支持函数指针。你想想看,硬件里哪有“堆”这个概念?

我建议你这样用指针:

  • 只用于函数参数:比如把数组或结构体通过指针传进来。
  • 避免指针运算:比如p++这种,HLS很难分析出你指向了哪里。
  • 用引用(&)代替指针:在C++风格的HLS代码里,引用更安全,综合效果也更好。

避坑指南:我曾经在一个项目中,用指针实现了环形缓冲区。结果HLS报错说“无法确定指针指向”。后来改成数组索引,问题就解决了。所以,能用数组就别用指针。

3.3 数组:映射成BRAM还是LUT?

数组在HLS里很常用。但你要知道,数组最终会被综合成Block RAM(BRAM)或者分布式RAM(LUT)。

怎么选?看大小:

  • 小数组(< 64个元素):默认映射成LUT,读写快,但占逻辑资源。
  • 大数组(>= 64个元素):默认映射成BRAM,容量大,但读写有延迟。

你可以用#pragma HLS RESOURCE来手动指定映射方式。比如:

#pragma HLS RESOURCE variable=my_array core=RAM_1P_BRAM
int my_array[256];

我个人习惯是:查表类的数据(比如正弦表)用BRAM,临时变量用LUT。这样资源分配最合理。

3.4 结构体:打包还是拆开?

结构体在HLS里很实用,尤其是当你需要把多个相关数据组合在一起时。比如一个像素点包含R、G、B三个分量。

但要注意:结构体默认是“对齐”的。比如你定义:

struct pixel {
    unsigned char r;
    unsigned char g;
    unsigned char b;
};

这个结构体在内存里占3个字节,但HLS可能会把它对齐成4字节。浪费了1个字节。

解决办法是加#pragma HLS PACK:

struct __attribute__((packed)) pixel {
    unsigned char r;
    unsigned char g;
    unsigned char b;
};

这样就能紧凑排列,不浪费位宽。我在做图像处理项目时,经常用这个技巧,能省不少BRAM。

3.5 HLS特有的任意精度类型:ap_int / ap_fixed

好了,重点来了。这是HLS最强大的武器之一。

标准C语言的int是32位,short是16位。但如果你只需要13位呢?或者7位呢?

这时候就用ap_int和ap_fixed。它们允许你定义任意位宽的数据类型。

3.5.1 ap_int:任意位宽整数

用法很简单:

#include <ap_int.h>

ap_int<13> a;  // 13位有符号整数
ap_uint<7> b;  // 7位无符号整数

为什么好用?因为你可以精确控制位宽。比如一个计数器最大值是5000,需要13位(2^13=8192)。用ap_uint<13>比用int省了19位。

我的经验:在做数字信号处理时,我经常用ap_int来匹配数据路径的位宽。比如ADC输出是12位,我就用ap_int<12>。这样综合出来的乘法器正好是12x12,不会浪费DSP资源。

3.5.2 ap_fixed:任意位宽定点数

定点数比整数更灵活。它允许你指定整数位宽和小数位宽。

#include <ap_fixed.h>

ap_fixed<16, 8> x;  // 总16位,整数8位,小数8位
ap_ufixed<24, 12> y; // 无符号,总24位,整数12位,小数12位

这里ap_fixed<W, I>表示总位宽W,整数位宽I,小数位宽就是W-I。

定点数的好处是:没有浮点数的开销,但又能表示小数。我在做PID控制器时,就用ap_fixed<24, 12>来表示误差和积分项,综合出来只用了几个DSP,比浮点快得多。

3.5.3 量化模式与溢出模式

用定点数时,你还要考虑两个问题:

  • 量化模式:小数部分截断时怎么处理?是直接截断(AP_TRN)还是四舍五入(AP_RND)?
  • 溢出模式:整数部分溢出时怎么办?是饱和(AP_SAT)还是绕回(AP_WRAP)?

你可以这样指定:

ap_fixed<16, 8, AP_RND, AP_SAT> z;

我个人习惯是:在控制回路里用饱和模式,防止积分饱和;在数据路径里用截断模式,节省资源。

3.6 本章知识体系图

下面这张图,帮你理清本章的核心逻辑:

HLS数据类型与扩展知识体系 C语言基础 → HLS扩展 标准数据类型 char/short/int/long float/double 指针与引用 避免指针运算 多用引用(&) 数组映射 小→LUT / 大→BRAM RESOURCE pragma 结构体打包 PACK pragma 紧凑排列 HLS核心武器:任意精度类型 ap_int / ap_uint 任意位宽整数 精确控制位宽 ap_fixed / ap_ufixed 定点数:整数+小数 量化/溢出模式 实际应用场景 DSP / 滤波器 PID控制 / 图像处理

3.7 小结

这一章的内容,说白了就是一句话:在HLS里,数据类型的选择直接决定了你的资源消耗和性能。

  • 能用窄位宽就别用宽的。
  • 能用定点就别用浮点。
  • 能用数组就别用指针。
  • 能用结构体打包就别散着放。

记住这些,你的HLS设计就能少走很多弯路。下一章咱们会深入讲流水线设计,到时候这些数据类型知识都会用上。

小技巧:如果你不确定某个数据类型综合出来是什么样子,可以用Vivado HLS的“综合报告”看一下。里面会告诉你用了多少LUT、多少DSP、多少BRAM。多看看,慢慢就有感觉了。

可可精品资料库