4. HLS中的数据类型详解

数据类型这东西,说简单也简单,说复杂真能坑死人。我在做HLS加速器的时候,至少有一半的调试时间都花在数据类型上。你想想看,C/C++里写个int用得好好的,到了HLS里怎么就那么多讲究?

嗯,今天我们就来掰扯清楚。

4.1 为什么需要任意精度数据类型?

先问个问题:你用int定义一个变量,它占多少位?

32位,对吧?那如果这个变量只需要存0到100的值呢?

浪费了22位。在FPGA上,每一位都对应着实实在在的LUT和FF资源。我见过一个项目,就因为把所有变量都定义成32位整型,结果资源爆了。后来换成ap_int<8>,资源直接砍掉60%。

这就是任意精度数据类型存在的意义——用多少位,就定义多少位。

核心原则:在满足精度和范围的前提下,位宽越小越好。

4.2 ap_int / ap_uint:定长整型

这两个是HLS里最常用的数据类型。ap_int是有符号的,ap_uint是无符号的。用法很简单:

#include <ap_int.h>

ap_int<8> a;      // 8位有符号整数,范围 -128 ~ 127
ap_uint<16> b;    // 16位无符号整数,范围 0 ~ 65535
ap_int<33> c;     // 33位?没问题,HLS支持任意位宽

我个人习惯是:能用无符号就别用有符号。为什么?因为无符号数在硬件里少算一个符号位,综合出来的逻辑更简单。我在做图像处理时,像素值都是0-255,用ap_uint<8>就刚刚好。

小技巧:如果你不确定该用有符号还是无符号,先想想这个数会不会出现负数。不会?那就用无符号。

4.3 ap_fixed:定点数

定点数,说白了就是固定小数点位数的数。它的定义方式是这样的:

#include <ap_fixed.h>

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

这里有个容易搞混的地方:第二个参数是整数部分的位数,不是小数部分的位数。小数位数 = 总位数 - 整数位数。

举个例子:ap_fixed<16, 8>,整数8位,小数8位。那它能表示的范围是多少?

整数部分8位有符号,范围是-128到127。小数部分8位,精度是1/256 ≈ 0.0039。

够用吗?看你的应用。我在做音频处理时,常用ap_fixed<24, 12>,因为音频信号的动态范围大概在-1到1之间,整数部分留一位符号位就够了,剩下的都给小数。

注意:定点数的精度是均匀的。不像浮点数,大数和小数的精度不一样。这一点在某些算法里很关键。

4.4 float / double:浮点数

HLS支持float和double,但我不建议你用,除非万不得已。

为什么?因为浮点运算在FPGA上太费资源了。一个浮点加法器,比定点加法器多消耗5-10倍的资源。我做过一个对比:同样的算法,用float实现,资源占用是用ap_fixed实现的3倍,频率还低了30%。

那什么时候用浮点?

  • 算法本身对精度要求极高,且动态范围很大
  • 快速原型验证,先不管资源
  • 和CPU/GPU做数据交互,懒得做格式转换

我的建议:能用定点就别用浮点。实在要用,先试试ap_fixed能不能满足精度要求。

4.5 数据类型转换

HLS里的类型转换,比C/C++要小心得多。因为位宽变了,硬件行为也会变。

常用的转换方式:

// 静态转换
ap_int<16> a = 100;
ap_int<8> b = a;           // 截断高位,b = 100(如果a的值在-128~127之间)
ap_int<32> c = a;          // 符号扩展,c = 100

// 显式转换
ap_int<8> d = (ap_int<8>)a;  // C风格转换
ap_int<8> e = ap_int<8>(a);  // C++风格转换

// 位宽转换函数
ap_int<16> f = a;           // 自动扩展
ap_uint<8> g = a.range(7,0); // 取低8位

我曾经踩过一个坑:把一个ap_int<16>赋值给ap_int<8>,结果数值溢出了,但仿真没报错。查了两天才发现是这里的问题。所以我现在都会加断言:

assert(a >= -128 && a <= 127);  // 确保不会溢出

避坑指南:我曾经在数据转换时忘记考虑符号位,结果负数变成了正数。记住:有符号转无符号,或者位宽不同的转换,一定要先确认数值范围。

4.6 位宽优化实战

光说不练假把式。我们来看一个实际的优化案例。

假设我们要实现一个FIR滤波器,系数是16位定点数,输入是12位ADC数据。原始代码可能是这样的:

// 优化前
int32_t input[64];
int32_t coeff[64];
int32_t output = 0;

for (int i = 0; i < 64; i++) {
    output += input[i] * coeff[i];
}

全部用32位整型,资源浪费严重。优化后:

// 优化后
ap_int<12> input[64];     // ADC是12位
ap_int<16> coeff[64];     // 系数16位
ap_int<28> output = 0;    // 12+16=28,再加6位防止累加溢出

for (int i = 0; i < 64; i++) {
    output += input[i] * coeff[i];
}

资源对比:

方案 LUT FF DSP
优化前(32位) 1240 860 64
优化后(自定义位宽) 520 380 32

看到没?资源直接砍半。这就是位宽优化的威力。

4.7 知识体系总览

说了这么多,我们来张图总结一下:

HLS数据类型选择决策树 数据类型选择 整数类型 小数类型 ap_int (有符号) ap_uint (无符号) ap_fixed (定点) float/double (浮点) 范围确定 无负数 → ap_uint 有负数 → ap_int 位宽按需定义 精度可控 资源最优 精度高但资源大 万不得已再用 核心建议 能用无符号不用有符号 能用定点不用浮点

4.8 总结一下

数据类型的选择,说白了就是在精度、范围和资源之间找平衡。没有银弹,每个项目都要具体分析。

我的经验是:先搞清楚数据范围,再确定精度要求,最后选最省资源的数据类型。别一上来就用int或float,多想想能不能用ap_int<N>或ap_fixed<W,I>。

嗯,今天就聊到这儿。数据类型这块内容不少,但掌握了这些,HLS开发中80%的数据类型问题你都能搞定了。

可可精品资料库