电子指南针一倾斜就跑偏,问题藏在水平投影里

2026-11-161

把电子指南针平放在桌面上,指向看起来很稳定。手腕一抬,机身没有明显转向,屏幕上的角度却变了。此时直接给磁力计的 X、Y 读数套 atan2,算到的是传感器平面里的角度,未必是地面水平面里的航向。

解决它需要两类信息。磁力计提供磁场方向,加速度计在近似静止时提供竖直方向。本文用 ESP-SensairShuttle 的 BMI270 与 BMM350 子板说明接口,再实现一个独立 C11 航向后处理模块。改进已经在主机上编译并通过合成旋转与异常输入测试,范围是计算与有效性判定;没有把示例接入出厂固件,也没有进行整机精度实测。

先把版本和总线认清

本次在线 EDA 的主板对象是 MainBoard V1_1,传感器子板对象是 ShuttleBoard-BMI270&BMM350-V1_2。以下原理图和 PCB 都从这次实际编辑器导出。需要留意一个版本细节,子板工程树写 V1_2,PCB 丝印仍写 V1.1。本文保留这个差异,不凭丝印推断另一份工程。

IMU子板完整原理图

图1 子板 V1_2 对象的完整原理图,标题栏保留 Espressif 署名。下方局部图用于阅读引脚和数值。

U1 是 BMI270,U3 是 BMM350。中间还画有 U2 BMM150,但器件名带 NC,不能把三个器件都当作默认装配,也不能给 BMM350 发送 BMM150 的初始化序列。

BMI270接口局部

图2 BMI270 的主接口是 SDI/SDA、SCK/SCL、SDO 与 CS。ASDX/ASCX 对应的 R2、R6 为 0Ω(NC),并非默认接通的辅助总线。

U1 的 CS 通过 R7 10kΩ 上拉到 VDDIO;SDA、SCL 旁的 R3、R4 标为 4.7kΩ(NC),不能数到系统有效上拉里。磁力计 A_SDA、A_SCL 则经 R11、R9 两只 0Ω 接回主总线。查看电路时应追实际网络,不能仅凭两个芯片都支持 I²C 就猜接法。

BMM350接口局部

图3 U3 的 ADSEL 接地;C2 为 2.2µF,C5/C6 为 100nF。旁路和电源节点按图保留,不用软件补偿代替正确供电。

官方 v1.0 使用指南给出的 BMM350 地址选择为接地 0x14、接 VDDIO 0x15;本次子板图中 ADSEL 接地与前者相符。BMI270 的 SDO 还连接主板,地址要结合主控对该线的实际配置确认,不能只看子板 R1 下拉就认定始终是 0x68。驱动应先核对器件身份和初始化结果,再允许数据进入计算。

主板传感器接口

图4 MainBoard V1_1 的 Shuttle Board 局部。R46 为 0Ω,R47 为 0Ω(NC),当前图纸将 VDD_SENSOR 接到 3.3V;U8/U10 是 PCA9306 电平转换器。

官方 v1.0 指南只作功能和地址语义的参考,不与本次 V1_1/V1_2 图纸混称同版本。实际电源选择应看装配的 R46/R47,不能照旧文档想象成任意可切换的排针。

PCB 上的坐标标记不能省掉

传感器子板顶层

图5 同一子板对象的顶层铜与丝印,顶视。IMU、MAG 的轴标记提醒我们,两颗芯片的输出必须先变换到同一坐标系。

传感器子板底层

图6 同一对象的底层导出,保持从顶面向下看的方向,因此底层文字反向。它用于看布线与网络关系,不是底部实物照片。

不能把 BMI270 的 XYZ 和 BMM350 的 XYZ 不经检查直接拼起来。数据手册中的芯片轴、封装安装方向、驱动输出约定以及机身的“前方”,共同决定轴映射。映射可以是带符号的轴交换,也可以是标定得到的旋转矩阵;两个输入最终必须处于同一个右手坐标系。

下面代码采用机身前、右、下组成的右手系作为示例,前向为 (1,0,0)。这只是函数接口约定,并不是已经核实的两颗芯片寄存器轴映射。实际接入前,应分别沿已知方向转动、倾斜,核对每个轴的符号与方向,再填写映射。

不先求欧拉角,直接构造水平面

设归一化的向下向量为 d,校准后的磁场为 m,机身前向为 f。将一个向量 v 投影到水平面,可写成 v_h = v - d * dot(v,d)。

