简介这份资源面向使用STM32进行嵌入式开发的初学者与工程师聚焦GY87九轴传感器的数据读取问题。GY87集成了MPU6050六轴姿态传感器、HMC5883三轴地磁传感器与BMP180气压温度传感器可完成加速度、角速度、磁场强度及海拔高度的测量常用于无人机导航、物联网设备等场景的环境感知。资源包共6个文件包含3个.h头文件与3个.c源文件分别对应MPU6050、BMP180、HMC5883三个传感器的驱动实现压缩包约4KB结构精简便于直接移植到工程中。目前已有1168人学习下载。通过阅读这些驱动代码读者可以掌握STM32的I2C接口配置、传感器寄存器读写、数据校准与滤波处理等关键环节理解中断服务程序在实时采集中的作用从而快速搭建起多传感器数据采集框架为后续姿态解算与项目开发打下基础。1. 拿到 GY-87 别急着上电先搞清它到底能给你什么数据很多人第一次把 GY-87 接到 STM32 上满心期待串口打印出姿态角结果读回来一堆 0 或者固定值甚至 I2C 直接卡死。问题往往不在代码而在于没搞清 GY-87 到底是什么——它不是一颗传感器而是一块集成了三颗芯片的模块板。板上通常有 MPU6050三轴加速度计 三轴陀螺仪、HMC5883L三轴磁力计和 BMP180气压计对外走 I2C 总线地址各不相同。STM32 要做的就是分别跟这三个从机通信把原始数据读回来再决定是直接上报还是做姿态解算。这套东西适合做平衡车、四轴飞控、云台稳定、倾角监测这类需要姿态感知的项目也适合刚接触 I2C 和多传感器融合的开发者拿来练手。下面按“接线与总线确认 → 三个芯片逐个读 → 数据校验与融合 → 常见翻车点 → 二次开发改造”的顺序拆一遍代码基于 HAL 库F1/F4 系列都能直接搬。2. 硬件接线与 I2C 总线确认先把物理层跑通2.1 模块引脚与 STM32 对应关系GY-87 对外一般引出 6 个引脚VCC、GND、SCL、SDA、XDA、XCL。其中 XDA/XCL 是给 MPU6050 扩展用的辅助 I2C接外部磁力计时才会用到GY-87 板上已经把 HMC5883L 挂到了 MPU6050 的辅助总线上所以 STM32 只需要接 VCC、GND、SCL、SDA 四根线。供电方面模块标称 3~5V但 MPU6050 和 HMC5883L 的 IO 电平是 3.3V 逻辑如果 STM32 是 3.3V 系统直接连就行如果是 5V 系统SCL/SDA 最好加电平转换否则长期跑可能损伤传感器。我一般会把 SCL 接到 PB6、SDA 接到 PB7用 STM32 的硬件 I2C1速率先设 100kHz等确认通信稳定再往 400kHz 提。接线确认后先别写业务代码用最简短的扫描程序把总线上所有从机地址打出来。这一步能排除 80% 的“读不到数据”问题。// I2C 地址扫描确认 GY-87 上三个芯片是否都在线 #include stm32f1xx_hal.h extern I2C_HandleTypeDef hi2c1; void GY87_ScanBus(void) { uint8_t addr; printf(Scanning I2C bus...\r\n); for (addr 1; addr 128; addr) { // HAL 库地址参数需要左移一位 if (HAL_I2C_IsDeviceReady(hi2c1, addr 1, 2, 10) HAL_OK) { printf(Found device at 0x%02X\r\n, addr); } } printf(Scan done.\r\n); }这段代码逐个地址调用HAL_I2C_IsDeviceReady如果从机应答就打印地址。正常情况你会看到三个地址MPU6050 是 0x68AD0 接地或 0x69AD0 接 VCCHMC5883L 是 0x1EBMP180 是 0x77。如果只看到 0x68 而看不到 0x1E 和 0x77说明 MPU6050 的辅助 I2C 没有把磁力计和气压计透传出来需要先配置 MPU6050 的USER_CTRL和INT_PIN_CFG寄存器打开 I2C 旁路模式BYPASS_EN让 STM32 能直接访问辅助总线上的设备。这一步是 GY-87 跟单独用 MPU6050 最大的区别很多人卡在这里就是因为不知道要开旁路。2.2 打开 MPU6050 旁路模式MPU6050 的寄存器 0x6A 是USER_CTRLbit5 是 I2C_MST_ENbit4 是 I2C_IF_DIS寄存器 0x37 是INT_PIN_CFGbit1 是 BYPASS_EN。要让 STM32 直接访问 HMC5883L 和 BMP180需要关掉主机模式、打开旁路。// 配置 MPU6050 进入 I2C 旁路模式使能直接访问辅助总线设备 #define MPU6050_ADDR (0x68 1) #define MPU6050_USER_CTRL 0x6A #define MPU6050_INT_PIN 0x37 void MPU6050_EnableBypass(void) { uint8_t data; // 关闭 I2C 主机模式清除 SLEEP 位 HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, MPU6050_USER_CTRL, 1, data, 1, 100); data ~(1 5); // I2C_MST_EN 0 HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_USER_CTRL, 1, data, 1, 100); // 打开旁路使能 HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, MPU6050_INT_PIN, 1, data, 1, 100); data | (1 1); // BYPASS_EN 1 HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_INT_PIN, 1, data, 1, 100); }HAL_I2C_Mem_Read/Write的第二个参数是从机地址已左移第三个是寄存器地址第四个是寄存器地址长度MPU6050 是 8 位寄存器地址所以填 1。执行完这段再跑一次扫描三个地址应该都能出现。如果 HMC5883L 还是不出来检查模块背面有没有把磁力计的跳线断开有些批次出厂时辅助总线是断开的。3. 三个芯片的读取实现从原始寄存器到物理量3.1 MPU6050 加速度与陀螺仪读取MPU6050 的加速度数据在 0x3B~0x40陀螺仪在 0x43~0x48每个轴 2 个字节高字节在前。上电后需要先唤醒并设置量程加速度常用 ±2g陀螺仪常用 ±250°/s对应灵敏度分别是 16384 LSB/g 和 131 LSB/(°/s)。// 初始化 MPU6050 并读取加速度、陀螺仪原始值 #define MPU6050_PWR_MGMT_1 0x6B #define MPU6050_ACCEL_CFG 0x1C #define MPU6050_GYRO_CFG 0x1B #define MPU6050_ACCEL_XOUT 0x3B typedef struct { int16_t ax, ay, az; int16_t gx, gy, gz; } MPU6050_Raw; void MPU6050_Init(void) { uint8_t val 0x00; // 唤醒选择内部 8MHz 时钟 HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_PWR_MGMT_1, 1, val, 1, 100); // 加速度 ±2g val 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_ACCEL_CFG, 1, val, 1, 100); // 陀螺仪 ±250°/s val 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_GYRO_CFG, 1, val, 1, 100); } void MPU6050_ReadRaw(MPU6050_Raw *out) { uint8_t buf[14]; // 从 0x3B 连续读 14 字节加速度 6 温度 2 陀螺仪 6 HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, MPU6050_ACCEL_XOUT, 1, buf, 14, 100); out-ax (int16_t)((buf[0] 8) | buf[1]); out-ay (int16_t)((buf[2] 8) | buf[3]); out-az (int16_t)((buf[4] 8) | buf[5]); // buf[6]、buf[7] 是温度这里跳过 out-gx (int16_t)((buf[8] 8) | buf[9]); out-gy (int16_t)((buf[10] 8) | buf[11]); out-gz (int16_t)((buf[12] 8) | buf[13]); }一次连续读 14 字节比逐寄存器读效率高得多也避免了三轴数据来自不同采样时刻的问题。读回来的是有符号 16 位整数转物理量时加速度除以 16384 得到 g陀螺仪除以 131 得到 °/s。注意HAL_I2C_Mem_Read的超时参数别设太小100ms 比较稳妥太小会在总线忙时误报失败。3.2 HMC5883L 磁力计读取HMC5883L 的地址是 0x1E配置寄存器 A0x00设采样平均次数和输出速率寄存器 B0x01设增益模式寄存器0x02设连续测量。数据在 0x03~0x08顺序是 X、Z、Y注意不是 X、Y、Z这个顺序坑过不少人。// HMC5883L 初始化与读取 #define HMC5883L_ADDR (0x1E 1) #define HMC5883L_CFG_A 0x00 #define HMC5883L_CFG_B 0x01 #define HMC5883L_MODE 0x02 #define HMC5883L_DATA 0x03 typedef struct { int16_t mx, my, mz; } HMC5883L_Raw; void HMC5883L_Init(void) { uint8_t val; val 0x70; // 8 次平均15Hz 输出 HAL_I2C_Mem_Write(hi2c1, HMC5883L_ADDR, HMC5883L_CFG_A, 1, val, 1, 100); val 0x20; // 增益 1.3Ga适合一般环境 HAL_I2C_Mem_Write(hi2c1, HMC5883L_ADDR, HMC5883L_CFG_B, 1, val, 1, 100); val 0x00; // 连续测量模式 HAL_I2C_Mem_Write(hi2c1, HMC5883L_ADDR, HMC5883L_MODE, 1, val, 1, 100); } void HMC5883L_ReadRaw(HMC5883L_Raw *out) { uint8_t buf[6]; HAL_I2C_Mem_Read(hi2c1, HMC5883L_ADDR, HMC5883L_DATA, 1, buf, 6, 100); // 数据顺序X 高、X 低、Z 高、Z 低、Y 高、Y 低 out-mx (int16_t)((buf[0] 8) | buf[1]); out-mz (int16_t)((buf[2] 8) | buf[3]); out-my (int16_t)((buf[4] 8) | buf[5]); }增益 0x20 对应 1.3Ga换算关系是 1090 LSB/Ga。如果所在环境磁场干扰大读数经常饱和可以把增益调低到 0.88Ga0x60但分辨率会下降。磁力计最容易受板载电流和附近铁磁材料影响读出来的值跳变严重时先别怀疑代码把模块远离电源线和金属再试。3.3 BMP180 气压与温度读取BMP180 地址 0x77读之前要先读 0xAA 开始的 22 字节校准系数然后写 0xF4 启动温度或气压转换等转换完成再读 0xF6 开始的 3 字节结果。气压转换时间随过采样设置不同从 4.5ms 到 25.5ms 不等。// BMP180 读取温度简化版只取 UT #define BMP180_ADDR (0x77 1) #define BMP180_CTRL 0xF4 #define BMP180_RESULT 0xF6 int32_t BMP180_ReadUT(void) { uint8_t cmd 0x2E; // 温度转换等待 4.5ms uint8_t buf[2]; HAL_I2C_Mem_Write(hi2c1, BMP180_ADDR, BMP180_CTRL, 1, cmd, 1, 100); HAL_Delay(5); HAL_I2C_Mem_Read(hi2c1, BMP180_ADDR, BMP180_RESULT, 1, buf, 2, 100); return (int32_t)((buf[0] 8) | buf[1]); }这里只演示温度读取气压需要结合校准系数做二阶补偿计算代码较长实际项目里建议直接移植现成的 BMP180 驱动。HAL_Delay(5)是给转换留时间如果用了 RTOS这里应该换成任务延时别在中断里死等。4. 数据校验与姿态融合让读数变得可用4.1 原始数据的自检与滤波三个芯片读回来的原始值不能直接信。上电后先做静态自检模块水平静止时加速度 Z 轴应该接近 163841gX、Y 接近 0陀螺仪三轴接近 0磁力计各轴在一个稳定范围内。如果加速度 Z 轴差很远检查量程配置和安装方向。原始数据抖动明显时最简单的是滑动平均滤波对加速度和磁力计各轴维护一个长度 8 的环形缓冲陀螺仪因为要保留动态响应一般只做一阶低通。// 加速度滑动平均滤波窗口 8 #define FILTER_WIN 8 static int32_t acc_buf[3][FILTER_WIN]; static uint8_t acc_idx 0; void Accel_Filter(int16_t ax, int16_t ay, int16_t az, float *fx, float *fy, float *fz) { int32_t sum[3] {0}; uint8_t i; acc_buf[0][acc_idx] ax; acc_buf[1][acc_idx] ay; acc_buf[2][acc_idx] az; acc_idx (acc_idx 1) % FILTER_WIN; for (i 0; i FILTER_WIN; i) { sum[0] acc_buf[0][i]; sum[1] acc_buf[1][i]; sum[2] acc_buf[2][i]; } *fx (float)sum[0] / FILTER_WIN / 16384.0f; *fy (float)sum[1] / FILTER_WIN / 16384.0f; *fz (float)sum[2] / FILTER_WIN / 16384.0f; }窗口越大越平滑但延迟也越大8 是一个比较折中的值。滤波后的加速度单位是 g可以直接用来算倾角pitch atan2(-ax, sqrt(ay*ay az*az))roll atan2(ay, az)。但加速度计对振动敏感单靠它算姿态在运动场景下会跳得厉害所以需要陀螺仪积分来补。4.2 互补滤波做姿态估计互补滤波的思路很简单陀螺仪积分得到短期角度加速度计算出的角度作为长期参考两者按权重融合。陀螺仪动态响应好但有零漂加速度计静态准但噪声大互补正好取长补短。// 互补滤波陀螺仪积分 加速度计修正 float pitch 0.0f, roll 0.0f; #define ALPHA 0.98f #define DT 0.01f // 采样周期 10ms void Attitude_Update(float ax, float ay, float az, float gx, float gy, float gz) { float acc_pitch, acc_roll; // 加速度计解算角度弧度转角度 acc_pitch atan2f(-ax, sqrtf(ay * ay az * az)) * 57.29578f; acc_roll atan2f(ay, az) * 57.29578f; // 陀螺仪积分 加速度计修正 pitch ALPHA * (pitch gy * DT) (1.0f - ALPHA) * acc_pitch; roll ALPHA * (roll gx * DT) (1.0f - ALPHA) * acc_roll; }ALPHA取 0.98 表示 98% 信任陀螺仪、2% 信任加速度计这个比例适合大多数低速运动场景。如果应用里振动大可以降到 0.95如果要求静态精度高可以提到 0.99。DT必须和实际调用周期一致用定时器中断固定 10ms 调用一次最稳靠主循环里HAL_Delay会漂。偏航角需要磁力计参与把磁力计数据做椭圆拟合校准后再和陀螺仪 Z 轴融合复杂度更高建议先把 pitch/roll 跑稳再动 yaw。5. 避坑与排查GY-87 调试中最容易翻车的几个点5.1 扫描不到 0x1E 和 0x77现象是只看到 MPU6050 的地址磁力计和气压计完全没反应。原因通常是 MPU6050 没有进入旁路模式辅助总线还被主机模式占着。解决方法是先执行MPU6050_EnableBypass()再扫描。如果执行了还不行检查模块上 MPU6050 的 AD0 引脚电平确认地址是 0x68 还是 0x69扫描代码里两个都试一下。5.2 读回来的数据全是 0xFF 或 0x00现象是HAL_I2C_Mem_Read返回 HAL_OK但缓冲区里全是固定值。原因一般是 I2C 时序问题上拉电阻太大导致上升沿太慢或者总线速率设太高。解决方法是把 SCL/SDA 上拉换成 4.7kΩI2C 速率降到 100kHz超时时间加到 100ms 以上。如果用的是软件 I2C检查延时函数是否被编译器优化掉加volatile或者用__NOP()。5.3 磁力计读数乱跳现象是 HMC5883L 三轴数值无规律大幅波动。原因是磁力计对周围磁场极其敏感板载电源走线、电机、扬声器都会干扰。解决方法一是把模块用排线远离干扰源二是在代码里做校准水平旋转一圈记录各轴最大最小值算偏移量和缩放系数读回来的原始值先减偏移再乘缩放。校准后的数据才适合参与 yaw 解算。5.4 姿态角缓慢漂移现象是静止时 pitch/roll 慢慢往一个方向偏。原因是陀螺仪零漂没有补偿互补滤波的加速度计修正权重太低。解决方法是在上电时让模块静止 2 秒采集 200 组陀螺仪数据求平均作为零偏后续积分前先减去这个零偏。如果漂移仍然明显把ALPHA从 0.98 降到 0.95让加速度计多拉一点。5.5 I2C 总线死锁现象是程序跑一段时间后 I2C 完全无响应复位才能恢复。原因是某一帧通信被中断打断从机把 SDA 拉低不放。解决方法是在 I2C 初始化前加总线恢复逻辑把 SCL 配成推挽输出手动发 9 个时钟脉冲再把 SDA 拉高产生停止条件最后重新初始化 I2C。这个恢复函数放在HAL_I2C_ErrorCallback里调用能救回大部分死锁。6. 二次开发改造从能读到好用把三个芯片都读通之后下一步就是让这套代码真正能用在项目里。我一般会做三件事把传感器读取封装成独立任务、把姿态解算换成更稳的算法、把数据输出做成可配置的。第一件事是任务化。裸机主循环里轮询三个芯片任何一个超时都会拖慢整体节奏。改成定时器中断触发读取或者上 FreeRTOS 建一个传感器任务用队列把数据发给姿态解算任务和通信任务。这样即使某个芯片暂时无响应也不会阻塞其他功能。任务周期建议 10ms和互补滤波的DT对齐。第二件事是算法升级。互补滤波够用但精度有限如果项目对姿态要求高可以换成 Mahony 或 Madgwick 滤波。这两个算法输入都是加速度、陀螺仪、磁力计的物理量输出四元数再转欧拉角。移植时注意把 GY-87 的坐标系对齐MPU6050 和 HMC5883L 的轴向不一定一致需要根据实际安装方向做轴交换和符号翻转。我一般会在纸上画一遍模块的 X/Y/Z 方向再对着算法输入逐轴核对比在代码里试错快得多。第三件事是输出可配置。调试阶段用串口打印原始值和姿态角量产时可能只需要通过 CAN 或无线模块上报角度。把输出格式做成宏开关或者用结构体打包后统一发送避免在业务代码里散落一堆printf。下面是一个简单的数据打包示例// 统一姿态数据结构方便通过不同接口发送 typedef struct { float pitch; float roll; float yaw; float temperature; float pressure; uint32_t timestamp; } AttitudeData; void SendAttitude(const AttitudeData *d) { #ifdef USE_UART_OUTPUT printf(P:%.2f R:%.2f Y:%.2f T:%.1f\r\n, d-pitch, d-roll, d-yaw, d-temperature); #endif #ifdef USE_CAN_OUTPUT // 打包成 CAN 帧发送 #endif }改造过程中最容易忽略的是时间戳。多传感器融合对时间对齐很敏感如果加速度和陀螺仪来自不同时刻融合结果会引入额外误差。我习惯在读取 MPU6050 的 14 字节时一次性拿完加速度和陀螺仪保证它们同源同时刻磁力计和气压计因为更新率低单独打时间戳融合时按时间插值。从那以后我每次接新传感器模块都强制先跑一遍总线扫描和静态自检确认物理层和数据层都没问题再写业务逻辑省下来的调试时间远比这几步多。希望帮到你。本文还有配套的精品资源点击获取