4. 定点数基础:Q格式、定点数加减乘除运算规则

各位同学,今天我们来聊聊定点数。说实话,很多做软件出身的朋友第一次接触FPGA里的定点数,都会觉得有点别扭。我当年刚入行时也一样,心想:浮点数多方便啊,为什么非要用定点数?

原因很简单:FPGA里做浮点运算,资源消耗大、速度慢。你想想看,一个浮点乘法器占用的逻辑资源,够我做好几个定点乘法器了。在运动控制这种对实时性要求极高的场景下,我们追求的是每个时钟周期都能出结果,而不是等上几十个周期。

所以,定点数就成了我们的首选。说白了,就是用整数运算来模拟小数运算。

4.1 什么是Q格式?

Q格式,就是定点数的表示方法。它的核心思想是:约定一个固定的小数点位置。

比如Q15格式,表示16位有符号数中,有1位符号位,15位小数位。Q8.8格式,表示16位有符号数中,8位整数位,8位小数位。

我习惯用Qm.n这种写法,其中m是整数位数(含符号位),n是小数位数。总位数就是m+n。

格式 总位数 整数位(含符号) 小数位 数值范围 精度
Q15 16 1 15 -1 ~ 0.99997 1/32768
Q8.8 16 8 8 -128 ~ 127.996 1/256
Q4.12 16 4 12 -8 ~ 7.9998 1/4096
Q1.31 32 1 31 -1 ~ 0.9999999995 1/2147483648

嗯,这里要注意:Q格式只是我们脑子里的约定。在FPGA的寄存器里,存的还是二进制数。比如Q15格式下的0.5,存的就是16'h4000。为什么?因为0.5 × 2^15 = 16384 = 0x4000。

核心公式:

定点数值 = 整数编码值 × 2^(-n)

其中n是小数位数。

4.2 定点数加法

定点数加法,说白了就是整数加法。但有个前提:两个数的Q格式必须相同。

比如两个Q15格式的数相加:

// Q15格式:0.25 + 0.5 = 0.75
// 0.25 → 16'h2000
// 0.5  → 16'h4000
// 结果 → 16'h6000 → 0.75

reg [15:0] a, b, sum;
sum = a + b;  // 直接加,就是这么简单

如果格式不同怎么办?比如Q8.8加Q4.12?那就得先对齐小数点。我建议的做法是:统一转换成小数位更多的那个格式。

注意溢出问题:

两个Q15格式的数相加,结果可能超过16位能表示的范围。比如0.9 + 0.8 = 1.7,但Q15最大只能表示0.99997。这时候就会溢出。

我曾经在一个伺服驱动项目里吃过这个亏。位置环的输出加上速度环的输出,结果溢出了,电机直接飞车。从那以后,我每次做加法都会预留一位符号位扩展。

解决方案:加法前先扩展一位符号位。

reg [16:0] sum_ext;
sum_ext = {a[15], a} + {b[15], b};  // 17位加法,防止溢出

4.3 定点数减法

减法跟加法本质上一样。也是要求格式相同,也是要注意溢出。

不过减法有个特殊情况:0.3 - 0.8 = -0.5,结果变成负数了。这在补码表示下完全没问题,只要我们的数是有符号的就行。

// Q15格式:0.3 - 0.8 = -0.5
// 0.3  → 16'h2666
// 0.8  → 16'h6666
// 结果 → 16'hC000 → -0.5

reg signed [15:0] a, b, diff;
diff = a - b;  // 有符号减法

个人经验:

在Verilog里做减法,一定要用signed类型。不然的话,负数会被当成正数处理,结果就全乱了。我刚开始用Verilog时犯过这个错,仿真半天找不出问题,最后发现是忘了加signed关键字。

4.4 定点数乘法

乘法比加减法稍微复杂一点。两个Q15格式的数相乘,结果是多少位?

答案是:32位。因为16位 × 16位 = 32位。

那小数点的位置呢?Q15 × Q15 = Q30。也就是说,结果有30位小数位。

