尧图精选

STM32+MPU6050:I2C读取六轴数据并串口打印的完整实现

🕒 发布时间:2026/9/11 23:39:45 📁 来源:尧图网络
简介这是一份基于STM32F103C8T6的MPU6050六轴数据读取工程源码面向需要快速上手陀螺仪与加速度计开发的嵌入式初学者解决原始数据采集并串口输出的问题。项目采用IIC通信硬件接线清晰SDA接PB6SCL接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.3VGND 接 GNDSCL 接 PB6I2C1 的 SCLSDA 接 PB7I2C1 的 SDAAD0 接地此时 I2C 从机地址为 0x68七位地址XDA/XCL 不用管那是接外部磁力计用的如果你用的是 GY-521 这类现成模块板上已经有 10k 上拉电阻到 VCC可以不额外加。若使用裸片 MPU6050必须在 SCL 和 SDA 上各接 4.7k10k 上拉电阻到 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 的七位地址是 0x68HAL 库要求左移一位变成 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, reg_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, reg_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, reg_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, reg_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, reg_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±2g16384±250dps1310x01±4g8192±500dps65.50x08±8g4096±1000dps32.80x18±16g2048±2000dps16.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 默认映射在 PA9TX和 PA10RX。与 CH340 串口驱动对应的 USB 转 TTL 模块连接时TX 接 RX、RX 接 TXGND 接 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 字节所需时间约 5ms115200 波特率下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 0x550x010x0C12 字节六轴原始值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.3VBOOT1 接地然后按复位键进入系统存储器引导模式。如果使用 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 返回 0xFFSCL/SDA 接反或上拉缺失用逻辑分析仪看总线上有无信号返回随机值电源纹波过大加 100nF 去耦电容靠近 VCC数据为 0 但 ACK 正常量程寄存器写入失败读回 0x1B 寄存器确认是 0x18数据漂移很大未做零偏校准静止 2 秒求平均作为偏移量数据偶尔跳变串口波特率不匹配检查调试助手波特率是否为 1152005.3 进阶把六轴数据送到上位机做姿态可视化六轴数据打印到串口只是第一步姿态解算和可视化才是开发平衡车、四轴甚至 VR 头显的关键。常见做法是上位机接收串口数据后做互补滤波或 Madgwick 解算。互补滤波在 MCU 上实现更常见因为它计算量小配合加速度计修正陀螺仪积分漂移的效果在 ±60° 范围内足够好。如果源码工程只打印原始数据也没关系你可以把硬件放到任意平台上继续处理。比如用 CP2102 或 FTDI 串口驱动线连接开发板Python 端读取串口利用pyserial和matplotlib绘制实时曲线import serial import matplotlib.pyplot as plt ser serial.Serial(COM10, 115200, timeout0.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 轴的可视化。实际使用中要注意串口读取超时设置timeout0.1能避免程序卡在readline()上。如果开发板输出的行尾是\r\nPython 的strip()会同时移除两者不会出现多余空行。更高阶的做法是在 MCU 端用串口 DMA 配合环形缓冲区实现 1kHz 采样率下不丢帧的持续传输再在上位机做姿态动画渲染——但这是另一个工程话题了核心前提仍然是先把这里讲到的六轴原始数据链路跑通。最后补充一个实用技巧在串口输出里周期性打印MPU6050 OK这类状态信息上位机用字符串匹配判断传感器和串口是否都在正常工作。多传感器系统中数据流本身会掩盖通信异常一个独立于数据帧的心跳信息能省下大量排错时间。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →