关于软件模拟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,主要是加深印象。

d3bbca378362c28829cee243a8497ef7

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 */

posted @ 2025-08-07 18:56  瀚海浮萍  阅读(624)  评论(0)    收藏  举报