起因

STM32 项目里经常要用 I2C 接 EEPROM、传感器、触摸芯片这类低速从设备,但硬件 I2C 外设并不总是可用:引脚被其他功能占用了、目标引脚不在硬件 I2C 的复用表里,或者项目要在多个系列之间移植。这时就需要用普通 GPIO 手动模拟 I2C 时序,也就是常说的软件模拟 I2C。

软件模拟 I2C 移植性及稳定性更好(不依赖具体型号的外设和 DMA 配置),代价是要自己保证时序正确。

需求

  • 用两个普通 GPIO(本文 PB8=SCL、PB9=SDA)模拟出标准 I2C 主机时序
  • 不依赖具体芯片型号,在 F1/F4 等系列间可移植
  • 能驱动 24CXX 系列 EEPROM

技术实现原理

I2C 总线是什么

I2C 英文名为 Inter-Integrated Circuit,是用于多设备通讯的两线式串行总线,分为主机 Master 和从机 Slave,通常有一个主机和多个从机,从机之间可通过地址进行区分,不同种类的设备地址不同。

两根线一根是时钟线 SCL,一根是数据线 SDA。时钟始终由主机产生,主机通过起始条件和停止条件"框住"一次传输,从机在收到每个字节后回一个应答位。

为什么必须"开漏输出 + 外部上拉"(重点)

GPIO 模拟 I2C 通信的时候,输出一定要设置为开漏输出、外部上拉。这不是可选项,而是 I2C 物理层的规定:

  • 推挽输出内部有上、下两个 MOS 管,输出 1 时由上管把引脚"硬推"到电源,输出 0 时由下管拉到地;
  • 开漏输出只有下管,输出 0 时拉低,输出 1 时下管关闭、引脚悬空,电平由外部上拉电阻决定。

开漏 + 上拉带来一个关键性质——线与(Wired-AND):任何一台设备把线拉低,整根线就是低;所有设备都释放,线才是高。I2C 的三个机制都建立在它上面:

  1. ACK 应答:主机发完 8 位后必须"释放" SDA(输出 1 或切成输入),从机才能拉低表示应答。如果主机是推挽输出 1、从机同时拉低,就形成电源到地的短路,轻则数据错乱,重则烧毁 IO 口;
  2. 时钟拉伸(clock stretching):从机来不及处理时可以把 SCL 拉低,逼主机等待。主机只有释放 SCL 后回读,才知道从机有没有拉线;
  3. 多主机仲裁:多主机同时抢总线时,靠线与加回读判断谁先退出。

时序规则(软件模拟要模仿的就是这几条)

  • 空闲:SCL、SDA 都为高;
  • 起始条件:SCL 为高期间,SDA 由高变低;
  • 停止条件:SCL 为高期间,SDA 由低变高;
  • 数据有效性:SCL 为低期间允许 SDA 变化,SCL 为高期间 SDA 必须保持稳定(接收方在 SCL 高电平采样);
  • 应答:每传完 8 位,第 9 个时钟由接收方控制 SDA,拉低 = ACK,保持高 = NACK,发送器收到 NACK 就不再发下一字节;
  • 速率:由 SCL 频率决定,常用 100kHz(标准模式)/ 400kHz(快速模式),体现在代码里就是 delay_us 延时的长短。

2.代码实现(方式一):推挽输出 + SDA 方向切换(F4 标准库)

2.1 I2C初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*******************************************************************************
* 函 数 名         : IIC_Init
* 函数功能		   : IIC初始化
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);//使能 GPIOB 时钟
	
	//GPIOB8,B9初始化设置
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
	IIC_SCL=1;
	IIC_SDA=1;	
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//IO操作函数	 
#define IIC_SCL    PBout(8) //SCL
#define IIC_SDA    PBout(9) //SDA	 
#define READ_SDA   PBin(9)  //输入SDA


/*******************************************************************************
* 函 数 名         : SDA_OUT
* 函数功能		   : SDA输出配置	   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void SDA_OUT(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	//GPIOB9初始化设置
	GPIO_InitStructure.GPIO_Pin =GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
}

/*******************************************************************************
* 函 数 名         : SDA_IN
* 函数功能		   : SDA输入配置	   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void SDA_IN(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	//GPIOB9初始化设置
	GPIO_InitStructure.GPIO_Pin =GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;//输入模式
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
}

2.2 发送IIC开始信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*******************************************************************************
* 函 数 名         : IIC_Start
* 函数功能		   : 产生IIC起始信号   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Start(void)
{
	SDA_OUT();     //sda线输出
	IIC_SDA=1;	  	  
	IIC_SCL=1;
	delay_us(5);
 	IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
	delay_us(6);
	IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
}	

2.3 发送IIC停止信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*******************************************************************************
* 函 数 名         : IIC_Stop
* 函数功能		   : 产生IIC停止信号   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Stop(void)
{
	SDA_OUT();//sda线输出
	IIC_SCL=0;
	IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
 	IIC_SCL=1; 
	delay_us(6); 
	IIC_SDA=1;//发送I2C总线结束信号
	delay_us(6);							   	
}

2.4 IIC发送一个字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*******************************************************************************
* 函 数 名         : IIC_Send_Byte
* 函数功能		   : IIC发送一个字节 
* 输    入         : txd:发送一个字节
* 输    出         : 无
*******************************************************************************/		  
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {              
        if((txd&0x80)>0) //0x80  1000 0000
			IIC_SDA=1;
		else
			IIC_SDA=0;
        txd<<=1; 	  
		delay_us(2);   //对TEA5767这三个延时都是必须的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }	 
} 

