ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 HAL库驱动LSM303DLH:磁力计与加速度计实战指南

STM32 HAL库驱动LSM303DLH:磁力计与加速度计实战指南

简介:这份资源面向在STM32平台上开发电子罗盘、运动追踪与导航应用的嵌入式工程师及学习者,提供LSM303DLH三轴磁传感器与三轴加速度计的完整驱动程序。压缩包共4个文件,包含2个.c源文件与2个.h头文件,整体约4KB,其中自定义I2C驱动负责底层通信,LSM303驱动负责传感器配置与数据读取,头文件则定义结构体、枚举与函数原型,便于工程直接引用。资源覆盖I2C协议实现、HAL库与自定义驱动编程、传感器量程与数据速率配置、磁感应与加速度数据读取及校准等知识点,读者可据此快速完成传感器初始化、周期采样与方向解算,为电子罗盘和运动检测功能打下基础。目前已有1072人学习下载,适合希望掌握传感器驱动移植与调试思路的开发者参考。

1. LSM303DLH 三轴磁传感+加速度驱动:从寄存器到 HAL 库的落地路径

如果你手头有一块 LSM303DLH 模块,想用 STM32 的 HAL 库把三轴磁力计和三轴加速度计同时跑起来,大概率会遇到一个尴尬局面:网上能找到的例程要么是标准库时代的古董代码,要么只调通了加速度计、磁力计那部分语焉不详。LSM303DLH 这颗芯片本身不算新,但它把磁传感和加速度集成在一个封装里,I2C 地址、寄存器映射、量程配置各有各的脾气,直接照搬别的传感器代码基本会翻车。这份驱动资源解决的就是这个问题——它把 LSM303DLH 的磁力计和加速度计两条数据链路都打通,基于 STM32 HAL 库封装,适合正在做姿态解算、电子罗盘、倾角检测的嵌入式开发者。不管你是刚上手 HAL 库的新手,还是想从标准库迁移过来的老手,下面这套流程都能直接抄作业。

2. LSM303DLH 的 I2C 通信与寄存器映射:为什么不能直接套用 MPU6050 的代码

2.1 双从机地址的坑:磁力计和加速度计不在同一个地址上

LSM303DLH 最容易被忽略的一点是:它的加速度计和磁力计在 I2C 总线上是两个独立的从机地址。加速度计部分固定为 0x32(7 位地址左移一位后的写地址),磁力计部分固定为 0x3C。很多开发者习惯性地用同一个设备地址去读所有寄存器,结果加速度数据正常、磁力数据全是 0 或者 0xFFFF,然后开始怀疑硬件坏了。

常见做法是定义两个宏,分别管理两条 I2C 事务:

/* LSM303DLH 设备地址定义,注意 7 位地址左移一位 */ #define LSM303DLH_ACC_ADDR (0x32) /* 加速度计写地址 */ #define LSM303DLH_MAG_ADDR (0x3C) /* 磁力计写地址 */ /* HAL 库 I2C 读写封装 */ HAL_StatusTypeDef LSM303DLH_WriteReg(uint8_t devAddr, uint8_t reg, uint8_t data) { return HAL_I2C_Mem_Write(&hi2c1, devAddr, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); } HAL_StatusTypeDef LSM303DLH_ReadReg(uint8_t devAddr, uint8_t reg, uint8_t *buf, uint16_t len) { return HAL_I2C_Mem_Read(&hi2c1, devAddr, reg, I2C_MEMADD_SIZE_8BIT, buf, len, 100); }

这里HAL_I2C_Mem_Write的第二个参数是 HAL 库要求的「左移后的地址」,所以宏定义里直接写了 0x32 和 0x3C。如果你用的是软件 I2C,地址要写成 0x19 和 0x1E(7 位原始地址),这一点在移植时务必确认。超时参数 100ms 是保守值,实际总线正常时几毫秒就返回,但调试阶段给宽一点能避免偶发 NACK 导致的误判。

2.2 关键寄存器清单:CTRL_REG 系列怎么配

LSM303DLH 的配置寄存器分布在两个地址空间里,加速度计侧有 CTRL_REG1_A 到 CTRL_REG4_A,磁力计侧有 CRA_REG_M、CRB_REG_M、MR_REG_M。下面这张表是我实际调通后整理的必配寄存器,照着填就能出数据:

