STM32 I2C通信完整教程:从协议原理到硬件实现_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); // 释放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;}// 获取设备IDuint8_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 优缺点对比
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通信
// 使用中断方式的硬件I2Cvolatile 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标准外设库文档
作者声明:
本教程基于实际项目经验整理,所有代码均经过测试验证。如有疑问或建议,欢迎交流讨论。