☰
Linux I2C字符设备i2c-dev注册流程与ioctl调试
2026/10/26 6:58:07 网站建设 项目流程

调一个 I2C 上的器件,很多人第一反应是写个内核驱动,编译、烧板、看 dmesg,来回好几轮才能读出一个寄存器。其实绝大多数调试场景根本用不着这一步,先看看ls /dev/i2c-*有没有节点,有的话用几十行用户态代码就能把寄存器读出来。这里说的主角就是Linux I2C 字符设备,也就是i2c-dev这个模块暴露出来的/dev/i2c-N。它的注册过程藏在内核源码里,决定了这个节点什么时候出现、次设备号怎么分配;它的使用方式则完全靠ioctl这套命令族撑起来,open 之后能干什么、不能干什么,全都写在那几个命令的语义里。这篇内容适合正在调 I2C 外设的嵌入式软件工程师、刚接手一块新板子需要快速验证传感器的驱动开发者,以及想搞清楚/dev/i2c-1背后到底发生了什么的人。我会把注册流程拆到每一行关键代码,再把使用侧从单字节读写到组合传输、SMBus 块传输全部走一遍,最后把踩过的坑列成表,方便你直接对照排查。

1. 先搞清楚 i2c-dev 在内核里的定位

1.1 从一个真实调试场景切入

板子上挂了一颗温湿度传感器,地址是 0x44,你想先确认它到底在不在、能不能应答。这时候最省事的路径不是写驱动,而是打开/dev/i2c-1,用ioctl把它设成从机地址,然后发一个字节。整个过程不涉及设备树匹配、不涉及 probe、不需要重新编译内核模块,改一行代码重新编译用户态程序就行。这就是 i2c-dev 存在的意义:把 I2C 适配器抽象成一个字符设备,让用户态能直接下发起始条件、地址、数据、停止条件这一整套时序。

但要注意,i2c-dev本身不是一个"驱动某个具体芯片"的驱动,它驱动的是I2C 适配器(adapter),也就是 SoC 内部那个 I2C 控制器。适配器在设备树里描述、在平台驱动 probe 时注册进 I2C 核心层,i2c-dev只是给每个注册成功的适配器配一个字符设备节点。所以你在/dev下看到的i2c-0、i2c-1,编号对应的是适配器编号,不是器件地址。这一点理解错了,后面看代码会一直别扭。

1.2 三条访问路径的对比

Linux 下访问一颗 I2C 器件,常规有三条路,选哪条取决于你的目的是"验证"还是"交付"。

路径入口适用阶段优点代价
i2c-dev 字符设备/dev/i2c-N+ ioctl调试、产测、小工具不用写内核代码,改完即跑用户态权限、无中断支持、性能一般
i2c_driver 内核驱动i2c_driver+probe正式产品能注册 input/led/hwmon 等子系统开发周期长,调试要反复编译
i2c_client + 裸传输i2c_transfer在别的驱动里调用复合设备复用现有适配器依然在内核态,改动成本高

大部分人的实际节奏是:先用第一条路把器件摸透,寄存器表、时序要求、上电流程全验证完,再决定要不要下沉到第二条路。i2c-dev的价值就在于把"摸透"这一步的成本压到最低。

1.3 它和厂商驱动不是竞争关系

有个常见误解:既然/dev/i2c-1能直接操作总线,那内核里的i2c_driver是不是多余了?不是。字符设备走的是"原始报文"模型,你发什么它发什么,内核不知道你在跟谁说话;而i2c_driver走的是"设备模型",内核知道这条总线上挂了一颗什么芯片、它归哪个子系统管、休眠唤醒怎么处理。两者可以共存,但如果同一颗器件已经被某个内核驱动 probe 了,你再用/dev/i2c-N去发同样的命令,就可能出现状态互相踩踏。所以我的习惯是:调试阶段确认目标器件没有被内核驱动绑定,ls /sys/bus/i2c/devices/看一眼就知道,避免出现"用户态读出来全是 0xFF"这种莫名其妙的现象。

2. 注册流程拆解:那几十行代码到底做了什么

2.1 模块初始化函数的三个动作

i2c-dev.c的入口函数叫i2c_dev_init,做的事情非常克制,可以归纳成三步:注册字符设备、创建 class、注册总线通知链。核心那行是:

