Linux驱动-在用户态控制I2C-两种方案ioctl和文件控制

📅 2026/7/29 18:38:00
Linux驱动-在用户态控制I2C-两种方案ioctl和文件控制
提示Linux驱动-在用户态控制I2C文章目录前言一、参考资料二、关联代码关联源码关联源码解析核心定位核心数据结构struct i2c_dev结构体open 时创建匿名 struct i2c_client文件操作接口 i2cdev_fops重中之重i2cdev_write ()i2cdev_read ()ioctl 核心函数 i2cdev_ioctl()分支 1I2C_SLAVE / I2C_SLAVE_FORCE分支 2I2C_RDWR推荐方案入口分支 3I2C_SMBUS设备初始化与生命周期1.模块入口 i2c_dev_init2. 总线通知器 i2cdev_notifier_call3. open /release 流程两条用户态 API 路径完整调用链路对比路径 Aread () /write () 方案I2C_SLAVE路径 Bioctl (I2C_RDWR) 方案推荐三、Client 端控制代码ioctl 实现i2c 控制文件方式read/write 方式实现i2c 控制两种client端控制i2c 区别总结前言前面知识点都是基于驱动程序知识点下面我们看看用户态 client 端如何通过i2c 接口实现i2c 驱动控制。这里讲解两种控制方案ioctl 和 文件控制 方案。一、参考资料这里侧重于之前的笔记知识点i2c 基本知识点前面知识点基础一定要掌握Linux驱动-i2c 驱动框架编写Linux驱动-IIC完善FT5X06设备节点和驱动Linux驱动-I2C通信-FT5X06驱动程序部分编写Linux驱动-通过逻辑分析仪-认识-分析IIC波形Linux 驱动-I2C输入子系统相关知识点Linux驱动-IIC通信-FT5X06驱动-数据上报案例-终端测试RK3568驱动指南第十五篇 I2C-第179章在应用程序中使用I2Cioctl 知识点驱动-传参实验-ioctl驱动-地址传参实验-ioctl二、关联代码关联源码路径/kernel/drivers/i2c/i2c-dev.c关联源码解析核心定位i2c-dev.c是 Linux 内核提供的【用户态 I2C 适配器驱动】作用暴露/dev/i2c-X字符设备允许应用层程序直接操作硬件 I2C 总线不需要编写内核驱动。它不是 I2C 硬件控制器驱动只是上层封装最终所有传输都会调用 I2C 核心i2c_transfer()。核心数据结构struct i2c_dev结构体struct i2c_dev{struct list_head list;struct i2c_adapter*adap;// 指向硬件I2C适配器I2C控制器struct device dev;struct cdev cdev;// 字符设备核心结构绑定file_operations};每一条硬件 I2C 总线/dev/i2c-0//dev/i2c-1对应一个i2c_devadap底层硬件控制器RK3568/MTK 的 I2C 硬件驱动最终操作这个 adapteropen 时创建匿名struct i2c_clientstaticinti2cdev_open(struct inode*inode,struct file*file){// ...clientkzalloc(sizeof(*client),GFP_KERNEL);client-adapteradap;file-private_dataclient;}打开/dev/i2c-x时内核分配一个匿名 i2c_client。这个 client不会注册到内核设备树驱动模型仅仅用来保存从机地址client-addr由I2C_SLAVEioctl 设置标志位10bit 地址、PEC 校验等如果你使用read()/write()这套方案必须先调用ioctl(I2C_SLAVE_FORCE, addr)填充 client-addr否则读写直接报错。I2C_RDWR不需要依赖这个 client 内部 addr因为每条i2c_msg自带 addr。文件操作接口i2cdev_fops重中之重staticconststruct file_operations i2cdev_fops{.ownerTHIS_MODULE,.llseekno_llseek,.readi2cdev_read,// 用户态 read(fd, buf, len).writei2cdev_write,// 用户态 write(fd, buf, len).unlocked_ioctli2cdev_ioctl,// 用户态 ioctl().compat_ioctlcompat_i2cdev_ioctl,// 32位兼容.openi2cdev_open,.releasei2cdev_release,};i2cdev_write ()staticssize_ti2cdev_write(struct file*file,constchar__user*buf,size_t count,loff_t*offset){struct i2c_client*clientfile-private_data;// 1. 用户空间buf拷贝到内核空间 tmptmpmemdup_user(buf,count);// 2. 调用 i2c_master_sendreti2c_master_send(client,tmp,count);kfree(tmp);returnret;}i2c_master_send内部行为自动组装单条struct i2c_msg写标志调用i2c_transfer()时序START 数据 STOP每次 write 都是独立 I2C 事务首尾自动加 START/STOPi2cdev_read ()staticssize_ti2cdev_read(struct file*file,char__user*buf,size_t count,loff_t*offset){struct i2c_client*clientfile-private_data;reti2c_master_recv(client,tmp,count);// 内核数据拷贝回用户空间copy_to_user(buf,tmp,ret);}i2c_master_recv自动组装单条带I2C_M_RD的 i2c_msg时序START 读地址 接收数据 STOP致命限制// 向寄存器 0x80 写入数据 0x66ft5x06_write_reg(fd,0x80,0x66);// 读取寄存器 0x80 的值ft5x06_read_reg(fd,0x80);内核执行两次独立事务writeSTART reg STOPreadSTART READ STOP中间出现 STOP没有 RESTART绝大多数寄存器芯片FT5x06收到 STOP 后丢弃寄存器地址读取失败。根源就在read/write 各自封装独立一次 i2c_transfer无法合并多条 msg。ioctl 核心函数i2cdev_ioctl()所有ioctl(fd, cmd, arg)进入这个函数通过 cmd 分支分发。分支 1I2C_SLAVE / I2C_SLAVE_FORCEcaseI2C_SLAVE:caseI2C_SLAVE_FORCE:client-addrarg;// 把从机地址存入匿名clientreturn0;I2C_SLAVE如果内核已经有驱动占用该从地址 → 返回-EBUSYI2C_SLAVE_FORCE强制覆盖地址无视内核驱动占用⚠️ 风险用户态和内核驱动同时访问同一个从机总线冲突、数据错乱分支 2I2C_RDWR推荐方案入口caseI2C_RDWR:{struct i2c_rdwr_ioctl_data rdwr_arg;// 1. 把用户态 struct i2c_rdwr_ioctl_data 拷贝进内核copy_from_user(rdwr_arg,...);// 2. 拷贝用户态 i2c_msg 数组到内核rdwr_pamemdup_user(rdwr_arg.msgs,rdwr_arg.nmsgs*sizeof(struct i2c_msg));// 3. 调用核心处理函数returni2cdev_ioctl_rdwr(client,rdwr_arg.nmsgs,rdwr_pa);}重点i2cdev_ioctl_rdwr()一次性传入多条 i2c_msg 数组内核 I2C 核心规则同一组多条连续 i2c_msg只发送一次 START消息之间使用RESTART无 STOP全部执行完成后发送一次 STOP。✅ 刚好满足 FT5x06 寄存器读时序msg0写寄存器地址 → RESTART → msg1读数据这就是为什么I2C_RDWR可以正常读取触控芯片寄存器分支 3I2C_SMBUS提供 SMBus 简化接口兼容 SMBus 协议设备普通 I2C 寄存器外设很少使用。设备初始化与生命周期1.模块入口i2c_dev_init注册字符设备主设备号I2C_MAJOR注册总线通知器监听 I2C 适配器的插拔遍历所有已经存在的 I2C 适配器自动创建/dev/i2c-X2. 总线通知器i2cdev_notifier_call新增 I2C 适配器 →i2cdev_attach_adapter()→ 创建/dev/i2c-x卸载 I2C 适配器 →i2cdev_detach_adapter()→ 删除设备节点3. open /release 流程open(/dev/i2c-1)→i2cdev_open()→ 获取i2c_adapter → 创建匿名i2c_client存入file-private_dataclose(fd)→i2cdev_release()→ 释放i2c_client内存 → 释放adapter引用计数两条用户态 API 路径完整调用链路对比路径 Aread () /write () 方案I2C_SLAVEappwrite()→ sys_write →i2cdev_write()→i2c_master_send()→i2c_transfer()【单条msg】 appread()→ sys_read →i2cdev_read()→i2c_master_recv()→i2c_transfer()【单条msg】缺陷两次系统调用 两次独立i2c_transfer中间 STOP无法 RESTART。路径 Bioctl (I2C_RDWR) 方案推荐appioctl(fd,I2C_RDWR,data)→ sys_ioctl →i2cdev_ioctl()→i2cdev_ioctl_rdwr()→i2c_transfer(...,nmsgs条消息)优势一次系统调用多条 msg 交给内核连续执行支持 RESTART 时序。三、Client 端控制代码ioctl 实现i2c 控制#includestdio.h#includelinux/i2c.h#includelinux/i2c-dev.h#includesys/ioctl.h#includesys/types.h#includesys/stat.h#includefcntl.h#includestring.h/** * brief 从 I2C 设备的寄存器中读取数据 * param fd 打开的 I2C 设备文件描述符 * param slave_addr I2C 设备的从机地址 * param reg_addr 要读取的寄存器地址 * return 寄存器的值 */intft5x06_read_reg(intfd,unsignedcharslave_addr,unsignedcharreg_addr){unsignedchardata;struct i2c_rdwr_ioctl_data i2c_msgs;intret;// 定义两个 i2c_msg 结构体, 第一个用于写入寄存器地址, 第二个用于读取数据struct i2c_msg dev_msgs[]{[0]{.addrslave_addr,.flags0,.lensizeof(reg_addr),.bufreg_addr,},[1]{.addrslave_addr,.flagsI2C_M_RD,// 设置读取标志.lensizeof(data),.bufdata,}};i2c_msgs.msgsdev_msgs;i2c_msgs.nmsgs2;// 两个 i2c_msg 结构体// 调用 ioctl 函数执行读取操作retioctl(fd,I2C_RDWR,i2c_msgs);if(ret0){printf(read error\n);returnret;}returndata;}/** * brief 向 I2C 设备的寄存器写入数据 * param fd 打开的 I2C 设备文件描述符 * param slave_addr I2C 设备的从机地址 * param reg_addr 要写入的寄存器地址 * param data 要写入的数据 * param len 数据长度 */voidft5x06_write_reg(intfd,unsignedcharslave_addr,unsignedcharreg_addr,unsignedchar*data,intlen){unsignedcharbuff[256];struct i2c_rdwr_ioctl_data i2c_msgs;intret;// 定义一个 i2c_msg 结构体, 用于写入寄存器地址和数据struct i2c_msg dev_msgs[]{[0]{.addrslave_addr,.flags0,.lenlen1,.bufbuff,}};// 将寄存器地址和数据拷贝到 buff 数组中buff[0]reg_addr;memcpy(buff[1],data,len);i2c_msgs.msgsdev_msgs;i2c_msgs.nmsgs1;// 调用 ioctl 函数执行写入操作retioctl(fd,I2C_RDWR,i2c_msgs);if(ret0){printf(write error\n);}}intmain(intargc,char*argv[]){intfd;intID_G_THGROUP;// 打开 I2C 设备文件fdopen(/dev/i2c-1,O_RDWR);if(fd0){printf(open error\n);returnfd;}// 向 0x38 地址的寄存器 0x80 写入 0x55unsignedchardata0x55;ft5x06_write_reg(fd,0x38,0x80,data,1);// 从 0x38 地址的寄存器 0x80 读取数据ID_G_THGROUPft5x06_read_reg(fd,0x38,0x80);printf(ID_G_THGROUP is 0x%02X\n,ID_G_THGROUP);return0;}文件方式read/write 方式实现i2c 控制#includestdio.h#includesys/ioctl.h#includesys/types.h#includesys/stat.h#includefcntl.h#includestring.h#includelinux/i2c.h#includelinux/i2c-dev.h#includeunistd.h/* * 从 I2C 设备读取寄存器值 * param fd: I2C 设备文件句柄 * param reg_addr: 要读取的寄存器地址 */voidft5x06_read_reg(intfd,unsignedcharreg_addr){unsignedcharrd_data[1];rd_data[0]reg_addr;write(fd,rd_data,1);read(fd,rd_data,1);printf(reg value is %x\n,rd_data[0]);}/* * 向 I2C 设备写入寄存器值 * param fd: I2C 设备文件句柄 * param reg_addr: 要写入的寄存器地址 * param data: 要写入的数据 */voidft5x06_write_reg(intfd,unsignedcharreg_addr,unsignedchardata){unsignedcharwr_data[2];wr_data[0]reg_addr;wr_data[1]data;write(fd,wr_data,2);}intmain(intargc,char*argv[]){intfd;// 打开 I2C 设备文件fdopen(/dev/i2c-1,O_RDWR);if(fd0){printf(open error\n);returnfd;}// 设置从设备地址为 0x38ioctl(fd,I2C_SLAVE_FORCE,0x38);// 向寄存器 0x80 写入数据 0x66ft5x06_write_reg(fd,0x80,0x66);// 读取寄存器 0x80 的值ft5x06_read_reg(fd,0x80);return0;}备注并不是所有的IC 都支持这种方式所以对于主控也是默认关闭的那么自己要检查一下是否打开了这种通讯方式。当然对于从设备的i2c 的IC 可能都不支持实战中如果发现不支持要核对下时是否真的支持不然茫然。两种client端控制i2c 区别对比项I2C_RDWR (方案 1)I2C_SLAVE read/write (方案 2)系统调用入口ioctl(I2C_RDWR)open → ioctl(I2C_SLAVE) → read/write事务控制一次 ioctl 执行多条 msg支持 RESTART每次 read/write 独立事务强制 STOP从机地址每条 i2c_msg 独立指定支持多从机整个 fd 绑定单一地址切换需要重设 ioctl标准寄存器读时序完美支持写 RESTART 读时序断裂大部分芯片读取失败代码复杂度较高很低和内核驱动i2c_transfer行为高度一致上层封装内核自动拆分事务总结这里给出了两种client 端控制i2c 外设的方式。 ioctl 和 文件控制方式。多琢磨不同方案的不同、优劣自己思考。