☰
Linux内核I2C驱动核心结构体与数据通路详解
2026/10/19 1:19:16 网站建设 项目流程

搞 Linux 内核驱动的人,十有八九都给 I2C 外设写过驱动。这总线串两个电阻,两根线,看 data sheet 上时序图好像也不复杂,可真在内核里走一遍数据通路,很多人才发现 I2C 并不是一个简单的read()/write()函数就能带过的东西。如果你打算在 Linux 下深入搞 I2C 内核驱动,不管是温度传感器、触摸屏控制器还是电源管理芯片,第一件事不是急着复制示例代码,而是把i2c_adapter、i2c_algorithm、i2c_client、i2c_driver这几个核心角色之间的关系理清。这篇文章算是我多年调 I2C 的经验沉淀,适合想读懂 Linux I2C 内核驱动源码的开发者,也适合准备自己写 adapter 驱动、却总被i2c-core绕晕的人。读完你至少会明白:一次 I2C 读写在 Linux 里究竟经历了什么,出问题该优先去哪个模块找原因。

1. 这层框架到底在解决什么问题

1.1 低层硬件到处都是,但没人希望上层面对裸寄存器

I2C 总线在物理上只有两条线:SCL 和 SDA。嵌入式主控里通常不止一个 I2C 控制器,每个控制器可能挂着温度传感器、EEPROM、PMIC、触摸屏等一堆设备。如果每写一个外设驱动,就要求它直接操作控制器寄存器,那必然是一场灾难:每个 SoC 控制器寄存器布局不同,每个外设的驱动都得为不同平台维护一套底层收发代码,代码爆炸,维护成本飙升。

内核在这里选择的做法和 PCI、USB 一样,加一层“中介抽象”,也就是 I2C 核心模块。控制器驱动负责把自家控制器包装成统一的i2c_adapter,实现标准的收发回调;设备驱动只负责描述“我是什么设备、我要通过标准 API 和数据交换”。这样一来,同一个外设驱动,在甲板子上用寄存器映射的 I2C 控制器,到乙板子上用 GPIO 模拟的 I2C 控制器,驱动代码一行都不用改。这是整个框架存在的最大价值:隔离变化,统一语义。

1.2 内核里哪些代码是干什么的

看 Linux 源码,I2C 相关代码主要集中在drivers/i2c/目录和include/linux/i2c.h。搞懂这几块比直接一头扎进某一家的 controller 驱动更有用。

  • i2c-core-base.c:I2C 核心框架,负责 adapter/client/driver 的注册、匹配、注销和总线的加锁调度。
  • i2c-core-smbus.c:SMBus 协议的封装,很多 I2C 设备走的是 SMBus 风格的读寄存器命令。
  • i2c-dev.c:把 adapter 暴露成/dev/i2c-N字符设备的桥,方便用户空间用 i2c-tools 直接调试。
  • algos/i2c-algo-bit.c:纯软件 GPIO 模拟 I2C 时序的算法层,如果你的主控没有硬件控制器,就能挂这套 bit-bang 算法。
  • 各平台有些i2c-xxx.c文件,这些是硬件控制器驱动,负责把具体的寄存器操作封装成标准回调。

我建议新手先看i2c-core-base.c里的i2c_transfer()和i2c_add_adapter(),把一条主流程跑通,之后再看具体控制器驱动会快很多。核心框架不理解,看到 SoC 厂商的 BSP 驱动就会觉得满屏都是寄存器读写,完全抓不住重点。

1.3 一次 I2C 数据请求的完整旅行

从设备驱动发起一次读操作,到总线上串行出现波形,完整路径大体是这样的:

  1. 设备驱动调用i2c_transfer(),传入 adapter 指针、struct i2c_msg数组和消息数量。
  2. i2c-core获取总线锁,防止多个设备驱动同时使用同一条 I2C 总线造成事务交叉。
  3. 核心层把请求路由到 adapter 的i2c_algorithm->master_xfer()回调。
  4. 控制器驱动(或 bit-bang 算法)根据 msg 数组中的地址、标志位、长度,构造 start 条件、地址帧、数据帧、stop 条件。
  5. 硬件把电平发送到 SCL/SDA 总线上,控制器驱动等待传输完成,或者通过中断/轮询读取结果。
  6. 返回已经成功传输的 msg 数量,或者负数错误码;i2c-core释放总线锁,错误向上层传播。