res = register_chrdev(I2C_MAJOR, "i2c", &i2cdev_fops);

I2C_MAJOR在早期内核里被硬编码成 89,后来改成优先尝试 89,占用失败就退化成动态分配。这里为什么要抢一个固定主设备号?因为用户态工具(比如各种 i2c 调试脚本)历史上依赖mknod /dev/i2c-0 c 89 0这种写法,固定主设备号能保证老脚本不改也能跑。但现代发行版基本都靠 udev 自动创建节点,硬编码的意义在减弱,所以你会看到不同内核版本这行代码的处理方式略有差别,有的是register_chrdev,有的换成alloc_chrdev_region+cdev_add的组合。

register_chrdev这个老接口一次干了三件事:申请主设备号范围、初始化 cdev 并挂到内核、把i2cdev_fops这套操作函数绑定上去。它的缺点是次设备号范围写死成 0~255,对 I2C 来说完全够用,因为一块板子上挂十几条 I2C 总线已经算多了。

接下来是创建 class:

i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");

这行的作用是让 udev 有依据去/dev下建节点。名称 "i2c-dev" 会出现在/sys/class/i2c-dev/下面,每个适配器对应一个子目录。注意不同内核版本的class_create参数个数不一样,5.15 之后去掉了 owner 参数,移植代码的时候这里是高频报错点。

2.2 次设备号与设备节点的对应关系

真正把/dev/i2c-N建出来的是device_create:

device_create(i2c_dev_class, &adap->dev, MKDEV(I2C_MAJOR, adap->nr), NULL, "i2c-%d", adap->nr);

这里的关键是adap->nr,也就是适配器编号。它由 I2C 核心层在适配器注册时分配,来源可能是设备树里的 aliases,也可能是注册顺序。次设备号直接等于适配器编号,所以/dev/i2c-2对应的就是adap->nr == 2的那个适配器,主设备号统一是 89(或动态分配的号)。我给新人解释的时候喜欢打个比方:主设备号像"楼层",次设备号像"房间号",i2c-dev这一层楼里每个房间放一条总线,房间号就是总线编号。

这里有个实操细节值得记住:如果你在设备树里写了aliases { i2c2 = &i2c1; }这类别名,节点编号就会按别名走,而不是按 probe 顺序。板子上有多条 I2C 总线时,强烈建议用别名固定编号,否则内核升级、驱动加载顺序一变,/dev/i2c-1可能就换成了另一条物理总线,用户态程序直接读错设备。

2.3 i2cdev_fops 的成员逐个过

i2cdev_fops定义了用户态能对这个节点做什么,成员不多但每个都有讲究:

static const struct file_operations i2cdev_fops = { .owner = THIS_MODULE, .llseek = no_llseek, .read = i2cdev_read, .write = i2cdev_write, .unlocked_ioctl = i2cdev_ioctl, .compat_ioctl = compat_i2cdev_ioctl, .open = i2cdev_open, .release = i2cdev_release, };

llseek直接给了no_llseek,意思是这个设备不支持定位,你不能lseek到一个偏移再读,因为 I2C 是流式报文,没有"文件偏移"这个概念。compat_ioctl是为了让 32 位程序在 64 位内核上跑ioctl时能正确转换结构体指针,跨架构部署工具时这个成员不能少。.owner = THIS_MODULE保证文件打开期间模块不会被卸载,防止你在ioctl中途模块被rmmod掉导致内核崩溃。

read和write这两个成员看起来最亲切,但它们的语义和普通文件差别很大,下一节单独说。

2.4 总线通知链让节点跟着适配器动态出现

适配器不是一次性全部注册完的,尤其是 I2C 控制器驱动可能编译成模块、可能延迟 probe。所以i2c-dev不能只在初始化时扫一遍,还要挂一个通知链:

res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);

i2cdev_notifier_call里只关心两个事件:BUS_NOTIFY_ADD_DEVICE和BUS_NOTIFY_DEL_DEVICE。前者在适配器注册后调用i2cdev_attach_adapter建节点,后者在适配器注销时调用i2cdev_detach_adapter删节点。判断条件里有个小细节:只有dev->type == &i2c_adapter_type的才是适配器,i2c_client也挂在同一条总线上,不加这个判断会把每个从设备都当成适配器处理。

