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复用脚,踩过坑)#defineI2C_SCL_PINGPIO_PIN_6#defineI2C_SDA_PINGPIO_PIN_7#defineI2C_GPIO_PORTGPIOB// SCL高低电平控制(别用HAL_GPIO_WritePin,直接操作ODR寄存器更快)#defineI2C_SCL_H()(I2C_GPIO_PORT-BSRR=I2C_SCL_PIN)#defineI2C_SCL_L()(I2C_GPIO_PORT-BRR=I2C_SCL_PIN)// SDA输入输出切换(这里有个坑:切换前必须确保引脚状态稳定)staticvoidI2C_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);}staticvoidI2C_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)voidI2C_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);// 至少4usI2C_SCL_L();// SCL拉低,准备传输数据}delay_loop(10)这个值要根据主频调整。我习惯用示波器实测:在100kHz下,每个循环大约0.5us(72MHz主频),10次就是5us,留了余量。别用精确的us延时函数,中断一来就废了。2.4 字节发送: