简介:STM32F103C8T6与MPU6050完整工程是一套可直接编译运行的姿态解算方案,围绕MPU6050惯性测量单元和STM32标准外设库展开,适合嵌入式初学者入门,也可用于飞行器、机器人、智能穿戴等项目的快速原型验证。压缩包共188个文件,大小仅6.16MB,包含32个C源代码、35个H头文件、Keil工程文件(.uvprojx),以及编译生成的.axf与.hex输出文件;.o、.crf等中间产物也一并保留,便于研究编译流程或直接下载运行。源码覆盖STM32F10x标准库的定时器、Flash、RCC、ADC、I2C、CAN、USART等外设模块,并基于I2C读取MPU6050的加速度与角速度数据,提供姿态解算的相关实现,用户无需额外配置即可在Keil中打开和编译。无论初学者想理解MPU6050驱动编写,还是开发者希望移植到四轴飞行器、平衡车或智能穿戴设备,都可在该工程基础上快速二次开发。目前已有2617人学习下载,整体代码完整、目录清晰,是一份实用的开发模板。
1. 拿下一套 STM32F103C8T6 + MPU6050 完整工程:从编译到串口出姿态角
想验证 stm32 MPU6050 姿态解算的完整链路,最省事的办法是找一套源码完整的工程直接开干。这套基于 STM32F103C8T6 的项目就是干这个的:板子上电后,MPU6050 通过 I2C 把六轴原始数据送进单片机,经过 DMP 硬件解算或 Maghony 互补滤波转成欧拉角,再通过串口以二进制帧发出来。你只需要打开工程、改好下载器、烧进去,串口助手就能看到稳定的 roll / pitch / yaw。
它适合三类人:想快速跑通 MPU6050 最小系统、不想从寄存器逐行写驱动的新手;手里在调平衡车、云台或姿态检测原型,需要一份能直接改的源码底子的开发者;做上位机或算法验证,需要一个稳定输出姿态数据的数据源的工程师。下面从目录结构、硬件接线、核心代码到踩坑记录,围绕这一条实际链路展开。
2. 工程结构与硬件接线:数据从 MPU6050 到串口的完整链路
2.1 拿到源码先看这三个目录
打开工程压缩包,先别急着点编译。这个 MDK-ARM 工程解压后主要的代码分三块:
- App 目录放 main.c 和 init.c,负责初始化顺序、主循环调度,以及姿态数据到串口帧的组装;
- Bsp 目录放 bsp_i2c.c、bsp_mpu6050.c、bsp_usart.c,对应 I2C 驱动、传感器驱动、串口驱动,硬件相关的东西全部收在这里;
- Dmp 目录放的是厂商提供的运动驱动库,也就是常说的 DMP 库,包含 inv_mpu.c 和 inv_mpu_dmp_motion_driver.c 两个核心文件以及一堆头文件。
顶层还有 project.uvprojx 工程文件,双击它就能用 MDK-ARM 打开。DMP 库这部分的代码函数间耦合比较强,变量命名也偏老派,建议原样保留,不要手痒去改名或精简,否则编译报错会消耗你大量时间。整个数据流动路线是:MPU6050 六轴原始数据 → I2C 读入 → 姿态解算得到四元数/欧拉角 → USART1 发出帧头+数据+校验的二进制帧 → 上位机解析显示。
2.2 接线和引脚配置
STM32F103C8T6 最小系统板与 MPU6050 模块的接线如下,这是工程默认配置:
| MPU6050 引脚 | 接到 STM32 引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 必须 3.3V,不要接 5V |
| GND | GND | 与板子共地 |
| SDA | PB7 | I2C1 复用功能 SDA |
| SCL | PB6 | I2C1 复用功能 SCL |
| INT | PB0(可选) | DMP 中断通知脚,工程里可不用 |
| AD0 | GND | 决定从机地址,接地为 0x68 |
注意 MPU6050 模块大多数自带 I2C 上拉电阻,直接用杜邦线连即可。如果以后自己画板子,SDA 和 SCL 上必须各加一个 4.7kΩ 上拉到 3.3V,否则总线时序会不稳定。AD0 接 GND 后从机地址是 0x68,接 VCC 则变成 0x69,这一点是最容易翻车的地方,后面避坑章节会专门讲。
2.3 硬件 I2C 与模拟 I2C 的选择
工程默认使用 STM32F103 的 I2C1 外设,速率配置为 400kHz。F1 系列的硬件 I2C 有一个历史难题:总线异常后状态寄存器可能不按预期置位,导致程序卡死在等待标志位的循环里。很多老工程师因此更愿意用 GPIO 模拟 I2C,靠 CPU 翻转引脚,虽然占用资源,但不会锁死。
这个工程在 bsp_i2c.h 里留了一个切换宏,默认走硬件 I2C。如果你在调试中遇到总线卡死,可以改成软件模拟。我一般建议先用硬件 I2C 跑通,跑不通再切模拟,毕竟硬件 I2C 的时序抖动更小,读取速率也更稳定。
3. 核心代码拆解:初始化、六轴读取与两套姿态解算
3.1 MPU6050 初始化:寄存器到底在配什么
MPU6050 的寄存器配置是整份源码的起点,很多工程跑不出数据都是这里配置和量程没对应上。看这段初始化代码:
#define MPU6050_ADDR 0x68 #define PWR_MGMT_1 0x6B #define SMPLRT_DIV 0x19 #define CONFIG 0x1A #define GYRO_CONFIG 0x1B #define ACCEL_CONFIG 0x1C void MPU6050_Init(void) { I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x80); // 复位整个芯片 delay_ms(100); // 等内部稳压起来 I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x00); // 唤醒,时钟源选内部时钟 I2C_WriteByte(MPU6050_ADDR, SMPLRT_DIV, 0x07); // 采样率 = 1kHz / (1 + 7) I2C_WriteByte(MPU6050_ADDR, CONFIG, 0x06); // DLPF 截止频率 5Hz I2C_WriteByte(MPU6050_ADDR, GYRO_CONFIG, 0x18); // 陀螺仪量程 ±2000dps I2C_WriteByte(MPU6050_ADDR, ACCEL_CONFIG, 0x00); // 加速度计量程 ±2g }第一行先写 0x80 给 PWR_MGMT_1,这是让整个传感器复位,复位后必须延时 100ms 等内部稳压器和晶振稳定。第二行写 0x00 才是真正的唤醒配置,注意不要跳过复位那一步,否则会偶发读回全 0xFF。
SMPLRT_DIV 配 0x07 的含义是:陀螺仪内部默认输出速率 1kHz,实际采样率 = 1kHz / (1 + 7) = 125Hz。这个 125Hz 是后面 DMP 读取频率和串口发送频率的依据。CONFIG 配 0x06 表示 DLPF 数字低通滤波器截止频率为 5Hz,会把噪声压得比较干净,代价是响应稍有滞后,动态测试时角度会慢半拍。GYRO_CONFIG 配 0x18 即 ±2000dps 量程,对应的灵敏度系数是 16.4 LSB/(dps/秒),这部分换算在后面读取代码里要用到。ACCEL_CONFIG 配 0x00 是 ±2g 量程,灵敏度 16384 LSB/g。这些量程必须和后续代码里的转换系数保持一致,改了一处就得同步改另一处。
3.2 连续读六轴寄存器:高位字节拼接的细节
读数据不需要逐字节读,MPU6050 的寄存器地址是自增的,从 ACCEL_XOUT_H 开始连续读 14 字节,一次 I2C 突发事务把加速度计和陀螺仪全拿回来:
void MPU6050_ReadRaw(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz) { uint8_t buf[14]; I2C_ReadBytes(MPU6050_ADDR, ACCEL_XOUT_H, buf, 14); *ax = (int16_t)((buf[0] << 8) | buf[1]); // 高位在前 *ay = (int16_t)((buf[2] << 8) | buf[3]); *az = (int16_t)((buf[4] << 8) | buf[5]); *gx = (int16_t)((buf[8] << 8) | buf[9]); *gy = (int16_t)((buf[10] << 8) | buf[11]); *gz = (int16_t)((buf[12] << 8) | buf[13]); }每个轴的原始值都是 16 位有符号数,高字节在低地址。这里最隐蔽的坑是(buf[0] << 8) | buf[1]如果不强制转成 int16_t,buf[0] 会被当作无符号 char 提升为 int,最高位为 1 时结果直接变成正数,数值大得离谱。转成 int16_t 后,负数补码才能正确解析。
量程与灵敏度对应关系如下,和前面的初始化参数是配套的:
| 加速度计量程 | 换算系数 | 陀螺仪量程 | 换算系数 |
|---|---|---|---|
| ±2g | 16384 LSB/g | ±250dps | 131.0 LSB/(dps) |
| ±4g | 8192 LSB/g | ±500dps | 65.5 LSB/(dps) |
| ±8g | 4096 LSB/g | ±1000dps | 32.8 LSB/(dps) |
| ±16g | 2048 LSB/g | ±2000dps | 16.4 LSB/(dps) |
拿到原始值后,加速度除以 16384.0f 得到单位为 g 的物理量,陀螺仪除以 16.4f 得到单位为 dps 的角速度。工程里把这两个系数定义成了宏,放在 bsp_mpu6050.h 头部,方便改量程时一次性替换。
3.3 姿态解算方案一:DMP 四元数转欧拉角
DMP 是 MPU6050 内部的数字运动处理器,它在传感器端完成滤波、融合和四元数解算,单片机这边只需要把四元数读出来做一次数学转换。这是很多完整工程采用的方式,因为它对主控性能要求低,算法稳定性也经过厂商验证。工程默认配置 DMP 输出频率为 50Hz,每 20ms 在定时器中断里读取一次:
short quat[4]; int MPU6050_DMP_GetEuler(float *roll, float *pitch, float *yaw) { if (dmp_read_fifo(NULL, NULL, quat, NULL) != 0) { return -1; // FIFO 溢出或还没准备好 } float q0 = quat[0] / 16384.0f; // DMP 输出定点四元数,除以 16384 float q1 = quat[1] / 16384.0f; float q2 = quat[2] / 16384.0f; float q3 = quat[3] / 16384.0f; *roll = atan2f(2.0f*(q0*q1 + q2*q3), 1.0f - 2.0f*(q1*q1 + q2*q2)) * 57.29578f; *pitch = asinf(2.0f*(q0*q2 - q1*q3)) * 57.29578f; *yaw = atan2f(2.0f*(q0*q3 + q1*q2), 1.0f - 2.0f*(q2*q2 + q3*q3)) * 57.29578f; return 0; }dmp_read_fifo 的前两个参数可以传陀螺仪和加速度计的原始值,不需要时传 NULL,只要四元数的指针。DMP 内部把四元数以 16384 定点的形式存在 FIFO 里,所以这里要除以 16384.0f 转回浮点,格式是 q0 为实部。
转换公式里 roll 用 atan2f,pitch 用 asinf,yaw 用 atan2f,这是标准的四元数转欧拉角顺序 RPY。这里必须提醒一句:当 pitch 接近 ±90° 时会遇到万向锁,yaw 和 roll 会出现跳变,这是姿态描述方法本身的特性,不是代码 bug。如果做的是云台或平衡车这种不会翻到垂直的应用场景,这个问题可以无视;如果要做全姿态飞行器,应该直接使用四元数或旋转矩阵做控制,不要依赖欧拉角。
3.4 姿态解算方案二:Mahony 互补滤波
DMP 虽然省事,但库文件体积大,而且初始化流程必须先加载固件。如果资源紧张,或者想完全掌控融合逻辑,工程里还提供了一套 Mahony 互补滤波实现,核心代码是一个经典函数:
#define Kp 2.0f // 比例增益,越大收敛越快 #define Ki 0.005f // 积分增益,用于消除稳态偏置 void Mahony_Update(float gx, float gy, float gz, float ax, float ay, float az, float halfT) { static float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f; static float exInt = 0.0f, eyInt = 0.0f, ezInt = 0.0f; float norm, vx, vy, vz, ex, ey, ez; // 加速度计归一化 norm = sqrtf(ax*ax + ay*ay + az*az); if (norm < 0.001f) return; norm = 1.0f / norm; ax *= norm; ay *= norm; az *= norm; // 用当前四元数推算重力方向 vx = 2.0f*(q1*q3 - q0*q2); vy = 2.0f*(q0*q1 + q2*q3); vz = q0*q0 - q1*q1 - q2*q2 + q3*q3; // 叉积求误差 ex = ay*vz - az*vy; ey = az*vx - ax*vz; ez = ax*vy - ay*vx; // PI 补偿,把误差积分后叠加到陀螺仪角速度上 exInt += ex * Ki; eyInt += ey * Ki; ezInt += ez * Ki; gx += Kp*ex + exInt; gy += Kp*ey + eyInt; gz += Kp*ez + ezInt; // 一阶龙格库塔更新四元数 q0 += (-q1*gx - q2*gy - q3*gz) * halfT; q1 += ( q0*gx + q2*gz - q3*gy) * halfT; q2 += ( q0*gy - q1*gz + q3*gx) * halfT; q3 += ( q0*gz + q1*gy - q2*gx) * halfT; // 归一化 norm = 1.0f / sqrtf(q0*q0 + q1*q1 + q2*q2 + q3*q3); q0 *= norm; q1 *= norm; q2 *= norm; q3 *= norm; }这个函数里最关键的是"叉积求误差"这一步:加速度计测到的是实际重力方向,四元数推算出来的是估计重力方向,两者叉积的大小正比于姿态误差,把误差经过 PI 补偿后修正陀螺仪的角速度,达到互补效果。低频时相信加速度计,高频时相信陀螺仪,互补就是这么实现的。
调用时有两个参数需要特别注意。halfT 是半个采样周期,如果姿态更新周期是 10ms,halfT 就传 0.005f;陀螺仪的 gx/gy/gz 必须换算成弧度每秒,也就是把 dps 值乘以 0.0174533f,不然四元数更新步长是错的。Kp 和 Ki 的调参思路是:Kp 太小姿态收敛慢,调太大会有高频抖动;Ki 用来消除陀螺仪零偏导致的稳态漂移,但过大会引起低频振荡。工程里默认 Kp=2.0、Ki=0.005 是大多数场景能直接用的起点值。
3.5 串口帧协议:浮点姿态怎么发出去
单片机端算出来的是 float 型欧拉角,直接以文本形式发送解析麻烦、带宽浪费,工程里采用二进制帧格式。帧结构是:帧头 0xAA 0x55 + 数据长度 + 6 字节数据 + 校验和:
void Send_Attitude(float roll, float pitch, float yaw) { uint8_t buf[12]; int16_t r = (int16_t)(roll * 100.0f); // 放大 100 倍,保留 0.01° 精度 int16_t p = (int16_t)(pitch * 100.0f); int16_t y = (int16_t)(yaw * 100.0f); uint8_t sum = 0; buf[0] = 0xAA; // 帧头 buf[1] = 0x55; buf[2] = 6; // 后面数据的字节数 memcpy(&buf[3], &r, 2); memcpy(&buf[5], &p, 2); memcpy(&buf[7], &y, 2); for (uint8_t i = 0; i < 9; i++) sum += buf[i]; buf[9] = sum; // 单字节累加校验 HAL_UART_Transmit(&huart1, buf, 10, 100); }角度乘 100 后转成 int16_t,是因为 -180° 到 180° 的范围乘以 100 后正好落在 int16_t 的表示域内,用两个字节就能传输,上位机解析时再除以 100 还原。这种方式比传 float 少一半字节,而且避免了浮点数在大小端不同平台上的差异。上位机拿到帧后先校验和,再用小端方式读三个 short,分别除以 100 就是三个欧拉角。我自己用 Python 脚本做上位机调试时,就是按这个协议解包的。
4. 从导入工程到串口看数据:编译下载实测与避坑记录
4.1 打开工程、选定芯片和下载器
双击工程目录下的 project.uvprojx,MDK-ARM 打开后第一件事是确认芯片型号。在 Options for Target 的 Device 选项卡里选择 STM32F103C8,Debug 选项卡里选择 ST-Link 下载器,然后进入 Flash Download 页面勾选 Reset and Run,并确认 Programming Algorithm 里选的是 STM32F10x Med-density Flash 64K。
这个 Med-density 的选项是 F103C8T6 能正常烧录的关键。C8T6 是 64KB Flash 的中容量芯片,如果误选了 High-density 的算法,烧录时会出现 FLASH Timeout 或者 Program 失败。下载速度如果 SWD 连接不稳定,把 Interface 里的频率从 4MHz 降到 1MHz,大多数 No Target Connected 问题都能解决。设置好之后直接点 Build,Build Output 窗口出现 0 Error(s) 说明工程本身没问题,可以烧录。
4.2 串口助手看数据
用 USB 转 TTL 接板子,注意下面三根线:USB 转 TTL 的 TX 接板子 PA10(USART1 的 RX),RX 接 PA9(USART1 的 TX),GND 必须和板子共地。打开串口助手,波特率 115200,数据位 8,停止位 1。
上电后板子处于静止状态,串口会以每 20ms 一帧的速度输出姿态数据。掰动模块,roll 和 pitch 会随之变化,yaw 也会累加变化。这个阶段先不要纠结绝对精度,主要确认三点:数据有实时变化、变化方向与掰动方向一致、静止一段时间后数值不回跳。如果这三点都满足,说明从传感器到串口的链路已经通了,接下来才进入调优阶段。
4.3 五条实际踩过的坑
以下五条是这套方案里出现频率最高的问题,按照实际排查顺序整理:
| 现象 | 原因 | 解决 |
|---|---|---|
| 加速度计和陀螺仪读数全是 0xFF | I2C 地址不对,AD0 未接 GND 导致从机地址为 0x69,而驱动里写死 0x68 | 确认 AD0 接 GND,或把代码里的 MPU6050_ADDR 改为 0x69,也可以用 I2C 扫描确认地址 |
| I2C 卡死,程序停在 while 等待事件标志 | F1 硬件 I2C 在总线异常后状态寄存器不置位,等待循环永远跳不出去 | 给等待循环加超时计数,超时后重新初始化 I2C;总线异常频繁就直接切模拟 I2C,GPIO 轮询不会锁死 |
| 静止时 yaw 持续漂移,十几秒偏 2~3° | 陀螺仪零偏未校准,DMP 的自动校准未触发 | 上电后让板子静止 1~2 秒,采集 100 次求均值作为零偏减去;对航向精度要求高的场景应增加磁力计融合 |
| 下载报 No Target Connected | SWD 引脚被程序复用,或 Flash 算法选错 | 按住板子复位键点下载;或 BOOT0 拉高进入 ISP 模式擦除后再恢复 SWD 烧录 |
| 串口输出乱码 | USB 转 TTL 与板子没共 GND,或波特率不一致 | 接上 GND 线,统一 115200;若最小系统板用的内部 RC 时钟,改外接 8MHz 晶振或调整波特率误差 |
这些坑最大的共性是:链路每一段都要有对应的排查手段。比如读全 0xFF 大概率是地址或接线问题,但有时候也是模块供电不足,此时 VCC 引脚用示波器看会有明显跌落。串口乱码除了共地问题,还有一个玄学来源是 USB 转 TTL 芯片本身质量参差,换一根线往往就好了。
4.4 实测时怎么判断数据是否合理
静止时 roll 和 pitch 应该在 ±1° 以内小幅跳动,yaw 可以缓慢漂移但不应跳变。快速翻转模块 90°,pitch 应接近 ±90°。把板子平放,加速度计 z 轴读数应接近 16384(即 1g),x、y 轴接近 0。陀螺仪静止时读数应接近 0,但允许存在几百 LSB 的零偏,这个零偏就是漂移的来源。
如果做云台或自平衡,还需要关注滤波后的响应延迟。DLPF 配 5Hz 时数据很平稳,但甩动模块时角度会"拖尾",如果影响控制效果,可以把 CONFIG 调到 0x03(约 44Hz 截止)。动态响应和噪声抑制天然冲突,想在两者之间找到平衡,就得回来调这个寄存器,这是姿态滤波绕不开的权衡。
5. 二次开发:把它改成自己的硬件和算法
5.1 改引脚、改串口、改采样率
大部分硬件改动都集中在 bsp_i2c.h 和 bsp_usart.h 这两个头文件里。I2C 引脚的切换,如果是硬件 I2C1,SDA/SCL 的复用功能引脚是固定的 PB7/PB6,想换引脚只能切换到 I2C2(PB10/PB11),前提是目标板子有对应引脚引出;如果使用软件模拟 I2C,直接把 SDA_PIN 和 SCL_PIN 的宏改成任意两个 GPIO 即可,这是最灵活的方案。串口同理,USART1 默认 PA9/PA10,需要换串口时注意重映射和中断向量。
采样率的修改有两处要同步:DMP 配置里调用 dmp_set_fifo_rate(50),定的是 DMP 往外吐数据的频率;定时器中断周期要与之匹配,比如 50Hz 对应 20ms 进一次中断。这两处不一致,会出现角度"卡顿"或者 FIFO 溢出报错,属于很容易漏改的联动参数。
5.2 把角度接进你的控制逻辑
做自平衡小车的话,pitch 角度和 Y 轴角速度是核心输入。角度环用 P 控制,角速度环用 PD 控制,这是最通用的串级 PID 结构。要注意的是,从串口读出来的角度会有约 5Hz DLPF 带来的滞后,如果你的控制周期是 10ms 级别,这个滞后会被控制器明显感知。更好的做法是不经过串口,直接在解算函数所在的源文件里定义全局变量,控制任务从内存直接读取角度,省掉串口传输这一环。
5.3 我养成的调试习惯
每次拿到新的传感器模块,我都会强制自己先做一遍裸数据读取测试:上电后只读原始六轴数据,通过串口打印出来,确认 I2C 读写稳定、量程换算正确、数据方向符合直觉,然后再叠加滤波、解算和上位机协议。这个习惯帮我避开过很多次"算法没问题,是硬件连接有问题"的假象。
如果你要自己维护这份工程,建议在 main.c 里加一个调试开关,一键切换"原始数据输出模式"和"姿态角输出模式"。这样以后改硬件、改量程、换滤波算法时,都能快速回到源头观察问题,不用反复交叉排查。希望这篇拆解能帮到你,少走我当年走过的那段弯路。
本文还有配套的精品资源,点击获取