一、简介
1、I2C(Inter-Integraed Circuit,集成电路间总线),飞利浦1982年推出的一种通用数据总线;
同步半双工、串行、多主从总线,只用2根信号线即可挂载多个设备(一主多从、多主多从),广泛用于传感器、OLED、EEPROM、RTC、AD/DA等低速外设。

2、两根核心信号线:所有I2C设备的SCL连在一起,SDA连一起
1)SDA:Serial Data,数据线,双向传输数据;
2)SCL:Serial Clock,时钟线,由主机产生同步时钟;
3、硬件关键:上拉电阻
SDA、SCL都是开漏输出,只能拉低电平,无法主动输出高电平;
必须外接4.7~10kΩ的上拉电阻到电源(3.3V/5V);
总线空闲时,SDA、SCL都是高电平。
必须开漏原因:I2C的SDA、SCL都是共享双向总线,多个设备并联在同一根线上,不能强行输出高电平,只能拉低,只有开漏能实现:只拉低、不主动拉高的电气特性。
推挽输出VS开漏输出
推挽:内部两个MOS管:上管接VCC,下管接GND。输出高电平:上管导通→强行把信号线拉到VCC;
输出低电平:下管导通→强行拉到GND;
致命问题:总线上两个设备同时控制一根线时会打架:设备A输出高(强制VCC),设备B输出低(强制GND),电源直接对地短路→大电流烧坏芯片IO口。
开漏输出:内部只有下拉MOS管,没有上拉管;
输出低:下拉管导通→信号线拉到GND;
输出高:下拉管断开→IO浮空高阻,不控制这条线。所以总线电平完全靠外部上拉电阻维持,芯片不能主动强行拉高。
4、电气电平兼容
标准:5V I2C,现在主流3.3V。可通过电平转换模块实现5V→3.3V。
5、时钟频率
标准模式:100KHz,通用低速外设;
快速模式:400KHz,大多数传感器、屏幕等外设;
一句话本质:SCL提供节拍,SDA在时钟高/低电平的时候改变数据,一问一答通信。
I2C铁律:数据只有SCL低的时候才能变;SCL高的时候SDA变化,只能用来做起始和停止,不能传输数据。
二、寻址机制
1、地址格式与寻址原理
I2C总线通过唯一的地址来区分挂载在同一总线上的不同从设备。每个从设备在出厂时都会被分配一个固定的7位或10位地址,主机在发起通信时,首先发送目标从机的地址,只有地址匹配的从机才会响应后续的数据传输。这种机制允许多个设备共享同一对信号线(SDA和SCL),实现了简洁的总线拓扑。
一条I2C总线理论上最多可挂载的设备数量由地址位数决定:
- 7位地址模式 :这是最常用的模式。7位地址范围是0x00到0x7F(二进制0000000到1111111),共128个地址。但其中0x00 (0000000) 被保留为广播地址,0x01到0x07 (0000001到0000111) 以及0x78到0x7F (1111000到1111111) 被保留用于特殊用途(如10位地址扩展、CBUS兼容等)。因此,实际可用的普通从机地址约为112个。在完整的I2C数据帧中,地址字节由7位地址和1位读写方向位(R/W)组成,共8位(1字节)。
- 10位地址模式:为满足更多设备的需求,I2C协议扩展了10位地址模式。其地址范围是0x000到0x3FF,共1024个地址。10位地址的传输需要两个字节:第一个字节的高5位是固定的11110,后两位是10位地址的最高两位(A9, A8),加上读写位;第二个字节是地址的低8位(A7-A0)。由于寻址过程更复杂,且大多数应用场景112个地址已足够,因此10位地址在实际项目中较少使用。
2、7位地址帧结构详解
一个完整的7位地址数据帧结构如下:
plaintext
| 7位从机地址 (A6-A0) | 读写位 (R/W) |
|---------------------|-------------|
| MSB First | 0=写, 1=读 |
读写位 (R/W Bit):这是地址字节的第8位(最低位LSB)。
- 0 (写模式):表示主机将要向从机写入数据。
- 1 (读模式):表示主机将要从从机读取数据。
示例:从机地址0x48的读写指令
假设一个从设备(如某个温度传感器)的7位硬件地址是0x48(二进制0100 1000)。
- 主机写指令 :主机需要向该传感器写入配置寄存器。
计算:地址0x48左移1位(0x48 << 1 = 0x90),然后与写位0进行或操作(0x90 | 0 = 0x90 )。
因此,主机发送的地址字节是0x90(二进制1001 0000),其中高7位0100100是地址,最低位0表示写操作。 - 主机读指令 :主机需要从该传感器读取数据。
计算:地址0x48左移1位(0x90),然后与读位1进行或操作(0x90 | 1 = 0x91 )。
因此,主机发送的地址字节是0x91(二进制1001 0001),最低位1表示读操作。
在代码中,我们通常使用宏定义或移位操作来组合地址和读写位:
c
#define DEVICE_ADDR 0x48
#define WRITE_CMD ((DEVICE_ADDR << 1) | 0) // 0x90
#define READ_CMD ((DEVICE_ADDR << 1) | 1) // 0x91
3、ACK/NACK应答机制详解
ACK(Acknowledge)应答是I2C协议保证数据可靠传输的核心机制。其规则如下:
- 时机 :在每成功传输一个字节(8位数据)后 的第9个时钟周期,由数据的接收方发出应答信号。
- ACK信号(应答):接收方(无论是主机还是从机)在SCL的第9个高电平期间,将SDA线拉低(输出低电平),表示"已成功收到上一个字节,请继续发送"。
- NACK信号(非应答):接收方在第9个时钟周期保持SDA线为高电平(不拉低),表示"未成功接收或不再需要数据"。
- 物理过程:在第9个时钟周期,SCL由主机拉高。在此期间,发送方会释放SDA线(变为高阻态),而接收方则控制SDA线电平。若接收方拉低SDA,则总线呈现低电平,即ACK;若接收方不拉低,则上拉电阻将SDA维持在高电平,即NACK。主机在SCL高电平期间采样SDA线状态,以判断应答结果。
NACK出现的常见原因:
- 从机设备不存在:主机发送的地址在总线上无对应设备。
- 地址错误:主机发送的地址与从机硬件地址不匹配。
- 从机忙:从机正在处理其他任务,无法及时响应。
- 通信结束:主机在读取多个字节后,发送NACK告知从机停止发送数据。
- 从机故障:从机硬件损坏或电源异常。
4、广播地址与特殊地址
如前所述,部分地址被协议保留用于特殊功能:
- 广播地址 (0x00):主机可以向地址0x00发送数据,总线上所有从机都会接收。这用于同时配置多个同型号设备或发送系统复位命令等场景,但使用较少。
- CBUS地址 (0x01, 0x02):用于与旧的CBUS兼容设备通信。
- 保留地址 (0x03-0x07, 0x78-0x7F):为未来协议扩展或其他用途保留,不应分配给普通从机。
在实际选型和电路设计时,需要查阅器件数据手册,确认其使用的具体I2C地址,并注意避免地址冲突。
三、标准通讯时序
1、起始信号Start
SCL高电平期间,SDA由高→低,总线启动(告诉从设备,准备通信)。规则:SCL高,SDA跳变(起始/停止),SCL低时,才能切换SDA传输数据;
2、停止信号Stop
SCL高电平期间,SDA由低→高,通信结束;

