☰
嵌入式Linux I2C驱动开发:框架解析、核心数据结构与排障实战
2026/10/13 5:27:51 网站建设 项目流程

干过一段时间嵌入式 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 = <&reg_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 属性自动挂到 client

gpiod系列函数读节点里带-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 里。

一步步来:

  1. 确认设备树:找到对应 I2C 控制器节点,增加子节点sensor@48,compatible填一个内核驱动能匹配的字符串。
  2. 新建驱动文件:在drivers/i2c/chips/下或厂商目录下建my_sensor.c,写好of_match_table、i2c_driver结构体和module_i2c_driver。
  3. 实现probe:读取芯片 ID 校验匹配;保存client指针;注册sysfs接口或提交到 IIO/hwmon 框架。
  4. 实现读数据函数:利用i2c_smbus_read_word_data读回 16 位数据,再根据手册拆出温度和湿度字段。
  5. 编译、运行、验证:启动内核后查看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 2

i2cdetect -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 里读到全 0x00SDA 可能被拉死,或设备处于错误模式断电重新上电,检查是否有地址冲突
能读到设备但写寄存器无效写使能时序未满足、寄存器地址大小端问题对比手册时序图,用 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 这个东西,说难是难在细节多,说简单也简单,因为只要总线物理正常,绝大多数问题都能通过一步一步的读写实验定位出来。

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

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

立即咨询