这个机制的实用价值在于热插拔场景。比如 USB 转 I2C 的桥接器插上之后,适配器动态注册,/dev/i2c-3会自动出现;拔掉之后节点自动消失。用户态程序如果一直拿着 fd,拔掉瞬间的read会返回ENODEV,这个错误码就是从这里来的。

3. open 与 read/write 的真实语义

3.1 open 阶段做的三件事

i2cdev_open的逻辑很短但很关键。第一,从inode->i_rdev里取出次设备号,调用i2c_get_adapter(minor)拿到适配器指针,同时给适配器的引用计数加一——这一步保证了只要还有 fd 打开着,适配器就不会被注销。第二,在i2c_dev数组里占一个槽位,把私有数据挂到file->private_data上,后续所有ioctl都从这里取上下文。第三,初始化一个"临时 client",把addr设成 0,等你后面用I2C_SLAVE再填。

这里有个容易忽略的点:i2c_get_adapter返回的是适配器指针,如果此时适配器正在被注销,函数可能返回 NULL,open就会失败并返回ENODEV。所以"open 失败不一定是权限问题",这一点在排查章节还会提。

还有一个老生常谈的细节:open 之后client->addr是 0,如果你忘了ioctl(I2C_SLAVE, ...)直接read,内核会拿地址 0 去发包。地址 0 是 I2C 的通用呼叫地址(general call),绝大多数器件不应答,于是你收到ENXIO或者EREMOTEIO,然后开始怀疑人生。我带的实习同学十个里有八个在这栽过。

3.2 read/write 为什么只能搬一个字节

看一眼i2cdev_read的实现就明白了:

static ssize_t i2cdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { char *tmp; struct i2c_msg msgs; ... if (count > 8192) count = 8192; msgs.addr = client->addr; msgs.flags = I2C_M_RD; msgs.len = count; msgs.buf = tmp; ret = i2c_transfer(client->adapter, &msgs, 1); ... }

它只构造了一条i2c_msg,而且这条消息里没有"寄存器地址"这一说。对于 EEPROM 这类需要在读之前先写地址的器件,一条纯读消息是没意义的——器件不知道你要读哪个位置,会从内部地址指针的当前位置返回数据。所以你用read读 EEPROM,第一次可能读到 0x00,多读几次发现数据在动,这就是地址指针在自增。

write更直接:一条消息,把用户缓冲区整个作为数据发出去,第一个字节通常会被器件解释成寄存器地址,后面的才是数据。于是"先写地址再读数据"这个经典的两段式操作,在read/write模型下是做不到原子的,因为两次系统调用之间总线可能被别的进程抢走。

所以结论很清晰:read/write只适合那些不需要内部地址的器件,或者你自己能容忍两次调用之间被打断的场景。严谨的器件访问一定要走I2C_RDWR。

3.3 release 时的资源回收

i2cdev_release做的事和 open 对称:把槽位清空、i2c_put_adapter减引用计数、释放临时 client。这里有个历史遗留问题:早期代码里客户端结构是动态分配的,释放不及时会泄漏,所以老内核里能看到kfree(client)这类调用。现代内核把 client 内嵌在i2c_dev结构里,跟着槽位一起管理,泄漏风险小很多。

引用计数的意义在于:如果适配器对应的平台驱动被卸载,而用户态还开着/dev/i2c-N,卸载流程会卡住等引用归零。这个行为对调试是好事,能防止你在传输中途被釜底抽薪;但也意味着"模块卸不掉"有时候不是 bug,是有人忘了 close。

4. ioctl 命令族与传输模型

4.1 连接与参数类命令

i2cdev_ioctl是一个大 switch,前半部分是参数设置,后半部分是数据传输。

I2C_SLAVE是最常用的,参数是 7 位从机地址。它内部会检查地址合法性,并且在目标地址上已经绑定了内核驱动时拒绝,返回EBUSY。这个保护很贴心,避免你手滑去操作已经被系统接管的器件。I2C_SLAVE_FORCE则跳过检查强行设置,只有在你明确知道自己在干什么、比如临时接管一颗被错误绑定的器件时才用。

I2C_TENBIT用于切换 10 位地址模式,参数非 0 表示后续地址按 10 位解释。这里有个坑:很多器件的 10 位地址支持和普通 7 位地址不能混用,切换之后要重新设I2C_SLAVE。

