I2C驱动开发:软件模拟I2C与硬件I2C外设实现(以STM32为例)
一、从一次诡异的EEPROM读写失败说起
去年做一款工业传感器采集板,STM32F103通过I2C挂载AT24C02存储校准参数。硬件I2C外设配置完,读出来的数据总是隔三差五出现0xFF——典型的“总线卡死”症状。示波器抓波形,SCL时钟正常,SDA在第九个时钟后莫名其妙被拉低。查了三天,最后发现是I2C外设的“时钟延展”功能没处理好,从机拉低SCL等待内部操作完成时,主机直接超时复位了。
这个坑让我意识到:硬件I2C外设不是万能的,软件模拟I2C在某些场景下反而更可靠。今天就把这两种实现方式的工程细节掰开揉碎,结合STM32实战,把那些文档里不会写的“潜规则”讲清楚。
二、软件模拟I2C:裸奔的时序控制
2.1 为什么还要用软件模拟?
硬件I2C外设虽然方便,但遇到以下场景就抓瞎:
- 引脚复用冲突:某个I2C引脚被其他外设占用,只能用普通GPIO模拟
- 总线频率特殊:需要非标速率(比如12kHz),硬件外设分频器算不出来
- 从机行为怪异:某些国产传感器时序不标准,硬件外设的自动状态机处理不了
2.2 核心代码:别被网上那些“优雅”写法骗了
网上很多模拟I2C代码喜欢用delay_us()做精确延时,实际工程中千万别这么干——系统调度、中断抢占会导致时序抖动。正确的做法是用循环等待+超时保护:
// 模拟I2C的SCL/SDA引脚定义(别用PB3/PB4这种JTAG复用脚,踩过坑)
#define I2C_SCL_PIN GPIO_PIN_6
#define I2C_SDA_PIN GPIO_PIN_7
#define I2C_GPIO_PORT GPIOB
// SCL高低电平控制(别用HAL_GPIO_WritePin,直接操作ODR寄存器更快)
#define I2C_SCL_H() (I2C_GPIO_PORT->BSRR = I2C_SCL_PIN)
#define I2C_SCL_L() (I2C_GPIO_PORT->BRR = I2C_SCL_PIN)
// SDA输入输出切换(这里有个坑:切换前必须确保引脚状态稳定)
static void I2C_SDA_OUT(void)
{
GPIO_InitTypeDef gpio = {
0};
gpio.Pin = I2C_SDA_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出,别用推挽!
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(I2C_GPIO_PORT, &gpio);
}
static void I2C_SDA_IN(void)
{
GPIO_InitTypeDef gpio = {
0};
gpio.Pin = I2C_SDA_PIN;
gpio.Mode = GPIO_MODE_INPUT; // 输入模式
gpio.Pull = GPIO_PULLUP;
HAL_GPIO_Init(I2C_GPIO_PORT, &gpio);
}
关键点:SDA输出必须用开漏模式(OD),配合外部上拉电阻。用推挽输出的话,多个设备同时驱动SDA会短路——别问我怎么知道的。
2.3 起始信号:时序容差要留够
// I2C起始信号(这里延时用循环计数,别用HAL_Delay)
void I2C_Start(void)
{
I2C_SDA_OUT(); // 确保SDA为输出模式
I2C_SDA_H(); // SDA高
I2C_SCL_H(); // SCL高
delay_loop(10); // 至少4.7us(100kHz标准模式)
I2C_SDA_L(); // SDA拉低
delay_loop(10); // 至少4us
I2C_SCL_L(); // SCL拉低,准备传输数据
}
delay_loop(10)这个值要根据主频调整。我习惯用示波器实测:在100kHz下,每个循环大约0.5us(72MHz主频),10次就是5us,留了余量。别用精确的us延时函数,中断一来就废了。
