当我在GD32F303上运行了65536次float sinf(float)和int16_t fast_sin(uint16_t)的对比测试后,看到串口打印出来的数据,我愣住了——没有FPU的时候,fast_sin()比sinf()快了14倍;就算打开了FPU,fast_sin()依然领先超3倍。
01 一切从“为什么关了FPU,sin()耗时不变”开始
事情是这样的:我在GD32F303VGT6上做算法优化实验,像往常一样打开了Keil的FPU选项,测了一下sin函数的执行时间。然后顺手关掉FPU,又测了一次。
结果几乎一样。
当时我就懵了——FPU开关竟然对sin()没有影响?
02 你猜怎么着?我踩了一个经典的坑
排查了一圈,发现原因其实很简单:Cortex-M4的FPU只支持单精度(float),而sin()函数处理的是双精度(double)。
双精度浮点运算在Cortex-M4上只能用软件模拟,跟FPU开关没有任何关系。
正确的做法是使用sinf(),也就是单精度版本的sin()函数。
在Cortex-M系列上,所有带‘f’后缀的数学函数(sinf()、sqrtf()、powf()等)才是真正能调用FPU硬件加速的版本。
03 浮点乘法验证:确认FPU真的在工作
为了确认FPU配置没有问题,先跑一个简单的浮点乘法测试:
测试结果(DWT周期计数,主频120MHz):
| 测试代码 | DWT周期计数 | 耗时 | FPU状态 |
|---|---|---|---|
| float a = 1.1f;for (uint32_t i = 0; i < 100000; i++) { a = a * 1.1f;} | 1200011 | 10.0ms | 开启 |
| float a = 1.1f;for (uint32_t i = 0; i < 100000; i++) { a = a * 1.1f;} | 8557420 | 71.3ms | 关闭 |
速度提升了超7倍——硬件FPU工作正常。
04 真正的主角:定点数查表算法
既然FPU对单精度运算有加速效果,那用定点数(纯整数运算)实现的sin()函数,在FPU开启和关闭时表现如何?
我写了一个基于Q15格式的查表+线性插值算法,表格只有129个点(0°~90°,步长0.703125°),代码完全不用浮点数:
// Q15格式:-1-1映射到 -32768-32767
const int16_t sin_table[129] = {
0x0000,0x0192,0x0324,0x04B6,0x0648,0x07D9,0x096A,0x0AFB,
0x0C8C,0x0E1C,0x0FAB,0x113A,0x12C8,0x1455,0x15E2,0x176E,
// ... 省略中间数据,完整数组见Gitee
0x7FFF
};
// 输入:0-65535 对应 0°-360°,输出:Q15格式正弦值
int16\_t fast\_sin(uint16\_t angle\_q16) {
uint16_t quadrant = (angle_q16 >> 14) & 0x03;
uint16\_t angle\_90 = angle\_q16 & 0x3FFF;
uint16\_t offset = 0;
if (quadrant &0x01) {
angle\_90 = 0x3FFF - angle\_90;
offset = 1;
}
uint16_t index = angle_90 >>7; // 查表索引 0~90
int32_t result = (int32_t)sin_table[index + 1] - sin_table[index];
`result *= (angle_90 & 0x7F)+offset;
result += 64;
result >>= 7;
result += (int32_t)sin_table[index];`
if (quadrant &0x02) return -(int16_t)result;
return (int16_t)result;
}
05 巅峰对决:sinf() vs fast_sin()
测试结果(主频120MHz,DWT周期计数):
| 测试函数 | 开启FPU | 关闭FPU |
|---|---|---|
| DWT周期计数 (耗时) | DWT周期计数 (耗时) | |
| sinf()*65536 | 11536330 (96.1ms) | 53311568 (444.3ms) |
| fast_sin()*65536 | 3768329 (31.4ms) | 3768329 ( 31.4ms) |
两个关键结论:
-
fast_sin()完全不受FPU开关影响——因为内部全是整数运算,根本没有浮点指令。
-
fast_sin()比FPU开启后的sinf()还快超3倍——算得准不如算得巧。
速度提升汇总:
sinf()关闭FPU vs 开启FPU:近5倍加速(硬件FPU的功劳)
开启FPU sinf() vs fast_sin():fast_sin()快超3倍(算法的胜利)
关闭FPU sinf() vs fast_sin():fast_sin()快超14倍(极端场景下的降维打击)
06 精度够用吗?
速度是快了,但精度如何?毕竟工业控制不是做科研,够用就行。
我在0°~360°范围内按0.1°步长测试了3600个值,将fast_sin()的输出与sinf()的标准值对比:
| 精度指标 | 最大绝对误差 | 平均绝对误差 | 均方根误差 |
|---|---|---|---|
| 数值 | 0.000064 | 0.000020 | 0.000025 |
0.000064的最大绝对误差,相对于满量程1.0,相当于 64ppm 的误差。对于PID控制、电机FOC、姿态解算等场景完全够用。
07 示波器实测:波形对比
为了更直观地对比,我用DAC输出两路正弦波:
通道1(黄色):sinf()生成的波形
通道2(绿色):fast_sin()生成的波形
两路波形几乎完全重叠,肉眼无法分辨差异
图4 示波器波形对比图
08 算法原理:Q15格式 + 查表插值
为什么用Q15?
浮点数运算慢,在Cortex-M4上虽然可以用FPU加速,但在M0/M3等不带FPU的芯片上就是灾难。Q15格式用整数表示小数,所有运算都用整数ALU完成,速度极快。
Q15的数值范围:-32768 ~ 32767 对应 -1.0 ~0.99997
为什么只用129个表?
正弦函数在0°~360°范围内具有高度对称性:
[0°~90°) → 直接查表
[90°~180°) → sin(180° - θ)
[180°~270°) → -sin(θ - 180°)
[270°~360°) → -sin(360° - θ)
所以只需要存0°~90°的129个点,配合象限映射就能算出所有角度。
插值提升精度
步长0.703125°的查表精度显然不够,所以在两个相邻表值之间做线性插值:
f(x) = y₀+ (y₁- y₀) × (x - x₀) / (x₁- x₀)
所有运算都是整数乘加,一个周期内完成。
09 源码与工程
完整的Keil工程已上传Gitee,包含:
GD32F303VGT6的完整工程
fast_sin完整源码
性能测试代码(使用DWT周期计数)
精度测试代码
Gitee地址:[gitee.com/jervis_luo/…]
10 总结
| 对比维度 | sinf()(关闭FPU) | sinf()(开启FPU) | Fast_sin()(定点数) |
|---|---|---|---|
| 65536次计算耗时 | 444.3ms | 96.1ms | 31.4ms |
| 最大误差 | ~1e-7 | ~1e-7 | 0.000064 |
| 是否需要FPU | 否 | 是 | 否 |
| 使用芯片 | M0/M3/M4/M7全系列 | Cortex-M4/M7 | M0/M3/M4/M7全系列 |
三点核心收获:
-
算法选型比硬件堆砌更重要——好的算法可以在廉价芯片上跑出旗舰性能。
-
定点数是嵌入式工程师的核心竞争力——理解Q格式、查表、插值,就能在资源受限的场景下游刃有余。
-
性能测试要用对方法——DWT周期计数比示波器更精确,sinf()比sin()更适合嵌入式。
这组数据告诉我们:在嵌入式世界,聪明的算法往往比昂贵的硬件更管用。希望这篇文章能帮你重新思考“性能优化”这件事。
附录:文中图表说明
| 序号 | 图表内容 | 图表说明 |
|---|---|---|
| 图1 | FPU开/关浮点计算耗时对比柱状图 | 10.0ms vs 71.3ms,7.1倍差距 |
| 图2 | sinf() vs fast_sin()耗时对比柱状图 | 96.1ms vs 444.3ms vs 31.4ms |
| 图3 | 误差分布散点图 | 误差-角度,误差在±0.000064以内 |
| 图4 | 示波器波形对比图 | sinf()(黄色)和fast_sin()(绿色)重叠 |
后记:写完这篇文章后,我顺手把fast_sin()用在了自己做的两轮平衡小车上。原本用sinf()时,角度环的更新频率只能跑到2kHz(CPU被浮点运算拖累);换上fast_sin()后,直接飙到5kHz。小车稳得像钉在地上。这大概就是优化的乐趣所在。 如果你对定点数学库感兴趣,欢迎关注我,下一期我们来聊聊fast_atan2()——这个在电机FOC控制里比sin()更常用的函数。