I2C_RETRIES设置传输失败后的重试次数,默认是 1。总线上有干扰、从机偶发不应答时,把它调到 2 或 3 能显著提升稳定性,代价是失败时耗时变长。I2C_TIMEOUT单位是 jiffies,设置单次传输的超时,默认值在不同内核版本里不一样,从 1 到 10 都有。对于慢速器件(比如某些带内部写周期的 EEPROM),适当调大能避免误判超时。

I2C_FUNCS是必查项。它返回一个位图,告诉你当前适配器支持哪些能力:是否支持原始 I2C 传输、是否支持 10 位地址、支持哪些 SMBus 协议。为什么必查?因为不是所有适配器都实现了完整 SMBus 协议栈,有些只支持I2C_FUNC_I2C(原始传输),你调I2C_SMBUS会直接返回EOPNOTSUPP。写可移植工具时,先查一次能力位图再选路径,比事后猜错误码强得多。

4.2 I2C_RDWR 组合消息

I2C_RDWR是这套接口里最有价值的一个命令,因为它支持一次系统调用、多条消息、原子执行。参数结构是这样:

struct i2c_rdwr_ioctl_data { struct i2c_msg __user *msgs; __u32 nmsgs; };

msgs指向用户态的一个消息数组,nmsgs是条数。每条i2c_msg描述一段传输:

struct i2c_msg { __u16 addr; __u16 flags; __u16 len; __u8 *buf; };

flags里最常用的是I2C_M_RD,表示这一段是读。还有几个值得记住的:I2C_M_NOSTART表示这一段不发起始条件,直接接着上一段的数据流继续,用于某些需要"重复起始"语义的特殊器件;I2C_M_IGNORE_NAK让内核忽略从机的 NAK,用于探测场景;I2C_M_RECV_LEN配合 SMBus 块读,动态接收长度。

"原子"这两个字是整个命令的核心价值。内核在i2cdev_ioctl_rdwr里会先一次性把用户态的消息数组拷进内核、逐条校验地址和长度,然后调i2c_transfer把整个消息序列交给适配器驱动连续发出去。中间不会释放总线,也不会被别的进程插入。对于"写地址 + 重复起始 + 读数据"这种经典时序,这是唯一正确的做法。

关于长度上限,i2cdev_ioctl_rdwr会检查msgs[i].len不超过I2C_RDWR_IOCTL_MAX_MSGS(32)条消息,单条长度也有限制,超了直接返回EINVAL。实际调试大块数据时,如果一次要读几百字节,得分批读,别指望一口气搬完。

4.3 SMBus 命令族与数据布局

I2C_SMBUS是另一套抽象,参数结构更长:

struct i2c_smbus_ioctl_data { __u8 read_write; /* I2C_SMBUS_READ / I2C_SMBUS_WRITE */ __u8 command; /* 寄存器地址 */ __u32 size; /* 协议类型 */ union i2c_smbus_data __user *data; };

size的取值决定了这一次传输的格式,每个值对应一种协议:

size 宏值传输格式典型用途
I2C_SMBUS_QUICK0只有地址+读写位,无数据探测器件是否存在
I2C_SMBUS_BYTE1一个字节,无 command简单命令器件
I2C_SMBUS_BYTE_DATA2command + 1 字节数据大部分寄存器访问
I2C_SMBUS_WORD_DATA3command + 2 字节(小端)16 位寄存器
I2C_SMBUS_BLOCK_DATA5command + 长度字节 + 最多 32 字节块读写
I2C_SMBUS_I2C_BLOCK_DATA8command + 纯 I2C 块(无长度字节)类 EEPROM 器件

数据容器是个 union:

union i2c_smbus_data { __u8 byte; __u16 word; __u8 block[I2C_SMBUS_BLOCK_MAX + 2]; /* 34 字节 */ };

block数组为什么是 34 而不是 32?因为第一个字节存长度,最后一个字节留给 PEC 校验,中间的 32 字节才是有效载荷。写块数据的时候,必须先把长度写进block[0],内核会把它作为长度字节发出去;读块数据的时候,内核把器件返回的长度填进block[0],从block[1]开始才是数据。这个布局我第一次用的时候搞反了,把数据从block[0]开始填,结果器件收到的第一个字节变成了长度,行为诡异了半天。