很多人误以为i2c_transfer()是“直接操作硬件”的函数,实际上它更像一个交通枢纽。真正蹲在底层搬电平的,是master_xfer回调。理解了这条路径,后面排查问题就知道该去哪层加日志。

2. 绕不开的核心结构体:每个字段都不是白给的

2.1 i2c_adapter:控制器的“接口卡”

struct i2c_adapter在 Linux 里代表一个可以挂设备的 I2C 控制器实例。别把它想得太玄,可以类比成电脑上的 PCI 控制器:总线控制器本身是硬件,内核用一个对象描述它,并给上层提供统一操作入口。

adapter 里几个关键字段:

  • owner:通常是THIS_MODULE,防止使用中模块被卸载。
  • algo:指向struct i2c_algorithm,这是 adapter 的功能核心,后面专门讲。
  • algo_data:算法层私有数据,bit-bang 方式经常用它存放 GPIO 句柄和延时参数。
  • class:该 adapter 能支持哪类设备。这是老式非设备树枚举方式下用来“设备发现”的过滤条件,比如I2C_CLASS_HWMON表示可以挂硬件监控类设备。
  • dev.parent:把 adapter 挂到对应的平台设备节点上,sysfs 里就能看到从属关系。
  • nr:总线编号。-1表示让内核自动分配,也可以指定一个编号。
  • name:总线的名字,在/sys/bus/i2c/devices/下会显示成i2c-N的别名。

注册 adapter 过程也不复杂:分配结构体,填好algo、owner、dev.parent,然后调用i2c_add_adapter()。成功后,内核会自动创建一条虚拟总线,并在 sysfs 中创建目录。你在用户空间看到的i2c-0、i2c-1就是这么来的。

2.2 i2c_algorithm:控制器驱动要交作业的地方

adapter 只是个壳,真正决定数据怎么收发的是struct i2c_algorithm。里面最核心的是:

static const struct i2c_algorithm xxx_i2c_algo = { .master_xfer = xxx_i2c_xfer, .smbus_xfer = xxx_i2c_smbus_xfer, .functionality = xxx_i2c_functionality, };

master_xfer是 I2C 数据通路的真正主角。它的签名大概是:

int xxx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

msgs数组可以包含一条或多条消息,内核用一次调用表达一个完整的复合事务。例如“先写寄存器地址,再读数据”这种操作,通常就是两个i2c_msg:第一个 msg 方向是写,第二个 msg 方向是读。控制器驱动要把两条消息串成一个不被打断的 I2C 事务,这就是“repeated START”的典型应用场景。

functionality回调也很重要。它返回 adapter 支持的能力位,比如I2C_FUNC_I2C、I2C_FUNC_SMBUS_READ_BYTE_DATA。上层和用户空间的 i2c-tools 都会通过它判断某些操作能否在这个总线上执行。如果你没实现 smbus_xfer,又返回了 SMBUS 特性,上层就会用通用的 SMBus 模拟流程,最后也还是落到master_xfer。

2.3 i2c_client 和 i2c_driver:设备与驱动的“劳动合同”

struct i2c_client代表“挂在某条 I2C 总线上的一个从设备”,它记录了设备地址、名字、挂在哪个 adapter 上。而struct i2c_driver代表“能够驱动这类从设备的驱动代码”。二者之间的关系,就是内核设备和驱动模型里的“匹配-绑定”。

一般情况下,i2c_client不是设备驱动自己创建的。如果你用设备树描述硬件,内核会扫描 adapter 节点下的子节点,读到 compatible 和 reg 后自动创建对应的i2c_client。然后设备驱动模型会拿着这个 client 的 compatible 去匹配所有已注册的i2c_driver,匹配成功就调用驱动里的probe()函数。

