Linux I2C驱动实战:从总线模型到设备树与调试技巧 干这行久了你会发现Linux I2C驱动几乎就是嵌入式开发里的“万金油”。板子上挂个EEPROM、加速度计、温湿度传感器都靠这条两线制总线跑数据。Linux I2C驱动说起来很模式化结构常年稳定但真要动手写设备树、总线匹配、并发访问、地址换算这些坑还是能绊倒不少人。这篇文章我以一次实际开发经历为主线从总线模型讲到设备树再给出可运行的最小驱动代码最后把调试手段和踩坑记录一并交代清楚。适合已经会写内核模块基础程序、但对I2C子系统还不太熟悉的朋友看完基本可以独立开始写自己的客户端驱动。1. 先站在总线模型角度看I2C驱动1.1 物理线背后的角色分工I2C物理层简单得离谱就两根线SCL时钟线和SDA数据线。但先别急着写代码把逻辑角色搞清楚比背API更重要。总线上有三个角色主机控制器Adapter、从设备Client、通信算法Algorithm。主机控制器是SoC内部那个I2C外设负责把寄存器配置变成SCL和SDA上的实际时序从设备就是我们天天要驱动的传感器、EEPROM、RTC之类的东西算法这一层则是控制器和上层驱动之间的翻译器决定时钟频率、ACK处理、起始停止位怎么生成一般跟着SoC的IP核走不需要你改。内核里这三位分别对应struct i2c_adapter、struct i2c_client、struct i2c_algorithm。对你来说控制器和算法早就由芯片厂商写好了你真正要打交道的是i2c_client和i2c_driver这两个东西。很多人刚开始学I2C驱动时老往硬件寄存器里钻纠结某个控制器的FIFO怎么配其实方向就偏了。大多数场景下你不需要碰控制器寄存器把总线上“设备”和“驱动”的关系搞定活儿就完成了八成。1.2 总线模型里的三个角色Linux设备模型里最基本的三个概念是bus、device、driver。I2C子系统完全沿用这套框架bus对应i2c总线本身device分成两类一类是控制器设备也就是adapter另一类是挂在i2c总线上的从设备clientdriver自然就是我们自己写的i2c_driver。bus核心负责把device和driver匹配起来匹配成功的瞬间会调用driver里的probe函数这就是一切初始化的入口。这个过程可以打个比方。设备树就像是小区物业的住户登记表写明哪栋楼哪个房间住着谁i2c_driver像物业经理擅长服务某一类住户总线模型就是物业柜台负责在住户和经理之间做匹配。三方能对上才正式签合同上岗。如果住户登记表上写的房号跟经理手里的客户档案对不上那就算住户住在眼前经理也不会去服务他——对应到代码里就是probe永远不执行。1.3 匹配机制probe是怎么被调起来的匹配过程有两种触发方向。系统启动或者设备热插拔时从设备被注册到i2c总线上驱动加载时i2c_driver也被注册到同一条总线。之后总线核心开始遍历比对驱动维护的匹配表和设备携带的信息。主流的匹配依据有三条路径设备树compatible属性与驱动of_match_table中的compatible字符串匹配这是目前ARM平台最常用的一条。驱动id_table里的name字段与从设备名称匹配主要用于无设备树的老式平台或者ACPI环境。通过i2c_new_client_device这类接口手动创建的i2c_client靠device ID信息进行匹配。第一条是你最该盯紧的。很多新人写了of_match_table却忘了在设备树里加compatible或者两边字符串差了一个字母结果probe就是不跑。排查这类问题的诀窍不是反复看代码而是把设备树里的compatible字段和驱动里的匹配表放一起逐字符比对。1.4 你需要写哪一部分看清了模型你的工作范围其实很清楚在设备树里正确声明从设备节点实现一个i2c_driver并注册到i2c总线在probe里完成设备初始化向用户态提供数据接口。adapter和algorithm都不需要碰那是SoC厂商的事。站在这个高度上想问题就不会被一些底层的寄存器实现细节带偏方向写代码的路径也清晰很多。2. 动手前环境、内核配置与设备树2.1 内核选项和基础工具开工前先确认内核把I2C核心支持编进去了。一般需要CONFIG_I2C和对应的控制器驱动具体选项名字随SoC平台变化大多数开发板默认已经开启。开发调试期间还有两个配置建议打开CONFIG_I2C_CHARDEV它会在用户态生成/dev/i2c-N节点方便后面用工具直接扫总线再就是CONFIG_I2C_DEBUG_CORE和CONFIG_I2C_DEBUG_ALGO这类调试开关会在内核日志里打印总线收发细节开发期非常有用。源码方面你主要面对的头文件是include/linux/i2c.h里面定义了全部核心结构体和API。我习惯的做法是随时保持一份与当前内核版本一致的源码树写之前先grep一下相关结构体确认字段没变比硬记API可靠得多。内核版本升级频繁不少接口都有过调整源码本身才是最权威的文档。2.2 设备树里怎么声明一个从设备以一颗挂在一号I2C控制器上的从设备为例一个最简的节点是这样写的i2c1 { status okay; clock-frequency 100000; myi2c18 { compatible vendor,myi2c-dev; reg 0x18; }; };这段话说得很直白我把一颗兼容vendor,myi2c-dev协议描述的从设备挂到了i2c1总线上它在总线上的地址是0x18。status okay确保控制器被使能clock-frequency指定控制器跑100kHz标准模式这是绝大多数低速传感器都能接受的速度。节点名myi2c18只是人类可读的名字内核真正看的是compatible和reg这两个属性名字怎么起不影响功能但最好和reg保持一致方便人眼排查。2.3 reg到底是7位还是8位地址换算这个坑我当年结结实实踩进去过。I2C从设备地址本质上只有7位有效位读写位是单独控制的。很多数据手册里写的“设备地址0x30”指的是包含写位的8位地址翻译到设备树时要右移一位变成0x18。如果手册明确写的是“7位地址0x18”那设备树直接写0x18就行。判断依据很简单I2C总线上实际发送的第一字节是7位地址左移一位再拼上读写位。而设备树里的reg属性会被内核直接填到i2c_client的addr字段里内核内部使用的是7位地址。写完设备树后用i2cdetect扫描出来的地址如果和reg不一致先检查是不是换算问题别急着怀疑驱动。这个换算关系要是记不牢probe匹配不上的时候排查半天都找不到方向。3. 核心代码解析从匹配到数据收发3.1 驱动主体结构和两个匹配表一个标准I2C驱动骨架包含三个核心部分of_device_id匹配表、i2c_device_id匹配表、i2c_driver本体。先看两个匹配表怎么写#include linux/module.h #include linux/i2c.h #include linux/of_device.h #include linux/miscdevice.h #include linux/uaccess.h #include linux/mutex.h #define DRV_NAME myi2c-demo static const struct i2c_device_id myi2c_id[] { { DRV_NAME, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, myi2c_id); static const struct of_device_id myi2c_of_match[] { { .compatible vendor,myi2c-dev }, { } }; MODULE_DEVICE_TABLE(of, myi2c_of_match);i2c_device_id表用于非设备树环境of_device_id表用于设备树环境两个都写上兼容性最好。MODULE_DEVICE_TABLE这行容易被忽视它的作用是把匹配表信息导出到模块的元数据区modprobe才能根据设备节点上的信息自动加载对应模块。少了这行模块只能手动insmod系统重启后驱动不会自己起来体验差距很大。i2c_driver本体长这样static struct i2c_driver myi2c_driver { .driver { .name DRV_NAME, .of_match_table myi2c_of_match, }, .probe myi2c_probe, .remove myi2c_remove, .id_table myi2c_id, }; module_i2c_driver(myi2c_driver); MODULE_LICENSE(GPL);module_i2c_driver宏帮我们完成了模块装载时的注册和卸载时的注销比手写module_init和module_exit短一截。这里有个细节.driver.name和id_table里的name并不会直接影响设备树匹配但它决定了/sys/bus/i2c/drivers/myi2c-demo这个目录的名字后面查设备状态时按这个名字找挺重要。3.2 probe里该做什么probe在设备匹配成功后触发入口参数就是已经匹配上的i2c_client。这个函数的核心任务通常有三步分配私有数据结构、初始化锁或者状态机、确认设备在线。struct myi2c_dev { struct i2c_client *client; struct mutex lock; u8 id; }; static int myi2c_probe(struct i2c_client *client) { struct myi2c_dev *dev; int ret; dev devm_kzalloc(client-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev-client client; mutex_init(dev-lock); ret i2c_smbus_read_byte_data(client, 0x00); if (ret 0) { dev_err(client-dev, device not responding: %d\n, ret); return ret; } dev-id (u8)ret; dev_info(client-dev, probe ok, id0x%02x\n, dev-id); i2c_set_clientdata(client, dev); return 0; }两个点值得单独讲。第一devm_kzalloc是设备资源管理接口probe失败、设备移除时内核会自动释放内存省掉一大堆手动清理代码第二probe里做一次“读ID验证”看起来多余实际价值很大。很多硬件层面的问题比如虚焊、地址错、总线没上拉都会在读ID这一步暴露出来比上层应用运行到一半报怪错好排查得多。一个probe就能把绝大多数硬件问题挡在门口。3.3 数据收发API怎么选I2C收发接口分两层。底层是i2c_transfer()它组织一条或者多条i2c_msg消息进行传输上层是smbus系列封装比如i2c_smbus_read_byte_data()、i2c_smbus_write_word_data()内部也是基于i2c_transfer实现的。日常写驱动优先用smbus封装。比如读一个寄存器物理上要发一个寄存器地址字节、再读一个数据字节i2c_smbus_read_byte_data(client, reg)一句话搞定。遇到一次读一大块连续数据或者设备的读写协议不是标准smbus格式再考虑用底层接口。看一个裸的i2c_msg例子u8 reg 0x10; u8 val; struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, .len 1, .buf reg }, { .addr client-addr, .flags I2C_M_RD, .len 1, .buf val }, }; ret i2c_transfer(client-adapter, msgs, 2); if (ret ! 2) { dev_err(client-dev, transfer failed: %d\n, ret); return ret; }这条消息序列的逻辑是先发一个字节的寄存器地址然后发重复起始位再以读模式收一个字节。flags为0表示写I2C_M_RD表示读。特别注意i2c_transfer的第一个参数是adapter而不是client可以从client-adapter取到。返回值和预期消息数量不一致时大概率是设备NACK或者总线异常不要把返回值当普通错误码一丢了事。必须牢记的是这两类接口都是会睡眠的内部要等总线空闲、等事务完成所以绝不能在自旋锁临界区、中断上下文里面调用。这是驱动挂死最常见的原因之一。3.4 把数据暴露给用户态驱动最终要服务应用层接口形式一般有三种往/sys里挂属性节点、注册miscdevice字符设备、或者用regmap加上debugfs辅助。我常用的是注册一个miscdevice配合ioctl或者read/write操作。这样应用层代码写起来简单驱动侧也不麻烦。ioctl里处理一次读操作的样子static long myi2c_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct myi2c_dev *dev file-private_data; u8 val; int ret; switch (cmd) { case MYI2C_READ_ID: ret i2c_smbus_read_byte_data(dev-client, 0x00); if (ret 0) return ret; val (u8)ret; if (copy_to_user((u8 __user *)arg, val, sizeof(val))) return -EFAULT; return 0; default: return -EINVAL; } }这里有个非常容易踩的坑i2c_smbus_read_byte_data返回的是int小于0是错误码大于等于0才是读到的数据。如果你用u8去接返回值-1会变成255错误被包装成合法数据bug极其隐蔽。我在review代码时看到好几个人犯过这个错务必记牢。3.5 最小完整驱动直接抄把上面的碎片拼起来一份能编译的最小模块主体就出来了。省略了文件操作结构体和部分初始化细节核心骨架就是这样。对照你自己的内核版本看头文件把结构和字段确认一遍把这个框架搭通之后往里加IRQ、加regmap、加批量读写都只是工作量问题。这个骨架本身不复杂难的是理解为什么每一步都这么写——匹配表、probe、收发接口每条线索都串在第一节讲的总线模型上。4. 调试三板斧日志、扫描、实测4.1 打开内核的I2C调试日志驱动跑起来没反应第一件事不是改代码是开日志。内核里的I2C调试开关有这么几个CONFIG_I2C_DEBUG_CORE打印设备和驱动的匹配、注册过程probe有没有被调用一看便知。CONFIG_I2C_DEBUG_ALGO打印算法层的收发细节能看到消息被组织成什么样子。CONFIG_I2C_DEBUG_BUS打印总线消息的字节内容最详细也最吵。打开之后dmesg里会出现类似i2c i2c-1: master_xfer[0] ...的输出虽然格式看着乱但能精确告诉你消息有没有发出、错误码是什么。开发阶段全开没问题生产环境别开日志刷屏会把其他信息全淹没。4.2 用i2c-tools校准设备地址内核驱动还没写好的时候先用用户态工具确认硬件在线比闷头写代码重要得多。i2c-tools这套工具里i2cdetect -l列出系统里所有adapteri2cdetect -y -r 1扫描1号总线上的设备地址。扫描出地址说明硬件链路健康扫不出来大概率是上拉电阻缺失、供电没到位、SDA和SCL接反或者总线上地址冲突。i2cget、i2cset、i2cdump还能直接读写寄存器帮你确认设备内部行为。这个环节必须熟练它能帮你把问题一刀切成两半到底是软件没写对还是硬件根本不对。先扫总线再调驱动是效率最高的顺序。4.3 逻辑分析仪和示波器才是终审软件日志只能反映内核认为发生了什么物理线上真正发生了什么还是得上仪器。I2C是慢速总线几百kHz以内的信号用常见的逻辑分析仪就能抓得很清楚。抓一次地址帧和ACK位基本能判断设备地址、时序、ACK行为是否符合预期。这里有个容易忽略的硬件问题I2C总线必须接上拉电阻常见阻值在1k到10k欧姆之间上拉到电源。板子设计偷懒上拉没接或者阻值过大会出现偶发NACK和丢数据。这种问题靠日志排查特别费劲波形抓一次就现原形。4.4 一个典型的排查顺序我遇到“驱动不工作”时的排查路径是这样先i2cdetect -l确认adapter节点存在再i2cdetect -r扫描看设备挂在哪个地址然后用逻辑分析仪抓启动位和ACK最后才回头查内核驱动代码。这个顺序千万别反过来否则你会花大量时间在一个本身没问题的驱动上反复改配置越改越乱。先硬件后软件先用户态后内核态这条原则适用于绝大多数外设驱动调试。5. 我踩过的坑和给你的避坑清单5.1 compatible不匹配最频繁的坑没有之一。设备树里写的compatible和of_match_table里的字符串只是差一个大小写字符probe就不调用。排查办法是把两边内容复制出来逐字符对比比盯着屏幕苦想效率高得多。别忘了compatible推荐格式是“厂商,型号”比如vendor,myi2c-dev中间是逗号不是别的符号。5.2 地址换算错7位地址和8位地址的混用我已经在设备树小节里强调过。这里再补充一个习惯设备树里的reg与i2cdetect扫出来的地址必须互相印证两个值一致才继续往下走。如果设备树写了0x30而扫描显示0x18先想想是不是换算没做不要急着改驱动里的地址。这种错误一旦发生排查成本远高于写代码时多花一分钟确认。5.3 并发访问导致时序错乱驱动能跑通之后并发问题才慢慢浮出水面。用户态多个进程同时打开同一个misc设备时如果文件操作里的读写路径不加锁两次I2C事务就可能交错执行读到的寄存器数据张冠李戴。解决方法就是给每个client配一把mutex在read、write、ioctl这些入口加锁开锁。I2C事务本身很短锁开销几乎可以忽略但不加锁的后果有时要跑很久才能复现调试体验极差。5.4 睡眠上下文和超时问题I2C收发是可能睡眠的操作内部要等待适配器完成一次事务。如果你在自旋锁保护的临界区或者中断处理函数里调用i2c_smbus相关API内核调度器会直接报错表现为驱动挂死或者oops。这种情况在低成本传感器驱动里出现频率极高因为很多人习惯在中断里读数据。正确做法是中断里只置标志位或者唤醒工作队列把真正的I2C传输放到进程上下文或者专用内核线程里完成。5.5 几个实用工作习惯长期和I2C驱动打交道我攒了几个习惯分享出来也许能帮你少走弯路。设备树节点先只写compatible和reg能成功匹配再加其他属性这样一旦probe没跑问题只在最小范围内。改代码后先rmmod再insmod重载失败先翻dmesg看是不是设备被占用。多利用i2c_set_clientdata和i2c_get_clientdata保存自己的私有数据结构避免为每个设备开一个全局变量否则挂多个同型号设备时数据会互相覆盖。打印地址时十六进制和十进制都打出来跟工具扫描结果对应更直观。5.6 内核版本差异是个沉默杀手新内核里有些接口已经变了网上很多老教程的代码在新内核上根本编译不过。比如i2c_driver的remove回调返回值类型、i2c_new_device到i2c_new_client_device的命名变化不同版本之间都有过调整。我的做法是每次升级内核后先打开include/linux/i2c.h把用到的结构体字段确认一遍再动代码。这个习惯帮我省过很多个下午建议你也养成。看代码、查源码、动手验证永远比记忆可靠。