I2C_PEC用于开关 SMBus 的包错误校验,参数非 0 打开。打开之后每次传输末尾会多一个 CRC 字节,由硬件或内核计算。这个功能对长距离、高干扰环境有价值,但要求器件本身支持,不支持的话打开之后所有传输都会失败。

4.4 命令与能力位速查表

调试时最烦的是"记不住哪个宏对应哪个值",尤其在没有头文件的环境里手写 ioctl 调用。这里列一份常用的:

命令值参数类型作用
I2C_RETRIES0x0701unsigned long设置重试次数
I2C_TIMEOUT0x0702unsigned long(jiffies)设置超时
I2C_SLAVE0x0703unsigned long地址设置从机地址(带检查)
I2C_SLAVE_FORCE0x0706unsigned long地址强制设置从机地址
I2C_TENBIT0x0704unsigned long0/110 位地址开关
I2C_FUNCS0x0705unsigned long *查询适配器能力
I2C_RDWR0x0707struct i2c_rdwr_ioctl_data *组合消息传输
I2C_PEC0x0708unsigned long0/1PEC 校验开关
I2C_SMBUS0x0720struct i2c_smbus_ioctl_data *SMBus 协议传输

能力位图里常看的那几位:I2C_FUNC_I2C(0x01)表示支持原始传输,I2C_FUNC_SMBUS_BYTE_DATA(0x00080000)表示支持字节数据协议,I2C_FUNC_SMBUS_READ_I2C_BLOCK(0x04000000)表示支持纯 I2C 块读。判断方式就是按位与,结果为 0 说明不支持,赶紧换路径。

5. 用户态读写 EEPROM 的完整实现

5.1 硬件准备与地址确认

拿一颗 24C 系列 EEPROM 做例子,容量 2Kbit(256 字节),7 位地址 0x50,页大小 8 字节,内部地址是单字节。这类器件是最经典的 I2C 字符设备练手对象,因为它的时序要求清清楚楚:写操作是"地址 + 数据",读操作是"写地址 + 重复起始 + 读数据"。

上电之前先确认三件事。第一,总线上有没有上拉电阻,很多开发板把上拉做在 SoC 侧,外接模块上如果也带了上拉,并联之后阻值变小,高速率下波形反而变差。第二,地址引脚 A0/A1/A2 怎么接的,全接地就是 0x50,接一个高就是 0x51,别拿万用表量完就忘。第三,ls /sys/bus/i2c/devices/看一下有没有现成的驱动绑在这颗器件上,有的话先unbind或者干脆换个器件地址,避免冲突。

确认总线编号也很重要。ls /dev/i2c-*列出来的只是编号,物理对应关系得看设备树或者cat /sys/class/i2c-dev/i2c-N/name,输出的名字通常是控制器名,能帮你对上号。

5.2 头文件与编译方式

用户态用这套接口,头文件有两个来源。内核头文件<linux/i2c-dev.h>和<linux/i2c.h>,前者定义了 ioctl 常量和i2c_smbus_ioctl_data,后者定义了i2c_msg和I2C_M_RD。编译环境装了内核头文件的话直接:

#include <linux/i2c.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> #include <fcntl.h> #include <unistd.h>

有些发行版里这两个头文件会重复定义某些宏,编译报redefined的时候,把i2c-dev.h里的冲突部分注释掉,或者干脆自己手写一遍常量定义。另一种做法是用 i2c-tools 项目提供的i2c/smbus.h,它把用户态需要的东西重新组织了一遍,兼容性更好,代价是多一个依赖。

编译命令很朴素:

gcc -O2 -Wall -o i2c_eeprom i2c_eeprom.c

交叉编译就换成交叉工具链的前缀,注意--sysroot指向目标板的根文件系统,否则头文件版本可能对不上。

5.3 单字节写与页写

先看最简单的单字节写:

int eeprom_write_byte(int fd, unsigned char addr, unsigned char val) { unsigned char buf[2] = { addr, val }; if (write(fd, buf, 2) != 2) { perror("write"); return -1; } usleep(5000); /* 等内部写周期结束 */ return 0; }

write把buf整个发出去,器件收到的第一个字节是内部地址,第二个字节是数据。这里必须等 5ms,因为 EEPROM 在收到停止条件后进入内部写周期,这段时间内它不会应答任何请求。如果你紧接着发下一次写,器件要么 NAK,要么数据写坏。5ms 是这类器件的典型值,具体看手册里的tWR。