2.5 IIC读取一个字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*******************************************************************************
* 函 数 名         : IIC_Read_Byte
* 函数功能		   : IIC读一个字节 
* 输    入         : ack=1时,发送ACK,ack=0,发送nACK 
* 输    出         : 应答或非应答
*******************************************************************************/  
u8 IIC_Read_Byte(u8 ack)
{
	u8 i,receive=0;
	SDA_IN();//SDA设置为输入
    for(i=0;i<8;i++ )
	{
        IIC_SCL=0; 
        delay_us(2);
		IIC_SCL=1;
        receive<<=1;
        if(READ_SDA)receive++;   
		delay_us(1); 
    }					 
    if (!ack)
        IIC_NAck();//发送nACK
    else
        IIC_Ack(); //发送ACK   
    return receive;
}

2.6 IIC等待ACK信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*******************************************************************************
* 函 数 名         : IIC_Wait_Ack
* 函数功能		   : 等待应答信号到来   
* 输    入         : 无
* 输    出         : 1,接收应答失败
        			 0,接收应答成功
*******************************************************************************/
u8 IIC_Wait_Ack(void)
{
	u8 tempTime=0;
	SDA_IN();      //SDA设置为输入  
	IIC_SDA=1;
	delay_us(1);	   
	IIC_SCL=1;
	delay_us(1);	 
	while(READ_SDA)
	{
		tempTime++;
		if(tempTime>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;//时钟输出0 	   
	return 0;  
} 

2.7 IIC发送ACK信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*******************************************************************************
* 函 数 名         : IIC_Ack
* 函数功能		   : 产生ACK应答  
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Ack(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(5);
	IIC_SCL=0;
}

2.8 IIC不发送ACK信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/*******************************************************************************
* 函 数 名         : IIC_NAck
* 函数功能		   : 产生NACK非应答  
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/		    
void IIC_NAck(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(5);
	IIC_SCL=0;
}	

3.附:24CXX EEPROM 驱动函数

这组函数只调用上面的时序原语(Start / Send_Byte / Wait_Ack / Read_Byte / Stop),与时序的具体实现方式无关,方式一、方式二都能直接用。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*******************************************************************************
* 函 数 名         : AT24CXX_Init
* 函数功能		   : AT24CXX初始化
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void AT24CXX_Init(void)
{
	IIC_Init();//IIC初始化
}

/*******************************************************************************
* 函 数 名         : AT24CXX_ReadOneByte
* 函数功能		   : 在AT24CXX指定地址读出一个数据
* 输    入         : ReadAddr:开始读数的地址 
* 输    出         : 读到的数据
*******************************************************************************/
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{				  
	u8 temp=0;		  	    																 
    IIC_Start();  
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0XA0);	   //发送写命令
		IIC_Wait_Ack();
		IIC_Send_Byte(ReadAddr>>8);//发送高地址	    
	}
	else 
	{
		IIC_Send_Byte(0XA0+((ReadAddr/256)<<1));   //发送器件地址0XA0,写数据
	} 	   
	IIC_Wait_Ack(); 
    IIC_Send_Byte(ReadAddr%256);   //发送低地址
	IIC_Wait_Ack();	    
	IIC_Start();  	 	   
	IIC_Send_Byte(0XA1);           //进入接收模式			   
	IIC_Wait_Ack();	 
    temp=IIC_Read_Byte(0);		   
    IIC_Stop();//产生一个停止条件	    
	return temp;
}

