news 2026/10/15 18:58:14

STM32读取GY-87全攻略:I2C多传感器读取、寄存器配置与滤波调校

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32读取GY-87全攻略:I2C多传感器读取、寄存器配置与滤波调校

简介:面向STM32嵌入式开发者,资源以GY87九轴传感器模块为对象,聚焦MPU6050、HMC5883、BMP180三颗芯片的驱动读取,覆盖加速度、陀螺仪、地磁与气压海拔数据的采集处理。提供完整可参考的C语言驱动代码,压缩包共6个文件,包含3个源文件与3个头文件,整体仅4KB,结构精简,分别对应三颗传感器的初始化、寄存器读写与校准逻辑,便于直接移植到I2C或SPI工程中。已有1165人浏览学习,适合正在做无人机导航、姿态解算或物联网环境感知的开发者参考。通过示例代码可快速掌握STM32的I2C/SPI接口配置、传感器数据手册查阅方法以及中断服务程序编写,并了解HMC5883的磁场强度提取和BMP180的温度补偿流程,帮助缩短传感器调试周期。

1. stm32 读取 gy87:一块合板、三颗芯片、一条 I2C 总线

stm32 读取 gy87 这件事,一上来就能劝退一批人:线接好、程序烧进去,串口打印出来全是 0,或者隔一会儿蹦出一个离谱的大数。GY-87 这块模块看着像单个传感器,翻开背面其实是三颗芯片拼在一张板子上——六轴惯性测量单元、三轴磁力计、气压计,全部挂在同一条 I2C 总线上。它的价值在于:一个模块同时给出加速度、角速度、磁场和气压,做姿态估计、自平衡小车、航向测量都不用再拼一堆零件。这篇文章从接线开始,把寄存器配置、原始数据读取、物理量换算和滤波串成一条完整链路,最后列出一线调试时踩过的高频坑。适合第一次碰 I2C 传感器的开发者,也适合熟手快速对照参数。

2. GY-87 到底是什么模块:芯片组成、默认地址与接线注意事项

2.1 先看板子上有什么

GY-87 不是一颗芯片,是三颗芯片的合板。绝大多数批次上能看到的组合是:MPU6050(三轴加速度计 + 三轴陀螺仪)、HMC5883L(三轴磁力计)、BMP180 或 BMP280(气压计 + 温度计)。三颗芯片各自有独立的 I2C 地址,互不干扰,所以「读 GY-87」的本质是「在一条 I2C 总线上分别操作三个从机」。

芯片功能默认 I2C 地址
MPU6050三轴加速度 + 三轴陀螺仪0x68(AD0 接地)
HMC5883L三轴磁力计0x1E(固定)
BMP180 / BMP280气压计 / 温度计0x77(常见)

第一个坑就藏在这里:MPU6050 的地址由 AD0 引脚决定,接地是 0x68,接高是 0x69。有些卖家板子上 AD0 默认焊了上拉电阻,你拿到的地址可能不是 0x68 而是 0x69。所以我不建议直接硬编码地址,上电先扫一遍总线,这个习惯能省半小时。

2.2 接线:3.3V 供电,SDA/SCL 开漏加上拉

GY-87 的引脚标准是 VCC、GND、SCL、SDA,部分版本还引出 AD0、DRDY 之类。最常见的接法是 VCC 接 3.3V,GND 接 GND,SCL 和 SDA 分别接 STM32 的 I2C 引脚。STM32 的 I2C 引脚是开漏结构,所以 SCL/SDA 必须有上拉电阻。GY-87 板载了 4.7k 上拉,短距离(杜邦线 < 20cm)直接能用;如果线长、或者接在面包板上,建议在靠近模块端再并一个 2.2k 上拉到 3.3V,否则上升沿太缓,数据容易翻车。

供电上要留意:有些 GY-87 版本板载了低压差稳压器,5V 供电也能跑;但更多版本是纯 3.3V 电路,5V 直接进 VCC 可能烧掉磁力计。我用 STM32 一律 3.3V 供电,省心。电平匹配方面,STM32 的 I2C 引脚是 3.3V 容限,和模块同电平,不需要转换。

注意 AD0 引脚:如果模块引出了 AD0,你可以自己控制它来改 MPU6050 的地址;如果没引出,就看背面焊盘上有没有把 AD0 拉到 VCC。焊盘上的默认状态直接决定你代码里写 0x68 还是 0x69,这是「读出来全 0」的高频原因之一。

2.3 上电第一件事:扫 I2C 地址,确认总线正常

我拿到任何新模块的第一件事不是读数据,而是跑一个 I2C 扫描,把总线上所有设备地址打出来。这个动作能一次性确认三件事:接线是否通、上拉是否正常、设备的实际地址是什么。用 HAL 库写扫描特别简单,HAL_I2C_IsDeviceReady会向目标地址发一个起始条件和地址帧,收到 ACK 就说明设备在线。

void i2c_scan(I2C_HandleTypeDef *hi2c) { for (uint16_t addr = 0x08; addr <= 0x77; addr++) { // 10ms 超时,超过 1 次尝试都失败就认为该地址无设备 if (HAL_I2C_IsDeviceReady(hi2c, (uint16_t)(addr << 1), 1, 10) == HAL_OK) { printf("I2C device found at 0x%02X\r\n", addr); } } }

这段代码遍历了 0x08 到 0x77 的所有 7 位地址,addr << 1是因为 HAL 库的DevAddr参数要求 8 位格式(7 位地址左移一位,最低位是读写标志)。正常 GY-87 上电后会扫出 0x68 或 0x69、0x1E、0x77 三个地址。如果只扫出一个或两个,先查接线;如果全扫不到,十有八九是 SCL/SDA 接反了,或者供电电压不够。如果扫出的是 0x69 而不是 0x68,说明你手上这块的 AD0 被拉高了,代码里对应改地址就行。

3. 用 STM32 的 I2C 读原始数据:从初始化到三颗芯片的完整代码

3.1 初始化 I2C 与唤醒 MPU6050

MPU6050 上电后默认处于睡眠模式,不唤醒的话读出来的加速度和陀螺仪数据全是 0。这是「stm32 读取 gy87 全是 0」最常见的第一个原因。唤醒操作很简单:往电源管理寄存器PWR_MGMT_1(地址 0x6B)写 0x00,让芯片进入正常模式。在写之前,我习惯先读WHO_AM_I(地址 0x75)确认通信正常,这个寄存器固定返回 0x68,能帮你排除地址错误。

// I2C1 已在 CubeMX 中配置为 400kHz,下面直接操作寄存器 uint8_t who_am_i = 0; // 读 WHO_AM_I,确认通信链路正常,返回值应为 0x68 HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x75, I2C_MEMADD_SIZE_8BIT, &who_am_i, 1, 100); // 往电源管理寄存器写 0x00,退出睡眠模式并选择内部时钟源 uint8_t pwr_mgmt = 0x00; HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, I2C_MEMADD_SIZE_8BIT, &pwr_mgmt, 1, 100);

这里的0x68 << 1是 HAL 库的固定写法,因为 HAL 的DevAddr参数需要 8 位地址格式。I2C_MEMADD_SIZE_8BIT指定寄存器地址是 8 位宽度,MPU6050、HMC5883L 的寄存器地址都小于 0xFF,用 8 位没问题。0x75是 WHO_AM_I 寄存器,读出来应该是 0x68;如果不是,先别往下走,回去查地址和接线,否则后面全是白忙。

3.2 连续读 14 字节,拿加速度和陀螺仪原始值

MPU6050 的数据寄存器从 0x3B 开始,按顺序排列:加速度 X 高字节、X 低字节、Y 高字节、Y 低字节、Z 高字节、Z 低字节、温度高字节、温度低字节、陀螺仪 X 高字节、X 低字节,一直到陀螺仪 Z 低字节,一共 14 个字节。关键点是一次性把这 14 字节读完,不要分两次读。分开读会出现一种很隐蔽的错位:中间有一次 I2C 传输被打断,高字节和低字节不是同一时刻采样的,数据跳变的幅度会让你怀疑人生。

uint8_t buf[14]; int16_t ax, ay, az, gx, gy, gz, temp; // 从 0x3B 连续读 14 字节,一次读完保证同一时刻的采样结果 HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); // 传感器数据是大端格式:高字节在前,低字节在后 ax = (int16_t)(((uint16_t)buf[0] << 8) | buf[1]); ay = (int16_t)(((uint16_t)buf[2] << 8) | buf[3]); az = (int16_t)(((uint16_t)buf[4] << 8) | buf[5]); temp = (int16_t)(((uint16_t)buf[6] << 8) | buf[7]); gx = (int16_t)(((uint16_t)buf[8] << 8) | buf[9]); gy = (int16_t)(((uint16_t)buf[10] << 8) | buf[11]); gz = (int16_t)(((uint16_t)buf[12] << 8) | buf[13]);

拼接时用(uint16_t)强转再左移,是为了避免buf[0]是uint8_t时整数提升带来的符号位问题。int16_t会把最高位解释为符号位,得到的就是有符号的原始值。这套代码做完,串口打印 ax、ay、az 应该能到静态值:平放时 ax 和 ay 接近 0,az 接近 16384(默认 ±2g 量程下对应 1g),动一动板子数值会有明显变化。如果全是 0,回到 3.1 查 PWR_MGMT_1。

3.3 磁力计 HMC5883L:需要先配置模式再读 6 字节

HMC5883L 的寄存器布局和 MPU6050 不一样,它有三个配置寄存器:配置寄存器 A(地址 0x00)控制采样平均次数和输出速率,配置寄存器 B(地址 0x01)控制增益,模式寄存器(地址 0x02)控制工作模式。上电默认是空闲模式,不写模式寄存器的话读不到数据。我一般这样配置:寄存器 A 写 0x70(8 次平均、15Hz 输出速率),寄存器 B 写 0x20(对应 ±1.3 高斯量程,灵敏度 1090 LSB/高斯),模式寄存器写 0x00 进入连续测量模式。

uint8_t cfg_a = 0x70; // 8 次平均,15Hz 输出,正常测量 uint8_t cfg_b = 0x20; // 增益配置,对应 ±1.3 高斯量程 uint8_t mode = 0x00; // 连续测量模式 // 依次写三个配置寄存器 HAL_I2C_Mem_Write(&hi2c1, 0x1E << 1, 0x00, I2C_MEMADD_SIZE_8BIT, &cfg_a, 1, 100); HAL_I2C_Mem_Write(&hi2c1, 0x1E << 1, 0x01, I2C_MEMADD_SIZE_8BIT, &cfg_b, 1, 100); HAL_I2C_Mem_Write(&hi2c1, 0x1E << 1, 0x02, I2C_MEMADD_SIZE_8BIT, &mode, 1, 100); // 数据寄存器从 0x03 开始,连续读 6 字节:X 高、X 低、Z 高、Z 低、Y 高、Y 低 uint8_t mag_buf[6]; int16_t mx, my, mz; HAL_I2C_Mem_Read(&hi2c1, 0x1E << 1, 0x03, I2C_MEMADD_SIZE_8BIT, mag_buf, 6, 100); mx = (int16_t)(((uint16_t)mag_buf[0] << 8) | mag_buf[1]); mz = (int16_t)(((uint16_t)mag_buf[2] << 8) | mag_buf[3]); my = (int16_t)(((uint16_t)mag_buf[4] << 8) | mag_buf[5]);

注意 HMC5883L 的数据寄存器顺序是 X、Z、Y,不是 X、Y、Z,很多人在这里栽过。读出来的 mx、my、mz 是 LSB 值,静态下模块水平放置并转动方向,mx 和 my 会随朝向变化,mz 在水平时接近一个固定值(地磁垂直分量)。如果数值完全不随方向变化,先检查是不是连续测量模式没写进去。

4. 从原始值到角度:量程换算、加速度计定姿与一阶互补滤波

4.1 量程与灵敏度:先搞清每个 LSB 代表多少物理量

MPU6050 的原始值不是直接能用的,要先除以灵敏度。灵敏度由量程决定,量程由配置寄存器决定。默认情况下,加速度计量程是 ±2g,陀螺仪量程是 ±250°/s。实际项目里我经常需要改量程,比如做自平衡小车时陀螺仪量程改到 ±500°/s 或 ±1000°/s,防止转向过猛时数据削顶。

加速度计量程灵敏度(LSB/g)陀螺仪量程灵敏度(LSB/(°/s))
±2g16384±250°/s131
±4g8192±500°/s65.5
±8g4096±1000°/s32.8
±16g2048±2000°/s16.4

量程的配置寄存器是GYRO_CONFIG(0x1B)的低三位和ACCEL_CONFIG(0x1C)的低四位。比如要把陀螺仪量程改为 ±500°/s,就往 0x1B 写 0x08;要把加速度计量程改为 ±4g,就往 0x1C 写 0x08。改完量程,换算公式里的分母必须同步改,否则角度计算会差整整一倍,这是那种「数据看起来对、用起来全飘」的典型原因。

4.2 原始值换算成物理量:除法放在最后

换算本身就是一个除法,但很多人会把原始值先转成 float 再做除法,在无浮点运算单元的 MCU 上会拖慢速度。我的习惯是先用整数运算把原始值读出来,需要输出或者参与角度计算时再转 float。下面这段代码把 3.2 节读到的原始值换算成 g 和 °/s。

float ax_g = (float)ax / 16384.0f; // 默认 ±2g 量程,灵敏度 16384 float ay_g = (float)ay / 16384.0f; float az_g = (float)az / 16384.0f; float gx_dps = (float)gx / 131.0f; // 默认 ±250°/s 量程,灵敏度 131 float gy_dps = (float)gy / 131.0f; float gz_dps = (float)gz / 131.0f;

这里的 16384 和 131 必须和你实际配置的量程一致。如果改了量程却忘了改分母,静态下加速度计三轴平方和就不是 1g,而是 2g 或 0.5g。验证方法很简单:板子水平静置,算sqrt(ax_g*ax_g + ay_g*ay_g + az_g*az_g),结果应该接近 1.0。差得远就先查量程配置。

4.3 加速度计算俯仰角和横滚角:atan2 的坐标系约定

加速度计在静态下可以直接算出俯仰角(pitch)和横滚角(roll),原理是利用重力在三个轴上的分量。常用公式是:pitch = atan2(-ax, sqrt(ayay + azaz)),roll = atan2(ay, sqrt(axax + azaz)),角度制再乘 180/π。注意坐标系约定:我默认模块平放、Z 轴垂直向上,这是 MPU6050 数据手册的右手坐标系。

#include "math.h" float pitch = atan2f(-ax_g, sqrtf(ay_g * ay_g + az_g * az_g)) * 57.2958f; float roll = atan2f(ay_g, sqrtf(ax_g * ax_g + az_g * az_g)) * 57.2958f;

这里有个细节:pitch 公式里的负号不是固定的,取决于你的模块安装方向和角度定义。比如模块竖着装在车头上,X 轴朝前,公式可能就要换成 atan2(ax, sqrt(ayay+azaz))。我的建议是:先用一个已知角度(比如把模块竖起来 90°)实测一遍输出,如果符号反了就把负号去掉,如果横滚俯仰对调就交换公式。角度符号靠实测验证,别靠猜,这也是「数据看起来合理但控制方向反了」的常见翻车点。

4.4 一阶互补滤波:陀螺仪短时准,加速度计长时准

陀螺仪积分出来的角度短时间很平滑,但会随时间漂移;加速度计算出来的角度不漂移,但震动时会剧烈跳动。互补滤波的思路就是两者加权融合:高速时信陀螺仪,低速时信加速度计。一阶互补滤波的实现非常简单,两行代码:

// dt 是采样间隔,单位秒,由定时器实测得到,比如 10ms 采样一次就填 0.01f float dt = 0.01f; float alpha = 0.98f; // 陀螺仪权重,越大越信任陀螺仪 // 陀螺仪角速度积分 + 加速度计角度修正 pitch = alpha * (pitch + gx_dps * dt) + (1.0f - alpha) * pitch_acc; roll = alpha * (roll + gy_dps * dt) + (1.0f - alpha) * roll_acc;

alpha的取值是经验值,0.98 是常见起点,意思是在一个采样周期内,98% 信任陀螺仪积分结果,2% 信任加速度计。这个系数要跟着实际效果调:如果角度反应迟钝、跟手慢,减小 alpha;如果静止时角度抖动大,增大 alpha。dt 必须是你实际的采样周期,我见过有人直接写 0.01 但实际主循环跑了 5ms,积分时间差一倍,角度会持续性超前。dt 最好用定时器实测,或者用一个计数值转出来的真实时间,不要拍脑袋填。

5. GY-87 读取避坑指南:全 0、乱跳、硬件卡死从哪里查起

5.1 现象:读出来全是 0,一点变化都没有

原因一:MPU6050 还在睡眠模式。上电默认 PWR_MGMT_1 是 0x40,芯片处于睡眠状态,所有数据寄存器都是 0。原因二:I2C 地址不对,你读的地址上根本没有设备,HAL 读失败后缓冲区保持初始值 0。原因三:AD0 被拉高,实际地址是 0x69,代码里写 0x68。

解决:先扫地址(用 2.3 节的函数),确认设备实际地址。然后读 WHO_AM_I 验证链路,再写 PWR_MGMT_1 退出睡眠。这三步按顺序做,全是 0 的问题能解决八成。

5.2 现象:读出来全是 0xFF 或 255

原因:SDA 线上没有有效的设备响应,但读到了高电平。常见于 SDA 断开、上拉电阻缺失、或者 SCL/SDA 接反。0xFF 和 0x00 的区别恰好说明问题方向不同——全 0 是设备在响应但没数据,全 0xFF 是总线上根本没设备。

解决:检查 SCL/SDA 是否接反,量一下引脚电压。正常空闲状态 SCL/SDA 都应该是 3.3V 左右的高电平,如果 SDA 是 0V,检查上拉电阻或者模块是否供电。用逻辑分析仪是最好的排查手段,没有的话就用 GPIO 翻转 I2C 的方式模拟时序,辅助判断引脚通断。

5.3 现象:加速度计 Z 轴读数不是 1g,动起来数值乱跳

原因:一是量程配置和换算系数不匹配,比如寄存器里设了 ±4g 但代码里还用 16384 做分母,静态值变成 8192 左右而不是 16384;二是模块物理摆放不是水平的,Z 轴有分量被 X/Y 分走;三是电源纹波太大,传感器供电不稳导致数据跳动。

解决:先看静态三轴平方和,确认是否接近 1g。如果量程不对,改代码里的分母或者改寄存器配置,保持两边一致。电源问题的话,给模块的 VCC 加一个 100nF 去耦电容,电源线和地线用短而粗的杜邦线,避免和电机驱动共用电源。

5.4 现象:磁力计数据乱跳,或者转动方向时数值变化毫无规律

原因:周围有铁磁物质干扰。HMC5883L 对磁场非常敏感,电机、扬声器、螺丝刀、甚至桌子上的铁件都会影响读数。另一个原因是没做硬磁校准,板载的磁力计周围如果有焊锡、铜柱,会引入一个固定的偏置磁场,让数据在某个方向上一直偏大。

解决:把模块拿到远离铁器的地方测试,排除环境干扰。硬磁校准的做法是让模块在空中缓慢绕 8 字,记录三个轴的最大值和最小值,取中值作为偏移量,每次读数减去这个偏移。偏移量通常不是一个常数,所以正规做法是每次上电做一次校准,或者至少首次使用时校准后把偏移量存下来。

5.5 现象:硬件 I2C 跑一段时间卡死,BUSY 标志一直为 1

原因:I2C 总线上的噪声或时序异常导致状态机进入错误状态,HAL 库的HAL_I2C_GetState会一直停在HAL_I2C_STATE_BUSY。这个现象在 STM32 硬件 I2C 上出现频率不低,尤其当总线上挂了三颗芯片、线又比较长的时候。有人会归咎于芯片本身的玄学,但本质是总线竞争和上拉电阻不合适。

解决:彻底断电后重启能恢复,但程序里要做保护。常见做法是每次通信前检查HAL_I2C_GetState,如果卡在 BUSY,调用HAL_I2C_DeInit再重新HAL_I2C_Init。如果问题频繁,我最推荐的办法是改用 GPIO 模拟 I2C,用两个引脚手动翻转时钟和数据线,速度降到 100kHz 左右。模拟 I2C 的时序完全可控,遇到总线异常可以直接拉低 SCL 复位从机,这个方案在我的项目里几乎不会再卡死。

6. 进阶技巧:陀螺仪零偏自动校准,比滤波更先该做的事

互补滤波能压制高频抖动,但压不掉陀螺仪零偏。零偏就是静止时陀螺仪输出不为 0 的那个偏差,它会被积分累积成持续漂移的角度。很多人调半天滤波系数发现角度还是慢慢飘,其实就是零偏没有校准。我一般会在程序里放一个自动校准流程:上电后要求模块静止 2 秒,采集 100 帧陀螺仪数据求平均,把这个平均值作为零偏存下来,运行时每帧都减去它。

// 上电静止校准:采集 100 次陀螺仪 X 轴数据求均值 int32_t sum_gx = 0; for (int i = 0; i < 100; i++) { sum_gx += read_gyro_x_raw(); // 读取原始值 HAL_Delay(2); // 每帧间隔 2ms,总耗时约 200ms } float gx_offset = (float)sum_gx / 100.0f;

校准后的使用方式:运行时先读原始值,减去gx_offset,再除以灵敏度,得到修正后的角速度,最后参与互补滤波积分。注意这个校准必须在静止状态下完成,如果上电时模块在晃动,校准值就是错的。我会在代码里加一个提示:校准期间串口打印「请保持模块静止」,等它打印「校准完成」后再允许用户移动。这个逻辑虽然简单,但效果立竿见影——静止时角度漂移能从每分钟几十度压到几分钟一度以内。

另外一个习惯是:每次上电都重新校准,不要把校准值写死在程序里。陀螺仪零偏会随温度变化,冬天和夏天的值不一样,同一块板子换个环境就得重新校。把校准做成开机必经流程,看起来多花 200ms,实际上换来的是整个运行期间的数据可靠性。希望这篇笔记能帮你把 stm32 读取 gy87 这条路走顺,少踩几个我当年踩过的坑。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 18:58:13

Eigen3 头文件库落地实战:从解压配置到矩阵运算与性能调优

简介&#xff1a;由 VS2017 配合 CMake3 编译的 Eigen3 最新版预编译包&#xff0c;是面向 Visual Studio 环境 C 算法开发者的矩阵计算基础库。内置 libeigen*.dll.a 静态库&#xff0c;工程中仅需设置链接输入即可使用&#xff0c;省去手动编译整套 Eigen 源码的繁琐流程&…

作者头像 李华
网站建设 2026/10/15 18:58:12

C语言实现彩色转灰度图像:BMP解析与定点灰度化

简介&#xff1a;本资源是面向数字图像处理初学者与高校实验课程学习者的C语言灰度化实践项目&#xff0c;聚焦彩色BMP图像向灰度图像的转换原理与工程实现。项目基于Visual Studio 2010开发环境&#xff0c;完整覆盖BMP文件头解析、RGB像素遍历、加权平均灰度公式&#xff08;…

作者头像 李华
网站建设 2026/10/15 18:56:07

STM32 HAL库下0.96寸OLED驱动移植:I2C与SPI实战避坑指南

简介&#xff1a;这份资源是江协科技0.96寸OLED驱动函数的HAL库移植版本&#xff0c;面向使用STM32微控制器进行嵌入式显示开发的初学者与进阶开发者&#xff0c;帮助解决OLED屏在HAL库环境下初始化、通信与绘图操作繁琐的问题。资源包共151个文件&#xff0c;以c与h源码文件为…

作者头像 李华
网站建设 2026/10/15 18:56:02

如何快速掌握 smc.peering.tw:内置新手引导之旅完整拆解

【免费下载链接】smc.peering.tw A dashboard that shows the current status of submarine cables that connected Taiwan to the world 项目地址&#xff1a; https://gitcode.com/gh_mirrors/sm/smc.peering.tw 点击查看 免费下载 smc.peering.tw 是一个展示台湾海底电缆&a…

作者头像 李华