基于i2c子系统的驱动分析
来源:互联网 发布:现场工程师 linux维护 编辑:程序博客网 时间:2024/05/17 18:13
基于i2c子系统的驱动分析
和i2c有关的代码都在源码drivers/i2c目录下。内核提供了两种i2c的实现方法:
- 第一种叫i2c_dev,对应drivers/i2c/i2c-dev.c,这种方法仅仅封装了soc的i2c控制器操作,并向应用层提供操作接口。其本质是为应用层提供了一个库,驱动功能由应用层实现,不是主流的做法
- 第二种是驱动层实现所有驱动功能,是比较主流的做法
第二种可以认为是正统的i2c驱动,其本质是:工程师任意选用input子系统、misc框架、普通字符驱动等方式实现i2c驱动,i2c子系统的意义仅仅是为硬件操作提供接口(库)
1.i2c子系统的结构
如图
可以看出,i2c子系统基本机制和platform很类似,都是设备和驱动两者匹配来工作。i2c驱动只需调用核心层提供的接口(相当于核心层提供了库),即可方便地操作i2c
2.i2c总线核心分析
i2c总线核心提供了设备驱动和设备(client)的注册、注销方法, 还提供了一组不依赖于硬件平台的接口函数,I2C 总线驱动和设备驱动之间依赖于 I2C 核心作为纽带
3.i2c适配器(adapter)驱动分析
所谓的i2c适配器驱动,就是soc内部的i2c控制器的驱动,由原厂移植内核时提供,一般位于driver/i2c/busses内。而i2c适配器设备的注册,在3.x后的kernel中采用了设备树节点的方式,故这里需要分类讨论
老内核下的i2c适配器
我们这里用的是i2c-s3c2410.c,该驱动兼容三星大部分的soc,包括210。该驱动由platform总线实现,该驱动probe函数中主要做了:
- 填充了一个i2c_adapter结构体,并调用接口注册之,i2c_adapter 对应于SOC上的一个适配器
- 从platform_data(自留地)接收硬件信息,做必要的处理(为寄存器申请虚拟地址映射、申请中断等)
- 通过操作寄存器,对soc内的i2c适配器做初始化,比如把i2c速率设置为默认的100k。这一套设置基本通吃大部分器件,一般情况不用改动的
新内核下的i2c适配器
在新内核下,i2c适配器的驱动倒是没有变化,而i2c适配器设备体的注册,却采用了设备树的方式
- 下面是imx6qdl.dtsi中对i2c1适配器设备的定义和注册,里面定义了很多参数,一般来说我们是根本不用去修改这个节点的。假设我们要修改其中的参数(比如频率),只需在项目的dts中引用该节点,并重写即可
i2c1: i2c@021a0000 { #address-cells = <1>; #size-cells = <0>; compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c"; reg = <0x021a0000 0x4000>; interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks IMX6QDL_CLK_I2C1>; status = "disabled";};
4.i2c设备(client)注册分析
所谓的i2c设备(client),就是挂在i2c上的外设(比如各种传感器),这个需要我们自己注册,在3.x后的kernel中采用了设备树节点的方式,故这里需要分类讨论
老内核下的i2c设备(client)
对于老版本的内核,首先应该进入mach-xxx.c完成i2c设备(client)的注册。如何注册?这方面i2c和platform有较大不同,主要是soc上有多个i2c,所以是分开注册的
- 在mach-xxx.c中的xxx_machine_init函数中,发现由i2c_register_board_info来注册三个i2c上各自的设备。以i2c_devs0为例,i2c_devs0是一个数组,里面是i2c0上所有的设备
i2c_register_board_info(0, i2c_devs0, ARRAY_SIZE(i2c_devs0)); i2c_register_board_info(1, i2c_devs1, ARRAY_SIZE(i2c_devs1)); i2c_register_board_info(2, i2c_devs2, ARRAY_SIZE(i2c_devs2));
- 查看i2c_devs0的定义,我们发现该数组内部都是i2c_board_info结构体,如果要添加设备到i2c0,只需在该数组中使用I2C_BOARD_INFO这个宏即可,第一个参数是名字,第二个参数是设备在i2c上的地址,此宏的本质就是填充一个struct i2c_board_info,这一步作用是把wm8580以i2c设备的身份被注册,并且绑定i2c0这个适配器
static struct i2c_board_info i2c_devs0[] __initdata = { { I2C_BOARD_INFO("wm8580", 0x1b), /*假如要添加设备,就在这里加*/ },};
- 分析到这,我们可以发现结构体i2c_board_info就代表了i2c设备,那么client和这怎么没关系啊?其实结构体i2c_board_info是制造client的原料,client将会在i2c总线核心初始化时被制造
新内核下的i2c设备(client)
- 下面是imx6dl-hummingboard.dts中对于一个pcf8523的注册,它首先引用了imx6qdl.dtsi中的i2c1适配器,并重写了其中的status 属性为okay,如果设备对频率有要求,也可以重写clocks属性。在i2c1节点中定义了一个pcf8523节点,只有定义在这,该节点才会以i2c设备的身份被注册,并且绑定i2c1这个适配器
&i2c1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hummingboard_i2c1>; status = "okay"; rtc: pcf8523@68 { compatible = "nxp,pcf8523"; reg = <0x68>; };};
5.驱动编写流程
首先要明白一点,对于驱动工程师,如果手中是移植过的内核,则i2c总线核心和i2c适配器驱动是不需要动的,我们主要关注点在:提供i2c设备(client)、编写i2c设备驱动
- i2c子系统的本质是:工程师任意选用input子系统、misc框架、普通字符驱动等方式实现i2c驱动,i2c子系统的意义仅仅是为硬件操作提供接口(库)
- 此外,对于新内核和老内核,设备树会导致驱动会有一些细微的不同,主要体现在驱动和设备match的部分
老内核下的i2c驱动
#include <linux/i2c.h>#include <linux/module.h>#include <linux/string.h>#include <linux/fs.h>#include <linux/device.h>#include <linux/cdev.h>#include <linux/delay.h>#include <asm/uaccess.h>/*mpu6050内部寄存器地址 */#define MPU6050_RA_PWR_MGMT_1 0x6B#define MPU6050_RA_ACCEL_XOUT_H 0x3B#define MPU6050_CNT 1#define MPU6050_NAME "mpu6050"struct i2c_client *mpu6050_client;/* * 写mpu6050内部的寄存器。先发寄存器地址再发寄存器的值 */static int mpu6050_write_reg(unsigned char addr, unsigned char dat){ int ret = -1; struct i2c_msg msgs[2]; msgs[0].addr = mpu6050_client -> addr;//MPU6050_ADDR msgs[0].buf = &addr; msgs[0].len = 1; //长度1 byte msgs[0].flags = 0; //表示写 msgs[1].addr = mpu6050_client -> addr;//MPU6050_ADDR msgs[1].buf = &dat; msgs[1].len = 1; //长度1 byte msgs[1].flags = 0; //表示写 /*连续发送两帧信息*/ ret = i2c_transfer(mpu6050_client ->adapter, msgs, 2); if (ret != 2) { printk(KERN_INFO "i2c_transfer(mpu6050 write) error \n"); return -EIO; } return 0;}/* *读mpu6050内部的寄存器。先发寄存器地址再读寄存器的值 */static int mpu6050_read_reg(unsigned char addr, unsigned char buf){ int ret = -1; struct i2c_msg msgs[2]; msgs[0].addr = mpu6050_client -> addr;//MPU6050_ADDR msgs[0].buf = &addr; msgs[0].len = 1; //长度1 byte msgs[0].flags = 0; //表示写 msgs[1].addr = mpu6050_client -> addr;//MPU6050_ADDR msgs[1].buf = &buf; msgs[1].len = 1; //长度1 byte msgs[1].flags = I2C_M_RD; //表示读 /*连续发送两帧信息*/ ret = i2c_transfer(mpu6050_client ->adapter, msgs, 2); if (ret != 2) { printk(KERN_INFO "i2c_transfer(mpu6050 read) error \n"); return -EIO; } return 0;}static int mpu6050_open(struct inode *inode, struct file *file){ printk(KERN_INFO "open mpu6050\n"); msleep(50); mpu6050_write_reg(MPU6050_RA_PWR_MGMT_1, 0X80);//复位 /*这里仅仅做个例子,一般在这里要做初始化*/ return 0;}ssize_t mpu6050_read(struct file *file, char __user *ubuf, size_t size, loff_t *opp){ unsigned char buf [6] = {0}; mpu6050_read_reg(MPU6050_RA_GYRO_XOUT_H, buf[0]); /*这里仅仅是举个例子,怎么从外设中读数据*/ ret = copy_to_user(ubuf, buf , size); if (ret) { printk(KERN_INFO "copy_to_user fail\n"); return -EINVAL; } return 0;}static int mpu6050_release(struct inode *inode, struct file *file){ return 0;}static const struct file_operations mpu6050_fops = { .owner = THIS_MODULE, .open = mpu6050_open, .read = mpu6050_read, .release = mpu6050_release,};static struct cdev *mpu6050_pcdev;static struct class *mpu6050_pclass;dev_t mpu6050dev_num = 0;unsigned int mpu6050dev_major = 0;int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id){ int ret = -1; mpu6050_client = client; /*内核自动分配一个设备号*/ ret = alloc_chrdev_region(&mpu6050dev_num, 0, MPU6050_CNT, MPU6050_NAME); mpu6050dev_major = MAJOR(mpu6050dev_num); if (ret < 0) { printk(KERN_INFO "alloc_chrdev_region fail\n"); goto out_err_0; } printk(KERN_INFO "MAJOR %d\n", mpu6050dev_major); /*实例化一个字符设备体*/ mpu6050_pcdev = cdev_alloc(); /*填充cdev设备体 。最主要是将file_operations填充进去*/ cdev_init(mpu6050_pcdev, &mpu6050_fops); /* 将设备体与设备号绑定并向内核注册一个字符设备*/ ret = cdev_add(mpu6050_pcdev, mpu6050dev_num, MPU6050_CNT); if (ret) { printk(KERN_INFO "cdev_add fail\n"); goto out_err_1; } /*创建类、设备*/ mpu6050_pclass = class_create(THIS_MODULE, "mpu6050"); if (IS_ERR(mpu6050_pclass)) { //排错 printk(KERN_ERR "can't register class\n"); goto out_err_2; } device_create(mpu6050_pclass, NULL, mpu6050dev_num, NULL, "mpu6050"); return 0;/* “倒影式”错误处理流程*/out_err_3: class_destroy(mpu6050_pclass);out_err_2: cdev_del(mpu6050_pcdev);out_err_1: unregister_chrdev_region(mpu6050dev_num, MPU6050_CNT);out_err_0: return -EINVAL;}int mpu6050_remove(struct i2c_client *client){ /*倒影式注销流程*/ device_destroy(mpu6050_pclass, mpu6050dev_num); class_destroy(mpu6050_pclass); cdev_del(mpu6050_pcdev); unregister_chrdev_region(mpu6050dev_num, MPU6050_CNT); return 0;}/* * i2c设备驱动结构体内的id_table。用作匹配功能 */static struct i2c_device_id mpu6050_id[] = { { "mpu6050", 0}, { }};/* * 这里开始定义i2c设备驱动结构体 */static struct i2c_driver mpu6050_driver = { .driver = { .name = "mpu6050",//i2c总线和platform不同这个name仅仅是名字。并不用作匹配功能 .owner = THIS_MODULE, }, .probe = mpu6050_probe, .remove = mpu6050_remove, .id_table = mpu6050_id,//i2c总线和platform不同。只用id_table来匹配driver和client};/* * 模块加载函数负责注册i2c设备驱动 */static int __init mpu6050_init(void){ return i2c_add_driver(&mpu6050_driver);}static void __exit mpu6050_exit(void){ i2c_del_driver(&mpu6050_driver);}module_init(mpu6050_init);module_exit(mpu6050_exit);MODULE_LICENSE("GPL");
- 整个程序很简单,关键点主要是driver结构体里要有一个id_table,如果mach-xxx中定义的设备名字和id_table相同,那么会probe函数就被触发
- 触发probe后,
i2c_client *client
将作为参数传入probe,这个i2c_client里面就包含了设备的私有数据(比如设备的i2c地址、绑定的i2c适配器等),类似plat_data,我们在probe中将i2c_client *client
绑定给全局变量mpu6050_client
,这样就能在read、write等函数中用mpu6050_client -> addr
来得到设备的i2c地址了,用mpu6050_client ->adapter
来得到绑定的i2c适配器 - 只要read、write知道了设备的i2c地址、绑定的i2c适配器,那么就能利用kernel提供的接口来进行i2c传输。上面代码中用的接口是
i2c_transfer
,这种方法有点老旧;kernel官方强烈推荐smbus族接口来进行i2c收发,smbus是i2c_transfer的子集,很多soc可能不支持i2c_transfer
这个接口,这时就只能使用smbus族接口。这两个接口在内部逻辑上有很大不同,比如我们要写mpu6050内部的RA_PWR_MGMT_1寄存器,根据上面的代码,我们调用了两次i2c_transfer
,第一次发送RA_PWR_MGMT_1寄存器的地址,第二次发送要写的值。而对于smbus族接口来说,只需调用一次就行了,可以认为smbus族接口进行了更好的封装,不仅写操作如此,读操作也如此,具体接口如下
/*第一个参数是client,第二个参数是i2c设备内的寄存器地址,第三个参数是要写入的值*/i2c_smbus_write_byte_data(mpu6050_client, MPU6050_RA_PWR_MGMT_1, data);/*第一个参数是client,第二个参数是i2c设备内的寄存器地址,返回值是读出来的值*/read_val = i2c_smbus_read_byte_data(mpu6050_client, MPU6050_RA_ACCEL_XOUT_H);
- 如果要以16bit为单位读写i2c,那么可以用下面的接口,使用方法都是类似的
i2c_smbus_read_word_data();i2c_smbus_write_word_data();
新内核下的i2c驱动
设备树对i2c设备的注册有比较大的影响,详见前面的章节,这里不再赘述;而对于驱动程序,设备树带来的变化极小,主要是驱动和设备之间的匹配方式变了
- 老旧的id_table方式不再使用,取而代之的是类似的一种结构:of_match_table
- 这里以pcf8523驱动为例,只要驱动中的of_match_table 中的compatible 值和设备节点中的compatible 相匹配,那么probe函数就会被触发。不仅i2c是这样,platform、spi等都是这个原理
/*定义的of_match_table*/static const struct of_device_id pcf8523_of_match[] = { { .compatible = "nxp,pcf8523" }, { }};/*driver 结构体中的of_match_table*/static struct i2c_driver pcf8523_driver = { .driver = { .name = DRIVER_NAME, .owner = THIS_MODULE, .of_match_table = of_match_ptr(pcf8523_of_match), }, .probe = pcf8523_probe, .id_table = pcf8523_id,};
- i2c和spi驱动还支持一种“别名匹配”的机制,就以pcf8523为例,假设某程序员在设备树中的pcf8523设备节点中写了compatible = “pcf8523”;,显然相对于驱动id_table中的”nxp,pcf8523”,他遗漏了nxp字段,但是驱动却仍然可以匹配上,因为别名匹配对compatible中字符串里第二个字段敏感
- 基于i2c子系统的驱动分析
- 基于I2C子系统的I2C驱动编写
- 基于i2c子系统的驱动分析-设备树
- Linux I2C子系统分析-I2C总线驱动
- Linux I2C子系统分析-I2C设备驱动
- Linux I2C子系统分析-I2C总线驱动
- Linux I2C子系统分析-I2C总线驱动
- Linux I2C子系统分析-I2C总线驱动
- Linux I2C子系统分析-I2C设备驱动
- Linux I2C子系统分析-I2C总线驱动&&Linux I2C子系统分析-I2C设备驱动
- 基于input子系统的驱动分析
- i2c 子系统驱动经典例子深入分析
- Linux I2C子系统分析-I2C总线驱动 1
- Linux I2C子系统分析-I2C设备驱动 2
- tcc893x基于Linux内核的I2C总结(I2C基础、I2C适配器驱动分析)
- linux驱动子系统--I2C
- linux内核I2C驱动子系统分析(一)
- linux内核I2C驱动子系统分析(二)
- Spark Streaming和Storm的区别和联系
- 免费、高性能的人脸检测库
- 如何判断一个二叉树是否为平衡二叉树。
- json字符串转化为复杂的java对象问题解决分享
- Hadoop和Spark的联系和区别
- 基于i2c子系统的驱动分析
- Spark概述
- typedef的用法
- 欢迎使用CSDN-markdown编辑器
- 通过设置Weblogic设置线程数提高系统并发
- var 全局变量 局部变量
- 解决jquery easyui-datagrid列过多且无数据时列显示不全的bug
- iOS开发手机震动效果
- SQL多表连接查询