在嵌入式开发中,I2C 总线凭借几大核心优势成为热门选择:

  • 硬件极简:仅需两根线(SDA 传输数据,SCL 同步时钟),大幅节省芯片引脚资源;
  • 多设备支持:通过给每个外设分配独立地址(支持7位或扩展的10位地址),可轻松连接EEPROM存储芯片、温度传感器、运动检测模块等多种设备;
  • 即插即用:同一总线上可自由增减设备,无需复杂配置,特别适合空间受限的电路板设计 .

1. 随着系统复杂度提供,所面临的困境:

  • 硬件资源瓶颈:微控制器内置 I2C 控制器数量不足,尤其在多外设场景下捉襟见肘​ ;
  • 时序兼容性问题:部分老旧外设或定制化模块对 I2C 时序(如时钟频率、信号建立时间)有特殊要求​;
  • 同址设备难题:多个具有相同 I2C 地址的外设无法通过硬件 I2C 直接驱动;

  通过 GPIO 模拟 I2C 协议无需依赖硬件控制器,仅需通用 IO 口即可实现完整的 I2C 通信,在 STM32、ESP32、Arduino 等平台上均有成熟应用。本文将从协议底层时序出发,结合 STM32 开发环境,提供可直接复用的工程级解决方案。


2. IIC时序图

2.1 读时序图

在这里插入图片描述

2.2 写时序图

在这里插入图片描述



2. 核心代码实现(STM32示例)

2.1 通用接口定义

i2cSoft.h

                  
// 枚举 I2C接口索引
typedef enum{
   I2C_SOFT_1   = 0,  
   I2C_SOFT_2   = 1,  
    
   I2C_SOFT_NUM
}I2C_SOFT_INDEX;


bool I2CSoftInit(I2C_SOFT_INDEX index);  // IIC 初始化
bool I2CSoftDeInit(I2C_SOFT_INDEX index); // IIC 反初始化

bool I2CSoftWriteByte( I2C_SOFT_INDEX index, unsigned char slaveAddr, unsigned char regAddr, unsigned char data); // IIC 写单字节数据
unsigned short I2cSoftWriteMulBytes(I2C_SOFT_DATA* data); // IIC 写多字节数据

unsigned char I2CSoftReadByte(I2C_SOFT_INDEX index, unsigned char slaveAddr, unsigned char regAddr); // IIC 读单字节数据
unsigned short I2cSoftReadMulBytes(I2C_SOFT_DATA* data); // IIC 读多字节数据




3 跨平台硬件配置

3.1 跨平台硬件配置

i2cHardConfig.h


// 平台识别宏(在项目全局配置中定义)
 #define I2C_PLATFORM_STM32   0
 #define I2C_PLATFORM_GD32    1
 #define I2C_PLATFORM_HC32    2

 #define I2C_PLATFORM_CFG     I2C_PLATFORM_STM32  // 根据实际应用,启用对应的宏




//=====================================================================================================================
// GPIO 硬件配置
//=====================================================================================================================


// 使用引脚定义
#if(I2C_PLATFORM_GD32 == I2C_PLATFORM_CFG)   // GD32
  

// I2C1 接口映射配置 
#define I2C1_SCL_PORT        GPIOB    
#define I2C1_SCL_PIN         GPIO_PIN_5    
static inline void I2C1_SCL_PORT_CLK_EN(void) { rcu_periph_clock_enable(RCU_GPIOB); }


// SDA 引脚配置
#define I2C1_SDA_PORT        GPIOB    
#define I2C1_SDA_PIN         GPIO_PIN_5  
static inline void I2C1_SDA_PORT_CLK_EN(void) { rcu_periph_clock_enable(RCU_GPIOB); }


#elif(I2C_PLATFORM_HC32 == I2C_PLATFORM_CFG)  // HC32

// I2C1 接口映射配置 
#define I2C1_SCL_PORT        3    
#define I2C1_SCL_PIN         6    
#define I2C1_SCL_PORT_CLK_EN 0

// SDA 引脚配置
#define I2C1_SDA_PORT        3    
#define I2C1_SDA_PIN         5  
#define I2C1_SDA_PORT_CLK_EN 0




#else // STM32

// 当前使用的是 STM32平台
// I2C1 接口映射配置 
// SCL 引脚配置
#define I2C1_SCL_PORT        GPIOB    
#define I2C1_SCL_PIN         GPIO_PIN_8    
static inline void I2C1_SCL_PORT_CLK_EN(void) { __HAL_RCC_GPIOB_CLK_ENABLE(); }


