3、I2C通信基础:I2C协议时序、主从模式、速率选择、嵌入式I2C驱动框架
好,咱们今天聊聊I2C。这个总线在嵌入式系统里太常见了,传感器、EEPROM、ADC、DAC,几乎每个板子上都能找到它的身影。我个人觉得,搞懂I2C是驱动开发的基本功,就像学武术得先扎马步一样。
3.1 I2C协议时序:两根线上的舞蹈
I2C只用两根线:SCL(时钟线)和SDA(数据线)。说白了,就是靠这两根线的电平变化来传递信息。你想想看,两根线要完成地址发送、数据读写、应答确认这么多事,时序上肯定有讲究。
空闲状态:SCL和SDA都是高电平。这时候总线是空闲的,谁都可以发起通信。
起始信号:SCL为高电平时,SDA从高电平跳变到低电平。这个下降沿告诉所有从设备:“注意了,我要开始说话了!”
停止信号:SCL为高电平时,SDA从低电平跳变到高电平。这个上升沿表示:“我说完了,你们可以歇着了。”
数据传输:数据在SCL的上升沿被采样。也就是说,发送方必须在SCL低电平时改变SDA的状态,然后在SCL高电平时保持稳定。每个时钟周期传输一个bit,8个bit组成一个字节。
应答机制:每发送完一个字节,接收方要拉低SDA表示“收到了”。如果接收方没拉低,发送方就知道出问题了。我在项目中遇到过,有时候从设备没上电或者地址不对,就会一直发NACK,这时候就得检查硬件连接了。
关键时序参数:
- SCL时钟频率:标准模式100kHz,快速模式400kHz,高速模式3.4MHz
- 上升时间:标准模式不超过1000ns,快速模式不超过300ns
- 保持时间:起始信号后SCL低电平至少4.7μs(标准模式)
3.2 主从模式:谁说了算?
I2C总线上的设备分为主设备和从设备。主设备负责产生时钟、发起通信、决定什么时候开始什么时候结束。从设备只能被动响应,不能主动说话。
一个总线上可以有多个主设备,但同一时刻只能有一个主设备控制总线。这就涉及到仲裁机制了——如果两个主设备同时发起通信,谁先拉低SDA谁就赢。嗯,这里要注意,仲裁过程中不会丢失数据,这是I2C协议设计得很巧妙的地方。
从设备地址:每个从设备都有一个7位或10位的地址。主设备在起始信号后先发送地址字节,其中前7位是地址,第8位是读写位(0表示写,1表示读)。
我曾经调试过一个项目,两个传感器用了相同的I2C地址,结果读回来的数据全是乱的。后来查手册才发现,其中一个传感器的地址引脚可以配置,改一下硬件就解决了。所以啊,画原理图的时候就要规划好地址分配。
| 地址类型 | 地址范围 | 使用场景 |
|---|---|---|
| 7位地址 | 0x00 - 0x7F | 大多数I2C设备 |
| 10位地址 | 0x00 - 0x3FF | 地址资源紧张时使用 |
| 保留地址 | 0x00, 0x01, 0x7F等 | 特殊用途,用户不可用 |
3.3 速率选择:不是越快越好
I2C支持多种速率模式,常见的有:
- 标准模式:100kHz,最常用,兼容性最好
- 快速模式:400kHz,大多数现代设备都支持
- 快速模式+:1MHz,需要更强的上拉驱动
- 高速模式:3.4MHz,对硬件要求高
我个人习惯先用100kHz调试,等通信稳定了再慢慢提高频率。为什么?因为高频下信号质量容易出问题,尤其是线缆比较长或者上拉电阻没选对的时候。
上拉电阻选择经验:
总线电容越大,上拉电阻就要越小。一般100kHz用4.7kΩ,400kHz用2.2kΩ。如果总线上的设备很多,可以适当减小电阻值。我曾经在一个项目里用了10个I2C设备,总线电容太大,400kHz死活跑不起来,换成2.2kΩ就好了。
3.4 嵌入式I2C驱动框架:从底层到应用
在嵌入式Linux里,I2C驱动分为三个层次:
- I2C核心层:提供统一的接口,管理总线、设备和驱动
- I2C总线驱动:操作具体的I2C控制器硬件,处理时序
- I2C设备驱动:针对具体设备,比如温度传感器、EEPROM
写驱动的时候,我们主要关注设备驱动层。下面是一个简单的读写示例:
// I2C读操作示例
int i2c_read(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
struct i2c_msg msg[2];
int ret;
// 先写寄存器地址
msg[0].addr = client->addr;
msg[0].flags = 0; // 写操作
msg[0].len = 1;
msg[0].buf = ®
// 再读数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读操作
msg[1].len = len;
msg[1].buf = buf;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret != 2) {
dev_err(&client->dev, "I2C read failed\n");
return -EIO;
}
return 0;
}
这个函数先发送寄存器地址,然后读取数据。注意看,这里用了两个消息(msg),第一个是写地址,第二个是读数据。I2C协议支持这种“组合传输”,非常实用。
避坑指南:
我曾经在写驱动时忘了加延时,结果连续读写时数据对不上。后来发现,有些从设备在两次操作之间需要一点时间来处理内部状态。解决办法是在每次传输之间加一个usleep_range(100, 200),给设备一点喘息的时间。
在设备树中,I2C设备的描述也很简单:
&i2c1 {
status = "okay";
clock-frequency = <400000>; // 400kHz
temperature_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
这里指定了I2C控制器的频率,以及从设备的地址和兼容性字符串。驱动匹配的时候,就是靠这个compatible来找到对应的驱动。
好了,I2C的基础就聊到这儿。下一章咱们会深入I2C驱动的实际开发,包括中断处理、DMA传输、以及一些高级技巧。记住,搞I2C驱动,时序是根本,地址要核对,速率别贪快。把这些基础打牢了,后面写什么驱动都不怕。