I2C 外设知识体系:从基础到 STM32 硬件实现
文章目录
I2C外设简介
I2C 通信实现方式对比
1. 软件模拟 I2C
- 原理:通过手动翻转 GPIO 引脚电平,模拟 I2C 时序(时钟线 SCL 和数据线 SDA)。
- 例:用软件控制 SCL 拉低 / 释放,逐位判断数据位并操作 SDA,生成通信波形。
- 优势
- 灵活性高,无需硬件支持,适用于简单场景。
- 资源限制少,只需复制代码即可开辟多路 I2C 总线。
- 局限
- 依赖软件延时,时序精度较低(同步时序允许一定不规整)。
- 占用 CPU 资源,不适合高频或复杂通信。
2. 硬件实现 I2C
- 原理:通过单片机内置的 I2C 外设自动生成时序,软件只需配置寄存器。
- 例:STM32 的 I2C 外设可自动处理起始 / 终止条件、时钟生成、数据收发、应答机制等。
- 优势
- 时序精准,效率高,释放 CPU 资源。
- 支持完整协议特性(如多主机模型、DMA 传输、CRC 校验)。
- 局限
- 依赖硬件资源,引脚固定(如 STM32 I2C1 对应 PB6/PB7 或重映射引脚)。
- 配置复杂度高于软件模拟(需理解寄存器和状态机)。
STM32 I2C 外设核心功能
1. 硬件特性
- 集成收发电路:自动处理时序逻辑,软件通过操作寄存器(CR 控制寄存器、DR 数据寄存器、SR 状态寄存器)控制通信。
- 多主机模型
- 固定多主机:总线上有固定主机和从机,主机间需总线仲裁(如果多主设备同时发起通信)。
- 可变多主机:设备可动态切换主从角色(STM32 采用此模型,默认从模式,需主动申请主机权限)。
- 地址模式
- 7 位地址:最常用(128 个地址,支持地址低位可配置)。
- 10 位地址:扩展至 1024 个地址,通过两字节寻址(首字节高 5 位为标志位 “11110”)。
- 通信速度
- 标准模式:最高 100kHz,快速模式:最高 400kHz(同步时序允许非严格频率)。
- 扩展功能:支持 DMA 传输(提升多字节收发效率)、兼容 SMBus 协议(系统管理总线)。
2. 寄存器与引脚
- 引脚复用
- I2C1:默认 PB6(SCL)、PB7(SDA),可重映射至 PB8/PB9。
- I2C2:默认 PB10(SCL)、PB11(SDA)。
- 注意:硬件 I2C 引脚固定,需配置为 “复用开漏输出” 模式(确保总线电平正确)。
- 核心寄存器
- 控制寄存器(CR):设置起始 / 终止条件、应答使能、主从模式等。
- 数据寄存器(DR):存放待发送 / 已接收的数据,与移位寄存器配合实现逐位传输。
- 状态寄存器(SR):包含标志位(如 SB 起始条件发送完成、ADDR 地址传输完成、TXE 发送寄存器空等),用于监测通信状态。
I2C框图
一、引脚接口
- SDA(数据线):双向传输数据,通过 “数据控制” 模块与内部电路交互,支持开漏输出(需外接上拉电阻)。
- SCL(时钟线):由 “时钟控制” 模块管理,用于同步数据传输,主机模式下生成时钟信号。
- SMBALERT:用于 SMBus 协议的警报信号,连接至 “控制逻辑电路”,常规 I²C 通信中较少使用。
二、数据处理模块
- 数据寄存器(DATA REGISTER):存储待发送或已接收的字节数据,是软件与硬件交互的核心接口。
- 数据移位寄存器:与数据寄存器协作,实现数据逐位发送(高位先行)或接收(高位先入)。发送时从数据寄存器获取数据,接收时将数据存入数据寄存器。
- 比较器:将接收到的地址与 “自身地址寄存器”(单地址模式)或 “双地址寄存器”(双地址模式)对比,判断是否为目标设备地址,确保通信寻址准确。
- 帧错误校验(PEC)计算:生成或校验帧错误校验码,用于数据完整性检查,结果存储于 “PEC 寄存器”,提升通信可靠性。
三、时钟控制模块
- 时钟控制寄存器(CCR):配置时钟频率(如标准模式 100kHz、快速模式 400kHz)和相位,精确控制 SCL 的生成时序,确保通信同步。
四、控制逻辑模块
- 控制寄存器(CR1 & CR2)
- 设置工作模式(主 / 从、发送 / 接收)、触发起始 / 终止条件、使能应答(ACK)等关键操作。
- 例如,主机发送时通过 CR1 设置 “START=1” 生成起始信号,从机通过地址寄存器响应寻址。
- 状态寄存器(SR1 & SR2)
- 实时反馈通信状态,如起始条件完成(SB 标志)、地址传输完成(ADDR 标志)、数据寄存器空(TXE 标志)等。
- 软件通过查询这些标志位(或依赖中断)监控通信流程,确保时序正确。
- 控制逻辑电路
- 协调各模块工作,根据控制寄存器指令生成控制信号,触发中断和 DMA 请求。
- 例如,数据传输完成时,向 CPU 发送中断;大批量数据传输时,发起 DMA 请求以减轻 CPU 负担。
五、辅助功能
- 中断:当特定事件(如地址匹配、数据传输完成、应答失败)发生时,控制逻辑电路触发中断,通知 CPU 及时处理异常或继续后续操作。
- DMA 请求与响应:支持直接内存访问,适用于大量数据收发场景,避免 CPU 频繁干预,提升系统效率
I2C基本结构
这个图是上面总线,进行简化之后的基本结构图,后面会根据这个图进行程序的编写。
主机发送
一、7 位主发送序列
- 起始条件(S):产生起始信号,触发 EV5(SB = 1),表示起始条件已发送。软件通过读状态寄存器 SR1 确认后,将 7 位从机地址(含写位)写入数据寄存器 DR,清除 EV5。
- 发送地址:地址通过 SDA 发送,从机应答(A)后触发 EV6(ADDR = 1),表示地址传输完成。软件读 SR1 后读 SR2,清除 EV6。
- 发送数据
- 首次发送数据(如数据 1)前,触发 EV8_1(TxE = 1,移位寄存器空),软件将数据写入 DR。
- 后续数据发送时,触发 EV8(TxE = 1,移位寄存器非空),每次数据写入 DR 后清除该事件。每个数据字节后等待从机应答(A)。
- 发送结束:最后一个数据(数据 N)发送后,触发 EV8_2(TxE = 1,BTF = 1),软件请求设置停止位(P)。硬件在产生停止条件时,自动清除 TxE 和 BTF 位。
二、10 位主发送序列
- 起始条件(S):同 7 位模式,触发 EV5(SB = 1),写帧头(10 位地址高 8 位,含写位)到 DR 清除。
- 发送帧头与地址
- 帧头发送后,从机应答(A),触发 EV9(ADDR10 = 1),读 SR1 后写 10 位地址低 2 位到 DR 清除。
- 地址低 2 位发送后,从机应答(A),触发 EV6(ADDR = 1),读 SR1 和 SR2 清除。
- 发送数据:与 7 位模式一致,通过 EV8_1 和 EV8 事件逐字节发送数据,每个数据后等待应答(A)。
- 发送结束:同 7 位模式,触发 EV8_2 后设置停止位(P),结束传输。
三、关键事件(EV)说明
- EV5(SB = 1):起始条件发送完成,读 SR1 后写地址 / 帧头到 DR 清除。
- EV6(ADDR = 1):地址传输完成(含应答),读 SR1 和 SR2 清除。
- EV8_1(TxE = 1):移位寄存器空,写 DR 准备发送数据。
- EV8(TxE = 1):数据寄存器空,写 DR 清除,确保数据连续发送。
- EV8_2(TxE = 1,BTF = 1):最后一字节发送完成,请求停止条件,硬件自动清除相关位。
- EV9(ADDR10 = 1):10 位地址帧头发送完成,读 SR1 后写地址低 2 位到 DR 清除。
四、注意事项
- SCL 拉长:EV5、EV6、EV9、EV8_1、EV8_2 事件会拉长 SCL 低电平,直至软件完成对应操作(如写 DR、读寄存器)。
- EV8 操作时序:EV8 的软件序列(写 DR)必须在当前字节传输结束前完成,避免时序错误
主机接收
一、7 位主接收序列
- 起始条件(S):产生起始信号,触发 EV5(SB = 1)。软件读状态寄存器 SR1 后,将 7 位从机地址(含读位)写入数据寄存器 DR,清除 EV5。
- 发送地址:地址通过 SDA 发送,从机应答(A)后触发 EV6(ADDR = 1)。软件读 SR1 后读 SR2,清除 EV6。
- 接收数据
- 接收第一个数据(如数据 1)时,无对应事件标志(EV6_1 仅适用于接收 1 字节场景,在 EV6 后清除应答和停止条件产生位)。
- 后续数据接收时,触发 EV7(RxNE = 1),表示数据寄存器非空,软件读 DR 清除该事件。
- 接收结束:最后一个数据(数据 N)接收时,触发 EV7_1(RxNE = 1)。软件读 DR 后,设置 ACK = 0(非应答)和 STOP 请求,结束通信。
二、10 位主接收序列
- 起始条件(S):同 7 位模式,触发 EV5(SB = 1),写 10 位地址帧头(高 8 位,含读位)到 DR 清除。
- 发送帧头与地址
- 帧头发送后,从机应答(A),触发 EV9(ADDR10 = 1)。软件读 SR1 后,写 10 位地址低 2 位到 DR 清除。
- 地址低 2 位发送后,从机应答(A),触发 EV6(ADDR = 1)。软件读 SR1 和 SR2 清除。若需继续接收,设置 CR2 的 START = 1,产生重复起始条件(Sr)。
- 接收数据:重复起始条件后,发送帧头(触发 EV6),后续数据接收同 7 位模式,通过 EV7 事件逐字节读取数据。
- 接收结束:同 7 位模式,最后一个数据触发 EV7_1,软件设置 ACK = 0 和 STOP 请求,硬件生成停止条件,结束传输。
三、关键事件(EV)说明
- EV5(SB = 1):起始条件发送完成,读 SR1 后写地址 / 帧头到 DR 清除。
- EV6(ADDR = 1):地址传输完成(含应答),读 SR1 和 SR2 清除。10 位模式下,该事件后若需继续接收,设置 START = 1。
- EV7(RxNE = 1):数据寄存器非空,读 DR 清除,用于常规数据接收。
- EV7_1(RxNE = 1):最后一字节数据接收,读 DR 后设置 ACK = 0 和 STOP,结束通信。
- EV9(ADDR10 = 1):10 位地址帧头发送完成,读 SR1 后写地址低 2 位到 DR 清除。
四、注意事项
- EV6_1 特殊处理:仅适用于接收 1 字节场景,在 EV6 后需手动清除应答和停止条件产生位。
- 10 位模式重复起始:若需连续接收,在 EV6 后设置 START = 1,重新生成起始条件,继续通信。
软件/硬件波形对比
硬件实现I2C使用MPU6050通信
//MPU6050.c #include "stm32f10x.h" // Device header
#include "MPU6050Reg.h"
#define MPU6050_Address 0xD0//超时等待函数 ,防止函数意外卡死
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{uint16_t Timeout = 10000;while(I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS){Timeout--;if(Timeout == 0){break;}}
}
//指定位置读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t data;
// //起始位
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_Address);
// //接收应答位
// MyI2C_ReadAck();
// //发送寄存器地址
// MyI2C_SendByte(RegAddress);
// MyI2C_ReadAck();
//
// //因为又要设置读写,所以要重启起始位
// MyI2C_Start();
// //发送从机地址,因为是读数据所以地址最后的读写位为1
// MyI2C_SendByte(MPU6050_Address | 0x01);
// //接收应答位
// MyI2C_ReadAck();
// data = MyI2C_ReadByte();
// //发送应答位 表示要停止
// MyI2C_SendAck(1);
// MyI2C_End();//将上面的软件I2c转化为对应的硬件I2cI2C_GenerateSTART(I2C2, ENABLE);//等待EV5事件 从机转化为主机MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//发送从机地址位I2C_Send7bitAddress(I2C2, MPU6050_Address, I2C_Direction_Transmitter);//不用接收应答位,发送接收都会自动接收应答位//等待EV6事件 转换为发送模式MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//不用等待EV8_1事件,就是告诉可以往数据寄存器发送数据了 发送寄存器地址I2C_SendData(I2C2, RegAddress);//等待EV8事件 等待字节转移完成MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);//重启起始位I2C_GenerateSTART(I2C2, ENABLE);//等待EV5事件 从机转化为主机MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//发送要从机地址I2C_Send7bitAddress(I2C2, MPU6050_Address, I2C_Direction_Receiver);//不用接收应答位,发送接收都会自动接收应答位//等待EV6事件 转换为接收模式MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//因为是接收一个字节所以要发送接收最后一个字节之前要配置应答位,因为接收完数据就晚了I2C_AcknowledgeConfig(I2C2, DISABLE);I2C_GenerateSTOP(I2C2, ENABLE);//等待EV7事件 等待数据接收完成MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//读取数据data = I2C_ReceiveData(I2C2);//完成之后再重新将标志位 设置为ENABLE 因为默认为使能,改为默认可以兼容在进行其他操作I2C_AcknowledgeConfig(I2C2, ENABLE);return data;
}
//指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
// //起始位
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_Address);
// //接收应答位
// MyI2C_ReadAck();
// //发送寄存器地址
// MyI2C_SendByte(RegAddress);
// MyI2C_ReadAck();
// //发送数据
// MyI2C_SendByte(Data);
// MyI2C_ReadAck();
// //停止位
// MyI2C_End();//将上面的软件I2c转化为对应的硬件I2cI2C_GenerateSTART(I2C2, ENABLE);//等待EV5事件 从机转化为主机MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);//发送从机地址位I2C_Send7bitAddress(I2C2, MPU6050_Address, I2C_Direction_Transmitter);//不用接收应答位,发送接收都会自动接收应答位//等待EV6事件 转换为发送模式MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//不用等待EV8_1事件,就是告诉可以往数据寄存器发送数据了 发送寄存器地址I2C_SendData(I2C2, RegAddress);//等待EV8事件 等待字节转移完成MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);//发送寄存器要写入的数据I2C_SendData(I2C2, Data);//等待EV8_2事件 等待字节转移完成MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);//产生结束位I2C_GenerateSTOP(I2C2, ENABLE);
}uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
void MPU6050_Init(void)
{
// MyI2C_Init();//硬件I2C初始化代码//1. 开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);//2.配置gpio口 10 11 为复用开漏//初始化gpioinit参数中的结构体GPIO_InitTypeDef GPIO_InitStructure;//将gpio口设置为推挽输出GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_OD;//初始化两个端口GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);// 3. 初始化I2cI2C_InitTypeDef I2C_InitStruct;//开启应答位I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;I2C_InitStruct.I2C_ClockSpeed = 50000;//占空比 2 比 1 其实都差不多I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;//应答地址,这里是主机所以不重要I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//是主机随便设置地址就可以了,不和其他硬件重合就可以了I2C_InitStruct.I2C_OwnAddress1 = 0x00;I2C_Init(I2C2, &I2C_InitStruct);//开启I2c总开关I2C_Cmd(I2C2, ENABLE);//配置相关的寄存器//电源管理寄存器 时钟选择陀螺仪x时钟MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//电源管理寄存器2 配置待机模式,都不待机MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//配置分频系数,值越小越快MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//配置寄存器 滤波模式 最平滑的滤波 0x06MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//陀螺仪配置寄存器 选择最大量程 11 MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//加速度计配置寄存器 最大量程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;DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);*AccX = DataH << 8 | DataL;DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);*AccY = DataH << 8 | DataL;DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);*AccZ = DataH << 8 | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);*GyroX = DataH << 8 | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);*GyroY = DataH << 8 | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);*GyroZ = DataH << 8 | DataL;}