将磁场的水平投影归一化得到磁北 n,再用 e = cross(d,n) 得到磁东。把机身前向也投影到这个平面,航向就是 atan2(dot(f_h,e), dot(f_h,n))。结果转为角度并归一化到 [0,360),北为 0°,东为 90°。

这避免了先求 roll、pitch 再展开公式的符号混乱,但不会凭空补足缺失的信息。当前向几乎竖直时,它的水平指向本来就不明确;磁场几乎竖直时,磁北的水平分量也不可靠。两种情况都应该返回无效,而不是凑出一个 0°。

加速度方向还有一个容易颠倒的细节。静止加速度计测到的是比力,通常指向“上”;函数需要的是指向“下”的重力估计。若驱动输出属于这种约定,完成轴映射后要取负。不要把符号约定不同的融合输出再次机械取负。

完整的航向计算与拒绝逻辑

将下面代码保存为 compass.c。它只依赖 C11 标准库,输入的加速度单位为 g,磁场单位为 µT。阈值是本示例面向准静态场景的工程选择,不是 Bosch 保证的有效性指标。

/* Independent example, Copyright (c) 2026 Meiziwanbiancheng. MIT License. */
#include <math.h>
#include <stddef.h>
#include <stdio.h>

typedef struct { double x, y, z; } V3;
enum HeadingStatus {
    HEADING_OK, BAD_INPUT, BAD_ACCEL, BAD_FIELD,
    VERTICAL_FIELD, VERTICAL_FORWARD
};
static int finite3(V3 a) {
    return isfinite(a.x) && isfinite(a.y) && isfinite(a.z);
}
static double norm3(V3 a) { return hypot(hypot(a.x, a.y), a.z); }
static double dot3(V3 a, V3 b) { return a.x*b.x+a.y*b.y+a.z*b.z; }
static V3 scale3(V3 a, double s) { return (V3){a.x*s,a.y*s,a.z*s}; }
static V3 horizontal(V3 a, V3 down) {
    double z=dot3(a,down);
    return (V3){a.x-z*down.x,a.y-z*down.y,a.z-z*down.z};
}
static V3 cross3(V3 a, V3 b) {
    return (V3){a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};
}
/* All vectors use ONE right-handed frame. down_g points down, in g;
 * mag_ut is calibrated magnetic field in uT; forward is the chosen nose.
 * Caller guarantees fresh synchronized samples and quasi-static motion. */
int magnetic_heading(V3 down_g, V3 mag_ut, V3 forward, double *degrees) {
    if (!degrees) return BAD_INPUT;
    *degrees=NAN;                 /* An error must never look like north. */
    if (!finite3(down_g)||!finite3(mag_ut)||!finite3(forward)) return BAD_INPUT;
    double an=norm3(down_g), mn=norm3(mag_ut), fn=norm3(forward);
    if (!isfinite(an)||an<0.85||an>1.15) return BAD_ACCEL;
    if (!isfinite(mn)||mn<10.0||mn>100.0) return BAD_FIELD;
    if (!isfinite(fn)||fn<1e-12) return BAD_INPUT;
    V3 d=scale3(down_g,1.0/an);
    V3 n=horizontal(mag_ut,d);
    double nh=norm3(n);
    if (nh/mn<0.10) return VERTICAL_FIELD;
    n=scale3(n,1.0/nh);
    V3 e=cross3(d,n);
    V3 f=horizontal(scale3(forward,1.0/fn),d);
    double fh=norm3(f);
    if (fh<0.173648178) return VERTICAL_FORWARD; /* Within 10 deg of vertical. */
    f=scale3(f,1.0/fh);
    double h=atan2(dot3(f,e),dot3(f,n))*(180.0/3.14159265358979323846);
    if (h<0.0) h+=360.0;
    if (h>=360.0) h=0.0;
    *degrees=(h==0.0)?0.0:h;
    return HEADING_OK;
}

#ifndef COMPASS_LIBRARY
int main(void) {
    /* Synthetic level samples. x=forward, y=right, z=down.
     * This field corresponds to a nose pointing east, not a sensor reading. */
    V3 down={0,0,1}, magnetic={0,-30,40}, nose={1,0,0};
    double h;
    int status=magnetic_heading(down,magnetic,nose,&h);
    if (status!=HEADING_OK) { printf("invalid %d\n",status); return 1; }
    printf("magnetic heading %.3f deg\n",h);
    return 0;
}
#endif

