
简介这份资源面向使用STM32进行嵌入式开发的初学者与工程师聚焦GY87九轴传感器模块的数据读取帮助解决多传感器I2C通信配置与数据采集问题。GY87集成了MPU6050六轴姿态传感器、HMC5883三轴地磁传感器和BMP180气压温度传感器可测量加速度、角速度、磁场强度与气压高度常用于无人机导航、物联网设备等场景。资源包共6个文件包含3个.h头文件与3个.c源文件分别对应MPU6050、BMP180、HMC5883三个传感器的驱动实现压缩包约4KB结构精简便于直接移植到STM32工程中。目前已有1168人学习下载。通过阅读这些驱动代码读者可以掌握STM32的I2C接口配置、传感器寄存器读写、数据校准与滤波处理等关键环节理解中断服务程序在实时采集中的作用并快速搭建起环境感知模块为姿态解算与导航应用打下基础。1. 从一块吃灰的 GY-87 说起STM32 读九轴数据的真实门槛手头有块 GY-87焊好排针插在面包板上STM32 的 I2C 也接好了结果HAL_I2C_Mem_Read一跑就返回 HAL_ERROR或者读出来的加速度永远是 0——这事我见过太多次。GY-87 本身不复杂一块板子上挂了 MPU6050三轴加速度 三轴陀螺仪和 HMC5883L三轴磁力计前者走 I2C 地址 0x68后者走 0x1E两颗芯片共用一组 SCL/SDA。真正卡人的不是“怎么读”而是初始化顺序、量程配置、数据拼接符号、以及 STM32 硬件 I2C 那几个玄学时序。这篇笔记就按我实际调通的顺序把 STM32 读 GY-87 从寄存器配置到姿态解算的完整链路拆开代码可以直接抄参数可以照着改坑我替你踩过一遍。适合刚上手 IMU 的嵌入式新手也适合从软件模拟 I2C 切到硬件 I2C 时被时序搞烦的老手。2. GY-87 的硬件底子与 STM32 侧选型为什么先搞清地址和上拉2.1 板载两颗芯片的地址与总线关系GY-87 不是一颗芯片是 MPU6050 HMC5883L 的拼板。MPU6050 的 7 位地址由 AD0 引脚决定AD0 接地是 0x68接 VCC 是 0x69GY-87 出厂默认 AD0 下拉所以写地址是0x68 1 0xD0读地址0xD1。HMC5883L 固定 7 位地址 0x1E写0x3C、读0x3D。两颗芯片的 SCL/SDA 在板上已经并联你只需要从 STM32 引一组 I2C 过去。这里有个容易忽略的点MPU6050 支持 400kHz 快速模式HMC5883L 手册标称最高 400kHz 但实际在 400kHz 下偶发丢数据我一般把总线降到 100kHz 或 200kHz 跑稳定性优先。另外 GY-87 模块自带上拉电阻通常 4.7kΩ如果你用的 STM32 开发板 I2C 引脚上已经有 10kΩ 上拉并联后等效阻值会偏小波形上升沿变陡但功耗略增一般不影响如果模块没上拉有些精简版必须在 SCL/SDA 各接一个 4.7kΩ 到 3.3V否则读出来全是 0xFF。2.2 硬件 I2C 还是软件模拟 I2C这是第一个选型分叉。STM32F1 系列的硬件 I2C 有已知的“死锁”问题——总线被拉低后无法自动恢复需要手动切换 GPIO 模式发 9 个时钟脉冲解锁。F4/H7/L4 系列硬件 I2C 已经好很多但如果你用的是 F103C8T6 这类经典板我建议直接上软件模拟 I2C用两个普通 GPIO 翻转时序完全可控出问题也好查。判断标准很简单主频低于 72MHz、I2C 总线上挂了两个以上从机、或者你已经被HAL_I2C_IsDeviceReady卡过——选软件模拟。反之 F4 以上、只挂 GY-87 一颗模块硬件 I2C 配 DMA 更省 CPU。下面代码以软件模拟 I2C 为主因为它跨系列通用硬件 I2C 的配置差异我在 2.3 里单独说。2.3 软件模拟 I2C 的 GPIO 配置与延时软件 I2C 的核心是开漏输出 外部上拉STM32 端把引脚配成GPIO_MODE_OUTPUT_OD这样输出 0 时拉低输出 1 时释放总线由上拉电阻拉高天然支持多设备。延时用__NOP()或简单循环100kHz 下半周期约 5usF103 72MHz 下循环 30 次左右。// 软件 I2C 引脚定义按你的接线改 #define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PIN GPIO_PIN_7 #define I2C_GPIO_PORT GPIOB // 开漏输出初始化 void I2C_GPIO_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin I2C_SCL_PIN | I2C_SDA_PIN; gpio.Mode GPIO_MODE_OUTPUT_OD; // 开漏必须 gpio.Pull GPIO_PULLUP; // 内部弱上拉外部仍建议接 4.7k gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(I2C_GPIO_PORT, gpio); HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN | I2C_SDA_PIN, GPIO_PIN_SET); } // 约 5us 延时100kHz 用 static void i2c_delay(void) { for (volatile uint8_t i 0; i 30; i) { __NOP(); } } // 起始条件SCL 高时 SDA 由高变低 void I2C_Start(void) { SDA_H(); SCL_H(); i2c_delay(); SDA_L(); i2c_delay(); SCL_L(); i2c_delay(); }逻辑说明开漏模式下写 1 不是真的输出高电平而是释放引脚靠上拉电阻把线拉高这是 I2C 多主多从的基础。i2c_delay里的循环次数决定总线速率30 次是我在 72MHz F103 上实测 100kHz 左右的值换主频要重新标定。起始条件必须在 SCL 为高时拉低 SDA顺序反了从机不认。参数说明GPIO_SPEED_FREQ_HIGH在 F1 上对应 50MHz 输出速度软件 I2C 其实用不到这么快但设高了不会错。内部上拉约 40kΩ太弱只能救急正式板子外部 4.7kΩ 不能省。3. MPU6050 初始化与六轴数据读取寄存器顺序不能乱3.1 复位、唤醒与量程配置MPU6050 上电后处于睡眠模式必须先把PWR_MGMT_10x6B的 SLEEP 位清零。我习惯先复位再唤醒写 0x80 到 0x6B 触发复位延时 100ms再写 0x00 唤醒并选内部 8MHz 时钟。时钟源选内部 RC 会有温漂对姿态解算要求高的话选 PLL with X gyro写 0x01。量程配置决定后面数据换算系数。加速度计ACCEL_CONFIG0x1C设 ±2g 时灵敏度 16384 LSB/g±4g 是 8192±8g 是 4096±16g 是 2048。陀螺仪GYRO_CONFIG0x1B设 ±250°/s 时 131 LSB/(°/s)±500 是 65.5±1000 是 32.8±2000 是 16.4。新手常犯的错是量程设了 ±2g 却按 4096 换算结果数据差一倍。#define MPU6050_ADDR 0xD0 // 7位地址 0x68 左移一位 // 写一个寄存器 uint8_t MPU_WriteReg(uint8_t reg, uint8_t data) { I2C_Start(); if (I2C_SendByte(MPU6050_ADDR) ! 0) { I2C_Stop(); return 1; } if (I2C_SendByte(reg) ! 0) { I2C_Stop(); return 1; } if (I2C_SendByte(data) ! 0) { I2C_Stop(); return 1; } I2C_Stop(); return 0; } // 初始化序列顺序不能换 uint8_t MPU6050_Init(void) { MPU_WriteReg(0x6B, 0x80); // 复位 HAL_Delay(100); MPU_WriteReg(0x6B, 0x01); // 唤醒选 X 陀螺 PLL MPU_WriteReg(0x1C, 0x00); // 加速度 ±2g MPU_WriteReg(0x1B, 0x00); // 陀螺 ±250°/s MPU_WriteReg(0x1A, 0x03); // DLPF 44Hz抑制振动噪声 MPU_WriteReg(0x19, 0x04); // 采样率 1kHz/(14)200Hz HAL_Delay(50); // 读 WHO_AM_I 验证应为 0x68 uint8_t id 0; MPU_ReadRegs(0x75, id, 1); return (id 0x68) ? 0 : 1; }逻辑说明复位后必须延时芯片内部要重新加载寄存器默认值延时不够会出现后续写入被忽略。0x1A的 DLPF 配置很关键不配的话陀螺仪噪声大姿态解算会抖。0x19采样率分频200Hz 对大多数应用够用配合 DLPF 44Hz 不会混叠。参数说明0x6B写 0x01 选 PLL比内部 RC 稳但上电后要等 PLL 锁定所以前面复位延时 100ms 不能省。DLPF 值 0x03 对应 44Hz 带宽、1kHz 采样如果你做四轴飞行器振动大可以设 0x0421Hz更狠一点。3.2 突发读取 14 字节与有符号拼接MPU6050 的加速度、温度、陀螺仪数据从 0x3B 开始连续排列共 14 字节加速度 X/Y/Z 各 2 字节、温度 2 字节、陀螺仪 X/Y/Z 各 2 字节。用突发读一次拿完避免分次读时数据不同步。typedef struct { int16_t ax, ay, az; int16_t gx, gy, gz; float ax_g, ay_g, az_g; // 换算成 g float gx_dps, gy_dps, gz_dps; // 换算成 °/s } MPU_Data_t; uint8_t MPU_ReadRaw(MPU_Data_t *d) { uint8_t buf[14]; if (MPU_ReadRegs(0x3B, buf, 14) ! 0) return 1; // 高字节在前拼成有符号 16 位 d-ax (int16_t)((buf[0] 8) | buf[1]); d-ay (int16_t)((buf[2] 8) | buf[3]); d-az (int16_t)((buf[4] 8) | buf[5]); d-gx (int16_t)((buf[8] 8) | buf[9]); d-gy (int16_t)((buf[10] 8) | buf[11]); d-gz (int16_t)((buf[12] 8) | buf[13]); // ±2g 灵敏度 16384±250°/s 灵敏度 131 d-ax_g d-ax / 16384.0f; d-ay_g d-ay / 16384.0f; d-az_g d-az / 16384.0f; d-gx_dps d-gx / 131.0f; d-gy_dps d-gy / 131.0f; d-gz_dps d-gz / 131.0f; return 0; }逻辑说明(buf[0] 8) | buf[1]拼完必须强转int16_t否则负数会被当成大正数。比如 -1 原始值是 0xFFFF不强转就是 65535。温度在 buf[6]、buf[7]这里跳过需要的话按temp raw/340.0 36.53换算。参数说明换算系数 16384 和 131 对应 3.1 里设的量程改量程必须同步改这里这是最常见的“数据不对”来源。除以浮点数会有性能开销F1 无 FPU高频读取时可以先存原始值姿态解算时再换算。4. HMC5883L 磁力计接入模式、增益与连续测量4.1 配置寄存器 A/B 与模式寄存器HMC5883L 比 MPU6050 娇气配置顺序错了就不出数据。三个关键寄存器配置 A0x00设平均次数和输出速率配置 B0x01设增益模式寄存器0x02设连续测量。我一般用配置 A 0x708 次平均、15Hz 输出配置 B 0x20增益 1.3Ga灵敏度 1090 LSB/Gauss模式 0x00连续测量。增益选择要看你的应用环境。增益 0x20 对应 ±1.3Ga地球磁场约 0.5Ga够用且分辨率高。如果靠近电机或扬声器磁场强要降到 0x40±1.9Ga或 0x60±2.5Ga否则读数饱和。数据寄存器从 0x03 开始X/Y/Z 各 2 字节注意 HMC5883L 的字节顺序是 X 高、X 低、Z 高、Z 低、Y 高、Y 低——Z 和 Y 的顺序和直觉相反这是血泪经验我第一次读出来航向角乱转就是栽在这。#define HMC5883L_ADDR 0x3C // 7位地址 0x1E 左移 uint8_t HMC_Init(void) { HMC_WriteReg(0x00, 0x70); // 8次平均15Hz HMC_WriteReg(0x01, 0x20); // 增益 1.3Ga HMC_WriteReg(0x02, 0x00); // 连续测量模式 HAL_Delay(10); return 0; } typedef struct { int16_t mx, my, mz; float mx_g, my_g, mz_g; // 单位 Gauss } HMC_Data_t; uint8_t HMC_ReadRaw(HMC_Data_t *d) { uint8_t buf[6]; if (HMC_ReadRegs(0x03, buf, 6) ! 0) return 1; // 顺序X高 X低 Z高 Z低 Y高 Y低 d-mx (int16_t)((buf[0] 8) | buf[1]); d-mz (int16_t)((buf[2] 8) | buf[3]); d-my (int16_t)((buf[4] 8) | buf[5]); // 增益 0x20 对应 1090 LSB/Gauss d-mx_g d-mx / 1090.0f; d-my_g d-my / 1090.0f; d-mz_g d-mz / 1090.0f; return 0; }逻辑说明配置 B 的增益值直接决定换算系数0x20 对应 10900x40 对应 8200x60 对应 6600x80 对应 4400xA0 对应 3900xC0 对应 3300xE0 对应 230。模式寄存器写 0x00 是连续测量写 0x01 是单次测量单次模式下每次读之前要重新触发麻烦一般用连续。参数说明15Hz 输出速率对姿态解算偏低但 HMC5883L 最高就 75Hz且速率越高噪声越大。如果做航向角融合15Hz 配合 MPU6050 的 200Hz用互补滤波时磁力计权重给小一点避免拖慢动态响应。4.2 磁力计校准硬铁与软铁补偿磁力计不校准航向角能偏几十度。硬铁偏移是固定磁场叠加表现为数据圆心偏离原点软铁是周围材料扭曲磁场表现为圆变椭圆。最简校准是平面旋转法把模块水平转几圈记录 X/Y 最大最小值偏移(maxmin)/2缩放(max-min)/2。// 校准参数实测填入 float mag_offset_x 0, mag_offset_y 0; float mag_scale_x 1, mag_scale_y 1; void HMC_Calibrate(int16_t *buf_x, int16_t *buf_y, uint16_t n) { int16_t min_x buf_x[0], max_x buf_x[0]; int16_t min_y buf_y[0], max_y buf_y[0]; for (uint16_t i 1; i n; i) { if (buf_x[i] min_x) min_x buf_x[i]; if (buf_x[i] max_x) max_x buf_x[i]; if (buf_y[i] min_y) min_y buf_y[i]; if (buf_y[i] max_y) max_y buf_y[i]; } mag_offset_x (max_x min_x) / 2.0f; mag_offset_y (max_y min_y) / 2.0f; float range_x (max_x - min_x) / 2.0f; float range_y (max_y - min_y) / 2.0f; float avg (range_x range_y) / 2.0f; mag_scale_x avg / range_x; mag_scale_y avg / range_y; } // 应用校准 void HMC_ApplyCalib(HMC_Data_t *d) { d-mx_g (d-mx - mag_offset_x) * mag_scale_x / 1090.0f; d-my_g (d-my - mag_offset_y) * mag_scale_y / 1090.0f; }逻辑说明平面旋转只能校准 X/Y 轴Z 轴要立体旋转一般嵌入式航向角只用 X/Y够用。缩放系数把椭圆拉回圆avg/range保证两轴灵敏度一致。参数说明采样点 n 建议 200 以上转的时候慢一点覆盖各个方向。校准参数存 Flash别每次上电重校。5. 避坑与排查STM32 读 GY-87 最常见的五个翻车点5.1 读出来全是 0xFF 或 0x00现象MPU_ReadRegs返回的数据全是 0xFF或者 WHO_AM_I 读不到 0x68。原因通常是上拉电阻缺失或接线松动。GY-87 模块虽然有上拉但排针虚焊、杜邦线接触不良很常见。解决万用表量 SCL/SDA 对 VCC 的阻值正常在 4.7kΩ 左右无穷大就是没上拉或断线。另外确认 STM32 的 I2C 引脚没被其他外设复用。5.2 加速度静止时 Z 轴不是 1g现象模块水平静止az_g读出来 0.5 或 1.5。原因一是量程换算系数写错二是模块没放平三是加速度计零偏。解决先确认 3.1 的量程和 3.2 的系数一致再用水平仪校准安装面零偏可以在静止时读 100 次取平均从后续数据里减掉。MPU6050 零偏典型值 ±50mg太大就是芯片问题。5.3 硬件 I2C 卡在 HAL_BUSY现象F103 上用硬件 I2C程序跑几次就卡在HAL_I2C_Mem_Read不返回。原因是 F1 硬件 I2C 的状态机死锁总线被从机拉低后无法恢复。解决换软件模拟 I2C或者加总线恢复函数——把 SCL/SDA 切回普通 GPIOSCL 发 9 个脉冲再重新初始化 I2C。我现在的习惯是 F1 项目直接软件 I2C省心。5.4 磁力计航向角跳变现象HMC5883L 读出来航向角静止时也在 ±10° 跳。原因一是没校准二是附近有铁质或电流干扰三是 Z/Y 字节顺序读反。解决先跑 4.2 的校准把模块远离电源线和电机核对 4.1 的字节顺序HMC5883L 是 X、Z、Y不是 X、Y、Z。这个顺序坑我踩过读出来航向角跟着俯仰变就是 Y/Z 反了。5.5 数据更新率上不去现象主循环里读 MPU6050发现每秒只能读几十次。原因是软件 I2C 延时太长或者HAL_Delay用在了读函数里。解决软件 I2C 延时循环减到 10 次以内200kHz 以上读函数里不要加延时MPU6050 的采样率设 200Hz主循环 1ms 读一次够用。如果还慢换硬件 I2C DMAF4 上能跑到 400kHz。6. 从原始数据到姿态角互补滤波的调参与验证数据读通了下一步是姿态解算。MPU6050 的陀螺仪积分短期准但漂移加速度计长期准但振动噪声大互补滤波就是各取所长。公式很简单angle 0.98 * (angle gyro * dt) 0.02 * accel_angle系数 0.98 和 0.02 是经验值dt 是采样周期。// 互补滤波只算俯仰和横滚 float pitch 0, roll 0; const float alpha 0.98f; void Attitude_Update(MPU_Data_t *d, float dt) { // 加速度计算角度 float accel_pitch atan2f(-d-ax_g, sqrtf(d-ay_g*d-ay_g d-az_g*d-az_g)) * 57.2958f; float accel_roll atan2f(d-ay_g, d-az_g) * 57.2958f; // 陀螺仪积分 加速度计修正 pitch alpha * (pitch d-gy_dps * dt) (1 - alpha) * accel_pitch; roll alpha * (roll d-gx_dps * dt) (1 - alpha) * accel_roll; }逻辑说明atan2f比atan好能处理分母为零的情况。57.2958是弧度转角度。alpha 越大越信陀螺仪动态响应快但漂移大越小越信加速度计静态稳但振动时抖。0.98 适合 dt5ms 的场景dt 变了要重调。参数说明dt 必须实测用定时器中断触发读取别用HAL_GetTick估算误差大。验证方法把模块放平pitch/roll 应接近 0绕 X 轴转 90°roll 应接近 90°静置 10 分钟漂移应小于 2°。如果漂移大检查陀螺仪零偏静止时读 500 次取平均从gx_dps里减掉。磁力计融合航向角用类似思路但要注意磁力计受倾斜影响得先用 pitch/roll 做倾斜补偿heading atan2(my*cos(roll) - mz*sin(roll), mx*cos(pitch) my*sin(pitch)*sin(roll) mz*sin(pitch)*cos(roll))。这个公式我每次都要翻笔记建议直接抄进函数里。从那以后我每次调 IMU都强制先跑一遍 WHO_AM_I 和静止零偏检查再动姿态解算——这两个不过后面全是白费功夫。希望帮到你。本文还有配套的精品资源点击获取