云计算百科
云计算领域专业知识百科平台

矩阵乘法的Q15定点实现,让卡尔曼滤波不再拖后腿

前五篇文章里,我用手写定点函数替换了sinf()、atan2f()、sqrtf()、expf()、logf()。但真正让嵌入式工程师头疼的,不是单个数学函数,而是矩阵运算——卡尔曼滤波、姿态解算、神经网络推理,核心都是矩阵乘法。今天我们来手撕矩阵乘法。

01 为什么是矩阵乘法?

在嵌入式开发中,矩阵乘法的出场频率极高:

– 卡尔曼滤波:预测和更新步骤全是矩阵乘法

– 姿态解算:四元数更新、旋转矩阵计算

– 神经网络推理:全连接层就是矩阵乘法

– 最小二乘拟合:法方程求解

– 电机FOC:Clarke/Park变换本质是2×2矩阵乘法

浮点矩阵乘法在 Cortex-M4 上虽然可以用 FPU 加速,但矩阵维度越大,运算耗时越明显(运算量按 O(N³) 增长),且执行时间受数据影响(异常值、非规格化数会导致流水线停顿)。而定点 mat_mul_q15() 用纯整数运算,执行时间更固定,在无 FPU 的 M0/M3 上速度优势明显。需要注意的是,定点运算的累积误差通常比浮点大。浮点的相对误差固定在 1e-7 量级,而定点的绝对误差会随累加次数线性增长。因此,定点矩阵乘法适合对速度、确定性要求高,但对绝对精度要求不苛刻的场景。

02 矩阵乘法的特殊难点

和单个数学函数不同,矩阵乘法有三个额外的难点:

难点一:Q格式乘法会“放大”位宽

两个 Q15 数相乘,结果是 Q30:

Q15 × Q15 → Q30

必须右移 15 位才能回到 Q15。如果忘了移位,结果会大 32768 倍。

难点二:累加会溢出

矩阵乘法的每个输出元素是 N 个乘积之和。如果 N=8,每个乘积最大 32767×32767 ≈ 10⁹,累加 8 次就是 8×10⁹,远超 int32_t 的 2.1×10⁹。

必须用int64_t 累加,或者用 Q15 输入但中间累加用 Q30 格式。

难点三:数据布局影响Cache命中率

矩阵乘法是内存访问密集型操作。行优先和列优先的访问模式,在 Cortex-M7 的 Cache 下性能差异可达数倍。

03 核心思路:Q15输入,Q30累加,Q15输出

定点矩阵乘法的核心公式:

C[i][j] = Σ A[i][k] × B[k][j]

每一步:

1. A 和 B 的元素是 Q15

2. 乘积是 Q30

3. 累加用 int64_t(防止溢出)

4. 最后右移 15 位,回到 Q15

关键洞察:矩阵乘法的定点化不是“把浮点换成定点”那么简单,而是在每一步都精确控制Q格式的缩放。

04 定点矩阵乘法实现

基础版本(通用,任意维度)

#include <stdint.h>

/**

 * @brief  Q15 定点矩阵乘法

 * @param  A  M×K 矩阵,Q15 格式

 * @param  B  K×N 矩阵,Q15 格式

 * @param  C  M×N 输出矩阵,Q15 格式

 * @param  M  行数

 * @param  K  内维度

 * @param  N  列数

 */

void mat_mul_q15(const int16_t *A, const int16_t *B, int16_t *C,

                 int M, int K, int N) {

    for (int i = 0; i < M; i++) {

        for (int j = 0; j < N; j++) {

            int64_t sum = 0;

            for (int k = 0; k < K; k++) {

                sum += (int32_t)A[i * K + k] * B[k * N + j];

            }

            // 累加结果是 Q30,右移 15 位回到 Q15

            // 加 16384 实现四舍五入

            C[i * N + j] = (int16_t)((sum + 16384) >> 15);

        }

    }

}

优化版本一:循环顺序优化

上面的代码在k循环中访问 B[k*N+j],这是列访问,在行优先存储中不连续。改成 `i-k-j` 循环顺序可以改善:

void mat_mul_q15_ikj(const int16_t *A, const int16_t *B, int16_t *C,

                     int M, int K, int N) {

    // 先清零

    for (int i = 0; i < M * N; i++) C[i] = 0;

    for (int i = 0; i < M; i++) {

        for (int k = 0; k < K; k++) {

            int16_t a = A[i * K + k];

            for (int j = 0; j < N; j++) {

                // C[i][j] += A[i][k] * B[k][j]

                // 这里用 int32_t 累加 Q30,注意溢出

                int32_t prod = (int32_t)a * B[k * N + j];

                // 需要把 prod 从 Q30 转成 Q15 再累加

                // 但这样会损失精度,更好的做法是用 int64_t

                C[i * N + j] += (int16_t)(prod >> 15);

            }

        }

    }

}

注意:这个版本为了减少内存访问,牺牲了精度(每次乘积先转Q15再累加)。推荐用基础版本,精度更高。

优化版本二:2×2 矩阵专用

对于 FOC 的 Clarke/Park 变换,2×2 矩阵乘法可以完全展开:

// [c00 c01]   [a00 a01]   [b00 b01]

