STM32F103+HMC5883L电子罗盘实战:从I2C读取到校准与航向角计算

发布时间:2026/9/2 15:52:57
STM32F103+HMC5883L电子罗盘实战:从I2C读取到校准与航向角计算 简介这是一份基于STM32F103与HMC5883L的三轴磁力计驱动源码工程面向嵌入式初学者及电子罗盘、地磁导航应用开发者解决I2C/SPI通信配置、磁场数据读取和俯仰角、偏航角、滚转角解算等关键问题。工程共201个文件压缩包仅3.32MB包含完整Keil MDK工程c/h为源程序uvproj/uvopt为工程配置hex/axf为可烧录与调试映像o/lst为编译中间产物sct/ini等为链接与运行配置另有启动文件、标准外设库与应用代码分层组织便于快速查找与定位。目前已有3123人学习与下载。程序采用标准外设库编写涵盖HMC5883L寄存器映射、16位三轴数据解析、atan2角度换算及I2C底层驱动同时附有stm32f10x_tim.c、stm32f10x_flash.c等官方库文件便于对照学习外设初始化。非常适合课程设计、毕业设计、项目原型开发与传感器驱动初步学习也可快速移植到同类Cortex-M3平台。 前阵子帮人看一块 STM32F103HMC5883L 的电子罗盘板子对方说角度读数像喝醉了酒转一圈回来数字完全对不上。我扫了一眼代码I2C 驱动写得很规矩数据也确实读出来了问题出在他把原始值直接套公式算角度完全没做校准而且模块旁边有一条大电流的电源走线。这类问题在“快速跑通”的项目里几乎天天见。其实“F103 读 HMC5883L”这件事芯片手册和网上例程一大堆但真正要把一个可靠的电子罗盘做出来里面藏着一串细节I2C 怎么读才不容易出错、原始数据要不要滤波、校准为什么是必经之路、角度怎么算才不飘以及真实项目里怎么把它和低功耗、掉电保存这些需求合到一起。这篇文章就把这些串起来讲一遍适合正在做航向测量、小车导航、手持罗盘或者想把手头 F103 板子玩出新花样的朋友。1. 为什么这么多年过去这对组合还是那么经典1.1 便宜、够用、生态成熟的三角平衡STM32F103 这块芯片大家太熟了Cortex-M3 内核72MHz 主频I2C、SPI、USART、ADC 应有尽有C8T6 这种型号几块钱一片跑个姿态解算绰绰有余。HMC5883L 是霍尼韦尔的经典三轴磁阻传感器I2C 接口3.3V 供电量程从正负 0.88Ga 到正负 8.1Ga 可以配十多年前就被大量用在航模、自平衡车、电子罗盘和手持设备里。把这两颗东西放一起基本就是最省事的磁航向参考方案。1.2 它和陀螺仪不一样磁力计看的是“绝对方向”很多人第一次用 HMC5883L 会拿它跟 MPU6050 的陀螺仪比其实两个东西解决的问题完全不同。陀螺仪测角速度角度是靠积分算出来的时间一长必然漂这是所有惯性器件的通病。磁力计不一样它测的是地磁场方向地球磁场像一个稳定的大参考系读出来的数据是绝对方向误差不会随时间累积。所以九轴方案里磁力计的核心作用就是给陀螺仪的航向角做漂移修正。HMC5883L 内部是磁阻传感结构四个磁阻电阻组成惠斯通电桥地磁场变化让电桥输出微小电压差经过片内放大和 ADC 之后变成三轴数字量。这三个数值就是地磁场在 X、Y、Z 三个方向上的投影。如果你只是做一个指南针功能磁力计单用就行如果你想做四轴或者 AGV 小车的航向保持磁力计加上陀螺仪才是完整方案。下面讲的内容主要以“单独用 HMC5883L 做水平罗盘”为主线但原理和坑点在组合方案里同样适用。2. I2C 读数不只是“把数据读出来”寄存器细节与软件 I2C 的坑2.1 器件地址与寄存器地图HMC5883L 的 7 位 I2C 地址固定是 0x1E换算下来写地址是 0x3C读地址是 0x3D。常用寄存器不多我列个表看比较直观寄存器地址名称作用0x00Configuration Register A采样平均次数、数据输出速率0x01Configuration Register B增益设置决定量程0x02Mode Register连续测量/单次测量/空闲模式0x03-0x04数据输出 X16 位有符号高位在前0x05-0x06数据输出 Z16 位有符号高位在前0x07-0x08数据输出 Y16 位有符号高位在前0x09Status Registerbit0 为 1 表示数据已就绪注意数据寄存器顺序是 X、Z、Y不是 X、Y、Z。很多人第一次读数据发现 Y 和 Z 反了正是栽在这里。另外所有多字节数据都是高位在前拼数据时要左移 8 位再或上低字节。2.2 增益怎么配默认不一定适合你Configuration Register B 的低 5 位控制增益。默认值 0x20 对应正负 1.3Ga 量程增益约 1090 LSB/Ga。对于一般室内环境地磁场强度大约 0.25 到 0.65Ga默认量程其实是够用的。但如果板子上有电机、扬声器这类会产生附加磁场的器件或者在精度要求不高的场景里追求更大动态范围我会把增益调到 0x60也就是正负 2.5Ga此时增益约 660 LSB/Ga读数范围大不容易饱和。量程和分辨率的取舍看应用场景常用的链路如下0x20正负 1.3Ga分辨率约 0.92mGa适合干净环境下的精密罗盘0x60正负 2.5Ga分辨率约 1.52mGa适合板载电机且环境磁场偏强的场合0x80正负 4.0Ga分辨率约 2.27mGa适合磁场很脏的工业环境我自己的习惯是先看安装位置周边有没有明显磁源再决定量程。一上来就抄默认配置等到饱和了再回头查反而浪费时间。2.3 为什么我建议用软件 I2CSTM32F103 的硬件 I2C 从标准库时代就被调侃“不稳定”实际用起来主要问题是状态机过于复杂总线忙标志清不掉、从机 NACK 之后状态卡死、事件标志判断顺序不对就超时。HAL 库把它拆成了中断、轮询、DMA 三套接口表面上更清晰了但底层那套状态机问题还在遇到 SCL 被拉低或者从机复位依然可能挂住。HMC5883L 这种慢速器件工作频率 100kHz 到 400kHz 都行GPIO 模拟 I2C 反而更可控。软件 I2C 的好处是出了问题可以直接用逻辑分析仪抓引脚电平时序哪一步不对一眼就能看出来坏处是占用两个 GPIO 和 CPU 时间但在 72MHz 主频下完全不是问题。模拟 I2C 的关键是 SCL 和 SDA 都要配成开漏输出外部各接一个 4.7kΩ 上拉到 3.3V这样既能输出低电平又能把高电平交给上拉电阻去拉避免两个器件互相打架。读一组数据的大致代码如下void hmc_read_data(int16_t *mx, int16_t *my, int16_t *mz) { uint8_t buf[6]; i2c_start(); i2c_send_byte(0x3C); // 器件写地址 i2c_send_byte(0x03); // 从 X MSB 寄存器开始 i2c_start(); // 重复起始 i2c_send_byte(0x3D); // 器件读地址 for (int i 0; i 6; i) { buf[i] i2c_read_byte(i 5); // 前 5 字节回 ACK最后一字节回 NACK } i2c_stop(); *mx (int16_t)((buf[0] 8) | buf[1]); *mz (int16_t)((buf[2] 8) | buf[3]); *my (int16_t)((buf[4] 8) | buf[5]); }注意读回数据后的符号处理。地磁场在某个方向可能会让某个轴的读数为负值所以一定要把拼接结果转成 int16_t不能直接用无符号数去算角度否则负值会变成很大的正数角度完全错乱。2.4 读数时机与噪声处理HMC5883L 的数据寄存器随时可读但如果不看数据是否更新可能读到新旧混合的数据。最稳的做法是每次都先读 Status Register检查 bit0 是否为 1为 1 表示这次数据已经刷新。省掉这一步会怎样在连续测量模式下输出速率设成 75Hz你每隔 10ms 读一次大概率会连读两遍旧数据角度表现就是“一顿一顿的”。如果你想省掉状态寄存器的读取那就把读取间隔控制在输出周期以上并且宁可慢一点也不要读得太勤。噪声方面HMC5883L 的原始数据在干净电源下其实还算平坦但一旦板上有 DCDC 开关噪声或者模块离电机近低字节就会跳动。处理方案很简单连续读 8 到 16 次做滑动平均。以 16 次平均为例配合 75Hz 输出率有效更新率大约是 4.7Hz适合静态或者慢速转动的场景。要是设备转动很快平均次数太多会让角度滞后这个要根据实际动态来调没有万能参数。3. 校准是绕不过去的坎圆心偏移、标度因子与椭球拟合3.1 不校准会怎样HMC5883L 刚上电读出来的原始数据直接拿去算角度大概率是不准的。把板子水平旋转一圈记录 X 和 Y 的原始值并画在坐标纸上你会看到两种情况要么圆心不在原点要么形状是扁的椭圆。前者叫硬铁偏差来源于板子上的恒定磁场比如走线电流、扬声器磁铁、电机、附近钢制螺丝等这些磁场叠加在地磁场上相当于给传感器加了个固定偏置。后者叫软铁偏差来源于磁性材料对地球磁场方向的“扭曲”它让某个轴上的响应变大或变小直观表现就是圆变成椭圆。不校准的后果有多严重我实测过一块带步进电机的板子电机静止时转一圈算出来的航向角误差大约十五度电机通电后最大误差能干到三十度以上。这种程度的角度误差放在小车直线行驶或者无人机航向保持上基本是灾难。3.2 最实用的平面校准做法转圈找偏移如果你的设备只做水平面使用那最省事的校准就是水平转圈采集数据。具体操作是让板子水平放置绕垂直轴慢慢旋转至少一整圈最好转两三圈过程中以固定频率采集 X、Y 数据记录最大值和最小值。然后计算偏移量int32_t offset_x (x_max x_min) / 2; int32_t offset_y (y_max y_min) / 2;这里可以顺便算出标度因子把椭圆拉回正圆float scale_x (x_max - x_min) / 2.0f; float scale_y (y_max - y_min) / 2.0f; float fx (x - offset_x) / scale_x; float fy (y - offset_y) / scale_y;正常地磁场环境下scale_x 和 scale_y 不应该差太多如果差到 1.5 倍以上说明传感器附近有比较强的软磁材料光做偏移矫正不够还得考虑标度归一化也就是上面这条代码的用处。我见过不少项目偷懒只做偏移结果椭圆长轴和短轴相差 20% 以上角度精度依然上不来。3.3 三维场景要上椭球拟合如果设备会在三维空间里随便翻转比如手持设备或者无人机平面校准就不够用了。这种情况下磁力计的数据点分布在一个三维椭球面上理想球心在原点、半径恒定实际是椭球并且球心偏移。校准目标就是算出椭球中心和三个轴上的缩放系数把点云修正成标准球。常用方法是最小二乘椭球拟合网上有现成算法也可以用 Matlab 或者 Python 里的 pyField 等工具先离线拟合出参数再用现成矩阵写进 MCU 里跑。这块展开讲会很长这里我只给一个提醒不做三维校准时设备倾斜超过二三十度航向角误差会剧烈增大单纯靠算法补偿加速度计倾角也救不回来根源就是磁力计的数据源本身没校准。3.4 校准参数一定要存进 Flash校准做完参数不能丢否则每次上电都要拿着板子转圈不现实。STM32F103 内部 Flash 正好能干这活。我一般把校准参数存在最后一页不用单独外接 EEPROM。以 C8T6 为例Flash 共 64KB最后一页从 0x0800FC00 开始。写入前先擦除整页然后按半字写入#define CAL_FLASH_ADDR 0x0800FC00 FLASH_Unlock(); FLASH_ErasePage(CAL_FLASH_ADDR); FLASH_ProgramHalfWord(CAL_FLASH_ADDR, (uint16_t)(int16_t)offset_x); FLASH_ProgramHalfWord(CAL_FLASH_ADDR 2, (uint16_t)(int16_t)offset_y); FLASH_ProgramHalfWord(CAL_FLASH_ADDR 4, (uint16_t)(int16_t)offset_z); FLASH_Lock();上电时读出来做合法性判断比如某个偏移量明显超过正常范围比如正负几千以上就认为参数无效需要重新校准。需要提醒的是Flash 有擦写寿命和页大小限制调试阶段频繁校准、频繁写 Flash 没问题但别在每次测量后都存一遍会让 Flash 寿命大打折扣。4. 从三轴数据到航向角不是一行 atan2 就完事的4.1 角度公式与方向定义水平放置时地磁场在水平面上的分量可以用 X 和 Y 表示。航向角用 atan2 计算它能自动处理四个象限float heading atan2f((float)fy, (float)fx) * 180.0f / 3.14159265f; if (heading 0.0f) { heading 360.0f; }注意这里用的是校准后的 fx 和 fy不是原始值。另外方向定义和模块丝印方向强相关有的模块把 X 朝前有的模块把 Y 朝前装到产品里还要考虑朝向偏差最终角度是加 90 度还是 180 度最好先用一个已知方向标定一次在代码里做个常量偏移。这个“标定偏移”和传感器校准是两个独立概念别混在一起。很多人改传感器位置之后忘了改方向常量角度绕了一圈都不对其实是丝印方向和代码逻辑对不上。4.2 磁偏角把“磁北”换算成“真北”HMC5883L 指向的是磁北不是地理北极。磁北极和地理北极之间存在一个角度差叫磁偏角不同地区差异很大从西偏二十度到东偏十几度都有而且在缓慢漂移。如果应用只要求相对航向比如保持直线、返航角度差磁偏角可以暂时不管如果要求真北方位角就必须查当地磁偏角并修正float declination 5.0f; // 正值表示磁北偏东 heading declination; if (heading 360.0f) { heading - 360.0f; } if (heading 0.0f) { heading 360.0f; }磁偏角一般用在线 API 或者查表获得东经西经、北纬南纬符号规则很容易搞混建议在代码注释里写明自己当时采用的坐标系约定。4.3 倾斜补偿是个大话题设备一旦倾斜只看 X 和 Y 算出来的角度就废了。道理很简单水平时地磁场的水平分量全部落在 X-Y 平面里倾斜后部分磁力线分量跑到 Z 轴上去了X-Y 平面里的投影变短直接用 atan2(fy, fx) 算出来的角度就偏离真实航向。补偿方案是用加速度计算出倾斜角再把磁力计三轴数据通过旋转矩阵变换到水平坐标系最后取变换后的水平分量。这个计算在 STM32F103 上跑完全没压力但前提是磁力计三轴都做了校准。很多人在 MPU6050 HMC5883L 的组合方案里发现融合出来的航向在倾斜时飘根子往往不是融合算法不行而是磁力计没校准或者安装位置有磁干扰。4.4 滤波与响应速度的平衡磁力计数据不像陀螺仪那样有漂移但噪声和突然跳变依然存在。我常用的滤波有两种滑动平均和一阶低通。滑动平均适合输出速率固定的场合简单可靠缺点是有固定延迟一阶低通实现起来更轻量angle_f 0.9f * angle_f 0.1f * heading;系数 0.1 是我在 50Hz 输出速率下常用的起始值对应截止频率大约 1Hz 左右静态时角度很稳缓慢转动也能跟上。如果你要做快速旋转的小车或者手持云台这个系数要做大一点比如 0.3 到 0.5否则转快了角度会严重滞后。滤波系数没有银弹我的习惯是先跑一版数据看波形再决定系数。5. 工程化落地最小系统、FreeRTOS、低功耗与那些会咬人的坑5.1 最小系统供电布局比选型更要命STM32F103 最小系统大家都会画8MHz 晶振加两个 20pF 负载电容复位电路BOOT0 下拉到 GND3.3V 供电配 0.1uF 和 10uF 去耦电容。但 HMC5883L 这种磁传感器PCB 布局比原理图更重要。模块底下不要走大电流电源线尤其是电感、DCDC 开关节点、电机供电线。我见过一块板MCU 旁边过了一条 1A 的电源走线校准之后偏移量特别大而且电流一变角度就跟着变。这类问题靠软件很难完全消除只能调整布局。如果结构上没法避开至少用覆铜把传感器区域单独隔离或者把传感器换成带屏蔽罩的版本。5.2 在 FreeRTOS 里怎么安排读取任务项目一上 RTOSHMC5883L 的读取就有了并发问题。我的做法是开一个单独的任务负责周期读磁力计读到的数据通过队列发给其他需要航向角的任务。I2C 总线如果还挂了别的传感器比如气压计、光照度传感器必须加互斥量防止两个任务同时操作 I2C。软件 I2C 用 GPIO 模拟时序敏感读取过程中不能被更高优先级任务抢占太久否则 SCL 电平会被拉长传感器可能误判数据。一个土办法是在模拟 I2C 的关键时序段关一下调度器或者把磁力计读取任务的优先级设得较高反正读取一次也就几十微秒对系统响应影响很小。5.3 低功耗模式下怎么玩HMC5883L 支持单次测量模式模式寄存器写 0x01 触发一次测量测量完成自动回 idle。这个特性跟 STM32F103 的 Stop 模式搭配非常合适。实际流程是MCU 先唤醒初始化 I2C给 HMC5883L 发单次测量命令等数据就绪后读回来然后立刻把 I2C 外设关闭、GPIO 拉低再进入 Stop 模式。唤醒源可以用 RTC 闹钟或者外部按键中断。Stop 模式下外设时钟会关闭所以唤醒后不仅要重新初始化 I2C还要重新设置 GPIO 的复用功能。我这里踩过的坑是只初始化了 I2C忘记把 SCL/SDA 的 GPIO 重新配置成开漏模式结果第二次唤醒后 I2C 时序完全不对。5.4 几个高频坑第一GPIO 耐 5V 问题。STM32F103 的 GPIO 标称可以容忍 5V很多人就想当然把 I2C 上拉电阻接到了 5V。F103 本身死不了但 HMC5883L 是纯 3.3V 器件逻辑高电平超过它的绝对最大额定值会烧坏。所以 I2C 上拉必须接 3.3V不能接 5V。用逻辑分析仪抓波形时也要注意探头地线不要形成额外回路否则可能引入噪声。第二用内部晶振的问题。HMC5883L 数据读取本身对主频精度不敏感HSI 的 1% 误差完全不影响 I2C 通信。但如果你同时用 USART 打印调试数据HSI 的误差会让波特率产生偏差115200 下偶尔出现乱码9600 下问题不大。如果是产品化项目还是建议用外部晶振省去一堆诡异问题。第三掉电保存的细节。写 Flash 前必须关中断否则写一半被打断程序会跑飞。调试时要加一个“恢复出厂校准”的测试入口比如按住按键 3 秒重新执行校准流程否则你把参数写进 Flash 后想去掉校准参数会很麻烦。最后分享一个我自己的习惯校准参数和传感器方向常量一定要写成宏或者结构体放在代码最显眼的地方注释写清楚“哪个轴朝前、磁偏角用了多少度、测出来的偏移量是多少”。这种板子只要改一版硬件丝印方向变了或者模块位置挪了旧的校准参数就是废的。我见过太多人拿着之前的配置去调新板子怀疑这怀疑那最后发现就是方向定义反了。把这些信息固化在代码里换板子时第一件事就是核对它们能省下大把调板时间。本文还有配套的精品资源点击获取