Linux驱动-i2c读写EEPROM

前言

写文章的目的是想通过记录自己的学习过程,以便以后使用到相关的知识点可以回顾和参考。

一、简介

对于 I2C驱动,分为两个部分:i2c总线驱动和i2c设备驱动,总线驱动一旦编写完成就不需要再做修改,其他的 I2C 设备直接调用主机驱动提供的 API 函数完成读写操作即可。这个正好符合 Linux 的驱动分离与分层的思想。下面简单介绍这两部分驱动的区别:
1、I2C 总线驱动,I2C 总线驱动就是 SOC 的 I2C 控制器驱动,也叫做 I2C 适配器驱动。
2、I2C 设备驱动,I2C 设备驱动就是针对具体的 I2C 设备而编写的驱动。

本次实验主要编写i2c设备驱动,因为i2c总线驱动已经由soc厂家编写好了,不过我们还是要学习一下总线驱动的原理,所以讲解总线驱动的流程会在下一遍博客中分析,本次先完成稍微简单的i2c设备驱动实验。

二、i2c设备驱动有关的结构体和API

I2C 设备驱动重点关注两个数据结构:i2c_clienti2c_driver,i2c_client 就是描述设备信息的,i2c_driver 描述驱动内容,类似于 platform_driver。

1、i2c_client 结构体

i2c_client 结构体定义在 include/linux/i2c.h 文件中,内容如下:

struct i2c_client {
	unsigned short flags;		/* 标志		*/
	unsigned short addr;		/* 芯片地址,7 位,存在低 7 位	*/
					/* addresses are stored in the	*/
					/* _LOWER_ 7 bits		*/
	char name[I2C_NAME_SIZE];	/* 名字 */
	struct i2c_adapter *adapter;	/*  对应的 I2C 适配器	*/
	struct i2c_driver *driver;	/* i2c_driver 驱动结构体	*/
	struct device dev;		/* 设备结构体		*/
	int irq;			/* 中断	*/
	struct list_head detected;
};

一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会给这个 I2C 设备分配一个i2c_client。

2、i2c_driver 结构体

i2c_driver 类似 platform_driver,是我们编写 I2C 设备驱动重点要处理的内容,i2c_driver 结构体定义在 include/linux/i2c.h 文件中,内容如下:

struct i2c_driver {
	unsigned int class;

	int (*attach_adapter)(struct i2c_adapter *) __deprecated;
	int (*detach_adapter)(struct i2c_adapter *) __deprecated;

	/* probe 和 remove函数,跟platform平台设备驱动一样,
	 * 设备和驱动匹配成功就会执行probe函数。
	 */
	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 *);
	int (*suspend)(struct i2c_client *, pm_message_t mesg);
	int (*resume)(struct i2c_client *);

	
	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);
	
	/* device_driver 驱动结构体,如果使用设备树的话,
	 * 需要设置 device_driver 的of_match_table 成员变量,
	 * 也就是驱动的兼容(compatible)属性。
	 */
	struct device_driver driver;
	/* id匹配表,用来跟设备进行配对的 */
	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 设备驱动编写人来说,重点工作就是构建 i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。i2c_driver 注册函数为i2c_register_driver。

3、i2c_register_driver 和 i2c_add_driver 函数

i2c_driver 注册函数为 int i2c_register_driver,此函数原型如下:
int i2c_register_driver(struct module owner, struct i2c_driver driver)
函数参数和返回值含义如下:
owner:一般为 THIS_MODULE。
driver:要注册的 i2c_driver。
返回值:0,成功;负值,失败。

另外 i2c_add_driver 也常常用于注册 i2c_driver,i2c_add_driver 是一个宏,定义如下:

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

i2c_add_driver 就是对 i2c_register_driver 做了一个简单的封装,只有一个参数,就是要注册的 i2c_driver。

4、 i2c_del_driver 函数

注销 I2C 设备驱动的时候需要将前面注册的 i2c_driver 从 Linux 内核中注销掉,需要用到i2c_del_driver 函数,此函数原型如下:
void i2c_del_driver(struct i2c_driver driver)
函数参数和返回值含义如下:
driver:要注销的 i2c_driver。
返回值:无。

5、i2c_msg 结构体

