
做四轴、平衡小车或者给机械臂加个姿态反馈MPU6050基本都是入门首选。这块芯片把三轴陀螺仪和三轴加速度计封装在一起内部还带了一个叫DMP的数字运动处理器可以直接输出四元数省去在MCU上跑卡尔曼滤波的麻烦。STM32F103又是玩嵌入式绕不过去的平台HAL库则是ST官方现在主推的开发方式。把这三样凑在一起就是标题里那件事用HAL库的STM32F103通过软件I2C读写MPU6050再移植DMP库完成姿态解算。写这篇文章的动机是最近帮几个朋友看代码发现大家在MPU6050上卡住的点高度一致要么I2C读不到数据要么DMP固件加载失败要么编译报一大串链接错误。其实这些问题都有固定解法只是网上资料比较分散。这篇我按从底层到上层的顺序把GPIO模拟I2C、寄存器配置、DMP移植的完整路径走一遍代码和排查思路都给到。整个过程不需要用到F1系列的硬件I2C外设所以也就不存在硬件I2C那些著名的坑很适合不想折腾、想尽快跑通姿态数据的场景。1. 项目方案拆解为什么是“软件I2C HAL库 DMP”这套组合1.1 为什么不用F1的硬件I2C这是个老话题。STM32F103的硬件I2C被吐槽了很多年尤其是在标准外设库时代I2C事件处理特别繁琐加上一些已知的总线卡死问题劝退过不少人。HAL库发布之后底层用状态机封装了I2C流程比标准库好用了不少但很多工程师做传感器读取时仍然保留着用软件模拟I2C的习惯。软件I2C在这个场景下有实实在在的优势引脚任意分配不在乎I2C外设复用到的具体引脚改版方便。时序完全透明出问题时用逻辑分析仪看波形自己心里有数。速率可控遇到线长、干扰、器件不识别时把时钟调慢一点往往就好了。代码基本零依赖换个平台只需要改两个GPIO操作宏。对于MPU6050这种数据量不大、读取频率通常在100Hz到200Hz的传感器软件I2C的带宽完全够用。只有当系统里挂了多个I2C从机、又追求低CPU占用时硬件I2C加DMA才有明显优势。1.2 HAL库还是标准库现在新开项目我基本都推荐HAL库。一是CubeMX生成工程速度快时钟树、引脚配置一目了然二是用户代码区域做了隔离生成代码时不用担心手写部分被覆盖三是HAL库的命名和封装风格统一看别人的工程也能少花点时间。软件I2C本质上就是操作GPIO用HAL库的HAL_GPIO_WritePin和HAL_GPIO_ReadPin就够了没有额外的复杂度加成。唯一要稍微留意的是HAL库的GPIO初始化结构体参数比标准库多一点但对应用层的代码贡献就是几行配置后面会给出完整的CubeMX操作流程。1.3 DMP库解决了什么问题DMP是MPU6050内部的一块可编程处理器I2C主机把固件写进去之后姿态解算就在传感器内部完成了。MCU只需要按设定的频率从FIFO寄存器里读取四元数再做一次角度换算就能得到roll、pitch、yaw。这样省去了MCU端跑互补滤波或卡尔曼滤波的算力开销也避开了调滤波参数的麻烦对入门和小型项目都非常友好。不过DMP固件并不是开放源码的实际使用中是调用InvenSense提供的驱动库也就是几个.c和.h文件拼起来的方案。库文件的版本比较多网上找来的代码细节往往有出入只要搭配合适都能正常工作。2. MPU6050关键寄存器速查读写前必须搞清的协议细节2.1 器件地址与引脚连接MPU6050的I2C从机地址由AD0引脚决定。模块上AD0接地时地址是0x68AD0接高电平时地址是0x69。大多数成品模块默认接地所以代码里通常看到0x68。在I2C总线上发送时这个7位地址需要左移一位再拼上读写位写地址0xD0读地址0xD1。接线方面模块的VCC接3.3VGND接GNDSCL和SDA分别接到单片机的两个GPIO。如果用的是裸芯片而不是模块SCL和SDA上各需要加一个4.7kΩ的上拉电阻到3.3V。成品模块一般板载了上拉电阻直接接线即可。INT引脚建议也接到MCU的一个GPIO上用于DMP的FIFO中断通知后面会细说。2.2 常用寄存器清单寄存器名地址上电默认值作用WHO_AM_I0x750x68读取器件ID用于确认I2C通信正常PWR_MGMT_10x6B0x40电源管理bit6为SLEEP位需清零唤醒SMPLRT_DIV0x190x00采样率分频实际采样率 内部采样率 / (1该值)CONFIG0x1A0x00数字低通滤波DLPF配置影响带宽和延时GYRO_CONFIG0x1B0x00陀螺仪量程选择ACCEL_CONFIG0x1C0x00加速度计量程选择FIFO_EN0x230x00选择哪些数据写入FIFOINT_ENABLE0x380x00中断使能INT_STATUS0x3A0x00中断状态标志USER_CTRL0x6A0x00FIFO复位、I2C主模式、传感器复位FIFO_COUNT_H/L0x72/0x730x00FIFO中未读数据的字节数ACCEL_XOUT_H0x3B-三轴加速度计数据从0x3B到0x40共6字节GYRO_XOUT_H0x43-三轴陀螺仪数据从0x43到0x48共6字节初始化时最核心的就三个寄存器先写电源管理寄存器唤醒设备再配置CONFIG设置低通滤波最后分别设置陀螺仪和加速度计的量程。采样率分频寄存器如果保持默认采样率是1kHz也能用但实际项目里一般会配合DLPF设置成200Hz或100Hz。2.3 I2C读写MPU6050的时序要点MPU6050是标准I2C从机读写时序没有特殊要求。写一个字节的过程是起始位、发送从机地址加写位、等待ACK、发送寄存器地址、等待ACK、发送数据、等待ACK、停止位。读多个连续寄存器时先发送寄存器起始地址然后发一个重复起始位再把从机地址切到读模式之后每个字节回ACK只有最后一个字节回NACK。很多新手在软件模拟时漏掉“重复起始位”导致读取失败。先写寄存器地址、再切读模式这一步必须通过重复起始位来完成不能在中间插入停止位否则读操作会错乱。3. 软件I2C驱动编写GPIO模拟时序的完整实现3.1 引脚选择与CubeMX初始化配置我习惯用PB6做SCL、PB7做SDA。这两个引脚在F103上靠近布线方便而且不在JTAG占用的默认引脚里。在CubeMX里把PB6和PB7都设置为GPIO_Output模式输出类型选择Open Drain开漏启用内部上拉。开漏输出配合上拉电阻是标准I2C推荐的接法。这样做的原因是I2C总线上的设备通过拉低电平来通信高电平由电阻提供避免多个设备同时驱动总线造成短路。CubeMX里勾选Pull-up后内部上拉电阻大约40kΩ左右可以直接使用。如果你选择推挽输出也能工作但严谨场景下我不建议因为推挽输出强拉高电平如果总线上某个从机正在拉低电平就会形成冲突。GPIO初始化代码大致长这样GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct);串口调试和LED灯如果占用了其他引脚正常配置即可不冲突。3.2 核心时序代码起始、停止、发送、接收软件I2C的核心难点在于把时序写对。我用宏定义封装了引脚操作这样以后换引脚只改宏就行。#define I2C_SCL_PORT GPIOB #define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PORT GPIOB #define I2C_SDA_PIN GPIO_PIN_7 #define I2C_SCL_H() HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET) #define I2C_SCL_L() HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_RESET) #define I2C_SDA_H() HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_SET) #define I2C_SDA_L() HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_RESET) #define I2C_SDA_READ() HAL_GPIO_ReadPin(I2C_SDA_PORT, I2C_SDA_PIN) static void I2C_Delay(void) { uint8_t i 20; while (i--); }延时函数决定了I2C时钟频率。F103跑72MHz时一次空循环大概几个周期i等于20时半周期大概5us左右整体时钟在100kHz上下正好符合标准I2C的速率要求。如果线比较长或者干扰大把i调大时钟放慢稳定性会明显提升。void I2C_Start(void) { I2C_SDA_H(); I2C_SCL_H(); I2C_Delay(); I2C_SDA_L(); I2C_Delay(); I2C_SCL_L(); } void I2C_Stop(void) { I2C_SCL_L(); I2C_SDA_L(); I2C_Delay(); I2C_SCL_H(); I2C_Delay(); I2C_SDA_H(); }起始位的定义是SCL高电平期间SDA从高变低停止位是SCL高电平期间SDA从低变高。这两个时序必须严格满足很多I2C通信失败就是起始位和停止位顺序写错了。发送一个字节并读取从机ACK的代码uint8_t I2C_SendByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { if (data 0x80) I2C_SDA_H(); else I2C_SDA_L(); data 1; I2C_Delay(); I2C_SCL_H(); I2C_Delay(); I2C_SCL_L(); } // 释放SDA由从机拉低表示ACK I2C_SDA_H(); I2C_Delay(); I2C_SCL_H(); I2C_Delay(); uint8_t ack I2C_SDA_READ(); I2C_SCL_L(); return (ack 0) ? 0 : 1; }数据位在SCL高电平期间必须保持稳定从机在SCL高电平时采样SDA。所以每次数据翻转要等SCL拉低之后再操作SCL拉高之后不能再动SDA。接收一个字节并决定是否回ACKuint8_t I2C_ReadByte(uint8_t ack) { uint8_t i, data 0; I2C_SDA_H(); // 释放SDA总线 for (i 0; i 8; i) { data 1; I2C_SCL_H(); I2C_Delay(); if (I2C_SDA_READ()) data | 0x01; I2C_SCL_L(); I2C_Delay(); } if (ack) I2C_SDA_L(); // 回ACK else I2C_SDA_H(); // 最后一字节回NACK I2C_SCL_H(); I2C_Delay(); I2C_SCL_L(); I2C_SDA_H(); return data; }接收字节时要先把SDA切换成输入状态或者至少确保当前输出是高电平否则读到的永远是自己的输出值。上面代码用I2C_SDA_H()释放总线配合开漏输出模式等效于让出SDA控制权此时读取引脚电平是有效的。3.3 软件I2C的注意事项实际操作中有几个点需要特别注意第一发送地址后从机没有拉低ACK最常见原因是器件地址不对。模块上AD0如果接高电平地址就是0x69而不是0x68所有读写地址都要跟着变。第二读多字节时最后一个字节必须回NACK。如果最后一个字节也回ACK从机会认为主机还想继续读导致后面总线状态错乱。第三程序里尽量减少中断对时序的干扰。软件I2C对延时敏感如果系统里开了多个中断且中断处理时间较长SCL高电平时间会被拉长。I2C协议本身允许时钟拉伸所以一般问题不大但极端情况下会降低通信速率。4. MPU6050驱动实现初始化到数据读取4.1 初始化流程唤醒、量程、滤波、采样率MPU6050上电默认处于睡眠模式PWR_MGMT_1寄存器bit6为1。初始化第一步就是清掉这个睡眠位。[ PWR_MGMT_1 0x01 ]写入0x01不仅唤醒设备还顺便把时钟源切到了X轴陀螺仪PLL。默认时钟源是内部8M RC振荡器精度一般且受温度影响陀螺仪数据容易产生漂移。X轴陀螺仪PLL的时钟精度高得多对姿态解算有实际帮助。量程设置上陀螺仪我一般选±2000°/s写入GYRO_CONFIG寄存器0x18。加速度计量程选±2gACCEL_CONFIG写0x00。对于一般的平衡车、机械臂项目±2000°/s足够±2g加速度量程也能保证分辨率最高。如果你做的是剧烈运动的无人机加速度计量程可能需要调大灵敏度会相应下降。CONFIG寄存器设置数字低通滤波。DLPF3时3dB带宽约44Hz采样率约1kHz。对于姿态解算来说44Hz带宽能把高频振动滤掉不少同时延迟也不算大。如果运动频率较高可以调低DLPF的档位让带宽更大比如配置为0x02对应94Hz。SMPLRT_DIV寄存器用来分频采样率设置采样率为200Hz时写入值4因为内部1kHz采样率要除以(14)5。初始化函数完整代码uint8_t MPU6050_Init(void) { uint8_t val; // 读WHO_AM_I验证I2C通路 MPU6050_Read_Byte(0x75, val); if (val ! 0x68) return 1; // 唤醒MPU6050选择X轴陀螺仪PLL时钟 val 0x01; MPU6050_Write_Byte(0x6B, val); // 配置数字低通滤波 44Hz val 0x03; MPU6050_Write_Byte(0x1A, val); // 陀螺仪量程 ±2000°/s灵敏度16.4 LSB/(°/s) val 0x18; MPU6050_Write_Byte(0x1B, val); // 加速度计量程 ±2g灵敏度16384 LSB/g val 0x00; MPU6050_Write_Byte(0x1C, val); // 采样率 1kHz / (14) 200Hz val 0x04; MPU6050_Write_Byte(0x19, val); return 0; }这里有个小设计经验先读WHO_AM_I如果通信不通后面配置再多次也是白费。实测中WHO_AM_I能读到0x68基本可以确认接线、地址和I2C时序都没问题接下来调试心态会稳很多。4.2 读取六轴原始数据六轴数据从加速度计起始寄存器0x3B开始连续读取14个字节一次就能把三轴加速度、温度、三轴陀螺仪全部取完。数据都是16位有符号整数高字节在前。typedef struct { int16_t ax; int16_t ay; int16_t az; int16_t temp; int16_t gx; int16_t gy; int16_t gz; } MPU6050_DataTypeDef; uint8_t MPU6050_Read_Accel_Gyro(MPU6050_DataTypeDef *data) { uint8_t buf[14]; if (MPU6050_Read_Len(0x3B, buf, 14)) return 1; >#define MPU6050_INCLUDE_DMP这个宏加上之后inv_mpu.c里的DMP相关代码才会被编译进去否则dmp_开头的函数都是未定义的。有些版本还会用到MPU6050_DRIVER_USE_I2C之类的宏按参考工程里的定义来即可。头文件路径要添加进工程否则编译会报找不到inv_mpu.h。Keil里在Options for Target - C/C - Include Paths里添加对应目录。有两点编译经验先说在前面第一inv_mpu.c在Keil默认AC5编译器下基本能直接过AC6编译器下可能会报一些类型转换警告不影响使用但建议把警告级别适当放宽。第二DMP库内部有大量一次性初始化数组占用RAM较多F103的20KB RAM完全够用但如果你在工程里开了多个大buffer注意观察编译输出的RW和ZI数据量。5.3 平台适配四个关键回调函数inv_mpu.c内部通过宏定义调用了平台相关的底层函数这是移植的核心。需要自己实现i2c_write、i2c_read、delay_ms、get_ms和reg_int_cb五个接口。先用宏定义把库函数映射到我们自己的驱动函数#define i2c_write MPU6050_Write_Len #define i2c_read MPU6050_Read_Len #define delay_ms delay_ms #define get_ms get_ms #define reg_int_cb reg_int_cb然后实现这些函数。注意库内部传入的从机地址是7位地址也就是0x68需要左移一位拼上读写位int MPU6050_Write_Len(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *data) { I2C_Start(); if (I2C_SendByte((addr 1) | 0)) // 写模式 { I2C_Stop(); return 1; } I2C_SendByte(reg); for (uint8_t i 0; i len; i) I2C_SendByte(data[i]); I2C_Stop(); return 0; } int MPU6050_Read_Len(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *data) { I2C_Start(); if (I2C_SendByte((addr 1) | 0)) // 写模式先发寄存器地址 { I2C_Stop(); return 1; } I2C_SendByte(reg); I2C_Start(); // 重复起始位 if (I2C_SendByte((addr 1) | 1)) // 切到读模式 { I2C_Stop(); return 1; } for (uint8_t i 0; i len - 1; i) data[i] I2C_ReadByte(1); // 回ACK data[len - 1] I2C_ReadByte(0); // 最后一字节回NACK I2C_Stop(); return 0; }延时函数在DMP固件加载时会被调用用来等待传感器内部处理完成。用HAL库的HAL_Delay即可void delay_ms(unsigned long ms) { HAL_Delay(ms); } long get_ms(unsigned long *count) { *count HAL_GetTick(); return 0; }reg_int_cb用于注册中断回调如果使用轮询方式读取FIFO这个回调可以先留空不影响后续流程。5.4 DMP初始化调用顺序DMP初始化有严格的调用顺序顺序乱了轻则输出异常重则加载固件失败。我踩过几次坑之后总结了一套稳定的调用序列struct int_param_s int_param; int ret; // 1. 初始化MPU6050基础配置 ret mpu_init(int_param); // 2. 开启传感数据 ret mpu_set_sensors(INV_XYZ_GYRO | INV_XYZ_ACCEL); // 3. 配置FIFO ret mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL); // 4. 设置采样率 ret mpu_set_sample_rate(200); // 5. 加载DMP固件 ret dmp_load_motion_driver_firmware(); // 6. 设置传感器方向 ret dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation)); // 7. 使能DMP功能 ret dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT | DMP_FEATURE_SEND_RAW_ACCEL); // 8. 设置DMP输出频率 ret dmp_set_fifo_rate(100); // 9. 开启DMP ret mpu_set_dmp_state(1);每一步的返回值都应该检查我见过有人在dmp_load_motion_driver_firmware这一步返回非零后面读四元数全是零。固件加载失败最常见的原因是I2C时序不够稳定把软件I2C的延时调大一点基本能解决。gyro_orientation数组在inv_mpu.c里有一份默认定义描述的是传感器安装方向。如果MPU6050平放且芯片丝印朝上默认值一般不用改。如果你的模块立着安装或者倒着安装需要按实际方向改这个数组否则roll和pitch符号会反。5.5 读取四元数与欧拉角换算DMP使能后FIFO里会按设定频率产生姿态数据。读取代码short gyro[3]; short accel[3]; long quat[4]; unsigned long timestamp; short sensors; unsigned char more; ret dmp_read_fifo(gyro, accel, quat, timestamp, sensors, more);dmp_read_fifo返回非零表示FIFO里暂时没有新数据正常读取流程是每次都读如果more标志为1说明FIFO里还有滞留数据需要继续读直到more为0。否则数据会出现滞后姿态角变化不流畅。DMP输出的四元数每个分量都是Q30格式也就是实际数值等于原始值除以(2^{30})即1073741824。换算欧拉角的代码float q0 (float)quat[0] / 1073741824.0f; float q1 (float)quat[1] / 1073741824.0f; float q2 (float)quat[2] / 1073741824.0f; float q3 (float)quat[3] / 1073741824.0f; float roll atan2f(2.0f * (q0 * q1 q2 * q3), 1.0f - 2.0f * (q1 * q1 q2 * q2)) * 57.2958f; float pitch asinf(2.0f * (q0 * q2 - q1 * q3)) * 57.2958f; float yaw atan2f(2.0f * (q0 * q3 q1 * q2), 1.0f - 2.0f * (q2 * q2 q3 * q3)) * 57.2958f;这里乘以57.2958是把弧度转成角度。有些版本的DMP库还提供了dmp_get_yaw_pitch_roll函数可以直接返回欧拉角但不同版本函数行为差异比较大我建议还是自己用四元数换算看得明白也方便后面做姿态控制的调试。5.6 中断与轮询的选择DMP有两种读取方式。一种是把INT引脚接到MCU的EXTI下降沿触发中断在中断里置一个标志位主循环检测到标志位后再读取FIFO。另一种是主循环里每隔一段时间主动调用dmp_read_fifo不管有没有新数据全靠函数返回值判断。实测下来轮询方式在F103上完全可行100Hz的DMP输出频率下主循环每5ms到10ms去读一次FIFO数据不会丢。中断方式的好处是CPU不用反复查询但要注意中断服务函数里尽量别做耗时操作只置标志位。如果使用中断方式在初始化传int_param_s结构体时需要正确配置中断回调。我的经验是新手上来先用轮询方式跑通确认DMP输出正常后再改中断方式不迟。6. 常见问题排查从编译报错到数据异常6.1 编译报错“undefined symbol mpu6050”这个错误在Keil里非常常见说的是链接器找不到名为mpu6050的符号。一般原因是参考示例代码里定义了一个全局变量MPU6050_DataTypeDef mpu6050;但你的工程里没有定义或者把定义写在了某个函数内部作用域不对。解决步骤在某个.c文件的全局位置定义MPU6050_DataTypeDef mpu6050;在需要使用该变量的文件里加extern声明extern MPU6050_DataTypeDef mpu6050;检查所有.c文件是否都添加到了Keil的Source Group里漏加文件也会导致符号缺失。6.2 WHO_AM_I读不到0x68这是接线问题最常见的表现。按优先级排查检查项操作供电VCC必须3.3V不能接5V地址AD0接了高电平就改成0x69接线SCL、SDA是否接反GND是否共地外部上拉裸芯片需要加4.7k上拉电阻模块一般不用波形用逻辑分析仪看SCL和SDA是否有数据有次帮人排查发现SDA和SCL两根线在杜邦线中间断了信号时有时无读WHO_AM_I时好时坏。这种物理层面的问题用万用表量一下通断就能查出来。6.3 数据全是0或固定不变如果WHO_AM_I能读到0x68但加速度计和陀螺仪数据全是0大概率是设备还处于睡眠状态。检查PWR_MGMT_1寄存器是否成功写入0x01。可以读回来核对一下uint8_t val; MPU6050_Read_Byte(0x6B, val);正常情况val应该等于0x01。如果读到0x40说明写入失败回看I2C时序或地址配置。如果写入成功但数据还是0检查GYRO_CONFIG和ACCEL_CONFIG的写入值有人会在寄存器地址上写错一位。6.4 DMP固件加载失败的排查dmp_load_motion_driver_firmware返回值非零大概率是I2C传输问题。固件大小有几千字节加载过程中任何一位数据出错传感器内部的校验都可能失败。常见原因第一软件I2C延时太短在连续传输几千字节时出错率升高。把I2C_Delay里的循环次数翻倍时钟降到50kHz左右再试一次。第二加载过程中有其他中断频繁抢占拖慢了I2C时序。可以临时关闭一些无关中断确认是否改善。第三库文件版本与MPU6050固件版本不匹配。换一个来源的DMP库文件比如从其他开源平衡车项目里找一套完整的通常能解决。6.5 DMP输出的姿态角漂移大首先明确一点yaw角在室内、没有磁力计参考的情况下飘是正常的。静置几分钟后yaw慢慢变化属于陀螺仪零偏累积的结果。如果yaw漂移速度非常快比如每秒几度那先检查采样率配置DMP输出频率和实际读取频率是否匹配。roll和pitch在静止时误差超过1度考虑先校准陀螺仪零偏。初始化后静置几十秒取陀螺仪数值的平均值后续读取时减去这个偏移量。有些版本的DMP库内部会自动统计零偏长时间开启后姿态数据会越来越稳。6.6 数据跳变与毛刺如果数据偶尔出现一个很大的跳变优先怀疑供电。MPU6050对电源纹波比较敏感VCC上可以并联一个10uF钽电容加0.1uF陶瓷电容数采会稳定很多。其次是接线太长杜邦线超过20cm后I2C信号质量会明显下降把时钟放慢能缓解。另一个容易被忽略的点是数字低通滤波配置。CONFIG寄存器设置DLPF后FIFO数据是经过滤波的。如果为了追求响应速度把DLPF关掉数据噪声会变大姿态角也会跟着抖。平衡车这类项目DLPF设在带宽44Hz或者94Hz都是常用选择。6.7 程序运行一段时间后数据不更新FIFO溢出是常见原因。DMP输出频率设为100Hz如果主循环里读取不及时FIFO会积满数据新数据进不去数据看起来就像卡死了。解决方式是检查FIFO计数或者调低DMP输出频率。uint16_t fifo_count; MPU6050_Read_Byte(0x72, val); fifo_count (uint16_t)val 8; MPU6050_Read_Byte(0x73, val); fifo_count | val;如果fifo_count长期等于最大值1024说明读取不及时。要么提高读取频率要么降低DMP输出频率要么在确认数据不需要时调用mpu_reset_fifo清空。写在最后的使用体会这套方案我在F103C8T6和F103RCT6上都跑过后来移植到GD32F103和AT32F403A也没有改太多东西GPIO操作宏和延时函数换一下DMP库文件几乎不用动。MPU6050本身是个很皮实的传感器大多数问题出在I2C通信和库文件适配这两个环节把这两块跑通后面加数据滤波、姿态控制都是水到渠成的事。如果后续项目要接多个I2C传感器比如OLED显示屏、气压计、磁力计都在同一条总线上建议把软件I2C再封装一层提供一个统一的读写接口这样应用层调用时不需要关心具体用的是软件I2C还是硬件I2C代码结构会清爽很多。做完这一版之后我最大的体会是DMP库并没有想象中难移植真正花时间的是理解I2C时序的细节把底层基础打牢上层什么都好说。