Linux的I2C驱动框架分析

2020-03-17 14:51:56 浏览数 (1)

1.基本概念


总线

总线代表着同类设备需要共同遵守的工作时序,不同的总线对于物理电平的要求是不一样的,对于每个比特的电平维持宽度也是不一样,而总线上传递的命令也会有自己的格式约束。如I2C总线、USB总线、PCI总线等等。以I2C总线为例,在同一组I2C总线上连接着不同的I2C设备。

设备

设备代表真实的、具体的物理器件,在软件上用器件的独特的参数属性来代表该器件。如I2C总线上连接的I2C从设备都有一个标识自己的设备地址,由这个设备地址来确定主设备发过来的命令是否该由它来响应。

驱动

简单的说驱动代表着操作设备的方式和流程。

Linux总线设备框架的工作原理

如果想要弄清楚I2C驱动框架,必须深刻的理解Linux的总线设备框架。之所以会形成这样的框架,很重要的原因是为了代码的复用性。因为驱动和设备的关系是一对多的,对于相同类型的不同的设备,可共用同一套驱动程序接口。为了提高驱动的可移植性,Linux抽象出一套管理资源的函数。设备是存在的硬件,在设备里包含自己的属性,也包含需要用到的资源。

总线的作用就是在软件层面上对设备和驱动进行管理,设备要让系统感知到自己的存在,所以需要向总线去注册设备,驱动同样也要向总线去注册。对于总线,有I2C总线,Platform总线等等。但是Platform是虚拟总线。

对于总线上设备与驱动的匹配,由总线负责,设备在注册的时候,总线会遍历注册在总线上的驱动,如果名字相同,则匹配上了,此时调用驱动程序的probe函数。同样的驱动在注册的时候,也会遍历总线上的设备,如果匹配上(名字一样),则也会调用驱动程序的probe函数。

2.I2C传输协议


对于I2C来说,有如下的特点:

1.一条串行数据线(SDA),一条串行时钟线(SCL)

2.每个接到总线上的器件都可以使用软件根据它的唯一地址来识别。

3.串行的8位双向数据传输,位速率在标志模式下可达100kbit/s,在快速模式下可达400kbit/s。在高速模式下可达3.4Mbit/s。

下面来看一下具体的硬件连接

以上是TFS上的摄像头I2C的连接方式,只有两根线即可实现数据的传输。在传输过程中,需要注意以下三种类型的信号:

(1)开始信号(S):SCL为高电平时,SDA由高向低电平跳变,开始传输数据

(2)结束信号(P):SCL为高电平时,SDA由低向高电平跳变,结束传输数据

(3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA的电平

以上就是I2C的硬件层与协议层的基本概述,这部分可以作为基本认知。

3.Linux下I2C驱动程序的体系结构


对于Linux下的I2C驱动,其体系结构的组成主要分为三个部分

(1)I2C核心:I2C核心提供了I2C总线驱动和设备驱动的注册,注销方法,I2C通信方法(”algorithm”)上层的,与具体适配器无关的代码以及探测设备,检测设备地址的上层代码等。

(2)I2C总线驱动:I2C总线驱动是对I2C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。

(3)I2C设备驱动:I2C设备驱动(也称为客户驱动)是对I2C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。

比较重要的文件

kerneldriversi2ci2c-core.c

这个文件实现了 I2C 核心的功能以及/proc/bus/i2c*接口。同时对I2C底层的收发函数进行封装。会调用i2c_transfer ,里面实现了adap->algo->master_xfer(adap, msgs, num)

kerneldriversi2ci2c-dev.c

该函数注册了一个设备文件的功能,也就是注册了一个字符设备驱动程序,可以通过/dev/i2c-0(i2c-0, i2c-1,…, i2c-10,…)找到具体的I2C适配器,这个I2C设备的主设备号为89,次设备号0~255。通过访问这个接口,可以通过open()、 write()、 read()、 ioctl()和 close()等来访问这个设备。

kerneldriversi2cbussesi2c-v12-jz.c

该函数对君正的x1000底层的I2C操作控制函数,通过设置寄存器来进行I2C的控制。其最底层的收发函数都在该文件里定义。重要的是i2c_jz_algorithm,其中algorithm实现了对底层寄存器的操作。

比较重要的结构体

i2c_driver、 i2c_client、 i2c_adapter 和 i2c_algorithm这四个结构体十分的关键

i2c_driver

对应一套驱动方法,是纯粹的用于辅助作用的数据结构,它不对应于任何的物理实体。

i2c_client

对应于真实的物理设备,每个 I2C 设备都需要一个 i2c_client 来描述。i2c_client 一般被包含在 I2C 字符设备的私有信息结构体中。

i2c_adpater

用来匹配i2c_driver与i2c_client。即 i2c_client 依附于 i2c_adpater。由于一个适配器上可以连接多个 I2C 设备, 所以一个 i2c_adpater 也可以被多个 i2c_client 依附, i2c_adpater 中包括依附于它的 i2c_client 的链表 。

i2c_algorithm

代码语言:javascript复制
struct i2c_algorithm{
    //i2c模式下,收发函数接口
     int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);
    //用于SMBUS模式下,收发函数接口
     int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,  unsigned short flags, char read_write,  u8 command, int size, union i2c_smbus_data *data);
    //用于检查I2C主控制器所支持访问接口,如I2C_FUNC_SMBUS_BYTE,查看是否支持smbus单字节读取和写操作
     u32 (*functionality) (struct i2c_adapter *);
}