要使用i2c进行读取和写入数据之前,必须先定义一个 i2c_msg 类型的变量,并完善 i2c_msg 变量里面的成员,其实可以把 i2c_msg 变量理解为一个数据包,它可以存将要写入的数据,或者存读取到的数据,之后把它作为参数使用 i2c_transfer 函数进行写入和读取。i2c_msg 结构体定义在include/linux/i2c.h 文件中,内容如下:

struct i2c_msg {
	__u16 addr;	/* slave address	i2c设备地址		*/
	__u16 flags;		/* 标记,最常用:0为写,I2C_M_RD 为读 */
#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_NOSTART		0x4000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#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		数据缓存区	*/
};

6、i2c_transfer 函数

把 i2c_msg 类型变量完善好后,使用 i2c_transfer 函数进行数据写入和读取,i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_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 数量。
·

三、i2c设备信息描述

编写i2c设备驱动前,先完善好i2c设备的信息,比如设备的名字等等,驱动程序就是通过name字段(名字)来进行驱动与设备的配对。在arch/arm/plat-s5p6818/device.c 中使用 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;
	int		irq;
};

type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,一个是 I2C 设备的器件地址。
完善好 i2c_board_info 结构体后使用 i2c_register_board_info 函数把设备注册到i2c设备链表中。
i2c_register_board_info 函数定义在drivers/i2c/i2c-boardinfo.c文件中,内容如下:

int __init
i2c_register_board_info(int busnum,
	struct i2c_board_info const *info, unsigned len)
{
	int status;

	down_write(&__i2c_board_lock);

	/* dynamic bus numbers will be assigned after the last static one */
	if (busnum >= __i2c_first_dynamic_bus_num)
		__i2c_first_dynamic_bus_num = busnum + 1;

	for (status = 0; len; len--, info++) {
		struct i2c_devinfo	*devinfo;	/* i2c设备信息结构体,一个设备对应一个 i2c_devinfo	*/

		devinfo = kzalloc(sizeof(*devinfo), GFP_KERNEL);
		if (!devinfo) {
			pr_debug("i2c-core: can't register boardinfo!\n");
			status = -ENOMEM;
			break;
		}

		devinfo->busnum = busnum;
		devinfo->board_info = *info;
		/* 把i2c设备(即i2c_devinfo)加入到 __i2c_board_list 链表中 */
		list_add_tail(&devinfo->list, &__i2c_board_list);	
	}

	up_write(&__i2c_board_lock);

	return status;
}

所以,这里可以猜测一下设备和驱动的匹配方式,可能就是通过遍历__i2c_board_list 链表,找到一个i2c_board_infotype成员跟驱动的i2c_device_id的name成员内容一样的,表示配对成功,这个配对过程就是内核中的i2c-core.c核心代码完成的。

四、i2c设备驱动框架

/* i2c 驱动的 probe 函数 */
static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	/* 函数具体程序 */
	return 0;
}

/* i2c 驱动的 remove 函数 */
static int ap3216c_remove(struct i2c_client *client)
{
	/* 函数具体程序 */
	return 0;
}

/* 传统匹配方式 ID 列表 */
static const struct i2c_device_id xxx_id[] = {
	{"xxx", 0}, 
	{}
};

/* i2c 驱动结构体 */
static struct i2c_driver xxx_driver = {
	.probe = xxx_probe,
	.remove = xxx_remove,
	.driver = {
		.owner = THIS_MODULE,
		.name = "xxx",
	},
	.id_table = xxx_id,
};
 
 /* 驱动入口函数 */
 static int __init xxx_init(void)
 {
	 int ret = 0;
 	 ret = i2c_add_driver(&xxx_driver);
 	 return ret;
 }
 
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	i2c_del_driver(&xxx_driver);
}
module_init(xxx_init);
module_exit(xxx_exit);

五、AT24LC04型号的eeprom设备驱动编写

1、添加设备信息描述

打开arch/arm/plat-s5p6818/device.c文件,找到合适位置添加以下内容;

#define EEPROM_I2C_BUS		(2)		/* 使用i2c2总线 */

static struct i2c_board_info __initdata s5p6818_eeprom_i2c_bdi = {
	 I2C_BOARD_INFO("s5p6818_eeprom", 0x50),	/* at24c04设备地址0x50 */
	.platform_data = &s5p6818_eeprom,		
};

i2c_register_board_info(EEPROM_I2C_BUS, &s5p6818_eeprom_i2c_bdi, 1);

2、驱动程序

#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include  
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define LEDDEV_NAME    "s5p6818_eeprom"    /* 设备名字 */


/* 定义一个设备信息的结构体 */
struct eeprom_dev{

    struct i2c_client *client;      /* 描述设备信息的结构体 */
    void *private_data;             /* 私有数据 */
};

struct eeprom_dev eepromdev;


/* 从 at24 读取多个寄存器数据 */
static int eeprom_read_regs(struct eeprom_dev *dev, u8 reg, void *dbuf, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = dev->client;

    msg[0].addr = client->addr;         /* at24epprom 地址 */
    msg[0].flags = 0;                   /* 标记为发送数据 */
    msg[0].buf = ®                  /* 读取的首地址 */
    msg[0].len = 1;                     /* reg 长度 */

    msg[1].addr = client->addr; 
    msg[1].flags = I2C_M_RD;            /* 标记为读取数据 */  
    msg[1].buf =  dbuf;                 /* 读取数据缓冲区 */
    msg[1].len = len;                   /* 要读取的数据长度 */

    ret = i2c_transfer(client->adapter, msg, 2);
    if(ret == 2){
        ret = 0;
    }else{
        printk(KERN_EMERG "i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
        ret = -EREMOTEIO;
    }

    return ret;
}

/* 向 at24 多个寄存器写入数据 */
static int eeprom_write_regs(struct eeprom_dev *dev, u8 reg, void *dbuf, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = dev->client;

    msg[0].addr = client->addr;         /* at24epprom 地址 */
    msg[0].flags = 0;                   /* 标记为发送数据 */
    msg[0].buf = ®                  /* 读取的首地址 */
    msg[0].len = 1;                     /* reg 长度 */

    msg[1].addr = client->addr; 
    msg[1].flags = 0 | I2C_M_NOSTART;   /* 标记为发送数据,不带start信号 */  
    msg[1].buf =  dbuf;                 /* 读取数据缓冲区 */
    msg[1].len = len;                 /* 要读取的数据长度 */

    ret = i2c_transfer(client->adapter, msg, 2);

    return ret;
}

#if 0
 /* 向 at24 指定寄存器写入指定的值,写一个寄存器 
  * return : 无
  */
static void epprom_write_reg(struct eeprom_dev *dev, u8 reg, u8 data)
{
    u8 buf = 0;
    buf = data;
    eeprom_write_regs(dev, reg, &buf, 1);
}

 /* 读取 at24 指定寄存器值, 读一个寄存器 
  * return : 读取到的寄存器值 
  */
static unsigned char epprom_read_reg(struct eeprom_dev *dev, u8 reg)
{
    u8 data = 0;
    eeprom_read_regs(dev, reg, &data, 1);
    return data;
}
#endif


/* 接口函数open,read,write,release */
static int eeprom_open(struct inode *inode, struct file *filp)
{		
    /* 把设备结构体设置为私有数据 */
    filp->private_data = &eepromdev;

    printk(KERN_EMERG "s5p6818_epprom open dev ok!\r\n");
    return 0;
}


static ssize_t eeprom_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;
    char wbuf[256] = {0};   /* 一次最多写 256-1 个字节数据 */
    u8 reg = 0;
    struct eeprom_dev *dev = (struct eeprom_dev *)filp->private_data;

    if(cnt > sizeof(wbuf)-1)
        return -EINVAL;
    
    ret = copy_from_user(wbuf, buf, cnt+1);     /* 从应用层拷贝过来的数据放进wbuf, wbuf[0]为寄存器地址 */
    if(ret < 0)
        return ret;

    reg = wbuf[0];

    ret = eeprom_write_regs(dev, reg, &wbuf[1], cnt);   /* 调用上面编写的写寄存器的函数 */
    if(ret < 0)
        return ret;

    return cnt;
}

static ssize_t epprom_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    int ret;
    u8 reg = 0;
    char rbuf[256] = {0};
    struct eeprom_dev *dev = (struct eeprom_dev *)filp->private_data;

    ret = copy_from_user(rbuf, buf, 1);   /* 从应用层拷贝过来的寄存器地址放进rbuf[0] */
    if(ret < 0)
        return ret;

    reg = rbuf[0];
    ret = eeprom_read_regs(dev, reg, &rbuf[1], cnt);   /* 调用上面编写的读寄存器值的函数 */
    if(ret < 0)
        return ret;

    ret = copy_to_user(buf, rbuf, cnt+1);
    if(ret < 0)
        return ret;

    return cnt;
}