页写就是把多个字节一次性发出去:

int eeprom_write_page(int fd, unsigned char addr, const unsigned char *data, int len) { unsigned char buf[1 + 8]; if (len > 8) len = 8; if ((addr & 0x07) + len > 8) len = 8 - (addr & 0x07); /* 不跨页 */ buf[0] = addr; memcpy(buf + 1, data, len); if (write(fd, buf, len + 1) != len + 1) { perror("write page"); return -1; } usleep(5000); return len; }

这里有两个细节。第一,不能跨页。这类器件的内部地址指针在页内会自增,到了页边界会回卷到页首,你写 8 个字节跨越页边界,后几个会覆盖掉页首的数据,而且不会有任何报错。第二,页大小不同型号不一样,8 字节、16 字节、32 字节都有,写之前查手册,别拍脑袋。

5.4 用 I2C_RDWR 做组合传输读

读操作才是I2C_RDWR的主场:

int eeprom_read(int fd, unsigned char addr, unsigned char *out, int len) { struct i2c_msg msgs[2]; struct i2c_rdwr_ioctl_data ioctl_data; msgs[0].addr = 0x50; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = &addr; msgs[1].addr = 0x50; msgs[1].flags = I2C_M_RD; msgs[1].len = len; msgs[1].buf = out; ioctl_data.msgs = msgs; ioctl_data.nmsgs = 2; if (ioctl(fd, I2C_RDWR, &ioctl_data) < 0) { perror("I2C_RDWR"); return -1; } return 0; }

两条消息,第一条写地址(不发停止),第二条读数据,内核会在两条消息之间发重复起始条件。这正是 EEPROM 手册要求的时序。对比一下用write+read两次系统调用的写法,那种写法中间有可能插入别的进程的总线操作,如果别的进程也动了这颗 EEPROM,它的内部地址指针就被改了,你读出来的数据是错位的。I2C_RDWR一次传输搞定,中间不会撒手。

如果你要读的器件不是 EEPROM,而是带 16 位寄存器地址的传感器,把第一条消息的len改成 2,buf指向两字节的大端地址数组就行。不同器件对地址字节序的要求不一样,有的手册写"MSB first",有的直接画时序图,看时序图最保险。

5.5 写周期与 ACK 轮询

固定usleep(5000)是最简单的做法,但不够优雅。更好的方式是 ACK 轮询:写完之后立刻发一个地址探测,器件忙的时候会 NAK,忙完了就 ACK。

int eeprom_wait_ready(int fd, int timeout_ms) { struct i2c_msg msg; struct i2c_rdwr_ioctl_data d; int waited = 0; msg.addr = 0x50; msg.flags = 0; msg.len = 0; msg.buf = NULL; d.msgs = &msg; d.nmsgs = 1; while (waited < timeout_ms) { if (ioctl(fd, I2C_RDWR, &d) >= 0) return 0; usleep(500); waited += 1; } return -1; }

len = 0的消息就是一次纯地址探测,等价于 SMBus 的 Quick 命令。器件忙的时候不应答,内核返回ENXIO或EREMOTEIO,你继续等就行。这种方式比死等固定时间快,尤其是写少量数据的时候,实际写周期可能只有 2~3ms。

不过要注意,某些适配器驱动对len = 0的消息支持不好,会直接返回EINVAL。保险起见,可以改成写一条 1 字节的哑数据到无效地址,行为上是等价的。我在两三个平台的控制器上都遇到过这个问题,写之前先试一次探测命令,能过就过,过不了换哑写,这个分支值得加上。

5.6 完整流程与验证

把上面的片段拼起来,主函数大致是:

