浮点数近似计算和表示的几种方法

最近做IoT传感器相关的工作
需要在无FPU的场合下进行一些浮点运算
稍微整理一下绕开浮点数的计算和表示方法


计算:定点数方式

最适合计算 比如DSP就是以这种方法算定点数的..

相关开源库
定点数学库libfixmathd
定点矩阵运算libfixmatrix

这里转一篇不错的文章 原文: http://blog.qz828.com/html/05/705-8890.html


DSP定点小数运算

许多DSP芯片只支持整数运算,如果现在这些芯片上进行小数运算的话,定点小数运算应该是最佳选择了,此外即使芯片支持浮点数,定点小数运算也是最佳的速度选择。
在DSP 世界中,由于DSP芯片的限制,经常使用定点小数运算。所谓定点小数,实际上就是用整数来进行小数运算。下面先介绍定点小数的一些理论知识,然后以C语言为例,介绍一下定点小数运算的方法。在TI C5000 DSP系列中使用16比特为最小的储存单位,所以我们就用16比特的整数来进行定点小数运算。
先从整数开始,16比特的储存单位最多可以表示0x0000到0xffff,65536种状态,如果它表示C语言中的无符号整数的话,就是从0到 65535。如果需要表示负数的话,那么最高位就是符号位,而剩下的15位可以表示32768种状态。这里可以看出,对于计算机或者DSP芯片来说,符号并没有什么特殊的储存方式,其实是和数字一起储存的。为了使得无论是无符号数还是符号数,都可以使用同样的加法减法规则,符号数中的负数用正数的补码表示。
我们都知道-1 + 1 =0,而0x0001表示1,那么-1用什么来表示才能使得-1 + 1 =0呢?答案很简单:0xffff。现在就可以打开Windows的计算器,用16进制计算一下0xffff+0x0001,结果是0x10000。那么 0x10000和0x0000等价麽,我们刚才说过用16比特来表达整数,最高位的1是第17位,这一位是溢出位,在运算寄存器中没有储存这一位,所以结果是低16位,也就是0x0000。现在我们知道负数的表达方式了。举个例子:-100。首先我们需要知道100的16进制,用计算器转换一下,可以知道是0x0064,那么-100就是0x10000 - 0x0064,用计算器算一下得0xff9c。
还有一种简单的转换符号的方法,就是取反加一:把数x写成二进制格式,每位0变1,1变0,最后把结果加1就是-x了。
好,复习了整数的相关知识之后,我们进入定点小数运算环节。所谓定点小数,就是小数点的位置是固定的。我们是要用整数来表示定点小数,由于小数点的位置是固定的,所以就没有必要储存它(如果储存了小数点的位置,那就是浮点数了)。既然没有储存小数点的位置,那么计算机当然就不知道小数点的位置,所以这个小数点的位置是我们写程序的人自己需要牢记的。
先以10进制为例。如果我们能够计算12+34=46的话,当然也就能够计算1.2+3.4 或者 0.12+0.34了。所以定点小数的加减法和整数的相同,并且和小数点的位置无关。乘法就不同了。 12*34=408,而1.2*3.4=4.08。这里1.2的小数点在第1位之前,而4.08的小数点在第2位之前,小数点发生了移动。所以在做乘法的时候,需要对小数点的位置进行调整?!可是既然我们是做定点小数运算,那就说小数点的位置不能动!!怎么解决这个矛盾呢,那就是舍弃最低位。也就说1.2*3.4=4.1,这样我们就得到正确的定点运算的结果了。所以在做定点小数运算的时候不仅需要牢记小数点的位置,还需要记住表达定点小数的有效位数。上面这个例子中,有效位数为2,小数点之后有一位。
现在进入二进制。我们的定点小数用16位二进制表达,最高位是符号位,那么有效位就是15位。小数点之后可以有0 - 15位。我们把小数点之后有n位叫做Qn,例如小数点之后有12位叫做Q12格式的定点小数,而Q0就是我们所说的整数。
Q12 的正数的最大值是 0 111 . 111111111111,第一个0是符号位,后面的数都是1,那么这个数是十进制的多少呢,很好运算,就是 0x7fff / 2^12 = 7.999755859375。对于Qn格式的定点小数的表达的数值就它的整数值除以2^n。在计算机中还是以整数来运算,我们把它想象成实际所表达的值的时候,进行这个运算。
反过来把一个实际所要表达的值x转换Qn型的定点小数的时候,就是x*2^n了。例如 0.2的Q12型定点小数为:0.2*2^12 = 819.2,由于这个数要用整数储存,所以是819 即 0x0333。因为舍弃了小数部分,所以0x0333不是精确的0.2,实际上它是819/2^12 =0.199951171875
我们用数学表达式做一下总结:
x表示实际的数(*一个浮点数), q表示它的Qn型定点小数(一个整数)。

