1 I2C总线的硬件构成
I2C 总线协议只有两条总线线路,一条是串行数据线(SDA),一条是串行时钟线(SCL)。SDA 负责数据的传输,SCL 负责数据传输的时钟同步。I2C 设备通过这两条总线连接到处理器的I2C总线控制器上,不同设备之间通过7 位地址来区别,而且数据的传输是双向的,方向的确定由1位二进制数确定,地址位加方向位是操作I2C 设备的惟一标示,I2C 设备与CPU 的连接如图1所示。
图1 I2C设备与CPU连接图
I2C 总线上有3 种类型的信号,分别是:开始信号,结束信号和应答信号。这些信号都是由SDA和SCL上的电平变化来表示的。
开始信号(S):当SCL为高电平时,SDA由高电平向低电平跳变,表示开始传输数据。
结束信号(P):当SCL为高电平时,SDAY由低电平向高电平跳变,表示结束传输数据。
相应信号(ACK):从机接收到8位数据后,在第9个时钟周期,拉低SDA电平,表示已经接收到数据。
当总线空闲时,SDA 和SCL 都处于高电平,主机检测到总线空闲就可以向从机发送数据。主机首先发送开始信号S,接着发出8位数据(包括前7位的从机地址和1 为的方向位),然后等待从机发回确认信号ACK.
当第8位为0时,表示向从机传输数据,主机收到确认信号后就可以连续的向从机写入8 位数据;当第8 位为1时,表示向从读取数据,这时主机就可以接收来自从机的一系列数据。最后当总个数据传输过程完成后,由主机发送结束信号P,表示本次的数据传输完成。
2 Linux 的I2C设备驱动程序的层次结构
因为I2C设备的种类繁多,如果为每一款I2C设备都编写一个驱动程序,显然不太现实也不太可能做到。所以,Linux中是对I2C 设备驱动采取了层次化处理,分为总线层和设备层。将I2C设备驱动的一些共同属性抽象起来归结起来作为总线层,而将具体I2C设备特殊操作作为设备层。在Linux中I2C设备驱动中用到的数据结构[4,7-8]的关系如图2 所示。关于这部分代码位于Linux内核源码树的/driver/i2c中。
图2 I2C驱动程序框架
理解这层次结构重点是要理解4个数据结构,分别是属于设备层的i2c_driver 与i2c_client,属于总线层的i2c_adapter与i2c_algorithm.下面分别对这四个数据结构做简要的说明。
struct i2c_driver:具体的每一个I2C设备都应该对应着的一个驱动,这个结构体里面定义了Linux设备模型中用于I2C 总线管理的一系列函数指针和I2C 设备的信息。其中最重要的两个成员是适配器检测函数指针at-tach_adapter,和设备ID表id_table.
struct i2c_client:一个连接在SDA 和SCL 总线上的具体设备是由i2c_client结构体描述的,定义了两个成员变量表示这个具体设备所对应的适配器和驱动。
struct i2c_adapter:此结构体表示CPU 里面具体的I2C控制器,本质上也是对应着一个物理设备,其中最要的成员变量是指向适配器驱动的程序的algo 结构体指针。
struct i2c_algorithm:里面定义了具体适配器驱动程序的函数指针。特别是master_xfer函数指针,这个函数实现了适配器最底层的操作方法,也是I2C设备驱动中总线层里面要编写的重要函数。
i2c_dev 里面定义了读写I2C 设备应用层的读写接口,但由于其缺少通用性,一般很少用到所以并不做详细的介绍。
i2c_core在驱动框架中起到了承上启下的作用,里面定义了许多重要的函数。例如:adapter注册/注销函数,增加/删除设备驱动函数,增加/删除I2C设备的函数,I2C传输,发送和接收函数。这些函数都是在编写I2C设备驱动程序中必须要用到的接口函数,正是由于这些通用的接口函数才使得代码具有很强的可移植性和重用性。
3 编写I2C设备驱动的思路
在了解Linux中I2C设备驱动的基本框架后,要编写自己的设备驱动首先要弄清楚的一个问题是到底内核已经实现了那部分,需要实现的又是那部分。因为I2C设备驱动是基于总线设备驱动模型的,一般而言在移植Linux操作系统中,Linux内核已经对总线部分已经有了很好的实现,所以总线部分的驱动一般可以不必关心。
在此需要实现的是设备层的i2c_driver与i2c_client结构体,并利用I2C 子系统提供的接口函数挂接到I2C 总线上。
每一个I2C设备驱动,必须首先创造一个i2c_driver结构体对象,该结构体包含了I2C设备探测和注销的一些基本方法和信息。其中包括设备驱动的名字,适配器的挂接/取消函数指针等。一个例子如下所示,name字段标识本驱动的名称(不要超过31 个字符),at-tach_adapter和detach_client字段为函数指针,这两个函数在I2C设备注册的时候会自动调用,需要自己实现这两个函数。
上面定义的i2c_driver对象,抽象为一个I2C的驱动模型,提供对I2C设备的探测和注销方法,接下来就是要定义i2c_client 结构体,其代表着一个具体的I2C 设备,该结构体有一个data指针,可以指向任何私有的设备数据,在复杂点的驱动中可能会用到。
每一个I2C设备芯片,都通过硬件连接设定好了该设备的I2C设备地址。因此,I2C设备的探测一般是靠设备地址来完成的。那么,首先要在驱动代码中声明你要探测的I2C设备地址列表以及一个宏,示例如下:
有了i2c_client结构体代表了具体的设备和设备ID后就可以实现attach_adapter 和detach_client 函数。这两个函数是系统自动调用的,它的实现是有一定的框架的,可以在linux内核源码的驱动例子中找到,由于代码过长这里不做具体的分析。针对不同的设备函数的实现会略有不同,一般attach_adapte需要完成的工作是对i2c_client结构体成员赋值和调用接口函i2c_attach_cli-ent把设备挂接到适配其中。而detach_client 函数则是完成相反的工作。
最后的一步是编写模块的初始化与退出函数把驱动加进I2C驱动子系统中,示例可以是:
至此,I2C设备的驱动已经完成了,但是到了这一步本驱动并没有实际的用处,它仅仅提供的是一个设备驱动程序的管理框架,所以必须还要进行两方面的补充。
第一方面是,利用I2C总线读写外部芯片的控制/状态寄存器;第二方面是,向应用层提供I2C设备的读写接口,令应用程序可以对设备节点的读写实现对I2C具体物理设备的读写。为了实现I2C 设备寄存器的读写操作,必须要用到Linux的I2C子系统提供的读写接口函数:
利用这两个函数根据芯片的读写时序进行封装,就可以读写芯片内部的寄存器,以写芯片寄存器为例,必须写往总线上写寄存器的地址,然后写入要往寄存器里写入的数据,示例代码如下所示。读寄存器的时序则是则是先写入要读寄存器的地址,然后接受总线上的数据,区别不大,不做示例。
要想向应用层提供读写接口,则必须再对I2C设备驱动进行一次简单字符设备驱动的封装,将I2C设备作为一个简单字符设备,依次实现字符设备文件操作函数结构体file_operation 里面的函数指针所对应的接口函数,这里只给出了大体的框架,具体的实现对于不同的芯片有很大的不同。
定义一个字符设备结构体cdev,将I2C 设备当做一个普通的字符设备处理。
定义一个文件操作函数结构体,填写里面函数指针,指出设备操作所对应的具体函数,一般的例子是:
接着就是编写file_operations所对应的具体函数。
最后一步是在模块的初始化和退出函数中增加对简单字符设备的注册和注销操作,包括设备号申请与注销,设备注册与注销两方面。
至此,将编译好的模块加载进内核后就可以在用户空间利用文件系统的API对设备文件进行各种操作。