// SDA 引脚配置
#define I2C1_SDA_PORT        GPIOB    
#define I2C1_SDA_PIN         GPIO_PIN_9  
static inline void I2C1_SDA_PORT_CLK_EN(void) { __HAL_RCC_GPIOB_CLK_ENABLE(); }


// I2C2 接口映射配置
// SCL 引脚配置
// #define I2C2_SCL_PORT        GPIOA    
// #define I2C2_SCL_PIN         GPIO_Pin_11    
//static inline void I2C2_SCL_PORT_CLK_EN(void) { __HAL_RCC_GPIOB_CLK_ENABLE(); }


// // SDA 引脚配置
// #define I2C2_SDA_PORT        GPIOA   
// #define I2C2_SDA_PIN         GPIO_Pin_12 
//static inline void I2C2_SDA_PORT_CLK_EN(void) { __HAL_RCC_GPIOB_CLK_ENABLE(); }



#endif




// 引脚控制定义 
#if(I2C_PLATFORM_GD32 == I2C_PLATFORM_CFG)   // GD32
  
typedef  unsigned int I2C_GPIO_PORT;         // 统一GPIO端口类型

// 控制引脚 输出高低电平定义         GPIO_BOP(gpio_periph) = (uint32_t)pin;
#define I2C_SCL_LOW(  ops)         ( GPIO_BC((ops.port)) = (uint32_t)ops.pin )
#define I2C_SCL_HIGH( ops)         ( GPIO_BOP((ops.port)) = (uint32_t)ops.pin )

#define I2C_SDA_LOW(  ops)         ( GPIO_BC((ops.port)) = (uint32_t)ops.pin )
#define I2C_SDA_HIGH( ops)         ( GPIO_BOP((ops.port)) = (uint32_t)ops.pin)

// 定义 SDA 引脚方向模式 
#define SET_SDA_DIR_IN( ops)   do{\
                                  GPIO_CTL( (ops.port)) &= ~( 0x00000003 << ( ops.pinSource * 2)); \
                                  GPIO_CTL( (ops.port)) |= ( 0x00000000 << ( ops.pinSource * 2)); \
                                }while(0)        // 设为输入模式

#define SET_SDA_DIR_OUT( ops)  do{\
                                  GPIO_CTL( (ops.port)) &= ~( 0x00000003 << ( ops.pinSource * 2)); \
                                  GPIO_CTL( (ops.port)) |= ( 0x00000001 << ( ops.pinSource * 2)); \
                                }while(0)       // 设为输出模式

// 读取SDA引脚的状态。低电平返回 0,高电平返回 1                       
#define GET_SDA_STATUS(  ops)    ( (GPIO_ISTAT((ops.port)) & (ops.pin)) ? 1 : 0 )



#elif(I2C_PLATFORM_HC32 == I2C_PLATFORM_CFG)  // HC32
    
typedef  unsigned int I2C_GPIO_PORT;         // 统一GPIO端口类型

// 控制引脚 输出高低电平定义 
#define I2C_SCL_LOW(  ops)         ( GPIO_SetPinOutLow((ops.port), ops.pin) )
#define I2C_SCL_HIGH( ops)         ( GPIO_SetPinOutHigh((ops.port), ops.pin))

#define I2C_SDA_LOW(  ops)         ( GPIO_SetPinOutLow((ops.port), ops.pin) )
#define I2C_SDA_HIGH( ops)         ( GPIO_SetPinOutHigh((ops.port), ops.pin))

// 定义 SDA 引脚方向模式 
#define SET_SDA_DIR_IN( ops)   do{\
                                  WRITE_BIT((uint32_t)&M0P_GPIO->P0DIR + (ops.port) * GPIO_GPSZ, ops.pin, GpioDirIn); \
                                }while(0)        // 设为输入模式
#define SET_SDA_DIR_OUT( ops)  do{\
                                  WRITE_BIT((uint32_t)&M0P_GPIO->P0DIR + (ops.port) * GPIO_GPSZ, ops.pin, GpioDirOut); \
                                }while(0)       // 设为输出模式

// 读取SDA引脚的状态。低电平返回 0,高电平返回 1                       
#define GET_SDA_STATUS(  ops)    ( (GPIO_GetPinIn((ops.port), ops.pin) ) ? 1 : 0 )