int main(void) { int fd = open("/dev/i2c-1", O_RDWR); if (fd < 0) { perror("open"); return 1; } if (ioctl(fd, I2C_SLAVE, 0x50) < 0) { perror("I2C_SLAVE"); close(fd); return 1; } unsigned char pattern[8] = {0xA5, 0x5A, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; eeprom_write_page(fd, 0x00, pattern, 8); eeprom_wait_ready(fd, 10); unsigned char rd[8] = {0}; eeprom_read(fd, 0x00, rd, 8); for (int i = 0; i < 8; i++) printf("%02X ", rd[i]); printf("\n"); close(fd); return 0; }

注意I2C_SLAVE那一步只是设置"临时 client 的默认地址",I2C_RDWR里的每条msgs[i].addr才是真正生效的地址。两者不一致的时候以内核校验为准,如果msgs[i].addr跟I2C_SLAVE设的不一样,内核会返回EINVAL。很多人写代码时复制粘贴,把I2C_SLAVE的地址和消息里的地址写成两样,然后就卡在这个错误上。

验证的时候有个技巧:先用i2cdetect之类的工具扫一遍总线,确认目标地址有应答,再跑自己的程序。扫不到就说明硬件连接、上拉、供电有问题,跟软件无关,别浪费时间调代码。数据写完之后读回来对比,如果能稳定读回一致的内容,说明时序、页边界、写周期都处理对了。

6. 踩过的坑与排查方法

6.1 打开设备失败的几种可能

open("/dev/i2c-1", O_RDWR)返回 -1,先看errno。

ENOENT说明节点不存在,可能是i2c-dev模块没加载(modprobe i2c-dev),也可能是适配器本身没注册成功,dmesg | grep i2c能看到控制器 probe 的日志。EACCES是权限问题,节点属组通常是i2c,把你的用户加进去,或者临时用sudo跑一次确认。ENODEV一般出现在i2c_get_adapter失败,也就是次设备号对应的适配器不存在,常见于拔掉了桥接器但代码还拿着旧编号。

还有一种特别隐蔽的:open成功但后续所有操作都失败,检查一下是不是打开了/dev/i2c-0这种"占位"节点。有些平台的适配器注册了但实际没接任何东西,i2c_transfer会直接返回ENXIO。

6.2 传输失败错误码对照

errno常见含义排查方向
ENXIO从机无应答地址错、器件没供电、上拉缺失
EREMOTEIO传输中途 NAK写周期未结束、数据超页、器件忙
EINVAL参数不合法消息地址与I2C_SLAVE不符、长度超限
EOPNOTSUPP适配器不支持该协议先查I2C_FUNCS再换路径
ETIMEDOUT总线超时时钟拉伸、总线被拉死、从机挂起
EBUSY地址被内核驱动占用先解绑或换地址
ENODEV适配器已消失检查硬件连接、模块是否被卸载

EREMOTEIO是最常见也最容易被误判的一个。它不代表"器件不存在",而是"器件存在但在某个字节上回了 NAK"。EEPROM 写周期没等够、SMBus 块长度写错、写数据超过页边界,都会表现成这个错误。我第一次遇到时以为是地址写错,换了三个地址才反应过来是写周期问题。

6.3 数据错位与重复起始的坑

读回来的数据整体偏移一位,或者前几个字节是上一轮的数据,八成是内部地址指针没重置。用write+read分开写的代码最容易出这个问题,改成I2C_RDWR组合传输基本能根治。

另一个坑是I2C_M_NOSTART用错。有些器件手册里画了"写地址、重复起始、读数据"的时序,有人误以为要用I2C_M_NOSTART来抑制起始条件,结果发现数据全乱。实际上I2C_RDWR在两条消息之间默认就会发重复起始,不需要额外标志。I2C_M_NOSTART是给那些要求"不发起始、直接续流"的特殊器件用的,普通 EEPROM 和传感器用不上。

还有个小细节:多条消息共享同一个buf指针的时候要小心生命周期。ioctl返回之后内核就不再碰这个缓冲区,但如果你的缓冲区是栈上的局部数组,而调用发生在另一个线程里异步执行,就会读到野数据。串行执行的话没问题,这点不用过度设计。

6.4 时钟拉伸与总线卡死

有些从机在忙的时候会拉住 SCL 不放,这叫时钟拉伸。控制器如果没实现拉伸处理,传输就会超时返回ETIMEDOUT。这种情况在低速器件上很常见,比如某些温湿度传感器转换期间会拉低 SCL 几十毫秒。

解决思路有两个方向。软件侧把I2C_TIMEOUT调大,给足时间;硬件侧检查上拉阻值,阻值太大导致上升沿变缓,等效于人为增加了拉伸时间。上拉阻值的经验值:100kHz 用 4.7k 到 10k,400kHz 用 1.5k 到 2.2k,具体看总线电容。总线电容超过 400pF 之后波形会明显变差,这时候要么降速,要么加缓冲器。

总线彻底卡死(SDA 一直被拉低)的时候,i2c_transfer会一直超时,所有后续操作都失败。恢复手段是手动发 9 个时钟脉冲让从机把数据吐完,或者干脆给器件断电重启。这个操作在用户态做不了,得靠控制器驱动提供恢复钩子,或者用 GPIO 模拟时序强行解锁。

6.5 多进程并发访问的隐患

/dev/i2c-N是全局共享的,两个进程同时 open 同一个节点,各自设不同的I2C_SLAVE地址,然后交叉传输,总线上就会出现地址和数据混在一起的诡异波形。内核层面没有对单次传输做互斥,i2c_transfer内部有 adapter 锁,但那把锁只保证单次调用不被打断,不保证你的"两次调用"是原子的。

实际项目里的做法是加文件锁:

int lock_fd = open("/var/lock/i2c-1.lock", O_CREAT | O_RDWR, 0666); flock(lock_fd, LOCK_EX); /* 传输 */ flock(lock_fd, LOCK_UN);

锁的粒度建议按总线来,不要按器件地址,因为不同器件挂在同一条总线上,物理层是共享的。另外注意flock在 NFS 上行为不一样,嵌入式本地文件系统没这个问题,但如果你把锁文件放在网络挂载的目录上,就可能失效。

7. 什么时候该从用户态下沉到内核驱动

7.1 内核侧 API 对照

用户态玩得差不多了,该判断要不要写驱动。内核侧其实有一套和I2C_RDWR完全对应的 API:

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); int i2c_master_send(struct i2c_client *client, const char *buf, int count); int i2c_master_recv(struct i2c_client *client, char *buf, int count); s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command); s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value); s32 i2c_smbus_read_i2c_block_data(struct i2c_client *client, u8 command, u8 length, u8 *values);

