20_Linux I2C 驱动

Linux I2C 驱动

一.I2C 简介

I2C 是很常见的一种总线协议, I2C是NXP公司设计的,I2C 使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),因为I2C这两条数据线是开漏输出的,所以需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。 I2C 总线标准模式下速度可以达到 100Kb/S,快速模式下可以达到 400Kb/S。

如果大家之前玩过51单片机,肯定对模拟I2C时序这个操作并不陌生,但是在Linux上,还需要我们继续来模拟I2C的时序吗,答案是不需要的,cpu会自带I2C控制器,有了这个I2C控制器之后,我们就不用在来取模拟时序了,我们只需要关系怎么把数据写到寄存器和怎么从寄存器读数据即可,具体的时序都是由I2C控制器来帮我们自动完成。

Linux把I2C控制器抽象成了一个i2c_adapter,我们只要来分配这个i2c_adapter,就可以得到一个I2C控制器。

我们可以先来体验一下,在Linux上操作I2C是多么的容易,我们可以先来看一下我们的系统里面都有哪些I2C的节点,这里以终结者的开发板为例。如下图所示:

img

Linux有一个非常重要的概念叫一切皆文件,那么我们能不能在应用层通过open这些节点来操作I2C来跟外设I2C通信的芯片进行一个数据交流呢?当然是可以的,我们来一起看一下,这里我们以7寸RGB屏幕上的触摸芯片FT5X06为例,我们的所有的开发板都是支持迅为7寸RGB屏幕屏的,所有都是可以进行这个实验的。

我们通过原理图先来确定一下FT5X06使用的是哪个I2C,这里以终结者开发板的底板为例,后面整理笔记的时候会把其他的开发板的截图都补充上,这个大家不必担心,通过下面的截图我们可以得到在终结者开发板上,触摸芯片FT5X06使用的是I2C2,对应的节点是dev下面的i2c-1。如果我们要在终结者的上和触摸芯片FT5X06进行通信,是不是操作dev下的i2c-1这个节点就可以了呢?

img

那我们怎么在应用层操作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时,就是我们要发送的数据。

len单位字节

那么我们要怎么设计我们的程序呢?我们来看一下。

头文件

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

app.c

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

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

int fd;
/* I2C读数据
 * slave_addr: 从机地址
 * reg_addr: 寄存器地址
 */
int i2c_read_data(unsigned int slave_addr, unsigned char reg_addr)
{
    struct i2c_rdwr_ioctl_data i2c_read_lcd;
    unsigned char data;
    int ret;
    
    struct i2c_msg msg[2] = {
        [0]=  {
            .addr = slave_addr, //从机地址
            .flags = 0, //写操作
            .buf = &reg_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)
    {
        printf("ioctl is error\n");
        return -1;
    }
    printf("ioctl is ok\n");
    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)
    {
        TD_STATUS = i2c_read_data(0x38, 0x02);
        printf("TD_STATUS value is %d\n", TD_STATUS);
        sleep(1);
    }
    close(fd);
    return 0;
}

一.Linux I2C 驱动框架简介

Linux中的I2C也是按照平台总线模型设计的,既然也是按照平台总线模型设计的,是不是也分为一个device和一个driver呢?但是I2C这里的device不叫device,而是叫client。在讲 platform 的时候就说过, platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于 I2C 而言,不需要虚拟出一条总线,直接使用 I2C总线即可。

非设备树实现 i2c

在没有设备树之前我们是怎么实现的I2C的device部分,也就是client部分。然后在学习有了设备树之后,我们的client是怎么编写的。按照Linux的发展路径来学习。

在没有使用设备树之前,我们使用的是i2c_board_info这个结构体来描述一个I2C设备的,i2c_board_info这个结构体如下:

struct i2c_board_info {
    char type[I2C_NAME_SIZE]; /* I2C 设备名字 */
    unsigned short flags; /* 标志 */
    unsigned short addr; /* I2C 器件地址 */
    void *platform_data;
    struct dev_archdata *archdata;
    struct device_node *of_node;
    struct fwnode_handle *fwnode;
    int irq;
};

在这个结构体里面,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 函数,如下:

1. i2c_get_adapter函数

作用:获得一个I2C适配器

原型:

struct i2c_adapter *i2c_get_adapter(int nr);

​ 参数:要获得的哪个I2C适配器的编号

​ 返回值:失败返回NULL。

2. i2c_put_adapter函数

作用:释放I2C适配器

原型:

void i2c_put_adapter(struct i2c_adapter *adap);

参数:要释放的I2C适配器

返回值:失败返回NULL

3 . i2c_new_device函数

作用:把I2C适配器和I2C器件关联起来。

原型:

struct i2c_client * i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info);

参数:struct i2c_adapter *adap:I2C适配器

struct i2c_board_info const *info:i2c_board_info的指针

返回值:struct i2c_client *失败返回NULL;

4.i2c_unregister_device函数

作用:注销一个client。

原型:

