我让GD32的sin计算快了14倍:定点数查表的降维打击

16 阅读7分钟

当我在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;}120001110.0ms开启
float a = 1.1f;for (uint32_t i = 0; i < 100000; i++) {    a = a * 1.1f;}855742071.3ms关闭

图片1.png 速度提升了超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()*6553611536330 (96.1ms)53311568 (444.3ms)
fast_sin()*655363768329 (31.4ms)3768329 ( 31.4ms)

图片2.png

两个关键结论:

  1. fast_sin()完全不受FPU开关影响——因为内部全是整数运算,根本没有浮点指令。

  2. 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.0000640.0000200.000025

  图片3.png

0.000064的最大绝对误差,相对于满量程1.0,相当于 64ppm 的误差。对于PID控制、电机FOC、姿态解算等场景完全够用。

07 示波器实测:波形对比

为了更直观地对比,我用DAC输出两路正弦波:

通道1(黄色):sinf()生成的波形

通道2(绿色):fast_sin()生成的波形

两路波形几乎完全重叠,肉眼无法分辨差异

图4 示波器波形对比图   图片4.png

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.3ms96.1ms31.4ms
最大误差~1e-7~1e-70.000064
是否需要FPU
使用芯片M0/M3/M4/M7全系列Cortex-M4/M7M0/M3/M4/M7全系列

三点核心收获:

  1. 算法选型比硬件堆砌更重要——好的算法可以在廉价芯片上跑出旗舰性能。

  2. 定点数是嵌入式工程师的核心竞争力——理解Q格式、查表、插值,就能在资源受限的场景下游刃有余。

  3. 性能测试要用对方法——DWT周期计数比示波器更精确,sinf()比sin()更适合嵌入式。

这组数据告诉我们:在嵌入式世界,聪明的算法往往比昂贵的硬件更管用。希望这篇文章能帮你重新思考“性能优化”这件事。

附录:文中图表说明

序号图表内容图表说明
图1FPU开/关浮点计算耗时对比柱状图10.0ms vs 71.3ms,7.1倍差距
图2sinf() 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()更常用的函数。