简介面向STM32初学者与嵌入式开发者的MPU6050姿态解算完整工程基于STM32F103C8T6平台搭建可直接编译下载运行省去环境配置与工程移植的重复工作。压缩包共188个文件、约6.16MB包含C源码、头文件、Keil工程配置、编译中间文件、启动汇编、映射与HEX输出等类型既保留可复现的构建产物也便于对照工程结构与依赖关系。资源围绕六轴传感器数据读取、姿态解算与外设驱动展开适合飞行器、机器人、智能穿戴及健康监测等场景的原型验证。已有2619人学习下载可作为课程设计、毕业设计或项目起步的参考模板通过示例代码与文档读者能快速理解MPU6050与STM32的配合方式掌握工程编译、下载调试和姿态数据处理的思路减少从零搭建时的困惑与排错成本。1. 从「读不到数据」开始的 STM32F103C8T6 MPU6050 工程一块 STM32F103C8T6 最小系统板加一个 MPU6050 模块串口能把 roll、pitch、yaw 三个角稳定地打出来这是很多嵌入式课设和毕设的第一道门槛。真正卡住人的地方往往不是姿态解算的公式而是上电后 WHO_AM_I 读回 0xFF、六轴数据一直不动、角度抖得没法看这些底层问题。这套 STM32F103C8T6 MPU6050 完整工程把 I2C 驱动、初始化序列、零偏校准、互补滤波姿态解算和串口上报串成一条能直接编译下载的通路源码按层拆分改一个量程或换一个滤波系数不用翻遍整个工程。它适合要交课程设计、做毕设原型、或者第一次把六轴传感器接上 MCU 的人。下面按硬件通路、软件分层、驱动与解算、避坑、验收的顺序拆开讲每一步都给出可以直接照抄的配置和代码。2. 硬件通路设计引脚分配、I2C 时序与量程参数2.1 六根线的接法电源和上拉是最容易埋雷的两处MPU6050 模块引出脚通常是 VCC、GND、SCL、SDA、XDA、XCL、AD0、INT 八根实际用得到的只有六根。VCC 这一根要先看模块带 LDO 稳压芯片的模块标称 3.3V5V 输入可以直接挂到开发板的 5V 排针裸芯片或者只做了电平转换的模块供电必须限定在 3.3V超压会直接把芯片烧穿而且这种损坏往往表现为「能读 WHO_AM_I 但数据全乱」很难第一时间怀疑到电源上。SCL 接 PB6、SDA 接 PB7对应 STM32F103C8T6 的 I2C1 硬件外设这两个引脚必须配置成复用开漏模式不能配成推挽。上拉电阻是另一个高频问题MPU6050 模块板上一般已经焊了 4.7k 上拉如果开发板的 I2C 排针上还并了一组 4.7k等效阻值变成 2.35k在 400kHz 下上升沿会变得过陡示波器上看是过冲实际表现是偶发 NACK。我一般的做法是只在模块侧保留一组上拉开发板侧如果焊了可拆的排阻就拆掉。AD0 决定从机地址接地是 0x68接 VCC 是 0x69。工程里默认按 0xD0 写地址、0xD1 读地址处理也就是 7 位地址 0x68 左移一位。INT 引脚接 PB5 是可选项用数据就绪中断替代轮询能降低主循环抖动但如果只是交课设10ms 定时轮询已经完全够用不必为了中断而中断。模块引脚接到 STM32F103C8T6配置要点VCC3.3V 或 5V看模块是否带 LDO与开发板共地电源加 0.1uF 去耦GNDGND单点共地避免和电机共电源SCLPB6复用开漏4.7k 上拉SDAPB7复用开漏4.7k 上拉AD0GND决定地址 0x68INTPB5可选上拉输入下降沿触发杜邦线的长度值得单独说一句。I2C 在 100kHz 下对线长不敏感但很多人为了走线好看把 SCL/SDA 拉成 30cm 以上的排线再叠上电机或者舵机的干扰读出来的加速度就会有几十 LSB 的毛刺。这类问题在串口曲线上看是「角度偶尔跳一下」很容易被误判成滤波没调好实际上是硬件在报信。2.2 量程与灵敏度先统一单位再谈精度MPU6050 的加速度四档、陀螺四档量程选错了不会报错只会让所有数值整体缩小或溢出。工程里默认配置为加速度 ±2g、陀螺 ±250°/s对应的灵敏度是 16384 LSB/g 和 131 LSB/(°/s)。这两个数字必须写死在换算函数里一旦改了配置寄存器却忘了改换算系数角度会整体偏差几倍而且因为曲线形状还是对的排查时极易走弯路。加速度量程AFS_SEL灵敏度典型用途±2g016384 LSB/g静态倾角、慢速姿态±4g18192 LSB/g一般手持设备±8g24096 LSB/g有冲击、有振动±16g32048 LSB/g跌落检测、碰撞记录陀螺量程FS_SEL灵敏度典型用途±250°/s0131 LSB/(°/s)云台、慢速转动±500°/s165.5 LSB/(°/s)一般姿态跟踪±1000°/s232.8 LSB/(°/s)快速翻转±2000°/s316.4 LSB/(°/s)高动态、剧烈运动换算写成宏或者内联函数比在每个调用点手写除法靠谱得多。加速度转成 m/s² 时乘 9.80665转成 g 就直接除以灵敏度陀螺除以灵敏度得到 °/s解算里再乘 0.0174533 转弧度。单位不统一是新手最常见的隐性错误互补滤波里陀螺项用的是 °/s加速度反解出来的角度用的是弧度两边直接相加结果就是角度永远追不上。2.3 采样率与低通滤波让解算节拍和传感器节拍对齐SMPLRT_DIV 寄存器决定采样率公式是 1kHz/(1DIV)。DIV 写 9 得到 100Hz写 4 得到 200Hz写 0 是 1kHz。这个值要和主循环的读取周期严格对应主循环 10ms 读一次采样率就该配 100Hz配成 1kHz 只会让每次读到的都是最新值中间 9 个样本被丢掉白白增加总线占用。CONFIG 寄存器里的 DLPF_CFG 是数字低通滤波默认写 3对应加速度带宽 44Hz、陀螺带宽 42Hz、群延迟 4.9ms。带宽压得太低会让角度响应变迟钝转弯时明显拖尾带宽放得太高则把机械振动全放进来互补滤波的加速度分支会被噪声带偏。做手部姿态或者小车平衡DLPF_CFG3 基本是通用解如果结构上有明显电机振动可以降到 2带宽 94/98Hz反而更差正确做法是降到 4 或 5 把带宽压到 20Hz 附近再靠算法补响应。提示DLPF 使能状态下陀螺输出速率固定为 1kHzSMPLRT_DIV 才有意义如果把 DLPF_CFG 配成 0关闭采样率行为会变成 8kHz 分频这时候按 100Hz 去读会读到大量重复样本。3. 软件分层与工程骨架一次 14 字节读取如何贯穿全流程3.1 三层结构bsp、driver、app 各管一段工程按 bsp、driver、app 三层组织目的不是套架构模板而是让「换传感器」和「换算法」这两件事互不影响。bsp 层只负责 STM32F103C8T6 的片上外设I2C1 的初始化和带超时的字节读写、USART1 的重定向、SysTick 的毫秒计数、TIM2 的 10ms 节拍。driver 层只认 MPU6050 的寄存器手册对外暴露初始化、读原始数据、读温度、校准零偏四个函数。app 层拿到的已经是换算好的物理量和姿态角不关心数据是从 I2C 还是 SPI 来的。Project/ ├── CMSIS/ # 内核头文件与启动文件 ├── StdPeriph_Driver/ # 标准外设库 ├── bsp/ │ ├── bsp_i2c.c/.h # I2C1 初始化、带超时的读写、总线恢复 │ ├── bsp_usart.c/.h # USART1 115200printf 重定向 │ └── bsp_timer.c/.h # TIM2 10ms 节拍、SysTick 延时 ├── driver/ │ ├── mpu6050.c/.h # 寄存器定义、初始化、原始数据读取 │ └── mpu6050_cfg.h # 量程、采样率、DLPF 的宏配置 ├── app/ │ ├── attitude.c/.h # 互补滤波、四元数、欧拉角输出 │ └── report.c/.h # 串口帧组装与上报 └── main.c这样分层的好处很实际想把 MPU6050 换成 LSM6DS3只需要重写 driver 层attitude.c 里的滤波和解算一行不动想把互补滤波换成 Mahony也只需要动 attitude.c驱动层完全不用碰。课设答辩时老师问「你这套代码换个传感器要改多少」能当场指出来改动面只有两个文件比空谈模块化有说服力。3.2 主循环节拍中断只置标志主循环只做消费/* main.c —— 100Hz 采样、100Hz 解算、20Hz 上报 */ #include stm32f10x.h #include bsp_i2c.h #include bsp_usart.h #include bsp_timer.h #include mpu6050.h #include attitude.h #include report.h static MPU_Raw_t raw; static MPU_Phys_t phys; static Attitude_t att; static int16_t gyro_bias[3]; static uint8_t report_cnt 0; int main(void) { SystemInit(); USART1_Init(115200); I2C1_Init(); /* PB6/PB7400kHz */ TIM2_Init_10ms(); /* 10ms 中断置 g_tick_10ms */ if (MPU_Init() ! 0) { /* 内部含 WHO_AM_I 自检 */ printf(mpu6050 init failed\r\n); while (1); } MPU_CalibrateGyro(gyro_bias, 500); /* 上电静止校准500 个样本 */ Attitude_Init(att, 0.98f); while (1) { if (g_tick_10ms) { /* 中断里只置位这里清零 */ g_tick_10ms 0; if (MPU_ReadRaw(raw) 0) { MPU_ToPhysical(raw, phys); /* LSB - g / dps */ Attitude_Update(att, phys, gyro_bias, 0.01f); if (report_cnt 5) { /* 20Hz 上报 */ report_cnt 0; Report_Send(att, phys); /* CSV 或自定义帧 */ } } } } }这段主循环的关键点在「中断只置标志」。很多初学者的写法是在 TIM2 中断里直接调 I2C 读数据再解算一旦 I2C 因为干扰卡住中断就再也退不出来整个系统表现为死机串口一个字都不吐排查时连现场都没有。把耗时操作全部挪到主循环、中断里只做g_tick_10ms 1最坏情况也只是丢一帧数据系统还能继续跑串口还能打印错误码。Attitude_Update的 dt 传的是 0.01f和 10ms 节拍对应。这里不要用「两次调用之间的 SysTick 差值」当 dt虽然更精确但在主循环有阻塞操作时 dt 会忽大忽小反而让滤波系数失配。固定节拍、固定 dt是这个工程刻意做的取舍。3.3 一次连续读 14 字节保证同一时刻的数据一致性/* driver/mpu6050.c —— 从 0x3B 起连续读 14 字节 */ uint8_t MPU_ReadRaw(MPU_Raw_t *out) { uint8_t buf[14]; /* 0x3B~0x48加速度 6 字节、温度 2 字节、陀螺 6 字节 */ if (I2C1_ReadBytes(MPU_ADDR, MPU_REG_ACCEL_XOUT_H, buf, 14) ! 0) return 1; /* 高字节在前拼成有符号 16 位 */ out-ax (int16_t)((buf[0] 8) | buf[1]); out-ay (int16_t)((buf[2] 8) | buf[3]); out-az (int16_t)((buf[4] 8) | buf[5]); out-temp (int16_t)((buf[6] 8) | buf[7]); out-gx (int16_t)((buf[8] 8) | buf[9]); out-gy (int16_t)((buf[10] 8) | buf[11]); out-gz (int16_t)((buf[12] 8) | buf[13]); return 0; }从 0x3B 一次读满 14 字节而不是分三次读加速度、温度、陀螺原因是 MPU6050 的寄存器地址会自动递增一次连续读拿到的是一组同一采样时刻的快照。分三次读的话三次事务之间可能隔了几百微秒快速转动时加速度和陀螺对应的根本不是同一个姿态互补滤波会把这种时间差当成真实运动表现为角度在高动态下明显滞后或者过冲。I2C1_ReadBytes内部要做三件事发送起始条件、发送设备地址加写位、发送寄存器地址、重复起始、发送地址加读位、连续接收并回 ACK、最后 NACK 加停止条件。每一个等待标志位的循环都必须带超时计数这一点在下一章的避坑里会展开。4. 驱动与解算落地从寄存器序列到欧拉角输出4.1 上电初始化序列与 WHO_AM_I 自检/* driver/mpu6050.c —— 初始化序列返回 0 表示成功 */ uint8_t MPU_Init(void) { uint8_t id 0; I2C1_ReadBytes(MPU_ADDR, MPU_REG_WHO_AM_I, id, 1); if (id ! 0x68) return 1; /* 地址错、接线错、没供电都会走到这 */ MPU_WriteByte(MPU_ADDR, MPU_REG_PWR_MGMT_1, 0x80); /* 软复位 */ Delay_ms(100); /* 复位后必须等 */ MPU_WriteByte(MPU_ADDR, MPU_REG_PWR_MGMT_1, 0x01); /* 唤醒 PLL 时钟源 */ Delay_ms(10); MPU_WriteByte(MPU_ADDR, MPU_REG_SMPLRT_DIV, 0x09); /* 1kHz/(19)100Hz */ MPU_WriteByte(MPU_ADDR, MPU_REG_CONFIG, 0x03); /* DLPF 44/42Hz */ MPU_WriteByte(MPU_ADDR, MPU_REG_GYRO_CFG, 0x00); /* ±250 dps */ MPU_WriteByte(MPU_ADDR, MPU_REG_ACCEL_CFG, 0x00); /* ±2 g */ MPU_WriteByte(MPU_ADDR, MPU_REG_INT_ENABLE, 0x01); /* 数据就绪中断 */ return 0; }这段序列有三处不能省。第一处是软复位后延时 100msMPU6050 复位期间寄存器不响应省掉延时会出现「初始化写进去了但读出来还是默认值」的诡异现象。第二处是 PWR_MGMT_1 写 0x01 而不是 0x000x00 只是唤醒但时钟源仍是内部 8MHz RC温漂和频偏都大0x01 选 X 轴陀螺 PLL 作时钟源陀螺输出更稳。第三处是 WHO_AM_I 自检放在最前面读不到 0x68 就直接返回错误让上层打印失败原因而不是带着一个没初始化的设备继续跑。温度换算容易被忽略T raw/340.0f 36.53单位摄氏度。它不参与姿态解算但可以用来做温漂补偿的参考量也可以作为「传感器还活着」的心跳指标——串口帧里带一个温度字段答辩演示时能直观说明数据是真的从芯片里读出来的。4.2 零偏校准让静止时的角度不再慢慢爬陀螺的零偏是姿态解算里最实在的敌人。MPU6050 在静止时 gx/gy/gz 的原始值通常不是 0而是几十到几百 LSB折算成角速度可能有 13°/s。这个偏差进到互补滤波的积分项里角度就会以每秒几度的速度单向爬一分钟能漂出去几十度看曲线像是「传感器坏了」其实只是没做校准。/* driver/mpu6050.c —— 静止状态下采样求均值作为零偏 */ void MPU_CalibrateGyro(int16_t *bias, uint16_t samples) { int32_t sum[3] {0, 0, 0}; MPU_Raw_t r; for (uint16_t i 0; i samples; i) { MPU_ReadRaw(r); /* 采样间隔约 10ms500 次约 5 秒 */ sum[0] r.gx; sum[1] r.gy; sum[2] r.gz; Delay_ms(10); } bias[0] (int16_t)(sum[0] / samples); bias[1] (int16_t)(sum[1] / samples); bias[2] (int16_t)(sum[2] / samples); }500 个样本、约 5 秒的窗口是一个折中值样本太少均值不稳样本太多用户等得不耐烦。校准期间设备必须真的静止放在桌面上有人拿在手里点下载校准出来的零偏会把手的微小动作一起算进去后面怎么调滤波都不对。这一点我在答辩现场见过翻车演示前一分钟还在手里晃板子然后直接复位上电角度基准从一开始就是歪的。工程预留的升级路径是把 bias 三个值写进 Flash 的最后一页下次上电直接读取跳过 5 秒等待。要注意 Flash 写入前必须擦除整页而 STM32F103C8T6 只有 64KB Flash最后一页是 0x0800FC00 起的 1KB别和自己的代码段撞上。4.3 互补滤波两行公式背后的参数整定/* app/attitude.c —— 一阶互补滤波 */ void Attitude_Update(Attitude_t *a, const MPU_Phys_t *p, const int16_t *bias, float dt) { /* 陀螺角速度减去零偏转成弧度/秒 */ float gx (p-gx_dps - bias[0] / 131.0f) * 0.0174533f; float gy (p-gy_dps - bias[1] / 131.0f) * 0.0174533f; /* 加速度反解倾角atan2 保证全象限正确 */ float roll_acc atan2f(p-ay, p-az); float pitch_acc atan2f(-p-ax, sqrtf(p-ay * p-ay p-az * p-az)); /* 陀螺积分给短期加速度给长期α 决定信任谁 */ a-roll a-alpha * (a-roll gx * dt) (1.0f - a-alpha) * roll_acc; a-pitch a-alpha * (a-pitch gy * dt) (1.0f - a-alpha) * pitch_acc; a-roll Angle_Normalize(a-roll); /* 归一到 ±π */ a-pitch Angle_Normalize(a-pitch); }α 取 0.98 对应的时间常数大约 0.5 秒含义是「陀螺积分占 98% 权重加速度每 10ms 只修正 2%」。α 越大角度越平滑但跟随越慢加速度里的振动被压得越狠α 越小静态精度越好但动态抖动明显。实际整定时我会先给 0.98用手快速翻转板子看跟随如果明显拖尾就降到 0.95如果静止时 угол 还在小幅抖就升到 0.99。这个参数没有 universal 值和结构刚性、DLPF 带宽、采样率都相关。roll 用 atan2(ay, az)、pitch 用 atan2(-ax, sqrt(ay²az²))这两个式子和很多人抄的 atan2(ax, az) 不一样差别在于坐标轴定义。工程里约定 X 轴朝前、Y 轴朝左、Z 轴朝上pitch 是绕 Y 轴俯仰此时 ax 为负对应抬头。如果发现倾斜方向和串口数值反了先检查这两个式子而不是去改符号位硬凑。yaw 是这套方案的天然短板没有磁力计偏航角只能靠 gz 积分零偏再准也会有残余漂移。工程里对 yaw 只做积分不做修正串口照常输出但验收标准要放宽——静态 10 分钟漂移在 5° 以内就算合格。这一点在答辩时要主动说明比被老师问出来要好。4.4 什么时候该上四元数互补滤波在 roll/pitch 接近 ±90° 时会出现万向节死锁这是欧拉角表示的固有缺陷不是代码写错了。如果项目需要全姿态、需要输出四元数给上位机做 3D 显示或者需要把加速度、磁力计一起融合就该升级到 Mahony 或者 Madgwick。/* app/attitude.c —— Mahony 姿态解算核心三步节选 */ /* 1. 用当前四元数推算重力方向与实测加速度做叉乘得到误差 */ ex (q1 * q3 - q0 * q2) - ax; ey (q0 * q1 q2 * q3) - ay; ez (q0 * q0 - q1 * q1 - q2 * q2 q3 * q3) - az; /* 2. 误差经 PI 调节器反馈到陀螺修正积分漂移 */ gx 2.0f * Kp * ex Ix; gy 2.0f * Kp * ey Iy; gz 2.0f * Kp * ez Iz; Ix 2.0f * Ki * ex * dt; Iy 2.0f * Ki * ey * dt; Iz 2.0f * Ki * ez * dt; /* 3. 一阶龙格库塔更新四元数再归一化 */ q0 (-q1 * gx - q2 * gy - q3 * gz) * 0.5f * dt; q1 ( q0 * gx q2 * gz - q3 * gy) * 0.5f * dt; q2 ( q0 * gy - q1 * gz q3 * gx) * 0.5f * dt; q3 ( q0 * gz q1 * gy - q2 * gx) * 0.5f * dt; Quat_Normalize(q);Kp 一般取 1.02.0Ki 取 0.0050.01Kp 管收敛速度、Ki 管稳态误差。四元数法的代价是浮点运算量上升STM32F103C8T6 没有硬件 FPU一次完整更新大概几百微秒100Hz 节拍下占用不到 5% 的 CPU完全跑得动。真正的门槛是调参和调试如果课设只要求 roll/pitch 两个角互补滤波的性价比明显更高代码短、参数少、出了问题能一眼看穿。5. 避坑与排查五类最常见的翻车现场5.1 WHO_AM_I 读回 0x00 或 0xFF现象是初始化函数第一行就返回失败串口打印「mpu6050 init failed」示波器抓 SCL 有波形但 SDA 一直高或者一直低。原因通常有三类模块 VCC 没接或者接错电压导致芯片没工作SDA/SCL 接反PB6 和 PB7 对调是最常见的接线错误上拉电阻缺失或者从机地址搞错AD0 接了 VCC 却仍然按 0x68 去访问。解决顺序是先用万用表量模块 VCC 对 GND 是不是 3.3V再确认 PB6 接 SCL、PB7 接 SDA然后拿示波器看第九个时钟周期 SDA 有没有被从机拉低ACK。如果硬件都对但就是不应答用软件 I2C 逐个引脚翻转测试能定位到底是 MCU 侧外设没配对还是模块坏了。我一般会在 bsp_i2c.c 里留一个I2C1_ScanBus()函数上电时扫 0x080x77把应答的地址打印出来这一招在换任何 I2C 器件时都好用。5.2 数据全是 0 或者一直不变现象是 WHO_AM_I 能读到 0x68初始化也返回成功但读出来的加速度恒定为 0或者六个轴的值一直是同一组数字。根本原因是 PWR_MGMT_1 的 SLEEP 位没清掉——MPU6050 上电默认处于睡眠模式寄存器可读但传感器不出新数据。另一种可能是只写了 PWR_MGMT_1 却忘了延时写操作还没生效就开始读。还有一种更隐蔽的情况连续读函数里寄存器地址发对了但读长度参数写成了 1结果只拿到 ax 的高字节后面全是从上一次缓冲区残留的值。排查方法是把 14 字节原始缓冲一次性打印成十六进制静止时 az 应该在 16384 附近±2g 量程、Z 轴朝上如果 az 恒为 0 或者恒为某个奇怪的值问题就在读取长度或者字节拼接顺序上。5.3 角度抖动大、静止时数值乱跳现象是串口曲线上角度在 ±2° 范围内高频抖动用手轻轻碰一下桌子数值就跳好几度。原因可能是 DLPF 带宽开得太高把机械振动放了进来也可能是杜邦线太长引入了耦合噪声还可能是把模块放在了有电机、有开关电源的位置。处理顺序是先看原始数据静止时连续打印 ax/ay/az 的原始值如果波动超过 ±100 LSB说明问题在硬件和配置不在滤波算法。此时把 DLPF_CFG 从 3 降到 4 或 5缩短杜邦线给模块电源加 0.1uF 加 10uF 去耦基本能压下去。如果原始数据很稳但角度还是抖才轮到调 α 或者检查 dt 是否真的等于 0.01。把「传感器噪声」和「算法噪声」分开定位能省掉大量盲目调参的时间。5.4 I2C 总线卡死程序停在 while 里出不来现象是程序跑几分钟到几十分钟后死机串口不再输出复位后又正常。STM32F103 的 I2C 外设有已知的总线锁死问题从机在传输中途被干扰把 SDA 一直拉低主机等待 BUSY 标志的循环就永远出不来。代码里所有while (I2C_CheckEvent(...) ! SUCCESS)如果不带超时就是一颗定时炸弹。解决分两层。第一层是给每个等待循环加超时计数超时后返回错误码而不是死等第二层是加总线恢复函数把 PB6/PB7 临时切成开漏输出手动打 9 个 SCL 脉冲把从机移位寄存器里的残留数据挤出去再补一个 STOP 条件最后重新初始化 I2C 外设。/* bsp/bsp_i2c.c —— 总线恢复9 个 SCL 脉冲 手动 STOP */ void I2C1_BusRecover(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_Out_OD; /* 开漏模拟 I2C 电平 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); for (uint8_t i 0; i 9; i) { /* 9 个时钟把从机读空 */ GPIO_ResetBits(GPIOB, GPIO_Pin_6); Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); Delay_us(5); } GPIO_ResetBits(GPIOB, GPIO_Pin_7); /* SCL 高时 SDA 拉低 */ Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_6); Delay_us(5); GPIO_SetBits(GPIOB, GPIO_Pin_7); /* SDA 拉高形成 STOP */ Delay_us(5); I2C1_Init(); /* 重新配置外设 */ }这段函数的逻辑是模拟一次完整的从机复位时序主机连续发 9 个时钟从机如果卡在某个字节的中间会在这 9 个时钟里把剩余位吐完并释放 SDA随后在 SCL 为高时让 SDA 由低变高产生一个 STOP 条件把从机状态机拉回空闲。延时 5us 对应约 100kHz恢复完成后必须重新调用 I2C1_Init否则外设寄存器还停留在锁死前的状态。5.5 串口输出乱码或者 printf 打不出浮点现象是串口助手收到一堆乱码或者整数能打、一打%f就输出空白或?。前者多半是波特率不对或者系统时钟没配对——STM32F103C8T6 外部晶振 8MHz 经 PLL 倍频到 72MHz如果 SystemInit 里晶振起振失败退回内部 8MHz RCUSART 的分频值全错波特率就偏了。后者是 Keil 默认的 microlib 或者标准库没开浮点打印支持。处理办法是先用示波器或者逻辑分析仪量 PA9 的波形算出实际波特率对不上就检查SystemInit和stm32f10x.h里的HSE_VALUE宏是不是 8000000。浮点打印在 Keil 里勾选 Use MicroLIB或者在工程选项里把 C Library 的 printf 支持浮点打开更稳的做法是自己在 report.c 里把浮点拆成整数部分和小数部分分别打印不依赖库的浮点格式化代码量十几行行为完全可控。6. 验收与答辩用一张表证明这套工程真的能跑课设和毕设的验收环节老师通常不看代码写得多漂亮看的是「你怎么证明它是对的」。把这套工程拆成六个可测量的验收项每项给出方法和判据现场按表走一遍比口头描述「效果挺好」有说服力得多。验收项测试方法合格判据器件自检上电读 WHO_AM_I 并串口打印返回 0x68静态零偏水平静置 60 秒记录 roll/pitch峰峰值波动小于 0.5°静态角精度用角度尺分别倾斜 15°、30°、45°每个点误差小于 1°动态跟随手动翻转 90° 再回位看曲线200ms 内收敛到 ±1°长时间稳定性静置 10 分钟记录 yaw 漂移漂移小于 5°无磁力计通信完整性串口连续抓 1000 帧丢帧率低于 0.1%静态角精度这一项要特别注意操作方法把模块贴在一块平整的小板上用角度尺垫起一端等读数稳定 3 秒再记录。直接用手掰着看串口数字误差来源是手而不是传感器测出来的数据没有意义。动态跟随的判据「200ms 内收敛」来自 α0.98 时约 0.5 秒时间常数的实测经验如果整定到 0.95收敛会更快但静态抖动会上升两个指标要一起看。答辩现场有几个高频问题值得提前准备。第一个是「为什么用互补滤波不用卡尔曼滤波」可以从算力、参数可解释性、调试成本三个角度回答STM32F103C8T6 没有 FPU卡尔曼的矩阵运算在这个平台上属于杀鸡用牛刀而且卡尔曼的 Q/R 矩阵调参对课设规模来说不划算互补滤波两个参数就能覆盖需求。第二个是「yaw 为什么会漂」坦诚说明没有磁力计就没有绝对航向基准只能靠陀螺积分漂移是原理性缺陷解决的路径是加 HMC5883 或者换九轴。第三个是「如果 I2C 断了会怎样」正好可以现场演示总线恢复函数把 SDA 短接到地再松开展示系统能自愈。现场演示最怕的是临时出问题没有后备。我的习惯是准备两套一套是实板实时跑另一套是提前录好的串口数据回放脚本万一现场杜邦线松了、电脑串口驱动抽风可以切到回放模式先把数据曲线讲完再回头排查硬件。这个习惯来自一次血泪经验答辩前五分钟随手挪了一下板子杜邦线接触不良现场读了十分钟的 0xFF最后靠回放数据才把场子圆回来。从那以后我每次带硬件去答辩固定流程是先量电源、再读 WHO_AM_I、再跑静态零偏三步都过了才打开上位机画曲线宁可多花两分钟也不赌运气。这套 STM32F103C8T6 MPU6050 工程的目录结构、驱动、解算和串口上报都在资源里接线对了就能直接编译下载把上面那张验收表走一遍基本就是一份能交差的完整方案。希望帮到你。本文还有配套的精品资源点击获取