i2c_transfer就是I2C_RDWR的内核版本,参数结构一模一样,只是地址在client->addr里,不用每次填。i2c_master_send/recv对应write/read的单消息语义。SMBus 那一组函数则把i2c_smbus_ioctl_data的组装过程封装掉了,调用起来更清爽。写驱动时如果只需要读寄存器,直接用i2c_smbus_read_byte_data,比自己拼i2c_msg少写十几行。

7.2 选型判断的实际标准

我的判断标准很朴素,满足下面任意一条就该下沉到内核驱动:

第一,需要中断。器件通过 INT 引脚通知事件,用户态没有中断概念,只能轮询,延迟和功耗都不可接受。第二,需要接入子系统。比如温湿度传感器要暴露成 hwmon,触摸屏要走 input,LED 要走 leds,这些都得在内核里注册。第三,需要参与电源管理。系统休眠时要给器件下电、唤醒时要重新初始化,这套流程挂在dev_pm_ops上,用户态做不了。第四,性能敏感。用户态每次ioctl都有系统调用开销,高频采样场景下这个开销占比不小。

反过来,如果只是产测、调试、一次性配置,用户态工具是更好的选择。改动成本低、可以热更新、出问题不影响系统稳定性。我见过不少产品把产测逻辑也塞进内核驱动里,结果每次调整测试流程都要重新烧固件,纯属给自己找麻烦。

写驱动时还有一点值得提醒:probe函数里不要做耗时操作,尤其是长延时。有些器件的上电初始化需要等几十毫秒,放在probe里会拖慢整个系统的启动。更好的做法是把初始化拆成异步任务,或者延迟到第一次实际使用时再做。这个经验在多个项目里都验证过,probe里堆延时是后期启动时间超标的高频原因。

我个人在实际项目里的体会是,i2c-dev这套接口真正的价值不在于"能读写",而在于它把调试成本压到了几乎为零。一颗新器件上手,先用它把寄存器表、时序要求、异常行为全部摸清楚,写成一份可复现的测试脚本,再交给内核驱动去实现,两边的行为能对得上,出了问题也知道该怀疑哪一层。至于那些"打开就失败"、"读出来全是 0xFF"的情况,九成以上不是接口本身的问题,而是地址、上拉、写周期、页边界这些硬件和时序细节没对齐,把这几项逐个排掉,剩下的基本就是顺理成章的事了。

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

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

立即咨询