可跨平台移植的 GPIO模拟I2C:从原理到实战指南
1. 描述IIC基本时序以及应用;2. 模拟IIC可跨平台移植配置,目前成功在STM32、GD32、HC32等平台运行过;3. i2cSoft功能接口描述以及使用步骤;
·
在嵌入式开发中,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从机的寄存器读取多个字节的数据。
具体操作见:
更多推荐



所有评论(0)