1. 项目概述:基于STM32的软件模拟I2C驱动实现
在嵌入式开发中,I2C总线因其简洁的两线制设计(SCL时钟线和SDA数据线)和主从架构,成为连接各类传感器、存储芯片的常用接口。但硬件I2C外设常面临引脚固定、时序调试复杂等问题。本文介绍的SoftI2CMasterObj项目,通过GPIO模拟实现了灵活的软件I2C主机驱动,特别适合STM32F10x系列MCU在硬件I2C资源不足或需要动态切换引脚时的应用场景。
这个驱动库的核心价值在于:
- 完全通过软件控制GPIO实现I2C协议,不依赖硬件I2C外设
- 支持运行时动态配置SCL/SDA引脚
- 提供可扩展的延时函数接口适配不同速率需求
- 采用面向对象设计思想,便于集成到XCOSnTh实时操作系统
2. 硬件设计与初始化配置
2.1 引脚定义与硬件连接
驱动默认使用PB10(SCL)和PB11(SDA),但可通过修改宏定义灵活调整:
c复制#define SCL_RCC RCC_APB2Periph_GPIOB
#define SCL_PORT GPIOB
#define SCL_PIN GPIO_Pin_10
#define SDA_RCC RCC_APB2Periph_GPIOB
#define SDA_PORT GPIOB
#define SDA_PIN GPIO_Pin_11
硬件连接建议:
- SCL/SDA线各接4.7KΩ上拉电阻至3.3V
- 避免与高频信号线平行走线,防止信号干扰
- 长距离传输时建议增加I2C缓冲器(如PCA9600)
2.2 初始化流程解析
SIIC_PhyInit()函数完成关键硬件初始化:
c复制static void SIIC_PhyInit(void *cThis) {
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIO时钟
RCC_APB2PeriphClockCmd(SCL_RCC | SDA_RCC, ENABLE);
// SDA引脚配置为推挽输出(初始状态)
GPIO_InitStructure.GPIO_Pin = SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SDA_PORT, &GPIO_InitStructure);
// SCL引脚配置为推挽输出
GPIO_InitStructure.GPIO_Pin = SCL_PIN;
GPIO_Init(SCL_PORT, &GPIO_InitStructure);
}
注意:初始化时需确保SCL和SDA线处于高电平状态(I2C总线空闲条件)
3. 核心通信协议实现
3.1 时钟信号生成
clk()函数控制SCL线电平变化:
c复制static void clk(void *cThis, unsigned char signal) {
if(signal) {
GPIO_SetBits(SCL_PORT, SCL_PIN); // SCL拉高
} else {
GPIO_ResetBits(SCL_PORT, SCL_PIN); // SCL拉低
}
}
时序要点:
- 标准模式(100kHz)下,SCL高/低电平持续时间应≥4.7μs
- 快速模式(400kHz)下,持续时间应≥1.3μs
- 上升/下降时间应≤300ns(可通过GPIO_Speed配置)
3.2 数据线双向控制
sda()函数实现SDA线的动态方向切换:
c复制static int sda(void *cThis, SoftI2CMasterRWEnum rw, int signal) {
if(signal < 0) { // 模式切换请求
switch(signal) {
case SoftI2CMasterR: // 切换为输入模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
break;
case SoftI2CMasterW: // 切换为输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
break;
default: return 0xFF; // 错误代码
}
GPIO_Init(SDA_PORT, &GPIO_InitStructure);
return -1;
} else { // 读写操作
switch(rw) {
case SoftI2CMasterR: // 读取SDA状态
return GPIO_ReadInputDataBit(SDA_PORT, SDA_PIN);
case SoftI2CMasterW: // 写入SDA状态
signal ? GPIO_SetBits(SDA_PORT, SDA_PIN) :
GPIO_ResetBits(SDA_PORT, SDA_PIN);
return 0;
}
}
}
关键细节:每次从主机发送切换到接收时,必须先将SDA配置为输入模式,否则无法正确读取从机应答
4. 协议时序优化技巧
4.1 延时函数定制
虽然示例中的delay()为空函数,实际应用需根据需求实现:
c复制static void delay(void *cThis, SoftI2CMasterDelayPosition position,
unsigned int subPostion) {
switch(position) {
case StartConditionDelay:
Delay_us(5); // 启动条件保持时间
break;
case StopConditionDelay:
Delay_us(5); // 停止条件建立时间
break;
case DataHoldTime:
Delay_us(1); // 数据保持时间
break;
}
}
推荐延时参数:
| 时序参数 | 标准模式(100kHz) | 快速模式(400kHz) |
|---|---|---|
| SCL低电平时间 | ≥4.7μs | ≥1.3μs |
| SCL高电平时间 | ≥4.0μs | ≥0.6μs |
| 启动条件保持时间 | ≥4.0μs | ≥0.6μs |
| 数据建立时间 | ≥250ns | ≥100ns |
4.2 错误处理机制
增强驱动鲁棒性的建议:
- 增加超时检测(如SCL被从机拉低超过50ms判定为总线错误)
- 实现自动重试机制(连续3次通信失败后复位总线)
- 添加CRC校验支持(对关键数据传输)
5. 实际应用案例
5.1 驱动AT24Cxx系列EEPROM
c复制void EEPROM_WriteByte(uint8_t devAddr, uint16_t memAddr, uint8_t data) {
// 发送启动条件
SoftI2C_Start(&I2CTest);
// 发送设备地址(写模式)
SoftI2C_WriteByte(&I2CTest, (devAddr << 1) | 0);
if(!SoftI2C_CheckAck(&I2CTest)) return ERROR;
// 发送内存地址
SoftI2C_WriteByte(&I2CTest, (uint8_t)(memAddr >> 8));
SoftI2C_WriteByte(&I2CTest, (uint8_t)memAddr);
// 写入数据
SoftI2C_WriteByte(&I2CTest, data);
// 发送停止条件
SoftI2C_Stop(&I2CTest);
// 等待写入完成
Delay_ms(5);
}
5.2 读取BMP280气压传感器
c复制float BMP280_ReadTemperature() {
uint8_t data[3];
SoftI2C_Start(&I2CTest);
SoftI2C_WriteByte(&I2CTest, BMP280_ADDR << 1);
SoftI2C_WriteByte(&I2CTest, REG_TEMP_MSB);
SoftI2C_Start(&I2CTest); // 重复启动条件
SoftI2C_WriteByte(&I2CTest, (BMP280_ADDR << 1) | 1);
data[0] = SoftI2C_ReadByte(&I2CTest, 1); // 发送ACK
data[1] = SoftI2C_ReadByte(&I2CTest, 1);
data[2] = SoftI2C_ReadByte(&I2CTest, 0); // 最后字节NACK
SoftI2C_Stop(&I2CTest);
int32_t adc_T = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
// 进行温度补偿计算...
return compensated_temp;
}
6. 常见问题与调试技巧
6.1 典型故障排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无设备应答 | 1. 设备地址错误 | 用逻辑分析仪确认实际地址 |
| 2. 上拉电阻过大 | 减小上拉电阻(2.2KΩ-4.7KΩ) | |
| 数据波形畸变 | 1. 总线电容过大 | 缩短走线或增加驱动能力 |
| 2. 延时参数不匹配 | 调整delay()函数时序 | |
| 随机通信失败 | 1. 电源噪声干扰 | 增加电源去耦电容 |
| 2. 未正确处理总线冲突 | 实现总线仲裁和重试机制 |
6.2 逻辑分析仪调试建议
- 使用Saleae Logic或PulseView抓取I2C波形
- 重点关注:
- 启动/停止条件是否规范
- SCL/SDA上升时间是否达标
- 设备地址是否正确
- ACK/NACK响应是否符合预期
- 建议采样率≥4MHz以保证波形细节
在调试一个I2C-RTC模块时,曾遇到读取时间总是0xFF的问题。通过逻辑分析仪发现SCL上升沿过缓(约1.2μs),将GPIO速度从10MHz提升到50MHz后问题解决。这个案例说明,即使软件逻辑正确,硬件信号质量同样关键。