/*******************************************************************************
* 函 数 名         : AT24CXX_WriteOneByte
* 函数功能		   : 在AT24CXX指定地址写入一个数据
* 输    入         : WriteAddr  :写入数据的目的地址 
					 DataToWrite:要写入的数据
* 输    出         : 无
*******************************************************************************/
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{				   	  	    																 
    IIC_Start();  
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0XA0);	    //发送写命令
		IIC_Wait_Ack();
		IIC_Send_Byte(WriteAddr>>8);//发送高地址	  
	}
	else 
	{
		IIC_Send_Byte(0XA0+((WriteAddr/256)<<1));   //发送器件地址0XA0,写数据
	} 	 
	IIC_Wait_Ack();	   
  IIC_Send_Byte(WriteAddr%256);   //发送低地址
	IIC_Wait_Ack(); 	 										  		   
	IIC_Send_Byte(DataToWrite);     //发送字节							   
	IIC_Wait_Ack();  		    	   
    IIC_Stop();//产生一个停止条件 
	delay_ms(10);	 
}

/*******************************************************************************
* 函 数 名         : AT24CXX_WriteLenByte
* 函数功能		   : 在AT24CXX里面的指定地址开始写入长度为Len的数据
					 用于写入16bit或者32bit的数据
* 输    入         : WriteAddr  :写入数据的目的地址 
					 DataToWrite:要写入的数据
					 Len        :要写入数据的长度2,4
* 输    出         : 无
*******************************************************************************/
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{  	
	u8 t;
	for(t=0;t<Len;t++)
	{
		AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
	}												    
}

/*******************************************************************************
* 函 数 名         : AT24CXX_ReadLenByte
* 函数功能		   : 在AT24CXX里面的指定地址开始读出长度为Len的数据
					 用于读出16bit或者32bit的数据
* 输    入         : ReadAddr   :开始读出的地址 
					 Len        :要读出数据的长度2,4
* 输    出         : 读取的数据
*******************************************************************************/
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{  	
	u8 t;
	u32 temp=0;
	for(t=0;t<Len;t++)
	{
		temp<<=8;
		temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1); 	 				   
	}
	return temp;												    
}

/*******************************************************************************
* 函 数 名         : AT24CXX_Check
* 函数功能		   : 检查AT24CXX是否正常
* 输    入         : 无
* 输    出         : 1:检测失败,0:检测成功
*******************************************************************************/
u8 AT24CXX_Check(void)
{
	u8 temp;
	temp=AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX			   
	if(temp==0x36)return 0;		   
	else//排除第一次初始化的情况
	{
		AT24CXX_WriteOneByte(255,0X36);
	    temp=AT24CXX_ReadOneByte(255);	  
		if(temp==0X36)return 0;
	}
	return 1;											  
}

/*******************************************************************************
* 函 数 名         : AT24CXX_Read
* 函数功能		   : 在AT24CXX里面的指定地址开始读出指定个数的数据
* 输    入         : ReadAddr :开始读出的地址 对24c02为0~255
					 pBuffer  :数据数组首地址
					 NumToRead:要读出数据的个数
* 输    出         : 无
*******************************************************************************/
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
	while(NumToRead)
	{
		*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);	
		NumToRead--;
	}
} 

/*******************************************************************************
* 函 数 名         : AT24CXX_Write
* 函数功能		   : 在AT24CXX里面的指定地址开始写入指定个数的数据
* 输    入         : WriteAddr :开始写入的地址 对24c02为0~255
					 pBuffer  :数据数组首地址
					 NumToRead:要读出数据的个数
* 输    出         : 无
*******************************************************************************/
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
	while(NumToWrite--)
	{
		AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
		WriteAddr++;
		pBuffer++;
	}
}

4.代码实现(方式二):开漏输出(F1 标准库,规范做法)

这是另一套实现,两个 GPIO 直接配置成 GPIO_Mode_Out_OD 开漏输出,不再需要 SDA_IN/SDA_OUT 来回切方向:

  • 输出 0 = 拉低总线,输出 1 = 释放总线(靠上拉电阻回高),天然满足线与;
  • 释放 SDA 就是 IIC_SDA_1,从机拉低不会和主机冲突,行为更接近硬件 I2C;
  • 读总线直接用 GPIO_ReadInputDataBit,开漏模式下输入电路一直可用。