#else   // STM32

typedef void* I2C_GPIO_PORT;         // 统一GPIO端口类型

// 控制引脚 输出高低电平定义 
#define I2C_SCL_LOW(  ops)         (((GPIO_TypeDef*)ops.port)->BSRR = (uint32_t)(ops.pin << 16))
#define I2C_SCL_HIGH( ops)         (((GPIO_TypeDef*)ops.port)->BSRR = (uint32_t)ops.pin)

#define I2C_SDA_LOW(  ops)         (((GPIO_TypeDef*)ops.port)->BSRR = (uint32_t)(ops.pin << 16))
#define I2C_SDA_HIGH( ops)         (((GPIO_TypeDef*)ops.port)->BSRR = (uint32_t)ops.pin)

// 定义 SDA 引脚方向模式 
#define SET_SDA_DIR_IN( ops)   do{\
                                 ((GPIO_TypeDef*)ops.port)->MODER &= ~( 0x00000003 << ( ops.pinSource * 2)); \
                                 ((GPIO_TypeDef*)ops.port)->MODER |= ( 0x00000000 << ( ops.pinSource * 2)); \
                                }while(0)        // 设为输入模式
#define SET_SDA_DIR_OUT( ops)  do{\
                                 ((GPIO_TypeDef*)ops.port)->MODER &= ~( 0x00000003 << ( ops.pinSource * 2)); \
                                 ((GPIO_TypeDef*)ops.port)->MODER |= ( 0x00000001 << ( ops.pinSource * 2)); \
                                }while(0)       // 设为输出模式


// 读取SDA引脚的状态。低电平返回 0,高电平返回 1                       
#define GET_SDA_STATUS(  ops)    ( (((GPIO_TypeDef*)ops.port)->IDR & ops.pin ) ? 1 : 0 )




#endif


//=====================================================================================================================
// GPIO 硬件配置结束
//=====================================================================================================================





3.2 初始化硬件资源

i2cHardConfig.c

/*!
 * 
 * @param[in] index:I2C接口索引号(参考枚举 I2C_SOFT_INDEX)
 * @param[out] none
 * @return I2C接口硬件参数结构指针
 * 
 * @brief 获取对应索引号的端口硬件参数,若获取成功,返回不为 NULL的 I2C_HARD_PARAMS* 指针.
 * 
 */
I2C_HARD_PARAMS* I2cHardGetParams(unsigned index)
{
    // 根据索引号,获取对应接口的参数
    switch(index)
    {
        case 0: // I2C1 接口
        
            s_I2cHardParams.scl.port      = I2C1_SCL_PORT; 
            s_I2cHardParams.scl.pin       = I2C1_SCL_PIN ; 
            s_I2cHardParams.scl.pinSource = getPortPinBitNum(I2C1_SCL_PIN);
            s_I2cHardParams.scl.portClkEn = I2C1_SCL_PORT_CLK_EN ;
           
            s_I2cHardParams.sda.port      = I2C1_SDA_PORT; 
            s_I2cHardParams.sda.pin       = I2C1_SDA_PIN ; 
            s_I2cHardParams.sda.pinSource = getPortPinBitNum(I2C1_SDA_PIN);
            s_I2cHardParams.sda.portClkEn = I2C1_SDA_PORT_CLK_EN ;
            
        break;

        case 1: // I2C2 接口
        
            // s_I2cHardParams.scl.port      = I2C2_SCL_PORT; 
            // s_I2cHardParams.scl.pin       = I2C2_SCL_PIN ; 
            // s_I2cHardParams.scl.pinSource = getPortPinBitNum(I2C2_SCL_PIN);
            // s_I2cHardParams.scl.portClkEn = I2C2_SCL_PORT_CLK_EN ;
           
            // s_I2cHardParams.sda.port      = I2C2_SDA_PORT; 
            // s_I2cHardParams.sda.pin       = I2C2_SDA_PIN ; 
            // s_I2cHardParams.sda.pinSource = getPortPinBitNum(I2C2_SDA_PIN);
            // s_I2cHardParams.sda.portClkEn = I2C2_SDA_PORT_CLK_EN ;
            
        break;

        default : return NULL;
    }
    
    return &s_I2cHardParams;
}


/*!
 * 
 * @param[in] params :I2C硬件参数结构指针
 * @param[out] none
 * @return none
 * 
 * @brief 对指定I2C硬件参数进行初始化.
 * 
 */