static int eeprom_release(struct inode *inode, struct file *filp)
{
    printk(KERN_EMERG "s5p6818_epprom release ok!\r\n");
    return 0;
}

/* 字符设备提供给应用层的接口函数 */
static struct file_operations eeprom_fops = {
    .owner = THIS_MODULE,
    .open = eeprom_open,
    .write = eeprom_write,
    .read = epprom_read,
    .release = eeprom_release,
};

/* misc设备结构体 */
static struct miscdevice eeprom_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,    /* 动态分配子设备号 */
    .name = LEDDEV_NAME,    //设备节点的名字 /dev/xxx
    .fops = &eeprom_fops,
};


/* probe函数 */
static int s5p6818_eeprom_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret;

    /* 保存 client 指针*/
    eepromdev.client = client;

    /* 检查是否支持i2c功能 */
    if(!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)){

        printk(KERN_EMERG "i2c_check_functionality fail!\r\n");
    }

    /* 注册misc设备驱动 */
    ret = misc_register(&eeprom_miscdev);
    if(ret < 0){
        printk(KERN_EMERG "register misc device failed!\r\n");
        return ret;
    }
    
    return 0;
}

/* remove函数 */
static int s5p6818_eeprom_remove(struct i2c_client *client)
{

    /* 注销misc设备驱动 */
    misc_deregister(&eeprom_miscdev);

    return 0;
}

/* 传统匹配方式 ID 列表 */
static const struct i2c_device_id s5p6818_eeprom_id[]={
    {"s5p6818_eeprom", 0},  //设备类型(名字),id号
    {},
};

/* 添加到内核i2c设备列表 */
MODULE_DEVICE_TABLE(i2c, s5p6818_eeprom_id);

/* i2c 驱动结构体 */
static struct i2c_driver s5p6818_eeprom_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = "s5p6818_eeprom",
    },
    .probe = s5p6818_eeprom_probe,
    .remove = s5p6818_eeprom_remove,
    .id_table = s5p6818_eeprom_id,
};

/* 驱动入口函数 */
static int __init s5p6818_eeprom_init(void)
{
    return i2c_add_driver(&s5p6818_eeprom_driver);
}


/* 驱动出口函数 */
static void __exit s5p6818_eeprom_exit(void)
{
    i2c_del_driver(&s5p6818_eeprom_driver);
}

module_init(s5p6818_eeprom_init);
module_exit(s5p6818_eeprom_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzj");

2、应用程序

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

int main(int argc , char *argv[])
{
    int fd, ret, len;
    char *filename;
    char dbuf[2];

    if(argc != 2){
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if(fd < 0){
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

   
    while(1){
        printf("read or write at24_eeprom: 1.write  2.read :");
        scanf("%d", &ret);
        switch(ret){
            case 1:
                printf("addr without 0x:");    scanf("%x",&dbuf[0]);
                printf("data without 0x:");    scanf("%x",&dbuf[1]);
                len = write(fd, dbuf, 1);   /* dbuf[0]为写入地址,写入一个字节, 写入内容放入dbuf[1]*/
                if(len > 0){
                    printf("write ok!\r\n");
                }else{
                    printf("write fail!\r\n");
                }
                memset(dbuf, 0, sizeof(dbuf));
                printf("\r\n");
                break;

            case 2:
                printf("addr without 0x:");    scanf("%x",&dbuf[0]);
                len = read(fd, dbuf, 1);    /* dbuf[0]为读取地址,读取一个字节,读取内容放入dbuf[1] */
                if(len > 0){
                    printf("read data at 0x%x , data: 0x%x\r\n", dbuf[0], dbuf[1]);
                }else{
                    printf("read fail!\r\n");
                }
                memset(dbuf, 0, sizeof(dbuf));
                printf("\r\n");
                break;

            default:
                break;
        }
    }

    /* 关闭设备 */
    ret = close(fd);
    if(ret < 0){
        printf("Can't close file %s\r\n", filename);
        return -1;
    }

    return 0;
}

3、测试

Linux驱动-i2c读写EEPROM_第1张图片
往0x55地址的寄存器写入0xff,然后读取0x55地址的寄存器值,成功读取到0xff。

你可能感兴趣的:(ARM,linux)