寄存器地址推荐值作用
CTRL_REG1_A0x200x2750Hz 输出率,开启 X/Y/Z 轴
CTRL_REG2_A0x210x00高通滤波默认关闭
CTRL_REG3_A0x220x00中断不映射到引脚
CTRL_REG4_A0x230x00±2g 量程,连续更新模式
CRA_REG_M0x000x1430Hz 磁数据输出率
CRB_REG_M0x010x20±1.3 高斯量程
MR_REG_M0x020x00连续转换模式

配置顺序有讲究:先配加速度计,再配磁力计,中间加一点延时。磁力计从断电模式切到连续转换模式后,第一组数据需要等一个转换周期才有效,立刻读会拿到旧值或零值。

void LSM303DLH_Init(void) { /* 加速度计配置:50Hz,XYZ 使能 */ LSM303DLH_WriteReg(LSM303DLH_ACC_ADDR, 0x20, 0x27); /* ±2g,连续更新 */ LSM303DLH_WriteReg(LSM303DLH_ACC_ADDR, 0x23, 0x00); HAL_Delay(10); /* 磁力计配置:30Hz 输出率 */ LSM303DLH_WriteReg(LSM303DLH_MAG_ADDR, 0x00, 0x14); /* ±1.3 高斯量程 */ LSM303DLH_WriteReg(LSM303DLH_MAG_ADDR, 0x01, 0x20); /* 连续转换模式 */ LSM303DLH_WriteReg(LSM303DLH_MAG_ADDR, 0x02, 0x00); HAL_Delay(20); /* 等第一组磁数据就绪 */ }

HAL_Delay(20)这个延时不是随便写的。磁力计在 30Hz 输出率下,一个转换周期约 33ms,但首组数据通常在 10ms 内就能就绪,给 20ms 是留余量。如果你把输出率改成 75Hz,这个延时可以缩短到 10ms 以内。注意CTRL_REG4_A的 bit7 是 BDU 位,置 1 可以防止读数据时高低字节被刷新导致数据撕裂,但在 50Hz 下不置位也基本没问题,追求稳定的话建议置 1。

3. 加速度与磁场数据的读取和单位换算:从原始 LSB 到物理量

3.1 加速度计数据读取:六字节连续读与符号处理

加速度计的输出寄存器从 0x28 开始,依次是 OUT_X_L_A、OUT_X_H_A、OUT_Y_L_A、OUT_Y_H_A、OUT_Z_L_A、OUT_Z_H_A。LSM303DLH 支持地址自动递增,只要在读的时候把寄存器地址的最高位置 1(即 0x28 | 0x80 = 0xA8),就能一次性读出六个字节。HAL 库的HAL_I2C_Mem_Read在连续读时是否自动处理递增,取决于芯片是否支持,LSM303DLH 是支持的,所以直接给起始地址 0x28 | 0x80 即可。

typedef struct { int16_t x; int16_t y; int16_t z; } AxisData_t; void LSM303DLH_ReadAcc(AxisData_t *acc) { uint8_t buf[6]; /* 0x28 | 0x80 开启地址自动递增 */ LSM303DLH_ReadReg(LSM303DLH_ACC_ADDR, 0x28 | 0x80, buf, 6); acc->x = (int16_t)((buf[1] << 8) | buf[0]); acc->y = (int16_t)((buf[3] << 8) | buf[2]); acc->z = (int16_t)((buf[5] << 8) | buf[4]); }

这里拼接时先高后低,因为 LSM303DLH 是小端格式,低字节在低地址。(int16_t)强制转换不能省,否则负数会变成大正数。量程为 ±2g 时,灵敏度是 1mg/LSB,也就是说原始值 1000 对应 1g。如果你改成 ±4g 或 ±8g,灵敏度分别变成 2mg/LSB 和 4mg/LSB,换算时除数要相应调整。

3.2 磁力计数据读取:高低字节顺序与量程换算

磁力计的输出寄存器从 0x03 开始,顺序是 OUT_X_H_M、OUT_X_L_M、OUT_Z_H_M、OUT_Z_L_M、OUT_Y_H_M、OUT_Y_L_M。注意这里和加速度计不一样:磁力计是高字节在前,而且 Z 轴排在 Y 轴前面。这个寄存器顺序是 LSM303DLH 的经典坑,很多人读出来的 Y 和 Z 是反的,或者数值完全不对,就是因为按加速度计的顺序去套了。

void LSM303DLH_ReadMag(AxisData_t *mag) { uint8_t buf[6]; /* 磁力计输出从 0x03 开始,同样开启自动递增 */ LSM303DLH_ReadReg(LSM303DLH_MAG_ADDR, 0x03 | 0x80, buf, 6); /* 注意顺序:X_H, X_L, Z_H, Z_L, Y_H, Y_L */ mag->x = (int16_t)((buf[0] << 8) | buf[1]); mag->z = (int16_t)((buf[2] << 8) | buf[3]); mag->y = (int16_t)((buf[4] << 8) | buf[5]); }

量程为 ±1.3 高斯时,XY 轴灵敏度是 1100 LSB/Gauss,Z 轴是 980 LSB/Gauss。换算成微特斯拉(1 Gauss = 100 uT),XY 轴约 0.09 uT/LSB,Z 轴约 0.102 uT/LSB。实际使用中如果只做方向判断,不换算也能用,但做姿态融合时单位必须统一。

3.3 数据就绪判断:别用延时代替状态寄存器

很多例程用固定延时来等数据,比如HAL_Delay(20)后再读。这在低速下能用,但一旦输出率提高或者系统负载变重,就会读到重复数据或旧数据。正确做法是读状态寄存器:加速度计的状态寄存器是 0x27,磁力计的状态寄存器是 0x09。bit3 置位表示 ZYX 数据全部就绪。

uint8_t LSM303DLH_AccDataReady(void) { uint8_t status; LSM303DLH_ReadReg(LSM303DLH_ACC_ADDR, 0x27, &status, 1); return (status & 0x08) ? 1 : 0; } uint8_t LSM303DLH_MagDataReady(void) { uint8_t status; LSM303DLH_ReadReg(LSM303DLH_MAG_ADDR, 0x09, &status, 1); return (status & 0x01) ? 1 : 0; }

磁力计的状态寄存器 bit0 是 DRDY 位,和加速度计的 bit3 位置不同,这个细节在数据手册里写得很清楚,但容易被跳过。用状态判断后,主循环里就不需要硬延时了,数据更新更及时,也避免了 CPU 空转。

4. 避坑与排查:LSM303DLH 调试中最容易翻车的五个点

4.1 现象:磁力计读数始终为 0 或 0xFFFF

原因:磁力计没有从断电模式切到连续转换模式,或者 MR_REG_M 写入了错误的值。LSM303DLH 上电默认是断电模式,必须显式写 0x00 到 MR_REG_M 才能启动连续转换。另一个可能是 I2C 地址用错,把磁力计地址写成了加速度计地址。

解决:先用逻辑分析仪或示波器抓 I2C 波形,确认 0x3C 地址有 ACK 响应。然后单独读 MR_REG_M 寄存器,确认写入的值确实是 0x00。如果读回来是 0x03,说明芯片处于断电模式,配置没生效。

4.2 现象:加速度数据正常,但倾角计算出来抖动严重

原因:没有开启 BDU(Block Data Update)位,导致读取过程中高低字节被新数据刷新,出现瞬时跳变。另外,如果传感器安装在有振动的结构上,±2g 量程下噪声会很明显。

解决:把 CTRL_REG4_A 的 bit7 置 1,开启 BDU。同时在软件里加一个滑动平均滤波,窗口大小 4 到 8 即可,太大反而引入滞后。如果振动确实大,考虑换 ±4g 量程,牺牲一点分辨率换稳定性。

4.3 现象:磁力计 X 轴正常,Y 和 Z 轴数据互换或符号相反

原因:寄存器读取顺序搞错了。磁力计的输出顺序是 X_H、X_L、Z_H、Z_L、Y_H、Y_L,不是 XYZ 顺序。另外,如果模块焊接时芯片方向转了 90 度,物理轴和寄存器轴会对不上。

解决:对照数据手册的寄存器映射表,逐字节确认。如果确认顺序没错但方向不对,在代码里加一个轴映射宏,把物理轴和逻辑轴对应起来。符号相反的话,取反即可,但要注意这会影响到后续的姿态解算。

4.4 现象:I2C 通信偶尔失败,HAL 库返回 HAL_ERROR 或 HAL_TIMEOUT

