资讯动态

Linux I2C设备驱动开发实战:从设备树匹配到用户态调试

发布时间:2026/10/25 9:16:58 来源:尧图企业网站定制
做嵌入式Linux的人基本都绕不开I2C。温度传感器、触摸屏、EEPROM、电源管理芯片甚至摄像头模组都挂在I2C总线上。以前我在用户态用i2c-tools读寄存器觉得这总线很简单两根线、一个地址、往寄存器里写数据外设就能动。可真转到内核态去写Linux I2C驱动才发现这套子系统分层很清楚但逻辑链特别长设备树怎么描述、驱动怎么匹配、probe里怎么发消息、数据怎么给用户态任何一个环节出错设备就是纹丝不动。这篇文章我拿一个虚构的传感器芯片做例子把Linux I2C设备驱动的开发流程完整走一遍重点讲子系统结构、设备树匹配、API选型、用户态调试和典型坑适合刚接触内核驱动、想真正弄懂I2C这套框架的人。1. 先搞清楚I2C子系统是怎么组织的1.1 I2C协议本身有什么坑I2C是半双工、多主机的串行总线只有SCL时钟线和SDA数据线。两根线都靠上拉电阻拉到高电平设备通过拉低线来通信。每一帧传输由START信号开始然后发7位从机地址加1位方向位接着传数据最后STOP结束。协议本身不复杂但写驱动的人容易把注意力全放在API上反而忽略三个最基础的坑。第一个坑是地址格式。绝大多数芯片手册直接写7位地址比如0x3c这是不包含读写标志的原始地址。但实际通信时字节高位是7位地址最低位是R/W位所以0x3c会变成0x78或0x79。你写设备树reg、写i2c_msg.addr、用i2cget传地址时填的都是7位地址控制器驱动会自动左移一位拼上方向位。我见过不止一个新手把0x3c左移成0x78再填进去结果从设备死活不应答。第二个坑是ACK机制。每发完一个字节接收方需要拉低SDA表示应答。如果设备不存在、地址写错或者没上电主机会在地址阶段收到NACKi2c_transfer直接返回错误数据全是0xFF或超时。很多调试问题归根结底是地址对不上。第三个坑是时钟拉伸和总线仲裁。慢速设备可以拉低SCL迫使主机等待这叫clock stretching多个主机同时抢总线时需要仲裁。Linux的I2C算法层会处理这些但代价是传输时间是“不确定”的驱动里千万不要假设一次transfer一定能按微秒级完成。1.2 Linux内核里的三层结构Linux把I2C子系统分成三层理解这三层才能知道你的代码应该写在哪一层。第一层是adapter层对应I2C控制器硬件比如某SoC的I2C外设。adapter负责生成START、STOP、ACK、收发字节这些物理时序内部通过struct i2c_algorithm描述这些操作。这一层一般由平台厂商写好了你只需要在内核配置里把对应控制器驱动打开。第二层是core层也就是内核的i2c-core代码。它主要负责四件事管理adapter和client的注册、实现驱动和设备之间的匹配、把上层请求分发给对应的adapter、提供SMBus兼容接口。你在驱动里调的i2c_transfer、i2c_smbus_read_byte_data最终都会进到core层再由core层调adapter的algorithm。第三层就是大多数开发者写的client驱动层也叫设备驱动层。每个挂在I2C总线上的外设芯片对应一个i2c_client你的驱动是一个i2c_driverprobe函数在匹配成功后执行。日常写的温度传感器驱动、触摸屏驱动、EEPROM驱动都属于这一层。用大白话比喻adapter是“出租车公司”core是“调度中心”client驱动是“乘客”。出租车公司管路怎么开调度中心分配订单乘客只负责说要去哪。对写外设驱动的人来说你几乎不需要关心出租车怎么开只用会下单就行。1.3 驱动模型client、driver、adapterLinux的设备驱动模型是“设备驱动总线”的模式I2C子系统也不例外。i2c_client代表物理上存在的一个设备里面保存着设备地址、适配器指针、中断号等信息。i2c_driver代表一段驱动代码里面保存着compatible匹配表、probe/remove回调。adapter就是前面的控制器。匹配过程很关键。内核启动时设备树里描述的节点会被解析成i2c_board_info再在对应adapter下注册成i2c_client。当i2c_driver注册时内核会遍历同一条I2C总线上的所有client拿驱动的compatible匹配表和client的设备节点里的compatible去比对一比对得上就调用probe。所以probe不执行时第一步不是怀疑代码而是怀疑这个链条是否通了设备树节点有没有被解析client有没有注册adapter总线号对不对驱动有没有被加载后面第5部分我再细说排查顺序。2. 动手前先把硬件和底账摸清楚2.1 引脚、地址、寄存器这三样没法拍脑袋拿到一块开发板和一个传感器模块第一件事不是打开内核写代码而是把硬件信息确认清楚。看原理图确认SDA、SCL分别接到SoC的哪两个引脚属于哪一路I2C控制器。很多SoC有I2C0、I2C1、I2C2好几路接错控制器后面全白干。还要检查上拉电阻是否存在有些模块自带弱上拉但加上板级上拉更稳如果SCL或SDA悬空没上拉驱动写得再漂亮也没用。接着从芯片手册里找几个关键值7位从机地址、最大时钟频率、要操作的寄存器地址和读写长度。温度传感器这类设备通常一个字节就能指到寄存器EEPROM则可能是两字节地址。这些信息决定了你用单字节命令接口还是块读写接口。最后建议在板子上先跑一下i2cdetect在用户态确认设备到底在不在总线上再做内核驱动。跳过这一步直接写内核驱动非常容易出现在两个问题上反复横跳的情况。2.2 设备树节点能把外设“挂”上总线现代嵌入式Linux里描述一个I2C设备主要靠设备树。设备树节点要挂在对应I2C控制器节点下示例如下i2c1 { status okay; clock-frequency 400000; my-sensor3c { compatible vendor,my-sensor; reg 0x3c; interrupt-parent gpio0; interrupts 7 IRQ_TYPE_EDGE_FALLING; }; };compatible是驱动和设备匹配的钥匙必须和驱动里of_match_table中的字符串完全一致包括大小写、逗号和厂商前缀。reg填7位从机地址比如传感器的地址是0x3c这里就写0x3c不要左移。节点名里的3c是给调试人员看的地址标签内核主要用reg属性。还需要注意clock-frequency要符合传感器数据手册的上限。100kHz标准模式、400kHz快速模式别一股脑设成1MHz去挑战器件极限。中断这条属性要看芯片是否真有中断脚没有的话就删掉免得probe里请求中断失败导致整个驱动加载失败。板级信息不全是设备树老一些的平台还在用arch/arm/mach-xxx里的i2c_board_info结构体。如果你的平台是新的一律以设备树为准。2.3 内核配置和工具别漏了设备树配置好后还要确认内核使能了I2C相关功能否则设备树根本没机会被解析。我整理了一个最少需要的内核开关清单CONFIG_I2CI2C核心支持必开CONFIG_I2C_CHARDEV提供/dev/i2c-N用户态接口调试阶段建议开CONFIG_I2C_SMBUSSMBus协议支持很多传感器驱动依赖它CONFIG_I2C_GPIO如果控制器用GPIO模拟I2C需要这个CONFIG_DYNAMIC_DEBUG开动态调试排查驱动问题太好用了如果用的是某个真实SoC集成的I2C控制器还要确保对应Controller驱动被编译进内核比如常见平台都有的I2C adapter drvier。判断方法很简单启动后看dmesg里有没有“i2c-0”、“i2c-1”这类控制器注册日志。文件系统里最好也放一份i2c-toolsi2cdetect、i2cget、i2cset、i2ctransfer这几个命令能帮你省掉大量烧写和重启环节。后面第4部分会逐个说明。3. 从零写一个I2C设备驱动3.1 driver入口匹配表和probe回调下面就是一个最精简的I2C设备驱动框架。我用compatible字符串“vendor,my-sensor”作为匹配键probe函数里只做分配私有数据和打印#include linux/i2c.h #include linux/module.h #include linux/of_device.h #include linux/slab.h struct my_sensor_data { struct i2c_client *client; struct mutex lock; }; static const struct of_device_id my_sensor_of_match[] { { .compatible vendor,my-sensor }, { } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static int my_sensor_probe(struct i2c_client *client) { struct my_sensor_data *data; data devm_kzalloc(client-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >int ret i2c_smbus_read_byte_data(client, 0x1f); if (ret 0) { dev_err(client-dev, read chip id failed: %d\n, ret); return ret; } dev_info(client-dev, chip id 0x%02x\n, ret);第二个是i2c_transfer配合struct i2c_msg适合需要连续写寄存器地址、再连续读多个字节的场景。比如读一个两字节温度值u8 reg 0x10; u8 val[2]; struct i2c_msg msgs[] { { .addr client-addr, .flags 0, .len 1, .buf reg, }, { .addr client-addr, .flags I2C_M_RD, .len 2, .buf val, }, }; int ret i2c_transfer(client-adapter, msgs, ARRAY_SIZE(msgs)); if (ret ! ARRAY_SIZE(msgs)) { dev_err(client-dev, i2c_transfer failed: %d\n, ret); return ret; }i2c_transfer的返回值是成功传输的消息条数不是字节数。两个msg都成功才是2这里就按2来判别这是我一开始写得比较粗心的地方。第三个是块读写接口i2c_smbus_read_i2c_block_data适合一次读一大块数据比如读取寄存器缓冲区或EEPROM页。日常开发中前两个接口用得最多。3.3 把数据交出去sysfs和miscdevice内核驱动和外设通信只是第一步用户态要能读到数据才算完整。简单数据可以走sysfs属性文件比如把芯片ID暴露出来static ssize_t chip_id_show(struct device *dev, struct device_attribute *attr, char *buf) { struct i2c_client *client to_i2c_client(dev); int val i2c_smbus_read_byte_data(client, 0x1f); if (val 0) return val; return sysfs_emit(buf, 0x%02x\n, val); } static DEVICE_ATTR_RO(chip_id);然后用device_create_file把属性添加到设备节点下。加载驱动后/sys/bus/i2c/devices/1-003c/chip_id就是芯片ID。命令readfile和echo这个文件的逻辑最后都会跑到内核属性回调里。如果设备要支持多条较复杂的命令建议用miscdevice注册一个/dev/my_sensor节点然后在file_operations里实现read、write和ioctl。miscdevice的好处是自动分配主设备号不用和内核争号。如果数据属于温度、电压这些“传感器类”更标准的做法是注册到hwmon子系统用户态直接通过/sys/class/hwmon读数据但这要求芯片足够“标准化”简单传感器直接sysfs反而更直观。3.4 中断、互斥和失败重试带中断的I2C设备会涉及一个非常经典的错误在中断处理函数里直接调i2c_transfer。I2C传输过程可能睡眠不能出现在硬中断上下文。正确做法是用线程化中断比如if (client-irq) { ret devm_request_threaded_irq(client-dev, client-irq, NULL, my_sensor_irq_thread, IRQF_TRIGGER_LOW | IRQF_ONESHOT, my_sensor, data); if (ret) return ret; }在线程化中断处理函数里就可以正常调用i2c_smbus_read_byte_data这些接口了。搞不明白这个区别的驱动大概率会看到“BUG: sleeping function called from invalid context”这行标志性日志。互斥也很重要。我的习惯是struct my_sensor_data里放一个mutex凡是多个入口可能同时操作外设的地方都锁一遍。sysfs读、sysfs写、中断线程这三个看起来互不相干但如果同时去访问同一颗芯片会造成I2C消息穿插轻则读到脏数据重则把设备状态搞乱。失败重试要分场景。临时性错误比如总线忙可以在驱动里简单重试两三次。但如果是NACK这种“设备根本不理会”的错误重试只会拖慢启动不如尽早报错退出probe让上层日志告诉我们问题出在哪。4. 用户态调试i2c-dev这层用好事半功倍4.1 i2c-tools仅备不全写内核驱动之前强烈建议先在用户态把芯片行为和寄存器调清楚。i2c-tools里这几个命令是最常用的命令典型用法作用i2cdetecti2cdetect -l列出所有I2C总线i2cdetecti2cdetect -y 1扫描总线1上的设备地址i2cdumpi2cdump -y 1 0x3c连续dump设备寄存器i2cgeti2cget -y 1 0x3c 0x1f读某个寄存器i2cseti2cset -y 1 0x3c 0x10 0x30写某个寄存器i2ctransferi2ctransfer -y 1 w20x3c 0x10 0x00 r2组合读/写消息i2cdetect扫描时会往每个地址发探测帧少数器件对这帧会有非预期响应甚至可能改变状态所以别乱扫不熟悉的设备。实际产品调试中i2cdetect只适合找地址寄存器级调完最好用i2cdump和i2cget。有一点要反复说命令里的地址都填7位地址。i2cset -y 1 0x3c 0x1f 0xaa这里的0x3c是设备地址而不是8位总线地址。内核和工具都会自动处理最后一个R/W位你手动左移只会自找麻烦。4.2 手写一个ioctl读写工具有些场景总线被驱动占用或者想验证一个多字节时序这时候直接用ioctl写一个小工具更稳。核心代码如下#include fcntl.h #include linux/i2c-dev.h #include sys/ioctl.h #include unistd.h u8 buf[4] { 0x10, 0x00, 0x00, 0x00 }; struct i2c_msg msgs[2] { { .addr 0x3c, .flags 0, .len 1, .buf buf[0], }, { .addr 0x3c, .flags I2C_M_RD, .len 2, .buf buf[1], }, }; struct i2c_rdwr_ioctl_data xfer { .msgs msgs, .nmsgs 2, }; int fd open(/dev/i2c-1, O_RDWR); if (fd 0) { perror(open); return 1; } if (ioctl(fd, I2C_RDWR, xfer) 0) { perror(ioctl I2C_RDWR); return 1; } close(fd);第一个msg先发寄存器地址0x10第二个msg再从这个寄存器读两个字节这正好就是传感器常说的“write pointer then read”时序。I2C_RDWR的多个msg会在内核里被合并成一个原子传输不会在中间插入其他设备的访问。msg里的addr字段填的同样是7位地址。控制器算法层在真正发送时会帮你左移生成8位地址千万别自己左移。我见过有人在这里填0x78结果所有访问都落到错误的总线地址上。4.3 用户态能干的不必都塞进内核有些情况下I2C设备根本不需要专门写内核驱动。纯用户态打开/dev/i2c-N、用ioctl读写完全可以满足调试、产测、甚至某些量产功能。但如果设备需要接入标准体系情况就不同了。比如一个温度传感器要让系统温度监控机制统一管理那就必须写hwmon驱动否则/sys/class/hwmon下面不会有节点再比如一个触摸屏驱动要对接input子系统用户态裸读I2C是完全做不到的还有需要中断唤醒、系统休眠/恢复管理、并发访问控制的设备也最好用内核驱动。我个人的选型标准是只要能通过标准sysfs或者简单事件上报解决的问题优先用户态只有必须和内核子系统深度整合或者响应时间敏感才写内核驱动。用户态和内核态不是谁替代谁而是各干各擅长的。5. 常见问题与排查技巧实录5.1 probe不执行优先确认匹配链probe不执行是I2C驱动新人遇到最多的问题。排查顺序我建议固定下来首先启动后立刻看dmesg确认I2C控制器到底注册成功没有。如果连“i2c-1”都没出现说明adapter驱动没编译或设备树里控制器状态不是okay。然后去/sys/bus/i2c/devices目录看有没有1-003c这样的目录。没有这个目录说明设备树里你的子节点没被解析或者父控制器顺序不对。注意总线号可能会漂移比如重新编译设备树后原来的i2c1可能变成了i2c2。接下来查compatible。设备树里写“vendor,my-sensor”驱动of_match_table里就必须一字不差。字符串匹配是最容易忽略大小写的地方比如把“Sensor”写成了“sensor”。最后看驱动模块加载没。mm加载后dmesg里有没有“my_sensor”相关日志。模块加载顺序通常不是问题因为匹配是基于已有的client动态进行的。5.2 全FF和-121地址还是时序读寄存器读回一堆0xFF或者i2c_transfer返回-6、-121这类错误基本逃不出以下几个原因。设备没上电是最常见的低级错误。传感器VDD和GND没接好或者供电电压不对SCL/SDA被上拉拉高发出地址后设备压根不出现读回0xFF。地址不对是第二个高频原因。注意区分7位地址和8位地址检查设备树reg、i2c_msg.addr、i2ctransfer命令行参数是否都填的是7位地址。如果引脚上有地址选择线还要确认硬件上下拉是否和软件地址一致。时序不满足也会导致通信失败。I2C总线上挂了很多设备时某条总线的上拉电阻过大会导致上升沿太慢降低通信速率或者缩短总线长度可以缓解。用示波器双通道抓SCL和SDA重点看START条件、地址字节、ACK位是不是正常出现在预期位置。逻辑分析仪在这种场景比示波器更省事因为它能直接解析出I2C报文。还有一种人被拉死的总线SDA一直为低说明某个设备正在占用总线或进入了异常状态。这种情况下i2cdetect可能全报0x00或所有地址都应答。把总线上的设备逐个断开找到罪魁祸首再说。5.3 数据错乱字节序、块读和寄存器长度寄存器值读出来是有的但数值完全不对这种问题通常是“读错位置”或“拼错字节”。多字节数据要留意寄存器Map的顺序。常见的16位温度值是高字节在前还是低字节在前手册里都会有描述。如果拿到的温度明明是25℃读出来却是0x2500之类的值多半是高低字节颠倒了。用i2c_smbus_read_word_data时要清楚设备是高字节在前还是低字节在前再决定要不要做u16换算。块读时还有一个坑不少传感器读一整个寄存器块时内部会自动地址自增但有些设备不会。如果你读到重复的数据检查是否需要先写起始地址、以及是否需要把寄存器宽度设置成两字节地址。EEPROM就经常是两字节地址这时i2c_smbus_read_byte_data完全不够用必须构造i2c_msg先发两个地址字节。还有就是要确认读长度。读传感器ID只需要1字节结果你读了4字节后面几个字节其实是相邻寄存器的值。打印时按缓冲区里取前几个字节就好别把脏字节读进来影响判断。5.4 稳定性经验别在不能睡的地方传数据驱动能跑通只是第一步长期稳定运行才是真问题。I2C驱动最常见的稳定性事故就是在原子上下文里传数据。中断处理函数、自旋锁保护的区域、timer回调里都不能调i2c_transfer因为传输过程会进调度器睡眠。具体报错就是“sleeping function called from invalid context”这种日志一出现直接找代码里哪个调用链带着spinlock或确实在硬中断里。另一个稳定性点是并发。sysfs属性、ioctl、中断线程、内核轮询任务四个上下文可能同时接触同一个i2c_client。没有锁保护时两个读写会被core层拆成多个消息穿插严重时会得到混乱数据。给私有数据结构里加一个mutex把所有访问外设的操作包进去能解决绝大多数并发问题。还要注意错误恢复。I2C设备偶发NACK时驱动里可以重试一次但重试次数不要太多。如果连续多次失败应记录错误并考虑重置设备。对于可复位设备GPIO复位或软件复位命令是更好的恢复手段而不是无脑重试。调试时那堆打印也是有讲究的。probe开始和结束各加一条日志读关键寄存器失败时把地址和返回值一起打出来。不要在每个read路径都开普通printk日志会被刷屏改用dev_dbg加动态调试开关需要时再打开echo file i2c-core.c p /sys/kernel/debug/dynamic_debug/control配合dmesg时间戳基本能还原出问题发生时驱动在干什么。我个人这几年写I2C驱动最大的心得是内核驱动本身不复杂复杂的是调试链。先放下写内核代码的冲动在用户态把协议、地址、时序问题全部跑通再进内核态能省至少一半时间。还有个每次都会用的小技巧新芯片上板后第一件事在probe里读一次ID寄存器并打日志。ID能读对DTS匹配、client驱动接口、消息封装基本就全通了后续分配缓存和寄存器配置都顺理成章。真到了内核态还是读不到数据与其抱着代码猜不如直接抓逻辑分析仪看SCL、SDA的波形一次就能定位是地址问题还是时序问题。希望这篇东西能让你少踩几个我踩过的坑。

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

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

免费获取报价 →
↑