// [c10 c11] = [a10 a11] × [b10 b11]

void mat_mul_2x2_q15(const int16_t *A, const int16_t *B, int16_t *C) {

    int64_t s00 = (int64_t)A[0]*B[0] + (int64_t)A[1]*B[2];

    int64_t s01 = (int64_t)A[0]*B[1] + (int64_t)A[1]*B[3];

    int64_t s10 = (int64_t)A[2]*B[0] + (int64_t)A[3]*B[2];

    int64_t s11 = (int64_t)A[2]*B[1] + (int64_t)A[3]*B[3];

    C[0] = (int16_t)((s00 + 16384) >> 15);

    C[1] = (int16_t)((s01 + 16384) >> 15);

    C[2] = (int16_t)((s10 + 16384) >> 15);

    C[3] = (int16_t)((s11 + 16384) >> 15);

}

展开后没有循环,编译器可以做更好的寄存器分配,速度比通用版本快 2~3 倍。

优化版本三:3×3 矩阵专用(用于卡尔曼滤波)

void mat_mul_3x3_q15(const int16_t *A, const int16_t *B, int16_t *C) {

    for (int i = 0; i < 3; i++) {

        for (int j = 0; j < 3; j++) {

            int64_t sum = 0;

            for (int k = 0; k < 3; k++) {

                sum += (int32_t)A[i*3+k] * B[k*3+j];

            }

            C[i*3+j] = (int16_t)((sum + 16384) >> 15);

        }

    }

}

3×3 矩阵是卡尔曼滤波的典型维度(状态向量3维),手写展开可以进一步优化。

05 精度分析

定点矩阵乘法的误差来源:

误差来源

量级

说明

Q15输入量化

~0.003%

每个元素的分辨率

乘积舍入

~0.001%

每次乘积的舍入

累加舍入

~0.001%

累加后的移位舍入

总相对误差

~0.01%

取决于矩阵维度

对于卡尔曼滤波、姿态解算、神经网络推理,0.01% 的相对误差完全够用。

06 性能实测

测试平台:GD32F303VGT6 @120MHz,DWT周期计数。

测试条件

– 矩阵维度:4×4,单次调用

函数

开启FPU(周期)

关闭FPU(周期)

mat_mul_q15()

1549

1549

说明:mat_mul_q15() 完全采用整数运算,因此开启或关闭 FPU 对其耗时没有影响。

07 应用场景

 卡尔曼滤波

// 预测步骤:x = F·x

mat_mul_q15(F, x, x_pred, 4, 4, 1);

// 协方差预测:P = F·P·Fᵀ + Q

mat_mul_q15(F, P, FP, 4, 4, 4);

mat_mul_q15(FP, F_T, P_pred, 4, 4, 4);

姿态解算

// 四元数更新:q = q + 0.5·q⊗ω·dt

// 四元数乘法是4×4矩阵乘法

mat_mul_q15(q_matrix, omega, q_dot, 4, 4, 1);

神经网络推理

// 全连接层:y = W·x + b

mat_mul_q15(W, x, y, 1, N, 1);

08 源码与工程

完整的Keil工程已上传Gitee,包含:

– GD32F303VGT6的完整工程

– `mat_mul_q15` 完整源码(通用版 + 2×2 + 3×3 + 4×4)

– 性能测试代码(使用DWT周期计数)

– 精度测试代码

Gitee地址:https://gitee.com/jervis_luo/gd32_mat_mul.git

09 总结

对比维度

mat_mul_q15()

单次4×4矩阵耗时周期

1549

最大相对误差

~0.01%

是否需要FPU

否

适用芯片

M0/M3/M4/M7

三点核心收获:

1. 定点矩阵乘法的核心是Q格式缩放——Q15×Q15→Q30,累加后右移15位回到Q15。

2. 累加必须用int64_t——防止N个乘积之和溢出int32_t。

3. 执行时间固定——没有循环和分支预测失败,对实时控制非常友好。

系列文章

1. fast_sin:比官方sinf快14倍

2. fast_atan2:FOC角度计算优化:128点查表法让atan2f快8倍

3. fast_sqrt:FOC矢量幅值计算优化:256点查表+牛顿迭代,让sqrtf快2.5倍

4.fast_exp:拆整数+查表+插值,关闭FPU快9倍

5.fast_log:提取二进制指数+查表,让对数运算快11倍

6.mat_mul_q15:定点矩阵乘法,让卡尔曼滤波不再拖后腿(本文)

后记:写完这篇文章后,我把 mat_mul_q15() 用在了自己做的卡尔曼滤波姿态解算里。原本用浮点矩阵乘法时,4×4矩阵乘法每个周期要花几十微秒;换上mat_mul_q15()后,直接降到几微秒。姿态更新率从1kHz提升到了5kHz。这大概就是优化的乐趣所在。

如果你对定点数学库感兴趣,欢迎关注我。这个系列到这里已经覆盖了 sin()、atan2()、sqrt()、exp()、log() 和矩阵乘法,基本覆盖了嵌入式中最常用的数学运算。后续如果大家感兴趣,我可以继续写定点矩阵求逆、定点卡尔曼滤波或者定点神经网络推理。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 矩阵乘法的Q15定点实现,让卡尔曼滤波不再拖后腿
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!