2、HLS基础语法与数据类型:ap_int/ap_fixed等任意精度数据类型。C/C++ 综合基础。
好,咱们正式开始啃HLS的硬骨头了。这一章,我打算聊聊HLS里最基础、也最容易被忽视的东西——数据类型。你可能会想:“数据类型有什么好讲的?不就是int、float吗?” 嗯,如果你这么想,那在FPGA上写HLS,大概率会踩坑。我当年刚接触HLS时,就用C语言的int写过图像处理,结果综合出来的资源消耗吓死人,时序还一塌糊涂。后来才明白,HLS里的数据类型,跟软件C/C++完全是两码事。
2.1 为什么需要任意精度数据类型?
先问个问题:在软件里,你定义一个int,它通常是32位的。但在FPGA里,你处理一个8位的像素值,真的需要32位吗?
说白了,不需要。用32位去存8位的数据,就是浪费。浪费LUT,浪费DSP,浪费BRAM。FPGA的资源是有限的,你想想看,一个4K视频流,每秒几十帧,每个像素都多出24位的冗余,这资源消耗得翻多少倍?
所以,HLS引入了任意精度数据类型。你可以精确地指定位宽,比如ap_uint<8>就是8位无符号整数,ap_int<16>就是16位有符号整数。这就像你买衣服,以前只有S、M、L三个码,现在可以量身定制,不浪费一丁点布料。
2.2 ap_int 和 ap_uint:整数类型的利器
ap_int和ap_uint是HLS里最常用的整数类型。它们定义在ap_int.h头文件中。用法很简单:
#include <ap_int.h>
// 定义一个8位无符号整数,范围0~255
ap_uint<8> pixel_value;
// 定义一个16位有符号整数,范围-32768~32767
ap_int<16> signed_data;
// 定义一个1位的布尔值
ap_uint<1> flag;
我个人习惯,只要涉及硬件接口或者中间计算,一律用ap_int或ap_uint。为什么?因为可控。你告诉综合工具“我就用8位”,它就不会自作主张给你扩展成32位。
我在项目中遇到过一个问题:用C语言的unsigned char做累加器,结果综合出来一个32位的加法器。明明我的累加结果最大也就1024,10位就够了。后来改成ap_uint<10>,资源直接降了一半。嗯,这就是教训。
2.3 ap_fixed 和 ap_ufixed:定点数的艺术
图像处理里经常要算加权平均、高斯滤波、矩阵乘法。这些运算涉及小数。在软件里,你用float或者double就行了。但在FPGA里,浮点运算代价极高——一个浮点乘法器要消耗几百个LUT和好几个DSP。
怎么办?用定点数。ap_fixed(有符号定点)和ap_ufixed(无符号定点)就是干这个的。
定点数的定义需要两个参数:总位宽和小数位宽。比如:
#include <ap_fixed.h>
// 总位宽16位,小数位宽8位,有符号
// 整数部分占7位(1位符号位 + 6位整数),小数部分占8位
ap_fixed<16, 8> coeff;
// 总位宽8位,小数位宽4位,无符号
// 整数部分占4位,小数部分占4位
ap_ufixed<8, 4> weight;
你想想看,一个ap_fixed<16,8>的乘法,综合出来就是一个定点乘法器,资源消耗远小于浮点乘法器。而且,定点数的运算速度更快,流水线更容易做。
ap_fixed。一般总位宽16位、小数位宽8位,就能满足大部分精度需求。如果精度不够,再考虑增加小数位宽。
2.4 数据类型转换与位宽匹配
HLS里,不同位宽的数据类型做运算,会触发隐式类型转换。这个转换规则跟C语言类似,但后果更严重——它会增加额外的硬件逻辑。
举个例子:
ap_uint<8> a = 100;
ap_uint<16> b = 200;
ap_uint<8> c = a + b; // 这里会发生什么?
答案是:a会被隐式扩展成16位,然后做加法,结果再截断成8位赋值给c。综合工具会生成一个16位加法器,外加一个截断逻辑。这显然不是我们想要的。
正确的做法是:手动进行位宽匹配。
ap_uint<8> a = 100;
ap_uint<16> b = 200;
ap_uint<16> temp = a + b; // 用16位存中间结果
ap_uint<8> c = temp.range(7, 0); // 手动截取低8位
2.5 C/C++ 综合基础:可综合与不可综合
HLS不是万能的。它只能综合C/C++的一个子集。有些语法写起来很爽,但综合不了。比如:
- 动态内存分配:
malloc、new、free、delete统统不行。FPGA没有堆的概念。 - 虚函数: 多态在硬件里实现不了,别想了。
- 递归: 递归调用会生成无限循环的硬件,综合工具会报错。
- 标准模板库(STL): 大部分STL容器(如vector、list)不可综合。但
ap_int、ap_fixed这些HLS专用类型是可以的。
那什么可以综合?
- 基本数据类型:
int、char、float(但不推荐)、ap_int、ap_fixed等。 - 数组: 静态数组(固定大小)可以综合,会被映射成BRAM或寄存器。
- 循环:
for循环可以综合,但循环次数必须是常量。 - 条件语句:
if-else、switch-case可以综合。 - 函数: 函数调用可以综合,但函数不能有静态局部变量(除非用HLS特定的方式)。
2.6 知识体系总览
下面这张图,是我自己总结的HLS数据类型与综合基础的知识结构。你可以把它当作一个快速索引。
2.7 实战建议:从软件思维切换到硬件思维
最后,我想分享一点个人感悟。很多从软件转过来的工程师,写HLS代码时总带着软件思维——变量随便定义,类型随便用,反正编译器会处理。但在HLS里,每一行代码都对应着硬件资源。你多用1位,可能就多消耗几十个LUT。
所以,我的建议是:
- 先算位宽: 写代码前,先算清楚数据范围,确定最小位宽。
- 显式转换: 所有类型转换都写清楚,别偷懒。
- 避免浮点: 能用定点,绝不用浮点。除非精度要求极高。
- 检查综合报告: 每次综合后,看一眼资源报告。如果某个模块的LUT消耗异常,多半是数据类型没用好。
嗯,这一章的内容就到这里。数据类型是HLS的基石,打好基础,后面的优化才能事半功倍。