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 本章知识体系图
下面这张图,帮你理清本章的核心逻辑:
3.7 小结
这一章的内容,说白了就是一句话:在HLS里,数据类型的选择直接决定了你的资源消耗和性能。
- 能用窄位宽就别用宽的。
- 能用定点就别用浮点。
- 能用数组就别用指针。
- 能用结构体打包就别散着放。
记住这些,你的HLS设计就能少走很多弯路。下一章咱们会深入讲流水线设计,到时候这些数据类型知识都会用上。
小技巧:如果你不确定某个数据类型综合出来是什么样子,可以用Vivado HLS的“综合报告”看一下。里面会告诉你用了多少LUT、多少DSP、多少BRAM。多看看,慢慢就有感觉了。