
简介这份资源面向嵌入式初学者与STM32开发者提供在STM32F103C8T6最小系统板上驱动MPU6050六轴传感器的完整工程解决IIC通信配置、寄存器数据读取、加速度与角速度校准转换以及串口输出显示等实际问题。压缩包共224个文件约4.53MB以c与h源码、o与crf编译中间文件、d依赖文件、s启动文件为主另含uvprojx与uvoptx工程配置、hex烧录文件、map映射表及少量txt说明构成可直接编译调试的Keil工程目录。资源已有2119人学习下载说明其在同类教程中具备一定参考价值。读者可从中获得GPIO模拟IIC总线、MPU6050寄存器读写、原始数据单位换算与零偏校正、UART数据打包发送等关键环节的完整代码并借助Mpu_test测试程序快速验证功能适合作为运动控制与姿态检测入门的实践素材。1. 从一块最小系统板到六轴数据MPU6050 到底能拿来做什么手头有一块 stm32f103c8t6 最小系统板又翻出一片 MPU6050 模块很多人第一反应是“接上 I2C 读个原始值就完事”。真上手才发现读出来的加速度计和陀螺仪数据飘得厉害静止时角速度不为零稍微动一下姿态角就发散。问题不在芯片而在于从寄存器到可用姿态之间还隔着量程配置、零偏校准、数据融合三道坎。这篇笔记就围绕 stm32f103c8t6 最小系统板驱动 MPU6050 模块这条主线把硬件接线、I2C 时序、寄存器配置、原始数据换算、互补滤波到串口输出整条链路拆开讲。适合刚接触惯性测量单元、想用最便宜的方案把姿态跑起来的嵌入式开发者也适合已经能读出数据、但被零偏和噪声折磨过的熟手对照排查。2. 硬件连线与 I2C 通信先把“能读到 WHO_AM_I”这件事做扎实2.1 为什么优先用硬件 I2C 而不是软件模拟stm32f103c8t6 有两路硬件 I2CI2C1 默认落在 PB6SCL和 PB7SDAI2C2 在 PB10/PB11。MPU6050 是标准 I2C 从机7 位地址 0x68AD0 接地或 0x69AD0 接 VCC。常见做法是直接用硬件 I2C因为 MPU6050 的寄存器读写需要“写寄存器地址 重复起始 读数据”这种复合时序软件模拟在 400kHz 下容易因为延时不准导致 ACK 丢失。硬件 I2C 由外设自动处理起始、地址、ACK 和停止CPU 只负责填数据寄存器和等标志位稳定性高出一截。不过硬件 I2C 在 F1 系列上有历史遗留的“卡死”问题表现为 BUSY 标志一直置位。血泪经验是初始化时先手动把 SCL 拉低再拉高若干次给从机一个复位时序再配置外设。这个动作能解决大部分上电后第一次通信失败的情况。接线本身不复杂但有两个细节容易翻车。第一MPU6050 模块上的上拉电阻通常是 4.7kΩ 或 10kΩ如果模块自带STM32 端就不需要再开内部上拉如果模块没有必须在 SCL 和 SDA 上各接一个 4.7kΩ 到 3.3V。第二VCC 一定接 3.3VMPU6050 的 IO 不耐 5V接 5V 可能当时能用长期会损伤芯片。INT 引脚如果不用中断可以悬空但建议接一个到 STM32 的 EXTI 引脚后面做数据就绪中断会用到。2.2 用寄存器方式初始化 I2C1 的最小代码下面这段代码基于标准外设库风格直接操作寄存器不依赖 HAL方便看清楚每一步在干什么。// i2c1_init.c // stm32f103c8t6 I2C1 初始化PB6SCL, PB7SDA, 400kHz #include stm32f10x.h #define MPU6050_ADDR 0x68 void i2c1_init(void) { // 1. 开时钟GPIOB 和 I2C1 RCC-APB2ENR | RCC_APB2ENR_IOPBEN; RCC-APB1ENR | RCC_APB1ENR_I2C1EN; // 2. PB6/PB7 配置为复用开漏输出50MHz GPIOB-CRL ~(0xFF 24); // 清 PB6/PB7 配置位 GPIOB-CRL | (0xEE 24); // 复用开漏50MHz // 3. 软件复位 I2C1 I2C1-CR1 | I2C_CR1_SWRST; I2C1-CR1 ~I2C_CR1_SWRST; // 4. 配置时钟频率PCLK136MHz目标 400kHz // FREQ[5:0] 36CCR 36MHz / (2*400kHz) 45 I2C1-CR2 36; I2C1-CCR 45; I2C1-TRISE 36 1; // 最大上升时间 1000ns / (1/36MHz) // 5. 使能 I2C1 I2C1-CR1 | I2C_CR1_PE; } // 读一个寄存器 uint8_t mpu6050_read_reg(uint8_t reg) { uint8_t val; // 等待总线空闲 while (I2C1-SR2 I2C_SR2_BUSY); // 发送起始条件 I2C1-CR1 | I2C_CR1_START; while (!(I2C1-SR1 I2C_SR1_SB)); // 发送从机地址 写 I2C1-DR (MPU6050_ADDR 1); while (!(I2C1-SR1 I2C_SR1_ADDR)); (void)I2C1-SR2; // 读 SR2 清 ADDR 标志 // 发送寄存器地址 I2C1-DR reg; while (!(I2C1-SR1 I2C_SR1_TXE)); // 重复起始条件 I2C1-CR1 | I2C_CR1_START; while (!(I2C1-SR1 I2C_SR1_SB)); // 发送从机地址 读 I2C1-DR (MPU6050_ADDR 1) | 1; while (!(I2C1-SR1 I2C_SR1_ADDR)); (void)I2C1-SR2; // 单字节读先关 ACK再清 STOP I2C1-CR1 ~I2C_CR1_ACK; I2C1-CR1 | I2C_CR1_STOP; // 等 RXNE while (!(I2C1-SR1 I2C_SR1_RXNE)); val I2C1-DR; return val; }这段代码里几个参数值得说明。CR2的 FREQ 字段必须填 APB1 总线频率单位 MHzF103 在 72MHz 主频下 APB1 通常是 36MHz填错会导致所有时序偏差。CCR在标准模式下是PCLK1 / (2 * 目标频率)快速模式则是PCLK1 / (3 * 目标频率)这里用 400kHz 快速模式所以是 36MHz / (2 * 400kHz) 45。TRISE填的是最大上升时间对应的时钟数加一标准模式 1000ns快速模式 300ns这里按 1000ns 算得 361。读寄存器函数里最容易出错的是 ACK 和 STOP 的顺序。单字节读必须在读 DR 之前先清 ACK 再置 STOP否则从机会继续发下一个字节STM32 收到后可能触发 BERR 或 ARLO。如果读出来一直是 0xFF先检查 AD0 引脚电平再确认模块供电和上拉。2.3 用 WHO_AM_I 验证链路是否通MPU6050 的 WHO_AM_I 寄存器地址是 0x75默认返回值 0x68。上电后第一件事就是读它读不到就不要再往下调。// main.c 片段 uint8_t id mpu6050_read_reg(0x75); if (id ! 0x68) { // 链路不通点灯或串口打印错误码 while (1); }如果返回值是 0x00 或 0xFF说明 I2C 没有收到 ACK。排查顺序是先量 SCL/SDA 静态电平是否被拉高到 3.3V再示波器看起始条件有没有发出来最后检查从机地址是否因为 AD0 接高而变成 0x69。这一步过了后面的寄存器配置才有意义。3. 寄存器配置与原始数据换算把 LSB 变成有物理意义的数字3.1 电源管理、量程和采样率的配置顺序MPU6050 上电后默认处于睡眠模式必须先把 PWR_MGMT_10x6B的 SLEEP 位清零。常见做法是写 0x00 唤醒然后选时钟源。时钟源字段 CLKSEL[2:0] 建议选 1PLL with X axis gyroscope reference比内部 8MHz 振荡器稳。如果选 0陀螺仪零偏会随温度漂移得更厉害。量程配置分两个寄存器。加速度计在 ACCEL_CONFIG0x1CAFS_SEL[1:0] 可选 ±2g、±4g、±8g、±16g对应灵敏度 16384、8192、4096、2048 LSB/g。陀螺仪在 GYRO_CONFIG0x1BFS_SEL[1:0] 可选 ±250、±500、±1000、±2000 °/s对应灵敏度 131、65.5、32.8、16.4 LSB/(°/s)。做姿态解算一般选 ±2g 和 ±250°/s因为分辨率最高静态噪声最小。但如果你的应用有快速旋转±250 会饱和那就得换 ±500 或 ±1000代价是分辨率下降。采样率由 SMPLRT_DIV0x19和 DLPF_CFG0x1A共同决定。采样率 陀螺仪输出率 / (1 SMPLRT_DIV)。陀螺仪输出率在 DLPF 关闭时是 8kHz开启后根据 DLPF_CFG 降到 1kHz。常见配置是 DLPF_CFG344Hz 带宽1kHz 输出率SMPLRT_DIV4得到 200Hz 采样率。这个组合在姿态解算里够用既不会因为带宽太低导致相位滞后也不会因为采样太高让 I2C 总线成为瓶颈。配置顺序建议是先唤醒并选时钟源再设量程再设 DLPF 和采样率最后使能数据就绪中断如果要用。顺序反了可能出现配置不生效的情况因为睡眠模式下部分寄存器不可写。3.2 一次读 14 个字节的突发读取实现MPU6050 的加速度、温度、陀螺仪数据连续排列在 0x3B 到 0x48共 14 个字节。用突发读取一次拿完比逐个寄存器读效率高得多也避免两次读取之间数据更新导致的时间不一致。// mpu6050_read_burst.c // 从 0x3B 开始连续读 14 字节 void mpu6050_read_raw(int16_t *accel, int16_t *gyro, int16_t *temp) { uint8_t buf[14]; uint8_t i; while (I2C1-SR2 I2C_SR2_BUSY); I2C1-CR1 | I2C_CR1_START; while (!(I2C1-SR1 I2C_SR1_SB)); I2C1-DR (MPU6050_ADDR 1); while (!(I2C1-SR1 I2C_SR1_ADDR)); (void)I2C1-SR2; I2C1-DR 0x3B; while (!(I2C1-SR1 I2C_SR1_TXE)); I2C1-CR1 | I2C_CR1_START; while (!(I2C1-SR1 I2C_SR1_SB)); I2C1-DR (MPU6050_ADDR 1) | 1; while (!(I2C1-SR1 I2C_SR1_ADDR)); (void)I2C1-SR2; // 使能 ACK准备接收 14 字节 I2C1-CR1 | I2C_CR1_ACK; for (i 0; i 14; i) { if (i 13) { // 最后一个字节前关 ACK 并置 STOP I2C1-CR1 ~I2C_CR1_ACK; I2C1-CR1 | I2C_CR1_STOP; } while (!(I2C1-SR1 I2C_SR1_RXNE)); buf[i] I2C1-DR; } // 拼接高字节在前 accel[0] (int16_t)((buf[0] 8) | buf[1]); // X accel[1] (int16_t)((buf[2] 8) | buf[3]); // Y accel[2] (int16_t)((buf[4] 8) | buf[5]); // Z *temp (int16_t)((buf[6] 8) | buf[7]); gyro[0] (int16_t)((buf[8] 8) | buf[9]); // X gyro[1] (int16_t)((buf[10] 8) | buf[11]); // Y gyro[2] (int16_t)((buf[12] 8) | buf[13]); // Z }这段代码的关键在最后一个字节的处理。I2C 协议规定主机在接收最后一个字节前必须发 NACK然后发 STOP从机看到 NACK 就知道数据结束。如果忘了关 ACK从机会继续输出STM32 的 RXNE 可能一直不置位程序卡死。另外buf数组必须按uint8_t处理拼接时先左移再或不能直接指针强转成int16_t*否则在大端小端上会出问题。3.3 原始值到物理量的换算与零偏校准拿到int16_t原始值后按量程换算成 g 和 °/s。以 ±2g 和 ±250°/s 为例// 换算与零偏校准 float accel_g[3], gyro_dps[3]; int16_t accel_raw[3], gyro_raw[3], temp_raw; int16_t gyro_bias[3] {0, 0, 0}; // 静止校准上电后保持模块静止采样 500 次求平均 void mpu6050_calibrate(void) { int32_t sum[3] {0, 0, 0}; int i, j; for (i 0; i 500; i) { mpu6050_read_raw(accel_raw, gyro_raw, temp_raw); for (j 0; j 3; j) sum[j] gyro_raw[j]; } for (j 0; j 3; j) gyro_bias[j] sum[j] / 500; } void mpu6050_convert(void) { accel_g[0] accel_raw[0] / 16384.0f; accel_g[1] accel_raw[1] / 16384.0f; accel_g[2] accel_raw[2] / 16384.0f; gyro_dps[0] (gyro_raw[0] - gyro_bias[0]) / 131.0f; gyro_dps[1] (gyro_raw[1] - gyro_bias[1]) / 131.0f; gyro_dps[2] (gyro_raw[2] - gyro_bias[2]) / 131.0f; }零偏校准是必须做的不做的话静止时角速度可能有 ±2°/s 的偏差积分几秒后姿态角就偏了。校准时要保证模块完全静止放在水平桌面上不要用手拿着因为手的抖动会被平均进去。500 次采样在 200Hz 下大约 2.5 秒时间太长可以降到 200 次但零偏估计的方差会大一些。温度值换算公式是temp_c temp_raw / 340.0f 36.53f这个值可以用来做温度补偿但一般应用里不强制。4. 姿态解算与串口输出让数据真正能用4.1 互补滤波的系数怎么定加速度计能给出绝对倾角但噪声大、动态响应差陀螺仪积分平滑但零偏和漂移会让角度慢慢跑偏。互补滤波就是把两者按频率互补高频信陀螺仪低频信加速度计。// 互补滤波输出 roll 和 pitch float roll 0.0f, pitch 0.0f; float alpha 0.98f; // 陀螺仪权重 uint32_t last_tick 0; void complementary_filter(float dt) { // 加速度计解算倾角 float accel_roll atan2f(accel_g[1], accel_g[2]) * 57.29578f; float accel_pitch atan2f(-accel_g[0], sqrtf(accel_g[1]*accel_g[1] accel_g[2]*accel_g[2])) * 57.29578f; // 陀螺仪积分 roll gyro_dps[0] * dt; pitch gyro_dps[1] * dt; // 互补融合 roll alpha * roll (1.0f - alpha) * accel_roll; pitch alpha * pitch (1.0f - alpha) * accel_pitch; }alpha的取值直接决定动态性能和漂移速度。取 0.98 时陀螺仪积分占主导角度平滑但长时间会漂取 0.90 时加速度计权重加大角度跟随快但抖动明显。常见做法是 0.95 到 0.98 之间根据你的采样率和运动剧烈程度调。dt必须用实际时间差不能用固定值否则采样率一变角度就错。可以用 SysTick 或者定时器捕获来算dt。注意atan2f的象限问题。加速度计解算 roll 时用atan2(ay, az)pitch 用atan2(-ax, sqrt(ay²az²))这样在 ±90° 范围内不会出现跳变。如果直接用asin或acos在接近 ±90° 时对噪声极其敏感。4.2 用串口把姿态角发到上位机看波形调姿态的时候光看数字很难判断滤波效果最好把 roll、pitch 和原始加速度一起发到上位机画波形。stm32f103c8t6 的 USART1 在 PA9TX和 PA10RX配置成 115200-8-N-1。// 串口初始化与发送 void usart1_init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; // PA9 复用推挽输出 GPIOA-CRH ~(0xFF 4); GPIOA-CRH | (0x0B 4); // PA10 浮空输入 GPIOA-CRH ~(0x0F 8); GPIOA-CRH | (0x04 8); USART1-BRR 36000000 / 115200; // PCLK236MHz? 注意 APB2 在 72MHz 下是 72MHz USART1-CR1 USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; } void usart1_send_float(float v) { char buf[16]; int n snprintf(buf, sizeof(buf), %.2f, v); for (int i 0; i n; i) { while (!(USART1-SR USART_SR_TXE)); USART1-DR buf[i]; } }这里有个容易翻车的点BRR的计算依赖 APB2 时钟。F103 在 72MHz 主频下APB2 不分频就是 72MHzAPB1 是 36MHz。上面代码里写 36MHz 是错的实际应该用 72MHz 算即BRR 72000000 / 115200 625。如果串口出来是乱码先查这个值。另外snprintf在 Keil 里默认可能没开浮点支持需要勾选 Use MicroLIB 或者自己写浮点转字符串。发送格式建议用roll,pitch,ax,ay,az\n这种 CSV上位机直接按逗号解析画波形方便。发送频率不要超过采样率200Hz 采样发 50Hz 就够了串口带宽有限发太快会丢数据。4.3 用数据就绪中断替代轮询轮询读数据的问题是采样间隔不均匀dt抖动大积分误差累积快。MPU6050 的 INT 引脚可以配置成数据就绪时输出一个脉冲接到 STM32 的 EXTI 引脚上在中断里读数据dt就由实际中断间隔决定均匀得多。配置步骤是写 INT_ENABLE0x38的 DATA_RDY_EN 位为 1写 INT_PIN_CFG0x37设置中断极性通常高电平有效、推挽输出然后把 INT 接到 PA0 或 PB0 这类 EXTI 引脚在 STM32 端配置下降沿或上升沿触发。中断服务函数里只做读数据和置标志位姿态解算放到主循环避免在中断里做浮点运算导致响应变慢。如果发现中断频率不对检查 SMPLRT_DIV 和 DLPF_CFG 的组合数据就绪频率等于采样率。另外 INT 引脚是 3.3V 电平直接接 STM32 没问题不要接 5V 上拉。5. 避坑与排查那些让数据飘、通信挂的常见问题5.1 读出来全是 0xFF 或 0x00现象是 WHO_AM_I 读不到 0x68或者读任何寄存器都返回固定值。原因通常是 I2C 从机没有应答。先确认 AD0 引脚电平接地是 0x68接 VCC 是 0x69。再量 SCL/SDA 的静态电平正常应该被上拉到 3.3V如果只有 0.7V 说明上拉电阻没接或模块供电不足。最后检查 I2C 初始化里 FREQ 字段是否填了正确的 APB1 频率填错会导致时序完全不对。5.2 静止时角速度不为零且随温度变化现象是模块静止不动陀螺仪读数在 ±1 到 ±5 LSB 之间跳换算成 °/s 有零点几到几度的偏差。原因是 MEMS 陀螺仪本身有零偏且随温度漂移。解决方法是上电后做一次零偏校准把 500 次采样的平均值存下来后续读数都减去这个偏置。如果应用环境温度变化大可以每隔一段时间重新校准或者用温度值做一阶补偿。注意校准期间必须保证模块绝对静止放在减震垫上更好。5.3 姿态角缓慢漂移几分钟后偏十几度现象是互补滤波输出的 roll/pitch 在静止时慢慢往一个方向跑。原因是陀螺仪零偏没有完全消除积分后变成角度误差。解决方法是提高零偏校准的采样次数或者在互补滤波里把 alpha 调低一点让加速度计更多地纠正漂移。但 alpha 太低会让角度对振动敏感所以要在漂移和抖动之间折中。另一个办法是只在加速度计模长接近 1g 时才用加速度计修正运动剧烈时纯陀螺仪积分避免线性加速度污染倾角。5.4 I2C 通信偶尔卡死在 BUSY 等待现象是程序跑一段时间后卡在while (I2C1-SR2 I2C_SR2_BUSY)出不来。原因是总线受到干扰或从机拉低 SDA 不放STM32 认为总线忙。解决方法是加超时机制超过一定时间就强制复位 I2C 外设先关 PE再手动翻转 SCL 九个脉冲把从机状态机复位然后重新初始化。这个后悔药在长时间运行的系统里必须加否则一次干扰就死机。5.5 串口输出乱码或丢数据现象是上位机收到的字符不对或者每隔几行少一段。乱码先查 BRR 计算用的时钟频率APB2 是 72MHz 不是 36MHz。丢数据通常是发送频率太高或者在主循环里用阻塞发送导致采样被拖慢。解决方法是降低发送频率或者用 DMA 发送把 CPU 解放出来。另外snprintf浮点格式化在中断里调用可能重入不要在中断里做字符串转换。6. 进阶技巧用定时器触发采样把 dt 钉死互补滤波和任何积分算法都对dt敏感。用 SysTick 或者通用定时器产生固定周期中断在中断里置一个标志主循环检测到标志就读 MPU6050 并解算这样dt就是定时器周期不需要每次去算时间差。以 TIM2 为例72MHz 时钟预分频 7199 得到 10kHz自动重装 49 得到 200Hz 更新频率。// TIM2 配置为 200Hz 更新中断 void tim2_init_200hz(void) { RCC-APB1ENR | RCC_APB1ENR_TIM2EN; TIM2-PSC 7199; // 72MHz / 7200 10kHz TIM2-ARR 49; // 10kHz / 50 200Hz TIM2-DIER | TIM_DIER_UIE; TIM2-CR1 | TIM_CR1_CEN; NVIC_EnableIRQ(TIM2_IRQn); } volatile uint8_t sample_flag 0; void TIM2_IRQHandler(void) { if (TIM2-SR TIM_SR_UIF) { TIM2-SR ~TIM_SR_UIF; sample_flag 1; } } // 主循环 while (1) { if (sample_flag) { sample_flag 0; mpu6050_read_raw(accel_raw, gyro_raw, temp_raw); mpu6050_convert(); complementary_filter(0.005f); // dt 1/200 0.005s // 每 4 次发一次串口即 50Hz static uint8_t div 0; if (div 4) { div 0; usart1_send_float(roll); usart1_send_float(pitch); } } }这样dt固定为 0.005 秒积分误差只来自陀螺仪零偏和量化噪声不再有定时抖动。如果发现姿态响应有延迟可以把 TIM2 改成 400Hz 或 500Hz但要注意 I2C 读取 14 字节在 400kHz 下大约需要 0.4ms200Hz 时每周期 5ms 绰绰有余500Hz 时每周期 2ms 也够再高就要算一下总线占用率。另一个技巧是把零偏校准做成上电自动执行用一个 LED 指示校准状态上电后 LED 快闪 2 秒期间保持静止校准完成后 LED 常亮开始正常输出。这个习惯能避免每次手动校准的麻烦也防止在模块运动时误校准。我自己调试时吃过亏有一次上电时手还在动零偏校准把手的晃动平均进去结果姿态角一直偏查了半天才想起来是校准没做好。后来就养成了上电先静置、看 LED 确认的习惯。如果要做更精确的姿态可以把互补滤波换成 Mahony 或 Madgwick 算法它们用四元数表示姿态能处理三维旋转和磁力计融合。但 MPU6050 没有磁力计yaw 角会漂所以纯六轴应用里互补滤波已经够用没必要上四元数增加计算量。stm32f103c8t6 没有硬件浮点四元数里的三角函数和开方会吃掉不少 CPU200Hz 下可能跑不满这也是选互补滤波的一个现实理由。希望帮到你。本文还有配套的精品资源点击获取