干过一段时间嵌入式 Linux 驱动开发的人,对 I2C 总线大概都是又爱又恨。爱的是它结构简单,两根线就能挂上一堆外设,恨的是真调起问题来,波形、地址、时序、设备树各种因素搅在一起,没点系统性认识很容易被绕进去。我当年从裸机 I2C 程序跳到 Linux 驱动,第一反应是"这不就是打开设备、写几个寄存器吗",后来被实际项目教育了几次,才认识到 Linux 下的 I2C 总线驱动是一个分层分得很清楚的框架,每个环节都有自己的职责边界,乱踩边界就会出各种奇怪问题。这篇东西不打算把内核源码逐行抄一遍,而是想从我实际做过的传感器、存储芯片、触摸屏这类外设移植和调试经验出发,把 I2C 总线驱动的整体脉络、核心数据结构和实操要点讲透,尤其是那些文档里不会明说、但踩坑之后才真正理解的东西。
适合谁看呢?正在接触 Linux 驱动开发但被 I2C 框架绕晕的初学者,以及能把设备调通但遇到"偶发性通信失败""probe 不到设备"这类问题就头皮发麻的同学。我会尽量把概念、代码、实操顺序和排障思路串成一条线,保证你看完能对"I2C 总线驱动到底由几块组成、每块该干什么、遇到问题往哪个方向排查"有一个清晰的地图。
1. 先把 I2C 总线的框架拆开,别再把驱动混为一谈
1.1 总线驱动、适配器、客户端各自的职责
很多人刚开始看内核代码时,会在目录drivers/i2c/下面看到一堆文件,很容易糊涂。其实先记住一个结论:Linux 的 I2C 框架由三大部分组成,它们各管一段。
第一部分是I2C 总线控制器驱动(Adapter Driver),它直接面对的是芯片上的硬件 I2C 控制器,也就是主机控制器(Controller)。这块驱动要做的事情是初始化控制器的时钟、中断、引脚配置,把寄存器操作封装成可收发数据的函数,并注册成一个i2c_adapter。你可以把它理解为"总线本身"的驱动,它不知道总线上挂了几个设备,也不关心设备是谁,只负责把一条条传输请求真实地发到物理总线上。
第二部分是I2C 客户端驱动(Client Driver),它面对的是挂在总线上的具体外设,比如温度传感器、EEPROM、触摸屏控制芯片。这块驱动要处理的是设备的寄存器地址、配置流程、数据格式,并且只通过通用的i2c_transfer或者 SMBus 接口向适配器发请求。真正的硬件细节在适配器那边完成,客户端驱动不关心控制器寄存器长什么样。
第三部分是I2C 核心(I2C Core),它管的是"适配器和客户端如何匹配""总线锁该怎么上""设备如何注册到框架里"这些公共逻辑。核心层提供了枚举设备、注册驱动、解析设备树的标准过程,让适配器和客户端两边不直接耦合。
这三个角色一句话总结:核心负责牵线,适配器负责发消息,客户端负责解释消息内容。
1.2 为什么要把控制器和设备拆成两套驱动
有些从裸机转过来的朋友会不理解:以前写单片机 I2C 程序,直接在工程里写一个I2C_WriteReg(addr, reg, val)函数,控制器操作和设备流程全在一块,干净利索。为什么 Linux 非要拆成两层?
一个很实际的原因是"共用"和"替换"。同一颗主控芯片,今天挂温度传感器,明天挂加速度计,后天换成一款新的触摸屏。如果设备逻辑和控制器逻辑耦合在一起,每次换外设都要把总线的底层寄存器代码重新抄一遍,出错的概率会成倍上升。拆开之后,控制器驱动写一次,被多个客户端驱动复用;客户端驱动针对具体芯片写,可以挂在不同主控上,只要对方的适配器接口符合内核规范。这也是为什么你在drivers/i2c/busses/里能看到针对各种主控的适配器驱动,又在drivers/i2c/chips/或各种厂商驱动目录下找到一大堆客户端驱动,它们通过struct i2c_adapter和struct i2c_client松耦合连接。
这里还要说一个容易混淆的点:很多人把"总线驱动"理解成那根"总线"本身,以为要写一个驱动程序去管理两根电线。实际上内核里的i2c_bus只是一个抽象的总线类型,负责给挂在同一控制器下的客户端提供一个匹配和管理的空间。你真正要写的驱动,要么是适配器驱动(很少,除非你换主控或控制器硬件比较特殊),要么是客户端驱动(绝大多数项目的工作量都在这)。搞清了这个边界,后面看哪份源码、改哪个文件就一目了然。
2. 适配器驱动是怎么"撑起"一条总线的
2.1 核心结构:i2c_adapter 与 i2c_algorithm
适配器驱动在代码里的核心是struct i2c_adapter,但光有这个结构还不够,它必须带一个struct 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 传输的入口,客户端驱动调用一次i2c_transfer时,最终会走到这里。smbus_xfer是SMBus 通信模式的专用入口,很多电源管理芯片和电池芯片用的是 SMBus 协议,可以理解为 I2C 的一种严格变体。functionality则用来告诉核心层和客户端,这个适配器支持哪些传输功能,比如是否支持 10 位地址、是否支持块读取。
适配器驱动要做的,就是把这片 SoC 上 I2C 控制器的初始化、时钟设置、中断注册、收发状态机都封装好,然后在master_xfer里根据传入的struct i2c_msg数组,去操作控制器的发送/接收寄存器和状态寄存器,一条条把消息发完,最终返回成功发送的消息条数。这里有个特别重要的约定:返回值的细节决定了总线行为的正确性。如果所有消息都完成,就返回num;如果中途仲裁失败或 NACK,要返回负的错误码;一条都没发出时返回 0。内核上层就是依赖这个返回值来判断重试策略的。
2.2 适配器的注册与解除注册
写完这些函数,接下来是注册动作。一般通过i2c_add_adapter()把适配器加入系统,并把它和设备模型关联起来。实际代码常见流程如下:
static struct i2c_algorithm my_controller_algo = { .master_xfer = my_master_xfer, .functionality = my_functionality, }; static struct i2c_adapter my_adap = { .owner = THIS_MODULE, .class = I2C_CLASS_HWMON | I2C_CLASS_SPD, .algo = &my_controller_algo, .dev.parent = &pdev->dev, .name = "my-i2c-controller", }; static int my_controller_probe(struct platform_device *pdev) { struct my_i2c_dev *i2c = platform_get_drvdata(pdev); ... i2c->adap = my_adap; i2c->adap.nr = pdev->id >= 0 ? pdev->id : 0; i2c->adap.dev.of_node = pdev->dev.of_node; return i2c_add_adapter(&i2c->adap); }这里我要特别提一句.class字段。它声明的是这个总线上可能挂什么类型的设备,比如I2C_CLASS_HWMON表示可能挂硬件监测外设。你如果让老式的"内核全总线探测"机制工作,这个字段会参与设备地址扫描。但现在大部分设备都走设备树,class对实际枚举的影响已经弱了很多,可它也不是完全没意义,用户空间的某些自动加载机制还会看它。我见过有人把所有客户端都改成设备树匹配后,把class随手清成 0,结果某些平台在系统启动阶段探测设备时行为变得非常奇怪,所以建议保留合理的 class,别乱删。
2.3 设备树如何把适配器和外设接起来
设备树在现代 Linux I2C 驱动开发中占了绝对主导地位。一个典型的设备树节点长这样:
&i2c1 { clock-frequency = <400000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c1>; status = "okay"; sensor@48 { compatible = "vendor,temperature-sensor"; reg = <0x48>; vdd-supply = <®_3v3>; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; }; };适配器驱动在 probe 时会通过i2c_add_adapter把dev.of_node关联到总线上。之后核心层会在系统解析设备树时,遍历i2c1节点下的每个@地址子节点,为每个外设创建一个struct i2c_client并在合适的时机调用对应的客户端驱动 probe。整个流程是自动的,这也是为什么"移植一个新外设"在现代驱动开发里往往意味着"写一个匹配 compatible 的客户端驱动 + 在设备树里加一个节点"。
注意:设备树里
reg填的是 7 位从机地址,不要填成带读写位拼出来的 8 位值。比如芯片手册写地址 0x90(含读写位的写法),实际在设备树里应该填 0x48。这地方我见过不止一个同事踩坑,规则就一条:内核里 I2C 地址一律以 7 位为准。
3. 客户端驱动的核心细节与 API 实操
3.1 i2c_client、i2c_driver 和 probe 的匹配关系
客户端驱动核心是两个结构体:struct i2c_client和struct i2c_driver。前者代表一个真实的、挂在总线上的外设实例,里面有addr、adapter、dev、name等字段;后者代表驱动本身,里面有probe、remove、id_table和detect等回调。
匹配过程其实很直接:当核心层创建设备时,会检查设备的compatible字符串是否在该总线上已注册的某个i2c_driver的of_match_table里。匹配命中后,系统会调用驱动的probe函数。所以客户端驱动最基本的骨架是:
static const struct of_device_id my_sensor_of_match[] = { { .compatible = "vendor,temperature-sensor" }, { } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static int my_sensor_probe(struct i2c_client *client) { struct my_sensor_data *data; struct device *dev = &client->dev; data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >struct device *dev = &client->dev; reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH); int_pin = devm_gpiod_get_optional(dev, "int", GPIOD_IN); vdd = devm_regulator_get_optional(dev, "vdd"); rate = device_property_read_u32(dev, "poll-interval-ms", &val); irq = client->irq; // 中断资源由核心根据 interrupts 属性自动挂到 clientgpiod系列函数读节点里带-gpios后缀的属性,regulator框架读*-supply属性。读取失败时函数会返回错误指针,可以用devm_版本省去手动释放。这里要留个心眼:只要可选的属性没失败就继续跑,别把get_optional的错误当成致命错误处理。有些芯片没有复位脚,你非要在驱动里强制要求"reset"引脚存在,probe 就会因为找不到属性直接失败,这在其他板卡上复用驱动时就显得很蠢。
3.3 数据收发:i2c_transfer 和 SMBus 的取舍
客户端驱动里真正跑数据的地方,一般是i2c_transfer和 SMBus 辅助函数。i2c_transfer是最底层的通用接口,传入一个struct i2c_msg数组:
struct i2c_msg { __u16 addr; // 从机地址,7位 __u16 flags; // 0 表示写,I2C_M_RD 表示读 __u16 len; // 数据长度 __u8 *buf; // 数据缓冲 }; static int sensor_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] = { reg, val }; struct i2c_msg msgs[] = { { .addr = client->addr, .flags = 0, .len = 2, .buf = buf }, }; int ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs)); if (ret != ARRAY_SIZE(msgs)) return ret < 0 ? ret : -EIO; return 0; }这个模式是"先写寄存器地址,再由控制器直接读回数据",在 I2C 协议里是通过"重复起始条件(Restart)"连接的,内核会按同一批次i2c_msg自动处理。注意我这里把两段消息放在同一个数组里,数量和最后返回值要仔细核对。i2c_transfer返回的是成功处理的消息数目,不是字节数。拿到返回值先和ARRAY_SIZE(msgs)比较,不等再判断正负,这种写法能避免很多误判。
SMBus 接口在linux/i2c.h里封装得更加友好。比如i2c_smbus_read_byte_data(client, reg)、i2c_smbus_write_byte_data(client, reg, val)、i2c_smbus_read_block_data(),适合处理大多数遵循 SMBus 格式的存储器件和传感器。它们的底层会调用adapter的smbus_xfer,如果没有实现这个回调,就退化走master_xfer拼成普通 I2C 消息再做模拟。只要芯片协议没有超出 SMBus 定义的范围,用这类接口比手动构造i2c_msg省事得多,代码可读性也高。
至于选哪种,我的原则是这样:寄存器访问一律优先i2c_smbus_*系列,因为简洁清晰;涉及到页写、连续读、或一次操作多条消息时,直接上i2c_transfer。但要注意,某些时序敏感的芯片,比如部分触摸屏控制器的固件升级模式,对中间是否会多插入一个停止条件非常敏感,SMBus 模拟路径可能不满足,这时必须使用显式的i2c_msg数组来强控消息边界。
4. 从零写一个客户端驱动的实操流程
4.1 最小可用版本的落地步骤
我不爱对着空文件教人怎么写,干脆拿一个典型的"某环境传感器"假设需求来演示。芯片只有一个寄存器地址,一个 ID 寄存器,一个温湿度数据寄存器。你拿到的需求可能是"让它在系统里表现为一个 hwmon 或 IIO 设备",这里为了聚焦 I2C 部分,先用最简单的方式把数据读出来放到 sysfs 里。
一步步来:
- 确认设备树:找到对应 I2C 控制器节点,增加子节点
sensor@48,compatible填一个内核驱动能匹配的字符串。 - 新建驱动文件:在
drivers/i2c/chips/下或厂商目录下建my_sensor.c,写好of_match_table、i2c_driver结构体和module_i2c_driver。 - 实现
probe:读取芯片 ID 校验匹配;保存client指针;注册sysfs接口或提交到 IIO/hwmon 框架。 - 实现读数据函数:利用
i2c_smbus_read_word_data读回 16 位数据,再根据手册拆出温度和湿度字段。 - 编译、运行、验证:启动内核后查看
ls /sys/bus/i2c/devices/下是否出现2-0048这类目录,再用cat读取自定义文件。
这里我想强调第 1 步里容易翻车的地方:设备树里address-cells和size-cells的写法取决于上级节点,reg只写地址不写长度一般没问题,但如果你直接把别的平台整棵i2c1节点拷贝过来,很有可能会带上冲突的时钟或 pinmux 配置。我建议只拷贝最小子节点,适配器父节点的配置单独核对。
4.2 老式无设备树设备的探测路径:detect 与 addr_list
如果你的内核跑在没有设备树或者不支持设备树的古老环境里,还有一个老派机制叫"在全总线上按地址探测"。i2c_driver结构里有两个相关字段:class和detect。当适配器的class与i2c_driver.class有交集时,内核会对驱动指定的addr_list里的每个地址发一个探测消息,然后调用detect回调来进一步确认设备。
static const unsigned short my_sensor_addr_list[] = { 0x48, 0x49, I2C_CLIENT_END }; static int my_sensor_detect(struct i2c_client *client, struct i2c_board_info *info) { struct i2c_adapter *adapter = client->adapter; if (i2c_smbus_read_byte_data(client, MY_SENSOR_ID_REG) != 0x1234) return -ENODEV; strscpy(info->type, "my-sensor", sizeof(info->type)); return 0; } static struct i2c_driver my_sensor_driver = { .class = I2C_CLASS_HWMON, .detect = my_sensor_detect, .address_list = my_sensor_addr_list, ... };这个机制有个明显的副作用——探测时会对每个地址实际发起传输。如果总线上某个地址恰好被一个"一旦收到数据就执行动作"的设备占用,探测就可能引发不可预期行为。所以现代内核开发中,只要带设备树,我就尽量不依赖detect探测,让设备树直接生成i2c_client。老方案不是不能用,只是你得清楚它的代价。
4.3 上线验证:从日志到波形的一整套检查
写完驱动加完设备树,不代表事情结束了。上板后第一件事是确认总线枚举是否成功:
ls /sys/bus/i2c/devices/ i2cdetect -l i2cdetect -y 2i2cdetect -y 2会在第 2 号总线上扫描所有地址。如果地址 0x48 位置出现48,说明总线能通信且设备回复 ACK。如果显示--或者UU,需要分别处理:--代表 NACK 或者无设备,优先确认电源、上拉电阻和从机地址;UU表示该地址已经被某个驱动占用,这本身不是错误,但如果你扫描时觉得"明明设备在,怎么标了 UU",说明要查注册流程里i2c_new_device或设备树节点是否重复声明了地址。
设备枚举成功后,我还会用逻辑分析仪抓一次实际波形。这是我认为最靠谱的验证方式。观察启动阶段是否出现 START、设备地址字节、ACK、寄存器地址、数据字节、STOP 的完整序列。使用 400kHz 传输时,时钟高电平宽度约 1.2 微秒,低电平也要相应成比例,如果看到时钟高电平被拉得很短、SDA 跳变点贴近 SCL 边沿,往往是上拉电阻阻值偏大或总线电容过高导致的边沿变缓。
注意:总线空闲时 SDA 和 SCL 都应该处于高电平,这是由外部上拉电阻保证的。上板发现总线忙时既可能是别的主机正在通信,也可能是某根线被故障设备拉死。这时优先断电检查硬件,再考虑软件问题,顺序不要反了。
5. 用户态调试与内核态驱动的边界
5.1 用 i2c-dev 和用户态工具快速确认芯片行为
调试 I2C 外设时,最痛苦的事是"第一个版本驱动还没写完,芯片行为就先要摸清楚"。这个阶段没必要急着写内核代码。Linux 专门开放了i2c-dev用户态接口,内核编译CONFIG_I2C_CHARDEV后,每个适配器会对应一个/dev/i2c-N节点。用户态可以用标准open、ioctl、read、write直接操作总线上任意地址,配合命令行工具i2cget、i2cset、i2cdump能很快验证芯片手册上的寄存器行为。
例如:
# 从 2 号总线的 0x48 设备读取 0x00 寄存器 i2cget -y 2 0x48 0x00 # 向某个控制寄存器写值 i2cset -y 2 0x48 0x10 0x01在写正式内核驱动之前,我一般会先拿这些命令把芯片完整"盘"一遍:能不能读到 ID、寄存器地址是递增还是固定、写寄存器后数据是否立即生效、有没有需要等待的转换时间。这些信息虽然芯片手册会写,但实际芯片行为与手册有出入的情况多得是,用用户态工具在真机上验证过,驱动 probe 的把握就大了很多。
5.2 什么时候必须写内核驱动,什么时候留在用户态更划算
也许你会想:既然用户态已经能操作 I2C,那我还写内核驱动干什么?这个选择要具体看场景。
如果你的外设只是"项目上线前调试一下",或者设备数量少、访问频率低、不需要和内核其他子系统协同,直接用i2c-dev在用户态写一个小工具完全够用。可一旦设备要面向生产环境、要支持 poll 中断、要注册成输入子系统或 hwmon、要和电源管理框架联动、要被多个进程安全共享,那就必须写内核驱动。内核驱动带来的不仅是性能,更关键的是统一的设备模型生命周期管理、中断上下文的处理能力,以及通过设备树声明依赖资源后自动解决时序问题的能力。
我自己踩过一个教训:为了图快,把一个小传感器的读取逻辑全部放在用户态脚本里跑,后来系统休眠唤醒流程里需要重新初始化传感器,用户态脚本拿不到内核休眠通知的时序,导致唤醒后传感器状态不一致,最后还是回归内核驱动解决。凡是和系统级电源管理、休眠唤醒、中断事件绑定的外设,老老实实走内核驱动才是正路。
6. 常见问题与排查技巧实录
6.1 总线不通时的几个经典信号
驱动调试中,I2C 不通的现场表现很固定,基本逃不出下面几种:
- probe 里读 ID 失败,返回 -ENODEV:优先看设备地址是否有误,其次看供电稳定。
- i2cdetect 显示全
--:说明对应地址没有 ACK。先量总线静态电平,SDA 和 SCL 是否都在高;再查设备是否处于复位状态、使能脚是否拉对。 - i2cdetect 一扫描就死机卡住:多半是总线上存在时钟拉伸问题,或者某个设备响应异常,把 SCL 拉低不放,导致主机一直等待。这种情况需要示波器同时抓 SCL 波形看拉低时间,必要时降低通信速率到 100kHz 再试。
- 扫描时某个地址总是跳变,时而有时而无:可能是总线上有多个设备地址重叠,或者设备内部工作状态会导致 ACK 不稳定,比如电源欠压、晶振没起振。这种问题最容易让人误判软件,建议先从电源纹波查。
6.2 一张表把典型问题和处理方向理清
我习惯把常见问题整理成一张速查表,排查时对照着走,效率高很多:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 所有地址都 NACK | 上拉电阻缺失、总线电容过大、设备未上电 | 量 SCL/SDA 静态电平,检查电源和上拉 |
| 单个地址 NACK | 从机地址配置错误、设备未使能 | 核对设备树 reg/硬件拨码,查设备手册 bit0 位处理 |
| 偶发通信失败 | 供电波动、速率过高、边沿过缓 | 降速到 100kHz 试,抓波形看时序余量 |
| probe 里读到全 0xFF | 设备不在线、复位引脚未拉高 | 检查使能和复位逻辑,先用户态 i2cget 验证 |
| probe 里读到全 0x00 | SDA 可能被拉死,或设备处于错误模式 | 断电重新上电,检查是否有地址冲突 |
| 能读到设备但写寄存器无效 | 写使能时序未满足、寄存器地址大小端问题 | 对比手册时序图,用 i2cset 逐步验证 |
| 总线扫描出现 UU | 该地址已被驱动占用 | 确认设备树节点重复或先卸载占用驱动 |
这张表不能覆盖所有场景,但能覆盖掉大概九成的 I2C 初查工作。遇到表外问题,我的经验是先抓波形再动代码,别凭空猜。
6.3 几条写在最后的排障心得
最后分享几个我个人的习惯,未必是教科书上的标准做法,但实战中确实救过我很多次。
第一,任何 I2C 通信问题,先确认地址没有算错。7 位地址、8 位地址、带读写位的地址,这三者之间差一个 bit,非常容易写错。我后来在代码里都用宏定义统一管理设备地址,并注释清楚来源,避免临时换算。
第二,调试用的打印要带上下文。不要只在错误路径打一行i2c error。要把寄存器地址、期望值、实际读到的值、传输返回码一起打印,这样一次串口日志就能定位问题,否则就得反复加打印重新编译,效率太低。内核里dev_dbg、dev_err的格式可以参考这种风格:
dev_err(&client->dev, "read chip id failed: reg=%02x ret=%d id=%04x\n", reg, ret, id);第三,当总线行为诡异时,把通信速率主动降下来试。很多硬件问题在低速下会消失,这能快速区分是时序不满足还是芯片逻辑错误。软件上可以在设备树clock-frequency或适配器初始化里临时改成 100000,有效排除边沿过缓的问题。
第四,善用CONFIG_I2C_DEBUG_BUS和CONFIG_I2C_DEBUG_CORE。开启后内核会在每次传输前后打印消息数组和返回值,这对理解i2c_transfer实际发了几条消息、每条消息的目标地址和长度,是非常直观的辅助。缺点是日志量很大,调试完记得关掉。
结尾
这套 I2C 总线驱动的框架思路,我用在不止一个项目上,从最初的传感器读取,到后来调触摸屏的固件升级流程,再到排查电池管理芯片的 SMBus 通信问题,基本上都靠"适配器管传输、客户端管语义、核心管匹配"这个主线来定位方向。真要说最深的体会,那就是别让设备树、地址、上拉电阻这些基础问题把精力消耗光,先把地图画清楚,再一头扎进细节。
最后再分享一个小技巧:如果你在调试一个没见过的新传感器,拿到开发板的第一天,不要急着写驱动文件,先用i2cdetect扫描地址,再用i2cget/i2cset把芯片手册里的关键寄存器全部访问一遍,把结果截图存档。这个"先验真机行为,再写软件逻辑"的顺序,能帮你省掉后面至少一半的排障时间。I2C 这个东西,说难是难在细节多,说简单也简单,因为只要总线物理正常,绝大多数问题都能通过一步一步的读写实验定位出来。