很多新手写了个i2c_driver,插上驱动却发现probe()不执行,绝大多数情况是 client 根本没有被创建,或者 compatible 的属性值两边没对上。可以先查/sys/bus/i2c/devices/下有没有生成N-0048这样的目录,有目录才说明这层“劳动合同”签了一半。

2.4 i2c_msg 和 i2c_transfer:真正搬数据的人

struct i2c_msg是数据包的最小单位。常见字段包括:

  • addr:目标设备地址,通常是 7 位地址。写读操作时,内核和底层会自己处理读写位和左移一位,驱动里拿到的是 7 位地址。
  • flags:方向和控制标志,最重要的是I2C_M_RD,置位表示读,否则表示写。
  • len:缓冲区长度。
  • buf:数据缓冲区。

设备驱动发起一次完整事务,通常这样构造:

struct i2c_msg msgs[2] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = &reg_addr, }, { .addr = client->addr, .flags = I2C_M_RD, .len = 2, .buf = data, }, }; ret = i2c_transfer(client->adapter, msgs, 2);

这里第一条 msg 写寄存器地址,第二条 msg 读两个字节的数据。读者要区分清楚:i2c_transfer负责安排总线锁和调度,i2c_msg负责描述数据语义,master_xfer里的硬件循环才真正按照这些描述把电平翻转出来。

3. 从零写一个 I2C 控制器驱动:关键不是抄结构体

3.1 控制器驱动到底要干哪几件事

写控制器驱动和写设备驱动的思路完全不同。控制器驱动面向的是 SoC 上的 I2C 硬件模块,不是某个传感器。它要做的事情可以总结为四件:

  1. 初始化控制器时钟、引脚、中断。
  2. 实现master_xfer,把i2c_msg翻译成硬件时序。
  3. 注册一个i2c_adapter,让这个控制器被 I2C 核心感知。
  4. 实现错误处理和超时机制。

很多人一上来就纠结寄存器细节,忽略了整体骨架。其实只要先把 adapter 注册起来,再写一个最简单的master_xfer,哪怕只支持“单字节读、单字节写”,这套驱动已经可以配合 i2c-tools 做大量验证了。寄存器细节可以逐步填,骨架错了改起来才痛苦。

3.2 一个最小 adapter 初始化流程

控制器驱动通常是一个平台驱动,在probe()里注册 adapter。核心流程可以精简成下面这个模样:

static int xxx_i2c_probe(struct platform_device *pdev) { struct xxx_i2c_dev *i2c_dev; struct i2c_adapter *adap; struct resource *res; void __iomem *base; int irq, ret; i2c_dev = devm_kzalloc(&pdev->dev, sizeof(*i2c_dev), GFP_KERNEL); if (!i2c_dev) return -ENOMEM; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res); i2c_dev->base = base; adap = &i2c_dev->adapter; i2c_set_adapdata(adap, i2c_dev); adap->owner = THIS_MODULE; adap->class = I2C_CLASS_HWMON; adap->algo = &xxx_i2c_algo; adap->dev.parent = &pdev->dev; adap->nr = -1; strscpy(adap->name, "xxx-i2c", sizeof(adap->name)); ret = i2c_add_adapter(adap); if (ret) return ret; platform_set_drvdata(pdev, i2c_dev); return 0; }

这里有几个看起来不起眼、但非常影响使用的细节:

  • i2c_set_adapdata()把私有结构体挂到 adapter 上,master_xfer里通过i2c_get_adapdata()取回硬件寄存器基地址和状态锁。
  • adap->nr = -1表示让内核自动分配总线号,避免你在多个控制器时手动分配造成冲突。
  • i2c_add_adapter()一旦成功,sysfs 里马上能看到这条总线,设备树里引用的i2c-0、i2c-1编号也可能跟着变。

3.3 master_xfer 怎么写才不容易翻车

