关于软件模拟IIC协议GPIO究竟使用开漏还是推挽输出(附代码)
软件模拟IIC的一些疑问
一、为什么选择软件模拟?
现在很多的开发板都支持硬件IIC,尤其笔者经常的接触的STM32,直接调用相关函数就可以完成数据传输而且问题少,不明白为什么不直接使用硬件,反而要自己写,不是会很麻烦。后来换了开发板,遇到一些bug就渐渐明白了原因了
1.可移植性强
并非所有开发板硬件IIC使用方法均一致,不同厂商之间的硬件IIC使用是不同的,比如TI开发板和STM开发板,在需要快速上手开发时软件模拟无疑是最好的选择,不论是什么型号的MCU均可以快速移植。
2.易于拓展
虽然一条IIC总线支持挂载多个主机与从机,但也有数量限制,挂载设备过多会导致信号噪声,严重影响数据传输。若硬件IIC少,就必须依靠软件模拟IIC扩容
二、推挽还是开漏?(SDA尽可能使用开漏)
网络上有很多模拟IIC的开源代码,但笔者十分疑惑为什么有人使用开漏输出,有人使用推挽输出。笔者早期学习的时候记得很清楚要使用开漏输出,但在实际操作中发现推挽输出也是可以进行软件IIC的。
1.开漏
开漏输出无法真正输出高电平,即高电平时没有驱动能力,必须进行外部上拉,需要借助外部上拉电阻完成对外驱动(开漏引脚不连接外部的上拉电阻时,只能输出低电平)。
开漏输出时,P-MOS截止,N-MOS导通,当输出数据寄存器输出0时,经过输出控制变为逻辑1,导通Vss,输出高电平。反之N-MOS截至,I/O引脚呈现高阻态。很显然可以看出芯片内部是无法输出高电平的,只能进行外接上拉电阻依靠外部电平输出高电平。
值得注意的是,开漏是不需要对GPIO口进行输入输出模式切换的,本身就可直接读取电平状况
开漏输出有一个优点,通过改变上拉电源的电压,便可以改变传输电平,比如STM32用3.3V供电,将GPIO设置为开漏输出模式,同时引脚外部接上拉电阻到5V,则高电平时可以拉到5V,不需要接特殊的电平转换电路或芯片
2.推挽
这里类比开漏简单介绍,推挽不需要依靠外部即可直接输出高低电平,但是推挽输出不能在输出状态下直接读取引脚状态,必须进行模式切换,转换为输入模式
PS:为什么呢,明明推挽输出时施密特触发器仍在工作?
解答:推挽输出是强输出电流模式,在此模式下的输出通道上的推挽结构MOS管,属于强上拉和强下拉的,这会影响读取输入数据寄存器的值,强上拉意味着会将来自外部的低电平输入强制置高,强下拉意味着会将来自外部的高电平输入强制置低
3.推挽可以模拟IIC吗
通过查阅资料以及实际操作,推挽是可以用作模拟IIC的,然而需要注意以下问题,并且实际使用体验较差,可能导致通信缓慢。
(1)无法实现线与,只能固定连接一个IIC设备
将多个开漏输出的IO口,连接到一条线上。通过一只上拉电阻,在不增加任何器件的情况下,形成“与逻辑”关系。这也是I2C,SMBus等总线判断总线占用状态的原理。
(2)注意连接的IIC设备输出电平大小,与IO口电平容忍情况
由于使用推挽时判断从机应答需要从机拉低/拉高SDA,也就是向IO输入电平,如果从机输出5V,而该IO口不能容忍5V很大概率会损坏IO口甚至MCU。
(3)多设备时序冲突问题
I2C被设计运用在多主机多从机的场景,所以不可避免地会遇到一个问题——总线冲突。
若因某种原因时序混乱导致一个IO口输出高电平,一个输出低电平,那么就会出现短路现象,有烧坏设备的风险。
4.注意事项
对于模拟IIC,可以SCL使用推挽输出进行模拟,SDA使用开漏输出,板载资源并非十分短缺则没有必要SDA使用推挽输出。尤其值得注意,只有单主-单从,即整条线上只有一个主机、一个从机,SCL不存在争抢控制权的情况下,使用推挽输出模拟SDA才没有问题。
5.简单介绍IIC
这里笔者还是简单介绍一下IIC,主要是加深印象。

