本节来看I2C驱动框架,题目叫IMX6ULL I2C框架,不是imx特有的i2c驱动框架,只是借着IMX6ULL的平台来分析linux内核中的I2C驱动框架,

在Linux内核中,把I2C分为I2c总线、设备、驱动,实现了分层的概念,那么,在内核中,I2C总线抽象为一个结构体叫做I2c_adapter,一个i2c_adapter就是平台上一个真实的物理I2C

i2c_adapter

来看一下I2C框架中最重要的i2c_adapter结构体的内容

imx6ull cpu架构_设备树

其中重要的成员是struct i2c_algorithm *algo,这个结构体中包含了数据传输的一些函数,这个函数都是和具体的单板相关的,比如我们使用IMX6ULL这个平台,那么恩智浦官方就会针对他们的平台提供这些

i2c的传输函数,然后注册到内核中,来看一个具体的例子

在官方的设备树中找到i2c的设备节点,根据compatible属性查找对应的驱动程序

imx6ull cpu架构_数据传输_02

注册平台设备驱动

imx6ull cpu架构_设备树_03

其中的匹配列表和设备树中对应

imx6ull cpu架构_数据传输_04

平台设备驱动重点就是probe函数

imx6ull cpu架构_数据_05

probe函数开始的时候申请了一个结构体imx_i2c_struct,这个结构体是nxp自己定义的结构体,但是它里边肯定包含linux内核要求的结构体i2c_adapter

imx6ull cpu架构_数据_06

果然如此的,再接着往下看,然后就是去获取i2c控制器的基地址,往下就是重点了,可以看到设置imx_i2c_struct结构体中的i2c_adapter成员了

imx6ull cpu架构_imx6ull cpu架构_07

重点就是设置adapter.algo      = &i2c_imx_algo; i2c_imx_algo结构体就是算法结构体,其中包含i2c控制器的数据传输的函数

imx6ull cpu架构_imx6ull cpu架构_08

master_xfer函数就是用于数据的传输的,点进去i2c_imx_xfer中可以看到,就已经去设置imx6ull平台的i2c控制寄存器了,来完成数据的传输,这里就不再去查看,

回到probe函数继续往下分析,下面就是时钟和中断的一些配置,最后是注册i2c_adapter结构体

imx6ull cpu架构_imx6ull cpu架构_09

总结:

到这里可以看出,对于内核中的I2C适配器要进行三个步骤

  1. 申请一个i2c_adapter结构体
  2. 设置i2c_adapter结构体,主要是struct i2c_algorithm成员的设置
  3. 注册i2c_adapter结构体

这样一来,和平台相关的I2C通信方法就注册好了,接下来就是具体的I2C设备来使用这些方法,具体的I2C设备也叫作I2C框架中的设备层

上面注册的i2c_adapter要怎么来使用呢?下面来看一个具体的例子

I2C实例

IMX6ULL平台使用的i2c器件ap3216c,它是在物理的i2c1总线下面,所以在设备树中添加节点时要把它添加到i2c1节点下面

i2c1 {
  clock-frequency = <100000>;
  pinctrl-names = "default";
  pinctrl-0 = <&pinctrl_i2c1>;
  status = "okay";
 
  ap3216c@1e {
   compatible = "alientek,ap3216c";
   reg = <0x1e>;
  };
};

针对设备树提供的驱动函数

/* 设备树匹配列表 */
 static const struct of_device_id ap3216c_of_match[] = {
 { .compatible = "alientek,ap3216c" },
 { /* Sentinel */ }
 };

 /* i2c 驱动结构体 */
 static struct i2c_driver ap3216c_driver = {
	 .probe = ap3216c_probe,
	 .remove = ap3216c_remove,
	 .driver = {
		 .owner = THIS_MODULE,
		 .name = "ap3216c",
		 .of_match_table = ap3216c_of_match,
	  },
	.id_table = ap3216c_id,
 };

static int __init ap3216c_init(void)
 {

	int ret = 0;

	ret = i2c_add_driver(&ap3216c_driver);
 	return ret;
 }

 static void __exit ap3216c_exit(void)
 {
	i2c_del_driver(&ap3216c_driver);
 }

 /* module_i2c_driver(ap3216c_driver) */

 module_init(ap3216c_init);
 module_exit(ap3216c_exit);
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("zuozhongkai");

申请并设置一个i2c_driver结构体,然后在入口函数中通过i2c_add_driver去注册这个结构体,注册i2c_driver结构体的部分比较复杂,因为它会根据设备树中的i2c节点的地址和之前已经注册的i2c_adapter去给设备发生输出,

根据设备的应答从而判断是否真实存在此设备,这个过程这里就不再列出来了,

但是我们需要知道的是,设备匹配成功之后会进入i2c_driver->probe函数,来看一下probe函数的参数

static int ap3216c_probe(struct i2c_client *client,const struct i2c_device_id *id)

第一个参数i2c_client,一个i2c_client就表示这个设备的一个代理,

imx6ull cpu架构_数据_10

i2c_client中有一个成员是i2c_adapter,到这里就会明白,在probe函数中我们拿到了i2c_client结构体,其中包含i2c_adapter结构体,这个i2c_adapter结构体就是具体的i2c设备挂载的i2c总线的抽象

前面所说的,i2c_adapter中有最重要的输出传输函数,那么,在probe函数中就可以访问i2c设备了,

不管在什么时候,只要我们拿到i2c_client结构体,就可以访问i2c设备

 

总结

简单过了一下i2c的注册过程,总结一下

内核要做的事情

  1. 申请i2c_adapter结构体
  2. 构造i2c_adapter结构体中的数据传输函数,填充i2c_adapter结构体
  3. 注册i2c_adapter结构体

内核主要做的事情是针对具体的单板提供i2c控制器的操作函数,以为它不知道我们要用i2c总线发送什么数据,但是它只知道怎么样来操作这个i2c总线,

所以,它为用户提供数据传输函数,至于传输什么数据它是不知道的.

 

接下来就是程序员要去做的事情,i2c总线的操作函数有了,就要使用这些函数操作具体的i2c设备了,这里也有一个重要的结构体i2c_driver

  1. 申请i2c_driver结构体
  2. 填充i2c_driver结构体,这里使用的是设备树,所以,着重的是它的匹配列表of_match_table
  3. 注册i2c_driver结构体

这个i2c_driver的注册过程会很复杂,因为要去匹配设备,

会获取设备树中匹配的i2c节点的设备地址,使用内核提供的i2c总线的操作函数,针对此i2c设备地址发送一个开始信号,然后检测i2c设备有没有回应,

如果有i2c设备的ack信号,说明i2c设备存在,那么这个I2c_adapter+i2c 地址这个组合就是没有问题的

然后,会创建一个i2c_client结构体,i2c_client结构体中包含i2c_adapter,以及i2c设备地址,并把这个i2c_client结构体作为参数返回调用i2c_driver中的probe函数

在i2c_driver结构体的probe函数中就可以访问想要操作的i2c设备,具体的根据程序员的需求去操作,到这里就结束了

 

内核中的驱动大多是这个流程,它会抽象出一套公共的部分,然后不同的部分留出来,由程序员去填充,实现了只需要修改设备,而不是每次都去重新定义那些公共代码,看内核驱动就会感叹代码的艺术