master_xfer的内部实现因硬件而异,但思路是差不多的:遍历每个i2c_msg,根据flags & I2C_M_RD决定发起读还是写,然后把块数据拆成一个个字节写进控制器数据寄存器。核心框架负责总线互斥,驱动内部仍要加一把私有的锁,防止并发访问同一控制器时寄存器状态错乱。

一个最小化的框架可以长这样:

static int xxx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct xxx_i2c_dev *i2c_dev = i2c_get_adapdata(adap); int i, ret; mutex_lock(&i2c_dev->bus_lock); for (i = 0; i < num; i++) { if (msgs[i].flags & I2C_M_RD) ret = xxx_i2c_read_bytes(adap, &msgs[i]); else ret = xxx_i2c_write_bytes(adap, &msgs[i]); if (ret < 0) { mutex_unlock(&i2c_dev->bus_lock); return ret; } } mutex_unlock(&i2c_dev->bus_lock); return num; }

最容易犯的错是:在master_xfer里忘记区分“这次传输是否是多 msg 复合事务”。有的控制器对重复 START 需要特殊寄存器配置,如果你只在每个 msg 之间做了 STOP 再 START,严格来说已经是两个独立事务,和上层要求的“先写地址再读数据”语义不一致。某些传感器能容忍这种宽松时序,但很多芯片会出错。写之前一定要看控制器手册有没有“连续传输”或者“重复起始位”相关的位需要设置。

另外,master_xfer返回的必须是“成功处理的 msg 数量”,不是字节数。例如两个 msg 都成功,就应该返回2。返回num这个做法是最稳妥的。

3.4 时序、时钟拉伸和硬件能力匹配

I2C 速率常用 100kbps 标准模式和 400kbps 快速模式,有些还支持 1Mbps。控制器驱动里的时钟分频配置要跟设备树里的clock-frequency一致。如果在快速模式总线上挂了只支持标准模式的慢速设备,通信会随机失败。这种情况下,两个选择:要么把整条总线的频率降下来,要么给慢速设备单独加适配逻辑,后者成本很高,绝大多数项目都会选择降频。

时钟拉伸也是老生常谈的坑。从设备为了等待数据准备好,会主动拉低 SCL,这叫做 clock stretching。如果控制器不支持自动等待,或者中断处理不及时,就会出现偶尔超时、偶发 ACK 错误。遇到这种问题,不要一上来怀疑驱动逻辑,先把总线频率降到 100kbps 试试,往往能快速区分是时序裕量问题还是协议 bug。我在某 ARM 平台上就遇到过类似情况:速率设 400k 时偶尔 -EIO,改成 100k 之后几天都没有复现,最后定位到是传感器对停止条件后的总线释放时间要求特别严格。

4. 写 I2C 设备驱动:你只需要关心怎么描述设备和怎么收发数据

4.1 设备驱动的基本框架

I2C 设备驱动写起来比控制器驱动简单得多,因为不需要面对寄存器时序,只需要注册一个i2c_driver,实现 probe 和读写函数。一个典型驱动框架如下:

