1. 为什么需要软件 IIC?从硬件 IIC 的痛点说起
刚开始接触 STM32 的 IIC 通信时,我和很多初学者一样,首先尝试的是硬件 IIC。毕竟 STM32 提供了专门的 I2C 外设,用起来应该很方便才对。但实际使用中,我发现硬件 IIC 并没有想象中那么美好。
硬件 IIC 最大的问题在于时序的严格性。STM32 的硬件 IIC 对时序要求非常苛刻,稍微有点干扰或者时序不匹配就容易出现卡死、无响应的情况。特别是在多设备共享总线时,一旦某个设备没有及时响应,整个总线就可能陷入死锁状态,需要重新初始化才能恢复。
相比之下,软件 IIC 虽然占用 CPU 资源,但有着无可替代的优势。首先是灵活性,你可以完全控制时序的每个细节,根据不同的从设备调整延时时间。其次是稳定性,不会出现硬件 IIC 那种莫名其妙的死锁问题。最重要的是,软件 IIC 能让你真正理解 IIC 协议的本质,而不是简单地调用库函数。
我在实际项目中选择软件 IIC 还有一个重要原因:引脚分配的灵活性。硬件 IIC 的引脚是固定的,当这些引脚被其他功能占用时,软件 IIC 可以任意选择可用的 GPIO 引脚,这在 PCB 布局时提供了很大的便利。
2. 深入理解 IIC 时序:从信号层面掌握通信本质
要写好软件 IIC 驱动,首先必须吃透 IIC 的时序要求。IIC 协议看起来简单,但细节很多,每个信号都有严格的时间要求。
起始信号(START)是通信的开始,当 SCL 为高电平时,SDA 从高电平跳变到低电平。这个跳变必须在 SCL 高电平期间完成,而且保持时间要足够长,确保从设备能够检测到这个变化。在实际编码中,我通常会这样实现:
void iic_start(void) {
IIC_SDA_H(); // 先保证 SDA 为高
IIC_SCL_H(); // SCL 拉高
iic_delay(); // 保持一段时间
IIC_SDA_L(); // SDA 拉低产生下降沿
iic_delay();
IIC_SCL_L(); // 最后拉低 SCL,准备数据传输
}
停止信号(STOP)则相反,在 SCL 高电平期间,SDA 从低电平跳变到高电平。这个信号告诉从设备通信结束,释放总线。
数据传输时有个重要规则:只有在 SCL 为低电平时,SDA 才能改变电平状态;SCL 为高电平时,SDA 必须保持稳定。这是因为从设备在 SCL 高电平时采样 SDA 的数据,如果此时 SDA 发生变化,就可能被误认为是起始或停止信号。
应答机制是 IIC 协议的精髓。主机每发送完 8 位数据,就会在第 9 个时钟周期释放 SDA 线,等待从设备拉低 SDA 作为应答。如果从设备没有拉低 SDA,表示未应答(NACK),主机可以根据这个判断通信是否成功。
3. GPIO 配置的关键细节:为什么选择开漏输出?
在配置 GPIO 时,SDA 线的模式选择很有讲究。推挽输出和开漏输出是两种不同的驱动方式,对 IIC 通信有直接影响。
推挽输出的特点是能够主动输出高电平和低电平,驱动能力强。但在 IIC 通信中,SDA 线需要被主机和从设备共享,推挽输出在切换方向时需要重新配置 GPIO 模式,增加了复杂度。
开漏输出只能主动拉低电平,高电平靠外部上拉电阻维持。