该函数主要实现其I2C底层的操作

4.GC0328摄像头I2C实例分析


对于摄像头驱动程序,首先要知道如何让摄像头能够正常工作。

第一步:摄像头上电

在这一步的工作中,可以控制相关的GPIO进行摄像头使能,控制RESET及POWERON来让摄像头正常工作。

GC0328的上电时序如下图所示:

第二步:给摄像头提供时钟

这一步也比较的关键,对于摄像头来说,其时钟就是心跳,如果要让摄像头正常的工作,则需要x1000的CIM提供24MHz的时钟给摄像头。

第三步:配置摄像头的寄存器

对于一个摄像头sensor,需要其输出指定大小及指定格式的图片,则需要配置摄像头的寄存器。而配置摄像头寄存器就是需要通过I2C来进行配置。

第四步:配置CIM

x1000内部的摄像头接口控制模块,可以将摄像头数据进行处理,可以进行帧错误检查以及数据的传输。这部分的控制需要那些CIM相关的寄存器来完成。

第五步:启动CIM

配置及初始化完成后就可以启动摄像头了,CIM负责数据传输及产生相应的中断。

以上是摄像头初始化的一个完整的过程,对于摄像头初始化部分,I2C又是如何进行初始化及设置的呢?这也是本文的重点。

根据前面的总线设备驱动的框架,有driver那么肯定会有device。这两者的匹配靠的是.id_table

对于gc0308,具体可以通过kernel/arch/mips/xburst/soc-x1000/chip-x1000/halley2/common/i2c_bus.c

可以看到向I2C总线注册的device的是gc0308

如果匹配上了,则调用driver的.probe函数。下面我们来看一下该函数具体做了什么事情。

在probe函数中,主要向v412_i2c_subdev提供了一个可操作的client,也就是相当于I2C的操作函数的接口交给V4L2视频驱动框架来进行管理。向V4L2视频驱动框架提供的函数如下:

第一个结构体是有关视频操作的接口,比如设备gc0328的输出格式,得到当前的视频输出格式等等

第二个结构体是控制camera上电与断电,以及控制白平衡,对焦的其他的参数

通过V4L2的I2C子设备控制来进行设置。下面来基本分析一下其调用过程:

当应用程序通过ioctl传递VIDIO_S_FMT,是可以设置摄像头输出的格式

然后看一下写寄存器的过程

调用了i2c_smbus_write_byte_data该函数在kerneldriversi2ci2c-core.c,这样就进入了i2c总线操作函数中。

该函数会调用i2c_smbus_xfer

为什么不满足条件,可以看注册的i2c的平台设备,在kerneldriversi2cbussesi2c-v12-jz.c路径下

有个i2c_algorithm的结构体

代码语言:javascript复制
struct i2c_algorithm{
    //i2c模式下,收发函数接口
     int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);
   //用于SMBUS模式下,收发函数接口
     int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,  unsigned short flags, char read_write,  u8 command, int size, union i2c_smbus_data *data);
    //用于检查I2C主控制器所支持访问接口,如I2C_FUNC_SMBUS_BYTE,查看是否支持smbus单字节读取和写操作
     u32 (*functionality) (struct i2c_adapter *);
}

所以只会向下执行,当执行到i2c_smbus_xfer_emulated ,会调用

该函数会调用

最后调用到kerneldriversi2cbussesi2c-v12-jz.c的最底层的实现

在kerneldriversi2cbussesi2c-v12-jz.c函数中

这个函数指向i2c_jz_xfer

在这个函数中,实现了I2C的读写,可以根据传递的flag进行判断是读操作函数写操作

最底层操作寄存器来实现其读写函数

到这里,一个I2C完整的传输流程就完成了。

5.总结


对于I2C完整的传输协议,最重要的是弄清楚总线驱动程序的框架,因为I2C也是属于总线框架。对于I2C总线设备框架的模型,可以用下图来说明:

也就是device与driver同时向i2c总线上注册。当注册在总线上时,可以通过id_table进行匹配,匹配上之后会调用driver的.probe函数。对于一般的I2C设备,可以在probe函数中注册一个字符设备驱动,从而应用层可以通过open函数打开/dev/i2c-0等设备节点。从而对I2C设备进行读写操作。而摄像头部分,直接将控制接口传递给V4L2进行管理,这样通过视频设备驱动框架进行摄像头调节,从而达到控制的目的。

0 人点赞