嵌入式Linux中I2C设备驱动程序的研究与实现
I2C是“Inter Integrated Circuit Bus”的缩写,中文译成“内部集成电路总线”, 它是Philips 公司于20 世纪80 年代研发成功的一种具有多端控制功能的双线双向串行数据总线标准, 其具有模块化、电路结构简单等优点。在嵌入式系统中,I2C总线已经成为器件接口的标准之一, 常用于连接RAM、EEPROM 以及LCD 控制器等设备。另外,总线的数据传输是以字节为单位的。
目前,标准的I2C的传输速率可以达到100kbit/s,能支持128 个设备,增强型I2C传输速率可达400kbit/s,能支持多达1024 个设备,高速模式下的I2C 传输速率更高达3.4Mbit/s。
序就可以轻松地操作和驱动硬件架构的分层。
2 Linux 的I2C 体系结构
2.1 Linux 下I2C 体系结构分析
Linux 的I2C 体系结构由3 大部分组成:
(1)I2C框架:I2C.h 和I2C-core.c 为I2C框架的主体,提供了核心数据结构的定义、I2C 适配器驱动和设备驱动的注册、注销方法,I2C 通信方法(algorithm)上层的、与具体适配器无关的代码、以及检测设备地址的上层代码等。作为核心的I2C-core.c 还为总线驱动设备提供了一些统一的调用接口进行读写和设置操作, 另外它还提供了将各种支持的总线设备驱动添加到这个体系中的方法, 以及当不再使用这些总线驱动时从体系中删除的方法。
(2)I2C 总线驱动I2C总线驱动是对I2C 硬件体系结构中适配器端的实现,I2C 总线驱动主要包含了I2C 适配器数据结构I2C_adapter, 以及描述在具体I2C 适配器上的总线通信方法i2c_algorithm 数据结构。
(3)I2C 设备驱动:I2C 设备驱动是对I2C 硬件体系结构中设备端的实现, 设备一般挂接在受CPU 控制的I2C 适配器上, 通过I2C 适配器与CPU 交换数据。I2C 设备驱动主要包含了数据结构i2c_driver 和i2c_client。
这三部分的关系如图1 所示。
图1Linux 中I2C 体系结构
2.2 I2C驱动程序中的重要数据结构
在I2C 框架的i2c.h 这个头文件中对4 个关键的结构体进行了定义, 它们分别是i2c_adapter、i2c_algorithm、i2c_driver 和i2c_client。结构体i2c_adapter 是一个I2C控制器的逻辑抽象,并且作为最核心的数据结构提供了I2C适配器的驱动。i2c_algorithm对应一套通信方法, 其封装了对一个I2C 控制器的读写操作, 并且提供的通信函数可以控制适配器上产生特定的访问周期,这套通信方法由驱动开发者来完成。i2c_driver 则是对应于一套驱动方法,用于辅助作用的数据结构,不对应任何物理实体,仅是提供了I2C 设备i2c_client 的驱动。而i2c_client 对应于真实的物理设备,描述具体设备可能的私有数据结构。
2.3I2C驱动程序中重要数据结构之间的关系
对于上述的4 个结构体来说, 其中的i2c_driver 和i2c_client 是与具体I2C 设备相关的,而i2c_adapter 和i2c_algorithm则共同构成I2C 总线适配器驱动。一个algorithm 可以适用于多个I2C 总线上的不同adapters, 但具体的每个adapter 只能对应于一个algorithm。在i2c_adapter 数据结构中设计了clients指针数组, 用于记录该总线上每个设备的i2c_client 数据结构。
另外, 定义内核中全局静态指针数组adapters 和drivers 分别记录已注册的I2C 适配器驱动和I2C 设备驱动程序。值得注意的是同一个i2c_adapter 中的不同的i2c_client 可能使用同一个i2c_driver,而分属于不同i2c_adapter 中的两个i2c_client 也可能使用同一个i2c_driver。
3 一个具体的I2C 设备驱动程序的开发
AT24C08 是由ATMEL 公司出品的一款EEPROM 存储器。
作为一个标准的I2C 设备AT24C08 有4 个块存储区, 一个块有256 个数据存储单元,整个AT24C08 具有1024 个存储单元。由于每个数据存储单元可存1 字节的数据,所以整块AT24C08 的存储能力为1KB。
3.1 I2C 设备驱动程序的一般结构及运行流程图
开发一个具体的I2C 设备驱动需要一个完整、标准的结构,而该结构的实现是通过编写两个方面的接口而完成的, 一个是用以挂接I2C adapter 层来实现对I2C 总线及I2C设备具体的访问方法, 即I2C 核心层的接口, 主要实现attach_adapter,detach_client,command 等接口函数。另一个是对用户应用层的接口, 提供用户程序访问I2C设备的接口, 包括实现open,release,read,write 以及ioctl 等标准文件操作的接口函数。下面将通过对核心层接口和应用层接口的分析来说明I2C 设备驱动程序的运行机制。图2 为I2C 设备驱动程序运行流程图(图中at 代表具体的设备AT24C08):
3.2 I2C 设备驱动的I2C 核心层接口分析
如图2 的用户空间在通过insmod 命令加载设备驱动程序时, 设备驱动将通过使用动态模块的方式加载并指向设备初始化函数at_init(),在初始化函数中使用register_chrdev()进行字符型设备的注册, 并可以通过静态和动态两种方法来申请注册到系统中的设备号。另外将调用核心i2c -core.c 中提供的i2c_add_driver()函数注册由at_driver 数据结构描述的驱动方法,该数据结构中完成了对驱动程序的标示, 并包含了两个重要的成员函数at_attach_adapter()和at_detach_client()。
在i2c_add_driver () 注册at_driver 数据结构后,at_attach_adapter()函数就会被自动调用,其遍历系统中的每个i2c 总线驱动, 探测想要访问的设备, 连接符合i2c driver 特定条件的i2c adapter,并通过i2c adapter 实现对I2C 总线及其设备的访问。
而at_attach_adapter()的功能则是依靠调用i2c-core.c 核心中的i2c_probe()函数来实现的,通过i2c_probe()函数可以认领adapter所指向的适配器上的所有合适的设备。设备可能使用的地址由addr_data 数组指出。通过设备地址每次检测到新设备后,i2c_probe()将使用它的第三个参数即回调函数初始化设备的数据结构i2c_client,并用i2c_check_functionality()确定I2C 适配器所支持的通信方法。另外再使用i2c_attach_client()知会I2C 核心系统中包含了一个新的I2C 设备。
通过rmmod 命令对设备驱动进行卸载时, 在卸载函数at_exit()中将使用i2c_del_driver(),其调用会引起与数据结构at_driver 关联的每个i2c_client 与之解除关联, 随后at_detach_client()函数也将因此而被调用,而at_detach_client()中的i2c_detach_client()又完成与i2c_attach_client()相反的过程,并使用kfree 释放由client 所占的内存。另外卸载函数at_exit()中还将使用unregister_chrdev()对字符型设备进行注销。
3.3I2C设备驱动用户应用层接口分析
在注册字符型设备时, 设备驱动中初始化了一个structfile_operations 文件操作结构体变量用于链接字符设备驱动程序和用户应用程序,在该结构中定义了一组函数指针。系统就是通过这组函数指针对AT24C08 进行具体的操作,系统首先通过设备文件的主设备号找到相应的设备驱动程序, 然后读取这个数据结构相应的函数指针,找到相关的功能函数,接着把控制权交给该函数,从而就在上层屏蔽了设备驱动的具体实现细节,提供给用户一个方便快捷的接口。该结构中的at_open(),对应于用户应用层的open()接口函数,其通过mknod 创建的设备节点对设备文件进行打开操作。而对应用户层release () 接口函数的at_release () 则负责设备文件的释放操作。file_operations 中的at_ioctl()则主要是为用户提供一些控制该AT24C08 的命令。对一块具体设备进行读写操作是编写驱动要达到目的,file_operations结构体中所指向的读写函数at_read(),at_write()完成了对AT24C08 的写入和读出操作。
就写函数而言, 在写数据之前必须先输入测试单元的起始地址, 然后再对写入的数据分配相应内存, 然后使用copy_from_user 命令把从用户空间获得的数据拷贝到内核空间,并构造I2C 消息数据,最终通过i2c-core.c 的i2c-transfer()函数进行I2C消息数组的传输,而i2c_transfer()将指向总线驱动中的算法i2c_algorithm 所对应的具体适配器的master_xfer()方法,这样就借助i2c-core.c 作为纽带连接了设备驱动和总线驱动,并完成了两者之间的通信,其运行流程如图2 的内核空间所示。
对于读函数at_read(),同样要对数据进行内存的分配,构造I2C消息,传输I2C 消息以及转换数据空间等。两者的主要区别则体现在对I2C 消息的构造上,在读出数据之前,先要写地址,根据写入的地址来寻找将要读出的数据的起始地址, 所以在读函数中就需要构造两条I2C 消息,一条用于写地址操作,另一条用于读数据操作。另外在转换数据空间时, 读函数将使用copy_to_user 把内核空间的数据拷贝到用户空间。
3.4 AT24C08 的单设备多驱动的实现方式
单设备多驱动是本文的一个创新点。设计中实现了分3 个设备驱动一对1 块AT24C08 进行操作。设备驱动1 对AT24C08的第1 个块操作,设备驱动2 对第2 个块操作,设备驱动3 对第3 和第4 个块进行操作。对块的分开操作体现在对设备地址的探测上,由于保存设备地址信息的是二元数组addr_data,所以在多驱动对单一的AT24C08 操作时就需要在该二元数组中指明每个设备驱动程序所控制的设备地址。对于控制第1 个块的设备驱动1,通过数组normal_addr 指出要进行操作的设备地址为0x50,如下所示:
static unsigned short normal_addr[]={ 0x50,I2C_CLIENT_END};
再通过其对数组addr_data 进行初始化, 这样, 设备驱动1就能检测到数组中所指出的AT24C08 的第1 个块,而跳过其他的块, 达到了只对单一特定块操作的目的。对于设备驱动2 来说, 只需把数组normal_addr 中地址改为AT24C08 的第2 个块的地址0x51 即可。同理,对设备驱动3,只需把normal_addr 中的单一地址改为两个地址即可,如下所示:
static unsigned short normal_addr [] = { 0x52,0x53, I2C_CLIENT_END};
这样就可使设备驱动只探测到后两个块,而跳过其他块,以达到对单一AT24C08 中多个块操作的目的。然后再用insmod命令加载编译好的三个.ko 驱动模块, 获得3 个不同的设备号后,接着根据所获得的设备号使用mknod 命令创建3 个不同的字符型设备节点, 最后通过用户层的3 个测试程序分别打开已创建的这3 个不同的设备节点就能分别对不同的块进行读写操作,至此就实现了单设备多驱动的控制方式。
同样除了分3 个驱动外, 驱动开发者也可以编写4 个设备驱动分别对每1 个块进行操作, 或者就只编写1 个设备驱动对4 个块一起操作,也适用于绑定非连续块进行操作,比如用一个设备驱动控制第1 和第3 个块。总之驱动开发人员可以根据不同的需要进行不同的组合方式。
3.5 AT24C08 设备驱动程序的验证与测试
设备驱动程序的验证, 需要通过用户层的测试程序来实现,测试程序如下:
fd=open("/dev/at", O_RDWR); //打开设备文件,获得设备文件的文件描述符。
scanf("%u", &start_address); //输入测试单元起始地址。
write(fd,buf,sizeof(buf)); //把以页写入方式把输入的16 个数据写入内核空间。