IIC协议的数据传输主要依靠SCL和SDA两根线,其中SDA的数据(电平信号变化)仅在SCL高电平期间有效。需要注意,数据的传递与接收均为高位在前,低位在后。
- 开始信号(START/S): SCL为高时,SDA从高到低的跳变产生开始信号
- 结束信号(STOP/P): SCL为高时,SDA从低到高的跳变产生结束信号
并且在完成数据接收与发送后主机或者从机都要进行应答(第9位),确认接收成功。
应答信号分为两种:
1)当第9位(应答位)为 低电平 时,为 ACK 信号
2)当第9位(应答位)为 高电平 时,为 NACK 信号
值得注意的是,现在很多IIC设备均具有连续读取写入功能,即主机或从机发送 ACK信号后,被写入或读取的从机会进行地址自增,从而不在重新发送开始信号,很快的提高了效率。
四、IIC通信中的问题
对于想要查找IIC通信问题,必须借助逻辑分析仪查看,不然根本无法确认发送的信号是否正确。
地址错误(常见硬件IIC)
这种问题常见于OLED,例如OLED的IIC地址为0x78,但事实上使用HAL库的硬件IIC发送地址应该是0x3C | (读写位),原因在于IIC设备地址由8位两部分组成,前七位为固定地址,最后一位为读写位。而HAL库自带的IIC默认只需要固定地址,读写位HAL库会自动左移添加。所以我们需要人工右移IIC地址来补偿HAL库代码。
五、附代码
笔者通过定义相应结构体,允许不同的IIC接口调用同一套代码,减少重复工作。需要注意的是,GPIO_PinState为HAL库自带的枚举结构,需要自己根据实际修改
/**
* @file iic_gpio.h
* @brief 模拟I2C的功能实现
* @author 瀚海浮萍
* @date 8/16/2025
*/
#include "iic_gpio.h"
// 定义引脚和端口(SDA SCL)
Temp_IIC iic1 = {GPIOA,GPIO_PIN_9,GPIOA,GPIO_PIN_8};
/*-------------------------GPIO引入()-------------------------------*/
/* 若使用CubeMX配置完成则不需要初始化 */
void I2C_GPIO_Init(Temp_IIC *iic)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 初始化SCL引脚
GPIO_InitStruct.Pin = iic->T_SCL_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(iic->GPIO_SCL, &GPIO_InitStruct);
// 初始化SDA引脚
GPIO_InitStruct.Pin = iic->T_SDA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(iic->GPIO_SDA, &GPIO_InitStruct);
// 将SCL和SDA拉高
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SCL_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SDA_Pin, GPIO_PIN_SET);
}
static void Set_High_SCL(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SCL_Pin, GPIO_PIN_SET);
}
static void Set_Low_SCL(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SCL, iic->T_SCL_Pin, GPIO_PIN_RESET);
}
static void Set_High_SDA(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SDA, iic->T_SDA_Pin, GPIO_PIN_SET);
}
static void Set_Low_SDA(Temp_IIC *iic)
{
HAL_GPIO_WritePin(iic->GPIO_SDA, iic->T_SDA_Pin, GPIO_PIN_RESET);
}
static GPIO_PinState Read_SDA(Temp_IIC *iic)
{
return HAL_GPIO_ReadPin(iic->GPIO_SDA, iic->T_SDA_Pin);
}
static void Delay_us(volatile uint32_t delay)
{
int last, curr, val;
int temp;
while (delay != 0)
{
temp = delay > 900 ? 900 : delay;
last = SysTick->VAL;
curr = last - CPU_FREQUENCY_MHZ * temp;
if (curr >= 0)
{
do
{
val = SysTick->VAL;
}
while ((val < last) && (val >= curr));
}
else
{
curr += CPU_FREQUENCY_MHZ * 1000;
do
{
val = SysTick->VAL;
}
while ((val <= last) || (val > curr));
}
delay -= temp;
}
}
/*-------------------------协议基础代码实现(除引脚枚举外,无需用户修改)-------------------------------*/
// 设置SCL线电平
static void I2C_Set_SCL(Temp_IIC *iic,GPIO_PinState state)
{
if(state)
Set_High_SCL(iic);
else
Set_Low_SCL(iic);
}
// 设置SDA线电平
static void I2C_Set_SDA(Temp_IIC *iic,GPIO_PinState state)
{
if(state)
Set_High_SDA(iic);
Set_Low_SDA(iic);
}
// 读取SDA线电平
static GPIO_PinState I2C_Read_SDA(Temp_IIC *iic)
{
return Read_SDA(iic);
}
void I2C_Delay(void)
{
Delay_us(8);
}
//开始信号
void I2C_Start(Temp_IIC *iic)
{
I2C_Set_SDA(iic,GPIO_PIN_SET);
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SDA(iic,GPIO_PIN_RESET);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
}
//结束信号
void I2C_Stop(Temp_IIC *iic)
{
I2C_Set_SDA(iic,GPIO_PIN_RESET);
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SDA(iic,GPIO_PIN_SET);
I2C_Delay();
}
// 发送一个字节,返回ACK状态
IIC_StatusTypeDef I2C_Send_Byte(Temp_IIC *iic,uint8_t byte)
{
for (int8_t i = 7; i >= 0; i--)
{
GPIO_PinState bit = (byte & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET;
I2C_Set_SDA(iic,bit);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
}
// 接收ACK
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
GPIO_PinState ack = I2C_Read_SDA(iic); // 读取ACK信号
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
return (ack == GPIO_PIN_RESET) ? IIC_EOK : IIC_ERR;
}
// 读取一个字节,并发送ACK或NACK
uint8_t I2C_Read_Byte(Temp_IIC *iic,GPIO_PinState ack)
{
uint8_t byte = 0;
I2C_Delay();
for (int8_t i = 7; i >= 0; i--)
{
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
if (I2C_Read_SDA(iic) == GPIO_PIN_SET)
{
byte |= (1 << i);
}
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
}
// 发送ACK或NACK
I2C_Set_SDA(iic,ack); // 发送ACK或NACK
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_SET);
I2C_Delay();
I2C_Set_SCL(iic,GPIO_PIN_RESET);
I2C_Delay();
return byte;
}
/*-------------------------IIC代码实现(无需用户修改,直接调用即可)-------------------------------*/
/**
* @brief I2C向指定地址写入一字节
* @param iic:软件IIC接口 Addr:设备地址 pData:写入数据
* @retval 无
**/
IIC_StatusTypeDef I2C_Write_Byte(Temp_IIC *iic, uint8_t Addr, uint8_t pData)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,Addr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
if (I2C_Send_Byte(iic,pData) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
I2C_Stop(iic);
return IIC_EOK;
}
/**
* @brief 模拟I2C写入函数,类似HAL_I2C_Mem_Write,仅支持8位地址
* @param iic:软件IIC接口 devAddr:设备地址 memAddr:内存地址 pData:写入数据 size:写入字节数
* @retval 无
**/
IIC_StatusTypeDef G_I2C_Mem_Write(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
if (I2C_Send_Byte(iic,memAddr) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
for (uint16_t i = 0; i < size; i++)
{
if (I2C_Send_Byte(iic,pData[i]) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
}
I2C_Stop(iic);
return IIC_EOK;
}
/*
* @brief 模拟I2C读取函数,类似HAL_I2C_Mem_Read,仅支持8位地址
* @param iic:软件IIC接口 devAddr:设备地址 memAddr:内存地址 pData:读出数据存储 size:读出字节数
* @retval 无
**/
IIC_StatusTypeDef G_I2C_Mem_Read(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic, devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
if (I2C_Send_Byte(iic, memAddr) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
I2C_Start(iic); // 重新启动信号
if (I2C_Send_Byte(iic,(devAddr << 1) | 0x01) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
for (uint16_t i = 0; i < size; i++)
{
pData[i] = I2C_Read_Byte(iic,(i == size - 1) ? I2C_NACK : I2C_ACK);
}
I2C_Stop(iic);
return IIC_EOK;
}
/*
* @brief 模拟I2C写入函数,仅支持16位地址
* @param iic:软件IIC接口 devAddr:设备地址 memAddr:内存地址 pData:写入数据 size:写入字节数
* @retval 无
**/
IIC_StatusTypeDef I2C_Mem_Write(Temp_IIC *iic, uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 发送高位地址字节
if (I2C_Send_Byte(iic,(uint8_t)(memAddr >> 8)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 发送低位地址字节
if (I2C_Send_Byte(iic,(uint8_t)(memAddr & 0xFF)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 发送数据
for (uint16_t i = 0; i < size; i++)
{
if (I2C_Send_Byte(iic,pData[i]) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
}
I2C_Stop(iic);
return IIC_EOK;
}
/*
* @brief 模拟I2C读取函数,仅支持16位地址
* @param iic:软件IIC接口 devAddr:设备地址 memAddr:内存地址 pData:读出数据 size:读出字节数
* @retval 无
**/
IIC_StatusTypeDef I2C_Mem_Read(Temp_IIC *iic,uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size)
{
I2C_Start(iic);
if (I2C_Send_Byte(iic,devAddr << 1) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 发送高位地址字节
if (I2C_Send_Byte(iic,(uint8_t)(memAddr >> 8)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 发送低位地址字节
if (I2C_Send_Byte(iic,(uint8_t)(memAddr & 0xFF)) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
I2C_Start(iic); // 重新启动信号
if (I2C_Send_Byte(iic,(devAddr << 1) | 0x01) != IIC_EOK)
{
I2C_Stop(iic);
return IIC_ERR;
}
// 接收数据
for (uint16_t i = 0; i < size; i++)
{
pData[i] = I2C_Read_Byte(iic,(i == size - 1) ? I2C_NACK : I2C_ACK);
}
I2C_Stop(iic);
return IIC_EOK;
}
/**
* @file iic_gpio.h
* @brief 模拟I2C的功能实现
* @author 瀚海浮萍
* @date 8/16/2025
*/
#ifndef _IIC_GPIO_H_
#define _IIC_GPIO_H_
/*-----------------------根据实况进行修改-------------------------------*/
#include "stm32f1xx_hal.h"
#define CPU_FREQUENCY_MHZ 72 // STM32时钟主频
/*-----------------------以下无需进行修改-------------------------------*/
// 定义 I2C ACK 和 NACK
#define I2C_ACK GPIO_PIN_RESET
#define I2C_NACK GPIO_PIN_SET
/* IIC结构定义 */
typedef struct{
GPIO_TypeDef *GPIO_SDA;
uint16_t T_SDA_Pin;
GPIO_TypeDef *GPIO_SCL;
uint16_t T_SCL_Pin;
}Temp_IIC;
typedef enum
{
IIC_EOK = 0x00U,
IIC_ERR = 0x01U
}IIC_StatusTypeDef;
/*-----------------------用户直接外部调用-------------------------------*/
extern Temp_IIC iic1;
void I2C_GPIO_Init(Temp_IIC *iic);
void I2C_Delay(void);
void I2C_Start(Temp_IIC *iic);
void I2C_Stop(Temp_IIC *iic);
uint8_t I2C_Read_Byte(Temp_IIC *iic,GPIO_PinState ack);
IIC_StatusTypeDef I2C_Send_Byte(Temp_IIC *iic,uint8_t byte);
/*-------------------------用户API调用-------------------------------*/
// I2C API函数声明(向指定地址写入一字节 仅支持8位地址)
IIC_StatusTypeDef I2C_Write_Byte(Temp_IIC *iic, uint8_t Addr, uint8_t pData);
// I2C API函数声明(仅支持16位地址)
IIC_StatusTypeDef I2C_Mem_Write(Temp_IIC *iic ,uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size);
IIC_StatusTypeDef I2C_Mem_Read(Temp_IIC *iic ,uint8_t devAddr, uint16_t memAddr, uint8_t *pData, uint16_t size);
// I2C API函数声明(仅支持8位地址)
IIC_StatusTypeDef G_I2C_Mem_Write(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size);
IIC_StatusTypeDef G_I2C_Mem_Read(Temp_IIC *iic, uint8_t devAddr, uint8_t memAddr, uint8_t *pData, uint16_t size);
#endif /* __IIC_GPIO_H */

浙公网安备 33010602011771号