1
2
q = (int) (x * 2^n)
x = (float)q/2^n

由以上公式我们可以很快得出定点小数的+-*/算法:

1
2
3
4
5
假设q1,q2,q3表达的值分别为x1x2x3
q3 = q1 + q2x3 = x1 + x2
q3 = q1 - q2x3 = x1 - x2
q3 = q1 * q2 / 2^n若 x3 = x1 * x2
q3 = q1 * 2^n / q2x3 = x1 / x2

我们看到加减法和一般的整数运算相同,而乘除法的时候,为了使得结果的小数点位不移动,对数值进行了移动。
用c语言来写定点小数的乘法就是:

1
2
3
short q1,q2,q3;
....
q3=((long q1) * (long q2)) >> n;

由于/ 2^n和* 2^n可以简单的用移位来计算,所以定点小数的运算比浮点小数要快得多。下面我们用一个例子来验证一下上面的公式:
用Q12来计算2.1 * 2.2,先把2.1 2.2转换为Q12定点小数:

1
2
3
2.1 * 2^12 = 8601.6 = 8602
2.2 * 2^12 = 9011.2 = 9011
(8602 * 9011) >> 12 = 18923

18923的实际值是18923/2^12 = 4.619873046875 和实际的结果4.62相差0.000126953125,对于一般的计算已经足够精确了。
定点小数,指小数点的位置是固定的,浮点小数指小数点的位置可以变动,这样就可以表示更大或更小的数,因为计算机里的位数是有限的,不可能无限期的.
在小数点后增加数字,所以只能靠移动小数点的位置来储存不同大小的数


表示法

十进制浮点数表示法

1
2
3
4
5
6
7
/* 约定: n = value / scale  */
typedef struct {
int32_t value;
int32_t scale;
} dfloat_t; /* decimal float */

dfloat_t df_0 = { 123456000, 1e6 }; /* 1234.56000 */

表示法, 不适合计算
一个nmea0183 parser里用到这个结构
因为原始数据是”1.234”这样的字符串
很容易变换到这种表示法 供用户进一步处理

整数+小数表示法

1
2
3
4
5
6
7
/* 约定: n = integer + decimal*1e6  */
typedef struct {
int32_t integer;
int32_t decimal;
} ffloat_t; /* fixed float */

ffloat_t n0 = { 1234, 56000 }; /* 1234.56000 */

不适合计算
很适合显示 像这样printf("%d.%06d", n0.integer, n0.decimal);

zephyr的sensor subsystem用到这种表示法:

1
2
3
4
5
6
7
8
9
10
11
static void ak8975_convert(struct sensor_value *val, int16_t sample,
uint8_t adjustment)
{
int32_t conv_val;

conv_val = sample * AK8975_MICRO_GAUSS_PER_BIT *
((uint16_t)adjustment + 128) / 256;
val->type = SENSOR_VALUE_TYPE_INT_PLUS_MICRO;
val->val1 = conv_val / 1000000;
val->val2 = conv_val % 1000000;
}