static const struct i2c_device_id xxx_ids[] = { { "xxx-chip", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_ids); static const struct of_device_id xxx_of_match[] = { { .compatible = "vendor,xxx-chip" }, { } }; MODULE_DEVICE_TABLE(of, xxx_of_match); static struct i2c_driver xxx_driver = { .probe = xxx_probe, .remove = xxx_remove, .id_table = xxx_ids, .driver = { .name = "xxx-chip", .of_match_table = xxx_of_match, }, }; module_i2c_driver(xxx_driver);

module_i2c_driver()是个封装宏,展开后就是常规的 module_init/module_exit 加i2c_add_driver。

在probe()里,你会拿到struct i2c_client *client。这个结构体里有adapter、addr、name,基本就是你的操作入口。一般会在probe()里用devm_kzalloc分配私有数据,然后i2c_set_clientdata(client, chip)存起来,后面读函数里再用i2c_get_clientdata()取出来。

4.2 设备树里 compatible 和 reg 是如何影响匹配的

设备树里描述一个 I2C 从设备,通常是挂在某个 adapter 节点下:

&i2c2 { status = "okay"; clock-frequency = <400000>; xxx-chip@48 { compatible = "vendor,xxx-chip"; reg = <0x48>; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_LEVEL_LOW>; }; };

compatible的值必须和驱动里of_match_table中的字符串完全一致,一个逗号、一个字母都不能差。内核会把设备树节点解析成一个i2c_client,然后用这个 compatible 去匹配i2c_driver。如果不一致,probe 不会被调用。

reg写的是 7 位地址。这里有个非常经典的坑:很多传感器的 datasheet 上把地址写成一个字节,例如“设备地址 0x50”,实际上那是包含读写位在内的 8 位地址,换算成 7 位地址就是0x50 >> 1 = 0x28。如果你按 8 位地址写进设备树,驱动会去访问错误地址,通信表现为 ACK 异常或者设备找不到。我一开始也中过招,之后每次看到寄存器地址都习惯先问一句:这是 7 位还是 8 位?

4.3 常用 API 的选择策略

设备驱动层常见的 API 有这几种:

  • i2c_master_send(client, buf, len)和i2c_master_recv(client, buf, len):适合单条写、单条读,不需要自己填i2c_msg。
  • i2c_transfer(client->adapter, msgs, num):适合复合事务,比如“先写寄存器地址,再读 N 字节”,是读写传感器寄存器最常用的方式。
  • i2c_smbus_read_byte_data(client, command)、i2c_smbus_write_byte_data()这类:适合 SMBus 风格设备,内部会帮你打包成标准 SMBus 时序。

读一个传感器寄存器,用i2c_transfer的典型写法就是前面展示过的两段 msg。用i2c_master_send先写寄存器号,再用i2c_master_recv读数据,虽然也是可行的,但这两步是独立的 I2C 事务,中间可能被其他设备抢占总线,不够严谨。对于多字节寄存器,或者对时序一致性要求高的器件,建议优先用i2c_transfer一次搞定。

4.4 先别急着写内核驱动:i2c-dev 帮你验证链路

很多开发者一上来就写设备驱动,结果发现通信失败,分不清到底是硬件问题、地址问题还是控制器问题。更快的路径是先开启内核的CONFIG_I2C_CHARDEV,用 i2c-tools 直接操作/dev/i2c-N验证链路:

i2cdetect -l i2cdetect -y 0 i2cget -y 0 0x48 0x00 i2cset -y 0 0x48 0x01 0x02

i2cdetect -l列出系统里所有 I2C adapter,i2cdetect -y 0扫描总线 0 上的设备地址。如果能看到对应地址有响应,说明控制器、上拉电阻、设备都被打通了。这个时候再写内核驱动,心里就有底了。如果 i2c-tools 都通不过,先不要怀疑设备驱动,回头查硬件电路和设备树更实际。

用 i2c-dev 验证只能证明链路通了,不能替代真正的设备驱动。尤其是需要快速连续读写、中断触发、电源管理等场景,最终还是要靠内核驱动来保证事务的一致性和系统行为的可控。

5. 调试方法论:先分清三层问题,再决定动哪里

5.1 把问题拆成硬件、设备驱动、控制器驱动三层

I2C 出问题时,最怕“头痛医头,脚痛医脚”。我习惯把问题分成三层:

  • 硬件层:上拉电阻没接、接线错误、电平不匹配、地址线冲突。
  • 设备驱动层:compatible 不匹配、probe 没执行、API 用错、数据格式不对。
  • 控制器驱动/算法层:时序不对、中断和状态机没处理好、没有正确区分读写标志。

大多数时候,i2c-tools 可以直接帮你定位到第一层还是第三层。硬件层和控制器驱动层的问题在i2cdetect阶段就会暴露;设备驱动层的问题通常是 i2c-tools 能访问,但自己的驱动一跑就报错。

5.2 内核日志和动态调试的正确打开方式

I2C 核心和一部分控制器驱动里有调试日志。内核开启以下配置后在 dmesg 里能捞到不少信息:

  • CONFIG_I2C_DEBUG_CORE:I2C 核心消息。
  • CONFIG_I2C_DEBUG_ALGO:算法层消息,能看到每次 master_xfer 的调用。
  • CONFIG_I2C_DEBUG_BUS:总线驱动日志,也就是控制器驱动的日志。

如果你不想重新编内核,也可以用 dynamic debug 对指定文件开日志:

echo 'file i2c-core-base.c +p' > /sys/kernel/debug/dynamic_debug/control echo 'file drivers/i2c/busses/i2c-xxx.c +p' > /sys/kernel/debug/dynamic_debug/control

前提是驱动代码里用了dev_dbg()之类的动态调试接口,并且内核开启了CONFIG_DYNAMIC_DEBUG。这种方式比重新编译内核快很多,尤其是现场排查问题时,不必等一个小时的编译。

5.3 总线挂死、ACK 错误、probe 不回调排查套路

我整理了一个问题速查表,基本覆盖了我工作中遇到的大多数情况:

现象主要原因第一步排查
i2cdetect所有地址显示XXSDA 被拉低、总线挂死、上拉电阻缺失示波器量 SCL/SDA 静态电平,确认总线有高电平空闲状态
i2cdetect找不到目标地址地址写错、设备供电异常、从设备地址线配置不对查 device tree 的 reg,以及 datasheet 的地址引脚电平
指定地址响应但i2cget返回 -EIO设备 NACK、控制器状态机错误、时钟拉伸降低总线频率,看示波器波形,判断是 ACK 位异常还是数据时序错误
内核驱动 probe 不执行compatible 不匹配、设备树节点没挂到 adapter 下检查/sys/bus/i2c/devices/有没有对应 client,比对 compatible
偶发超时,降频后消失时序裕量不足、从设备 clock stretching 支持不好用慢速模式验证,必要时换硬件或调整控制器时序参数
复合事务中间被插入其他设备访问没用i2c_transfer一次传多个 msg,拆成多次独立调用改成单次i2c_transfer,不要拆成 send/recv

这张表不能直接告诉你根因,但能缩小排查范围。硬件层问题多数是静态电平不对,软件层问题多数是地址和匹配不对,剩下那种“时好时坏”的问题,优先怀疑时序和锁。

5.4 我踩过的几个坑,希望你别再踩一遍

有一次调试一个 EEPROM,设备树 reg 写的是 datasheet 上 8 位地址0xA0,也就是 7 位地址0x50左移后的结果。结果i2cdetect怎么也扫不到,折腾了整整一个下午,最后才发现地址位写多了一位。后来我给自己定了个规矩:凡是 reg 地址,一律先十进制转二进制看一眼,如果最低位经常为 0,几乎可以断定你写的是 8 位地址。

还有一次是 bit-bang 控制器驱动里,master_xfer每处理一个字节都udelay(5),在超高频 CPU 上看起来没问题,但系统开启实时任务后调度延迟变大,偶尔就丢 ACK。后来我改成根据 SCL 高电平和低电平时间分别控制延时,问题就消失了。其实很多 I2C 问题本质上是时序裕量不够,能硬件测波形就测波形,不要全靠猜。

另一个提醒:不要在master_xfer里做过重的休眠或长时间忙等。I2C 事务可能被内核用在上电时序、关机流程里,某些场景下调用上下文不允许长时间阻塞。如果你的控制器没有中断机制,最少也要保证每次轮询超时可配,并且循环里调用 cpu_relax(),避免独占总线。

最后说一个我自己的习惯:新板子调 I2C,我先不写任何内核设备驱动,只用 i2c-tools 把每一个挂在总线上的外设都扫一遍、读写一下关键寄存器,再把设备树 client 是否生成确认一遍,最后才让设备驱动接管。这样每一层都是独立验证的,出了问题不会三方面互相猜疑。相信我,这个流程能帮你省下大量本来要用来抓头发的调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询