简介:面向嵌入式开发者和电子爱好者的 STM32F103+HMC5883L 三轴磁力计程序包,可解决地磁测量、电子罗盘与姿态参考系统中的传感器驱动和方位解算问题。压缩包共 201 个文件,大小 3.32MB,涵盖 .c/.h 源代码、.uvproj/.uvopt 工程配置、编译生成的 .hex/.axf/.map 固件与调试文件,以及部分 .bak 备份,方便在 Keil MDK 中直接编译烧录和追溯修改。目前已有 3129 人学习浏览,程序核心包括 I2C 总线初始化、HMC5883L 寄存器读写、XYZ 三轴磁场数据转换与基于 atan2 的角度计算,并涉及数据格式和标度因子处理,覆盖从原始数据到最终航向角的完整链路。同时包含 STM32 标准外设库相关源码,帮助理解芯片时钟、GPIO 与通信外设的配置方法;整体目录结构清晰,可直接作为传感器应用开发的基础模板。
1. 从“走不直的小车”说起:STM32F103 + HMC5883L 到底解决了什么
如果你的轮式机器人走直线时总往一边偏,编码器计数正确、电机转速一致,可实际轨迹还是画歪,问题多半不在轮子上,而在“你不知道车头朝向哪里”。STM32F103 + HMC5883L 这种组合,就是给嵌入式系统补上一只“电子罗盘眼”:HMC5883L 是一颗 I2C 接口的三轴磁力计,能感知地磁场在三个轴上的分量,配合 STM32F103 做数学运算,就能在几十行代码内输出当前航向角。它解决的不是“走多快”,而是“朝哪走”,适合做 AGV 小车导航、机器人底盘闭环、手持设备指向、气象站风向标等场景。新手可以用它第一次跑通 I2C 传感器驱动,熟手则能用它快速验证一套低成本的航向参考方案。
2. HMC5883L 的寄存器与 I2C 驱动:先让芯片吐出原始数据
2.1 关键寄存器一览:增益、采样率、模式选错了会怎样
HMC5883L 是一颗很简单的外设,寄存器总共就那么十几个,真正每次都要打交道的不到五个。我当初读完数据手册第一反应是“这芯片比 IMU 好伺候多了”,但实际调的时候发现,寄存器配置选不对,后面算出来的角度全是玄学。
先看三个必须配置的控制寄存器:
| 寄存器 | 地址 | 位域 | 作用 | 常用值 |
|---|---|---|---|---|
| 配置寄存器 A | 0x00 | AVG[7:5] 采样平均次数;DO[4:2] 数据输出速率;MS[1:0] 测量模式 | 决定噪声水平和刷新率 | 0x70 = 8次平均 + 15Hz + 正常测量 |
| 配置寄存器 B | 0x01 | GN[7:5] 增益设置 | 决定量程与分辨率 | 0x20 = ±1.3Ga,1090 LSB/Gauss |
| 模式寄存器 | 0x02 | MD[1:0] 工作模式 | 连续测量、单次测量、空闲 | 0x00 = 连续测量模式 |
配置寄存器 A 里最容易被忽略的是采样平均次数。8 次平均能明显压低输出噪声,但代价是等效输出频率变低;如果做航向控制,15Hz 的输出率配上 8 次平均是大多数情况下的稳妥选择。配置寄存器 B 的增益决定了测量范围:增益越高分辨率越好,但越容易饱和。HMC5883L 的量程从 ±0.88Ga 到 ±8.1Ga 共 8 档,室内环境如果周围有电机、变压器或铁架,±0.88Ga 那一档很容易把输出顶到 32767 附近。
2.2 为什么先放弃 STM32F103 的硬件 I2C
这是我在这个项目上踩的第一个坑。STM32F103 的硬件 I2C 外设用起来让人头大,事件标志的状态机逻辑复杂,很多人照着参考代码写完后发现主设备偶尔卡在忙标志上,只能复位外设。我做了几年嵌入式,在这颗芯片上从来不用硬件 I2C 接传感器,而是用 GPIO 软件模拟 I2C。原因有三个:一是软件 I2C 时序完全可控,出了问题可以通过逻辑分析仪直接量引脚波形;二是引脚可以随便映射到任意 GPIO,画 PCB 时不用为了 I2C 功能复用绕线;三是 HMC5883L 的 I2C 时钟最高支持 400kHz,而软件模拟跑 100kHz 完全够用,逻辑简单反而更可靠。
软件 I2C 的电气连接有个硬性要求:SCL 和 SDA 两根线都要接上拉电阻到 3.3V,典型值 4.7kΩ。HMC5883L 的 VCC 供电范围是 2.16V 到 3.6V,跟 STM32F103 的 3.3V 电平可以直接对接。原理图里把传感器的 VCC 单独加一颗 100nF 去耦电容,靠近芯片引脚放,比什么软件滤波都管用。
2.3 可复用的软件 I2C 驱动代码:初始化与读写
下面这套代码是我在多个项目里复用的模板, GPIO 用 PB6/PB7,你也可以随意换引脚。关键在于 SDA 线在输出和输入之间切换,所以我把读引脚和写引脚都做了宏定义。
/* 软件 I2C 引脚定义,根据实际原理图修改 */ #define I2C_SCL_GPIO GPIOB #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_GPIO GPIOB #define I2C_SDA_PIN GPIO_Pin_7 #define SCL_H() GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN) #define SCL_L() GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN) #define SDA_H() GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN) #define SDA_L() GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(I2C_SDA_GPIO, I2C_SDA_PIN) /* 延时函数:模拟 I2C 在 100kHz 左右,SCL 半周期约 5us */ static void i2c_delay(void) { volatile uint32_t i = 8; while (i--); } /* I2C 起始条件:SCL 高电平时,SDA 产生下降沿 */ static void i2c_start(void) { SDA_H(); SCL_H(); i2c_delay(); SDA_L(); i2c_delay(); SCL_L(); } /* I2C 停止条件:SCL 高电平时,SDA 产生上升沿 */ static void i2c_stop(void) { SDA_L(); SCL_H(); i2c_delay(); SDA_H(); i2c_delay(); } /* 写一个字节,高位先出,每写完一位拉低 SCL */ static void i2c_write_byte(uint8_t dat) { for (int i = 0; i < 8; i++) { if (dat & 0x80) { SDA_H(); } else { SDA_L(); } dat <<= 1; SCL_H(); i2c_delay(); SCL_L(); i2c_delay(); } } /* 读一个字节,ack 参数:0 表示主设备发 ACK,1 表示发 NACK */ static uint8_t i2c_read_byte(uint8_t ack) { uint8_t dat = 0; GPIO_InitTypeDef gpio; /* 把 SDA 切到输入模式 */ gpio.GPIO_Pin = I2C_SDA_PIN; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(I2C_SDA_GPIO, &gpio); for (int i = 0; i < 8; i++) { SCL_H(); i2c_delay(); dat = (dat << 1) | (SDA_READ() ? 1 : 0); SCL_L(); i2c_delay(); } /* 切回输出模式,发送应答位 */ gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(I2C_SDA_GPIO, &gpio); if (ack) { SDA_H(); } else { SDA_L(); } SCL_H(); i2c_delay(); SCL_L(); SDA_H(); i2c_delay(); return dat; }这段代码的逻辑要点:起始和停止条件必须严格按照 I2C 时序来做,SCL 高电平期间 SDA 的变化才算起始/停止,否则会被从设备忽略。i2c_read_byte 里读最后一个字节时要回 NACK,告诉从设备“不用再发了”,否则从设备会一直拉着 SDA 不放,导致总线恢复不过来。
紧接着是 HMC5883L 的初始化函数。我把寄存器配置和自检放在一起,返回 0 代表设备正常:
#define HMC5883L_ADDR_W 0x3C /* 器件地址 0x1E 左移一位,写操作 */ #define HMC5883L_ADDR_R 0x3D /* 读操作 */ uint8_t hmc5883l_init(void) { /* 写入配置寄存器 A:8次平均、15Hz输出率、正常测量 */ uint8_t reg_a = 0x70; /* 写入配置寄存器 B:±1.3Ga 量程,1090 LSB/Gauss */ uint8_t reg_b = 0x20; /* 模式寄存器:连续测量 */ uint8_t mode = 0x00; uint8_t id_a, id_b, id_c; /* 读识别寄存器 0x0A~0x0C,正常芯片应读到 'H' '4' '3' */ hmc5883l_read_bytes(0x0A, &id_a, 1); hmc5883l_read_bytes(0x0B, &id_b, 1); hmc5883l_read_bytes(0x0C, &id_c, 1); if (id_a != 'H' || id_b != '4' || id_c != '3') { return 1; /* 设备不存在或地址不对 */ } hmc5883l_write_reg(0x00, reg_a); hmc5883l_write_reg(0x01, reg_b); hmc5883l_write_reg(0x02, mode); return 0; }初始化函数里先读识别寄存器这一步值得保留。很多时候芯片没反应不是焊接问题,而是你买到的物料是兼容芯片,识别寄存器内容和 HMC5883L 不一样,读出来不对就能立刻知道“此芯片非彼芯片”。如果返回 1,不要急着查代码,先用万用表量传感器 VCC 和 GND,再量 SCL/SDA 是否被拉低到 0。
读三轴原始数据的核心逻辑放在下面。HMC5883L 的数据寄存器从 0x03 开始,连续 6 个字节依次是 X 高、X 低、Y 高、Y 低、Z 高、Z 低,全部是有符号 16 位整数:
uint8_t hmc5883l_read_raw(int16_t *mx, int16_t *my, int16_t *mz) { uint8_t buf[6]; uint8_t reg = 0x03; /* 先发器件写地址和寄存器地址 */ i2c_start(); i2c_write_byte(HMC5883L_ADDR_W); if (i2c_wait_ack()) { i2c_stop(); return 1; } i2c_write_byte(reg); i2c_wait_ack(); /* 重新发送起始条件,切换为读模式 */ i2c_start(); i2c_write_byte(HMC5883L_ADDR_R); i2c_wait_ack(); /* 连读 6 字节,最后一字节回 NACK */ for (int i = 0; i < 6; i++) { buf[i] = i2c_read_byte(i == 5 ? 1 : 0); } i2c_stop(); *mx = (int16_t)((buf[0] << 8) | buf[1]); *my = (int16_t)((buf[2] << 8) | buf[3]); *mz = (int16_t)((buf[4] << 8) | buf[5]); return 0; }这段代码里 i2c_start 用了两次,第二次叫“重复起始条件”。有些从设备对“先停止再起始”的写法兼容性不好,重复起始是标准 I2C 协议推荐的做法。数据拼接时一定要先读高字节,HMC5883L 的数据寄存器是高字节在前,搞反了算出来的角度会在某个象限突然翻转。
3. 从原始数据到航向角:atan2、磁偏角和坐标系对齐
3.1 航向角公式:为什么用 atan2 而不是 atan
拿到三轴原始数据以后,求航向角的核心是一行公式:heading = atan2(Y, X)。这里 X 是传感器 X 轴方向的地磁分量,Y 是 Y 轴分量。用 atan2 而不是 atan 的原因是 atan2 能根据 X 和 Y 的符号自动判断角度落在哪个象限,输出范围是 -180° 到 +180°,省去手动补象限角的麻烦。
但这里有一个前提条件:传感器在 PCB 上的安装方向必须和你的设备坐标系对齐。如果芯片的 Y 轴朝着车头,而你想让 0° 表示车头朝北,公式就要改成 atan2(X, Y),否则会出现“车头朝北显示 90°”这种方向错位。我一般先在代码里写死一个方向定义注释:
/* 安装约定:HMC5883L 的 X 轴朝车头右侧,Y 轴朝车头前方,Z 轴朝上 */实际项目里经常出现 PCB 转 90° 安装的,与其在硬件上别扭,不如在软件里做个轴交换:把读到的 X 赋给公式里的 Y,或者把 Y 取反再代入。这个操作放在读原始数据的下一行,别改驱动层。
3.2 输出平滑与一阶低通滤波
HMC5883L 即使设置了 8 次平均,静止时的输出也会有少量抖动,算出的航向角在 0.1° 到 0.3° 之间跳。如果你的电机在转,这个抖动会被放大到几度。最简单的处理是加一阶低通滤波:
float heading_filtered = 0.0f; uint8_t first_sample = 1; float lowpass_heading(float new_heading, float alpha) { if (first_sample) { heading_filtered = new_heading; first_sample = 0; } else { heading_filtered = alpha * new_heading + (1.0f - alpha) * heading_filtered; } return heading_filtered; }alpha 取 0.2 到 0.4 之间比较常见。alpha 越大响应越快,噪声也越大;alpha 越小越平滑,但转向时会有明显滞后。还有一个容易被忽视的细节:航向角在 359° 和 0° 之间切换时,直接做低通会把 359° 和 1° 平均成 180°,这是完全错误的。处理办法是先算两个角度的差值,如果差值大于 180°,把新值加 360° 再滤波:
float diff = new_heading - heading_filtered; if (diff > 180.0f) new_heading -= 360.0f; if (diff < -180.0f) new_heading += 360.0f;我见过不止一个人在这个边界上翻车:小车原地顺时针转一圈,显示的角度从 0° 平滑到 359°,中间某处突然跳回 180°,其实就是 360° 跨越没有处理。
3.3 磁偏角补正与完整航向角计算
地磁北极和地理北极不是同一个点,不同城市的磁偏角差异能到 10° 以上。以国内为例,东部地区磁偏角在 -4° 到 -8° 之间(西偏),西部某些区域可能到 +5° 左右。这个值可以在公开的地磁模型网站按经纬度查,或者用设备在当地实测校准得到。
磁偏角补正的符号规则简单记:如果查到的磁偏角是西偏(declination 为负,例如 -5°),你需要把传感器读到的角度减去 5° 的绝对值;如果是东偏,则加上。用代码表示:
#define DECLINATION_DEG -5.0f /* 示例值:当地西偏 5° */ float calc_heading(int16_t mx, int16_t my) { float heading = atan2((float)my, (float)mx) * 180.0f / 3.14159265f; if (heading < 0) { heading += 360.0f; } heading += DECLINATION_DEG; if (heading < 0) heading += 360.0f; if (heading >= 360.0f) heading -= 360.0f; return heading; }这个函数输出 0° 到 360° 的角度,0° 表示车头指向地理北。如果你只需要相对方向(比如控制小车保持某个航向角),磁偏角可以暂时不补,因为控制量是差值,偏角会抵消掉。但如果你要把角度和地图坐标对应,磁偏角必须加上,不然导航路径会整体旋转一个角度。
4. 校准是精度的一半:圆规画圆法与椭圆修正
4.1 硬铁校准:原理与操作步骤
未校准的 HMC5883L 直接测,会发现一个典型现象:把设备水平转一圈,输出的 X 和 Y 组成一个圆心不在原点的圆。这个偏移来自电路板上的走线磁场、螺丝、电池等固定磁性物体,它们叠加在真实地磁场上,产生一个恒定偏置,叫硬铁误差。校准的目的就是把圆心拉回原点。
常见做法是“圆规画圆法”:保持设备水平,慢慢绕垂直轴转至少一整圈,期间连续采样 X 和 Y,记录最大值最小值,然后:
int16_t x_min = 32767, x_max = -32768; int16_t y_min = 32767, y_max = -32768; /* 旋转过程中持续采样,每采一次更新极值 */ void cal_update(int16_t mx, int16_t my) { if (mx < x_min) x_min = mx; if (mx > x_max) x_max = mx; if (my < y_min) y_min = my; if (my > y_max) y_max = my; } /* 旋转结束后计算偏移量 */ int16_t cal_offset_x = (x_max + x_min) / 2; int16_t cal_offset_y = (y_max + y_min) / 2;校准时的操作细节会影响精度:旋转速度要慢,保证每 10° 至少采到一个点;设备必须保持水平,倾斜超过 10° 会导致椭圆明显变形,算出来的偏移全是错的;旋转过程中人和附近不要有大块金属移动,否则会把干扰当成本底。采集完成后,把 cal_offset_x 和 cal_offset_y 存进 STM32F103 的内部 Flash,上电时读出来,每次读原始数据后先减掉偏移再做后续计算。
4.2 软铁校准:当圆变成椭圆
硬铁校完以后,如果数据点组成的是椭圆而不是正圆,说明存在软铁误差,即某些方向上的地磁被附近的铁磁性材料“放大”或“压缩”了。软铁误差用圆规画圆法处理不了,需要做椭圆拟合,得出长短轴比例,再按比例缩放各轴。
对大多数小车项目,软铁误差影响不大,可以忽略;但如果你的 PCB 上传感器旁边有大面积铺铜、屏蔽罩或者铁质外壳,就必须处理。工程上最简单的近似做法是:校完硬铁后,把旋转采集得到的 X 轴极差和 Y 轴极差的比值作为缩放因子:
/* 假设硬铁偏移已校正,raw 是减去 offset 后的值 */ float scale_x = (float)(y_max - y_min) / (float)(x_max - x_min); float corrected_x = raw_x * scale_x;这个近似做法假设椭圆长短轴与传感器 XY 轴对齐,实际中轴有夹角的话效果会打折扣。要求高的话需要用最小二乘法做椭圆拟合,求出旋转角和比例矩阵,那一套线性代数实现起来偏重,对入门项目性价比不高。先把硬铁校正好,通常能把航向精度从 ±15° 提升到 ±3° 以内,这就足够用了。
4.3 校准参数的存储与流水线验证
偏移量算出来以后,怎么存是个工程问题。常见做法有两种:直接存在 STM32F103 内部 Flash 的最后一个扇区,或者用一个外接 EEPROM。前者不需要额外芯片,但要注意 Flash 擦写寿命约 1 万次,校准不会频繁做,所以没问题。
/* 用内部 Flash 存储校准参数,放在最后一个扇区 */ #define CAL_FLASH_ADDR 0x0800F800 /* 以 64KB Flash 的芯片为例 */ typedef struct { int16_t offset_x; int16_t offset_y; float scale_x; uint16_t crc; } cal_param_t;存之前算一个简单的 CRC,读出来以后校验 CRC,不对就说明 Flash 里没有有效参数,需要触发校准流程。我习惯把“进入校准模式”做成一个独立函数:串口收到特定指令后,LED 闪烁提示操作者开始旋转设备,旋转完成后自动把参数算好并存档。这一步看起来简单,但在现场调试时非常好用,不用每次改代码重新烧录。
校准做完的验证方法也是固定的:装好设备,旋转一周,把计算出的航向角通过串口发给上位机,画成曲线应该是一条接近直线的斜坡,斜率为正、从 0° 走到 360°。如果曲线在某处出现台阶或反向,说明校准或轴方向定义有问题,别急着调参数,先检查安装方向。
5. HMC5883L 的常见坑与排查:从读不到设备到角度漂移
5.1 I2C 总线上找不到设备,地址确认没问题但通信失败
现象:初始化函数返回 1,逻辑分析仪上 SCL 有波形,SDA 也有应答信号,但识别寄存器读出来的不是H、4、3。
原因:最常见的是供电问题。HMC5883L 的 VCC 如果低于 2.16V,芯片不会工作,而 STM32F103 的 3.3V LDO 输出如果带载能力不足,上电瞬间会被拉低。另外,有些兼容芯片的识别寄存器内容和原厂不一样,读出来是别的值,但功能正常。
解决:第一步用万用表量 VCC 对地电压,确保 3.3V 稳定;第二步把 I2C 速度降到 50kHz 左右再试,有些兼容芯片对时序要求更严格;第三步是确认焊接,这类 LGA 封装的传感器引脚在芯片底部,虚焊非常隐蔽,用放大镜仔细检查或者热风枪重新吹一遍。如果上拉电阻没焊,SDA 会被从设备拉死,表现为总线一直忙。
5.2 角度在某几个方向突然跳变,数据接近 ±32767
现象:设备旋转时航向角大体正确,但转到某个方向会突然跳到异常值,或者原始数据 X、Y 中有某个值长时间停在 32767 附近。
原因:这是磁饱和。地磁场强度大约 0.5 到 0.6 Gauss,但周围如果有电机、喇叭、电流较大的导线,局部磁场能轻松超过 1 Gauss。配置寄存器 B 选的高增益档(比如 ±0.88Ga)分辨率高,但余量太小,信号一饱和输出就被钳位了。
解决:把配置寄存器 B 改成 0x20(±1.3Ga)或更低增益档。具体判断方法是把串口打印的原始数据转成 Gauss:读到的 LSB 数除以 1090(±1.3Ga 档的灵敏度)。如果最大值乘以灵敏度接近满量程,就换下一档。这个坑的隐蔽之处在于不是所有方向都饱和,只在设备转到某个角度时才触发,很容易被误判成校准问题。
5.3 电机一转数据就乱,静止时正常
现象:设备静止时航向角稳定,电机一启动,角度开始随机漂移,甚至会来回跳几十度。
原因:电机电刷或 PWM 驱动产生的变化磁场干扰了传感器。HMC5883L 本身没有屏蔽设计,对临近的交变磁场非常敏感。
解决:硬件上把传感器尽量远离电机和功率线,传感器下方 PCB 不要走大电流回路,电源加一颗磁珠再加 10uF 和 100nF 电容组合。软件上在电机 PWM 输出期间暂停采样,等 PWM 关断后再读,配合前面说的低通滤波,效果立竿见影。如果电机必须持续转,采样时刻避开 PWM 周期内的开关瞬间也能明显改善。
5.4 上电后第一次读到的数据是全 0 或明显错误
现象:程序跑起来后前几组数据全是 0,过几百毫秒才恢复正常;或者复位后读到一组乱码,再读就正常了。
原因:HMC5883L 上电后需要最多 5ms 的稳定时间,连续测量模式下也有“首个数据点可能不完整”的问题。你如果在模式寄存器写完后立刻读数据,读到的可能是芯片内部状态还没稳定时的垃圾值。
解决:初始化函数末尾加一个 50ms 延时,或者读数据之前先轮询状态寄存器 0x09 的第 0 位,该位置 1 才表示新数据就绪:
uint8_t status; do { hmc5883l_read_bytes(0x09, &status, 1); } while ((status & 0x01) == 0);这个轮询操作对连续测量模式尤其重要,因为连续模式的数据更新是异步的,不带 DRDY 判断直接读,有可能读到新旧数据混在一起的中间态。加了这行判断以后,数据稳定性提升非常明显。还有一点:改完配置寄存器后,芯片需要几个输出周期才能稳定,所以校准和正式运行之间留 200ms 的预热时间比较稳妥。
5.5 航向角整体偏移一个固定角度,校准做完也一样
现象:所有方向的角度都同时偏移同一个值,大约 5° 到 15°,而且校准后依然存在。
原因:磁偏角没有补,或者 PCB 上的元器件(比如螺丝、电池)产生了相对固定的硬铁偏置,但旋转校准做得不够完整,旋转范围不足一整圈或者速度太快采样密度不够。
解决:先用 4.3 节的验证方法确认输出曲线是否平滑;如果平滑但整体平移,那就是磁偏角问题,按 3.3 节补上当地磁偏角。如果曲线在某个象限明显上凸或下凹,说明椭圆拟合没做好,需要增大采样量。以我自己的经验,校准旋转至少做两圈慢速旋转,每圈 20 秒以上,得到的数据才算有用。
6. 让航向更稳:倾斜补偿与和运动传感器的融合
HMC5883L 单独使用时,要求设备保持水平,一旦倾斜,航向角误差会急剧变大。给系统加上加速度计(比如把 MPU6050 的加速度部分利用起来),做倾斜补偿,能把这个限制放宽到 ±30° 以内。核心思路是用加速度计算出俯仰角 pitch 和横滚角 roll,把磁力计的三轴数据投影回水平面再计算航向角:
float pitch = atan2f(-acc_x, sqrtf(acc_y * acc_y + acc_z * acc_z)); float roll = atan2f(acc_y, acc_z); float mag_x_h = mx * cosf(pitch) + my * sinf(roll) * sinf(pitch) + mz * cosf(roll) * sinf(pitch); float mag_y_h = my * cosf(roll) - mz * sinf(roll); float heading = atan2f(-mag_y_h, mag_x_h) * 180 / PI;这里的符号取决于你用的加速度计坐标定义,不同传感器轴方向不一样,需要自己在调试时验证正负号。我拿到新板子会先把板子固定在一个已知方向的支架上,分别做水平、前倾 30°、左倾 30° 三个姿态,看输出角度是否基本不变,这一步能快速确认公式的坐标系是否一致。
如果想要更高的动态稳定性,可以在磁力计和加速度计基础上加入陀螺仪,做互补滤波。陀螺仪短时间内非常准,但会积分漂移;磁力计和加速度计长时间稳定,但短期噪声大。互补滤波把两者结合,是低算力 MCU 上最划算的姿态融合方案:
float fused_heading = 0.98f * (fused_heading + gyro_z * dt) + 0.02f * mag_heading;0.98 和 0.02 是工程上常用的权重系数,实际调参时看你更信任谁:噪声主要在高频就把磁力计权重调低,漂移明显就把磁力计权重调高。这套方案不需要跑卡尔曼滤波器,普通 STM32F103 主频 72MHz 跑起来绰绰有余,也是我目前做机器人航向的首选结构。
我自己的习惯是:每次拿到新打的板子,先不接电机,把传感器焊好,刷一个最小例程,手持板子在空中慢速画几圈“8”字,同时把原始数据通过串口画成轨迹。轨迹圆不圆、圆心偏多少,一眼就能看出焊接、布局和芯片好坏。这个动作已经帮我避开了至少三次“传感器坏了但程序看着没问题”的无效调试。希望这个办法对你有用,航向传感器的精度不是调出来的,是从布线和校准里省出来的。
本文还有配套的精品资源,点击获取