函数用返回码区分输入异常、加速度幅值不合适、磁场异常以及两种投影退化。只要出错,角度写成 NaN。上层应检查返回码,显示“航向不可用”;若选择保留上次有效值,应同时显示其时间,不能把陈旧值当作当前读数。

0.85~1.15g 的幅值检查不能证明没有线加速度。有些运动合成后的模长仍接近 1g,因此还需要采样同步、新鲜度、静止判断,动态应用通常要结合陀螺仪和姿态融合。磁场的 10~100µT 范围检查也不能识别所有干扰,方向错误但幅值正常的磁场同样可能通过。

先验证计算,再接传感器

本次使用 Zig 0.13.0 的 C 编译器,在 Windows 上开启 -Wall -Wextra -Werror 编译并运行。以下命令在包含 zig.exe 的工具目录已配置到 PATH 时可复现最小示例;Linux 或 macOS 可用支持 C11 的 cc 并链接 libm。

zig cc -std=c11 -O2 -Wall -Wextra -Werror compass.c -o compass.exe
compass.exe
# Linux / macOS
cc -std=c11 -O2 -Wall -Wextra -Werror compass.c -lm -o compass
./compass

合成的水平朝东示例输出如下,这不是传感器实测值。

magnetic heading 90.000 deg

为避免用同一个公式检验自己,测试端独立构造“机身到北东地”的旋转矩阵,再把世界重力 (0,0,1) 和磁场 (30,0,40) 反变换到机身系,调用实际编译的 C 动态库。

测试 本次结果与含义
12 个航向 × 7 个俯仰 × 7 个横滚,共 588 组 全部通过;含 0.001°、179.999°、359.999°,俯仰覆盖 ±75°,横滚覆盖 ±150°
模型航向最大环形误差 约 5.69×10⁻¹⁴°,仅表示理想双精度输入下的数值一致性,不是指南针精度
11 组拒绝输入 自由落体、过大加速度、过弱/过强磁场、竖直/近竖直磁场、竖直/近竖直前向、零前向、NaN、无穷值,均返回对应错误且输出 NaN
改用 Y 轴作机身前向 得到预期 90°,检查接口不暗中绑定 X 轴
固定硬铁偏移的反例 期望 45° 时,加入 (12,-9,6)µT 偏移后算到约 35.705°;减去已知偏移后恢复 45°

最后一行尤其有用。它证明倾斜补偿没有替你完成磁标定。实际磁场通常需要先做硬铁偏移与软铁矩阵校正,再计算航向;不能因为一组固定偏移能够减掉,就宣称已经完成真实设备标定。

接入时按顺序排查

先保证总线、电压、器件身份与驱动初始化正确,再把两颗传感器的输出统一单位和坐标。校准磁场后,选择时间接近且仍新鲜的加速度与磁场样本;本文函数不读总线,也不替驱动管理时间戳。

在远离磁铁、喇叭、电机和大电流导线的位置,固定机身前向,逐步改变倾角。若变化仍明显,先核对坐标变换、比力符号与磁校准。磁航向与真北之间还差当地磁偏角,本文没有把它当作同一个角。

这套实现适合检查准静态磁航向计算和失效显示逻辑。它不会给出运动中不漂移、室内绝对指北或亚角度精度的承诺。整机接入、目标固件编译、磁场标定、动态性能和实测误差,需要在实际设备上另行验证。

工程与资料

硬件与 EDA 来源为 Espressif(立创账号 esp-college)的 ESP-SensairShuttle,工程页 https://oshwhub.com/esp-college/project_bkymsryt ,标注 GPL 3.0。本文采用的工程图来自同一在线工程,做了裁剪与等比缩小,未改动电路;第三方硬件与工程图继续适用其 GPL 3.0 许可,署名保留。

官方功能与接口参考 https://docs.espressif.com/projects/esp-dev-kits/zh_CN/latest/esp32c5/esp-sensairshuttle/user_guide_v1.0.html 。上游示例源码入口 https://github.com/espressif/esp-dev-kits/tree/master/examples/esp-sensairshuttle ,本次目录对应提交 4fad56d6b337281626c00ccd9a82403c54979179;本文没有复制其固件代码,也不把上游 SDK 支持声明当作本地编译结果。

本文航向模块为独立实现,示例代码采用 MIT 许可,版权所有 © 2026 美男子玩编程。验证范围为主机 C11 编译、588 组理想旋转及上述边界测试,未进行实物测量。MIT 许可文本 https://opensource.org/license/mit 。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -