简介:这是一份基于STM32F103C8T6的MPU6050六轴数据读取工程源码,面向需要快速上手陀螺仪与加速度计开发的嵌入式初学者,解决原始数据采集并串口输出的问题。项目采用IIC通信,硬件接线清晰(SDA接PB6,SCL接PB7),代码兼容F103全系列芯片,便于移植到RCT6、VET6等不同核心板。压缩包共72个文件,大小1.26MB,包含C源码、头文件、Keil工程文件及编译生成的hex烧录文件,可直接打开编译或烧录运行。已有483人学习下载。借助该工程可掌握MPU6050驱动与串口调试方法,并可在六轴数据基础上进一步拓展运动计步、跌倒判断、睡眠判断等实用功能。
1. 这包源码解决的不是“读六轴”,而是怎么把数据流变成可信的信号
很多人拿到 STM32F103C8T6 最小系统板和 MPU6050,第一件事就是网上搜例程、下源码工程,焊完线烧进去却发现串口助手上一片空白,或者打印出一堆不断漂移的数字。问题往往不在接线,而在对 I2C 时序的理解、对 MPU6050 寄存器初始化顺序的把握,以及串口到底该用阻塞发送还是中断+DMA。这个标题里的“源码工程”价值也正在于此——它把「最小系统板 + 六轴传感器 + 串口」这条链路完整打通,而不只是贴一段读寄存器的代码。这篇博文会从硬件连接讲起,把 MPU6050 的初始化、六轴数据读取与换算、串口打印到上位机这一整套流程拆开,结合源码工程里的关键代码段,把参数和坑位都交代清楚。适合正在做平衡车、四轴或姿态显示项目的人,也适合想理解 I2C 从机操作与串口调试配合的嵌入式初学者。
2. 硬件连接与 I2C 时序:先让 STM32F103C8T6 的 I2C 外设“活”过来
2.1 最小系统板上的引脚分配与 MPU6050 接线
STM32F103C8T6 是 48 脚的 LQFP 封装,核心板通常已经引出排针,PA 和 PB 端口基本可自由使用。MPU6050 默认工作在 I2C 模式,常用接法是:
- VCC 接 3.3V,GND 接 GND
- SCL 接 PB6(I2C1 的 SCL)
- SDA 接 PB7(I2C1 的 SDA)
- AD0 接地,此时 I2C 从机地址为 0x68(七位地址)
- XDA/XCL 不用管,那是接外部磁力计用的
如果你用的是 GY-521 这类现成模块,板上已经有 10k 上拉电阻到 VCC,可以不额外加。若使用裸片 MPU6050,必须在 SCL 和 SDA 上各接 4.7k~10k 上拉电阻到 3.3V,否则 I2C 通信时好时坏。
MPU6050 STM32F103C8T6 VCC -> 3.3V GND -> GND SCL -> PB6 (I2C1_SCL) SDA -> PB7 (I2C1_SDA) AD0 -> GND接线要特别注意共地问题。最小系统板如果通过 USB 供电,而 MPU6050 模块由外部电源供电,必须保证两个电源的 GND 连在一起,否则 I2C 电平没有参考点,读出来的数据会随机跳变甚至完全无应答。这个现象在串口打印时表现为地址 ACK 失败,后面会重点排查。
2.2 I2C 通信的时基:为什么选用硬件 I2C 而非软件模拟
STM32F103C8T6 的 I2C1 速度标准模式下为 100kHz,快速模式可达 400kHz。MPU6050 手册明确支持 400kHz。使用硬件 I2C 的好处是时序由外设自动生成,CPU 不需要打断地去翻转 GPIO,适合后续配合中断或 DMA 做更高频的传感器采样。
初始化 I2C 时,需要关注三个参数:
- 时钟速度:设为 400000,即 400kHz
- 从机地址:MPU6050 的七位地址是 0x68,HAL 库要求左移一位变成 8 位地址 0xD0
- 寻址模式:7 位地址
以 HAL 库为例,I2C1 初始化代码核心部分如下:
I2C_HandleTypeDef hi2c1; void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; // 400kHz 快速模式 hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 16/9 占空比,配合 400kHz hi2c1.Init.OwnAddress1 = 0; // 主机不需要自身地址 hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }这里有个容易踩的坑:很多人在国产替代芯片上照抄 ST 官方例程,把ClockSpeed设为 100000 也能正常工作,但一旦把传感器数据采集频率提高到 1kHz 以上,100kHz 的时钟总线可能成为瓶颈。MEMS 传感器的数据更新率通常在 1kHz 以内,I2C 速度 400kHz 时读取完整六轴数据(14 字节)大约需要 280μs,足够放在 1ms 的定时器中断里执行。
2.3 用逻辑分析仪验证 I2C 总线上的读写时序
如果串口打印不出数据,第一步应该确认 I2C 总线上有没有正确的 START、STOP 信号以及 ACK 应答。手头有逻辑分析仪的话,把通道 0 接 SCL、通道 1 接 SDA,解码方式选 I2C,采样率不低于 2MHz。如果看到 SDA 在传输过程中一直保持高电平且无 ACK,说明从机没有响应,优先检查地址和上拉电阻。
如果发现时序中数据位对不上,比如 SCL 高电平期间 SDA 发生了跳变,这本质上是电平冲突。常见原因是模块上的上拉电阻与最小系统板的内部上拉并联后等效电阻过小,导致信号沿变差。解决方法是降低 I2C 速率到 100kHz,或者拆除模块上的一个上拉电阻。这个细节在源码工程里通常不会写,但实际调板时遇到概率很高。
3. MPU6050 初始化与六轴数据读取:寄存器配置的顺序决定数据质量
3.1 上电后的寄存器初始化序列
MPU6050 上电后默认处于睡眠模式,加速度计和陀螺仪都不工作,只有温度传感器和寄存器接口可用。必须先设置电源管理寄存器 1(地址 0x6B),清除 SLEEP 位,然后依次配置采样率分频、数字低通滤波器和量程。
初始化序列有一个推荐的先后次序,颠倒会导致某些寄存器写入失败或数据异常:
uint8_t init_mpu6050(void) { uint8_t reg_val; // 1. 退出睡眠模式,使用内部 8MHz 振荡器 reg_val = 0x00; if (HAL_I2C_Mem_Write(&hi2c1, 0xD0, 0x6B, I2C_MEMADD_SIZE_8BIT, ®_val, 1, 100) != HAL_OK) return 1; // 2. 关闭内部 PLL,等系统稳定后再切换时钟源 HAL_Delay(10); // 3. 设置数字低通滤波器为 21Hz 带宽,适合姿态解算 reg_val = 0x03; if (HAL_I2C_Mem_Write(&hi2c1, 0xD0, 0x1A, I2C_MEMADD_SIZE_8BIT, ®_val, 1, 100) != HAL_OK) return 2; // 4. 设置陀螺仪量程为 ±2000dps,对应灵敏度 16.4 LSB/dps reg_val = 0x18; if (HAL_I2C_Mem_Write(&hi2c1, 0xD0, 0x1B, I2C_MEMADD_SIZE_8BIT, ®_val, 1, 100) != HAL_OK) return 3; // 5. 设置加速度计量程为 ±2g,对应灵敏度 16384 LSB/g reg_val = 0x00; if (HAL_I2C_Mem_Write(&hi2c1, 0xD0, 0x1C, I2C_MEMADD_SIZE_8BIT, ®_val, 1, 100) != HAL_OK) return 4; // 6. 采样率分频器,设置输出频率为 100Hz reg_val = 0x13; // 100Hz / (1 + 0x13) = 100Hz? 实际是 1kHz / (1 + 0x13) ≈ 100Hz if (HAL_I2C_Mem_Write(&hi2c1, 0xD0, 0x19, I2C_MEMADD_SIZE_8BIT, ®_val, 1, 100) != HAL_OK) return 5; return 0; }第五步的采样率需要结合内部采样时钟计算。MPU6050 的陀螺仪输出采样频率默认是 1kHz,加速度计也是 1kHz,而0x19寄存器的作用是分频:采样频率 = 1kHz / (1 + SMPLRT_DIV)。写入 0x13 即十进制 19,采样率约为 50Hz,适合串口打印观察;如果要提高数据更新率,写入 0x07 可以得到 125Hz。这里要留意,采样率设置得过高但数字低通滤波器带宽没有相应调高,会出现明显的混叠噪声,静止时数据也会抖动。
3.2 六轴数据的读取与 LSB 到物理量的换算
MPU6050 的数据寄存器从 0x3B 开始,依次存储加速度计 X、Y、Z(每轴 16 位)、温度(16 位)、陀螺仪 X、Y、Z(每轴 16 位),共 14 字节。建议一次连续读取全部 14 字节,避免多次启动 I2C 传输带来的时间开销和数据不同步。
uint8_t read_mpu6050_6axis(int16_t *accel_x, int16_t *accel_y, int16_t *accel_z, int16_t *gyro_x, int16_t *gyro_y, int16_t *gyro_z) { uint8_t buf[14]; if (HAL_I2C_Mem_Read(&hi2c1, 0xD0, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100) != HAL_OK) return 1; // 大端格式:高字节在前 *accel_x = (int16_t)((buf[0] << 8) | buf[1]); *accel_y = (int16_t)((buf[2] << 8) | buf[3]); *accel_z = (int16_t)((buf[4] << 8) | buf[5]); // buf[6..7] 是温度数据,本工程未使用 *gyro_x = (int16_t)((buf[8] << 8) | buf[9]); *gyro_y = (int16_t)((buf[10] << 8) | buf[11]); *gyro_z = (int16_t)((buf[12] << 8) | buf[13]); return 0; } // 换算示例:假设已读取陀螺仪原始值 gyro_x float gyro_x_dps = (float)gyro_x / 16.4f; // 量程 ±2000dps 时 float accel_x_g = (float)accel_x / 16384.0f; // 量程 ±2g 时寄存器顺序和字节序是两个容易出错的地方。MPU6050 的十六位数据是大端格式,也就是高字节存在低地址。如果按小端解析,静止时候的数据看起来没问题,但一旦板子旋转,数值方向可能完全相反。另外加速度计和陀螺仪的量程设置会直接影响灵敏度系数,表 1 列出了常用档位:
| 量程配置 | 加速度计量程 | 灵敏度 (LSB/g) | 陀螺仪量程 | 灵敏度 (LSB/dps) |
|---|---|---|---|---|
| 0x00 | ±2g | 16384 | ±250dps | 131 |
| 0x01 | ±4g | 8192 | ±500dps | 65.5 |
| 0x08 | ±8g | 4096 | ±1000dps | 32.8 |
| 0x18 | ±16g | 2048 | ±2000dps | 16.4 |
注意量程配置字节是独立的,加速度计寄存器 0x1C 的低四位是量程位,陀螺仪寄存器 0x1B 的低四位是量程位。上表把加速度计和陀螺仪放在一行只是为了对应灵敏度系数,实际配置时要分别写寄存器。比如陀螺仪想要 ±2000dps,写 0x18;加速度计想要 ±2g,写 0x00。
3.3 静止时的零漂处理与数据为什么不为零
MPU6050 陀螺仪在静止时输出值不会恰好是 0,这是 MEMS 器件的固有零偏。通常做法是上电后让板子静止 2-3 秒,采样 100 到 200 次陀螺仪原始值求平均,得到一个零偏值,之后每次读取都减去这个偏移量。
int32_t gyro_x_offset = 0; void calibrate_gyro(void) { int32_t sum = 0; int16_t gx; int16_t gy; int16_t gz; for (int i = 0; i < 200; i++) { read_mpu6050_6axis(&gx, &gy, &gz, &gx, &gy, &gz); // 仅用于校准 sum += (int32_t)gx; HAL_Delay(5); } gyro_x_offset = sum / 200; }注意上面这段代码为了演示简洁把加速度计和陀螺仪变量复用,实际工程中要分开声明。还有一种情况是刚上电时陀螺仪数据在几十秒内缓慢漂移,这跟芯片内部温度上升有关,此时零偏会发生变化。如果对精度要求较高,可以在数据采集主循环里周期性地检测静止状态并更新零偏,而不是只在初始化时校准一次。对于六轴数据的串口打印场景,静态校准一次足够看到大致姿态变化趋势。
零偏校准和量程是两个容易相互干扰的环节。量程选得越小,灵敏度越高,能分辨更细微的旋转;但量程小也意味着相同的零偏对应的原始值更大,换算成角度后漂移看起来更明显。初学者常把零漂大的原因归结为传感器坏了,实际是量程设置和校准方式不匹配。
4. 串口打印:从 HAL 库重定向 printf 到串口调试助手里的可视化
4.1 串口初始化与波特率选择
STM32F103C8T6 的 USART1 默认映射在 PA9(TX)和 PA10(RX)。与 CH340 串口驱动对应的 USB 转 TTL 模块连接时,TX 接 RX、RX 接 TX,GND 接 GND,这是串口通读的老大难问题。接线顺序错了数据完全收不到,还容易烧坏引脚。
HAL 库中串口初始化主要设置波特率、字长、停止位和流控:
UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; // 与串口调试助手一致 huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } }波特率选择要考虑两点:一是数据量,115200bps 下每秒最多传输约 11520 字节,以 50Hz 采集频率、每帧 60 字节算,占用不到 6000 字节,足够;二是 USB 转串口芯片的兼容性,CH340 驱动在 115200 波特率下表现最稳定,230400 甚至 460800 时部分劣质线材会出现丢字节。
4.2 printf 重定向与串口发送的阻塞/中断/DMA 之争
如果在 C 库中直接调用printf,输出默认走标准输出设备,对嵌入式 MCU 来说就是 JTAG 的 SWO 引脚或者没有重定向的串口外设。常见做法是重写fputc函数,把字符交给 HAL 库的串口发送接口。
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10); return ch; }这里用的是阻塞发送,每个字符等待发送完成或超时。在, MFG 数据采集循环中每 20ms 调用一次 printf,阻塞发送 60 字节所需时间约 5ms(115200 波特率下),CPU 占用不是问题。但如果采集频率提高到 1kHz,并且每帧数据量超过 100 字节,阻塞发送会严重拖慢主循环。此时有两种改进:一是使用HAL_UART_Transmit_IT中断发送,把数据放入环形缓冲区后立即返回;二是直接使用HAL_UART_Transmit_DMA`,让 DMA 控制器把内存中的数据搬运到串口数据寄存器。源码工程一般演示用阻塞发送,因为它最简单、最容易排查,但代码结构上预留了切换到中断发送的接口。
串口调试助手这一端也要注意:不要勾选“发送新行”却和程序端printf里的\r\n叠加成\r\r\n,有些上位机解析 CSV 格式时会因此多出一个空列。常用做法是程序内统一输出\r\n,上位机设置为不附加任何字符。
4.3 用 CSV 格式输出六轴数据以便在串口数据记录仪中分析
串口打印的数据格式直接影响后续处理效率。如果只给人看,可以打印带单位说明的文字;如果要导入 MATLAB 或 Python 做姿态解算,CSV 或 JSON 更合适。
while (1) { int16_t ax, ay, az, gx, gy, gz; if (read_mpu6050_6axis(&ax, &ay, &az, &gx, &gy, &gz) == 0) { printf("%d,%d,%d,%d,%d,%d\r\n", ax, ay, az, gx, gy, gz); } HAL_Delay(20); // 约 50Hz 输出频率 }这段代码输出的六个数均为原始值,没有做除以灵敏度的换算。这样设计的好处是保留整数精度,上位机可以自由选择量程对应的换算系数。如果你在串口调试助手里直接观察方向变化,原始值可能不够直观,可以把换算公式放到代码里:
float ax_g = ax / 16384.0f; float gx_dps = gx / 16.4f; printf("%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\r\n", ax_g, ay / 16384.0f, az / 16384.0f, gx_dps, gy / 16.4f, gz / 16.4f);浮点 printf 在 STM32F103C8T6 上会占用较多 Flash。使用%.2f格式符时,C 库会链接浮点打印支持代码,体积可能增加 10KB 以上。如果 Flash 空间紧张,可以把浮点数乘以 100 转成整数输出,或者用sprintf配合整数和一个小数点处理函数。另一个思路是在 Keil 的 Option for Target 里勾选 Use MicroLIB,它提供精简版 printf,浮点支持更省空间。这个工程标题既然带了“源码工程”,读者拿到的代码多半已经做了上述取舍,但了解原因对后续修改有帮助。
4.4 串口接收校验:用帧头帧尾让上位机稳定解析
打印数据容易,解析数据却常常出问题。姿态传感器数据流一旦发生错位,比如丢了一个字节,后续所有数据帧都会错位,直到下一个帧头出现。比较稳妥的方案是定义一个简单的帧格式:
| 帧头 | 数据类型 | 数据长度 | 数据区 | 校验和 |
|---|---|---|---|---|
| 0xAA 0x55 | 0x01 | 0x0C | 12 字节六轴原始值 | 1 字节 |
帧头固定 2 字节,数据区放 6 个int16_t,校验和对数据类型、长度和数据区所有字节求和取低 8 位。这样串口调试助手只要按字节流解析,在收到 0xAA 0x55 后读取类型、长度,再校验,就能确定一帧数据是否完整。如果校验失败,丢弃这一帧,继续找下一个帧头。
void send_frame(int16_t *data) { uint8_t frame[16]; frame[0] = 0xAA; frame[1] = 0x55; frame[2] = 0x01; // 0x01 表示六轴数据 frame[3] = 12; // 数据区长度 memcpy(&frame[4], data, 12); uint8_t sum = 0; for (int i = 2; i < 16; i++) sum += frame[i]; frame[15] = sum; HAL_UART_Transmit(&huart1, frame, 16, 20); }这个方案来自我实际调试串口数据记录仪的经验。如果上位机是自己写的,解析这种固定帧格式比 CSV 简单得多;如果只是用串口调试助手手工观察,CSV 更直观。源码工程里选哪种格式不重要,重要的是理解帧结构的组包逻辑,方便后续扩展温湿度、气压等其他传感器数据。
5. 实战排错与进阶:从数据不动到图像化上位机的完整调试路径
5.1 串口烧写失败与常见下载问题排查
STM32F103C8T6 通过串口 BOOT 下载程序时,BOOT0 引脚需要拉高到 3.3V,BOOT1 接地,然后按复位键进入系统存储器引导模式。如果使用 FlyMCU 或 mcuisp 这类工具时提示“串口烧写失败”,检查顺序是:CH340 驱动是否装好、设备管理器中端口号是否存在、BOOT0 跳线帽位置是否正确。很多人在 Windows 下插上 USB 转 TTL 后,设备管理器里显示的是 COM3 但无法打开,原因是该端口被其他程序占用,或者 CH340 驱动版本和 64 位系统不兼容。
如果烧写时提示“写入超时”且波特率已是默认的 115200,尝试降低到 9600。部分劣质 USB 转串口线在高速下载时电压跌落导致 MCU 复位不稳定,低速反而稳定。还有一类特殊现象:烧写成功后程序运行正常,但第二次烧写需要拔掉 VCC 或者手动按复位按键——这是目标板供电不足的典型表现。
5.2 数据一直为零或 ACK 失败的逐级排查
MPU6050 读取不到数据的现象分两种:I2C 通信异常和寄存器配置错误。I2C 通信异常时,HAL_I2C_Mem_Read返回HAL_ERROR或HAL_BUSY;寄存器配置错误时通信正常但数据恒为 0。
排查 I2C 通信问题的顺序:先测 MPU6050 的 VCC 引脚电压是否稳定在 3.3V,再用万用表测 SCL 和 SDA 的电平。空闲状态时两根线都应是高电平。如果 SDA 被拉低,说明从机或主机有一个正在占用总线,可以给最小系统板断电再上电复现。如果 SCL 正常但 SDA 一直是低,检查是不是 SDA 与某个地线短路。
排除硬件后,在代码中只读 WHO_AM_I 寄存器(地址 0x75),正常应返回 0x68。如果返回 0xFF 或 0x00,说明地址不匹配或从机未响应。注意有些仿制的 MPU6050 芯片地址可能不是 0x68,读 WHO_AM_I 是最可靠的验证手段。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读 WHO_AM_I 返回 0xFF | SCL/SDA 接反或上拉缺失 | 用逻辑分析仪看总线上有无信号 |
| 返回随机值 | 电源纹波过大 | 加 100nF 去耦电容靠近 VCC |
| 数据为 0 但 ACK 正常 | 量程寄存器写入失败 | 读回 0x1B 寄存器,确认是 0x18 |
| 数据漂移很大 | 未做零偏校准 | 静止 2 秒求平均作为偏移量 |
| 数据偶尔跳变 | 串口波特率不匹配 | 检查调试助手波特率是否为 115200 |
5.3 进阶:把六轴数据送到上位机做姿态可视化
六轴数据打印到串口只是第一步,姿态解算和可视化才是开发平衡车、四轴甚至 VR 头显的关键。常见做法是上位机接收串口数据后做互补滤波或 Madgwick 解算。互补滤波在 MCU 上实现更常见,因为它计算量小,配合加速度计修正陀螺仪积分漂移的效果在 ±60° 范围内足够好。
如果源码工程只打印原始数据也没关系,你可以把硬件放到任意平台上继续处理。比如用 CP2102 或 FTDI 串口驱动线连接开发板,Python 端读取串口,利用pyserial和matplotlib绘制实时曲线:
import serial import matplotlib.pyplot as plt ser = serial.Serial('COM10', 115200, timeout=0.1) ax_vals, gx_vals = [], [] while True: line = ser.readline().decode().strip() parts = line.split(',') if len(parts) >= 6: gx = int(parts[3]) ax_vals.append(gx) if len(ax_vals) > 100: ax_vals.pop(0) plt.clf() plt.plot(ax_vals) plt.pause(0.01)这段代码只演示了陀螺仪 X 轴的可视化。实际使用中要注意串口读取超时设置,timeout=0.1能避免程序卡在readline()上。如果开发板输出的行尾是\r\n,Python 的strip()会同时移除两者,不会出现多余空行。更高阶的做法是在 MCU 端用串口 DMA 配合环形缓冲区,实现 1kHz 采样率下不丢帧的持续传输,再在上位机做姿态动画渲染——但这是另一个工程话题了,核心前提仍然是先把这里讲到的六轴原始数据链路跑通。
最后补充一个实用技巧:在串口输出里周期性打印MPU6050 OK这类状态信息,上位机用字符串匹配判断传感器和串口是否都在正常工作。多传感器系统中,数据流本身会掩盖通信异常,一个独立于数据帧的心跳信息能省下大量排错时间。
本文还有配套的精品资源,点击获取