4. I2C总线驱动与设备探测:协议时序、主机驱动与从设备扫描
I2C总线,说实话,是嵌入式系统里最常用的低速总线之一。两根线——SCL和SDA,就能挂一堆设备。我在项目里见过有人用I2C挂了8个传感器加一个EEPROM,跑得稳稳的。
但I2C也有它的脾气。时序不对,设备不认。地址冲突,总线卡死。今天我们就从协议时序开始,一步步把I2C主机驱动写出来,再做个设备扫描器,把挂在上面的从设备都揪出来。
4.1 I2C协议时序:从START到STOP
I2C的通信,说白了就是主设备控制时钟,在SDA上按规则发送数据。每个字节8位,后面跟一个ACK位。就这么简单。
核心时序就几个:
- START条件:SCL高电平时,SDA从高变低
- 数据采样:SCL低电平时改变SDA,高电平时采样
- ACK/NACK:第9个时钟,从设备拉低SDA表示应答
- STOP条件:SCL高电平时,SDA从低变高
关键点:START和STOP是唯一允许SDA在SCL高电平时变化的时刻。其他时候SDA只能在SCL低电平时变化。这个规则一旦违反,总线上的设备就会乱掉。
我刚开始写I2C驱动时,就犯过这个错。在数据传输中间不小心让SDA在SCL高电平跳了一下,结果整个总线卡死,所有设备都不响应了。查了两天才发现是GPIO模拟时序时中断处理不当导致的。
标准I2C速率分几种:
| 模式 | 最大速率 | 典型应用 |
|---|---|---|
| 标准模式 | 100 kHz | 大多数传感器、EEPROM |
| 快速模式 | 400 kHz | 高分辨率ADC、音频编解码 |
| 快速+模式 | 1 MHz | 大容量存储器、显示驱动 |
| 高速模式 | 3.4 MHz | 视频、高速数据采集 |
我个人习惯,在MCU上做I2C驱动时,先按100kHz标准模式调通,再往上提速率。为什么?因为100kHz的时序容限大,GPIO模拟或硬件I2C都容易过。速率上去后,上拉电阻、线长、寄生电容都会影响信号质量。
4.2 主机模式驱动编写:从GPIO模拟到硬件I2C
写I2C主机驱动,有两种方式:GPIO模拟和硬件I2C外设。我们先看GPIO模拟,因为这种方式让你真正理解时序。
4.2.1 GPIO模拟I2C
核心就是操作两个GPIO引脚,按协议产生时序。下面是一个精简的模拟I2C驱动框架:
// I2C GPIO模拟驱动核心函数
static void i2c_delay(void) {
// 根据目标速率调整延时
// 100kHz -> 5us, 400kHz -> 1.25us
delay_us(5);
}
static void i2c_start(void) {
SDA_OUT(); // SDA设为输出
SDA_HIGH(); // SDA高
SCL_HIGH(); // SCL高
i2c_delay();
SDA_LOW(); // SDA低 -> START
i2c_delay();
SCL_LOW(); // SCL低,准备传输数据
}
static void i2c_stop(void) {
SDA_OUT();
SDA_LOW(); // SDA低
SCL_HIGH(); // SCL高
i2c_delay();
SDA_HIGH(); // SDA高 -> STOP
i2c_delay();
}
static int i2c_write_byte(uint8_t data) {
uint8_t i;
for (i = 0; i < 8; i++) {
if (data & 0x80)
SDA_HIGH();
else
SDA_LOW();
data <<= 1;
SCL_HIGH();
i2c_delay();
SCL_LOW();
i2c_delay();
}
// 读取ACK
SDA_IN();
SCL_HIGH();
i2c_delay();
int ack = (SDA_READ() == 0) ? 0 : -1;
SCL_LOW();
SDA_OUT();
return ack;
}
经验之谈:GPIO模拟I2C时,延时函数一定要准。我见过有人用空循环做延时,结果不同编译器优化等级下时序完全不一样。建议用硬件定时器或系统滴答时钟做微秒级延时。
4.2.2 硬件I2C外设驱动
大多数MCU都内置I2C外设。用硬件I2C,你只需要配置寄存器,剩下的时序由硬件完成。以STM32为例:
// 硬件I2C初始化
void i2c_hw_init(I2C_TypeDef *i2c, uint32_t speed) {
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// 2. 配置GPIO为复用开漏
// SCL - PB6, SDA - PB7 (I2C1)
GPIOB->AFR[0] |= (4 << 24) | (4 << 28); // AF4
GPIOB->MODER |= (2 << 12) | (2 << 14); // 复用模式
GPIOB->OTYPER |= (1 << 6) | (1 << 7); // 开漏输出
// 3. 配置I2C时序
i2c->TIMINGR = calc_i2c_timing(speed, APB1_CLK);
// 4. 使能I2C
i2c->CR1 |= I2C_CR1_PE;
}
// 硬件I2C发送
int i2c_hw_write(I2C_TypeDef *i2c, uint8_t addr,
uint8_t *data, uint32_t len) {
// 等待总线空闲
while (i2c->ISR & I2C_ISR_BUSY);
// 发送START + 从设备地址
i2c->CR2 = (addr << 1) | I2C_CR2_START;
// 发送数据
for (uint32_t i = 0; i < len; i++) {
while (!(i2c->ISR & I2C_ISR_TXIS));
i2c->TXDR = data[i];
}
// 等待传输完成
while (!(i2c->ISR & I2C_ISR_TC));
// 发送STOP
i2c->CR2 |= I2C_CR2_STOP;
return 0;
}
注意:硬件I2C的时序配置寄存器(TIMINGR)计算很关键。不同MCU的算法不同,建议直接用厂商提供的工具生成配置值。我自己曾经手动算错过一次,导致I2C时钟频率偏差30%,设备间歇性不响应。
4.3 从设备地址扫描与挂载
写好了驱动,怎么知道总线上挂了哪些设备?答案是地址扫描。I2C从设备地址是7位(也有10位地址模式,但用得少),范围0x00-0x7F。但有些地址是保留的,实际可用的地址范围是0x08-0x77。
扫描原理很简单:向每个可能的地址发送START条件+写操作,看是否有ACK回应。有ACK说明该地址有设备。
// I2C总线设备扫描
void i2c_scan_bus(void) {
uint8_t addr;
int device_count = 0;
printf("I2C Bus Scanning...\n");
printf(" ");
for (addr = 0; addr < 0x80; addr++) {
if (addr % 16 == 0) {
printf("\n0x%02X: ", addr);
}
// 发送START + 地址 + 写位,检查ACK
i2c_start();
int ack = i2c_write_byte((addr << 1) | 0); // 写操作
i2c_stop();
if (ack == 0) {
printf("0x%02X ", addr);
device_count++;
} else {
printf(" -- ");
}
}
printf("\nFound %d device(s)\n", device_count);
}
扫描结果通常以表格形式输出,方便查看:
| 地址范围 | 常见设备 | 备注 |
|---|---|---|
| 0x20 - 0x27 | PCF8574 I/O扩展 | A0/A1/A2引脚决定具体地址 |
| 0x3C - 0x3D | OLED显示屏(SSD1306) | 0x3C最常见 |
| 0x48 - 0x4F | 温度传感器(LM75, TMP102) | 地址引脚决定偏移 |
| 0x50 - 0x57 | EEPROM(AT24C系列) | 0x50最常用 |
| 0x68 | RTC(DS3231, PCF8563) | 部分RTC用0x6F |
| 0x76 - 0x77 | 气压计(BMP280, BME280) | SDO引脚决定 |
挂载流程:扫描到设备后,需要做三件事——验证设备ID寄存器(如果有)、初始化设备配置、注册到设备管理层。我习惯在扫描阶段就读取设备ID,避免地址冲突导致误判。
4.4 I2C总线知识结构
下面这张图把I2C总线驱动与设备探测的核心知识点串起来了:
4.5 实战中的避坑指南
写I2C驱动这么多年,踩过的坑不少。挑几个典型的说说:
- 总线卡死:从设备在传输中复位了,SDA被拉低不放。解决办法是加超时机制,超时后发9个时钟脉冲让设备释放总线。
- 地址冲突:两个设备用了相同地址。我遇到过一块板子上两个传感器都默认0x48,只能改硬件地址引脚或换设备。
- 速率不匹配:从设备不支持高速模式,主设备却配了400kHz。结果就是随机性通信失败。建议先按100kHz扫一遍,确认设备都响应再提速率。
- 上拉电阻:I2C总线需要上拉电阻。我见过有人忘了加上拉,结果总线一直低电平,所有设备都不响应。标准值是4.7kΩ,总线电容大时用2.2kΩ。
曾经有个项目,I2C总线挂了5个设备,偶尔出现通信失败。查了三天,最后发现是SCL线上串了100Ω电阻导致信号上升沿变缓,在400kHz下时序违规。换成0Ω电阻后问题解决。所以,高速I2C时走线要短,上拉要强,串联电阻要小。
好了,I2C总线驱动与设备探测的核心内容就这些。从协议时序到驱动实现,再到设备扫描挂载,每一步都有它的门道。写驱动时多想想时序,调试时多用逻辑分析仪看波形,很多问题一眼就能看出来。