stm32/sensor/mpu6050-imu.md

六轴姿态传感器(MPU6050,I2C)

知识库 · STM32 📅 初版 2026-09-12 ✏️ 更新于 2026-09-20

六轴姿态传感器(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 引脚模块引脚备注
SCLPB6(I2C1)MPU SCL4.7kΩ 上拉到 3.3V;本板与 OLED 共用总线,地址不冲突
SDAPB7(I2C1)MPU SDA4.7kΩ 上拉到 3.3V
VCC3.3VVCC务必 3.3V,5V 会烧
GNDGNDGND必须共地
AD0GND/3.3VAD0决定地址 0x68/0x69
INT任意 GPIOINT数据就绪中断(可选)

#三、关键寄存器与时序

  • 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)
±2g16384±250°/s131
±4g8192±500°/s65.5
±8g4096±1000°/s32.8
±16g2048±2000°/s16.4

选型建议:做姿态(倾角)用 ±2g / ±500°/s;做冲击、振动检测才用 ±8g/±16g。

#五、I2C 故障排查五步法(2026-09-19 实测复盘,比结论更值钱)

背景:官方固件上电后 OLED 只有框架、数字区全空。最终根因是接线接触不良,过程中还挖出原库一个真 bug。五步走:

  1. 从"缺内容"推出"卡住":数字由主循环逐位写入,数字区连 "0" 都没有 → 主循环没跑过 → 卡在 MPU6050_Init()。屏幕上"缺内容"往往比"显示错内容"信息量更大,直接指向程序卡在哪一步之前。
  2. 调试器读寄存器,先排除硬件(pyocd 可读任意外设寄存器):CR1=0x401(PE+ACK 使能)、SR1/SR2 干净(总线空闲)、GPIOB_CRL 复用开漏 ✔、IDR SCL/SDA 均为高 ✔、APB1ENR I2C1 时钟已开 ✔——硬件层没问题。注意读 CRL 时 nibble 顺序(PB7 在高 4 位)。彩蛋:DR 残留 0x78 = OLED 地址,说明最后一次成功通信是写 OLED。
  3. 读 PC/LR,精确到卡在哪:暂停 CPU 读寄存器,pc 落在库函数 I2C_CheckEvent、r1=0x00070082(=等待地址应答事件)→ MCU 发地址出去从机没回 ACK。不应答三种可能:地址不对(AD0 电平)/ 接线问题 / 模块没供电。
  4. 写带超时的探测固件,让总线上"有谁"自己报名:对候选地址发 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)也在线 → 地址正确,根因是接线接触,整理接线后恢复。留一个"已知正常"的基准设备,能立刻区分"总线坏了"和"设备没接好"。

  1. 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)→ 融合出角度再交应用层 → 长时间运行定期重校准。

#八、踩坑清单

  1. 先读 WHO_AM_I:读不到 0x68 先排物理层;读到 0xD1 = 缺 RxNE 等待读残留(修复见排查节第 5 步),不是芯片坏了。
  2. 必须解除睡眠:PWR_MGMT_1 默认睡眠,不写 0x00 读到的全是 0。
  3. 3.3V 供电:模块标 5V 耐受指 VCC 有 LDO,I2C 信号是 3.3V 逻辑;5V 信号进 3.3V MCU 有风险。
  4. 上拉电阻:I2C 必须 4.7kΩ 上拉,板上无则外加。
  5. 地址冲突:AD0 接 GND/3.3V 切换 0x68/0x69,两个模块共存靠它区分。
  6. I2C 驱动所有等待无超时 → 整机假死:原库 while(!I2C_CheckEvent(...)) 无超时,一个接线问题就卡死在初始化(屏幕"有框架没数据")。所有等待一律加超时,排查用带超时的探测函数(见排查节)。
  7. 接线/接触不良(本次根因):地址发出去没有 ACK。先整理接线,再用探测固件确认设备是否在线。
  8. UV4 是 GUI 程序:命令行 & UV4.exe -r 不等编译完就接着烧 → 烧进旧 hex(屏幕还是旧固件)。用 Start-Process -Wait,或核对烧录字节数与预期一致。
  9. pyocd 报 identical、programmed 0:改了没写进去 → pyocd flash --erase chip 强制全片擦写。
  10. 负数显示成大数:u16 解释补码,陀螺仪反向时显示 6 万多 → 数据改 s16 + 显示带符号。
  11. 加速度零偏:平放 Pitch 显示 3.4° 不是 0° 属正常(模块零偏/桌面倾斜),要求归零做水平校准。
  12. 姿态解算:原始 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、相关链接

#12、参考库/链接

  • MPU6050 寄存器映射(InvenSense PS-MPU-6000A-00)
  • 洋桃1号 A5 周围电路手册(加速度/陀螺仪章节)

#13、更新日志

📝共 2 条记录
日期类型更新内容
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)