这就是上文"重点"所说的规范接法。方式一的代码为什么也能跑通,见下一节对比。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#define IIC_SCL_1 GPIO_SetBits(GPIOB, GPIO_Pin_6) 
/* SCL = 1 */
#define IIC_SCL_0 GPIO_ResetBits(GPIOB, GPIO_Pin_6) 
/* SCL = 0 */
#define IIC_SDA_1 GPIO_SetBits(GPIOB, GPIO_Pin_7) 
/* SDA = 1 */
#define IIC_SDA_0 GPIO_ResetBits(GPIOB, GPIO_Pin_7) 
/* SDA = 0 */
#define IIC_READ_SDA() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) 
/* 读SDA口线状态 */
//初始化IIC
void IIC_Init(void) {
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;
	//开漏输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	IIC_Stop();
}
//产生IIC起始信号
//SCL为高电平时SDA由高变低
void IIC_Start(void) {
	IIC_SDA_1;
	IIC_SCL_1;
	delay_us(4);
	IIC_SDA_0;
	delay_us(4);
	IIC_SCL_0;
}
//产生IIC停止信号
//SCL为高电平时SDA由低变高
//IIC空闲时SCL和SDA均输出高电平,这样不会干扰其他设备的收发
void IIC_Stop(void) {
	IIC_SDA_0;
	IIC_SCL_1;
	delay_us(4);
	IIC_SDA_1;
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
uint8_t IIC_WaitAck(void) {
	uint8_t errCount = 0;
	uint8_t ack = 0;
	IIC_SDA_1;
	delay_us(4);
	IIC_SCL_1;
	delay_us(4);
	while(IIC_READ_SDA()) {
		errCount++;
		if(errCount > 250) {
			ack = 1;
			break;
		}
	}
	IIC_SCL_0;
	return ack;
}
//产生应答ACK
//SCL为高电平时SDA为低电平表示应答
void IIC_Ack(void) {
	IIC_SDA_0;
	delay_us(4);
	IIC_SCL_1;
	delay_us(4);
	IIC_SCL_0;
	delay_us(4);
	IIC_SDA_1;
	//释放SDA
}
//产生非应答NACK
//SCL为高电平时SDA为高电平表示非应答
void IIC_NAck(void) {
	IIC_SDA_1;
	delay_us(4);
	IIC_SCL_1;
	delay_us(4);
	IIC_SCL_0;
	delay_us(4);
}
//IIC发送一个字节
void IIC_WriteByte(uint8_t txd) {
	uint8_t i;
	IIC_SCL_0;
	for (i = 0; i < 8; i++) {
		(txd & 0x80) ? IIC_SDA_1 : IIC_SDA_0;
		txd <<= 1;
		delay_us(4);
		IIC_SCL_1;
		delay_us(4);
		IIC_SCL_0;
		delay_us(4);
	}
	IIC_SDA_1;
}
//读1个字节,ack=1时,发送ACK,ack=0,发送NACK
uint8_t IIC_ReadByte(uint8_t ack) {
	uint8_t i, rcv = 0;
	for (i = 0; i < 8; i++) {
		rcv <<= 1;
		IIC_SCL_1;
		delay_us(4);
		if(IIC_READ_SDA()) {
			rcv++;
		}
		IIC_SCL_0;
		delay_us(4);
	}
	ack ? IIC_Ack() : IIC_NAck();
	return rcv;
}

5.两种方式对比:为什么"重点"说必须开漏,方式一却是推挽

细心的读者会发现,方式一的初始化里配的是 GPIO_OType_PP(推挽),和上文"必须开漏"的说法看起来矛盾。它能工作的原因是:

  • 方式一在需要从机驱动 SDA 的所有窗口(等待 ACK、读数据)之前,都先调用 SDA_IN() 把 SDA 切成了输入,主机不再驱动这根线,避开了最主要的电平竞争;
  • 单主机、从机又不做时钟拉伸的场景下,SCL 由主机独占,推挽输出不会冲突。

但风险仍在:从机做时钟拉伸拉低 SCL 时,主机 SCL 推挽输出高,两边顶牛就是短路;多主机场景更不可用。所以:

方式一(推挽+方向切换) 方式二(开漏+上拉)
GPIO 配置 GPIO_OType_PP,读前切输入 GPIO_Mode_Out_OD,方向不变
释放总线 切成输入 输出 1
时钟拉伸 有短路风险 天然支持
多主机仲裁 不可用 支持
代码量 需要 SDA_IN/SDA_OUT 一对函数 更简洁

结论:方式一能跑通,但新项目建议直接按方式二配置开漏输出。

6.踩坑记录与注意事项

  • 上拉电阻:3.3V 系统典型 4.7kΩ,400kHz 时可减小到 2.2k~3.3kΩ;只靠内部上拉(约 40kΩ)波形上升沿太缓,高速率时不稳定;
  • 延时:delay_us 的数值决定 SCL 频率,换主频、换延时函数后要重新核对(半周期 4µs 左右 ≈ 125kHz);
  • 等 ACK 前先释放 SDA:方式一里 IIC_Wait_Ack()SDA_IN(),方式二里先 IIC_SDA_1,漏了这一步读到的永远是 0;
  • 超时保护IIC_Wait_Ack() 里 tempTime>250 就 Stop 返回,防止从机异常时主程序死等;
  • 24CXX 写入后要 delay_ms(10):EEPROM 内部写周期约 5~10ms,写完立即回读会失败;
  • 先确认从机地址:24CXX 是 0xA0(写)/ 0xA1(读),很多"模拟 I2C 不通"最后查出来是地址或接线错了。