一、i2c子系统简介
1. i2c总线
i2c总线因为只用SCL、SDA两根线就实现了设备之间的数据互传,极大的简化PCB布线,因此,2c总线在EEPROM、小型LCD等设备中应用极光。i2c的相关时序操作等介绍,请参考其他的博主博文,本文主要将的是Linux系统下的i2c子系统。
2. i2c子系统
Linux系统定义了i2c驱动体系结构,在Linux系统中,i2c驱动由三部分组成,分别是i2c核心、i2c总线、i2c设备驱动。这三部分互相配合协作,形成了非常通用、可适应强的i2c框架。
1)i2c核心:主要是i2c总线驱动和设备驱动的注册、注销方法,i2c通信方法,上层的与具体设备其无关的代码以及探测设备、检测设备地址的上层代码。i2c框架中已经做好,做设备驱动开发,一般不需要动。
2)i2c总线:是i2c硬件提携结构中适配器端的实现,适配器可由CPU控制,一般Soc芯片里面都集成了i2c适配器。在这部分里面,控制i2c适配器产生信号,控制读写周期、产生ACK等等。该部分的代码一般由Soc厂家进行编写。
3)i2c设备驱动:是对i2c体系的设备端的实现,i2c接口的设备一般挂载到CPU控制的i2c适配器上。主要的数据结构为i2c_driver、i2c_client,这部分主要由设备驱动开发人员完成。
二、代码实现
这部分主要以i2c接口的0.96寸oled代码为例展示使用方式,其对应的i2c地址为0x3c。
1. 入口、退出函数
一个驱动的最开始和结束,由入口函数和退出函数决定。如下:
入口函数
static int __init oled_init(void)
{
int ret = 0;
ret = i2c_add_driver(&oled_driver);
printk("i2c ret: %d\n",ret);
return ret;
}
module_init(oled_init);
退出函数
static void __exit oled_exit(void)
{
i2c_del_driver(&oled_driver);
}
module_exit(oled_exit);
其中,oled_driver是struct i2c_driver类型的结构体,全部在下面。
也可以使用更简洁的方式
module_i2c_driver(oled_driver);
2. 相关数据结构
i2c设备端代码主要是i2c_driver结构体的填写。如下
static struct i2c_driver oled_driver = {
.probe = oled_probe,
.remove = oled_remove,
.driver = {
.owner = THIS_MODULE,
.name = "oled",
.of_match_table = oled_of_match_table,
},
.id_table = oled_id_table,
};
在oled_drive结构体里面可以看到oled_probe、oled_remove,分别对应i2c驱动子系统匹配drive和device匹配到和卸载掉的情况。oled_of_match_table和oled_id_table则是对spi驱动子系统匹配需要的必要信息,只要在表里面的即可匹配。
oled_of_match_table和oled_id_table如下:
static const struct i2c_device_id oled_id_table[] = {
{"htq,oled", 0},
};
static const struct of_device_id oled_of_match_table[] = {
{ .compatible = "htq,oled", },
};
compatible是设备树中对应的名字描述。oled屏幕挂载到第0个i2c适配器上,地址是0x3c,在开发板中可以看到
3. 相关数据结构的填写
在i2c_driver结构体中,由probe、remove成员变量。probe是设备匹配到驱动之后调用的,其将两者(driver和device)在软总线上进行比较,若一致,则两者匹配且调用probe函数。而remove则是卸载之后调用的函数,在里面会释放掉申请的各种资源。i2c子系统是注册到platform总线上面的,具体的内容以后谈到platform时再谈。
设备匹配到之后,调用probe这个成员变量对应的函数,具体的则是oled_probe函数。
static int oled_probe(struct i2c_client *client, const struct i2c_device_id *id);
在这个函数里面,需要做:
1. 构建设备号
if(oled_device.major){
oled_device.dev_id = MKDEV(oled_device.major, 0);
register_chrdev_region(oled_device.dev_id, 1, OLED_NAME);
} else {
alloc_chrdev_region(&oled_device.dev_id, 0,1, OLED_NAME);
oled_device.major = MAJOR(oled_device.dev_id);
}
2. 添加字符设备到内核
cdev_init(&oled_device.cdev, &oled_fops);
cdev_add(&oled_device.cdev, oled_device.dev_id, 1);
3. 创建类
oled_device.class = class_create(THIS_MODULE, OLED_NAME);
4. 创建设备
oled_device.device = device_create(oled_device.class,NULL, oled_device.dev_id, NULL, OLED_NAME);
以上四个基本通用,下面的就是根据具体硬件做配置
5. 初始化i2c设备
等等
在第2步中,有一个oled_fops,
static const struct file_operations oled_fops = {
.open = oled_open,
.read =oled_read,
.write = oled_write,
.unlocked_ioctl = oled_unlocked_ioctl,
.release = oled_release,
};
这个oled_fops跟之前入门led驱动led_fops类似,给驱动添加open、release、write、read、ioctl等接口。
oled_device是struct oled_device类型的自定义结构体,如下
struct oled_device
{
dev_t dev_id; //设备id
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct device_node *device_node; //设备节点
void *privare_data; //私有数据
int major;
};
4. 发送、接受数据
1) 发送数据:主要使用
struct i2c_msg m;
struct i2c_client *client;
m.addr = client->addr; //i2c设备从机地址,0x3c
m.buf = buffer; //发送缓冲区
m.flags = 0;
m.len = size; //发送字节数
ret = i2c_transfer(client->adapter, &m, 1);
这两个结构体,在m中配置好addr、buf、flags、len即可。addr就是设备的i2c地址,这里是0x3c。
buf是发送的缓冲区,len是发送的字节数,flags为0即可。配置好之后,调用i2c_transfer(client->adapter, &m, 1),就能发送数据,这里1表示发送一个i2c_msg消息,并不是发送1Byte。
2)接受数据:与发送数据类似,只是flags需要对其进行标记为I2C_M_RD。
m.flags = I2C_M_RD;
三、总结
总结:i2c子系统整体比较复杂,由三部分组成,i2c核心、i2c总线驱动、i2c设备驱动。i2c核心是i2c总线驱动、i2c设备驱动中间枢纽,以通用的、与平台无关的接口实现了i2c中设备与适配器的沟通。i2c总线驱动填充i2c_adapter和i2c_algorithm结构体。i2c设备驱动填充i2c_driver结构体并实现其本身对应设备类型的驱动。使用i2c子系统,从设备驱动开发看,主要做的是i2c设备驱动,调用相关API,使用比较简单,并不需要考虑其他。
环境:服务器ubuntu16,正点原子imx6ull开发板emmc版本。
参考书:Linux设备驱动开发详解(基于最新的Linux4.0内核) 宋宝华著
Linux设备驱动程序 J & G著