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格式的定义,到加减乘除四种运算,再到格式选择策略。你想想看,是不是每个环节都有需要注意的地方?
好了,定点数的基础就讲到这里。记住一句话:定点数就是用整数思维做小数运算。只要把小数点位置记清楚,剩下的就是整数运算了。
公众号:蓝海资料掘金营,微信deep3321