void I2cHardGpioInit(const I2C_HARD_PARAMS* params)
{

#if(I2C_PLATFORM_GD32 == I2C_PLATFORM_CFG)   // GD32


    // 使能时钟
    params->scl.portClkEn();
    params->sda.portClkEn();

    gpio_mode_set(params->scl.port, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, params->scl.pin);
    gpio_output_options_set(params->scl.port, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, params->scl.pin);

    gpio_mode_set(params->sda.port, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, params->sda.pin);
    gpio_output_options_set(params->sda.port, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, params->sda.pin);


#elif(I2C_PLATFORM_HC32 == I2C_PLATFORM_CFG)  // HC32   

    Gpio_InitIO(params->scl.port, params->scl.pin, GpioDirOut);
    Gpio_InitIO(params->sda.port, params->sda.pin, GpioDirOut);


#else   // STM32

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    if( !params) return ;
    

    // 使能时钟
    params->scl.portClkEn();
    params->sda.portClkEn();

    // 引脚公用参数配置
	GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP; // 输出模式
	GPIO_InitStruct.Pull  = GPIO_PULLUP; //   GPIO_NOPULL GPIO_PULLUP
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    
    // SCL 引脚配置
    GPIO_InitStruct.Pin   = params->scl.pin;
    HAL_GPIO_Init((GPIO_TypeDef*)params->scl.port, &GPIO_InitStruct);
    
    // SDA 引脚配置
		GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_OD; // 开漏输出模式,这样就不用设置IO方向了
	  GPIO_InitStruct.Pin   = params->sda.pin;
    HAL_GPIO_Init((GPIO_TypeDef*)params->sda.port, &GPIO_InitStruct);
		
		
		
    
#endif


    // 初始化引脚电平
    I2C_SDA_HIGH( params->sda);
    I2C_SCL_LOW( params->scl);
    
}


4. 使用操作步骤

i2cSoft 功能文件添加到开发的工程下。

4.1 通信延时时钟配置

i2cSoft.h

// 系统时钟频率(单位:Hz),需根据实际芯片使用频率进行更改
#define I2C_SYSTEM_CLOCK_FREQ  168000000UL  


// 单次循环周期数(根据实际测试调整)
#define I2C_NOP_CYCLES         1    

// 每100ns所需循环次数
#define I2C_SYSTEM_CYCLES_PER_100NS  (I2C_SYSTEM_CLOCK_FREQ * 100e-9 / I2C_NOP_CYCLES)

   
// 定义模拟 I2C通信速度等级                                                
#define I2C_DLY_1000K    (10)  // 5->0.5us
#define I2C_DLY_400K     (30)  // 15->1.5us  25
#define I2C_DLY_100K     (100) // 50->5us                        

// 宏定义模拟I2C速度
#define I2C_DLY_NS       I2C_DLY_400K      


// 定义初略的100纳秒延时程序    
// ns - 延时时间,单位:100ns                   
#define I2C_DELAY_100NS( ns)  do { \
                                    volatile unsigned int cnt = (ns) * I2C_SYSTEM_CYCLES_PER_100NS; \
                                    while (cnt--) { \
                                     __NOP(); \
                                    } \
                               } while (0)




4.2 操作步骤


 1. i2cHardConfig.h 根据芯片平台,设置宏 I2C_PLATFORM_CFG 或更改 GPIO硬件配置 ;
 2. i2cHardConfig.c 文件,根据结合芯片,完成函数 I2cHardGetParams() 、
    I2cHardGpioInit() 、 I2cHardGpioDeInit() 的更改实现;
 3. 设置系统时钟频率 I2C_SYSTEM_CLOCK_FREQ 和 I2C_DLY_NS I2C通信速度设置 ;
 4. 使用函数 I2CSoftInit() 初始化 IIC 硬件资源。
 5. 函数 I2CSoftWriteByte( ) 向指定I2C从机的寄存器写入1字节数据。
 6. 函数 I2cSoftWriteMulBytes() 向指定I2C从机的寄存器写入多个字节的数据。
 7. 函数 I2CSoftReadByte() 从指定I2C从机的寄存器读取1字节数据。
 8. 函数 I2cSoftReadMulBytes() 从指定I2C从机的寄存器读取多个字节的数据。



具体操作见:

STM32F407_TestIIC 案例程序



Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