STM32F103C8T6核心板驱动MPU6050:从I2C时序到OLED显示的保姆级教程
当你第一次拿到STM32F103C8T6核心板和MPU6050模块时,可能会被I2C通信、寄存器配置、数据解析等一系列概念搞得晕头转向。这篇文章将带你从零开始,一步步实现MPU6050数据的读取和OLED显示,过程中遇到的每一个坑我都会提前预警。
1. 硬件准备与连接
在开始写代码之前,正确的硬件连接是成功的第一步。STM32F103C8T6核心板(俗称"蓝莓派")因其价格低廉且功能完善,成为许多嵌入式初学者的首选。MPU6050模块则集成了3轴加速度计和3轴陀螺仪,通过I2C接口与主控通信。
所需材料清单:
- STM32F103C8T6核心板 ×1
- MPU6050模块 ×1
- 0.96寸OLED显示屏(SSD1306驱动) ×1
- 杜邦线若干
- USB转TTL模块(用于程序烧录)
硬件连接时特别注意以下几点:
- MPU6050的VCC接3.3V,绝对不能接5V,否则可能损坏模块
- I2C通信需要上拉电阻,如果模块上没有集成,需在SDA和SCL线上各接4.7kΩ电阻到3.3V
- OLED显示屏同样使用I2C接口,可以与MPU6050共用I2C总线
具体接线方式:
| STM32引脚 | MPU6050引脚 | OLED引脚 |
|---|---|---|
| PB6 | SCL | SCL |
| PB7 | SDA | SDA |
| 3.3V | VCC | VCC |
| GND | GND | GND |
提示:如果使用硬件I2C,SCL应接PB6,SDA接PB7;如果使用软件模拟I2C,则可以任意选择两个GPIO口。
2. 软件I2C驱动实现
STM32的硬件I2C外设配置复杂且容易出问题,对于初学者来说,软件模拟I2C是更可靠的选择。下面我们从头构建一个稳定的软件I2C驱动。
2.1 I2C基础时序实现
I2C通信的核心是精确控制SCL和SDA线的时序。我们先定义基本的GPIO操作函数:
// MyI2C.h #ifndef __MYI2C_H #define __MYI2C_H #include "stm32f10x.h" void MyI2C_Init(void); void MyI2C_Start(void); void MyI2C_Stop(void); void MyI2C_SendByte(uint8_t byte); uint8_t MyI2C_ReceiveByte(void); void MyI2C_SendAck(uint8_t ack); uint8_t MyI2C_ReceiveAck(void); #endif对应的实现文件中,我们需要特别注意时序延迟。MPU6050的工作频率最高为400kHz(快速模式),但作为初学者,我们先使用100kHz的标准模式:
// MyI2C.c #include "MyI2C.h" #include "Delay.h" #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 #define I2C_PORT GPIOB // 初始化I2C GPIO void MyI2C_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(I2C_PORT, &GPIO_InitStructure); GPIO_SetBits(I2C_PORT, I2C_SCL_PIN | I2C_SDA_PIN); // 总线空闲状态 } // 产生起始条件 void MyI2C_Start(void) { GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); GPIO_SetBits(I2C_PORT, I2C_SCL_PIN); Delay_us(5); GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN); Delay_us(5); GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN); } // 产生停止条件 void MyI2C_Stop(void) { GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN); GPIO_SetBits(I2C_PORT, I2C_SCL_PIN); Delay_us(5); GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); Delay_us(5); }2.2 完整I2C通信函数
发送和接收字节是I2C通信的核心功能,需要严格按照时序图实现:
// 发送一个字节 void MyI2C_SendByte(uint8_t byte) { uint8_t i; for(i = 0; i < 8; i++) { if(byte & 0x80) { GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); } else { GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN); } Delay_us(2); GPIO_SetBits(I2C_PORT, I2C_SCL_PIN); Delay_us(5); GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN); Delay_us(2); byte <<= 1; } // 释放SDA线用于接收ACK GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); } // 接收一个字节 uint8_t MyI2C_ReceiveByte(void) { uint8_t i, byte = 0; GPIO_SetBits(I2C_PORT, I2C_SDA_PIN); // 释放SDA线 for(i = 0; i < 8; i++) { GPIO_SetBits(I2C_PORT, I2C_SCL_PIN); Delay_us(3); byte <<= 1; if(GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA_PIN)) { byte |= 0x01; } Delay_us(2); GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN); Delay_us(5); } return byte; }3. MPU6050驱动开发
有了可靠的I2C驱动后,我们就可以开始与MPU6050通信了。首先需要了解MPU6050的寄存器映射和配置方法。
3.1 MPU6050寄存器配置
MPU6050有多个配置寄存器,我们需要重点关注以下几个:
| 寄存器地址 | 名称 | 功能描述 |
|---|---|---|
| 0x6B | PWR_MGMT_1 | 电源管理,解除睡眠模式 |
| 0x1B | GYRO_CONFIG | 陀螺仪量程配置 |
| 0x1C | ACCEL_CONFIG | 加速度计量程配置 |
| 0x19 | SMPLRT_DIV | 采样率分频器 |
| 0x1A | CONFIG | 数字低通滤波器配置 |
| 0x75 | WHO_AM_I | 器件ID(0x68) |
创建MPU6050的驱动头文件:
// MPU6050_Reg.h #ifndef __MPU6050_REG_H #define __MPU6050_REG_H #define MPU6050_ADDRESS_AD0_LOW 0xD0 #define MPU6050_ADDRESS_AD0_HIGH 0xD1 // 寄存器地址定义 #define MPU6050_SMPLRT_DIV 0x19 #define MPU6050_CONFIG 0x1A #define MPU6050_GYRO_CONFIG 0x1B #define MPU6050_ACCEL_CONFIG 0x1C #define MPU6050_WHO_AM_I 0x75 #define MPU6050_PWR_MGMT_1 0x6B #define MPU6050_PWR_MGMT_2 0x6C #define MPU6050_ACCEL_XOUT_H 0x3B #define MPU6050_ACCEL_XOUT_L 0x3C // ...其他数据寄存器省略 #endif3.2 MPU6050初始化
初始化过程需要按照特定顺序配置多个寄存器:
// MPU6050.c #include "MPU6050_Reg.h" #include "MyI2C.h" #include "Delay.h" // 向指定寄存器写入数据 void MPU6050_WriteReg(uint8_t regAddr, uint8_t data) { MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS_AD0_LOW); // 写操作 MyI2C_ReceiveAck(); MyI2C_SendByte(regAddr); MyI2C_ReceiveAck(); MyI2C_SendByte(data); MyI2C_ReceiveAck(); MyI2C_Stop(); } // 从指定寄存器读取数据 uint8_t MPU6050_ReadReg(uint8_t regAddr) { uint8_t data; MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS_AD0_LOW); // 写操作 MyI2C_ReceiveAck(); MyI2C_SendByte(regAddr); MyI2C_ReceiveAck(); MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS_AD0_LOW | 0x01); // 读操作 MyI2C_ReceiveAck(); data = MyI2C_ReceiveByte(); MyI2C_SendAck(1); // NACK MyI2C_Stop(); return data; } // MPU6050初始化 void MPU6050_Init(void) { Delay_ms(100); // 上电延时 MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x80); // 复位设备 Delay_ms(100); MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x00); // 解除休眠 MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); // 所有轴都工作 MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x07); // 采样率1kHz MPU6050_WriteReg(MPU6050_CONFIG, 0x06); // 低通滤波器5Hz MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); // 陀螺仪±2000°/s MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); // 加速度计±16g }3.3 读取传感器数据
MPU6050的加速度计和陀螺仪数据都是16位有符号数,存储在连续的寄存器中:
// 读取6轴数据 void MPU6050_GetData(int16_t* accX, int16_t* accY, int16_t* accZ, int16_t* gyroX, int16_t* gyroY, int16_t* gyroZ) { uint8_t buf[14]; MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS_AD0_LOW); // 写操作 MyI2C_ReceiveAck(); MyI2C_SendByte(MPU6050_ACCEL_XOUT_H); MyI2C_ReceiveAck(); MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS_AD0_LOW | 0x01); // 读操作 MyI2C_ReceiveAck(); // 连续读取14个字节(6轴数据+温度) for(int i = 0; i < 13; i++) { buf[i] = MyI2C_ReceiveByte(); MyI2C_SendAck(0); // ACK } buf[13] = MyI2C_ReceiveByte(); MyI2C_SendAck(1); // NACK MyI2C_Stop(); // 组合高低字节 *accX = (buf[0] << 8) | buf[1]; *accY = (buf[2] << 8) | buf[3]; *accZ = (buf[4] << 8) | buf[5]; *gyroX = (buf[8] << 8) | buf[9]; *gyroY = (buf[10] << 8) | buf[11]; *gyroZ = (buf[12] << 8) | buf[13]; }4. OLED数据显示实现
最后一步是将读取到的传感器数据可视化显示在OLED屏幕上。我们使用常见的SSD1306驱动的0.96寸OLED屏。
4.1 OLED驱动初始化
首先初始化OLED显示屏:
// OLED.c #include "OLED.h" #include "Delay.h" void OLED_Init(void) { // 初始化I2C MyI2C_Init(); // 发送初始化命令序列 OLED_WriteCommand(0xAE); // 关闭显示 OLED_WriteCommand(0xD5); // 设置显示时钟分频 OLED_WriteCommand(0x80); OLED_WriteCommand(0xA8); // 设置多路复用率 OLED_WriteCommand(0x3F); // ...更多初始化命令 OLED_WriteCommand(0xAF); // 开启显示 OLED_Clear(); } // 写命令 void OLED_WriteCommand(uint8_t cmd) { MyI2C_Start(); MyI2C_SendByte(0x78); // OLED地址 MyI2C_ReceiveAck(); MyI2C_SendByte(0x00); // 命令标识 MyI2C_ReceiveAck(); MyI2C_SendByte(cmd); MyI2C_ReceiveAck(); MyI2C_Stop(); }4.2 数据显示实现
在OLED上清晰展示6轴数据,并添加适当的标签:
// 主函数 #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "MPU6050.h" int main(void) { int16_t accX, accY, accZ; int16_t gyroX, gyroY, gyroZ; Delay_init(); OLED_Init(); MPU6050_Init(); // 显示静态标签 OLED_ShowString(1, 1, "Acc:"); OLED_ShowString(3, 1, "Gyro:"); OLED_ShowString(1, 6, "X:"); OLED_ShowString(2, 6, "Y:"); OLED_ShowString(3, 6, "Z:"); OLED_ShowString(1, 12, "Y:"); OLED_ShowString(2, 12, "P:"); OLED_ShowString(3, 12, "R:"); while(1) { MPU6050_GetData(&accX, &accY, &accZ, &gyroX, &gyroY, &gyroZ); // 显示加速度计数据 OLED_ShowSignedNum(1, 8, accX, 5); OLED_ShowSignedNum(2, 8, accY, 5); OLED_ShowSignedNum(3, 8, accZ, 5); // 显示陀螺仪数据 OLED_ShowSignedNum(1, 14, gyroX, 5); OLED_ShowSignedNum(2, 14, gyroY, 5); OLED_ShowSignedNum(3, 14, gyroZ, 5); Delay_ms(100); // 100ms刷新一次 } }5. 常见问题与调试技巧
在实际开发过程中,你可能会遇到各种问题。以下是几个常见问题及其解决方法:
I2C通信失败
- 检查硬件连接是否正确,特别是SDA和SCL线是否接反
- 用逻辑分析仪或示波器观察I2C波形,确认时序是否符合标准
- 尝试降低I2C时钟频率(增加Delay时间)
MPU6050无响应
- 确认MPU6050的电源电压为3.3V
- 检查AD0引脚的电平,确保使用的I2C地址正确
- 读取WHO_AM_I寄存器(0x75),返回值应为0x68
数据跳动严重
- 尝试配置数字低通滤波器(CONFIG寄存器)
- 对数据进行软件滤波处理(如移动平均)
- 确保MPU6050固定牢固,避免机械振动影响
OLED显示异常
- 检查OLED的I2C地址(通常是0x78或0x7A)
- 确认初始化命令序列正确
- 如果显示内容错位,检查GRAM的写入逻辑
// 简单的移动平均滤波示例 #define FILTER_SIZE 5 int16_t filterBuffer[FILTER_SIZE]; uint8_t filterIndex = 0; int16_t applyFilter(int16_t newValue) { static int32_t sum = 0; sum -= filterBuffer[filterIndex]; filterBuffer[filterIndex] = newValue; sum += newValue; filterIndex = (filterIndex + 1) % FILTER_SIZE; return sum / FILTER_SIZE; }在实际项目中,我发现最影响MPU6050性能的是电源噪声。使用LDO稳压器为MPU6050单独供电,并添加适当的去耦电容(100nF靠近VCC引脚),可以显著提高数据稳定性。