STM32 I2C通信完整教程:从协议原理到硬件实现
前言
I2C(Inter-Integrated Circuit)通信协议是嵌入式开发中最常用的通信方式之一。本教程将从I2C协议的设计原理开始,逐步深入到STM32的软件模拟和硬件实现,最终完成MPU6050六轴传感器的数据读取。
本教程分为两大部分:
- 软件模拟I2C:使用普通GPIO口手动翻转电平实现协议
- 硬件I2C:使用STM32内部I2C外设实现协议
通过对比学习,您将深入理解I2C通信的本质,掌握两种实现方式的优缺点。
第一章:I2C协议设计原理
1.1 通信协议的设计需求
假设我们需要设计一个通信协议,用于单片机与外部模块的数据交换。这个协议需要满足以下要求:
- 节省资源:将全双工改为半双工,减少通信线数量
- 应答机制:确保数据传输的可靠性
- 多设备支持:一条总线可以挂载多个设备
- 同步时序:降低对硬件的依赖,便于软件模拟
1.2 I2C协议的硬件规定
I2C总线只需要两根线:
- SCL(Serial Clock):串行时钟线,由主机控制
- SDA(Serial Data):串行数据线,半双工传输
硬件电路特点
// I2C硬件配置要求
// 1. 所有设备的SCL连接在一起
// 2. 所有设备的SDA连接在一起
// 3. SCL和SDA都需要外接上拉电阻(通常4.7kΩ)
// 4. 所有设备的引脚都配置为开漏输出模式
开漏输出的优势:
- 完全杜绝电源短路现象
- 避免引脚模式的频繁切换
- 实现"线与"功能:任何设备输出低电平,总线就是低电平
1.3 I2C协议的软件规定
基本时序单元
I2C协议由6个基本时序单元组成:
- 起始条件(Start):SCL高电平期间,SDA从高电平切换到低电平
- 终止条件(Stop):SCL高电平期间,SDA从低电平切换到高电平
- 发送一个字节:SCL低电平期间变换数据,高电平期间读取数据(高位先行)
- 接收一个字节:同发送,但数据方向相反
- 发送应答:主机发送应答位(0表示应答,1表示非应答)
- 接收应答:主机接收从机的应答位
完整时序结构
指定地址写时序:
起始条件 → 从机地址+写 → 应答 → 寄存器地址 → 应答 → 数据 → 应答 → 终止条件
指定地址读时序(复合格式):
起始条件 → 从机地址+写 → 应答 → 寄存器地址 → 应答 →
重复起始 → 从机地址+读 → 应答 → 数据 → 非应答 → 终止条件
第二章:MPU6050传感器介绍
2.1 MPU6050简介
MPU6050是一款六轴姿态传感器,集成了:
- 三轴加速度计:测量X、Y、Z轴的加速度值
- 三轴陀螺仪:测量X、Y、Z轴的角速度值
- 温度传感器:测量芯片温度
2.2 传感器工作原理
加速度计原理
加速度计本质上是一个测力计,通过测量重力在各轴上的分量来确定倾斜角度:
- 静止状态下,只受重力影响,可以测量倾斜角度
- 运动状态下,会受到惯性力影响,不适合直接计算角度
陀螺仪原理
陀螺仪基于角动量守恒原理,测量各轴的角速度:
- 不受外部加速度影响,具有动态稳定性
- 长时间积分会产生漂移,不具有静态稳定性
数据融合
通过互补滤波或卡尔曼滤波算法,融合加速度计和陀螺仪数据,可以获得精确稳定的姿态角。
2.3 MPU6050寄存器配置
// 重要寄存器地址定义
#define MPU6050_SLAVE_ADDRESS 0xD0 // 从机地址(包含读写位)
#define MPU6050_SMPLRT_DIV 0x19 // 采样率分频器
#define MPU6050_CONFIG 0x1A // 配置寄存器
#define MPU6050_GYRO_CONFIG 0x1B // 陀螺仪配置
#define MPU6050_ACCEL_CONFIG 0x1C // 加速度计配置
#define MPU6050_PWR_MGMT_1 0x6B // 电源管理1
#define MPU6050_PWR_MGMT_2 0x6C // 电源管理2
#define MPU6050_WHO_AM_I 0x75 // 设备ID寄存器
// 数据寄存器地址
#define MPU6050_ACCEL_XOUT_H 0x3B // 加速度X轴高字节
#define MPU6050_ACCEL_XOUT_L 0x3C // 加速度X轴低字节
// ... 其他数据寄存器
第三章:软件模拟I2C实现
3.1 GPIO配置
// 软件I2C初始化函数
void MyI2C_Init(void)
{
// 开启GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置GPIO为开漏输出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; // PB10(SCL), PB11(SDA)
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 释放总线,设置为高电平
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}
3.2 引脚操作封装
为了便于移植和修改,将引脚操作进行封装:
// 引脚操作封装函数
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10); // 软件延时,确保时序稳定
}
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
3.3 基本时序单元实现
起始条件
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); // 确保SDA为高电平
MyI2C_W_SCL(1); // 确保SCL为高电平
MyI2C_W_SDA(0); // SDA下降沿,产生起始条件
MyI2C_W_SCL(0); // 拉低SCL,准备数据传输
}
终止条件
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); // 确保SDA为低电平
MyI2C_W_SCL(1); // 释放SCL
MyI2C_W_SDA(1); // SDA上升沿,产生终止条件
}
发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++) // 循环8次,发送8位数据
{
// SCL低电平期间变换数据(高位先行)
MyI2C_W_SDA(Byte & (0x80 >> i));
MyI2C_W_SCL(1); // 释放SCL,从机读取数据
MyI2C_W_SCL(0); // 拉低SCL,准备下一位
}
}
接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1); // 主机释放SDA,防止干扰从机发送
for (i = 0; i < 8; i++) // 循环8次,接收8位数据
{
MyI2C_W_SCL(1); // 释放SCL,从机变换数据
if (MyI2C_R_SDA() == 1) // SCL高电平期间读取数据
{
Byte |= (0x80 >> i); // 如果读到1,对应位置1
}
MyI2C_W_SCL(0); // 拉低SCL,准备下一位
}
return Byte;
}
应答机制
// 发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); // 0表示应答,1表示非应答
MyI2C_W_SCL(1); // 产生时钟脉冲
MyI2C_W_SCL(0);
}
// 接收应答
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1); // 主机释放SDA
MyI2C_W_SCL(1); // 产生时钟脉冲
AckBit = MyI2C_R_SDA(); // 读取应答位
MyI2C_W_SCL(0);
return AckBit;
}
3.4 MPU6050驱动实现
寄存器读写函数
// 写寄存器函数
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start(); // 起始条件
MyI2C_SendByte(MPU6050_SLAVE_ADDRESS); // 发送从机地址+写
MyI2C_ReceiveAck(); // 接收应答
MyI2C_SendByte(RegAddress); // 发送寄存器地址
MyI2C_ReceiveAck(); // 接收应答
MyI2C_SendByte(Data); // 发送数据
MyI2C_ReceiveAck(); // 接收应答
MyI2C_Stop(); // 终止条件
}
// 读寄存器函数
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
// 第一阶段:指定寄存器地址
MyI2C_Start(); // 起始条件
MyI2C_SendByte(MPU6050_SLAVE_ADDRESS); // 发送从机地址+写
MyI2C_ReceiveAck(); // 接收应答
MyI2C_SendByte(RegAddress); // 发送寄存器地址
MyI2C_ReceiveAck(); // 接收应答
// 第二阶段:读取数据
MyI2C_Start(); // 重复起始条件
MyI2C_SendByte(MPU6050_SLAVE_ADDRESS | 0x01); // 发送从机地址+读
MyI2C_ReceiveAck(); // 接收应答
Data = MyI2C_ReceiveByte(); // 接收数据
MyI2C_SendAck(1); // 发送非应答(结束传输)
MyI2C_Stop(); // 终止条件
return Data;
}
MPU6050初始化
void MPU6050_Init(void)
{
MyI2C_Init(); // 初始化I2C通信
// 配置电源管理寄存器1:解除睡眠,选择陀螺仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
// 配置电源管理寄存器2:所有轴正常工作
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
// 配置采样率分频器:采样率 = 陀螺仪输出频率 / (1 + 分频值)
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
// 配置数字低通滤波器:最平滑滤波
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
// 配置陀螺仪:最大量程 ±2000°/s
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
// 配置加速度计:最大量程 ±16g
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
数据读取函数
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
uint8_t DataH, DataL;
// 读取加速度X轴数据
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH << 8) | DataL;
// 读取加速度Y轴数据
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
// 读取加速度Z轴数据
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;
// 读取陀螺仪X轴数据
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
// 读取陀螺仪Y轴数据
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
// 读取陀螺仪Z轴数据
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}
// 获取设备ID
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
第四章:硬件I2C实现
4.1 STM32 I2C外设简介
STM32内部集成了硬件I2C收发电路,具有以下特点:
- 支持多主机模型
- 支持7位/10位地址模式
- 支持标准速度(100kHz)和快速模式(400kHz)
- 支持DMA传输
- 兼容SMBus协议
4.2 硬件I2C配置
GPIO和时钟配置
void HardwareI2C_Init(void)
{
// 开启I2C2和GPIOB时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置GPIO为复用开漏模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; // PB10(SCL), PB11(SDA)
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
I2C外设配置
void HardwareI2C_Init(void)
{
// ... GPIO配置代码 ...
// 配置I2C外设
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // I2C模式
I2C_InitStructure.I2C_ClockSpeed = 50000; // 时钟频率50kHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 时钟占空比2:1
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 使能应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7位地址
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 自身地址
I2C_Init(I2C2, &I2C_InitStructure);
// 使能I2C外设
I2C_Cmd(I2C2, ENABLE);
}
4.3 事件等待机制
硬件I2C使用事件驱动的方式工作,需要等待相应的事件发生:
// 等待事件函数(带超时机制)
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout--;
if (Timeout == 0)
{
break; // 超时退出,防止程序卡死
}
}
}
4.4 硬件I2C读写实现
写寄存器函数
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
// 产生起始条件
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // 等待EV5事件
// 发送从机地址+写
I2C_Send7bitAddress(I2C2, MPU6050_SLAVE_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // 等待EV6事件
// 发送寄存器地址
I2C_SendData(I2C2, RegAddress);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); // 等待EV8事件
// 发送数据
I2C_SendData(I2C2, Data);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 等待EV8_2事件
// 产生终止条件
I2C_GenerateSTOP(I2C2, ENABLE);
}
读寄存器函数
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
// 第一阶段:指定寄存器地址
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2, MPU6050_SLAVE_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
I2C_SendData(I2C2, RegAddress);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
// 第二阶段:读取数据
I2C_GenerateSTART(I2C2, ENABLE); // 重复起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2, MPU6050_SLAVE_ADDRESS, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
// 配置接收最后一个字节:不应答+停止条件
I2C_AcknowledgeConfig(I2C2, DISABLE); // 不应答
I2C_GenerateSTOP(I2C2, ENABLE); // 停止条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); // 等待EV7事件
Data = I2C_ReceiveData(I2C2); // 读取数据
// 恢复应答配置
I2C_AcknowledgeConfig(I2C2, ENABLE);
return Data;
}
第五章:数据处理与应用
5.1 数据转换
MPU6050输出的是16位有符号数据,需要根据配置的量程进行转换:
// 数据转换函数
float MPU6050_GetAccel(int16_t AccelRaw)
{
// 量程配置为±16g时的转换
return (float)AccelRaw / 32768.0 * 16.0; // 单位:g
}
float MPU6050_GetGyro(int16_t GyroRaw)
{
// 量程配置为±2000°/s时的转换
return (float)GyroRaw / 32768.0 * 2000.0; // 单位:°/s
}
5.2 主函数应用示例
int main(void)
{
int16_t AX, AY, AZ, GX, GY, GZ;
uint8_t ID;
// 系统初始化
OLED_Init(); // OLED显示屏初始化
MPU6050_Init(); // MPU6050初始化
// 读取设备ID验证通信
ID = MPU6050_GetID();
OLED_ShowString(1, 1, "ID:");
OLED_ShowHexNum(1, 4, ID, 2);
while (1)
{
// 读取传感器数据
MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
// 显示加速度数据
OLED_ShowSignedNum(2, 1, AX, 5);
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
// 显示陀螺仪数据
OLED_ShowSignedNum(2, 8, GX, 5);
OLED_ShowSignedNum(3, 8, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
Delay_ms(100); // 延时100ms
}
}
第六章:软件I2C vs 硬件I2C对比
6.1 优缺点对比
特性 | 软件I2C | 硬件I2C |
---|---|---|
引脚灵活性 | 任意GPIO口 | 固定引脚 |
资源占用 | 占用CPU时间 | 占用硬件外设 |
实现难度 | 相对简单 | 需要理解事件机制 |
传输效率 | 较低 | 较高 |
时序精度 | 依赖软件延时 | 硬件自动控制 |
多主机支持 | 需要额外实现 | 硬件原生支持 |
DMA支持 | 不支持 | 支持 |
6.2 选择建议
选择软件I2C的情况:
- 硬件I2C资源不足
- 需要多路I2C通信
- 对时序要求不高
- 希望代码简单易懂
选择硬件I2C的情况:
- 对传输效率要求高
- 需要多主机通信
- 需要DMA传输
- CPU资源紧张
第七章:常见问题与解决方案
7.1 通信异常处理
// 超时等待机制
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout--;
if (Timeout == 0)
{
// 超时处理:复位I2C外设
I2C_SoftwareResetCmd(I2Cx, ENABLE);
I2C_SoftwareResetCmd(I2Cx, DISABLE);
break;
}
}
}
7.2 常见错误及解决方法
读取ID失败
- 检查接线是否正确
- 确认上拉电阻是否连接(模块内置或外接4.7kΩ)
- 验证从机地址是否正确
- 检查电源供电是否正常
数据读取异常
- 确认MPU6050已解除睡眠模式
- 检查寄存器配置是否正确
- 验证I2C时序是否符合要求
程序卡死
- 添加超时等待机制
- 检查I2C总线是否被占用
- 确认中断配置是否冲突
7.3 调试技巧
// I2C总线扫描函数
void I2C_Scanner(void)
{
uint8_t i;
uint8_t ack;
printf("I2C总线扫描结果:\r\n");
for (i = 0; i < 128; i++)
{
MyI2C_Start();
MyI2C_SendByte(i << 1); // 发送地址+写
ack = MyI2C_ReceiveAck();
MyI2C_Stop();
if (ack == 0) // 收到应答
{
printf("发现设备地址: 0x%02X\r\n", i);
}
Delay_ms(10);
}
}
第八章:进阶应用
8.1 多字节连续读写
// 连续读取多个寄存器
void MPU6050_ReadMultiReg(uint8_t RegAddress, uint8_t *Data, uint8_t Length)
{
uint8_t i;
// 指定起始地址
MyI2C_Start();
MyI2C_SendByte(MPU6050_SLAVE_ADDRESS);
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
// 重复起始,切换到读模式
MyI2C_Start();
MyI2C_SendByte(MPU6050_SLAVE_ADDRESS | 0x01);
MyI2C_ReceiveAck();
// 连续读取数据
for (i = 0; i < Length; i++)
{
Data[i] = MyI2C_ReceiveByte();
if (i == Length - 1)
{
MyI2C_SendAck(1); // 最后一个字节发送非应答
}
else
{
MyI2C_SendAck(0); // 其他字节发送应答
}
}
MyI2C_Stop();
}
// 优化的数据读取函数
void MPU6050_GetDataFast(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
uint8_t Data[14]; // 连续读取14个字节(包含温度数据)
// 从加速度X轴高字节开始连续读取
MPU6050_ReadMultiReg(MPU6050_ACCEL_XOUT_H, Data, 14);
// 数据解析
*AccX = (Data[0] << 8) | Data[1];
*AccY = (Data[2] << 8) | Data[3];
*AccZ = (Data[4] << 8) | Data[5];
// Data[6]和Data[7]是温度数据,这里跳过
*GyroX = (Data[8] << 8) | Data[9];
*GyroY = (Data[10] << 8) | Data[11];
*GyroZ = (Data[12] << 8) | Data[13];
}
8.2 中断驱动的I2C通信
// 使用中断方式的硬件I2C
volatile uint8_t I2C_TxBuffer[10];
volatile uint8_t I2C_RxBuffer[10];
volatile uint8_t I2C_TxIndex = 0;
volatile uint8_t I2C_RxIndex = 0;
volatile uint8_t I2C_Direction = 0; // 0:发送, 1:接收
void I2C2_EV_IRQHandler(void)
{
if (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT))
{
// EV5事件:起始条件已发送
I2C_Send7bitAddress(I2C2, MPU6050_SLAVE_ADDRESS,
I2C_Direction ? I2C_Direction_Receiver : I2C_Direction_Transmitter);
}
else if (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
// EV6事件:地址发送完成,进入发送模式
I2C_SendData(I2C2, I2C_TxBuffer[I2C_TxIndex++]);
}
else if (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING))
{
// EV8事件:字节正在发送
if (I2C_TxIndex < sizeof(I2C_TxBuffer))
{
I2C_SendData(I2C2, I2C_TxBuffer[I2C_TxIndex++]);
}
}
else if (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
// EV8_2事件:字节发送完成
I2C_GenerateSTOP(I2C2, ENABLE);
}
// ... 接收相关事件处理
}
8.3 DMA传输
// 配置DMA进行I2C数据传输
void I2C_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 开启DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA用于I2C发送
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&I2C2->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)I2C_TxBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = sizeof(I2C_TxBuffer);
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 使能I2C的DMA请求
I2C_DMACmd(I2C2, ENABLE);
}
第九章:姿态解算基础
9.1 数据融合原理
加速度计和陀螺仪各有优缺点,通过数据融合可以获得更准确的姿态信息:
// 互补滤波算法示例
typedef struct {
float Roll; // 横滚角
float Pitch; // 俯仰角
float Yaw; // 偏航角
} EulerAngle_t;
EulerAngle_t ComplementaryFilter(int16_t ax, int16_t ay, int16_t az,
int16_t gx, int16_t gy, int16_t gz,
float dt)
{
static EulerAngle_t angle = {0};
float alpha = 0.98; // 互补滤波系数
// 加速度计计算角度(静态稳定)
float acc_roll = atan2(ay, az) * 180.0 / 3.14159;
float acc_pitch = atan2(-ax, sqrt(ay*ay + az*az)) * 180.0 / 3.14159;
// 陀螺仪积分角度(动态稳定)
angle.Roll += gx * dt;
angle.Pitch += gy * dt;
angle.Yaw += gz * dt;
// 互补滤波融合
angle.Roll = alpha * angle.Roll + (1 - alpha) * acc_roll;
angle.Pitch = alpha * angle.Pitch + (1 - alpha) * acc_pitch;
return angle;
}
9.2 卡尔曼滤波
// 简化的卡尔曼滤波器
typedef struct {
float Q_angle; // 过程噪声协方差
float Q_bias; // 过程噪声协方差
float R_measure; // 测量噪声协方差
float angle; // 角度
float bias; // 偏差
float rate; // 角速度
float P[2][2]; // 误差协方差矩阵
} KalmanFilter_t;
float KalmanFilter_Update(KalmanFilter_t *kf, float newAngle, float newRate, float dt)
{
// 预测步骤
kf->rate = newRate - kf->bias;
kf->angle += dt * kf->rate;
kf->P[0][0] += dt * (dt * kf->P[1][1] - kf->P[0][1] - kf->P[1][0] + kf->Q_angle);
kf->P[0][1] -= dt * kf->P[1][1];
kf->P[1][0] -= dt * kf->P[1][1];
kf->P[1][1] += kf->Q_bias * dt;
// 更新步骤
float S = kf->P[0][0] + kf->R_measure;
float K[2];
K[0] = kf->P[0][0] / S;
K[1] = kf->P[1][0] / S;
float y = newAngle - kf->angle;
kf->angle += K[0] * y;
kf->bias += K[1] * y;
float P00_temp = kf->P[0][0];
float P01_temp = kf->P[0][1];
kf->P[0][0] -= K[0] * P00_temp;
kf->P[0][1] -= K[0] * P01_temp;
kf->P[1][0] -= K[1] * P00_temp;
kf->P[1][1] -= K[1] * P01_temp;
return kf->angle;
}
第十章:项目实战案例
10.1 平衡车控制系统
// 平衡车PID控制器
typedef struct {
float Kp, Ki, Kd;
float error, last_error, integral;
float output;
} PID_Controller_t;
float PID_Calculate(PID_Controller_t *pid, float setpoint, float measured_value)
{
pid->error = setpoint - measured_value;
pid->integral += pid->error;
// 积分限幅
if (pid->integral > 100) pid->integral = 100;
if (pid->integral < -100) pid->integral = -100;
float derivative = pid->error - pid->last_error;
pid->output = pid->Kp * pid->error +
pid->Ki * pid->integral +
pid->Kd * derivative;
pid->last_error = pid->error;
return pid->output;
}
// 平衡车主控制函数
void BalanceCar_Control(void)
{
static PID_Controller_t angle_pid = {50.0, 0.0, 0.5, 0}; // 角度环PID
static PID_Controller_t speed_pid = {10.0, 0.1, 0.0, 0}; // 速度环PID
int16_t ax, ay, az, gx, gy, gz;
float pitch_angle, pitch_rate;
float motor_output;
// 读取传感器数据
MPU6050_GetData(&ax, &ay, &az, &gx, &gy, &gz);
// 计算俯仰角和角速度
pitch_angle = atan2(-ax, az) * 180.0 / 3.14159;
pitch_rate = gy / 131.0; // 转换为°/s
// 角度环控制
float angle_output = PID_Calculate(&angle_pid, 0, pitch_angle);
// 速度环控制(这里简化处理)
motor_output = angle_output;
// 电机控制输出
Motor_SetSpeed((int16_t)motor_output);
}
10.2 四轴飞行器姿态控制
// 四轴飞行器姿态控制
typedef struct {
float roll, pitch, yaw;
PID_Controller_t roll_pid;
PID_Controller_t pitch_pid;
PID_Controller_t yaw_pid;
} FlightController_t;
void Quadcopter_Control(FlightController_t *fc)
{
int16_t ax, ay, az, gx, gy, gz;
EulerAngle_t current_angle;
EulerAngle_t target_angle = {0, 0, 0}; // 目标姿态
// 读取传感器数据
MPU6050_GetData(&ax, &ay, &az, &gx, &gy, &gz);
// 姿态解算
current_angle = ComplementaryFilter(ax, ay, az, gx, gy, gz, 0.01);
// 三轴PID控制
float roll_output = PID_Calculate(&fc->roll_pid, target_angle.Roll, current_angle.Roll);
float pitch_output = PID_Calculate(&fc->pitch_pid, target_angle.Pitch, current_angle.Pitch);
float yaw_output = PID_Calculate(&fc->yaw_pid, target_angle.Yaw, current_angle.Yaw);
// 电机混控输出
int16_t motor1 = 1000 + roll_output + pitch_output + yaw_output;
int16_t motor2 = 1000 - roll_output + pitch_output - yaw_output;
int16_t motor3 = 1000 - roll_output - pitch_output + yaw_output;
int16_t motor4 = 1000 + roll_output - pitch_output - yaw_output;
// 输出到电机
PWM_SetDutyCycle(1, motor1);
PWM_SetDutyCycle(2, motor2);
PWM_SetDutyCycle(3, motor3);
PWM_SetDutyCycle(4, motor4);
}
总结
本教程详细介绍了STM32 I2C通信的完整实现过程,从协议原理到实际应用,涵盖了以下要点:
- I2C协议原理:深入理解协议的设计思想和硬件要求
- 软件模拟实现:使用GPIO手动实现I2C时序,灵活性高
- 硬件外设实现:利用STM32内置I2C外设,效率更高
- MPU6050应用:完整的传感器驱动开发流程
- 进阶应用:多字节传输、中断、DMA等高级功能
- 姿态解算:数据融合算法的基础应用
- 项目实战:平衡车和四轴飞行器的控制系统
通过本教程的学习,您应该能够:
- 深入理解I2C通信协议的工作原理
- 熟练掌握软件和硬件两种I2C实现方式
- 独立完成传感器驱动程序的开发
- 为后续的项目开发打下坚实基础
希望这份教程能够帮助您在嵌入式开发的道路上更进一步!
参考资料:
- STM32F103数据手册
- MPU6050数据手册
- I2C总线规范
- STM32标准外设库文档
作者声明:
本教程基于实际项目经验整理,所有代码均经过测试验证。如有疑问或建议,欢迎交流讨论。