3、数据传输
1)发送一个字节:
SCL拉低→主机改变SDA电平(0/1),高位先行;
SCL拉高→从机读取SDA电平;
所以SCL高电平期间,SDA不允许有数据变化,依次循环上述8次,即可发送一个字节。
即:低电平主机放数据,高电平从机读数据。

2)接收一个字节
SCL低电平,从机将数据位依次放SDA线,高位先行;
释放SCL,主机在SCL高电平期间读取数据位;
SCL高电平期间SDA不允许有数据变化,依次循环上述8次,即可接收一个字节,但主机在接收之前,需要释放SDA。
即:低电平从机放数据,高电平主机读数据

3)发送和接收应答
发送应答:主机接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答;
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,数据释放SDA)。
注:发送和接收应答,都是相对主机而言
4、完整写时序(主机写1字节到从机寄存器)
1)发送Start起始;
2)发送从机7位地址+写位(R/W=0);
3)等待从机ACK应答;
4)发送寄存器地址;
5)等待ACK;
6)发送要写入的数据字节;
7)等待ACK;
8)发送Stop停止;
5、完整读时序(读取从机寄存器数据)
I2C读分为2步:先指定寄存器,再切换读模式;
1)start
2)从机地址+写位,发送寄存器地址,ACK;
3)重复起始信号;
4)从机地址+读位(R/W=1);
5)主机释放SDA,等待从机输出数据;
6)读取数据:
继续读:主机返回ACK;
读完最后一个字节:主机返回NACK告知从机停止输出;
7)Stop停止;
四、两种实现方式
1、硬件I2C(STM32内置外设)
优点:硬件自动生成SCL时序,CPU占用低,速度稳定,支持DMA;
缺点:引脚固定,资源有限,配置较复杂;
适用:大量、高频I2C读写(屏幕、多传感器);
2、软件模拟
优点:任意2个GPIO口都能当SDA/SCL,灵活,跨芯片通用;
缺点:占用CPU,速度慢,中断会打断时序导致通讯异常;
适用:低速、简答外设、I2C引脚被占用时替代;
五、代码部分
采用软件模拟的方式,根据I2C时序,写I2C函数:
起始:MyI2C_Start();结束:MyI2C_Stopt();
主机发送一个字节:MyI2C_SendByte(uint8_t Byte);
主机接收一个字节:uint8_t MyI2C_ReceiveByte();
主机发送应答:MyI2C_SendAck(uint8_t AckBit);
主机接收应答:MyI2C_ReceiveAck();
1、MyI2C.c文件
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#define SCL_PORT GPIOB
#define SCL_PIN GPIO_Pin_10
#define SDA_PIN GPIO_Pin_11
/**
@brief BitValue设1或0就可以释放或者拉低SCL,也可在函数里加入延时函数,延时操作
@param
@retval
*/
void MyI2C_W_SCL(uint8_t BitValue)//w表示写
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
Delay_us(5);
}
void MyI2C_W_SDA(uint8_t BitValue)//w表示写
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
Delay_us(5);
}
/**
@brief 读取SDA的电平
@param
@retval
*/
uint8_t MyI2C_R_SDA()
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(5);
return BitValue;
}
void MyI2C_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructrue;
GPIO_InitStructrue.GPIO_Mode = GPIO_Mode_Out_OD;//配置成开漏输出,同时也可以输入
GPIO_InitStructrue.GPIO_Pin =GPIO_Pin_10 |GPIO_Pin_11;
GPIO_InitStructrue.GPIO_Speed =GPIO_Speed_50MHz ;
GPIO_Init(GPIOB,&GPIO_InitStructrue);
GPIO_SetBits(GPIOB,GPIO_Pin_10 |GPIO_Pin_11);
}
//先拉低SDA,再拉低SCL
void MyI2C_Start()
{
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
//除了终止条件,其他都会保证SCL以低电平结束,方便各个单元拼接
/停止/
void MyI2C_Stop()
{
MyI2C_W_SCL(0);
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
//主机发送
void MyI2C_SendByte(uint8_t Byte)
{
// MyI2C_W_SDA(Byte&0x80);//取出数据的最高位
uint8_t i;
for(i=0;i<8;i++)
{
MyI2C_W_SDA(Byte&(0x80>>i));//数据从高到低位,加载到SDA上
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
uint8_t MyI2C_ReceiveByte()
{
uint8_t i,Byte =0x00;
MyI2C_W_SDA(1);
for(i=0;i<8;i++)
{
MyI2C_W_SCL(1);
if(MyI2C_R_SDA()==1)
{
Byte |=(0x80>>i);
}
MyI2C_W_SCL(0);
}
return Byte;
}
//发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveAck()
{
uint8_t AckBit ;
MyI2C_W_SDA(1);//主机释放SDA
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
2、MyI2C.h文件
cpp
#ifndef __MYI2C_H
#define __MYI2C_H
void MyI2C_Init();
void MyI2C_Start();
void MyI2C_Stop();
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte();
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck();
#endif
3、main函数测试
以MPU6050,发送地址,看是否应答:0应答(代码正确),1非应答(代码不正确);
cpp
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "I2C.h"
int main()
{
OLED_Init();
MyI2C_Init();
MyI2C_Start();
MyI2C_SendByte(0xD0);
uint8_t ACK =MyI2C_ReveiveACK();
MyI2C_Stop();
OLED_ShowNum(1,1,ACK,2);
while(1)
{
}
}
经实测,变量ACK为0,代码正确。
六、适配I2C三大核心机制
1、多设备总线共享
一条I2C总线挂主机+个哥从机,所有SDA/SCL并联。
任何设备想发0:自己开漏拉低信号线;
任何设备想发1:自己高阻放手,由上拉电阻统一拉为高;
不存在一方强制拉高、另一方强制拉低短路冲突。
2、双向SDA数据
SDA是双向:主机发数据、从机回ACK/读取数据;
主机发送数据是:主机控制SDA电平;
从机应答ACK时:主机释放SDA(高阻),从机单独拉低SDA;如果是推挽,主机一直强制拉高,,,从机根本拉不低,ACK信号根本无法发出,通信失效。
3、多主机仲裁
I2C支持多个主机,仲裁逻辑:
主机发出1,但总线被另一台主机拉成0,本机检测到电平不一致,主动放弃总线。只有开漏架构,才能做到:多个主机可以独立拉低总线,但不能强制拉高",仲裁才能正常工作。
本文完!