前阵子帮同事调一块传感器板,i2c总线上的设备死活读不出数据,折腾了两天,最后发现不是硬件问题,而是驱动框架没吃透:字符设备节点注册好了,数据读写却一直挂在半路。顺着这个坑把整个i2c子系统捋了一遍,才发现只要你理解了“字符设备负责暴露接口,i2c子系统负责管总线”这条主线,写一个能跑的驱动其实没那么玄乎。
这篇东西就是把我踩过的坑和最终梳理清楚的框架一起倒出来,面向需要“照着写就能用”的人,也顺带讲讲为什么这么写、写的时候哪些地方不能改、哪些地方必须注意。如果你正准备在Linux下驱动一个i2c温度传感器、eeprom、触摸屏控制器之类的小设备,这篇应该能省你不少时间。
1. 先看清楚两个子系统是怎么合体的
1.1 字符设备和i2c子系统各自管什么
很多人一上来就找“i2c驱动模板”,其实容易绕晕。Linux下的i2c设备驱动,本质上是两套机制叠在一起:一套是字符设备机制,负责在/dev下生成节点,让用户态程序能有文件句柄去open、read、write、ioctl;另一套是i2c子系统机制,负责找到挂在总线上的设备、构造i2c_msg消息、发起传输、处理ACK/NACK。
这两者不冲突,反而是一前一后的关系。字符设备解决“怎么暴露给用户”,i2c子系统解决“怎么把数据真的送到硬件上”。你写驱动时,通常是先注册一个字符设备,把它和某个私有数据结构绑在一起,然后在读写回调里调用i2c子系统提供的传输接口。用户态感知不到i2c的存在,只看到打开了一个文件,读写这个文件;内核这边则干了两层活。
可以打个比方:字符设备像小区门口的传达室,用户把信投进去就完事;i2c子系统像小区里的邮政系统,负责把信按门牌号送到每户。你作为驱动作者,既要在传达室安排人值班,又得知道门牌号怎么编。
1.2 为什么不用纯字符设备直接操作GPIO模拟i2c
还有个常见的思路是自己用GPIO模拟i2c时序,写成字符设备直接操作寄存器。这个方案在某些极简单的单片机上可行,但在Linux下强烈不建议。
Linux的i2c子系统帮你处理了几件麻烦事:总线复用仲裁(多客户端共享一条总线时避免冲突)、时钟频率控制、ACK/NACK错误检测、系统休眠唤醒时的总线状态恢复,以及和设备树机制的无缝衔接。你自己用GPIO模拟,等于所有这些都得重造轮子,还容易因为时序不标准导致芯片工作不稳定。更关键的是,如果你不用子系统自带的i2c传输接口,就没法和Linux里其他机制配合,比如regmap框架、input子系统、hwmon框架,后续扩展起来很痛苦。
所以正规做法是:结构上依赖i2c子系统,接口上暴露字符设备。这样既保留了驱动的通用性,又能给应用层提供最简单的文件操作方式。
2. 动手前必须熟悉的几个关键API
2.1 四个核心结构体
写i2c字符设备驱动前,先把下面这几个结构体刻在脑子里,后面代码基本就是围着它们转:
- i2c_adapter:表示一条i2c总线控制器。通常由SoC的I2C控制器驱动注册,设备驱动不需要自己创建,只需要通过client->adapter拿指针。
- i2c_client:表示挂在总线上的设备。在设备树匹配成功后,内核会自动分配并填充。驱动里最常用的是client->addr(7位设备地址)和client->adapter(总线控制器指针)。
- i2c_driver:驱动主体。需要填充id_table(设备ID匹配表)和probe、remove回调,然后通过i2c_add_driver注册。
- i2c_msg:一条传输消息。包含addr、flags、len、buf。flags里可以标记是否读操作(I2C_M_RD)、是否设置10位地址等。
这个小集合里,i2c_msg是传输的关键。你要读芯片寄存器,就得构造两条消息:第一条是写寄存器地址(write),第二条是读数据(read),然后通过i2c_transfer一次性发出去。很多人第一次写的时候只发了一条写消息,然后单独发一条读消息,结果发现读回来的数据不对——就是因为没有先写寄存器地址再发读命令。
2.2 i2c_transfer是怎么工作的
i2c_transfer的原型是:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);它接收一个i2c_msg数组,数组里每条消息可以独立设置方向和目标地址。内核会尽量把这些消息组织成一次原子化的总线事务,中间不会被其他客户端插入,能保证多消息序列的连续性。这个特性重要在哪?调试过i2c的都懂:如果你先发一个写消息,再发一个读消息,中间被别的驱动插一脚,总线时序就乱了,读回的数据可能就不是你要的。
具体使用方式:
struct i2c_msg msgs[2]; int ret; msgs[0].addr = client->addr; msgs[0].flags = 0; // 写 msgs[0].len = 1; msgs[0].buf = ®_addr; msgs[1].addr = client->addr; msgs[1].flags = I2C_M_RD; // 读 msgs[1].len = sizeof(buf); msgs[1].buf = buf; ret = i2c_transfer(client->adapter, msgs, 2); if (ret < 0) { /* 错误处理 */ }注意返回值:i2c_transfer返回的是成功传输的消息条数,不是字节数。如果返回2说明两条都完成,返回1说明第一条完成第二条失败,返回负数说明传输过程出现问题。按条数判断,别按字节数判断,这是最容易写错的地方。
2.3 i2c_master_send和i2c_master_recv是什么
内核还封装了两个便捷函数:
int i2c_master_send(const struct i2c_client *client, const char *buf, int count); int i2c_master_recv(const struct i2c_client *client, char *buf, int count);它们内部就是把一条消息塞进i2c_transfer,适合那种不需要两步操作的简单设备。比如某块eeprom纯顺序读,直接i2c_master_recv就行。但如果你面对的是标准寄存器型传感器,要指定寄存器地址再读,那就要自己构造两条消息,i2c_master_send/recv反而不够灵活。我的习惯是:凡是涉及寄存器操作的,一律用i2c_transfer手动构造消息,可控性高。
3. 手写一个完整的i2c字符设备驱动
3.1 驱动骨架先搭起来
直接上一份可以编译的骨架,这里假设你要驱动一颗假的“xm_sensor”芯片,这颗芯片支持用寄存器读取温度和状态。代码风格我尽量保持内核习惯,不搞花活。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/i2c.h> #include <linux/uaccess.h> #include <linux/slab.h> #include <linux/mutex.h> #define XM_SENSOR_DEV_NAME "xm_sensor" #define XM_SENSOR_CLASS_NAME "xm_sensor_class" #define XM_SENSOR_REG_TEMP 0x01 #define XM_SENSOR_REG_CFG 0x02 struct xm_sensor_data { struct i2c_client *client; struct cdev cdev; struct device *device; struct mutex lock; dev_t devno; }; static struct class *xm_sensor_class; static int xm_sensor_major; static int xm_sensor_read_reg(struct xm_sensor_data *data, u8 reg, u8 *val) { struct i2c_client *client =>static int xm_sensor_open(struct inode *inode, struct file *filp) { struct xm_sensor_data *data = container_of(inode->i_cdev, struct xm_sensor_data, cdev); filp->private_data = data; return 0; } static int xm_sensor_release(struct inode *inode, struct file *filp) { return 0; }read回调用来看user buffer,然后从寄存器读值填进去。这里要注意一点,尽量别在read里做复杂的i2c事务而不加锁。i2c_transfer本身是线程安全的吗?不完全。同一时刻如果多个进程同时open这个设备并发读写,底层的传输可能出现交错。解决方式就是每笔事务之间加一把锁:
static ssize_t xm_sensor_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { struct xm_sensor_data *data = filp->private_data; u8 val = 0; int ret; mutex_lock(&data->lock); ret = xm_sensor_read_reg(data, XM_SENSOR_REG_TEMP, &val); if (ret) { mutex_unlock(&data->lock); return ret; } mutex_unlock(&data->lock); if (copy_to_user(buf, &val, sizeof(val))) return -EFAULT; return sizeof(val); }读一个字节直接copy_to_user一个字节,不要搞花哨。真实项目里遇到多字节读取,可以通过ioctl让用户态传入寄存器地址和长度,驱动侧循环或批量构造i2c_msg。我建议多字节读取也一次性构造多条消息来读,减少多次传输造成的时间开销。
3.3 ioctl是高效交互的关键
很多现实项目只靠read不够,比如要写设备配置寄存器、要循环读一组数据、要设置工作时间模式。这时候可以统一走ioctl,让用户态传一个结构体进来:
struct xm_sensor_rw_arg { u8 reg; u8 *buf; u8 len; u8 is_read; }; static long xm_sensor_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct xm_sensor_data *data = filp->private_data; struct xm_sensor_rw_arg rw_arg; struct i2c_msg msgs[2]; int ret; if (copy_from_user(&rw_arg, (void __user *)arg, sizeof(rw_arg))) return -EFAULT; mutex_lock(&data->lock); msgs[0].addr =>static int xm_sensor_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct xm_sensor_data *data; int ret; if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { dev_err(&client->dev, "i2c functionality not supported\n"); return -EOPNOTSUPP; } data = kzalloc(sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; mutex_init(&data->lock); >static const struct i2c_device_id xm_sensor_id[] = { { "xm_sensor", 0 }, { } }; static const struct of_device_id xm_sensor_of_match[] = { { .compatible = "xm,xm-sensor" }, { } }; static struct i2c_driver xm_sensor_driver = { .probe = xm_sensor_probe, .remove = xm_sensor_remove, .id_table = xm_sensor_id, .driver = { .name = "xm_sensor", .of_match_table = xm_sensor_of_match, }, }; module_i2c_driver(xm_sensor_driver); MODULE_LICENSE("GPL");注意compatible里确保和设备树里写的完全一致,包括逗号前后大小写。设备树里写“xm,xm-sensor”,驱动里也写“xm,xm-sensor”,内核才能匹配上。每一项of_match_table都用花括号包起来,最后留空项结束,这是规矩。
4. 设备树配置与模块加载流程
4.1 设备树节点到底要写什么
用设备树匹配驱动,是你的dts里要有一个i2c总线下的子节点。
&i2c1 { status = "okay"; clock-frequency = <100000>; xm_sensor: xm-sensor@2c { compatible = "xm,xm-sensor"; reg = <0x2c>; }; };reg就是芯片的7位i2c地址。等等,这里有个经典问题:芯片数据手册上写“地址0x58”,但设备树reg填写的是0x2c,为什么?因为7位地址和8位地址的区别。手册上喜欢给你完整字节(含读写位),比如写地址0xB0、读地址0xB1,对应的7位地址是0x58(右移一位)。而设备树reg要填7位地址,也就是0x58。如果不做这个转换,驱动就会一直在跟一个错地址的设备通信,表现是:传输返回成功但NACK或者根本没反应。
4.2 设备和驱动是怎么牵上线的
内核启动后或模块加载后,i2c子系统会扫描所有adapter总线上的设备(或者通过设备树解析),为每个设备创建i2c_client。随后,i2c子系统遍历已注册的i2c_driver,用of_match_table或者id_table去匹配client。匹配成功就调用probe。
这里有个容易被坑的地方:设备树节点本身不会触发i2c地址扫描,它是直接让内核知道“这条总线上这个地址有一个设备”。而如果不用设备树,传统方式是在板级文件里加入i2c_board_info,也会生成i2c_client。对现代内核来说,能设备树就设备树,干净利落。
4.3 insmod之后发生了什么
模块加载后,module_i2c_driver会展开成module_init和module_exit的组合,自动调用i2c_add_driver。此时如果设备树解析已完成,符合匹配条件的i2c_client已经在总线上等着,probe会被立即调用。字符设备的/dev节点也是在这个阶段创建的。
所以验证顺序很关键:先确认dts节点存在、compatible匹配,再确认/sys/bus/i2c/devices下有对应设备目录,最后再检查/dev/xm_sensor节点。我在调试时习惯按这个顺序排查:
ls /sys/bus/i2c/devices/ ls -l /dev/xm_sensor dmesg | tail -30如果/dev下有节点但/proc或sysfs里没设备目录,说明设备树节点有问题;如果设备目录在但/dev下没有节点,那就是probe没走完,多半是cdev_add或device_create出错了,看dmesg就能定位。
5. 用户态测试链路怎么搭
5.1 一个简单的测试程序
驱动写得好不好,最终要应用层验证。写个简洁的C程序测试:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <errno.h> /* 与驱动里的结构体保持一致 */ struct xm_sensor_rw_arg { unsigned char reg; unsigned char *buf; unsigned char len; unsigned char is_read; }; #define XM_SENSOR_IOC_RW _IOWR('x', 1, struct xm_sensor_rw_arg) int main(void) { int fd = open("/dev/xm_sensor", O_RDWR); if (fd < 0) { perror("open"); return 1; } struct xm_sensor_rw_arg arg; unsigned char val = 0; arg.reg = 0x01; arg.buf = &val; arg.len = 1; arg.is_read = 1; if (ioctl(fd, XM_SENSOR_IOC_RW, &arg) == 0) { printf("temp reg = 0x%02x\n", val); } else { perror("ioctl"); } close(fd); return 0; }开设备、发ioctl、读寄存器。就这么简单。很多内核驱动教程讲了半天驱动侧,却忘了告诉你怎么验证,我自己早期就被这个卡过。
5.2 在没有驱动时先用i2c-tools验证硬件时序
写驱动之前,强烈建议先用i2c-tools直接验证芯片是否存在、地址是否正确。Linux里一般自带或者可以装i2c-tools:
i2cdetect -y 1 i2cget -y 1 0x2c 0x01 i2cset -y 1 0x2c 0x02 0xff先看i2cdetect能不能扫到0x2c这个地址,再用i2cget/i2cset测读写。如果这个阶段就不通,别急着写驱动,先用示波器或逻辑分析仪看SDA/SCL波形。工具验证通过,基本能确定芯片时序没问题,后面驱动写不好就是软件问题。
设备树里clock-frequency我平时调100k,很多传感器支持400k,但是新手阶段先用100k稳妥。如果你有几个设备挂同一总线,有的芯片时序要求严格,把速率调太高容易出问题,100k是不容易出事的起点。
6. 高频问题排查和踩坑实录
6.1 i2c_transfer返回-EREMOTEIO
这是最常见的报错。含义是总线传输时收到了NACK。可能原因清单:
- 设备地址写错了(7位/8位混淆,或者芯片实际地址和手册不一致)。
- 芯片没上电或者复位引脚没拉对。
- 总线频率过快芯片不支持。
- 上拉电阻缺失或阻值不对,导致信号质量差。
- 设备不在这个总线上,可能挂到另一路i2c控制器上了。
排查顺序:先确认设备树节点reg正确;再用i2cdetect扫地址;然后用i2cget手动读;最后再看驱动代码逻辑。有个项目里经验是:某芯片手册上地址是“0x90写/0x91读”,但实际芯片A2/A1/A0引脚全部接地后i2cdetect显示0x48,当时就靠“先硬件验证、再软件排查”定位到了硬件地址,驱动没改一行。
6.2 /dev下没有设备节点
多半是probe没跑成功。用dmesg看有没有cdev_add失败的日志,或者of_match_table没匹配上。这里有个常见误解:以为只要加载了驱动模块就会自动生成节点,忽略了probe被调用需要“设备存在”。设备树节点没写好、compatible写错,probe根本不执行,自然没有节点。
6.3 读到的一直是0xff或者0x00
这类情况看起来像是“读到了”却全是固定值。可能原因:
- 寄存器地址没写对,芯片返回默认值或空值。
- 芯片处于掉电模式或者配置寄存器没初始化。
- 读时序不对,比如手册要求先写寄存器地址必须带停止位,你的两条消息被i2c子系统合并成一条事务,导致芯片状态机错乱。这时可以把msgs数组拆成两次i2c_transfer,中间加一点延迟看看是否变成正常读数。
6.4 总线锁死,SDA一直低
i2c总线的SDA线被某个设备拉低,通常是通信过程中设备端出了一个非正常状态。解决办法是总线恢复:
i2cdetect -y -r 1或者通过i2c-tools工具触发恢复时序。设计上尽量让设备驱动在remove时把总线状态清干净。调试时如果总线已经死了,最简单的是复位所有设备或重新上电,别老想在软件层面硬解。
6.5 多进程访问导致的混乱
如果不做锁保护,两个进程同时ioctl读写传感器,底层可能出现交叉消息,比如A的消息和B的消息混在一起,寄存器读写对不上。用了mutex之后,同一时刻只有一笔transaction在执行,简单有效。不过要注意,mutex_lock放在i2c_transfer外面而不是消息构造外面,代码里我通常是把消息构造和传输都包在同一把锁里,确保不会出现“构造到一半被切走”的情况。
6.6 数据长度不对齐
有些芯片写寄存器时需要先写两个字节的寄存器地址(比如16位寄存器地址),或者读数据时地址按页偏移。这时候i2c_msg长度必须精确匹配芯片要求,多一个字节少一个字节都会出问题。解决办法是死死对照芯片datasheet里的时序图,数清楚每个字节是什么角色。
7. 避开这些坑,驱动才能长期稳定
7.1 别忘了检查i2c_check_functionality
有些adapter不支持重复起始条件,或者不支持某种flags,确实存在。如果驱动里用了特殊flags(比如I2C_M_RECV_LEN这种),一定要在probe阶段提前检测。宁可在probe时返回错误,也不要等用户操作到一半才发现底层不支持。
7.2 设备树改完后要吃透生效条件
改了dts后,要么重新编译设备树烧写,要么在支持设备树overlay的系统上动态加载。有些人改了dts后只reboot不重新编译,然后发现没生效,浪费时间排查。确认加载的dtb确实包含你改的节点,可以用:
ls /proc/device-tree/查看实际加载的设备树内容,这个目录下能看到所有节点。
7.3 寄存器读写接口要统一
一个驱动里,读寄存器可能用i2c_transfer,写寄存器可能用i2c_master_send,功能上没错,但出错时排查路径不统一。我习惯把所有的寄存器读写都封装成两个函数,一个read_regs一个write_regs,里面统一走i2c_transfer。后续要加日志、加统计、加错误重试,都只改这两个函数,其他地方不用动。
7.4 多字节读用一次事务而不是循环单字节读
如果你要读一组连续寄存器,循环单字节读会占用大量总线时间,而且每次都要重新发起地址+数据的传输,很多芯片在连续读时有自动地址递增特性,一条长读事务就能搞定。所以能合并的消息尽量合并,性能差距在低速i2c上特别明显。
7.5 消息缓冲的生命周期要盯死
kmalloc出来的buf,在i2c_transfer返回之前不能释放,返回之后立刻释放没问题。i2c_transfer同步执行,所以这个生命周期很容易管理,但如果你以后引入异步传输框架,就得小心了。交付前做个内存检查,kmalloc和kfree一一配对。
8. 一点额外的收获和习惯养成
如果让我给刚接触这块的人一个建议,那就是“先工具验证硬件,再设计字符设备,最后才写具体传输逻辑”。这个顺序我踩过几次坑才建立起来。工具验证通过后,驱动写起来心里踏实得多;反过来,如果驱动半天调不通,至少你知道不是硬件时序的问题,排查范围一下子就缩小了。
另外建议在驱动里多加点调试打印。驱动开发阶段开着dev_dbg或者dev_info,记录每条i2c传输的寄存器地址和返回值,等一切稳定后再关掉或改成tracepoint。我自己见过不少驱动调试困难,都是因为日志太少,出问题时无从下手。打印不是给别人看的,是给自己排障用的,该加就加。
最后想说的是,字符设备加i2c子系统这套组合,看起来代码量不小,但真正核心的思想很朴素:字符设备让用户态好使,i2c子系统让底层传输可靠,两者通过i2c_client和i2c_transfer衔接。你把这两个纽带吃透,之后无论换什么传感器、什么eeprom,代码结构都不用大改,最多改一下消息构造和寄存器表而已。这个框架的价值就在于,它能陪着你把一个又一个新设备快速接入系统,而且接入之后心里有底。