原因:上拉电阻阻值不合适。LSM303DLH 在快速模式下对总线电容敏感,如果上拉电阻太大(比如 10k),上升沿变缓,高速通信时容易出错。另外,如果总线上挂了多个设备,地址冲突也会导致偶发失败。

解决:把上拉电阻换成 4.7k 或 2.2k,用示波器看 SCL 和 SDA 的上升沿是否干净。如果总线上有其他设备,逐个断开排查。HAL 库的超时时间可以适当放宽到 200ms,但根本解决还是要靠硬件。

4.5 现象:移植到不同 STM32 型号后,I2C 完全没反应

原因:不同 STM32 系列的 I2C 外设时钟配置不同,HAL 库的HAL_I2C_Init里时钟频率参数没改。另外,有些型号的 I2C 引脚需要重映射或者配置为开漏输出。

解决:检查hi2c1.Init.ClockSpeed是否和实际总线速率匹配,标准模式 100kHz,快速模式 400kHz。用 CubeMX 重新生成初始化代码是最稳妥的办法,手动改容易漏掉 GPIO 配置。如果用的是硬件 I2C 且引脚有重映射,确认__HAL_AFIO_REMAP_I2C1_ENABLE()之类的宏有没有调用。

5. 进阶技巧:用状态机替代阻塞读取,把驱动塞进 RTOS 任务

5.1 状态机读取框架

阻塞式读取在裸机主循环里能用,但一旦上了 RTOS 或者需要同时处理多个传感器,就会拖慢整个系统。我一般会把 LSM303DLH 的读取拆成三个状态:等待数据就绪、读取原始数据、换算并发布。这样每个状态执行时间都很短,不会阻塞其他任务。

typedef enum { LSM303_STATE_IDLE, LSM303_STATE_WAIT_ACC, LSM303_STATE_WAIT_MAG, LSM303_STATE_READY } LSM303_State_t; void LSM303_Task(void *argument) { LSM303_State_t state = LSM303_STATE_IDLE; AxisData_t acc, mag; for (;;) { switch (state) { case LSM303_STATE_IDLE: if (LSM303DLH_AccDataReady()) { state = LSM303_STATE_WAIT_ACC; } break; case LSM303_STATE_WAIT_ACC: LSM303DLH_ReadAcc(&acc); state = LSM303_STATE_WAIT_MAG; break; case LSM303_STATE_WAIT_MAG: if (LSM303DLH_MagDataReady()) { LSM303DLH_ReadMag(&mag); state = LSM303_STATE_READY; } break; case LSM303_STATE_READY: /* 发布数据到队列或全局结构体 */ state = LSM303_STATE_IDLE; break; } osDelay(1); /* 1ms 调度间隔,不占用 CPU */ } }

这个状态机的好处是:加速度和磁力计的读取解耦了,磁力计 30Hz、加速度计 50Hz,各自按自己的节奏更新,不会互相等待。osDelay(1)让出 CPU,其他任务照常跑。如果你不用 RTOS,把osDelay换成HAL_Delay(1)也能用,但实时性会差一些。

5.2 验证数据是否可信的土办法

调通之后怎么确认数据是对的?我的习惯是:把模块平放在桌面上,加速度计 Z 轴应该接近 1000(±2g 量程下对应 1g),X 和 Y 接近 0。然后绕 Z 轴慢慢旋转,磁力计的 X 和 Y 应该呈现正弦和余弦变化,幅值基本一致。如果 X 和 Y 幅值差很多,说明存在硬磁干扰,需要做椭圆拟合校准。这个验证方法不需要额外工具,上手就能做。

5.3 从 LSM303DLH 迁移到 LSM303AGR 的注意事项

如果你后面要换用 LSM303AGR,寄存器映射和 LSM303DLH 不完全兼容。AGR 的磁力计部分换成了不同的工艺,量程和灵敏度都变了,CTRL_REG 的位定义也有调整。迁移时不要只改设备地址,配置寄存器的值要重新对照数据手册算一遍。我一般会在驱动层加一个芯片型号宏,把差异部分隔离在初始化函数里,上层的数据读取接口保持不变。

从那以后我每次调新传感器,都强制先读一遍 WHO_AM_I 寄存器确认通信正常,再动配置寄存器。这个习惯帮我省了很多来回排查的时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进