STM32 I2C通信完整教程:从协议原理到硬件实现
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); // 释放SCLMyI2C_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); // 主机释放SDAMyI2C_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°/sMPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);// 配置加速度计:最大量程 ±16gMPU6050_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; // 时钟频率50kHzI2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 时钟占空比2:1I2C_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}; // 角度环PIDstatic PID_Controller_t speed_pid = {10.0, 0.1, 0.0, 0}; // 速度环PIDint16_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标准外设库文档
作者声明:
本教程基于实际项目经验整理,所有代码均经过测试验证。如有疑问或建议,欢迎交流讨论。