STM32学习23--I2C-1

一、简介

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协议保证数据可靠传输的核心机制。其规则如下:

  1. 时机 :在每成功传输一个字节(8位数据)后 的第9个时钟周期,由数据的接收方发出应答信号。
  2. ACK信号(应答):接收方(无论是主机还是从机)在SCL的第9个高电平期间,将SDA线拉低(输出低电平),表示"已成功收到上一个字节,请继续发送"。
  3. NACK信号(非应答):接收方在第9个时钟周期保持SDA线为高电平(不拉低),表示"未成功接收或不再需要数据"。
  4. 物理过程:在第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&lt;8;i++)
{
	MyI2C_W_SCL(1);
	if(MyI2C_R_SDA()==1)
	{
		Byte |=(0x80&gt;&gt;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,本机检测到电平不一致,主动放弃总线。只有开漏架构,才能做到:多个主机可以独立拉低总线,但不能强制拉高",仲裁才能正常工作。

本文完!

相关推荐
red_redemption1 小时前
自由学习记录(217)
学习
小羊先生car1 小时前
F429-HAL-Can总线(回环及双机实验)(2026/8/15)
c语言·单片机·嵌入式硬件·软件构建
LYS_06182 小时前
Python学习(3)(数据类型小尾巴,条件控制,循环语句)
学习
迪丽热爱2 小时前
每日英语-12
学习
PC2005-cloud2 小时前
Elasticsearch 学习笔记:集群实战(3 控制节点 + 3 数据节点部署与故障转移)
笔记·学习·elasticsearch
嵌入式阿蔡2 小时前
STM32时钟树详解:配错一个全乱套
c语言·stm32·单片机·嵌入式硬件
世人万千丶2 小时前
物品借还闭环:鸿蒙物品清单种子数据与清单效果
学习·华为·harmonyos·鸿蒙
我命由我123452 小时前
商圈六大配套
学习·职场和发展·求职招聘·职场发展·产品经理·学习方法·零售
云水初2 小时前
【agent篇】RAG 知识库构建避坑指南
开发语言·python·学习·agent·rag