I2C驱动-----FM24C02A
前言
I2C核心:主要为总线驱动和设备驱动提供各种API,比如设备探测、注册、注销,设备和驱动匹配等函数。它在I2C架构中处于中间的位置。
I2C总线驱动:I2C总线驱动维护了I2C适配器数据结构(i2c_adapter)和适配器的通信方法数据结构(i2c_algorithm)。所以I2C总线驱动可控制I2C适配器产生start、stop、ACK等。I2C总线驱动在整个架构中是处于最底层的位置,它直接和真实的物理设备相连,同时它也是受CPU控制。
I2C设备驱动:I2C设备驱动主要负责和用户层交互,此处的设备是一个抽象的概念,并非真实的物理设备,它是挂在I2C适配器上,通过I2C适配器与真实的物理设备通信。
实际上,Linux经过这么多年的发展,已经形成了一套完善的I2C驱动框架,现在编写I2C驱动,我们只需要完成上面所说的I2C设备驱动部分就可以,其他的芯片厂商已经为我们做好了。
根据bus-dev-drv框架模型,我们的主要工作是实现设备文件和驱动文件,也就是上图中的i2c_client和i2c_driver,i2c_client作用是完成设备和适配器的绑定 ,以确定设备驱动需要和哪个适配器下面的真实物理设备通信,i2c_driver的作用就是实现用户层的open、write、read等调用。
Linux编写I2C驱动程序的一般流程为:
1、创建i2c_client,并向内核注册这个client,注册的方法有图一中提到的4种方法。
2、创建i2c_driver,并向内核注册driver,一般使用i2c_add_driver注册driver。
3、注册driver时,总线会自动调用match函数,匹配client和driver,如果匹配成功,会调用driver里面的probe函数。
4、probe函数里面完成字符设备驱动那些工作,一个I2C驱动程序基本完成。
一、设备注册 i2c_register_board_info
这里会使用一个重要的结构体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;
};
addr:丛机设备地址右移一位
例如:
static struct i2c_board_info __initdata mxs_i2c_device[] = {
{ I2C_BOARD_INFO("pcf8563", 0x51) },
{ I2C_BOARD_INFO("FM24C02A", 0x50) },
{ I2C_BOARD_INFO("DS2460", 0x40) },
};
它的作用是描述物理设备信息,主要是name和addr,但内核不会探测这个addr的真实性,这个结构体是你已知真实物理设备的从机地址的情况下,可以直接指定设备信息,然后调用i2c_register_board_info注册设备
int __init i2c_register_board_info(int busnum,
struct i2c_board_info const *info,
unsigned len)
busnum :使用的是那个i2c总线
例如:
i2c_register_board_info(1, mxs_i2c_device, ARRAY_SIZE(mxs_i2c_device));
device.c
static struct i2c_board_info __initdata mxs_i2c_device[] = {
//{ I2C_BOARD_INFO("sgtl5000-i2c", 0xa), .flags = I2C_M_TEN }
//{ I2C_BOARD_INFO("rtc-pcf8563", 0x51) }
{ I2C_BOARD_INFO("pcf8563", 0x51) },
{ I2C_BOARD_INFO("FM24C02A", 0x50) },
{ I2C_BOARD_INFO("DS2460", 0x40) },
};
static void __init i2c_device_init(void)
{
//i2c_register_board_info(0, mxs_i2c_device, ARRAY_SIZE(mxs_i2c_device));
i2c_register_board_info(1, mxs_i2c_device, ARRAY_SIZE(mxs_i2c_device));
}
二、驱动注册
驱动实际就是字符设备驱动那一套流程,open、read、write等文件接口以及设备号申请、自动创建设备节点等操作。
只不过设备号申请、自动创建设备节点等操作需要放到probe函数里面去,注销操作则需要放到remove函数里面去。
read和write调用的实现:
下面是标准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)
1、添加驱动 i2c_add_driver
在模块初始化函数中调用 i2c_add_driver
static inline int i2c_add_driver(struct i2c_driver *driver)
{
return i2c_register_driver(THIS_MODULE, 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 FM24C02A_id[] =
{
{ "FM24C02A", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, FM24C02A_id); /* 进一步初始化 FM24C02A_id */
static struct i2c_driver FM24C02A_driver =
{
.driver = {
.name = "FM24C02A",
},
.probe = FM24C02A_probe,
.remove = FM24C02A_remove,
.id_table = FM24C02A_id,//这里的名字才是和设备进行匹配的
};
最后,在probe函数里注册常规字符设备驱动,在remove函数里注销字符设备驱动,i2c_driver工作就基本结束了。
2、删除驱动 i2c_del_driver
在模块卸载函数里调用 i2c_del_driver
void i2c_del_driver(struct i2c_driver *driver)
{
mutex_lock(&core_lock);
bus_for_each_dev(&i2c_bus_type, NULL, driver, __process_removed_driver);
mutex_unlock(&core_lock);
driver_unregister(&driver->driver);
pr_debug("i2c-core: driver [%s] unregistered\n", driver->driver.name);
}
drivice.c
#include<linux/miscdevice.h>
#include<linux/init.h>
#include<linux/module.h>
#include <linux/i2c.h>
#include <linux/bcd.h>
#include <linux/rtc.h>
#include <linux/slab.h>
#define DEVICE_NAME "FM24C02A"
#define CMD_SET_ROM_ADDR 0x1
static char rom_addr; /* 用于保存 FM24C02A 内部储存器的读/写地址 */
static struct i2c_client *g_client;
static const struct i2c_device_id FM24C02A_id[] =
{
{ "FM24C02A", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, FM24C02A_id); /* 进一步初始化 FM24C02A_id */
static ssize_t FM24C02A_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int ret = 0,i;
char data_buf[256];
ret = i2c_master_send(g_client, &rom_addr, 1); /* 向 FM24C02A 发送要读取数据的起始地址*/
printk("FM24C02A_read 1=%d\r\n",ret);
ret = i2c_master_recv(g_client, data_buf, count); /* 在 FM24C02A 连续读取数据 */
printk("FM24C02A_read 2=%d\r\n",ret);
for(i=0;i<ret;i++)
{
printk("%d ", data_buf[i]);
}
printk("read count=%d\r\n",count);
copy_to_user(buf, data_buf, count); /* 把读取的数据返回给用户空间 */
return ret;
}
static ssize_t FM24C02A_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
int ret = 0,i;
char data_buf[256 + 1];
data_buf[0] = rom_addr; /* 写入的起始地址 */
copy_from_user(data_buf + 1, buf, count); /* 在用户空间复制要写入的数据 */
for(i=0;i<(count+1);i++)
{
printk("%d ", data_buf[i]);
}
printk("write count=%d\r\n",count);
ret = i2c_master_send(g_client, data_buf, count+1); /* 把要写入的数据发送到 FM24C02A*/
printk("FM24C02A_write=%d\r\n",ret);
return ret;
}
static int FM24C02A_ioctl(struct inode *inode,struct file *flip,unsigned int command,unsigned long arg)
{
printk("command=%d, arg=%ld\r\n", command, arg);
if (command == CMD_SET_ROM_ADDR)
{
rom_addr = arg; /* FM24C02A内部储存器的读/写地址在arg参数中传入*/
}
return 0;
}
static struct file_operations FM24C02A_fops={
.owner = THIS_MODULE,
.write = FM24C02A_write,
.read = FM24C02A_read,
.ioctl = FM24C02A_ioctl,
};
static struct miscdevice FM24C02A_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &FM24C02A_fops,
};
static int FM24C02A_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int err = 0;
printk("FM24C02A device is detected \n");
g_client = client;
err = misc_register(&FM24C02A_miscdev); /* 生成字符设备 */
if (err)
{
printk("register FM24C02A device faile \n");
return -1;
}
return 0;
}
static int FM24C02A_remove(struct i2c_client *client)
{
misc_deregister(&FM24C02A_miscdev);
return 0;
}
static struct i2c_driver FM24C02A_driver =
{
.driver = {
.name = "FM24C02A",
},
.probe = FM24C02A_probe,
.remove = FM24C02A_remove,
.id_table = FM24C02A_id,
};
static int __init FM24C02A_init(void)
{
printk("FM24C02A_init\r\n");
return i2c_add_driver(&FM24C02A_driver);
}
static void __exit FM24C02A_exit(void)
{
i2c_del_driver(&FM24C02A_driver);
}
module_init(FM24C02A_init);
module_exit(FM24C02A_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("shenrt, ZhiYuan Electronics Co, Ltd.");
MODULE_DESCRIPTION("GPIO DRIVER FOR EasyARM-i.MX28xx");
最后编写测试函数 :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <string.h>
#define I2C_DEV_NAME "/dev/FM24C02A"
#define CMD_SET_ROM_ADDR 0X1
#define DATA_LEN 16
int main(int arg,char*args[])
{
unsigned int len;
int i,j;
int fd;
char tx_buf[DATA_LEN];
char rx_buf[DATA_LEN];
memset(tx_buf, 0, sizeof(tx_buf));
memset(rx_buf, 0, sizeof(rx_buf));
fd = open(I2C_DEV_NAME, O_RDWR); /* 打开 FM24C02A 驱动的设备文件*/
if(fd < 0)
{
printf("open %s failed\r\n", I2C_DEV_NAME);
return -1;
}
/* 设置 DS2460 内存储存器的读/写地址为 0 */
//ioctl(fd, CMD_SET_ROM_ADDR, 0x0);
for (i = 0; i < DATA_LEN; i++)
tx_buf[i] = i+1;
j=0;
ioctl(fd, CMD_SET_ROM_ADDR, 8);
len = write(fd, tx_buf, DATA_LEN/2); /* 把数据写入到FM24C02A内存储存器*/
if (len < 0)
{
printf("write data faile \n");
return -1;
}
usleep(1000*100);
ioctl(fd, CMD_SET_ROM_ADDR, 8);
len = read(fd, rx_buf, DATA_LEN/2); /* 把从FM24C02A内存储存器读出数据*/
if (len < 0)
{
printf("read data fail \n");
return -1;
}
for(i = 0; i < DATA_LEN; i++)
{
printf("%d ", rx_buf[i]);
}
close(fd);
return 0;
test_faile:
printf("test eeprom faile \n");
close(fd);
return -1;
}
将设备添加到内核进去重新编译并烧写到开发板后,insmod驱动,再运行app 效果如下:
总结
在linux系统下编写I2C驱动,目前主要有两种方法,
一种是把I2C设备当作一个普通的字符设备来处理(直接在用户层open(“i2c-0”)进行write和read),
另一种是利用linux下I2C驱动体系结构来完成(上面的实验)。