I2C 通信技术深度解析:硬件原理、时序逻辑与软件模拟实战指南
I2C 总线通过。
·
文章目录
I2C
I2C的基本知识
一、I2C 基本定义与开发背景
- 全称与缩写
- 英文全称:Inter-Integrated Circuit Bus,简称 I2C 或 I²C(读作 “I squared C”)。
- 由 飞利浦公司(Philips) 开发,是一种 通用同步半双工串行通信协议,用于单片机与外部模块通信。
- 应用场景
- 支持模块:
- MPU6050(陀螺仪加速度传感器);
- OLED 模块(显示模块);
- AT24C02(EEPROM 存储器);
- DS3231(实时时钟模块)等。
- 优势:跨设备兼容性强,学会一种模块操作后,其他 I2C 设备易于上手。
- 支持模块:
二、I2C 硬件引脚与电路特性
- 标志性引脚
- SCL(Serial Clock):串行时钟线,只能由主机控制,同步数据传输时序。
- SDA(Serial Data):串行数据线,半双工模式,主机与从机分时控制。
- 电路连接特点
- 总线结构:所有设备的 SCL、SDA 线分别并联,通过 4.7kΩ 上拉电阻 默认拉高至逻辑高电平。
- 开漏输出模式
- 引脚配置为开漏输出(低电压输出0,高电压显示高阻态由于上拉电阻,显示弱上拉),配合上拉电阻实现 “线与” 特性(任意设备拉低总线为低电平,所有设备释放时总线为高电平)。
- 优势:避免短路,支持软件模拟时序(如 51 单片机无硬件 I2C 时可用 GPIO 模拟)。
三、I2C 协议核心特点
- 同步半双工通信
- 同步时序:通过 SCL 协调传输,主机可随时暂停(时钟线暂停即同步暂停),适合低速场景。
- 半双工:SDA 同一时刻仅单向传输,分时复用实现双向通信,节省引脚。
- 数据应答机制
- 每传输一个字节后,接收方返回应答位(ACK)
- 0(ACK):接收成功,继续传输;
- 1(NACK):接收失败或结束,释放总线。
- 每传输一个字节后,接收方返回应答位(ACK)
- 多设备挂载能力
- 一组多从模型(课程重点)
- 单主机主导总线,多从机被动响应;
- 主机通过 7 位 / 10 位设备地址 寻址,从机仅在被点名时激活(类似 “老师点名 - 学生应答”)。
- 多主多从模型(进阶):支持多主机竞争总线,通过仲裁解决冲突,协议复杂,课程未深入。
- 一组多从模型(课程重点)
四、I2C 设备寻址与地址结构
- 设备地址(从机地址)
- 7 位地址模式(常用)
- 高 4-6 位由厂商固定(如 MPU6050 为
1101000),低 1-3 位可通过硬件引脚(如 AD0、A0)配置。 - 示例:MPU6050 的 AD0 接地时地址为
0x68(1101000),接高电平时为0x69(1101001)。
- 高 4-6 位由厂商固定(如 MPU6050 为
- 10 位地址模式:支持更多设备,应用较少。
- 7 位地址模式(常用)
- 读写标志位
- 设备地址后紧跟1 位读写标志位
- 0:主机写入数据;
- 1:主机读取数据。
- 组合为 8 位寻址字节(如
0xD0表示0x68+ 写操作,0xD1表示0x68+ 读操作)。
- 设备地址后紧跟1 位读写标志位
五、I2C 与其他通信协议对比
| 特性 | I2C | 串口(UART) |
|---|---|---|
| 通信模式 | 同步半双工 | 异步全双工 |
| 引脚数量 | 2 根(SCL、SDA) | 2 根(TX、RX) |
| 时钟依赖 | 需时钟线(SCL) | 无需时钟线,依赖波特率 |
| 多设备支持 | 支持(地址寻址) | 不支持(需硬件切换从机) |
| 软件模拟难度 | 低(同步时序灵活) | 高(异步时序严格依赖 timing) |
| 典型应用 | 短距离、低速、多设备场景 | 点对点、中高速场景 |
六、I2C 的优势与应用价值
- 硬件简单:仅需两根信号线,节省单片机引脚。
- 软件灵活:可通过硬件外设或软件模拟(如 GPIO 翻转)实现。
- 标准化协议:广泛兼容传感器、存储器等模块,降低开发成本。
- 适合下沉市场:对硬件要求低,可在低端单片机(如 51、STM32F103)运行。
总结
I2C 总线通过 双线制、同步时序、应答机制和多设备寻址,成为嵌入式系统常用通信协议。其核心优势为 简洁性、灵活性和跨设备兼容性,尤其适合单片机与传感器的低速通信。
硬件电路
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
主机与从机的权利
-
主机:控制 SCL(串行时钟线) 的时钟信号,在总线空闲时还可控制 SDA(串行数据线)。
-
从机:仅能被动读取 SCL 信号,仅在特定情况下(如数据应答时)可短暂获取 SDA 的控制权,其余时间为接收状态。
-
总线连接:所有设备的 SCL 引脚 相互连接,所有设备的 SDA 引脚 相互连接,形成共享的总线结构。
硬件接线要求
- 上拉电阻:SDA 和 SCL 需通过 4.7kΩ 左右的上拉电阻 连接至电源(如 +VDD),确保总线在空闲状态下保持高电平。
- 引脚模式:所有设备的 SCL 和 SDA 引脚需配置为 开漏输出模式,以实现 “线与” 特性(任意设备拉低总线为低电平,所有设备释放时总线为高电平)。
开漏输出模式的优势
- 避免电源短路:当多个设备同时操作总线时,开漏输出可防止不同设备间的电源冲突,保护电路安全。
- 减少引脚模式切换:无需频繁在输入与输出模式间切换,简化硬件设计与软件控制。
- 支持多主机模式:在多主机竞争总线时,开漏输出便于通过 “线与” 特性实现 时钟同步 和 总线仲裁,确保通信有序进行。

I2C时序基本单元
| 时序单元 | 详细描述 |
|---|---|
| 起始条件 | - 在 SCL 保持高电平期间,SDA 信号由高电平向低电平跳变。 - 标志着 I2C 通信的开始,仅由主机发起。 |
| 终止条件 | - 在 SCL 保持高电平期间,SDA 信号由低电平向高电平跳变。 - 标志着 I2C 通信的结束,仅由主机发起。 |
| 发送一个字节 | - 主机在 SCL 低电平期间,将数据位依次放置到 SDA 线上(高位在前)。 - SCL 高电平时,从机读取 SDA 数据,每 8 个时钟周期完成一个字节的发送。 |
| 接收一个字节 | - 从机在 SCL 低电平期间,将数据位依次放置到 SDA 线上(高位在前)。 - SCL 高电平时,主机读取 SDA 数据,每 8 个时钟周期完成一个字节的接收。 |
| 发送应答 | - 主机接收完一个字节后,在后续时钟周期中,若拉低 SDA 线,表示发送应答信号(ACK,确认接收成功); - 若保持 SDA 为高电平,则表示发送非应答信号(NACK)。 |
| 接收应答 | - 主机发送完一个字节后,释放 SDA 控制权。 - 在 SCL 高电平期间,若从机拉低 SDA,主机检测到低电平,即为接收到应答信号(ACK),表示从机成功接收数据; - 若 SDA 保持高电平,主机检测到高电平,即为接收到非应答信号(NACK),表示从机接收数据失败或通信结束。 |




I2C时序
指定地址写
对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
- 步骤
- 主机发送 起始条件。
- 主机发送 从机设备地址(写标志,即地址 + 0),等待从机 应答(ACK)。
- 主机发送 目标寄存器地址,等待从机 应答(ACK)。
- 主机发送 数据,等待从机 应答(ACK)。
- 主机发送 终止条件,结束通信。
- 作用:向从机指定寄存器写入数据。

当前地址读
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
- 步骤
- 主机发送 起始条件。
- 主机发送 从机设备地址(读标志,即地址 + 1),等待从机 应答(ACK)。
- 从机发送 当前指针指向的寄存器数据,主机接收后发送 非应答(NACK)(告知从机停止发送)。
- 主机发送 终止条件,结束通信。
- 作用:读取从机当前指针指向寄存器的数据。

指定地址读
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
- 步骤
- 先执行 指定地址写的前两步:发送起始条件 → 发送设备地址(写标志) → 等待应答。
- 发送 目标寄存器地址,等待从机应答(ACK)。
- 主机再次发送 起始条件(重复起始)。
- 发送 设备地址(读标志),等待从机应答(ACK)。
- 从机发送 指定寄存器数据,主机接收后发送 非应答(NACK)。
- 主机发送 终止条件,结束通信。
- 作用:先指定寄存器地址,再读取该寄存器的数据,是 “写 + 读” 的组合操作。

自动递增规则
-
适用场景
- 当通过 当前地址读 或 指定地址写 操作访问寄存器后,地址指针会自动递增 1,指向下一个寄存器地址。
- 示例
- 若向地址
0x19写入数据,操作完成后指针自动变为0x1A; - 若从当前指针指向的
0x1A读取数据,操作完成后指针自动变为0x1B。
- 若向地址
-
原理
- I2C 设备内部维护一个 当前地址指针,用于记录最后一次访问的寄存器位置。
- 每次读写操作(无论字节数多少)完成后,指针会根据数据长度自动递增(如读写 1 字节则递增 1,读写 N 字节则递增 N)。
-
关键说明
-
指定地址读
操作分为两步:
- 先通过 “写” 操作指定目标地址(此时指针指向该地址,不递增);
- 再通过 “读” 操作获取数据,此时指针在读取后 自动递增。
-
若需要连续访问多个寄存器(如批量读写),可利用此特性避免重复指定地址,提高效率。
-
软件模拟I2C的基本时序(代码实现)
//MyI2C
#include "stm32f10x.h" // Device header
#include "Delay.h"
//初始化GPio口,因为实现的是软件I2C,手动翻转电平模拟I2C,所以可以随便使用GPIO口
void MyI2C_Init(void)
{
//初始化GPio
//开启gpio时钟信号
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//初始化gpioinit参数中的结构体
GPIO_InitTypeDef GPIO_InitStructure;
//将gpio口设置为开漏输出,因为有线与的性质,开漏输出不只是可以输出,
//还可以输入,输入数据1读取输入数据寄存器就可以了
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
//初始化两个端口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//初始化完成将GPIO口,置一,因为I2c的初始条件以高电平为基础
GPIO_SetBits(GPIOB, GPIO_Pin_10|GPIO_Pin_11);
}
void MyI2C_W_SCL(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction) Bitvalue);
//因为I2c有时序的要求最好延时10us
Delay_us(10);
}
void MyI2C_W_SDA(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction) Bitvalue);
//因为I2c有时序的要求最好延时10us
Delay_us(10);
}
//还要编写读GPIO的函数
uint8_t MyI2C_R_SCL(void)
{
uint8_t Bitvalue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10);
//因为I2c有时序的要求最好延时10us
Delay_us(10);
return Bitvalue;
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t Bitvalue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
//因为I2c有时序的要求最好延时10us
Delay_us(10);
return Bitvalue;
}
//初始化I2C的基本单元
void MyI2C_Start(void)
{
//参数设置比较麻烦 可以使用 宏定义 无参宏 和 有参宏,传参数然后对宏中的参数进行定义,还可以使用对函数进行封装的方式
//这里使用封装函数的方法
//开始配置起始条件
//起始条件先将 SDA和SCl都 释放 就是都输出高电平
//为了以防万一,重复起始条件显示为SDA先释放,所以想要起始条件兼容重复起始条件,先释放SDA
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
//起始条件为当SCL处于高电平时,SDA拉低 ,SCL也随后拉低
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
void MyI2C_End(void)
{
//终止条件为在SCL处于高电平,SDA从低电平转换为高电平,
//因为只有在低电平时SDA才可以变化,所以可以理解为在玩木头人时,别人在数完123(高电平)时,有人动了,说明不玩了
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
void MyI2C_SendByte(uint8_t Byte)
{
//除了开始条件SCL以高电平结束其他都是低电平结束,方便拼接
//初始条件SCL为低电平,可以变换数据
//变换数据为高位先行
for(uint8_t i = 0; i < 8; i++)
{
//SCL低位放数据
MyI2C_W_SDA(Byte & (0x80 >> i));
//SCL高位读数据
MyI2C_W_SCL(1);
//释放准备下次读数据
MyI2C_W_SCL(0);
}
}
uint8_t MyI2C_ReadByte(void)
{
uint8_t byte = 0x00;
//此时SCL为低电平,主机释放SDA,从机就可以放置数据,然后SCL高电平主机读取数据
MyI2C_W_SDA(1);
//主机读取数据从高位开始读取
for(uint8_t i = 0; i < 8; i++)
{
MyI2C_W_SCL(1);
if(MyI2C_R_SDA() == 1)
{
byte = byte | (0x80>>i);
}
//不能把拉低函数放在if中,如果不是1,就不能及时的拉低,会影响时序
//释放从机可以再次放置数据
MyI2C_W_SCL(0);
}
return byte;
}
//发送应答,与发送一个字节的区别就是发一位
void MyI2C_SendAck(uint8_t BitAck)
{
//SCL低位放数据
MyI2C_W_SDA(BitAck);
//SCL高位读数据
MyI2C_W_SCL(1);
//释放准备下次读数据
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReadAck(void)
{
uint8_t Ackbit;
//此时SCL为低电平,主机释放SDA,从机就可以放置数据,然后SCL高电平主机读取数据
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
Ackbit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return Ackbit;
}
更多推荐



所有评论(0)