void i2c_unregister_device(struct i2c_client *client);

参数:i2c client的指针。

返回值:失败返回NULL

示例

client.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>

struct i2c_adapter *i2c_ada; //I2C适配器
struct i2c_client *i2c_client; //I2C_client

struct i2c_board_info ft5x06_info[] = { // 支持的I2C设备列表
    {
        I2C_BOARD_INFO("ft5x06_test", 0x38) //设置设备名字和器件地址
    },
    {
        
    }
};

static int ft5x06_client_init(void)
{
    printk("This is ft5x06_client_init\n");
    i2c_ada = i2c_get_adapter(1); //获得一个I2C 2适配器,因为从0开始,所以为1
    i2c_client = i2c_new_device(i2c_ada, ft5x06_info); //把I2C适配器和I2C器件关联起来。
    i2c_put_adapter(i2c_ada); //释放I2C适配器  
    return 0;
}

static void ft5x06_client_exit(void)
{
    printk("This is ft5x06_client_exit\n");
    i2c_unregister_device(i2c_client); //注销一个client
}

module_init(ft5x06_client_init);
module_exit(ft5x06_client_exit);

MODULE_LICENSE("GPL");
运行结果

image-20240429162527503

设备树实现 i2c

在使用了设备树以后,就不用这么复杂了,使用设备树的时候只要在对应的I2C节点下创建相应设备的节点即可,比如我想添加一个触摸芯片FT5X06的设备,我就可以在对应的I2C的节点下这样写,如下图所示:

注意:迅为10.1寸屏幕的触摸芯片是 gt911,4.3寸触摸芯片是 tsc2007。其它都是ft5426

&i2c2 {
	clock_frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c2>;
	status = "okay";
	edt-ft5x06@38 {
		compatible = "edt,edt-ft5306", "edt,edt-ft5x06", "edt,edt-ft5406";
        pinctrl-names = "default";
        pinctrl-0 = <&ts_int_pin
                     &ts_reset_pin>;
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 0>;
        reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
        irq-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
}

然后我们再来看driver部分。不管是使用设备树还是非设备树,driver部分就比较复杂了。和注册一个杂项设备或者是字符设备的套路一样,我们也是要先定一个一个i2c_driver的结构体,然后在对他进行初始化,我们来看一下这个结构体的定义,如下图所示:

struct i2c_driver {
	unsigned int class;

	/* Notifies the driver that a new bus has appeared. You should avoid
	 * using this, it will be removed in a near future.
	 */
	int (*attach_adapter)(struct i2c_adapter *) __deprecated;
	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);

	/* driver model interfaces that don't relate to enumeration  */
	void (*shutdown)(struct i2c_client *);

	/* Alert callback, for example for the SMBus alert protocol.
	 * The format and meaning of the data value depends on the protocol.
	 * For the SMBus alert protocol, there is a single bit of data passed
	 * as the alert response's low bit ("event flag").
	 */
	void (*alert)(struct i2c_client *, unsigned int data);

	/* a ioctl like command that can be used to perform specific functions
	 * with the device.
	 */
	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

	struct device_driver driver;
	const struct i2c_device_id *id_table;

	/* Device detection callback for automatic device creation */
	int (*detect)(struct i2c_client *, struct i2c_board_info *);
	const unsigned short *address_list;
	struct list_head clients;
};

在驱动注册之前 i2c_driver 结构体需要被正确地初始化,有4个成员要求必须被初始化,其中id_table不管用不用设备树都要被初始化,否则不能匹配成功:

static struct i2c_driver xxx_driver = {
    .driver	= {
		.name	= "xxx",
	},
	.probe		= xxx_probe,
	.remove		= xxx_remove,
	.id_table	= xxx_table,
};

初始化完成以后就是把i2c_driver注册进内核,注册进内核我们使用是的是i2c_add_driver。

,我们来看一下这个函数:

1,i2c_add_driver函数

​ 作用:注册一个i2c驱动

​ 函数原型:

#define i2c_add_driver(driver)  i2c_register_driver(THIS_MODULE, driver)

​ 参数:struct i2c_driver的指针。

​ 返回值:失败返回负值

2 i2c_del_driver函数

作用:删除一个i2c驱动

函数原型:

extern void i2c_del_driver(struct i2c_driver *);

​ 参数:struct i2c_driver的指针。

​ 返回值:失败返回负值

示例

将设备树加上

driver.c

#include <linux/init.h>
#include <linux/module.h>

#include <linux/i2c.h>

struct of_device_id	ft5x06_id[] = {
    {.compatible = "edt,edt-ft5306", 0, },
    {.compatible = "edt,edt-ft5x06", 0, },
    {.compatible = "edt,edt-ft5406", 0, },
    {},
};

static struct i2c_device_id ft5x06_id_ts[] = {
    {"xxx", 0, },
    {}
};

int ft5x06_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id)
{
    printk("This is ft5x06_probe\n");
    return 0;
}

int ft5x06_remove(struct i2c_client *client)
{
    printk("This is ft5x06_remove\n");
    return 0;
}

static struct i2c_driver ft5x06_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = "ft5x06_test",
        .linof_match_table = ft5x06_id,
    },
    .probe = ft5x06_probe,
    .remove = ft5x06_remove,
    .id_table = ft5x06_id_ts,
};

static int ft5x06_driver_init(void)
{
    int ret;
    
    printk("This is ft5x06_driver_init\n");
    ret = i2c_add_driver(&ft5x06_driver);
    if(ret < 0)
    {
        printk("i2c_add_driver is error\n");
        return -1;
    }
    return 0;
}

static void ft5x06_driver_exit(void)
{
    printk("This is ft5x06_driver_exit\n");
    i2c_del_driver(&ft5x06_driver);
}

module_init(ft5x06_driver_init);

module_exit(ft5x06_driver_exit);

MODULE_LICENSE("GPL");

I2C 驱动程序实现 I2C 通信

i2c_transfer函数

I2C设备数据收发处理流程
对 I2C 设备寄存器进行读写操作,这里就要用到 i2c_transfer 函数了。 i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,对于 I.MX6U 而言就是i2c_imx_xfer 这个函数。 i2c_transfer 函数原型如下:

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

adap: 所使用的 I2C 适配器, i2c_client 会保存其对应的 i2c_adapter。
msgs: I2C 要发送的一个或多个消息。
num: 消息数量,也就是 msgs 的数量。
返回值: 负值,失败,其他非负值,发送的 msgs 数量。

示例

driver.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>

static struct i2c_client *ft5x06_client; //I2C_client

static int ft5x06_read_reg(u8 reg_addr) //I2C读操作
{
    u8 data;
    
    struct i2c_msg msgs[] = {
        [0] = {
            .addr = ft5x06_client->addr, //从机地址 0x38
            .flags = 0, //写操作
            .len = sizeof(reg_addr), //数据大小
            .buf = &reg_addr, //要发送的数据
        },
        [1] = {
            .addr = ft5x06_client->addr, //从机地址 0x38
            .flags = 1, //读操作
            .len = sizeof(data), //数据大小
            .buf = &data, //要接收的数据
        },
    };
    i2c_transfer(ft5x06_client->adapter, msgs, 2); //对I2C设备寄存器进行读写操作
    
    return data;
}

static void ft5x06_write_reg(u8 reg_addr, u8 data, u8 len) //I2C写操作
{
    u8 buff[256];
    struct i2c_msg msgs[] = {
        [0] = {
            .addr = ft5x06_client->addr, //从机地址 0x38
            .flags = 0, //写操作
            .len = len+1, //数据大小
            .buf = buff, //要发送的数据
        },
    };
    buff[0] = reg_addr;
    memcpy(&buff[1], &data, len);
    i2c_transfer(ft5x06_client->adapter, msgs, 1); //对I2C设备寄存器进行写操作
}

struct of_device_id	ft5x06_id[] = {
    {.compatible = "edt,edt-ft5306", 0, },
    {.compatible = "edt,edt-ft5x06", 0, },
    {.compatible = "edt,edt-ft5406", 0, },
    {},
};

static struct i2c_device_id ft5x06_id_ts[] = {
    {"xxx", 0, },
    {}
};

int ft5x06_probe(struct i2c_client *i2c_client, const struct i2c_device_id *id)
{
    int data;
    
    printk("This is ft5x06_probe\n");
    ft5x06_client = i2c_client; 
    ft5x06_write_reg(0x80, 0x4b, 1); //往0x80寄存器写入0x4b,数据长度1字节
    data = ft5x06_read_reg(0x80); //读取0x80寄存器的值
    printk("data is %#x\n", data);
    return 0;
}

int ft5x06_remove(struct i2c_client *client)
{
    printk("This is ft5x06_remove\n");
    return 0;
}

static struct i2c_driver ft5x06_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = "ft5x06_test", //设备名称
        .linof_match_table = ft5x06_id, //通过.compatible来匹配
    },
    .probe = ft5x06_probe,
    .remove = ft5x06_remove,
    .id_table = ft5x06_id_ts,
};

static int ft5x06_driver_init(void)
{
    int ret;
    
    printk("This is ft5x06_driver_init\n");
    ret = i2c_add_driver(&ft5x06_driver); //注册一个i2c驱动
    if(ret < 0)
    {
        printk("i2c_add_driver is error\n");
        return -1;
    }
    return 0;
}

static void ft5x06_driver_exit(void)
{
    printk("This is ft5x06_driver_exit\n");
    i2c_del_driver(&ft5x06_driver); //删除一个i2c驱动
}

module_init(ft5x06_driver_init);

module_exit(ft5x06_driver_exit);

MODULE_LICENSE("GPL");
posted @ 2024-04-30 15:30  爱吃冰激凌的黄某某  阅读(24)  评论(0编辑  收藏  举报