简介这则STM32F429与MPU6050双设备交互的嵌入式工程压缩包面向单片机开发和传感器应用学习者演示了高性能Cortex-M4平台与六轴运动传感器的协同工作方式。包内共8个文件以4个.h与3个.c源文件为主体涵盖主程序、中断服务、系统时钟配置及外设头文件定义另含一个Keil UV4工程文件project.uvprojx便于直接打开编译。资源整体仅13KB结构精炼适合快速查阅代码框架。目前已有199人学习关注度良好。通过研读main.c、stm32f4xx_it.c等文件可理解MPU6050的初始化与数据读取流程、I2C总线时序处理、中断与定时器配合等关键知识点对入门姿态解算和传感器数据滤波也有实际参考价值。1. 一条 I2C 总线上挂两块 MPU6050先解决地址问题两块 MPU6050 同时出现在一个 STM32F429 项目里第一反应通常是“还能这么接”我先说结论能而且只占一条 I2C 总线。关键不是硬件资源而是地址冲突——传感器从设备的 7 位地址由 AD0 引脚的电平决定一块接地、一块接高地址就是 0x68 和 0x69物理上就分开了。这个“2DEVICES”的标题实际要处理的问题比想象中简单引脚接线、HAL 库初始化、寄存器配置、姿态数据读取再加上双设备的数据同步。适合的场景也很具体双轮平衡机器人两侧轮子各装一个 IMU、机械臂两个关节各自需要姿态反馈、或者同一刚性体上装两块传感器做冗余测量。下面这套流程按“一条总线 两个从机地址”的思路展开硬件连接少、代码量可控跑通了就能直接放到自己的项目里改。2. 静态地址分配与 F429 硬件 I2C 引脚接线2.1 AD0 电平决定 7 位地址0x68 与 0x69MPU6050 的 I2C 从机地址是 7 位但数据手册里写的是“110100X”其中低 1 位 X 由 AD0 引脚决定。AD0 接 GND 时地址低位置 0得到 7 位地址 0x68AD0 接 VDD 时地址低位置 1得到 0x69。这里有一个容易踩的坑7 位地址和 8 位地址混用。HAL 库的HAL_I2C_Mem_Read和HAL_I2C_Mem_Write要求传入的是 8 位地址也就是 7 位地址左移一位后在末尾补上读写方向位。所以代码里写 0x68 或 0x69调用 HAL 函数时要再左移一位。两块传感器分别焊接在同一块底板的两端AD0 走两根固定电平的过孔一块拉低、一块拉高。这是最可靠的做法不推荐用 GPIO 动态切换 AD0 实现“共用地址”——虽然理论上可以分时挂载同一地址但每次切换后都要重新初始化传感器而且期间总线上如果出现其他设备也在用这个地址会直接导致 ACK 冲突。2.2 接线表与上拉电阻选择以 F429 的 I2C1 为例SCL 和 SDA 默认复用 PB6 和 PB7。两块 MPU6050 的 SCL 和 SDA 直接并联到这两条线上INT 引脚则各自连到不同的 EXTI 引脚常用 PE1 和 PE0用于区分中断来源。VDD 统一接 3.3VGND 共地。信号MPU6050#1MPU6050#2STM32F429SCLSCLSCLPB6 (I2C1_SCL)SDASDASDAPB7 (I2C1_SDA)AD0GND3.3V固定电平VDD3.3V3.3V3.3VINTINTINTPE1 / PE0上拉电阻是很多人忽略的一环。F429 的 I2C 引脚是开漏输出必须外部上拉。4.7kΩ 是常用起点I2C 速率 400kHz、总线走线超过 10cm 时建议换成 2.2kΩ 或 1.8kΩ否则上升沿变缓严重时会产生通信错误。两块传感器并联后总线电容叠加这一点在示波器上看得最清楚SCL 上升沿如果超过 1μs 还到不了 3.3V就是上拉电阻太大。2.3 400kHz 快速模式直接扫描总线确认两个从机都回复MPU6050 数据手册标称 I2C 时钟最高支持 400kHz所以初始化时把I2C_InitStruct.ClockSpeed设为 400000。F429 的 I2C 外设内部会计算 CCR 分频系数这里有个前提I2C1 挂在 APB1 上APB1 时钟频率在 CubeMX 里必须正确配置成 42MHz。如果 APB1 配错HAL 计算出的分频值也会错总线时序全乱。上电后第一步不是直接读寄存器而是扫描总线确认两个地址都能得到 ACK。代码里用HAL_I2C_IsDeviceReady逐个试探地址uint8_t addr 0x00; for (addr 0x00; addr 0x80; addr) { if (HAL_I2C_IsDeviceReady(hi2c1, (addr 1), 2, 10) HAL_OK) { printf(I2C device found at 0x%02X\r\n, addr); } }这段代码的逻辑很简单HAL_I2C_IsDeviceReady会向目标地址发送一个起始条件和地址帧收到 ACK 就说明该地址上有设备在线。参数第一个是 I2C 句柄第二个是 8 位地址第三个是重试次数第四个是超时时间毫秒。正常情况会打印 0x68 和 0x69 两个地址。如果只出现一个先检查第二块传感器的 AD0 焊接尤其是 AD0 虚焊时引脚内部默认拉低两块都会变成 0x68。注意WHO_AM_I 寄存器0x75的返回值永远是 0x68和 AD0 无关不能用它来区分两块传感器只能验证通信链路是否正常。3. HAL 库的寄存器配置初始化序列与数据批量读取3.1 用结构体把两块传感器建模为两个实例写双设备驱动时最忌讳的是复制两份初始化函数。MPU6050 的寄存器操作完全一致唯一区别是 I2C 地址所以把设备相关内容收进一个结构体两份实例共用一套读写函数。结构体里除了地址还要保存原始数据和换算后的物理量。typedef struct { I2C_HandleTypeDef *hi2c; uint8_t addr; // 7位地址0x68 或 0x69 int16_t accel_raw[3]; // 原始值 int16_t gyro_raw[3]; float accel_g[3]; // 换算后的重力加速度单位 g float gyro_dps[3]; // 换算后的角速度单位 °/s } MPU6050_Dev;addr字段存的就是 0x68 或 0x69调用 HAL 函数时左移一位。HAL 库的 Mem 系列接口本质是先发地址帧带读写位再发寄存器地址最后连续收发数据。对两块传感器来说这个流程一模一样只是地址帧不同。封装一个底层的 8 位寄存器读写函数static HAL_StatusTypeDef mpu6050_read_regs(MPU6050_Dev *dev, uint8_t reg, uint8_t *buf, uint8_t len) { return HAL_I2C_Mem_Read(dev-hi2c, (dev-addr 1), // 8位地址 reg, // 寄存器地址 I2C_MEMADD_SIZE_8BIT, // 寄存器地址为8位 buf, len, 100); }I2C_MEMADD_SIZE_8BIT告诉库寄存器地址宽度是 8 位。MPU6050 的所有寄存器地址都在 0x00~0x7F 范围内用 8 位地址模式就够了。不要图省事把超时时间设成 1000I2C 通信失败时快速返回方便上层做重试。3.2 上电初始化序列顺序错了读出来全是零初始化分两个阶段。第一个阶段是设备复位和唤醒第二个阶段是量程、滤波器和采样率配置。顺序不能乱尤其要保证复位之后有足够延时。void mpu6050_init(MPU6050_Dev *dev, I2C_HandleTypeDef *hi2c, uint8_t addr) { uint8_t val 0; dev-hi2c hi2c; dev-addr addr; HAL_Delay(50); // 等待传感器内部上电稳定 val 0x80; // PWR_MGMT_1: 设备复位 mpu6050_write_regs(dev, 0x6B, val, 1); HAL_Delay(100); // 复位后必须等 100ms 以上 val 0x01; // 退出睡眠时钟源选择 PLL X 轴陀螺 mpu6050_write_regs(dev, 0x6B, val, 1); val 0x00; // PWR_MGMT_2: 所有轴都工作 mpu6050_write_regs(dev, 0x6C, val, 1); val 0x09; // SMPLRT_DIV: 1kHz / (19) 100Hz 采样 mpu6050_write_regs(dev, 0x19, val, 1); val 0x03; // CONFIG: DLPF 截止频率 44Hz mpu6050_write_regs(dev, 0x1A, val, 1); val 0x18; // GYRO_CONFIG: ±2000°/sFS_SEL3 mpu6050_write_regs(dev, 0x1B, val, 1); val 0x00; // ACCEL_CONFIG: ±2g mpu6050_write_regs(dev, 0x1C, val, 1); HAL_Delay(50); // 等配置稳定 }逐条说明PWR_MGMT_10x6B的 bit7 写 1 触发全寄存器复位复位完成后芯片仍处于睡眠模式必须再写一次 0x01 把 SLEEP 位清零并切换时钟源。不切换时钟源的话默认使用内部 RC 振荡器陀螺仪零漂会明显偏大。CONFIG寄存器0x1A的 bit[2:0] 是数字低通滤波器DLPF配置0x03 对应加速度和陀螺仪带宽约 44Hz这个值适合大多数机器人应用。最后两个量程寄存器要特别注意GYRO_CONFIG0x1B的 FS_SEL 位在 bit[4:3]0x18是二进制00011000正好对应 ±2000°/s如果写 0x08FS_SEL 是01量程只有 ±500°/s数据换算时容易出错。3.3 一次读 14 字节加速度和陀螺仪连读MPU6050 的加速度、温度、陀螺仪数据寄存器在地址上是连续的0x3B 到 0x48。用HAL_I2C_Mem_Read一次读出全部 14 字节比分三次读 626 字节更省时间而且能避免多次 I2C 起始条件引入的时间偏差。uint8_t buf[14]; int16_t *raw (int16_t *)buf; // 大端序直接按有符号整型解析 if (mpu6050_read_regs(dev, 0x3B, buf, 14) HAL_OK) { dev-accel_raw[0] (int16_t)((buf[0] 8) | buf[1]); dev-accel_raw[1] (int16_t)((buf[2] 8) | buf[3]); dev-accel_raw[2] (int16_t)((buf[4] 8) | buf[5]); // buf[6]~buf[7] 是温度 dev-gyro_raw[0] (int16_t)((buf[8] 8) | buf[9]); dev-gyro_raw[1] (int16_t)((buf[10] 8) | buf[11]); dev-gyro_raw[2] (int16_t)((buf[12] 8) | buf[13]); }注意这里不能用(int16_t *)buf直接强转解析因为传感器数据是大端序高位在前而 Cortex-M4 是小端。虽然强转取出来的数也能对上位数但负数的符号位会错位所以必须手工拼接。3.4 原始值到物理量的换算表换算方法非常机械读到的原始值除以对应量程的灵敏度系数。我习惯在读取函数里直接换算这样姿态解算拿到的就是带单位的浮点数。量程配置灵敏度换算方式加速度 ±2g16384 LSB/g原始值 / 16384加速度 ±4g8192 LSB/g原始值 / 8192加速度 ±8g4096 LSB/g原始值 / 4096加速度 ±16g2048 LSB/g原始值 / 2048陀螺仪 ±250°/s131 LSB/(°/s)原始值 / 131陀螺仪 ±500°/s65.5 LSB/(°/s)原始值 / 65.5陀螺仪 ±1000°/s32.8 LSB/(°/s)原始值 / 32.8陀螺仪 ±2000°/s16.4 LSB/(°/s)原始值 / 16.4初始化为 ±2000°/s 时陀螺仪的敏感度是 16.4而 ±2g 加速度是 16384两者相差 1000 倍。有些代码里把换算写成raw 14这种位运算只适用于 16384 和 /131 不是 2 的整数次幂的情况常规除法虽然慢一点但不会踩坑。F429 主频 168MHz一次除法耗时可忽略。注意加速度和陀螺仪的换算系数不同不能图省事用同一个系数。4. 双设备姿态解算DMP 中断冲突与时间戳同步4.1 DMP 还是互补滤波按数据用途定方案陀螺仪和加速度计原始值出来后下一步是转成姿态角。两个常见路径一是用 MPU6050 内部 DMP数字运动处理器直接输出四元数二是自己在 MCU 上跑互补滤波如经典的 Mahony 算法。DMP 的优势是省 CPU姿态解算在传感器内部完成F429 只需要定期读取 FIFO 里的四元数数据包。缺点是 Motion Driver 库是 C 语言写成内部大量使用全局变量双设备场景要复制两份源文件并手动改命名空间工程维护成本高。姿态解算的热搜词里经常提到 DMP但很多人没意识到如果需要双设备同时输出姿态DMP 的硬件中断只有一条 INT 线读取 FIFO 要区分数据来源时序处理比单设备复杂得多。如果只是做双轮平衡车、机械臂关节角度反馈Mahony 互补滤波完全够用。代码量大约 150 行每块传感器一份上下文结构体天然支持多实例。互补滤波的核心思路是陀螺仪积分提供短期姿态加速度计提供长期校正两者按比例融合。采样率设成 100Hz 时kP0.5、kI0.05 是一组可靠起步参数。4.2 两块芯片的 FIFO 中断与读取策略两块 MPU6050 的 INT 引脚分别接到 F429 的两个 EXTI 引脚。数据就绪时MPU6050 会把 INT 引脚拉高或按配置拉低F429 在两个外部中断回调函数里分别读取对应传感器的 FIFO。常见做法是在中断服务函数里只做“标记数据待读”的动作再回到主循环里做实际读 FIFO 和解析。volatile uint8_t mpu0_data_ready 0; volatile uint8_t mpu1_data_ready 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin MPU0_INT_Pin) mpu0_data_ready 1; if (GPIO_Pin MPU1_INT_Pin) mpu1_data_ready 1; }主循环里检查mpu0_data_ready和mpu1_data_ready对每个就绪标志依次读取INT_STATUS0x3A确认中断源再读FIFO_COUNT0x72 高位、0x73 低位判断 FIFO 里有多少字节。Motion Driver 默认配置下DMP 输出包长度通常是 42 字节只有 FIFO 计数大于等于一个包的长度时才读FIFO_R_W0x74。如果两个中断同时到达主循环会先处理优先级的那个另一个设备的 FIFO 继续累积并不会丢数据但 DMP 输出的时间是错的。4.3 采样时间的对齐直接用本地时钟记时间戳两块 MPU6050 各自有一颗内部时钟即使配置成同样的采样率真实采样时刻也是异步的。对多数控制应用正确做法不是强行让两块芯片同一时刻采样而是在读取数据时用HAL_GetTick()记录本地时间戳。mpu0.timestamp HAL_GetTick(); mpu1.timestamp HAL_GetTick();取每轮数据时两块传感器的时间戳差不超过一次 I2C 读取耗时。上面的代码里两块的时间戳分别记录在各自结构体的 timestamp 字段里因为第一块的读取过程会占一段时间等读取完第二块时HAL_GetTick 的值已经不同。更精确的做法是在两个 EXTI 中断回调里直接记录HAL_GetTick()——中断触发时刻就是传感器数据就绪时刻这个时间戳精度足够用于后续融合。需要硬件级同步时可以外接一条线把 F429 的一个 GPIO 输出脉冲连接到两块 MPU6050 的 xSetup 引脚MPU6050 的 X 方向自检/同步引脚实际是 9 号引脚但此方案要求两块传感器芯片都支持硬件同步且配置寄存器的步骤会多出不少。对于双 IMU 冗余、机械臂双关节反馈这类应用软件时间戳完全够不用上硬件同步。5. 验证双传感器数据一致性的三个方法5.1 静态数据合理性检查两块传感器放在同一个静置平面上先验证数据合理性再谈算法。加速度三轴平方和开根号应该约等于 1g即读数模长在 0.98~1.02 之间。陀螺仪静止时的零偏在 ±2°/s 以内算正常。通常给出的测试命令是直接把原始值打印出来观察滤波后的曲线是否平滑但透传原始字节看不出问题。推荐在固件里先做换算再通过串口输出accel_g和gyro_dps的浮点数。双设备的一致性指标要看两块板加速度模长的差值静置时差值小于 0.005g角速度的绝对差值小于 0.5°/s 属于正常。超过这个范围优先检查供电两块 MPU6050 的 VDD 走线如果经过细长过孔压降会造成两芯片内部参考电压不同反映出来的原始偏移也不同。5.2 用逻辑分析仪核对 0x68 和 0x69 的 ACK 时序I2C 调不通时90% 的情况是地址帧不对。用逻辑分析仪抓 SCL/SDA 波形找到 START 条件后第一个字节0x68 左移一位是 0xD0写方向0x69 左移一位是 0xD2。波形上应该能看到11010000或11010010后紧跟一个 ACK。如果只能看到第一个地址有 ACK第二个没有说明 AD0 没接对或者另一块传感器没上电。这一步比看代码日志更快能判断问题在硬件层面还是 HAL 配置层面。5.3 双设备数据融合的传送技巧串口一行两帧格式验证模块之间配合是否正常建议统一输出格式为#0,ax,ay,az,gx,gy,gz和#1,ax,ay,az,gx,gy,gz方便在 PC 端用 Python 快速画线对比。import serial ser serial.Serial(/dev/ttyACM0, 115200, timeout0.1) while True: line ser.readline().decode(errorsignore).strip() if line.startswith(#0): _, ax, ay, az, gx, gy, gz line.split(,) mag (float(ax)**2 float(ay)**2 float(az)**2) ** 0.5 print(fdev0 mag{mag:.3f} g, gyro_z{gz})这段脚本的作用是把#0开头的加速度分量取出来计算模长并打印。运行前确认串口号和波特率与固件一致观察静置时mag是否稳定在 1.0 附近用手转动板子时gyro_z是否随方向变化。本文还有配套的精品资源点击获取