简介这份资源是面向嵌入式开发初学者与STM32学习者的LSM303磁力计驱动程序包围绕三轴磁强计数据采集与I²C通信展开可解决电子罗盘、姿态检测等场景中磁场数据读取与解析的问题。压缩包共125个文件以35个h头文件、34个c源文件为核心配合12个o目标文件、12个d依赖文件及uvproj、uvopt等Keil工程文件另有hex、axf、map等编译产物整体约2.38MB结构完整可直接导入工程。内容涵盖I²C外设初始化、设备地址定义、数据读写函数、中断处理并涉及与MPU6050协同及卡尔曼滤波、互补滤波等数据融合思路便于理解多传感器融合的基本方法。目前已有1244人学习下载适合希望掌握STM32传感器驱动与嵌入式软硬件交互的开发者参考实践。1. 从一颗 LSM303 说起为什么 STM32 读磁力计总在“飘”很多人第一次把 LSM303 磁力计接到 STM32 上串口打印出来的三轴数据看着挺正常一算航向角就开始怀疑人生静止不动角度能自己转十几度手一靠近数值直接跳变换个房间零点又变了。这不是玄学是磁力计这个传感器本身的脾气——它测的是地磁场而地磁场强度只有几十微特斯拉和电机、扬声器、甚至 PCB 上的电源走线产生的磁场处在同一量级。LSM303 这颗芯片常见的有 LSM303DLHC、LSM303AGR、LSM303C 几个尾缀本质都是三轴加速度计加三轴磁力计的六轴组合走 I2C 或 SPI 和 STM32 通信。它便宜、好买、资料多是做电子罗盘、姿态参考、指向设备时绕不开的入门器件。但“能读出数”和“能算出可用的航向角”之间隔着校准、量程、输出速率、坐标系对齐这几道坎。这篇笔记面向的是手里已经有 STM32 开发板、想把 LSM303 磁力计程序真正跑通并用到项目里的人。我会按“先搞清楚芯片怎么工作再动手写驱动然后处理校准和航向角最后讲踩过的坑”这个顺序展开。新手可以照着代码一步步来熟手可以直接跳到参数配置和校准那几节看边界条件。2. LSM303 的寄存器地图与 STM32 的 I2C 时序先把通信跑通在写任何算法之前必须确认 STM32 能稳定读到 LSM303 的 WHO_AM_I 寄存器。这一步过不了后面全是空中楼阁。LSM303 系列不同尾缀的 I2C 从机地址和寄存器布局有差异这是第一个容易翻车的地方。2.1 先确认你手里是哪颗地址与 WHO_AM_I 的差异LSM303DLHC 的加速度计和磁力计是两个独立的 I2C 从机地址加速度计通常是 0x327 位地址左移后磁力计是 0x3C。而 LSM303AGR 把两者整合到同一组地址下加速度计 0x32、磁力计 0x3C 依然分开但寄存器映射变了。LSM303C 又不一样磁力计部分只支持 SPI 或特定模式。我一般会先写一个扫描程序把 I2C 总线上所有应答的地址打出来再对照数据手册确认。下面这段是 STM32 HAL 库下的地址扫描代码放在 main 初始化之后调用// I2C 地址扫描确认 LSM303 实际应答的从机地址 #include stm32f1xx_hal.h extern I2C_HandleTypeDef hi2c1; void LSM303_ScanBus(void) { uint8_t addr; HAL_StatusTypeDef ret; printf(Scanning I2C bus...\r\n); for (addr 1; addr 128; addr) { // HAL 库使用 8 位地址格式左移一位 ret HAL_I2C_IsDeviceReady(hi2c1, (uint16_t)(addr 1), 2, 10); if (ret HAL_OK) { printf(Device found at 7-bit address: 0x%02X\r\n, addr); } } printf(Scan done.\r\n); }这段代码的逻辑很直接逐个地址发起“设备就绪”探测收到 ACK 就打印。参数上(addr 1)是因为 HAL 库的HAL_I2C_IsDeviceReady要求传入 8 位格式地址而扫描循环用的是 7 位地址。2是重试次数10是超时毫秒数。如果扫描结果里出现 0x19 或 0x1E那可能是别的器件别急着当成 LSM303。确认地址后读 WHO_AM_I。以 LSM303AGR 为例加速度计的 WHO_AM_I 在 0x0F磁力计在 0x4F。返回值应该是 0x33 和 0x40。如果读出来是 0x00 或 0xFF先查上拉电阻——I2C 的 SDA/SCL 必须有 4.7k 到 10k 的上拉很多模块自带上拉但如果你是从裸芯片飞线忘了上拉就是这种表现。2.2 配置加速度计与磁力计的 ODR 和量程通信通了之后要设置输出数据速率ODR和满量程。这两个参数直接决定后面航向角的噪声水平和动态响应。加速度计这边我通常设 100Hz ODR、±2g 量程。磁力计设 10Hz 到 50Hz ODR、±4 高斯量程。为什么磁力计 ODR 不设太高因为磁力计内部有 offset 消除机制ODR 越高单次测量时间越短噪声反而越大。做电子罗盘这种低速应用10Hz 到 20Hz 足够。下面是以 LSM303AGR 为例的配置代码用寄存器直接写// LSM303AGR 加速度计与磁力计基础配置 #define LSM303_ACC_ADDR (0x19 1) // 7位地址0x19HAL用8位 #define LSM303_MAG_ADDR (0x1E 1) // 加速度计寄存器 #define ACC_CTRL_REG1 0x20 #define ACC_CTRL_REG4 0x23 #define ACC_OUT_X_L 0x28 // 磁力计寄存器 #define MAG_CFG_REG_A 0x60 #define MAG_CFG_REG_C 0x62 #define MAG_OUT_X_L 0x68 void LSM303_Init(void) { uint8_t data; // 加速度计100Hz ODR使能XYZ data 0x57; // 0101 0111: ODR100Hz, LPen0, XYZ使能 HAL_I2C_Mem_Write(hi2c1, LSM303_ACC_ADDR, ACC_CTRL_REG1, I2C_MEMADD_SIZE_8BIT, data, 1, 100); // 加速度计±2g高分辨率模式 data 0x80; // BDU1, BLE0, FS00(±2g), HR1 HAL_I2C_Mem_Write(hi2c1, LSM303_ACC_ADDR, ACC_CTRL_REG4, I2C_MEMADD_SIZE_8BIT, data, 1, 100); // 磁力计连续测量模式10Hz ODR温度补偿使能 data 0x00; // MD00(连续), ODR00(10Hz) HAL_I2C_Mem_Write(hi2c1, LSM303_MAG_ADDR, MAG_CFG_REG_A, I2C_MEMADD_SIZE_8BIT, data, 1, 100); // 磁力计BDU使能Z轴高量程 data 0x10; // BDU1, 其他默认 HAL_I2C_Mem_Write(hi2c1, LSM303_MAG_ADDR, MAG_CFG_REG_C, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }这里有几个参数需要解释。加速度计的CTRL_REG1写 0x57二进制是 0101 0111低三位是 XYZ 使能位中间两位是 ODR 选择100Hz 对应 0101。CTRL_REG4的 0x80 把 BDUBlock Data Update置 1这样读高低字节时不会读到一半被新数据覆盖这个位在连续读取时非常关键不设的话偶尔会出现数据错位。磁力计的CFG_REG_A写 0x00选择连续测量模式和 10Hz ODR。CFG_REG_C的 0x10 是 BDU 位。注意磁力计的 BDU 和加速度计是分开控制的别只设一个。2.3 读取三轴数据的正确姿势别在循环里反复初始化读数据的函数要尽量轻量。我见过有人在主循环里每次都重新配置一遍寄存器结果 ODR 被反复重置数据永远稳定不下来。正确的做法是初始化一次然后在定时器中断或主循环里按 ODR 周期读取。// 读取加速度计和磁力计三轴原始数据 typedef struct { int16_t acc_x, acc_y, acc_z; int16_t mag_x, mag_y, mag_z; } LSM303_Data_t; void LSM303_ReadRaw(LSM303_Data_t *out) { uint8_t buf[6]; // 加速度计连续读6字节低字节在前 HAL_I2C_Mem_Read(hi2c1, LSM303_ACC_ADDR, ACC_OUT_X_L | 0x80, I2C_MEMADD_SIZE_8BIT, buf, 6, 100); out-acc_x (int16_t)((buf[1] 8) | buf[0]); out-acc_y (int16_t)((buf[3] 8) | buf[2]); out-acc_z (int16_t)((buf[5] 8) | buf[4]); // 磁力计连续读6字节 HAL_I2C_Mem_Read(hi2c1, LSM303_MAG_ADDR, MAG_OUT_X_L | 0x80, I2C_MEMADD_SIZE_8BIT, buf, 6, 100); out-mag_x (int16_t)((buf[1] 8) | buf[0]); out-mag_y (int16_t)((buf[3] 8) | buf[2]); out-mag_z (int16_t)((buf[5] 8) | buf[4]); }关键点是寄存器地址或上 0x80。LSM303 系列支持地址自动递增但必须在读多字节时把最高位置 1否则读出来的六个字节全是同一个寄存器的值。这个坑我踩过当时串口打出来的 XYZ 完全一样查了半天以为是传感器坏了。另外HAL_I2C_Mem_Read的超时给 100ms 足够但如果你在中断里调用要确认 HAL 库的 I2C 操作不会阻塞太久。更稳妥的做法是用 DMA 或者状态机方式不过对于 10Hz 到 100Hz 的读取频率阻塞式读取完全够用。3. 磁力计校准把“飘”的根源摁住原始数据读出来只是开始。磁力计的输出里混着硬铁偏移和软铁畸变不校准的话航向角误差可以轻松超过 30 度。这一章讲清楚两种误差的来源和对应的校准方法。3.1 硬铁与软铁两种误差两套处理硬铁偏移是传感器周围永久磁体或带磁材料产生的固定磁场叠加表现是数据云团的中心偏离原点。软铁畸变是铁磁材料被地磁场磁化后产生的感应磁场表现是数据云团从球形变成椭球。硬铁校准简单把设备绕各个轴转几圈记录每个轴的最大值和最小值偏移就是 (maxmin)/2然后从原始数据里减掉。软铁校准复杂一些需要拟合椭球或者用最小二乘法求变换矩阵。我一般先用硬铁校准因为大部分消费级场景下硬铁是主要误差源。软铁校准在要求高的场合才做比如设备附近有扬声器或电机。3.2 硬铁校准的实操转圈、取极值、算偏移具体操作把 LSM303 拿在手里缓慢地绕 X、Y、Z 三个轴各转几圈同时以 10Hz 频率记录数据。转的时候要覆盖所有方向不能只在一个平面里转。记录时间大概 30 秒到 1 分钟。下面这段代码在采集阶段把每个轴的最大最小值存下来// 硬铁校准采集各轴极值 typedef struct { int16_t min_x, max_x; int16_t min_y, max_y; int16_t min_z, max_z; } Mag_Calib_t; Mag_Calib_t calib {32767, -32768, 32767, -32768, 32767, -32768}; void Mag_Calib_Update(int16_t x, int16_t y, int16_t z) { if (x calib.min_x) calib.min_x x; if (x calib.max_x) calib.max_x x; if (y calib.min_y) calib.min_y y; if (y calib.max_y) calib.max_y y; if (z calib.min_z) calib.min_z z; if (z calib.max_z) calib.max_z z; } void Mag_Calib_ComputeOffset(int16_t *off_x, int16_t *off_y, int16_t *off_z) { *off_x (calib.max_x calib.min_x) / 2; *off_y (calib.max_y calib.min_y) / 2; *off_z (calib.max_z calib.min_z) / 2; }采集完成后把偏移值存到 Flash 或全局变量里后续每次读取都减掉。注意int16_t的极值初始化用 32767 和 -32768别用 0否则如果某个轴的数据全是正的最小值会一直是 0偏移算出来就偏了。校准后的数据还要检查一下“圆度”三个轴的 (max-min)/2 应该接近相等。如果某个轴明显偏大或偏小说明软铁畸变比较严重或者转圈时没覆盖好。3.3 软铁校准的简化做法椭球拟合的最小实现完整的椭球拟合需要解一个 9 参数或 12 参数的方程组在 STM32 上跑有点重。我一般用简化版假设软铁畸变可以用一个 3x3 的对角矩阵近似只校正各轴的比例差异。做法是硬铁校准后计算每个轴的半幅值(max-min)/2然后求三个半幅值的平均值每个轴的数据乘以平均值 / 该轴半幅值。这样把椭球拉成近似球形。// 软铁简化校准各轴比例归一化 void Mag_SoftIron_Apply(int16_t *x, int16_t *y, int16_t *z, int16_t off_x, int16_t off_y, int16_t off_z, float scale_x, float scale_y, float scale_z) { float fx (float)(*x - off_x) * scale_x; float fy (float)(*y - off_y) * scale_y; float fz (float)(*z - off_z) * scale_z; *x (int16_t)fx; *y (int16_t)fy; *z (int16_t)fz; }scale_x等参数在硬铁校准后计算先求各轴半幅值half_x (max_x - min_x) / 2再求平均avg (half_x half_y half_z) / 3最后scale_x avg / half_x。这个方法不能处理轴间耦合但对于大多数没有强铁磁材料靠近的场景效果已经够用。注意校准时的环境磁场要和实际使用环境一致。在实验室校准完拿到现场用如果现场有大型金属结构偏移会变需要重新校准。4. 从磁力计数据到航向角倾斜补偿与坐标系对齐拿到校准后的磁力计数据下一步是算航向角。但磁力计测的是三维磁场矢量设备倾斜时X 和 Y 轴的读数会混入 Z 轴分量直接算 atan2 会出错。所以必须用加速度计做倾斜补偿。4.1 倾斜补偿的数学推导与代码实现设校准后的磁力计数据为 mx、my、mz加速度计数据为 ax、ay、az。先算横滚角和俯仰角roll atan2(ay, az) pitch atan2(-ax, sqrt(ay*ay az*az))然后用这两个角把磁力计数据旋转到水平面mx_h mx * cos(pitch) mz * sin(pitch) my_h mx * sin(roll) * sin(pitch) my * cos(roll) - mz * sin(roll) * cos(pitch) heading atan2(my_h, mx_h)最后把 heading 转成 0 到 360 度。如果磁力计安装方向和设备坐标系不一致还要加一个偏置角。#include math.h float LSM303_GetHeading(LSM303_Data_t *d) { float roll, pitch, mx_h, my_h, heading; // 加速度计数据转 float假设已校准 float ax (float)d-acc_x; float ay (float)d-acc_y; float az (float)d-acc_z; // 磁力计数据转 float假设已做硬铁和软铁校准 float mx (float)d-mag_x; float my (float)d-mag_y; float mz (float)d-mag_z; // 横滚和俯仰 roll atan2f(ay, az); pitch atan2f(-ax, sqrtf(ay * ay az * az)); // 倾斜补偿 mx_h mx * cosf(pitch) mz * sinf(pitch); my_h mx * sinf(roll) * sinf(pitch) my * cosf(roll) - mz * sinf(roll) * cosf(pitch); heading atan2f(my_h, mx_h) * 180.0f / 3.14159265f; if (heading 0) heading 360.0f; return heading; }这段代码里atan2f和sqrtf来自 math.h在 STM32 上编译要加-lm链接选项。如果用的是 Keil 或 IAR通常自动链接。注意atan2f的参数顺序是 (y, x)别写反。4.2 坐标系对齐芯片贴装方向决定符号LSM303 的磁力计和加速度计坐标系在数据手册里有明确标注但实际贴到 PCB 上时你可能旋转了 90 度或者反过来装。这时候读出来的 X 轴可能对应设备的 Y 轴符号也可能相反。我一般会在代码里加一层坐标映射用一个宏或配置表来定义// 坐标映射配置根据实际贴装方向修改 // 0: 不变, 1: 取反, 2: 交换到下一轴 #define MAG_MAP_X 0 #define MAG_MAP_Y 0 #define MAG_MAP_Z 0 void Mag_ApplyMapping(int16_t *x, int16_t *y, int16_t *z) { int16_t tx *x, ty *y, tz *z; #if MAG_MAP_X 1 tx -tx; #elif MAG_MAP_X 2 tx ty; // 示例X 取 Y 的值 #endif #if MAG_MAP_Y 1 ty -ty; #endif #if MAG_MAP_Z 1 tz -tz; #endif *x tx; *y ty; *z tz; }实际调试时把设备水平放置屏幕朝向自己慢慢旋转一圈看航向角是不是单调变化。如果角度跳变或者反向就是映射没配对。这个步骤没有捷径只能对着数据手册的坐标系图一点点试。4.3 航向角滤波滑动平均与互补滤波的取舍磁力计数据本身有噪声算出来的航向角会抖。最简单的处理是滑动平均取最近 N 个值求平均。N 取 5 到 10 比较合适太大响应会迟钝。如果设备同时有陀螺仪可以用互补滤波陀螺仪积分得到短期角度磁力计提供长期参考两者加权融合。没有陀螺仪的话滑动平均加一个死区角度变化小于 1 度不更新也能凑合用。// 航向角滑动平均滤波 #define HEADING_BUF_SIZE 8 static float heading_buf[HEADING_BUF_SIZE]; static uint8_t heading_idx 0; float Heading_Filter(float new_heading) { float sum 0; heading_buf[heading_idx] new_heading; heading_idx (heading_idx 1) % HEADING_BUF_SIZE; for (int i 0; i HEADING_BUF_SIZE; i) { sum heading_buf[i]; } return sum / HEADING_BUF_SIZE; }这个滤波器的缺点是角度在 0/360 边界附近会出问题比如 359 度和 1 度平均出来是 180 度。处理办法是先把角度转成矢量再平均或者用角度差累加的方式。简单场景下如果设备不会频繁跨越正北方向直接平均也能用。5. 避坑与排查LSM303 程序里最容易翻车的五个地方这一章记录我在调试 LSM303 过程中实际遇到过的五个问题每个都按“现象、原因、解决”写清楚。5.1 现象WHO_AM_I 读出来是 0x00 或 0xFF原因I2C 通信根本没建立。可能是上拉电阻缺失、地址搞错、或者供电电压不对。LSM303 的 VDD 通常是 1.8V 到 3.6VVDD_IO 要和 STM32 的 IO 电平匹配。如果 STM32 是 3.3V 而 LSM303 的 VDD_IO 接了 1.8V通信会失败。解决先用示波器看 SDA/SCL 有没有波形。没有波形查 STM32 的 I2C 初始化和引脚配置有波形但没 ACK查从机地址和上拉。上拉电阻用 4.7k 到 10k 接 3.3V别用 1k 以下否则电流太大。5.2 现象三轴数据完全一样或者只有第一个字节在变原因读多字节时没有设置地址自动递增位。LSM303 的寄存器地址在连续读模式下需要把最高位置 1否则每次读的都是同一个寄存器。解决检查HAL_I2C_Mem_Read的寄存器地址参数确保或上了 0x80。比如读加速度计从 0x28 开始传入的地址应该是 0x28 | 0x80 0xA8。5.3 现象航向角静止时缓慢漂移或者手一靠近就跳原因磁力计没有校准或者校准环境和使用环境不一致。硬铁偏移会随周围铁磁材料变化软铁畸变也会。解决重新做硬铁校准确保转圈时覆盖所有方向。如果使用环境有强磁场源考虑加磁屏蔽或者把传感器远离干扰源。软件上可以加一个变化率限制角度突变超过阈值时暂时不更新。5.4 现象倾斜补偿后航向角反而更不准原因加速度计数据没有校准或者设备在运动状态下加速度计测到的不只是重力。倾斜补偿的前提是加速度计只感受重力如果设备在加速或减速roll 和 pitch 会算错。解决静态场景下先校准加速度计做六面校准取偏移和比例。动态场景下如果设备有陀螺仪用陀螺仪积分代替加速度计算姿态。没有陀螺仪的话只能在设备相对静止时使用航向角。5.5 现象I2C 读取偶尔返回 HAL_BUSY 或超时原因I2C 总线被其他任务占用或者中断优先级配置导致 I2C 中断被延迟。HAL 库的阻塞式 I2C 操作在中断上下文里调用容易出问题。解决把 LSM303 的读取放在主循环或低优先级任务里不要放在高优先级中断中。如果必须用中断改用 DMA 方式。另外检查 I2C 的时钟频率100kHz 比 400kHz 更稳长走线时尤其明显。6. 进阶技巧用加速度计辅助磁力计做动态校准硬铁校准的偏移值不是一成不变的。设备在不同环境下使用周围铁磁材料变化会导致偏移漂移。一个实用的进阶做法是利用加速度计判断设备是否处于静止状态在静止时用磁力计数据的滑动窗口重新估计偏移。具体思路是维护一个最近 N 秒的磁力计数据缓冲区当加速度计三轴合成值接近 1g 且变化很小时认为设备静止。此时磁力计数据的均值可以近似看作当前环境的硬铁偏移。用这个新偏移和旧偏移做加权融合缓慢更新。// 动态硬铁偏移更新仅在设备静止时执行 #define MAG_WIN_SIZE 50 static int16_t mag_win_x[MAG_WIN_SIZE]; static int16_t mag_win_y[MAG_WIN_SIZE]; static int16_t mag_win_z[MAG_WIN_SIZE]; static uint8_t mag_win_idx 0; static uint8_t mag_win_full 0; void Mag_DynamicOffset_Update(LSM303_Data_t *d, int16_t *off_x, int16_t *off_y, int16_t *off_z) { // 判断静止加速度合成值在 0.95g 到 1.05g 之间 float acc_mag sqrtf((float)d-acc_x * d-acc_x (float)d-acc_y * d-acc_y (float)d-acc_z * d-acc_z); // 假设加速度计灵敏度为 16384 LSB/g±2g 量程 float acc_g acc_mag / 16384.0f; if (acc_g 0.95f || acc_g 1.05f) { return; // 设备在运动跳过 } // 存入窗口 mag_win_x[mag_win_idx] d-mag_x; mag_win_y[mag_win_idx] d-mag_y; mag_win_z[mag_win_idx] d-mag_z; mag_win_idx (mag_win_idx 1) % MAG_WIN_SIZE; if (mag_win_idx 0) mag_win_full 1; if (!mag_win_full) return; // 计算窗口均值 int32_t sum_x 0, sum_y 0, sum_z 0; for (int i 0; i MAG_WIN_SIZE; i) { sum_x mag_win_x[i]; sum_y mag_win_y[i]; sum_z mag_win_z[i]; } int16_t new_off_x (int16_t)(sum_x / MAG_WIN_SIZE); int16_t new_off_y (int16_t)(sum_y / MAG_WIN_SIZE); int16_t new_off_z (int16_t)(sum_z / MAG_WIN_SIZE); // 加权融合新偏移占 5%旧偏移占 95% *off_x (int16_t)(*off_x * 0.95f new_off_x * 0.05f); *off_y (int16_t)(*off_y * 0.95f new_off_y * 0.05f); *off_z (int16_t)(*off_z * 0.95f new_off_z * 0.05f); }这段代码的关键参数是窗口大小MAG_WIN_SIZE和融合系数。窗口取 50在 10Hz ODR 下是 5 秒的数据足够平滑掉噪声。融合系数 0.05 意味着偏移更新很慢避免设备短暂静止时被错误数据带偏。如果设备经常处于静止状态可以把系数调到 0.1。加速度计的灵敏度 16384 LSB/g 是 ±2g 量程下的典型值如果你设了别的量程这个数要改。比如 ±4g 是 8192 LSB/g。这个动态校准不能替代初始的硬铁校准它只是在初始校准基础上做缓慢修正。如果设备从一个磁场环境搬到另一个差异很大的环境还是需要重新做一次完整的转圈校准。我自己的习惯是产品出厂前做一次完整校准把偏移和比例系数存到 Flash。用户使用过程中动态校准在后台慢慢修正。如果检测到偏移变化超过某个阈值比如 20%就提示用户重新校准。这个阈值不能设太小否则会频繁误报。提示动态校准的静止判断也可以用陀螺仪如果设备有的话。陀螺仪输出接近零比加速度计判断更可靠因为加速度计在匀速运动时也可能接近 1g。最后说一个我踩过的坑动态校准的窗口数据在设备旋转时会混入不同方向的磁场导致均值没有意义。所以静止判断必须严格宁可漏更新也不要错误更新。我一般会把加速度变化率也加进去连续多次判断静止才触发更新。希望帮到你。本文还有配套的精品资源点击获取