i.MX283开发板有两个I2C接口,其中I2C0接了一个DS2460加密芯片,本文介绍Linux下如何编写I2C驱动程序读写DS2460。
Linux上I2C架构可以分为I2C核心、I2C总线驱动、I2C设备驱动三个部分:
I2C核心:主要为总线驱动和设备驱动提供各种API,比如设备探测、注册、注销,设备和驱动匹配等函数。它在I2C架构中处于中间的位置。
I2C总线驱动:I2C总线驱动维护了I2C适配器数据结构(i2c_adapter)和适配器的通信方法数据结构(i2c_algorithm)。所以I2C总线驱动可控制I2C适配器产生start、stop、ACK等。I2C总线驱动在整个架构中是处于最底层的位置,它直接和真实的物理设备相连,同时它也是受CPU控制。
I2C设备驱动:I2C设备驱动主要负责和用户层交互,此处的设备是一个抽象的概念,并非真实的物理设备,它是挂在I2C适配器上,通过I2C适配器与真实的物理设备通信。
下图是整个I2C驱动框架:
实际上,Linux经过这么多年的发展,已经形成了一套完善的I2C驱动框架,现在编写I2C驱动,我们只需要完成上面所说的I2C设备驱动部分就可以,其他的芯片厂商已经为我们做好了。
根据bus-dev-drv框架模型,我们的主要工作是实现设备文件和驱动文件,也就是上图中的i2c_client和i2c_driver,i2c_client作用是完成设备和适配器的绑定 ,以确定设备驱动需要和哪个适配器下面的真实物理设备通信,i2c_driver的作用就是实现用户层的open、write、read等调用。
下面将详细说明整个过程:
注意:i2c适配器就是cpu中的i2c接口,cpu有几个i2c接口,就代表有几个适配器,又称i2c主机。
由于i.mx283开发板有两个i2c接口,所以这里就有两个适配器。首先假设有4个E2PROM挂在两个适配器下面,现在用户想要调
用设备驱动2来读写E2PROM3,根据上面提到的设备驱动模型,设备驱动2分为i2c_client2和i2c_driver2,首先client要为自己取
一个名字,假设叫做“E2PROM3”,然后它需要把自己和适配器2绑定(因为E2PROM3是挂在适配器2下面的),最后向内核注
册自己,I2c总线就知道自己下面多了一个设备——“E2PROM3”,i2c_client2部分的工作就做完了。
接着,需要实现i2c_driver2部分的功能,首先,i2c_driver2也需要为自己取一个名字,也必须叫“E2PROM3”,然后它需要实现
open、close、write、read等这些文件接口,对于write和read,需要使用I2C核心层提供的I2C读写数据API接口。接着,需要
实现probe和remove函数接口,probe函数里面实现就是字符设备驱动注册的那一套流程,remove函数正好相反,最后,它也
需要向内核注册自己,也就是告诉I2C总线,有一个新的驱动需要添加——“E2PROM3”,i2c_driver2部分的工作就做完了。
i2c总线:当向内核注册i2c驱动时,会将i2c驱动添加到总线的链表中,遍历总线上所有设备,通i2c_client>name
, i2c_driver-
>i2c_device_id->name
进行字符串匹配,如果匹配,就调用驱动程序的probe函数。上面已经将client和driver的名字都设置为
"E2PROM3",所以它们是匹配的,然后,I2C总线会调用驱动的probe函数,并把client结构体通过形参传给driver,然后执行
probe函数,注册字符设备驱动,client结构体里主要保存了适配器的信息,这个非常重要,当用户APP进行read和write调用
时,首先会进入driver里面的write和read函数,刚刚提到driver的write和read,需要使用I2C核心层提供的I2C读写数据API接
口,这些接口是用来和适配器通信的,所以需要指定哪个适配器,适配器信息就在刚刚保存的client结构体里面,这样,用户层
和适配器就是通了,最后,适配器再和挂在它下面的真实物理设备通信,这个部分是不需要我们操心的,芯片厂家已经做好了这
部分的工作,至此,整个I2C通信流程就走完了。
Linux编写I2C驱动程序的一般流程为:
图一中提到4种注册方法,我们这里仅介绍第一种,利用i2c_new_probed_device或者i2c_new_device注册,这两个函数的区别是后者必须指定真实设备的从机地址,前者是指定一个地址范围,内核会一个个探测(发送起始信号,看是否有ACK)地址是否有效,若探测成功,则内核记录这个地址,再调用i2c_new_device注册设备。
这里会使用一个重要的结构体i2c_board_info:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short flags;
unsigned short addr;
void *platform_data;
struct dev_archdata *archdata;
#ifdef CONFIG_OF
struct device_node *of_node;
#endif
int irq;
};
它的作用是描述物理设备信息,主要是name和addr,但内核不会探测这个addr的真实性,这个结构体是你已知真实物理设备的从机地址的情况下,可以直接指定设备信息,然后调用i2c_new_device注册设备。
我们今天使用的是i2c_new_probed_device注册设备,所以还需要给定一个地址范围。
static const unsigned short addr_list[] =
{
0x30,0x35,0x40,0x50,I2C_CLIENT_END,
};
addr_list数组里面就定义了地址范围,这里的0x40是ds2460的真实地址,内核会从0x30一直探测到0x50,若某个地址探测成功,它会把这个地址保存到i2c_board_info.addr成员中,然后调用i2c_new_device注册设备。
注意:无论使用哪种方式,都需指定设备的名称,即i2c_board_info.type成员。使用i2c_new_probed_device注册的好处是当
设备地址不正确时,设备是无法注册成功的!
下面是ds2460_dev.c:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>
static struct i2c_board_info ds2460_info=
{
I2C_BOARD_INFO("ds2460",0x40),//设备名称+设备地址7bit
};
static const unsigned short addr_list[] =
{
0x30,0x35,0x40,0x50,I2C_CLIENT_END,
};
struct i2c_client *ds2460_client = NULL;//定义一个client
static int ds2460_dev_init(void)
{
struct i2c_adapter *adapter=NULL;
/*获取i2c适配器0 */
adapter = i2c_get_adapter(0);
/*创建一个client 并绑定适配器*/
ds2460_client = i2c_new_probed_device(adapter,&ds2460_info,addr_list);
//ds2460_client = i2c_new_device(adapter,&ds2460_info);
if(ds2460_client != NULL)
{
i2c_put_adapter(adapter);
printk("module init ok\n");
return 0;
}
else
{
printk("device not exist\n");
return -EPERM;
}
}
static void ds2460_dev_exit(void)
{
i2c_unregister_device(ds2460_client);
printk("module exit ok\n");
}
module_init(ds2460_dev_init);
module_exit(ds2460_dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");
驱动实际就是字符设备驱动那一套流程,open、read、write等文件接口以及设备号申请、自动创建设备节点等操作。
只不过设备号申请、自动创建设备节点等操作需要放到probe函数里面去,注销操作则需要放到remove函数里面去。
这里主要讲讲read和write调用的实现:
驱动是和适配器通信的,而I2C核心为我们提供了很多和适配器通信的接口函数,具体可见/linux-2.6.35.3/Documentation/i2c
里面i2c-protocol和smbus-protocol两则文档。
下面是标准I2C协议的通信函数:
int i2c_master_send(struct i2c_client *client,const char *buf ,int count)
int i2c_master_recv(struct i2c_client *client, char *buf ,int count)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
除此之外,还有SMBUS协议的通信函数,SMBUS是I2C协议的子集:
SMBus Receive Byte: i2c_smbus_read_byte()
SMBus Send Byte: i2c_smbus_write_byte()
SMBus Read Byte: i2c_smbus_read_byte_data()
SMBus Read Word: i2c_smbus_read_word_data()
实际上很多I2C器件用的协议都是SMBus协议,它们的时序和SMBus完全一样,所以这里我们选择SMBus的通信函数与DS2460通信。这里主要用到i2c_smbus_read_byte_data()和i2c_smbus_read_word_data()两个函数。
s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value)
/*写一个字节数据到指定的地址,地址通过command字节传送*/
/* S Addr Wr [A] Comm [A] Data [A] P */
/*===========================================================================*/
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command)
/*从指定的地址读取一个字节,地址通过command字节传送,返回值是读到的字节*/
/* S Addr Wr [A] Comm [A] S Addr Rd [A] [Data] NA P */
i2c_driver驱动还有个非常重要的结构体i2c_driver :
struct i2c_driver {
unsigned int class;
/* Notifies the driver that a new bus has appeared or is about to be
* removed. You should avoid using this if you can, it will probably
* be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *);
int (*detach_adapter)(struct i2c_adapter *);
/* 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 *);
int (*suspend)(struct i2c_client *, pm_message_t mesg);
int (*resume)(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;
};
其中,成员const struct i2c_device_id *id_table记录了驱动的名字和私有数据,驱动的名字必须和设备的名字一致,否则内核会匹配失败。
struct i2c_device_id {
char name[I2C_NAME_SIZE];
kernel_ulong_t driver_data /* Data private to the driver */
__attribute__((aligned(sizeof(kernel_ulong_t))));
};
i2c_driver 结构体填充如下:
/*配置驱动的名称和私有数据*/
static const struct i2c_device_id ds2460_id_table=
{
"ds2460",0//名称为“ds2460”需要和设备保持一致内核才会调用驱动的probe函数
//0 表示没有私有数据
};
/*创建i2c_driver结构体*/
static struct i2c_driver ds2460_driver =
{
.driver={
.name ="ds2460_driver",//这个名字无所谓
.owner=THIS_MODULE,
},
.probe = ds2460_probe,
.remove= __devexit_p(ds2460_remove),
.id_table = &ds2460_id_table,//这里的名字才是和设备进行匹配的
};
最后,在probe函数里注册常规字符设备驱动,在remove函数里注销字符设备驱动,i2c_driver工作就基本结束了。
ds2460_drv.c:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#define SEQUENT_RW 0 //读写模式 0:读写不连续 1:连续读写
#define DEVICE_NAME "ds2460_drv"//驱动名称
static struct i2c_client *ds2460_client =NULL;
static struct cdev *ds2460_cdev=NULL;
static struct class *ds2460_class = NULL;
static struct device *ds2460_device = NULL;
static dev_t device_id;
static int ds2460_open(struct inode *inode, struct file *fp)
{
return 0;
}
static int ds2460_release(struct inode * inode, struct file * file)
{
return 0;
}
static int ds2460_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
unsigned char address;//读取的地址
unsigned char *data = NULL;//需要读取的数据
unsigned char i;
int ret;
#if (SEQUENT_RW == 1)
else if(count == 1)
{
ret = copy_from_user(&address, buf, 1);
if(ret < 0)
{
printk("read param num error ret = %d ,buf = %d\n",ret,(int)buf[0]);
return -EINVAL;
}
ret = i2c_smbus_read_byte_data(ds2460_client,address);
if(ret < 0)
{
printk("i2c read error %d\n",ret);
return ret;
}
copy_to_user(buf, &ret, 1);
}
else
{
data = kmalloc(count, GFP_KERNEL);//申请count字节内存
ret = copy_from_user(data, buf, 1);//第1个字节是地址
if(ret < 0)
{
printk("write param num error \n");
kfree(data);
return -EINVAL;
}
address = data[0];
ret = i2c_smbus_read_i2c_block_data(ds2460_client,address,count,data);
if(ret < 0)
{
printk("i2c_smbus_read_i2c_block_data error %d\n",ret);
kfree(data);
return ret;
}
copy_to_user(buf, data, count);
kfree(data);
}
#else
/*申请内存*/
data = kmalloc(count,GFP_KERNEL);
ret = copy_from_user(data, buf, 1);
if(ret < 0)
{
printk("read param num error ret = %d ,buf = %d\n",ret,(int)buf[0]);
return -EINVAL;
}
/*用户buf第一个字节是需要读取的地址*/
address = data[0];
/*依次读取数据*/
for(i=0;i<count;i++,address++)
{
/*非连续读取 每次读取产生一次完整的I2
C通信*/
data[i] = i2c_smbus_read_byte_data(ds2460_client,address);
if(data[i] < 0)
{
printk("i2c read error %d\n",data[i]);
return data[i];
}
}
/*将数据拷贝到用户层buf*/
copy_to_user(buf, data, count);
/*释放内存*/
kfree(data);
#endif
return 0;
}
ssize_t ds2460_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
unsigned char address;//需要写入的地址
unsigned char *data = NULL;//需要写入的数据
unsigned char i;
int ret;
/*至少写入1个数据 即count至少等于2*/
if(count < 2)
{
printk("write param num error count = %d\n",count);
return -EINVAL;
}
#if (SEQUENT_RW == 1)
else if(count == 2)
{
data = kmalloc(2,GFP_KERNEL);
ret = copy_from_user(data, buf, 2);
if(ret < 0)
{
printk("write param num error \n");
kfree(data);
return -EINVAL;
}
address = data[0];
ret=i2c_smbus_write_byte_data(ds2460_client,address,data[1]);
if(ret < 0)
{
printk("i2c_smbus_write_byte_data error %d\n",ret);
kfree(data);
return ret;
}
kfree(data);
}
else
{
data = kmalloc(count,GFP_KERNEL);
ret = copy_from_user(data, buf, count);
if(ret < 0)
{
printk("write param num error %d\n",ret);
kfree(data);
return -EINVAL;
}
address = data[0];
ret=i2c_smbus_write_i2c_block_data(ds2460_client,address,count-1,&data[1]);
if(ret < 0)
{
printk("i2c_smbus_write_i2c_block_data error %d\n",ret);
kfree(data);
return ret;
}
kfree(data);
}
#else
/*申请内存*/
data = kmalloc(count,GFP_KERNEL);
/*拷贝count个字节到刚刚申请的内存中*/
ret = copy_from_user(data, buf, count);
if(ret < 0)
{
printk("write param num error %d\n",ret);
kfree(data);
return -EINVAL;
}
/*用户buf第一个字节是需要写入的地址*/
address = data[0];
/*依次写入数据*/
for(i=1;i<=count-1;i++,address++)
{
/*非连续读写 每写入1个字节产生一次完整的I2C通信*/
ret=i2c_smbus_write_byte_data(ds2460_client,address,data[i]);
if(ret < 0)
{
printk("i2c_smbus_write_byte_data error %d\n",ret);
kfree(data);
return ret;
}
/*E2PROM写入需要延时10ms*/
mdelay(10);
}
/*释放内存*/
kfree(data);
#endif
return 0;
}
static struct file_operations ds2460_fops =
{
.owner = THIS_MODULE,
.open = ds2460_open,
.release = ds2460_release,
.read = ds2460_read,
.write = ds2460_write,
};
static int __devinit ds2460_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret;
/*获取当前操作的设备 */
ds2460_client = client;
/*申请设备号*/
ret = alloc_chrdev_region(&device_id, 0, 1, DEVICE_NAME);
if(ret < 0)
{
printk(KERN_ERR "alloc dev_id error %d \n", ret);
return ret;
}
/*分配一个cdev结构体*/
ds2460_cdev = cdev_alloc();
if(ds2460_cdev != NULL)
{
/*初始化cdev结构体*/
cdev_init(ds2460_cdev, &ds2460_fops);
/*向内核添加该cdev结构体*/
ret = cdev_add(ds2460_cdev, device_id, 1);
if(ret != 0)
{
printk("cdev add error %d \n",ret);
goto error;
}
}
else
{
printk("cdev_alloc error \n");
return -1;
}
/*创建一个class*/
ds2460_class = class_create(THIS_MODULE, "ds2460_class");
if(ds2460_class != NULL)
{
ds2460_device = device_create(ds2460_class, NULL, device_id, NULL, DEVICE_NAME);
}
else
{
printk("class_create error\n");
return -1;
}
return 0;
error:
cdev_del(ds2460_cdev);
unregister_chrdev_region(device_id, 1);
return -1;
}
static int __devexit ds2460_remove(struct i2c_client *client)
{
/*删除cdev结构体*/
cdev_del(ds2460_cdev);
/*释放设备号*/
unregister_chrdev_region(device_id, 1);
/*删除设备*/
device_del(ds2460_device);
/*删除类*/
class_destroy(ds2460_class);
return 0;
}
/*配置驱动的名称和私有数据*/
static const struct i2c_device_id ds2460_id_table=
{
"ds2460",0//名称为“ds2460”需要和设备保持一致内核才会调用驱动的probe函数
//0 表示没有私有数据
};
/*创建i2c_driver结构体*/
static struct i2c_driver ds2460_driver =
{
.driver={
.name ="ds2460_driver",//这个名字无所谓
.owner=THIS_MODULE,
},
.probe = ds2460_probe,
.remove= __devexit_p(ds2460_remove),
.id_table = &ds2460_id_table,//这里的名字才是和设备进行匹配的
};
static int ds2460_drv_init(void)
{
/*向内核注册驱动 如果和设备匹配 会执行probe函数*/
i2c_add_driver(&ds2460_driver);
printk("module init ok \n");
return 0;
}
static void ds2460_drv_exit(void)
{
/*向内核注销驱动 如果和设备匹配 会执行remove函数*/
i2c_del_driver(&ds2460_driver);
printk("module exit ok \n");
}
module_init(ds2460_drv_init);
module_exit(ds2460_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");
最后,编写测试函数:
DS2460是一个SHA加密芯片,但是它内部包含有一个112字节的E2PROM区域,其E2PROM区域的首地址是0x80,我们首先向这个位置写入50个数据,再读取50个数据,对比看下是否正确,最后再读下芯片ID。
ds2460_test.c:
#include<stdio.h> /* using printf() */
#include<stdlib.h> /* using sleep() */
#include<fcntl.h> /* using file operation */
#include<sys/ioctl.h> /* using ioctl() */
#include <asm/ioctls.h>
#include <unistd.h> //sleep write read close
int main(int argc, const char * argv [ ])
{
int fd,i;
int value = 0;
unsigned char txbuf[51],rxbuf[50];
fd = open("/dev/ds2460_drv",O_RDWR);
if(fd < 0)
{
printf("open ds2460_drv error %d\n",fd);
return 0;
}
txbuf[0] = 0x80;//需要写入的地址
for(i = 1;i<=50;i++)
{
txbuf[i] = i;//填充写入的数据
}
printf("write:\n");
for(i=0;i<=50;i++)
{
printf("%d ",txbuf[i]);
}
printf("\n");//打印要写入的数据
write(fd,txbuf,51);//写入芯片0x80的位置
rxbuf[0] = 0x80;//读取的地址
read(fd,rxbuf,50);//从0x80读取50个字节
printf("read ds2460:\n");
for(i = 0; i < 50;i++)
{
printf("%d ",rxbuf[i]);//打印读到的数据
}
printf("\n");
rxbuf[0] = 0xF0;//读芯片ID
read(fd,rxbuf,8);
printf("ID:\n");
for(i = 0; i < 8;i++)
{
printf("%x ",rxbuf[i]);
}
printf("\n");
return 0;
}
编译ds2460_dev.c,ds2460_drv.c,ds2460_test,得到三个文件:ds2460_dev.ko /ds2460_drv.ko /ds2460_test.
在开发板上加载前面两个驱动模块,再执行最后一个测试程序:
可以看到,驱动加载成功,写入和读取的数据也是一致的,芯片ID(低字节在前)为:3C 53 7F 3e 0 0 0 39
我们编写的I2C驱动没有问题。
1.在linux系统下编写I2C驱动,目前主要有两种方法,一种是把I2C设备当作一个普通的字符设备来处理,另一种是利用linux下I2C驱动体系结构来完成。下面比较下这两种方法: 第一种方法: 优点:思路比较直接,不需要花很多时间去了解linux中复杂的I2C子系统的操作方法。 缺点: 要求工程师不仅要对I2C设备的操作熟悉,而且要熟悉I2C的适配器(I2C控制器)操作。 要求工程师对I2C的设备器及I2C的设备操作方法都比较熟悉,最重要的是写出的程序可以移植性差。 对内核的资源无法直接使用,因为内核提供的所有I2C设备器以及设备驱动都是基于I2C子系统的格式。
第一种方法的优点就是第二种方法的缺点, 第一种方法的缺点就是第二种方法的优点。
2.E2PROM支持连续取,对于DS2460,发送一次起始信号最多可连续读取8字节,但是本文没有采用这一方式(ds2460_drv.c文件 中有宏定义开关可以打开),本文采用的是最原始的方式,即每读写一个字节产生一次完整的I2C通信,这种方式会大大影响速度。
3.linux内核源码/linux-2.6.35.3/drivers/i2c中有I2C相关代码
busses/i2c-mxs.c:总线驱动文件即I2C适配器的驱动文件,包含I2C基本通信函数。
i2c-core.c:I2C核心文件,主要提供API,与硬件无关。
i2c-dev.c:通用设备驱动文件,它不针对某一款I2C芯片,它提供通用的方式让用户操作I2C设备,同时也是一个字符设备驱动。 用户直接open这个设备驱动就是上面讲的把I2C设备当作一个普通的字符设备来处理,I2C所有通信细节都需要用户自己完成。
下面推荐几篇写的比较好的Linux I2C驱动框架文章: