4、Python模拟I2C:使用RPi.GPIO库模拟I2C时序
说实话,很多刚接触嵌入式开发的朋友,一听到「模拟I2C」就觉得头大。觉得这东西太底层,太麻烦。但我个人的经验是——搞懂模拟I2C,你才算真正理解了I2C协议。硬件I2C虽然方便,但一旦出了问题,你连调试的抓手都没有。
这一章,我们就用RPi.GPIO库,在树莓派上纯软件模拟I2C时序。说白了,就是用GPIO引脚,一根一根地拉高拉低,把I2C的每一个细节都做出来。
4.1 为什么需要模拟I2C?
你可能会问:树莓派不是自带硬件I2C吗?为什么还要自己模拟?
嗯,我在项目里遇到过好几次这样的情况:硬件I2C总线被占用了,或者某个引脚功能冲突了。更常见的是——你要驱动的OLED模块,它的I2C地址和另一个设备冲突了。这时候,用两个普通GPIO引脚模拟I2C,就能完美绕开冲突。
另外,模拟I2C还有一个巨大的好处:调试方便。你可以精确控制每一根线的时序,甚至可以在关键位置插入打印信息,看看波形到底对不对。
4.2 准备工作:引脚与库
我们先确定一下用到的引脚。我个人习惯把SCL和SDA放在相邻的引脚上,这样接线方便。
| 信号 | 树莓派物理引脚 | BCM编号 | 说明 |
|---|---|---|---|
| SCL | Pin 11 | GPIO 17 | 时钟线,由主机控制 |
| SDA | Pin 12 | GPIO 18 | 数据线,双向传输 |
库方面,我们只用RPi.GPIO。这个库是树莓派的标准GPIO控制库,几乎每个树莓派上都预装了。如果没有,跑一句pip install RPi.GPIO就行。
4.3 核心函数:GPIO控制基础
在开始写I2C时序之前,我们先封装几个最基础的GPIO操作函数。这些函数虽然简单,但它们是整个模拟I2C的基石。
import RPi.GPIO as GPIO
import time
# 引脚定义
SCL_PIN = 17
SDA_PIN = 18
# 初始化GPIO
def i2c_gpio_init():
GPIO.setmode(GPIO.BCM)
GPIO.setup(SCL_PIN, GPIO.OUT)
GPIO.setup(SDA_PIN, GPIO.OUT)
# 初始状态:SCL和SDA都拉高(空闲状态)
GPIO.output(SCL_PIN, GPIO.HIGH)
GPIO.output(SDA_PIN, GPIO.HIGH)
# SCL控制
def scl_high():
GPIO.output(SCL_PIN, GPIO.HIGH)
def scl_low():
GPIO.output(SCL_PIN, GPIO.LOW)
# SDA控制
def sda_high():
GPIO.output(SDA_PIN, GPIO.HIGH)
def sda_low():
GPIO.output(SDA_PIN, GPIO.LOW)
# 延时函数(调整这个值可以改变I2C速度)
def i2c_delay():
time.sleep(0.000005) # 5微秒,约100kHz
这里有个细节要注意:延时函数。我刚开始做的时候,直接用time.sleep(0.001),结果OLED死活不响应。后来用示波器一看,时钟频率才1kHz,太慢了。OLED模块一般支持100kHz到400kHz,所以5微秒的延时差不多能跑到100kHz左右。
4.4 起始信号与停止信号
I2C协议里,起始信号和停止信号是最基本的「交通规则」。说白了,就是告诉从机:我要开始通信了,或者通信结束了。
起始信号(Start Condition): SCL为高电平时,SDA从高电平切换到低电平。
停止信号(Stop Condition): SCL为高电平时,SDA从低电平切换到高电平。
def i2c_start():
"""产生I2C起始信号"""
sda_high()
scl_high()
i2c_delay()
# SCL高电平时,SDA从高变低
sda_low()
i2c_delay()
scl_low()
i2c_delay()
def i2c_stop():
"""产生I2C停止信号"""
sda_low()
scl_high()
i2c_delay()
# SCL高电平时,SDA从低变高
sda_high()
i2c_delay()
4.5 数据发送与ACK信号
发送数据是I2C的核心操作。每个字节8位,从最高位(MSB)开始发送。每发送完一个字节,主机要释放SDA线,等待从机返回ACK(应答)信号。
ACK信号说白了就是:从机把SDA拉低,告诉主机「我收到了,继续发」。如果从机没拉低,那就是NACK,表示出问题了。
def i2c_send_byte(data):
"""发送一个字节数据,返回ACK状态"""
# 发送8位数据,从高位开始
for i in range(8):
# 在SCL低电平时设置SDA
if data & 0x80:
sda_high()
else:
sda_low()
data <<= 1
i2c_delay()
# 产生时钟脉冲
scl_high()
i2c_delay()
scl_low()
i2c_delay()
# 释放SDA线,准备接收ACK
GPIO.setup(SDA_PIN, GPIO.IN)
i2c_delay()
# 产生第9个时钟脉冲,读取ACK
scl_high()
i2c_delay()
# 读取SDA状态
ack = GPIO.input(SDA_PIN)
scl_low()
i2c_delay()
# 重新将SDA设为输出
GPIO.setup(SDA_PIN, GPIO.OUT)
# 返回ACK状态(低电平表示ACK)
return ack == 0
这里有一个关键点:发送完数据后,要把SDA引脚从输出模式切换为输入模式。为什么?因为ACK信号是从机拉低的,如果主机还占着SDA线,从机根本拉不动。我见过不少新手在这里翻车,SDA一直保持高电平,从机永远无法应答。
4.6 完整的数据写入流程
有了起始信号、停止信号和字节发送函数,我们就可以组合出一个完整的I2C写入流程了。以向OLED写一个命令为例:
def i2c_write_byte(dev_addr, data):
"""向指定设备地址写入一个字节"""
# 1. 起始信号
i2c_start()
# 2. 发送设备地址 + 写标志(最低位为0)
# 例如:0x3C << 1 | 0 = 0x78
if not i2c_send_byte(dev_addr << 1):
print("设备无应答!")
i2c_stop()
return False
# 3. 发送数据
if not i2c_send_byte(data):
print("数据发送失败!")
i2c_stop()
return False
# 4. 停止信号
i2c_stop()
return True
# 使用示例:向OLED发送命令
# OLED地址通常为0x3C
i2c_write_byte(0x3C, 0xAF) # 0xAF是OLED显示开启命令
4.7 知识结构图:模拟I2C核心流程
下面这张图,把整个模拟I2C的流程串起来了。你可以把它当作一个「速查地图」。
4.8 常见问题与避坑指南
模拟I2C看起来简单,但实际调试时坑不少。我把自己踩过的坑整理了一下:
- SDA方向切换忘记做:发送完数据后,一定要把SDA从输出切为输入,否则永远收不到ACK。我曾经因为这个,对着逻辑分析仪看了半小时,才发现SDA一直是被主机拉高的。
- 延时太短或太长:延时太短,从机跟不上;延时太长,通信速度太慢。5微秒是个不错的起点,如果不行再调整。
- 起始信号顺序搞反:先拉高SCL,再拉高SDA,然后拉低SDA,最后拉低SCL。这个顺序不能错,错了从机就不认。
- 停止信号后忘记释放总线:停止信号后,SCL和SDA都应该保持高电平,表示总线空闲。如果SDA被拉低,下一个起始信号可能无法被识别。
4.9 完整代码示例
最后,我把上面所有的函数整合成一个完整的模拟I2C类。这样用起来更方便:
import RPi.GPIO as GPIO
import time
class SoftI2C:
def __init__(self, scl_pin, sda_pin, delay_us=5):
self.scl = scl_pin
self.sda = sda_pin
self.delay = delay_us / 1_000_000
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.scl, GPIO.OUT)
GPIO.setup(self.sda, GPIO.OUT)
GPIO.output(self.scl, GPIO.HIGH)
GPIO.output(self.sda, GPIO.HIGH)
def _delay(self):
time.sleep(self.delay)
def start(self):
GPIO.output(self.sda, GPIO.HIGH)
GPIO.output(self.scl, GPIO.HIGH)
self._delay()
GPIO.output(self.sda, GPIO.LOW)
self._delay()
GPIO.output(self.scl, GPIO.LOW)
self._delay()
def stop(self):
GPIO.output(self.sda, GPIO.LOW)
GPIO.output(self.scl, GPIO.HIGH)
self._delay()
GPIO.output(self.sda, GPIO.HIGH)
self._delay()
def send_byte(self, data):
for i in range(8):
if data & 0x80:
GPIO.output(self.sda, GPIO.HIGH)
else:
GPIO.output(self.sda, GPIO.LOW)
data <<= 1
self._delay()
GPIO.output(self.scl, GPIO.HIGH)
self._delay()
GPIO.output(self.scl, GPIO.LOW)
self._delay()
GPIO.setup(self.sda, GPIO.IN)
self._delay()
GPIO.output(self.scl, GPIO.HIGH)
self._delay()
ack = GPIO.input(self.sda)
GPIO.output(self.scl, GPIO.LOW)
self._delay()
GPIO.setup(self.sda, GPIO.OUT)
return ack == 0
def write(self, dev_addr, data):
self.start()
if not self.send_byte(dev_addr << 1):
self.stop()
return False
if not self.send_byte(data):
self.stop()
return False
self.stop()
return True
# 使用示例
i2c = SoftI2C(17, 18)
i2c.write(0x3C, 0xAF) # 开启OLED显示
嗯,到这里,模拟I2C的核心内容就讲完了。你可能会觉得代码量有点大,但每一行都有它的意义。说白了,模拟I2C就是让你亲手触摸到协议的本质。下次遇到I2C通信问题,你就能从底层去分析,而不是对着数据手册干瞪眼。