六轴姿态传感器(MPU6050,I2C)
六轴姿态传感器(MPU6050,I2C)
MPU6050 集成了 3 轴加速度计 + 3 轴陀螺仪,经 I2C 读取,是平衡小车、四轴飞行器的姿态感知核心。本文为 STM32 驱动要点速查 + 一套实测复盘过的 I2C 故障排查方法(2026-09-19 上板验证)。
#一、元信息
| 字段 | 值 |
|---|---|
| 芯片/平台 | STM32F103C8T6(洋桃1号,官方例程 demo40;通用驱动写法见下,HAL 兼容) |
| 接口类型 | I2C(从机地址 0x68 或 0x69,AD0 决定;写地址 8 位形式 0xD0/0xD2) |
| 验证状态 | ✅ 已验证 |
| 验证环境 | 洋桃1号 / Keil MDK(AC5)/ 2026-09-19:故障排查 → 带符号显示 → 零偏校准 → 互补滤波输出 Pitch/Roll 全链路实测 |
| 驱动方式 | 官方软件 I2C 时序库(已修复缺 RxNE 等待 bug);通用 HAL 写法见下 |
#二、接线表
| 信号 | MCU 引脚 | 模块引脚 | 备注 |
|---|---|---|---|
| SCL | PB6(I2C1) | MPU SCL | 4.7kΩ 上拉到 3.3V;本板与 OLED 共用总线,地址不冲突 |
| SDA | PB7(I2C1) | MPU SDA | 4.7kΩ 上拉到 3.3V |
| VCC | 3.3V | VCC | 务必 3.3V,5V 会烧 |
| GND | GND | GND | 必须共地 |
| AD0 | GND/3.3V | AD0 | 决定地址 0x68/0x69 |
| INT | 任意 GPIO | INT | 数据就绪中断(可选) |
#三、关键寄存器与时序
- WHO_AM_I(0x75):读应得
0x68,用于确认通信正常、地址正确。读到 0xD1 是另一个病——见排查节第 5 步。 - PWR_MGMT_1(0x6B):写
0x00解除睡眠(默认上电睡眠)。 - SMPLRT_DIV(0x19):采样率分频,输出率 = 1kHz / (1+DIV)。
- CONFIG(0x1A):低通滤波(DLPF)配置。
- GYRO_CONFIG(0x1B)/ ACCEL_CONFIG(0x1C):量程配置,量程越小分辨率越高。
- 加速度数据:ACCEL_XOUT_H/L(0x3B 起,6 字节);陀螺仪:GYRO_XOUT_H/L(0x43 起,6 字节)。16 位补码,除以灵敏度得物理量。
#四、量程换算速查
| 加速度量程 | LSB/g | 陀螺仪量程 | LSB/(°/s) |
|---|---|---|---|
| ±2g | 16384 | ±250°/s | 131 |
| ±4g | 8192 | ±500°/s | 65.5 |
| ±8g | 4096 | ±1000°/s | 32.8 |
| ±16g | 2048 | ±2000°/s | 16.4 |
选型建议:做姿态(倾角)用 ±2g / ±500°/s;做冲击、振动检测才用 ±8g/±16g。
#五、I2C 故障排查五步法(2026-09-19 实测复盘,比结论更值钱)
背景:官方固件上电后 OLED 只有框架、数字区全空。最终根因是接线接触不良,过程中还挖出原库一个真 bug。五步走:
- 从"缺内容"推出"卡住":数字由主循环逐位写入,数字区连 "0" 都没有 → 主循环没跑过 → 卡在
MPU6050_Init()。屏幕上"缺内容"往往比"显示错内容"信息量更大,直接指向程序卡在哪一步之前。 - 调试器读寄存器,先排除硬件(pyocd 可读任意外设寄存器):
CR1=0x401(PE+ACK 使能)、SR1/SR2干净(总线空闲)、GPIOB_CRL复用开漏 ✔、IDRSCL/SDA 均为高 ✔、APB1ENRI2C1 时钟已开 ✔——硬件层没问题。注意读 CRL 时 nibble 顺序(PB7 在高 4 位)。彩蛋:DR残留 0x78 = OLED 地址,说明最后一次成功通信是写 OLED。 - 读 PC/LR,精确到卡在哪:暂停 CPU 读寄存器,
pc落在库函数I2C_CheckEvent、r1=0x00070082(=等待地址应答事件)→ MCU 发地址出去从机没回 ACK。不应答三种可能:地址不对(AD0 电平)/ 接线问题 / 模块没供电。 - 写带超时的探测固件,让总线上"有谁"自己报名:对候选地址发
START + 地址看谁回 ACK,所有等待必须带超时(所以探测函数必定返回,不像原库那样卡死):
cu8 I2C_Probe(u8 addr8){
u32 to = 20000;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) && --to); /* 等总线空闲(带超时) */
if(to == 0) return 0xFF;
I2C_GenerateSTART(I2C1, ENABLE);
to = 20000;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT) && --to);
if(to == 0){ I2C_GenerateSTOP(I2C1, ENABLE); return 0xFE; }
I2C_Send7bitAddress(I2C1, addr8, I2C_Direction_Transmitter);
to = 20000;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) && --to){
if(I2C_GetFlagStatus(I2C1, I2C_FLAG_AF)){ /* 无应答标志 */
I2C_ClearFlag(I2C1, I2C_FLAG_AF);
I2C_GenerateSTOP(I2C1, ENABLE);
return 0; /* NACK */
}
}
if(to == 0){ I2C_GenerateSTOP(I2C1, ENABLE); return 0xFE; }
I2C_GenerateSTOP(I2C1, ENABLE);
return 1; /* ACK */
}
探测结果(OLED 显示):78:ACK D0:ACK → 0x78(板上 OLED,已知正常的基准)在线、0xD0(MPU6050)也在线 → 地址正确,根因是接线接触,整理接线后恢复。留一个"已知正常"的基准设备,能立刻区分"总线坏了"和"设备没接好"。
- WHO_AM_I 读出 0xD1 = 原库真 bug:0xD1 恰等于读地址 0xD0|1。原库
I2C_READ_BYTE读 DR 前没等 RxNE,读回来的是上一拍残留:
cI2C_AcknowledgeConfig(I2C1,DISABLE);
I2C_GenerateSTOP(I2C1,ENABLE);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); /* 等 RxNE(补上这行) */
a = I2C_ReceiveData(I2C1);
多字节读函数 I2C_READ_BUFFER 里有 BYTE_RECEIVED 判断,所以六轴数据一直正常——这解释了"为什么数据能读、WHO_AM_I 却读不对"。
方法论总结:屏幕"缺内容"→ 优先怀疑卡在某步,而不是显示驱动;有无超时是嵌入式驱动的分水岭;调试器不只能单步,读任意寄存器、读 PC 比插 printf 更快;拿不准地址就写探测固件;留基准设备。
#六、数据处理链路(换算 → 校准 → 滤波 → 融合)
① 原始值是 u16 会丢符号:陀螺仪往回摆时 -1 显示成 65535。数据改 s16 接收(位模式相同,解释方式变了),OLED 显示"符号 + 5 位数字"。
② 零偏校准(必做):陀螺仪静止时不为 0 且随温度漂移。上电静置 1~2 秒采样几百次取平均作 offset,运行时减掉:
cfor(i = 0; i < 200; i++){ /* 板子必须静止 */
MPU6050_READ((u16*)t);
sx += (s16)t[3]; sy += (s16)t[4]; sz += (s16)t[5];
delay_ms(5);
}
gx_off = (float)sx / 200.0f;
rate_x = ((float)gx - gx_off) / 16.4f; /* 这才是真实角速度 */
加速度计也有零偏(实测 AX 偏 -1070 LSB ≈ 0.065g,会让"平放角"偏 3.4°)——要求"放平显示 0°"需另做水平校准,且校准时板子必须真水平。
③ 从数据到角度的三条路线:
- 路线 1 纯加速度算倾角:
pitch = atan2f(-ax, sqrtf(ayay+azaz)) * 57.29578f——长期不漂但动态错,适合静态倾角仪; - 路线 2 陀螺仪积分:
angle += rate * dt——响应快但误差累积,几分钟漂几度; - 路线 3 互补滤波(工程最常用,本模块采用):陀螺仪管短期、加速度慢慢纠正漂移,
pitch = 0.98f(pitch + rate_yDT) + 0.02f*pitch_acc,计算量极小 51 都跑得动。想更稳上卡尔曼;想省事用 DMP;要绝对偏航角必须外挂磁力计(MPU6050 没有)。
④ 最终效果(实测):上电静置 1.5s 自动校准 → OLED 显示 Pitch: +3.4 / Roll: -0.8,倾斜实时跟随、快速晃动后自动收敛;串口每秒打印 Pitch=3.42 Roll=-0.75。
#七、应用场景速查
| 应用 | 主要用到 | 数据处理要点 |
|---|---|---|
| 平衡车 / 自平衡机器人 | 倾角 | 互补滤波/卡尔曼出角度 → PID 闭环驱动电机 |
| 云台 / 相机稳定器 | 角速度 + 姿态 | 陀螺仪做前馈,角度做反馈 |
| 计步器 / 运动手环 | 加速度波形 | 低通滤波 → 峰值检测(阈值不能直接比原始值) |
| 跌倒检测 | 加速度 + 姿态突变 | 冲击阈值触发 + 姿态持续判定,避免误报 |
| 无人机飞控 | 姿态(四元数) | MPU6050 只能入门,正式产品用更好的 IMU |
| 工业振动监测 | 高频加速度(±16g) | FFT 看频谱 |
| 车辆坡道 / 机械倾角报警 | 静态倾角 | 加速度算角 + 平滑滤波 + 阈值比较 |
工程落地 Checklist:选量程 → 配 DLPF(5~20Hz)→ 定采样率(50~200Hz,定时器等间隔,别 delay_ms 凑)→ 上电静置校零偏 → 软件滤波(一阶 IIR y=0.9y+0.1x)→ 融合出角度再交应用层 → 长时间运行定期重校准。
#八、踩坑清单
- 先读 WHO_AM_I:读不到 0x68 先排物理层;读到 0xD1 = 缺 RxNE 等待读残留(修复见排查节第 5 步),不是芯片坏了。
- 必须解除睡眠:PWR_MGMT_1 默认睡眠,不写 0x00 读到的全是 0。
- 3.3V 供电:模块标 5V 耐受指 VCC 有 LDO,I2C 信号是 3.3V 逻辑;5V 信号进 3.3V MCU 有风险。
- 上拉电阻:I2C 必须 4.7kΩ 上拉,板上无则外加。
- 地址冲突:AD0 接 GND/3.3V 切换 0x68/0x69,两个模块共存靠它区分。
- I2C 驱动所有等待无超时 → 整机假死:原库
while(!I2C_CheckEvent(...))无超时,一个接线问题就卡死在初始化(屏幕"有框架没数据")。所有等待一律加超时,排查用带超时的探测函数(见排查节)。 - 接线/接触不良(本次根因):地址发出去没有 ACK。先整理接线,再用探测固件确认设备是否在线。
- UV4 是 GUI 程序:命令行
& UV4.exe -r不等编译完就接着烧 → 烧进旧 hex(屏幕还是旧固件)。用Start-Process -Wait,或核对烧录字节数与预期一致。 - pyocd 报 identical、programmed 0:改了没写进去 →
pyocd flash --erase chip强制全片擦写。 - 负数显示成大数:u16 解释补码,陀螺仪反向时显示 6 万多 → 数据改 s16 + 显示带符号。
- 加速度零偏:平放 Pitch 显示 3.4° 不是 0° 属正常(模块零偏/桌面倾斜),要求归零做水平校准。
- 姿态解算:原始 6 轴需融合。新手先用互补滤波;MPU6050 自带 DMP 可硬件输出四元数但配置复杂;要绝对偏航必须磁力计。
#九、驱动代码(要点,HAL 风格)
c#define MPU_ADDR 0x68
void mpu_write(uint8_t reg, uint8_t val)
{
uint8_t d[2] = {reg, val};
HAL_I2C_Master_Transmit(&hi2c1, MPU_ADDR<<1, d, 2, 100);
}
uint8_t mpu_read(uint8_t reg)
{
uint8_t v;
HAL_I2C_Mem_Read(&hi2c1, MPU_ADDR<<1, reg, 1, &v, 1, 100);
return v;
}
void mpu_init(void)
{
mpu_write(0x6B, 0x00); /* 唤醒 */
mpu_write(0x19, 0x07); /* 采样率 ~125Hz */
mpu_write(0x1A, 0x06); /* DLPF */
mpu_write(0x1B, 0x18); /* 陀螺 ±2000°/s */
mpu_write(0x1C, 0x10); /* 加速度 ±8g */
}
void mpu_read_all(int16_t *ax,int16_t *ay,int16_t *az,int16_t *gx,int16_t *gy,int16_t *gz)
{
uint8_t buf[14];
HAL_I2C_Mem_Read(&hi2c1, MPU_ADDR<<1, 0x3B, 1, buf, 14, 100);
*ax = (buf[0]<<8)|buf[1]; *ay = (buf[2]<<8)|buf[3]; *az = (buf[4]<<8)|buf[5];
*gx = (buf[8]<<8)|buf[9]; *gy = (buf[10]<<8)|buf[11]; *gz = (buf[12]<<8)|buf[13];
}
#十、调用示例
cmpu_init();
if (mpu_read(0x75) != 0x68) { /* 通信异常,按"排查五步法"走 */ }
int16_t ax,ay,az,gx,gy,gz;
mpu_read_all(&ax,&ay,&az,&gx,&gy,&gz);
sprintf(buf, "AX=%d AY=%d AZ=%d GX=%d GY=%d GZ=%d\r\n", ax,ay,az,gx,gy,gz);
#11、相关链接
- ADC+OLED 调试复盘——同系列调试方法论
- 软件 I2C:GPIO 模拟时序——I2C 总线的底层原理
- 温湿度传感器(DHT11,单总线)——同批实测的 OLED 显示改造模块
- 洋桃1号下载模式速查——SWD 连不上时的排查
- 文章:STM32 低功耗三兄弟实测——"等待无超时假死"的 RTC 版同类病
#12、参考库/链接
- MPU6050 寄存器映射(InvenSense PS-MPU-6000A-00)
- 洋桃1号 A5 周围电路手册(加速度/陀螺仪章节)
#13、更新日志
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-09-12 | 新增 | 首次建档:MPU6050 I2C 驱动 + 寄存器 + 踩坑 + 姿态解算提示 |
| 2026-09-20 | 实测更新 | 验证状态 ❌→✅(洋桃1号 2026-09-19 实测);新增"I2C 故障排查五步法"(读寄存器→读 PC→带超时探测固件→WHO_AM_I 0xD1 挖出原库缺 RxNE bug);新增量程换算表、数据处理链路(s16 带符号 / 零偏校准 / 互补滤波)与应用场景速查;踩坑清单并入实机坑(无超时假死、接线接触、UV4 GUI 不等待、pyocd identical) |