// Q15 × Q15 = Q30
// 0.25 × 0.5 = 0.125
// 0.25 → 16'h2000
// 0.5  → 16'h4000
// 乘积 → 32'h08000000 → 0.125

reg signed [15:0] a, b;
reg signed [31:0] product;
product = a * b;  // 32位结果,Q30格式

但实际应用中,我们通常不需要那么高精度。比如PID算法里,一般会把结果截断回原来的格式。

// 截断回Q15格式
reg signed [15:0] result;
result = product[30:15];  // 取高16位,相当于右移15位

为什么取[30:15]?因为Q30格式右移15位,就变成了Q15格式。注意这里取了30位而不是31位,因为符号位在bit31。

乘法截断规则:

Qm.n × Qp.q = Q(m+p).(n+q)

截断到Qm.n:取结果的[ m+n+q-1 : q ]

4.5 定点数除法

除法是定点数运算里最麻烦的。为什么?因为FPGA里没有现成的除法器。

我一般用以下几种方法:

  • 查表法:把除数倒数做成查找表,然后做乘法。适合除数变化范围不大的情况。
  • CORDIC算法:适合做三角函数和除法,但迭代次数多。
  • 移位法:如果除数是2的幂次,直接用移位操作。

举个移位法的例子:

// Q15格式:除以2
// 0.5 ÷ 2 = 0.25
// 0.5 → 16'h4000
// 结果 → 16'h2000 → 0.25

reg signed [15:0] a, result;
result = a >>> 1;  // 算术右移一位,相当于除以2

注意这里用的是算术右移(>>>),不是逻辑右移(>>)。算术右移会保留符号位,逻辑右移不会。

除法精度问题:

定点数除法会有精度损失。比如1/3,在Q15格式下只能近似表示。我一般会先用高精度格式做除法,再截断到目标格式。

曾经有个学员问我:为什么他的PID控制器输出总是抖?我一看代码,发现他在做除法时直接把余数丢了,导致控制量有阶梯状变化。后来改成四舍五入,问题就解决了。

4.6 Q格式的选择策略

选Q格式,说白了就是在范围和精度之间做权衡。

我的建议是:

  • 先确定范围:你的数据最大可能到多少?比如速度指令最大是±3000rpm,那整数位至少要能表示3000。
  • 再确定精度:你的控制精度要求是多少?比如位置误差要控制在0.001mm以内,那小数位至少要能表示0.001。
  • 最后算总位数:整数位+小数位+符号位,看能不能塞进16位或32位。

举个例子:

应用场景 数据范围 精度要求 推荐格式
电流环PI控制器 ±10A 0.001A Q8.8(16位)
速度环PID控制器 ±3000rpm 0.1rpm Q12.4(16位)
位置环PID控制器 ±100000脉冲 0.01脉冲 Q17.15(32位)

我的习惯:

在运动控制项目中,我一般会统一用Q15格式做PID运算。虽然范围只有-1到1,但我会在输入输出做归一化处理。这样做的好处是:所有系数都是Q15格式,加减乘除不用来回转换,代码写起来清爽很多。

4.7 本章知识体系

下面这张图,是我对定点数知识体系的总结:

定点数基础:Q格式与运算规则 定点数 Q格式定义 Qm.n:m整数位,n小数位 定点值 = 编码值 × 2^(-n) 加法运算 格式必须相同 注意符号位扩展防溢出 减法运算 使用signed类型 注意负数处理 乘法运算 Qm.n × Qp.q = Q(m+p).(n+q) 截断时注意精度损失 除法运算 查表法 / CORDIC / 移位 注意四舍五入 Q格式选择策略 先定范围,再定精度 推荐归一化到Q15

这张图把定点数的核心知识点都串起来了。从Q格式的定义,到加减乘除四种运算,再到格式选择策略。你想想看,是不是每个环节都有需要注意的地方?

好了,定点数的基础就讲到这里。记住一句话:定点数就是用整数思维做小数运算。只要把小数点位置记清楚,剩下的就是整数运算了。


公众号:蓝海资料掘金营,微信deep3321