i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、
【公众号】迅为电子
【粉丝群】258811263
第六十五章 Linux I2C驱动实验
65.1 应用程序与I2C通信
本章内容对应视频讲解链接(在线观看):
应用层实现I2C通信 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=44
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\021-i2c驱动实验”路径下。
我们可以先来体验一下,在Linux上操作I2C是多么的容易,我们可以先来看一下系统里面都有哪些I2C的节点,这里以iMX8MM开发板为例。如下图所示:
Linux有一个非常重要的概念叫一切皆文件,那么我们能不能在应用层通过open这些节点来操作I2C来跟外设I2C通信的芯片进行一个数据交流呢?当然是可以的,我们来一起看一下,这里我们以7寸LVDS屏幕上的触摸芯片FT5X06为例,迅为所有开发板都是支持迅为7寸LVDS屏幕屏的,所有都是可以进行这个实验的。迅为的屏幕除了4.3寸和10.1寸屏外,其他尺寸的屏幕的触摸芯片都是FT5X06,都是可以进行这个实验的。
本次实验我们使用的从机为 FT5X06触摸芯片。 FT5x06 系列 ICs 是单芯片电容式触摸屏控制器 IC,带有一个内置的 8 位微控制器单元(MCU)。采用互电容的方法,在配合的相互的电容式触摸面板,它支持真正的多点触摸功能。FT5x06 具有用户友好的输入的功能,这可以应用在许多便携式设备,例如蜂窝式电话,移动互联网设备,上网本和笔记本个人电脑。FT5x06 系列 IC 包括 FT5206/FT5306/FT5406。FT5x06 可以捕获5个触摸点,编写驱动时,只要去获取这几个点的数据,然后上报就可以了。之后我们的实验也是读取的其中一个寄存器,如下图所示,我们可以在FT5X06的数据手册上查找到。
我们打开IMX8MM开发板的底板原理图,我们通过原理图先来确定一下FT5X06使用的是哪个I2C,通过下面的截图我们可以看到在IMX8MM开发板上触摸芯片FT5X06使用的是I2C2。
我们输入如下图所示命令,查找I2C2对应的设备节点,我们查找如下图所示:
所以I2C2设备的地址是0038,对应的节点是dev下面的i2c-1。如果我们要在IMX8MM上和触摸芯片FT5X06进行通信,只要操作dev下的i2c-1这个节点就可以了。
那我们怎么在应用层操作I2C呢?应用层操作I2C是以数据包进行交流的,所以我们在应用层就要进行封包的操作。数据包对应的结构体是 i2c_rdwr_ioctl_data,这个结构体定义在include\uapi\linux\i2c-dev.h下面:定义如下:
/* This is the structure as used in the I2C_RDWR ioctl call */ struct i2c_rdwr_ioctl_data { struct i2c_msg __user *msgs; /* pointers to i2c_msgs */ __u32 nmsgs; /* number of i2c_msgs */ };
第一个结构体成员是我们要发送的数据包的指针,第二个结构体成员是发送数据包的个数。
我们来看一下i2c_msg结构体的定义,这个结构体是定义在include\uapi\linux\i2c.h下面,定义如下:
struct i2c_msg { __u16 addr; /* slave address */ __u16 flags; #define I2C_M_TEN 0x0010 /* this is a ten bit chip address */ #define I2C_M_RD 0x0001 /* read data, from slave to master */ #define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */ #define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */ __u16 len; /* msg length */ __u8 *buf; /* pointer to msg data */ };
结构体成员addr是我们从机的地址,flags为读写标志位,如果flags为1,则为读,反之为0,则为写。len为buf的大小,单位是字节。当flags为1是,buf就是我们要接收的数据,当flags为0时,就是我们要发送的数据。
那么我们要怎么设计我们的程序呢?我们来看一下。程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\021-i2c驱动实验\001”路径下。
/* * @Author: topeet * @Description: 应用程序与I2c通信 */ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <linux/input.h> #include <linux/input.h> #include <linux/i2c.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> int fd; int ret; /** * @description: i2c_read_data i2c读数据 * @param {unsignedint} slave_addr:从机设备的地址 * @param {unsignedchar} reg_addr:寄存器的地址 * @return {*} */ int i2c_read_data(unsigned int slave_addr, unsigned char reg_addr) { unsigned char data; //定义一个要发送的数据包i2c_read_lcd struct i2c_rdwr_ioctl_data i2c_read_lcd; //定义初始化i2c_msg结构体 struct i2c_msg msg[2] = { [0] = { .addr = slave_addr, //设置从机额地址 .flags = 0, //设置为写 .buf = ®_addr, //设置寄存器的地址 .len = sizeof(reg_addr)}, //设置寄存器的地址的长度 [1] = {.addr = slave_addr, //设置从机额地址 .flags = 1, //设置为读 .buf = &data, //设置寄存器的地址 .len = sizeof(data)}, //设置寄存器的地址 }; //初始化数据包的数据 i2c_read_lcd.msgs = msg; //初始化数据包的个数 i2c_read_lcd.nmsgs = 2; //操作读写数据包 ret = ioctl(fd, I2C_RDWR, &i2c_read_lcd); if (ret < 0) { perror("ioctl error "); return ret; } return data; } int main(int argc, char *argv[]) { int TD_STATUS; //打开设备节点 fd = open("/dev/i2c-1", O_RDWR); if (fd < 0) { //打开设备节点失败 perror("open error \n"); return fd; } while (1) { //i2C读从机地址为0x38,寄存器地址为0x02的数据 //我们从数据手册中得知TD_STATUS的地址为0x02 TD_STATUS = i2c_read_data(0x38, 0x02); // 打印TD_STATUS的值 printf("TD_STATUS value is %d \n", TD_STATUS); sleep(1); } close(fd); return 0; }
编译应用程序程序如下图所示:
我们在开发板上运行应用程序,当我们没有触摸屏幕时,如下图所示:
当我们用一根手指触摸时,如下图所示:
当我们用三根手指触摸时,如下图所示:
当我们用五根手指触摸时,如下图所示:
65.2 I2C总线实现client设备
上一章节我们学习了怎么在应用层来操作i2c,本章节我们来学习一下如何写一个i2c驱动。
本章内容对应视频讲解链接(在线观看):
I2C总线实现client设备 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=45
Linux中的I2C也是按照平台总线模型设计的,既然也是按照平台总线模型设计的,是不是也分为一个device和一个driver呢?但是I2C这里的device不叫device,而是叫client。在讲 platform 的时候就说过, platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于 I2C 而言,不需要虚拟出一条总线,直接使用 I2C总线即可。同样,我们也是先从非设备树开始,先来看一下,在没有设备树之前我们是怎么实现的I2C的device部分,也就是client部分。然后再学习有了设备树之后,我们的client是怎么编写的。
65.2.1 非设备树实现i2c
在没有使用设备树之前,我们使用的是i2c_board_info这个结构体来描述一个I2C设备的,i2c_board_info这个结构体如下:
在这个结构体里面,type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,这个名字就是用来进行匹配用的,一个是 I2C 设备的器件地址,也可以使用宏:
#define I2C_BOARD_INFO(dev_type, dev_addr) \
.type = dev_type, .addr = (dev_addr)
可以看出, I2C_BOARD_INFO 宏其实就是设置 i2c_board_info 的 type 和 addr 这两个成员变量。
I2C 设备和驱动的匹配过程是由 I2C 核心来完成的,在Linux源码的drivers/i2c/i2c-core.c 就是 I2C 的核心部分, I2C 核心提供了一些与具体硬件无关的 API 函数,如下:
函数 | struct i2c_adapter *i2c_get_adapter(int nr); |
nr | 要获得的那个I2C适配器的编号 |
返回值 | 成功返回0;失败返回NULL |
功能 | 获取I2C适配器 |
函数 | void i2c_put_adapter(struct i2c_adapter *adap); |
adap | 要释放的I2C适配器 |
返回值 | 成功返回0;失败返回NULL |
功能 | 释放I2C适配器 |
函数 | i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info); |
adap | I2C适配器 |
info | i2c_board_info的指针 |
返回值 | 成功返回0;失败返回NULL |
功能 | 把I2C适配器和I2C器件关联起来。 |
函数 | void i2c_unregister_device(struct i2c_client *client) |
client | i2c client的指针 |
返回值 | 成功返回0;失败返回NULL |
功能 | 注销一个client。 |
65.2.2 设备树实现i2c
在使用了设备树以后,就不用这么复杂了,使用设备树的时候只要在对应的I2C节点下创建相应设备的节点即可,比如我想添加一个触摸芯片FT5X06的设备,我就可以在对应的I2C的节点下这样写,如下所示:
注意:迅为10.1寸屏幕的触摸芯片是 gt911,4.3寸触摸芯片是 tsc2007,其它都是ft5426芯片。
在IMX8MM设备树中,/home/topeet/linux/linux-imx/arch/arm64/boot/dts/freescale/itop8mm-evk-7.0.dts为7寸LVDS屏幕的设备树文件。
&i2c2 { clock-frequency = <400000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; status = "okay"; typec1_ptn5110: tcpci@50 { compatible = "usb,tcpci"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_typec1>; reg = <0x50>; interrupt-parent = <&gpio2>; interrupts = <11 8>; src-pdos = <0x380190c8>; snk-pdos = <0x380190c8>; /* Only can sink 5V for safe */ max-snk-mv = <5000>; max-snk-ma = <3000>; op-snk-mw = <10000>; max-snk-mw = <15000>; port-type = "drp"; default-role = "sink"; status = "okay"; }; #if defined(LCD_TYPE_7_0) || defined(LCD_TYPE_9_7) || defined(LCD_TYPE_MIPI_7_0) ft5x06_ts@38 { compatible = "edt,edt-ft5x06"; reg = <0x38>; pinctrl-names = "defaults"; pinctrl-0 = <&pinctrl_ft5x06_int>; interrupt-parent = <&gpio1>; interrupts = <15 2>; status = "okay"; #if defined(LCD_TYPE_7_0) lcd_type = <0>; #elif defined(LCD_TYPE_9_7) lcd_type = <1>; #elif defined(LCD_TYPE_MIPI_7_0) lcd_type = <2>; #endif };
- 更改status为“okay”,使能i2c-2总线;
- 触摸屏所使用的 FT5x06 芯片节点,挂载 I2C2 节点下;“@”后面的“38”就是edt-ft5x06 的 I2C 器件地址
- compatible用于和驱动程序的compatible 匹配;
- reg属性描述ft5x的器件地址为0x38;
- interrupt-parent 属性描述中断 IO 对应的 GPIO 组为 GPIO1;
- interrupts 属性描述中断 IO 对应的是 GPIO1_C4
因为我们的开发板默认是设备树的镜像, 我们进入到开发板的/sys/bus/i2c/devices/目录下,因为通过查找原理图发现我们屏幕使用的是i2c2,所以进入到1-0038,查看name为ft5x0x_ts
接下来我们以非设备树的方式写一个client.c,然后加载进去,然后看一下和我们使用设备树的效果是不是一样的呢?有些同学可能会说,现在都是用设备树了,为什么还要用以前的方法呢?因为我们以前的方法也是需要熟悉的,我们只有学会以前的方法,才能够更好的理解现在的这种设备树的方法,而且有些老的版本还是使用低版本的内核,比如说kernel3.0。
65.2.3 修改设备树
因为我们现在使用的是设备树的源码,所以要在设备树文件去掉触摸的设备节点,打开设备树源码/home/topeet/linux/linux-imx/arch/arm64/boot/dts/freescale/itop8mm-evk-7.0.dts,注释掉如下图所示的内容。如果大家使用的是其他尺寸的屏幕,需要修改屏幕对应的设备树文件。
注释掉框柱的内容,如下图所示:
、我们输入make menuconfig,将ft5x06触摸芯片的驱动取消掉,如下图所示:
取消选中后退出保存修改。编译完成后,我们将编译好的镜像烧写到开发板,启动开发板后,我们进入到/sys/bus/i2c/devices/目录下,如下图所示,没有i2c设备1-0038了。
65.2.4 编写client.c
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\021-i2c驱动实验\002”路径下。
接下来我们以没有设备树的方法写一下i2c设备,然后注册进去。我们在Ubuntu的/home/topeet/imx8mm/21/002目录下新建client.c,拷贝前面实验的Makefile和build.sh到此目录下,编写client.c代码如下所示:
#include <linux/init.h> #include <linux/module.h> #include <linux/i2c.h> //分配一个i2c适配器指针 struct i2c_adapter *i2c_ada; //分配一个i2c_client指针 struct i2c_client *i2c_client; //支持i2c的设备列表 struct i2c_board_info ft5x06_info[] = { //每一项都代表一个i2C设备,这句话的意思是说这个设备的名字是ft5x06_test,器件地址是0x38 {I2C_BOARD_INFO("ft5x06_test", 0x38)}, {}}; static int ft5x06_client_init(void) { //调用i2c_get_adapter获得一个i2c总线,因为ft5x06是挂载到了i2c2上, // 所以这个参数是1,所以这句代码的意思是把这个触摸芯片挂载到i2c2上 i2c_ada = i2c_get_adapter(1); //把i2c client和i2c器件关联起来 i2c_client = i2c_new_device(i2c_ada, ft5x06_info); i2c_put_adapter(i2c_ada); //释放i2c控制器 printk("This is ft5x06_client_init \n"); return 0; } static void ft5x06_client_exit(void) { i2c_unregister_device(i2c_client); printk("This is ft5x06_client_exit \n"); } module_init(ft5x06_client_init); module_exit(ft5x06_client_exit); MODULE_LICENSE("GPL");
我们将刚刚编写的驱动代码编译为驱动模块,如下图所示:
我们进入加载驱动模块,如下图所示:
我们查看到/sys/bus/i2c/devices/目录下生成了1-0038,如下图所示:
如下图所示,我们可以查找到ft5x06,说明我们成功地注册了i2c设备。
65.3 I2C总线实现driver驱动
上一章节我们写了client.c,并且我们已经成功地把它加载到内核里面。i2c用非设备树实现,我们需要用i2c_board_info这个结构体来描述我们的i2c设备,如果我们用设备树的方法来实现,我们直接在设备树的节点下面添加创建对应设备的节点就可以了。后面的实验我们都用设备树的,非设备树的方法了解一下就可以了,本章节我们来设计i2c驱动的driver部分。
本章内容对应视频讲解链接(在线观看):
I2C总线实现driver驱动 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=46
我们在Ubuntu的/home/topeet/imx8mm/21/003目录下新建一个driver.c文件,拷贝前面实验的Makefile,build.sh文件,driver.c文件的代码如下所示:
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\021-i2c驱动实验\003”路径下。
/* * @Author: topeet * @Description: i2c总线实现driver驱动 */ #include <linux/init.h> #include <linux/module.h> #include <linux/i2c.h> //与设备树的 compatible 匹配 static const struct of_device_id ft5x06_id[] = { {.compatible = "edt,edt-ft5306", 0}, {.compatible = "edt,edt-ft5x06", 0}, {.compatible = "edt,edt-ft5406", 0}, {}}; // 无设备树的时候匹配 ID 表 static const struct i2c_device_id ft5x06_id_ts[] = { {"xxxxx", 0}, {}}; /* i2c 驱动的 remove 函数 */ int ft5x06_remove(struct i2c_client *i2c_client) { return 0; } /* i2c 驱动的 probe 函数 */ int ft5x06_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id) { printk("This is ft5x06_probe\n"); return 0; } //定义一个i2c_driver的结构体 static struct i2c_driver ft5x06_driver = { .driver = { .owner = THIS_MODULE, .name = "ft5x06_test", // 采用设备树的时候驱动使用的匹配表 .of_match_table = ft5x06_id, }, .probe = ft5x06_probe, .remove = ft5x06_remove, .id_table = ft5x06_id_ts}; /* 驱动入口函数 */ static int ft5x06_driver_init(void) { int ret; // 注册 i2c_driver ret = i2c_add_driver(&ft5x06_driver); if (ret < 0) { printk(" i2c_add_driver is error \n"); return ret; } printk("This is ft5x06_driver_init\n"); return 0; } /* 驱动出口函数 */ static void ft5x06_driver_exit(void) { // 将前面注册的 i2c_driver 也从 Linux 内核中注销掉 i2c_del_driver(&ft5x06_driver); printk("This is ft5x06_driver_exit\n"); } module_init(ft5x06_driver_init); module_exit(ft5x06_driver_exit); MODULE_LICENSE("GPL");
我们刚刚编写的驱动代码编译为驱动模块,如下图所示:
在加载驱动之前,我们要恢复设备树文件中我们之前章节注释掉的节点,还是取消掉原来的驱动,重新编译设备树内核,然后再烧写镜像。如下图所示
开发板启动后,我们进入到/sys/bus/i2c/devices/目录下查看是否有生成I2C节点,如下图所示:
我们进入共享目录并且加载驱动模块,如下图所示:
65.4 I2C驱动程序实现I2C通信
在第65.1章节学习i2c的时候,我们是在应用层操作设备节点对i2c设备进行读写的,那么如果我们在驱动里面对i2c设备进行读写要怎么办呢?本章节我们将来学习。
我们复制第65.3章节的代码,在此基础上进行修改。我们在应用里面对i2c进行读写,最重要的是对我们数据包的封包的操作,封装了一个i2c_rdwr_ioctl_data数据包,才对i2c进行读写,同样在驱动里面,我们也可以使用这种方法。
本章内容对应视频讲解链接(在线观看):
驱动程序实现I2C通信 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=47
我们对某个可读可写的寄存器进行读写操作,我们打开触摸芯片ft5x06的数据手册,打开2.1章节,如下图所示:
我们以0x08寄存器为例进行读写,大家也可以换其他的可读可写的寄存器进行读写操作。
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\021-i2c驱动实验\004”路径下。
完整的代码如下所示:
#include <linux/init.h> #include <linux/module.h> #include <linux/i2c.h> static struct i2c_client *ft5x06_client; static void ft5x06_write_reg(u8 reg_addr, u8 data, u8 len); static int ft5x06_read_reg(u8 reg_addr); //读寄存器函数 static int ft5x06_read_reg(u8 reg_addr) { u8 data; struct i2c_msg msgs[] = { [0] = { .addr = ft5x06_client->addr, .flags = 0, .len = sizeof(reg_addr), .buf = ®_addr, }, [1] = { .addr = ft5x06_client->addr, .flags = 1, .len = sizeof(data), .buf = &data, }, }; i2c_transfer(ft5x06_client->adapter, msgs, 2); return data; } //写寄存器函数 static void ft5x06_write_reg(u8 reg_addr, u8 data, u8 len) { u8 buff[256]; struct i2c_msg msgs[] = { [0] = { .addr = ft5x06_client->addr, .flags = 0, .len = len + 1, .buf = buff, }, }; buff[0] = reg_addr; memcpy(&buff[1], &data, len); i2c_transfer(ft5x06_client->adapter, msgs, 1); } /* 设备树匹配列表 */ static const struct of_device_id ft5x06_id[] = { {.compatible = "edt,edt-ft5x06", 0}, {.compatible = "edt,edt-ft5206", 0}, {.compatible = "edt,edt-ft5406", 0}, {}}; /* 传统匹配方式 ID 列表 */ static const struct i2c_device_id ft5x06_id_ts[] = { {"", 0}, {}}; /* i2c 驱动的 remove 函数 */ int ft5x06_remove(struct i2c_client *i2c_client) { return 0; } /* i2c 驱动的 probe 函数 */ int ft5x06_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id) { int ret; printk("This is ft5x06_probe\n"); //因为我们要在别的函数里面使用client,所以我们要把他复制出来 ft5x06_client = i2c_client; //往地址为0x80的寄存器里面写入数据0x4b ft5x06_write_reg(0x80, 0x4b, 1); //读出0x80寄存器的值 ret = ft5x06_read_reg(0x80); //打印0x80寄存器的值 printk("ret is %#x\n", ret); return 0; } //定义一个i2c_driver的结构体 static struct i2c_driver ft5x06_driver = { .driver = { .owner = THIS_MODULE, .name = "ft5x06_test", // 采用设备树的时候驱动使用的匹配表 .of_match_table = ft5x06_id, }, .probe = ft5x06_probe, .remove = ft5x06_remove, .id_table = ft5x06_id_ts}; /* 驱动入口函数 */ static int ft5x06_driver_init(void) { int ret; //注册 i2c_driver ret = i2c_add_driver(&ft5x06_driver); if (ret < 0) { printk(" i2c_add_driver is error \n"); return ret; } return ret; printk("This is ft5x06_driver_init\n"); return 0; } /* 驱动出口函数 */ static void ft5x06_driver_exit(void) { i2c_del_driver(&ft5x06_driver); printk("This is ft5x06_driver_exit\n"); } module_init(ft5x06_driver_init); module_exit(ft5x06_driver_exit); MODULE_LICENSE("GPL");
我们将刚刚编写的驱动代码编译为驱动模块,如下图所示:
我们进入共享目录并且加载驱动模块,如下图所示:
如上图所示,我们可以看到读写函数是没问题的,可以对寄存器进行正常的读写操作。