资讯动态

Linux I2C内核驱动开发实战:从总线架构到设备驱动调试

发布时间:2026/10/19 11:35:50 来源:尧图企业网站定制
做嵌入式Linux开发这些年我最常被问到的问题一半以上都跟I2C有关probe进不去、总线无应答、设备树地址对不上、读回来的寄存器全是0xFF。霸占问题榜第一名的不是多难的硬件而是开发者对I2C内核驱动的理解停留在调i2c_transfer就能跑的层面。I2C几乎是低速总线的默认选择工控板、智能硬件、物联网网关里触摸屏、EEPROM、RTC、PMIC、各种传感器全挂在这两条线上。会几行i2c_read只是入门真正看懂总线驱动、设备驱动、Linux设备模型三者怎么协作才谈得上深入理解。这篇文章我打算按自己踩坑的顺序来写先把I2C子系统整体架构摆清楚再分别拆总线驱动和设备驱动的实现路径然后讲用户态调试和regmap这种提高效率的做法最后把常见故障和排查手法整理成速查表。适合两类人看一类是刚接触内核驱动、想从源码层面理解I2C的开发者另一类是已经能调通驱动但遇到疑难杂症时不知道怎么定位的工程师。准备充足的话配合一块带逻辑分析仪的开发板效果更好。1. 先看清I2C子系统的整体骨架1.1 从两条物理线到Linux设备模型I2C物理层很简单一根数据线SDA、一根时钟线SCL都是开漏输出外部靠上拉电阻拉到高电平。设备通过拉低电平来传输数据所以低电平是驱动状态高电平是释放状态。这种设计天然支持一条总线上挂多个设备也天然支持多主模式谁抢到总线谁说了算。标准速率100kHz快速模式400kHz高速模式3.4MHz再往上就是超快模式1MHz这样的变体。但在Linux里I2C被抽象成了三层实体。最底层是i2c_adapter它代表一个具体的I2C总线控制器也就是负责产生时钟和收发数据的硬件。中间层是i2c_client它代表挂在总线上的某个从设备比如某颗温度传感器。最上层是i2c_driver它管理某个i2c_client对应的驱动逻辑。三者的关系可以用一个生活类比adapter是配电箱client是房间里的插头driver是插头连着的电器。配电箱管送电插头决定该送进哪个房间电器才负责真正干活。搞明白这个模型很多问题就解释通了。你写的是设备驱动核心操作对象叫i2c_client你配置的是总线硬件上对应i2c_adapter。两条线纠缠不了Linux把职责分得很干净。1.2 数据包在内核里的旅行路径一次普通的I2C读取操作从驱动调用到线上波形出现大概经历这么几步设备驱动构造struct i2c_msg数组描述要发送和接收的缓冲区、地址、标志位。调用i2c_transfer传入对应i2c_client和msg数组。i2c_transfer内部先加总线锁防止多进程并发乱掉总线状态。检查该adapter支持的功能集functionality不支持的传输直接拒绝。调用adapter的master_xfer回调这个回调通常属于i2c_algorithm。最终由具体控制器的寄存器操作把数据变成SCL/SDA上的电平跳变。很多驱动开发者直接和步骤1、2打交道但遇到为什么读出来不对时一定要往后走完整个链路。锁在i2c_transfer里功能检查也在i2c_transfer里这些都是标准路径不太会错最容易翻车的是步骤6比如控制器的时序寄存器配错或者中断风暴导致时钟线不干净。波形不对劲往往不是逻辑问题而是硬件时序问题。1.3 源码文件分布从哪几个文件开始啃内核的drivers/i2c目录就是I2C子系统的主战场里面文件不多但每个都有分工文件职责i2c-core.c子系统核心负责适配器注册、设备注册、传输分发i2c-dev.c用户态访问接口提供/dev/i2c-N设备节点i2c-smbus.cSMBus协议扩展实现i2c-boardinfo.c板级I2C设备注册和查找逻辑busses/各种控制器驱动比如bitbang、GPIO模拟、平台自带控制器algos/I2C算法封装主要是i2c-algo-bit、i2c-algo-pca我建议的阅读顺序是先读i2c-core.c里的核心注册接口理解adapter和client怎么配对再挑一个busses里的GPIO bitbang驱动看因为它逻辑最简单相当于把I2C时序用软件重新跑了一遍最后回头看自己手里那颗芯片的控制器驱动这时候硬件寄存器就会和框架对上号。2. 总线驱动适配器怎么落地2.1 i2c_algorithm适配器里的引擎总线驱动要干的活核心就两件事注册一个i2c_adapter以及实现一个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是核心传入的msgs数组可能包含多条消息num是消息数量。这个回调成功时返回传输的消息数量出错时返回负值。smbus_xfer是SMBus专用路径如果你的控制器和从设备都只走标准I2C可以不用实现它但要注意functionality里要把I2C_FUNC_SMBUS_*这些标志都清掉否则应用层调用SMBus接口会得到一个不支持的错误。functionality回调用来声明这个适配器支持哪些能力。举个例子有的控制器支持10位地址有的只支持7位有的支持I2C_FUNC_I2C即普通传输有的只能跑SMBus。i2c_transfer在执行前会做功能检查如果不匹配就直接返回错误码。我看到很多人踩过这个坑用只支持SMBus的适配器去跑普通I2C消息然后抱怨内核不干活。2.2 最小适配器驱动的注册骨架假设你在一颗新芯片上移植内核I2C控制器是芯片自带的IP在Linux里通常写成platform driver。简化后的注册流程大概是static int my_i2c_probe(struct platform_device *pdev) { struct i2c_adapter *adap; struct my_i2c_priv *priv; int irq; adap devm_kzalloc(pdev-dev, sizeof(*adap), GFP_KERNEL); priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); adap-owner THIS_MODULE; adap-class I2C_CLASS_DEPRECATED; adap-algo my_i2c_algo; adap-dev.of_node pdev-dev.of_node; strscpy(adap-name, my-i2c, sizeof(adap-name)); platform_set_drvdata(pdev, priv); return i2c_add_adapter(adap); }几个容易被忽略的细节adap-class这个字段在新内核里已经标记为过时老代码经常设成I2C_CLASS_HWMON或者I2C_CLASS_SPD它的作用是控制用户态探测工具比如i2cdetect是否默认扫描这个总线。不建议再依赖它。dev.of_node一定要赋值内核之后要在这里面挂i2c_client。中断、时钟、复位等控制器本身的资源在上面的骨架里被我省略了实际项目里这些都要正确使能很多总线驱动首次移植失败就失败在时钟没打开导致SCL根本没有波形。2.3 总线号怎么定固定编号还是自动分配i2c_add_adapter和i2c_add_numbered_adapter的区别直接决定你的I2C总线在系统里叫什么名字。前者是动态分配内核根据当前总线编号自动找一个空闲号后者是你明确指定编号比如叫i2c-0还是i2c-3。实际项目中我强烈建议给I2C总线固定编号。原因是设备树里经常会引用某个总线节点用户态工具也经常依赖路径来区分哪条总线上挂了哪一个传感器。如果每次都动态分配升级内核固件后总线号漂移你的开机脚本、监控脚本全部跟着遭殃。固定编号的常见做法是配合设备树aliases节点给每个i2c控制器分配序号aliases { i2c0 i2c0; i2c1 i2c1; };内核在解析设备树时会自动按aliases分配总线号这样你在用户态看到的总线号就和硬件控制器结对排查问题会轻松很多。3. 设备驱动从匹配到收发3.1 i2c_driver注册与probe触发时机设备驱动的入口是i2c_add_driver它本质上是对内核驱动模型的封装。一个标准的i2c_driver包含两个匹配表static const struct of_device_id my_sensor_of_match[] { { .compatible vendor,my-sensor }, { } }; static struct i2c_driver my_sensor_driver { .driver { .name my-sensor, .of_match_table my_sensor_of_match, }, .probe my_sensor_probe, .remove my_sensor_remove, .id_table my_sensor_id_table, };driver里的of_match_table负责和设备树里的节点匹配id_table负责和板级信息匹配老式的i2c_board_info方式。现在主流设备树天下of_match_table基本是标配。很多人对probe触发时机有误解以为只要驱动加载probe立刻跑。其实probe能触发前提是内核已经有对应的i2c_client。i2c_client从哪来两个渠道一是设备树扫描二是i2c_board_info静态注册。设备树扫描发生在适配器注册成功之后内核遍历adapter的of_node找到里面的i2c子节点逐个创建i2c_client。所以你设备树里如果没挂节点驱动probe永远都不会被调用这就是最常见的第一大坑。3.2 i2c_client里面的关键资源i2c_client在probe阶段会被传进来里面最值得关心的几个字段字段含义使用建议addr7位从机地址设备树里写多少这里就是多少adapter所属适配器用它来调用i2c_transferirq从机中断号一般由设备树interrupts解析devstruct device可以在里面保存私有数据i2c_client在probe之后生命周期和device树节点绑定移除设备树节点或拔掉设备可热插拔环境才会触发remove。驱动里要注意irq不一定有效很多传感器是轮询模式没有中断引脚probe里拿irq之前最好先判断大于0。设备树里还有电源、使能脚这样的资源需求它们不会自动变成i2c_client的成员你要在probe里用GPIO控线和regulator接口去获取。时序上要格外小心有些传感器上电以后需要一点稳定时间才能接受I2C命令probe里马上就发起传输会直接拿到NACK。3.3 用i2c_transfer读写一条报文的具体写法i2c_transfer是设备驱动里最常见的调用。以读取一颗EEPROM指定偏移处的数据为例static int my_eeprom_read(struct i2c_client *client, u8 offset, u8 *buf, int len) { struct i2c_msg msgs[2]; int ret; msgs[0].addr client-addr; msgs[0].flags 0; /* 写方向 */ msgs[0].len 1; msgs[0].buf offset; msgs[1].addr client-addr; msgs[1].flags I2C_M_RD; /* 读方向 */ msgs[1].len len; msgs[1].buf buf; ret i2c_transfer(client-adapter, msgs, 2); if (ret ! 2) return ret 0 ? ret : -EIO; return 0; }这里有个概念级的关键点msgs[0]和msgs[1]组合后物理线上会先发地址字节偏移字节随后是重复起始位repeated start然后再发地址读数据。很多从设备协议要求这种写偏移再读数据的组合序列如果分两次i2c_transfer调用中间会被释放总线别的设备有机会插队从设备状态机就可能错乱。正确做法是用一组msgs通过重复起始位把两个阶段连起来。静态缓冲区还是动态缓冲区如果单次传输量小直接塞栈上没问题数据量大或者有DMA参与时需要分配DMA安全的内存。一般传感器读写就几个字节栈上固定的u8数组足够了。3.4 提高可靠性的几个习惯调用i2c_transfer返回的值要严格判断。它返回的是实际完成的消息数量不是字节数。比如你发了2条msgs它返回1代表第一条写成功第二条没执行。这种半途失败的场景重试前必须确认从设备状态已经复位否则很容易读回错位数据。从设备对时钟极性、起始位时序的要求各不相同。有些国产传感器的数据手册对SCL高电平最小宽度写得很死如果你的控制器时钟源配置不佳会出现偶发NACK。这种问题在软件层很难根治优先用逻辑分析仪确认波形再调整控制器频率和时序寄存器。4. 用户态工具与regmap开发效率翻倍的两条近路4.1 在用户态先用ioctl把协议跑通内核配置里打开CONFIG_I2C_CHARDEV就能在/dev下看到i2c-N节点。这些节点由i2c-dev.c支撑它允许用户态进程直接发起I2C传输。最常用的ioctl包括ioctl作用I2C_SLAVE设置从机地址I2C_RDWR一次传入多条msg等价于内核里的组合传输I2C_FUNCS查询适配器能力I2C_SMBUS发起SMBus操作命令行工具里i2cdetect用来扫描总线上的从设备i2cget/i2cset读写单字节或单字i2ctransfer最灵活能构造任意msg序列。我在调试一颗新传感器时一定先在用户态用i2ctransfer把数据手册里的示例波形走一遍确认地址、寄存器、时序都对再写内核驱动。因为用户态环境里改参数不用重新编译模块调试迭代速度快一个数量级。4.2 用户态调试必需知道的局限用户态访问i2c-dev有一个致命问题它绕过了驱动层的资源管理。如果系统的某个内核驱动已经绑定了这个从设备你再用i2c-dev同时操作两条管道的读写会打架。轻则读到脏数据重则让从设备状态机卡死。所以生产系统上i2c-dev通常只留给调试使用或者通过严格的权限管理限制访问。另外ioctl的I2C_SLAVE设置的地址是7位地址千万别把资料里带读写位的8位地址填进来。比如某器件数据手册写0xA0这是包含写标志的地址实际7位地址是0x50填错了直接Address NACK。这个错误经典到几乎每个I2C调试记录里都能见到。4.3 regmap让你不用再手写寄存器操作如果你给传感器写驱动马上会发现几十个寄存器每个都得包一层i2c_transfer代码又臭又长还容易把长度、偏移写错。regmap就是解决这个问题的抽象层。初始化时用regmap_init_i2c传入i2c_client配置好寄存器位宽、值位宽、缓存策略之后读写就用regmap_read/regmap_write。static const struct regmap_config my_sensor_regmap_config { .reg_bits 8, /* 寄存器地址宽度 */ .val_bits 8, /* 寄存器值宽度 */ .read_flag_mask 0x80, /* 有些器件读操作最高位要置1 */ .cache_type REGCACHE_RBTREE, }; static struct regmap *regmap; regmap devm_regmap_init_i2c(client, my_sensor_regmap_config);现代内核驱动里温度传感器、触摸控制器、音频编解码器几乎全部走regmap因为它顺手解决了几个高频难题寄存器缓存和同步、定序锁、多设备并发访问。前提是regmap_config里的参数要和器件手册严格对齐尤其read_flag_mask和write_flag_mask这两个标志掩码很多器件读寄存器时要在第一个字节最高位打一个1没配好的话读出来永远是0。4.4 三种访问方式怎么选方式适合场景缺点原生i2c_transfer协议特别复杂的器件要自己处理缓存、锁、错误重试regmap绝大多数寄存器类传感器不擅长处理复合消息用户态i2c-dev调试阶段、快速验证不安全、无驱动逻辑我自己定了个规矩新器件先用用户态工具验证协议确定要落地成正式驱动后寄存器类器件一律regmap串行EEPROM这类需要大块连续读写的器件用原生i2c_transfer。这样代码可维护性强每个器件驱动只保留差异化的协议逻辑。5. 调试手段、经典实例与自学路径5.1 调试三板斧工具、仪表、内核日志第一板斧是i2cdetect。总线扫描结果可以快速发现问题比如0x50这个地址死活扫不到但示波器上明明有设备在响应那就是地址配置错误。第二板斧是逻辑分析仪或者示波器别省这一步。I2C波形就那么两条线抓一下起始位、停止位、重复起始位、第9个SCL周期的ACK位立刻能判断是地址不对、无上拉、还是从设备没复位。第三板斧是内核日志和ftrace。打开动态调试或给i2c-core加log能看到每次transfer的消息内容ftrace追踪函数调用关系能帮你把probe流程到底走没走完整查得明明白白。我在实际项目中遇到过一种隐蔽问题某传感器的SCK引脚和某个GPIO复用BSP初始化时把这个GPIO默认拉低了导致SCL时钟线一直为低总线上所有设备瞬间消失。这种问题用示波器夹一下SCL到地一目了然但光靠软件日志可能要好几天。5.2 高频故障速查表症状常见原因排查方向i2cdetect扫不到设备7位/8位地址混淆、上拉电阻缺失、从设备未上电确认地址、测量SCL/SDA静态电平probe一直不触发设备树没创建i2c_client、compatible不匹配查/sys/bus/i2c/devices下的节点读回数据全为0xFF从设备不该被选中、NACK后总线挂死抓波形看ACK位检查地址偶尔读错寄存器复合消息被拆成了两次传输确保用一组msgs发出组合传输总线一直busy之前一次操作没正确收尾、SDA被从设备拉住想办法复位总线和从设备检查SDA电平速率跑不上去上拉电阻过大、走线电容过大减小上拉电阻压低总线频率5.3 一张学习路线图想深入内核I2C我建议按下面这个顺序走先把Documentation/i2c/下几个rst文档通读一遍里面写的虽然简单但每一条都是前提常识。精读i2c-core.c的注册和传输两条主线函数重点看i2c_register_adapter和__i2c_transfer。看i2c-algo-bit的实现它是纯软件模拟I2C等于把协议用代码演了一遍读完会对时序原理有肌肉记忆。再看自己芯片平台下的原生控制器驱动对比它是怎么用中断或DMA完成master_xfer的。选一颗简单的传感器比如温度芯片完整走一遍设备树配置、probe、regmap初始化、中断上报的流程。只要这五步走完你对I2C内核驱动的理解就已经超过大多数拿着示例代码拼凑的开发了。再遇到问题心态会完全不一样你知道内核在哪里帮你加了锁在哪里查了能力位在哪里可能会被硬件时序拖垮。做I2C驱动开发时间久了我最大的体会是这类问题九成都不是内核框架的bug而是硬件细节和协议细节没对齐。软件层的i2c_transfer、probe、regmap都是稳定且枯燥的定式真正要求你的是看懂器件手册里的时序图再对照逻辑分析仪的波形去理解内核到底替你做了什么。最后分享一个很小的检查习惯每次写完一个新I2C设备驱动不要急着提交先用i2ctransfer在用户态把数据手册里的典型读写操作全部跑一遍再抓一次完整波形的起始、停止和ACK脉冲确认与手册时序对齐再进入内核封装。这套流程看起来慢实际上能省掉后面至少一周的调试时间。I2C说到底是两条线上的协议把线抓明白了内核也只是帮你把它翻译成代码而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