虽然I2C硬件体系结构比较简单,但是I2C体系结构在Linux中的实现却相当复杂.通过阐述Linux系统中I2C总线体系结构,在此基础上完成嵌入式Linux系统中I2C总线驱动的开发.

1. 嵌入式Linux中I2C驱动程序分析

I2C(Inter2IntegratedCircuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备.嵌入式系统中,微控制器通过I2C总线可随时可对各个系统中的组件进行设置和查询,以管理系统的配置或掌握组件的功能状态来控制外围设备.I2C总线因为协议成熟,引脚简单,传输速率高,支持的芯片多,并且有利于实现电路的标准化和模块化,得到了包括Linux在内的很多操作系统的支持,受到开发者的青睐.在Linux环境下使用I2C总线协议,需要理解Linux的I2C总线驱动的体系结构,在此基础上来进行嵌入式驱动程序和应用程序的开发.

1.1 Linux的I2C驱动框架

Linux内核的I2C总线驱动程序框架如图1所示:

Linux中IIC总线驱动分析

Linux的I2C体系结构分为3个组成部分:

I2C核心:I2C核心提供了I2C总线驱动和设备驱动的注册,注销方法,I2C通信方法(即"al2gorithm")上层的,与具体适配器无关的代码以及探测设备,检测设备地址的上层代码等.这部分是与平台无关的.与其对应的是Linux内核源代码中的drivers目录下的i2c2core.c.

I2C总线驱动:I2C总线驱动是对I2C硬件体系结构中适配器端的实现.I2C总线驱动主要包含了I2C适配器数据结构i2c_adapter,I2C适配器的algorithm数据结构i2c_algorithm和控制I2C适配器产生通信信号的函数,经由I2C总线驱动的代码,我们可以控制I2C适配器以主控方式产生开始位,停止位,读写周期,以及以从设备方式被读写,产生ACK等.不同的CPU平台对应着不同的I2C总线驱动,比如下文要提到的S3C2410的总线驱动i2c2s3c2410.c,它位于Linux内核源代码中的drivers目录下busses文件夹.

I2C设备驱动:I2C设备驱动是对I2C硬件

体系结构中设备端的实现.设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据.I2C设备驱动主要包含了数据结构i2c_driver和i2c_client,我们需要根据具体设备实现其中的成员函数.在Linux内核源代码中的drivers目录下的i2c2dev.c文件,实现了I2C适配器设备文件的功能,应用程序通过"i2c2%d"文件名并使用文件操作接口open(),write(),read(),ioctl()和close()等来访问这个设备.应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器并控制I2C设备的工作方式.

1.2 Linux的I2C驱动框架中的主要数据结构及其关系

Linux的I2C驱动框架中的主要数据结构包括:i2c_driver,i2c_client,i2c_adapter和i2c_algo2rithm.它们的定义在内核中的i2c.h头文件中.i2c_adapter对应于物理上的一个适配器,这个适配器是基于不同的平台的,一个I2C适配器需要i2c_algorithm中提供的通信函数来控制适配器,因此i2c_adapter中包含其使用的i2c_algorithm的指针.i2c_algorithm中的关键函数master_xfer()以i2c_msg为单位产生I2C访问需要的信号.不同的平台所对应的master_xfer()是不同的,开发人员需要根据所用平台的硬件特性实现自己的xxx_xfer()方法以填充i2c_algorithm的master_xfer指针.

i2c_driver对应一套驱动方法,不对应于任何的物理实体.i2c_client对应于真实的物理设备,每个I2C设备都需要一个i2c_client来描述.i2c_client依附于i2c_adpater,这与I2C硬件体系中适配器和设备的关系一致.i2c_driver提供了i2c2cli2ent与i2c2adapter产生联系的函数.当attach_a2dapter()函数探测物理设备时,如果确定存在一个client,则把该client使用的i2c_client数据结构的adapter指针指向对应的i2c_adapter,driver指针指向该i2c_driver,并调用i2c_adapter的client_register()函数来注册此设备.相反的过程发生在i2c_driver的detach_client()函数被调用的时候.

1.3 Linux的I2C体系结构中三个组成部分的作用

I2C核心提供了一组不依赖于硬件平台的接口函数,I2C总线驱动和设备驱动之间依赖于I2C核心作为纽带.I2C核心提供了i2c_adapter的增加和删除函数,i2c_driver的增加和删除函数,i2c_client的依附和脱离函数以及i2c传输,发送和接收函数,i2c传输函数i2c_transfer()用于进行I2C适配器和I2C设备之间的一组消息交互i2c_mas2ter_send()函数和i2c_master_recv()函数内部会调用i2c_transfer()函数分别完成一条写消息和一条读消息.

I2C总线驱动包括I2C适配器驱动加载与卸载以及I2C总线通信方法.其中I2C适配器驱动加载(与卸载)要完成初始化(释放)I2C适配器所使用的硬件资源,申请I/O地址,中断号,通过i2c_add_adapter()添加i2c_adapter的数据结构(通过i2c_del_adapter()删除i2c_adapter的数据结构)的工作.I2C总线通信方法主要对特定的I2C适配器实现i2c_algorithm的master_xfer()方法来实现i2c_msg的传输.不同的适配器对应的master_xfer()方法由其处理器的硬件特性决定.I2C设备驱动主要用于I2C设备驱动模块加载与卸载以及提供I2C设备驱动文件操作接口.I2C设备驱动的模块加载通用的方法遵循以下流程:首先通过register_chrdev()将I2C设备注册为一个字符设备,然后利用I2C核心中的i2c_add_a2dapter()添加i2c_driver.调用i2c_add_adapter()过程中会引发i2c_driver结构体中的yyy_attach_adapter()的执行,它通过调用I2C核心的i2c_probe()实现物理设备的探测.i2c_probe()会引发yyy_detect()的调用.yyy_detect()中会初始化i2c_client,然后调用内核的i2c_attach_client()通知I2C核心此时系统中包含了一个新的I2C设备.之后会引发I2C设备驱动中yyy_init_client()来初始化设备.卸载过程执行相反的操作.

I2C设备驱动模块加载与卸载的流程如图2所示:

Linux中IIC总线驱动分析

综上所述,对于特定的嵌入式Linux操作系统,由于I2C核心是不依赖硬件平台的,所以开发的主要工作在于特定平台的总线驱动的开发以及特定设备的驱动开发.

2. 实例:基于S3C2410的I2C设备驱动开发与应用

2.1 S3C2410I2C总线驱动设计

S3C2410处理器内部集成了一个I2C控制器,通过控制寄存器IICCON,IICDS和IICADD可在I2C总线线产生开始位,停止位,数据和地址,而传输的状态则通过IICSTAT寄存器获取.S3C2410处理器内部集成的I2C控制器可支持主,从两种模式,我们主要使用主模式.

一般来说,I2C总线驱动通常需要两个模块来描述,即一个由具体平台决定的适配器模块和这个适配器相对应的I2C总线通信方法.根据2.1中对I2C设备驱动的描述,针对S3C2410的I2C总线驱动设计主要要实现的工作包括设计S3C2410适配器的模块加载卸载函数i2c_adap_s3c_init(),i2c_adap_s3c_exit()以及S3C2410适配器的通信方法函数s3c24xx_i2c_xfer().

I2C适配器驱动作为一个单独的模块被加载进内核,这个过程只需要注册一个device_driver结构体,device_driver结构体包含了具体适配器的probe()函数,remove()函数,resume()函数指针等信息,它需要被定义和赋值.当通过device_regis2ter()函数注册device_driver结构体时,probe指针指向的s3c24xx2i2c_probe()函数将被调用,以初始化适配器硬件.然后通过I2C核心提供的i2c_add_adapter()函数添加这个适配器.下面进行详细的说明:device_driver结构体的定义如下:

 1 static struct device_driver s3c2410_i2c_driver={
 2     .name=“s3c2410-i2c”,
 3 
 4     .bus=&platform_bus_type,
 5 
 6     .probe=s3c24xx_i2c_probe,
 7 
 8     .remove=s3c24xx_i2c_remove,
 9 
10     .resume=s3c24xx_i2c_resume,
11 };

其中s3c24xx_i2c_probe()用来使能硬件并申请I2C适配器使用的I/O地址,中断号等,在这些工作都完成无误后,通过I2C核心提供的i2c_add_a2dapter()函数添加这个适配器.与s3c24xx_i2c_probe()函数完成相反功能的函数是s3c24xx_i2c_remove()函数,它在适配器模块卸载函数调用de2vice_unregister()函数时通过device_driver的re2move指针被调用.

在上述过程中用到了s3c24xx_i2c结构体进行适配器所有信息的封装.它类似于私有信息结构体,下面代码是一个由s3c2410总线驱动模块定义的一个s3c24xx_i2c结构体全局实例:

 1 static struct s3c24xx_i2c s3c24xx_i2c={
 2 
 3     .lock=SPIN_LOCK_UNLOCKED,/*自旋锁未锁定*/
 4 
 5     .wait=__WAIT_QUEUE_HEAD_INITIALIZER
 6 
 7     (
 8 
 9         s3c24xx_i2c.wait
10 
11     ),/*等待队列初始化*/
12 
13     .adap={
14 
15     .name=“s3c2410-i2c”,
16 
17     .owner=THIS_MODULE,
18 
19     .algo=&s3c24xx_i2c_algorithm,
20 
21     .retries=2,
22 
23     .class=I2C_CLASS_HWMON,
24 
25     },
26 
27 };

上述代码指定了S3C2410I2C总线通信传输函数s3c24xx_i2c_xfer(),它依赖于s3c24xx_i2c_doxfer()函数和s3c24xx_i2c_message_start()函数,上述三个函数结合s3c24xx_i2c_irq()和i2s_s3c_irq_nextbyte()函数共同完成algorithm中的master_xfer功能.具体的,master_xfer指针指向的s3c24xx_i2c_xfer()函数调用s3c24xx_i2c_doxfer()函数传输I2C消息.s3c24xx_i2c_doxfer()首先将S3C2410的I2C适配器设置为I2C主设备,其后初始化s3c24xx_i2c结构体,使能I2C中断并调用s3c24xx_i2c_message_start()函数启动I2C消息传输,s3c24xx_i2c_message_start()函数写S3C2410适配器对应的控制寄存器向I2C从设备传递开始位和从设备地址.具体通信过程可参照S3C2410数据手册的I2C总线部分.此外,master_xfer()功能的完整实现需要借助I2C适配器上的中断函s3c24xx_i2c_irq()以及i2s_s3c_irq_nextbyte()函数来步步推进.通过上述流程,可实现i2c_msg消息数组的传输.由上述讨论可以看出,在进行I2C设备读写时是以i2c_msg为单位进行的,i2c_msg结构体在I2C总线体系中是一个十分重要的结构,下面给出其定义:

1 struct i2c_msg{
2     __u16addr;/*设备地址*/
3 
4     __u16flags;/*标志*/
5 
6     __u16len;/*消息长度*/
7 
8     __u83buf;/*消息数据*/
9 };

2.2 S3C2410I2C设备驱动设计

一个具体的I2C设备驱动需要实现两个方面的接口.一个是对I2Ccore层的接口,用于挂载I2Cadapter层来实现对I2C总线及I2C设备具体的访问方法,包括要实现attach_adapter,detach_a2dapter,detach_client,command等接口函数.另一个是用户应用层的接口,提供用户程序访问I2C设备的接口,包括实现open, release,read,write以及ioctl等标准文件操作接口函数.

对I2Ccore层的接口函数的具体功能解释如下:

attach_adapter:I2C驱动在调用I2C_add_driver()注册时,对发现的每一个I2Cadapter(对应一条I2C总线)都要调用该函数,检查该I2Ca2dapter是否符合I2Cdriver的特定条件,如果符合条件则连接此I2Cadapter,并通过I2Cadapter来实现对I2C总线及I2C设备的访问。detach_a2dapter实现相反的功能.

detach_client:I2Cdriver在删除一个I2C device时调用该函数,清除描述这个I2Cdevice的数据结构,这样以后就不能访问该设备了.

command:针对设备的特点,实现一系列的子功能,是用户接口中的ioctl功能的底层实现.

在驱动中必须实现一个structi2c_driver的数据结构,并在驱动模块初始化时向I2Ccore注册一个I2C驱动,并完成对I2Cadapter的相关操作.其代码为:

 1 static struct i2c_driver i2cdev_driver={
 2     .owner=THIS_MODULE,
 3 
 4     .name=“dev_driver”,
 5 
 6     .id=I2C_DRIVERID_I2CDEV,
 7 
 8     .flags=I2C_DF_NOTIFY,
 9 
10     .attach_adapter=i2cdev_attach_adapter,
11 
12     .detach_adapter=i2cdev_detach_adapter,
13 
14     .detach_client=i2cdev_detach_client,
15 
16     .command=i2cdev_command,
17 };

以上几个接口函数使设备驱动程序实现了对I2C总线及I2Cadpater的挂接,因此就可以通过I2Ccore提供的对I2C总线读写访问的通用接口来实现设备驱动程序对用户应用层的接口函数.

为实现对用户应用层的接口,在I2C设备驱动中需要实现一个structfile_operations的数据结构,并向内核注册为一个字符类型的设备.

 1 static struct file_operations i2cdev_fops={
 2     .owner=THIS_MODULE,
 3 
 4     .read=i2cdev_read,
 5 
 6     .write=i2cdev_write,
 7 
 8     .ioctl=i2cdev_ioctl,
 9 
10     .open=i2cdev_open,
11 
12     .release=i2cdev_release,
13 };

数据结构i2cdev_fops中的i2cdev_open和i2cdev_release对应file_operations中的open和release,分别用来打开和关闭设备.i2cdev_ioctl对应file_operations中的ioctl,对用户提供的一系列控制设备的具体命令,如:I2C_SLAVE(设置从设备地址),I2C_RETRIES(没有收到设备ACK情况下的重试次数,缺省为1),I2C_TIMEOU(超时)以及I2C_RDWR.i2cdev_read对应file_opera2tions中的read,实现从字符设备到用户空间读数据.i2cdev_write对应file_operations中的write,实现从用户空间到字符设备写数据.实际上由于i2c_dev.c已经实现了I2C设备的文件操作接口,所以开发者只需要实现i2c_driver结构即可.

下面以S3C2410I2C设备的读过程中的函数调用过程进行图示(如图3)来进一步表明在实际的嵌入式Linux系统中I2C驱动体系三个组成部分即I2C核心,I2C总线驱动以及I2C设备驱动之间的关系(写过程类似).

Linux中IIC总线驱动分析

2.3 S3C2410I2C设备驱动应用

本部分主要涉及ARM端I2C总线驱动及应用程序的开发,来实现ARM与FPGA的数据交换.由于AT24C08的操作时序与项目需求近似,故FPGA从端暂时通过利用AT24C08(8K串行E2PROM)模拟,而FPGA端在后期进行开发.

通过以上对研究目的的描述,这部分的主要工作是利用3.2提到的I2C设备驱动中提供的接口read,write及ioctl来实现对I2C设备的读写,从图3可以看出,i2cdev_read(),i2cdev_write()读写函数分别调用I2C核心的i2c_master_recv()和i2c_master_send()函数来构造1条I2C消息并引发适配器algorithm通信函数的调用,完成消息的传输,他们的操作时序如图4所示.

Linux中IIC总线驱动分析

通过上面的时序不难看出这两个读写函数在一个读写周期内只实现了1条i2c_msg的传输,这样是无法实现对复杂的I2C从设备的读寄存器过程的(详见下文中的AT24C08).目前大多数的I2C设备都要求在一个读写周期内至少实现2条以上的i2c_msg的传输,即I2C总线协议中的Repstart模式,其操作时序如图5所示.

Linux中IIC总线驱动分析

由于i2cdev_read()和i2cdev_write()适用于非RepStart模式的情况.对于2条以上消息组成的读写,在用户空间需要组织i2c_msg消息数组并调用I2C_RDWRIOCTL命令来实现.应用程序需要通过i2c_rdwr_ioctl_data结构体来给内核传递I2C消息,其定义如下:

1 struct i2c_rdwr_ioctl_data{
2     struct i2c_msg__user3msgs;//pointerstoi2c_msgs
3 
4     __u32 nmsgs;//numberofi2c_msgs
5 };

通过在用户空间赋值由i2c_rdwr_ioctl_data结构体成员msgs指针指向的I2C消息数组来实现从设备的寻址,读写控制以及传输的数据.然后调用i2cdev_ioctl()函数的I2C_RDWR命令来完成i2c_msg从用户空间向内核空间的传送.由于传送的是i2c_msg消息数组,所以可以实现多个I2C消息的传输,从而实现了Repstart模式.下面就AT24C08的操作时序给出使用S3C2410I2C总线对AT24C08从设备的读写过程.

AT24C08是一个1024*8位串行CMOSEE2PROM,内部含有1024个8位字节,它通过I2C总线接口进行操作.通过使用它来模拟FPGA作为I2C从设备获取I2C总线上的数据并存储到内部存储器中.AT24C08的器件地址为0x50,它的写操作共有以下两种方式:

字节写:在此模式下,主器件发送起始命令和从器件地址信息以及读写bit给从器件,从器件产生ACK应答后,主器件发送字节地址,当再次收到ACK应答后,主器件发送数据到被寻址的存储单元.从器件再次应答,并在主器件产生停止信号后开始内部数据的擦写.

页写:此模式下,AT24C08可以一次写入16个字节的数据.页写操作的启动和字节写一样,不同在于传送了一个字节数据后并不产生停止信号.主器件被允许发送15个额外的字节.每发送一个字节数据后AT24C08产生一个应答位并将字节地址位加1.

At24C08的读操作共有以下三种方式:

立即地址读:AT24C08的地址计数器内容为最后操作字节的地址加1.即如果上次读写操作地址为N,则立即读的地址从N+1开始.如果N=1023,则计数器就翻转到0其继续输出数据.AT24C08接收到从器件地址信号后,发送ACK应答,然后发送一个8位字节数据.主器件不需发送ACK,但要产生一个停止信号.

选择性读:此模式允许主器件对寄存器的人一字节进行读操作.主器件首先通过发送起始信号,从器件地址和它想读取的字节数据的地址执行一个伪写操作.当AT24C08产生ACK后,主器件重新发送起始信号和从器件地址,此时R/W位置1,AT24C08响应并发送ACK,然后输出所要求的一个位字节数据.主器件不发送但是要产生一8ACK个停止信号.

连续读:此模式由立即读或选择性读操作启动.在AT24C08发送完一个8字节数据后,主器件产生一个应答信号告知AT24C08主器件需要更多的数据.对主机产生的每个ACK,AT24C08都发送一个8位数据字节.当主器件不发送ACK而发送停止位时操作结束.

根据AT24C08I2C的操作时序以及上文对设备读写时序的描述,下面给出对AT24C08写操作时序图示及实现的部分代码:

Linux中IIC总线驱动分析

对字节写和页写两种写操作,由于只需要一条I2C消息传输,所以既可以通过i2cdev_write()函数实现,也可以使用i2cdev_ioctl()函数的I2C_RDWR命令实现.第一种方式主要进行的操作如下:

 1 fd=open(“/dev/i2c/0”,O_RDWR);/*打开i2c设备*/
 2 
 3 ioctl(fd,I2C_SLAVE,0x50);/*设置eeprom地址*/
 4 
 5 ioctl(fd,I2C_TIMEOUT,1);/*设置超时*/
 6 
 7 ioctl(fd,I2C_RETRIES,1);/*设置重试次数*/
 8 
 9 buf[0]=byte_address;/*byte_address赋给buf[0]*/
10 
11 for(i=1;i<=MSG_LEN;i++)
12 
13     buf[i]=data_i;/*将要写的数据赋给buf[i]*/
14 
15 for(j=0;j<=MSG_LEN+1;j++)
16 
17     write(fd,buf[j],1);/*调用write函数发送数据*/
18 
19 close(fd);/*关闭i2c设备*/

字节写的情况下MSG_LEN=1,页写情况下MSG_LEN=16.第二种方式主要进行的操作如下:

 1 struct i2c_rdwr_ioctl_data msgtable;/*定义结构体*/
 2 
 3 fd=open(“/dev/i2c/0”,O_RDWR);/*打开i2c设备*/
 4 
 5 msgtable.nmsgs=1;/*消息数量*/
 6 
 7 msgtable.msgs[0].addr=slave_address;
 8 
 9 msgtable.msgs[0].buf[0]=byte_address;
10 
11 msgtable.msgs[0].len=DAT_LEN+1;/*byteaddress+data*/
12 
13 msgtable.msgs[0].flags=0;/*write mode*/
14 
15 ioctl(fd,I2C_TIMEOUT,1);/*设置超时*/
16 
17 ioctl(fd,I2C_RETRIES,1);/*设置重试次数*/
18 
19 ioctl(fd,I2C_RDWR,&msgtable);/*使用I2C_RDWR命令传输I2C消息*/
20 
21 close(fd);/*关闭i2c设备*/

对读操作的三种模式,对于立即读模式,从描述可以看出只需要1条I2C消息传输,故可使用i2cdev_read()就可以简单实现,但是这种操作由于很少使用应用价值不大.对于选择性读操作由于要先进行字节寻址,所以读操作时序中需要首先实现一个写字节地址的过程,这需要两条I2C消息实现,即Repstart模式.所以这里必须使用I2C_RDWR命令.下面给出选择性读操作的时序及实现的部分代码:

Linux中IIC总线驱动分析

 1 struct i2c_rdwr_ioctl_data msgtable;/*定义结构体*/
 2 
 3 fd=open(“/dev/i2c/0”,O_RDWR);/*打开i2c设备*/
 4 
 5 msgtable.nmsgs=2;/*消息数量*/
 6 
 7 msgtable.msgs[0].addr=slave_address;
 8 
 9 msgtable.msgs[0].buf[0]=byte_address;
10 
11 msgtable.msgs[0].len=1;
12 
13 msgtable.msgs[0].flags=0;/*write mode*/
14 
15 msgtable.msgs[1].addr=slave_address;
16 
17 msgtable.msgs[1].buf=sbuf;
18 
19 msgtable.msgs[1].len=SBUF_LEN;
20 
21 msgtable.msgs[1].flags=1;/*read mode*/
22 
23 ioctl(fd,I2C_TIMEOUT,1);/*设置超时*/
24 
25 ioctl(fd,I2C_RETRIES,1);/*设置重试次数*/
26 
27 ioctl(fd,I2C_RDWR,&msgtable);/*使用I2C_RDWR命令传输I2C消息*/
28 
29 close(fd);/*关闭i2c设备*/

对于基于选择性读的连续读的情况,只需要将第二条I2C消息的buf数组长度设为需要读出字节的长度即可.通过上述操作,便可以通过S3C2410的I2C总线对EEPROM进行读写.下一步只需利用verilog语言对FPGA进行编程来实现I2C从端的功能,模仿上述AT24C08的操作时序并将寄存器内的指令合理的分配给其它设备.

3. 结束语

嵌入式Linux中I2C2core框架提供了统一的,不需要修改的接口.而驱动程序开发者只需要实现特定的I2C总线适配器驱动和I2C设备驱动便可以进行用户空间的应用程序设计.本文阐述了Linux系统中I2C总线体系结构,并基于项目要求通过具体例子给出了基于S3C2410处理器利用I2C总线与I2C设备通信的用户空间的实现.

本文转自:道客巴巴《嵌入式Linux系统中I2C总线驱动的研究与应用》篇