I2C

I2C的基本知识

一、I2C 基本定义与开发背景

  1. 全称与缩写
    • 英文全称:Inter-Integrated Circuit Bus,简称 I2CI²C(读作 “I squared C”)。
    • 飞利浦公司(Philips) 开发,是一种 通用同步半双工串行通信协议,用于单片机与外部模块通信。
  2. 应用场景
    • 支持模块:
      • MPU6050(陀螺仪加速度传感器);
      • OLED 模块(显示模块);
      • AT24C02(EEPROM 存储器);
      • DS3231(实时时钟模块)等。
    • 优势:跨设备兼容性强,学会一种模块操作后,其他 I2C 设备易于上手。

二、I2C 硬件引脚与电路特性

  1. 标志性引脚
    • SCL(Serial Clock):串行时钟线,只能由主机控制,同步数据传输时序。
    • SDA(Serial Data):串行数据线,半双工模式,主机与从机分时控制。
  2. 电路连接特点
    • 总线结构:所有设备的 SCL、SDA 线分别并联,通过 4.7kΩ 上拉电阻 默认拉高至逻辑高电平。
    • 开漏输出模式
      • 引脚配置为开漏输出(低电压输出0,高电压显示高阻态由于上拉电阻,显示弱上拉),配合上拉电阻实现 “线与” 特性(任意设备拉低总线为低电平,所有设备释放时总线为高电平)。
      • 优势:避免短路,支持软件模拟时序(如 51 单片机无硬件 I2C 时可用 GPIO 模拟)。

三、I2C 协议核心特点

  1. 同步半双工通信
    • 同步时序:通过 SCL 协调传输,主机可随时暂停(时钟线暂停即同步暂停),适合低速场景。
    • 半双工:SDA 同一时刻仅单向传输,分时复用实现双向通信,节省引脚。
  2. 数据应答机制
    • 每传输一个字节后,接收方返回应答位(ACK)
      • 0(ACK):接收成功,继续传输;
      • 1(NACK):接收失败或结束,释放总线。
  3. 多设备挂载能力
    • 一组多从模型(课程重点)
      • 单主机主导总线,多从机被动响应;
      • 主机通过 7 位 / 10 位设备地址 寻址,从机仅在被点名时激活(类似 “老师点名 - 学生应答”)。
    • 多主多从模型(进阶):支持多主机竞争总线,通过仲裁解决冲突,协议复杂,课程未深入。

四、I2C 设备寻址与地址结构

  1. 设备地址(从机地址)
    • 7 位地址模式(常用)
      • 高 4-6 位由厂商固定(如 MPU6050 为1101000),低 1-3 位可通过硬件引脚(如 AD0、A0)配置。
      • 示例:MPU6050 的 AD0 接地时地址为0x681101000),接高电平时为0x691101001)。
    • 10 位地址模式:支持更多设备,应用较少。
  2. 读写标志位
    • 设备地址后紧跟1 位读写标志位
      • 0:主机写入数据;
      • 1:主机读取数据。
    • 组合为 8 位寻址字节(如0xD0表示0x68+ 写操作,0xD1表示0x68+ 读操作)。

五、I2C 与其他通信协议对比

特性 I2C 串口(UART)
通信模式 同步半双工 异步全双工
引脚数量 2 根(SCL、SDA) 2 根(TX、RX)
时钟依赖 需时钟线(SCL) 无需时钟线,依赖波特率
多设备支持 支持(地址寻址) 不支持(需硬件切换从机)
软件模拟难度 低(同步时序灵活) 高(异步时序严格依赖 timing)
典型应用 短距离、低速、多设备场景 点对点、中高速场景

六、I2C 的优势与应用价值

  1. 硬件简单:仅需两根信号线,节省单片机引脚。
  2. 软件灵活:可通过硬件外设或软件模拟(如 GPIO 翻转)实现。
  3. 标准化协议:广泛兼容传感器、存储器等模块,降低开发成本。
  4. 适合下沉市场:对硬件要求低,可在低端单片机(如 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 引脚需配置为 开漏输出模式,以实现 “线与” 特性(任意设备拉低总线为低电平,所有设备释放时总线为高电平)。
开漏输出模式的优势
  1. 避免电源短路:当多个设备同时操作总线时,开漏输出可防止不同设备间的电源冲突,保护电路安全。
  2. 减少引脚模式切换:无需频繁在输入与输出模式间切换,简化硬件设计与软件控制。
  3. 支持多主机模式:在多主机竞争总线时,开漏输出便于通过 “线与” 特性实现 时钟同步总线仲裁,确保通信有序进行。

在这里插入图片描述

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)

  • 步骤
    1. 主机发送 起始条件
    2. 主机发送 从机设备地址(写标志,即地址 + 0),等待从机 应答(ACK)
    3. 主机发送 目标寄存器地址,等待从机 应答(ACK)
    4. 主机发送 数据,等待从机 应答(ACK)
    5. 主机发送 终止条件,结束通信。
  • 作用:向从机指定寄存器写入数据。

在这里插入图片描述

当前地址读

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

  • 步骤
    1. 主机发送 起始条件
    2. 主机发送 从机设备地址(读标志,即地址 + 1),等待从机 应答(ACK)
    3. 从机发送 当前指针指向的寄存器数据,主机接收后发送 非应答(NACK)(告知从机停止发送)。
    4. 主机发送 终止条件,结束通信。
  • 作用:读取从机当前指针指向寄存器的数据。

在这里插入图片描述

指定地址读

对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

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

在这里插入图片描述

自动递增规则
  1. 适用场景

    • 当通过 当前地址读指定地址写 操作访问寄存器后,地址指针会自动递增 1,指向下一个寄存器地址。
    • 示例
      • 若向地址 0x19 写入数据,操作完成后指针自动变为 0x1A
      • 若从当前指针指向的 0x1A 读取数据,操作完成后指针自动变为 0x1B
  2. 原理

    • I2C 设备内部维护一个 当前地址指针,用于记录最后一次访问的寄存器位置。
    • 每次读写操作(无论字节数多少)完成后,指针会根据数据长度自动递增(如读写 1 字节则递增 1,读写 N 字节则递增 N)。
  3. 关键说明

    • 指定地址读

      操作分为两步:

      1. 先通过 “写” 操作指定目标地址(此时指针指向该地址,不递增);
      2. 再通过 “读” 操作获取数据,此时指针在读取后 自动递增
    • 若需要连续访问多个寄存器(如批量读写),可利用此特性避免重复指定地址,提高效率。

软件模拟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;
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