4. I2C协议详解与驱动实现:从总线架构到Linux子系统
I2C协议,说实话,是我在项目中打交道最多的串行总线之一。它只有两根线,却能挂载几十个设备,这种简洁高效的设计,让我每次用起来都觉得舒服。但舒服归舒服,坑也不少——时序不对、地址冲突、ACK丢失,哪个都够你调一整天。
这一章,我们就从I2C的总线架构讲起,把7位/10位地址、读写时序、ACK/NACK机制掰开揉碎,最后再深入Linux I2C子系统的核心数据结构。嗯,准备好了吗?
4.1 I2C总线架构:SCL与SDA
I2C总线就两条线:SCL(串行时钟线)和SDA(串行数据线)。所有设备都挂在这两根线上,通过开漏输出+上拉电阻的方式实现“线与”逻辑。
核心特点:
- 主从架构:主设备产生时钟,发起通信;从设备响应地址,收发数据
- 多主支持:多个主设备可以共享总线,通过仲裁机制决定谁占用
- 7位/10位地址:标准模式用7位地址,最多挂127个设备;扩展模式用10位地址
- 速率等级:标准模式100kHz,快速模式400kHz,高速模式3.4MHz
我个人习惯把I2C总线想象成一条“电话线”——SCL是振铃信号,SDA是通话内容。主设备控制振铃节奏,从设备在振铃间隙说话。谁先开口,谁就是主设备。
我的经验:上拉电阻的取值很关键。4.7kΩ适合大多数场景,但总线电容大时要用2.2kΩ甚至1kΩ。我曾经在一个项目里用10kΩ上拉,结果400kHz模式下波形完全变形——上升沿太慢,从设备根本识别不了。
4.2 7位地址模式与10位地址模式
I2C的地址模式,说白了就是怎么给设备编号。7位地址是标准,10位地址是扩展。
4.2.1 7位地址模式
7位地址模式下,地址范围是0x00~0x7F。但实际可用的地址没那么多——0x00是广播地址,0x01~0x07保留,0x78~0x7F也保留。真正能用的,大概112个。
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00 | 广播/通用呼叫地址 | 所有从设备都应响应 |
| 0x01~0x07 | 保留 | 用于特殊总线协议 |
| 0x08~0x77 | 可用地址 | 普通设备地址,共112个 |
| 0x78~0x7F | 保留 | 10位地址扩展用 |
你想想看,一个系统里挂十几个I2C设备很常见。如果每个设备都固定地址,冲突是迟早的事。所以很多芯片会提供多个地址引脚,让你通过硬件电平来配置地址。
4.2.2 10位地址模式
10位地址模式是为了解决7位地址不够用的问题。它的实现方式挺巧妙——用两个字节来传输地址。
10位地址传输流程:
- 第一个字节:11110XX + R/W,其中XX是10位地址的高2位
- 第二个字节:低8位地址
- 从设备识别:第一个字节的11110是标志位,告诉从设备这是10位地址
我记得第一次用10位地址时,差点被坑了。当时有个传感器芯片用了10位地址,我按7位地址去读,死活读不到。后来查手册才发现,原来第一个字节的格式不一样——不是简单的地址左移,而是有固定前缀的。
4.3 读写时序:起始条件、停止条件、ACK/NACK
I2C的时序,说白了就是一套“握手”规则。主设备通过SCL控制节奏,SDA上传输数据。每个bit的采样都在SCL的高电平期间完成。
4.3.1 起始条件与停止条件
这两个条件定义了通信的开始和结束:
- 起始条件(S):SCL高电平时,SDA从高电平跳变到低电平
- 停止条件(P):SCL高电平时,SDA从低电平跳变到高电平
为什么这样设计?因为数据在SCL高电平时采样,所以SDA在SCL高电平期间不能变化。起始和停止条件利用了SDA在SCL高电平时的跳变——这是一种“带外信号”,不会和数据混淆。
4.3.2 数据字节传输
每个字节8位,MSB先传。每传完一个字节,从设备要回复一个ACK(或NACK)。
- ACK:第9个SCL时钟,SDA被从设备拉低——表示“我收到了,继续”
- NACK:第9个SCL时钟,SDA保持高电平——表示“我没收到”或“我不想再收了”
避坑指南:我曾经在一个项目中遇到从设备偶尔不回复ACK的情况。排查了半天,发现是主设备时钟频率设置得太快,从设备跟不上。把频率从400kHz降到100kHz,问题就解决了。所以,遇到ACK丢失,先检查时钟频率是否匹配。
4.3.3 完整读写时序
写操作和读操作的流程不太一样,我习惯用表格来对比:
| 步骤 | 写操作 | 读操作 |
|---|---|---|
| 1 | 起始条件 | 起始条件 |
| 2 | 发送从设备地址 + W (0) | 发送从设备地址 + W (0) |
| 3 | 等待ACK | 等待ACK |
| 4 | 发送寄存器地址 | 发送寄存器地址 |
| 5 | 等待ACK | 等待ACK |
| 6 | 发送数据字节 | 重复起始条件 |
| 7 | 等待ACK | 发送从设备地址 + R (1) |
| 8 | 重复步骤6-7直到数据发完 | 等待ACK |
| 9 | 停止条件 | 读取数据字节,发送ACK/NACK |
| 10 | - | 重复步骤9直到读完,最后发NACK |
| 11 | - | 停止条件 |
读操作比写操作多了一个“重复起始条件”。为什么要重复起始?因为总线方向要切换——从写寄存器地址切换到读数据。如果不重新起始,总线会一直处于写方向,没法读。
4.4 Linux I2C子系统:i2c_adapter与i2c_client
Linux内核的I2C子系统,说白了就是一套框架,把I2C控制器和I2C设备抽象成两个核心数据结构:i2c_adapter和i2c_client。
4.4.1 i2c_adapter:I2C控制器抽象
i2c_adapter代表一个I2C总线控制器。它负责:
- 管理总线上的时钟和时序
- 实现主设备的数据收发
- 处理总线仲裁和错误
每个i2c_adapter都有一个唯一的编号(nr),对应系统中的一条I2C总线。比如i2c-0、i2c-1。
struct i2c_adapter {
struct module *owner;
unsigned int class; /* 适配器支持的设备类 */
const struct i2c_algorithm *algo; /* 算法实现 */
void *algo_data;
/* 数据字段 */
struct rt_mutex bus_lock;
int timeout; /* 超时时间,单位jiffies */
int retries; /* 重试次数 */
struct device dev; /* 设备模型 */
int nr; /* 总线编号 */
char name[48]; /* 适配器名称 */
struct completion dev_released;
struct mutex userspace_clients_lock;
struct list_head userspace_clients;
};
这里最关键的是i2c_algorithm,它定义了实际的数据收发函数:
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr, unsigned short flags,
char read_write, u8 command, int size, union i2c_smbus_data *data);
u32 (*functionality)(struct i2c_adapter *adap);
};
嗯,master_xfer是核心函数,所有I2C数据传输最终都会调用它。你写驱动时,主要就是实现这个函数。
4.4.2 i2c_client:I2C设备抽象
i2c_client代表一个挂在I2C总线上的从设备。它包含设备的地址、驱动绑定信息等。
struct i2c_client {
unsigned short flags; /* 标志位,如10位地址标志 */
unsigned short addr; /* 从设备地址,7位或10位 */
char name[I2C_NAME_SIZE]; /* 设备名称 */
struct i2c_adapter *adapter; /* 所属的适配器 */
struct device dev; /* 设备模型 */
int irq; /* 中断号,如果有的话 */
struct list_head detected; /* 用于设备检测 */
};
我的习惯:写I2C设备驱动时,我一般先通过i2c_new_client_device注册设备,然后在probe函数里获取i2c_client指针。之后所有的读写操作都通过i2c_client来发起——用i2c_master_send和i2c_master_recv,或者更灵活的i2c_transfer。
4.4.3 驱动开发流程
写一个I2C设备驱动,大致分三步:
- 定义i2c_driver:填充probe、remove、id_table等回调
- 注册驱动:调用
i2c_add_driver将驱动注册到内核 - 实现读写:在probe中获取client,通过i2c_transfer进行数据交换
static const struct i2c_device_id mydevice_id[] = {
{ "my_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, mydevice_id);
static int mydevice_probe(struct i2c_client *client)
{
struct mydevice_data *data;
int ret;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
/* 初始化设备 */
ret = mydevice_init(data);
if (ret)
return ret;
return 0;
}
static struct i2c_driver mydevice_driver = {
.driver = {
.name = "my_sensor",
},
.probe = mydevice_probe,
.remove = mydevice_remove,
.id_table = mydevice_id,
};
module_i2c_driver(mydevice_driver);
避坑指南:我曾经在probe函数里直接调用i2c_transfer,结果系统卡死了。为什么?因为probe执行时,I2C总线可能还没完全初始化。正确的做法是在probe里只做资源分配和注册,真正的硬件初始化放到一个工作队列或定时器里延迟执行。
4.5 总结
I2C协议看似简单,但细节很多。从总线架构到地址模式,从时序到Linux子系统,每一步都有坑。我个人觉得,掌握I2C的关键在于理解“主从关系”和“时序约束”——主设备控制一切,从设备只能被动响应。写驱动时,多看看芯片手册的时序图,多想想ACK/NACK的含义,很多问题就能迎刃而解。