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

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

提示:Linux驱动-在用户态控制I2C

文章目录

  • 前言
  • 一、参考资料
  • 二、关联代码
    • 关联源码
    • 关联源码解析
      • 核心定位
      • 核心数据结构
        • struct i2c_dev结构体
        • open 时创建匿名 `struct i2c_client`
      • 文件操作接口 `i2cdev_fops`(重中之重)
        • i2cdev_write ()
        • i2cdev_read ()
      • ioctl 核心函数 `i2cdev_ioctl()`
        • 分支 1:`I2C_SLAVE / I2C_SLAVE_FORCE`
        • 分支 2:`I2C_RDWR`(推荐方案入口)
        • 分支 3:I2C_SMBUS
      • 设备初始化与生命周期
        • 1.模块入口 `i2c_dev_init`
        • 2. 总线通知器 `i2cdev_notifier_call`
        • 3. open /release 流程
      • 两条用户态 API 路径完整调用链路对比
        • 路径 A:read () /write () 方案(I2C_SLAVE)
        • 路径 B:ioctl (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章在应用程序中使用I2C

ioctl 知识点
驱动-传参实验-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_dev
  • adap:底层硬件控制器,RK3568/MTK 的 I2C 硬件驱动最终操作这个 adapter
open 时创建匿名struct i2c_client
staticinti2cdev_open(struct inode*inode,struct file*file){// ...client=kzalloc(sizeof(*client),GFP_KERNEL);client->adapter=adap;file->private_data=client;}

打开/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={.owner=THIS_MODULE,.llseek=no_llseek,.read=i2cdev_read,// 用户态 read(fd, buf, len).write=i2cdev_write,// 用户态 write(fd, buf, len).unlocked_ioctl=i2cdev_ioctl,// 用户态 ioctl().compat_ioctl=compat_i2cdev_ioctl,// 32位兼容.open=i2cdev_open,.release=i2cdev_release,};
i2cdev_write ()
staticssize_ti2cdev_write(struct file*file,constchar__user*buf,size_t count,loff_t*offset){struct i2c_client*client=file->private_data;// 1. 用户空间buf拷贝到内核空间 tmptmp=memdup_user(buf,count);// 2. 调用 i2c_master_sendret=i2c_master_send(client,tmp,count);kfree(tmp);returnret;}

i2c_master_send内部行为

自动组装单条struct i2c_msg(写标志),调用i2c_transfer()
时序:START + 数据 + STOP

每次 write 都是独立 I2C 事务,首尾自动加 START/STOP

i2cdev_read ()
staticssize_ti2cdev_read(struct file*file,char__user*buf,size_t count,loff_t*offset){struct i2c_client*client=file->private_data;ret=i2c_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);

内核执行两次独立事务:

  1. write:START + reg + STOP
  2. read:START + READ + STOP
    中间出现 STOP!没有 RESTART!
    绝大多数寄存器芯片(FT5x06)收到 STOP 后丢弃寄存器地址,读取失败。

根源就在:read/write 各自封装独立一次 i2c_transfer,无法合并多条 msg。

ioctl 核心函数i2cdev_ioctl()

所有ioctl(fd, cmd, arg)进入这个函数,通过 cmd 分支分发。

分支 1:I2C_SLAVE / I2C_SLAVE_FORCE
caseI2C_SLAVE:caseI2C_SLAVE_FORCE:client->addr=arg;// 把从机地址存入匿名clientreturn0;
  • I2C_SLAVE:如果内核已经有驱动占用该从地址 → 返回-EBUSY
  • I2C_SLAVE_FORCE:强制覆盖地址,无视内核驱动占用

⚠️ 风险:用户态和内核驱动同时访问同一个从机,总线冲突、数据错乱!

分支 2:I2C_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_pa=memdup_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可以正常读取触控芯片寄存器!

分支 3:I2C_SMBUS

提供 SMBus 简化接口,兼容 SMBus 协议设备,普通 I2C 寄存器外设很少使用。

设备初始化与生命周期

1.模块入口i2c_dev_init
  • 注册字符设备主设备号I2C_MAJOR
  • 注册总线通知器:监听 I2C 适配器的插拔
  • 遍历所有已经存在的 I2C 适配器,自动创建/dev/i2c-X
2. 总线通知器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 路径完整调用链路对比

路径 A:read () /write () 方案(I2C_SLAVE)
appwrite()→ 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。

路径 B:ioctl (I2C_RDWR) 方案(推荐)
appioctl(fd,I2C_RDWR,&data)→ sys_ioctl →i2cdev_ioctl()i2cdev_ioctl_rdwr()i2c_transfer(...,nmsgs条消息)

优势:一次系统调用,多条 msg 交给内核连续执行,支持 RESTART 时序。

三、Client 端控制代码

ioctl 实现i2c 控制

#include<stdio.h>#include<linux/i2c.h>#include<linux/i2c-dev.h>#include<sys/ioctl.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<string.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]={.addr=slave_addr,.flags=0,.len=sizeof(reg_addr),.buf=&reg_addr,},[1]={.addr=slave_addr,.flags=I2C_M_RD,// 设置读取标志.len=sizeof(data),.buf=&data,}};i2c_msgs.msgs=dev_msgs;i2c_msgs.nmsgs=2;// 两个 i2c_msg 结构体// 调用 ioctl 函数执行读取操作ret=ioctl(fd,I2C_RDWR,&i2c_msgs);if(ret<0){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]={.addr=slave_addr,.flags=0,.len=len+1,.buf=buff,}};// 将寄存器地址和数据拷贝到 buff 数组中buff[0]=reg_addr;memcpy(&buff[1],data,len);i2c_msgs.msgs=dev_msgs;i2c_msgs.nmsgs=1;// 调用 ioctl 函数执行写入操作ret=ioctl(fd,I2C_RDWR,&i2c_msgs);if(ret<0){printf("write error\n");}}intmain(intargc,char*argv[]){intfd;intID_G_THGROUP;// 打开 I2C 设备文件fd=open("/dev/i2c-1",O_RDWR);if(fd<0){printf("open error\n");returnfd;}// 向 0x38 地址的寄存器 0x80 写入 0x55unsignedchardata=0x55;ft5x06_write_reg(fd,0x38,0x80,&data,1);// 从 0x38 地址的寄存器 0x80 读取数据ID_G_THGROUP=ft5x06_read_reg(fd,0x38,0x80);printf("ID_G_THGROUP is 0x%02X\n",ID_G_THGROUP);return0;}

文件方式read/write 方式实现i2c 控制

#include<stdio.h>#include<sys/ioctl.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<string.h>#include<linux/i2c.h>#include<linux/i2c-dev.h>#include<unistd.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 设备文件fd=open("/dev/i2c-1",O_RDWR);if(fd<0){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 和 文件控制方式。
  • 多琢磨不同方案的不同、优劣,自己思考。