- 共享资源,欢迎转载:http://hbhuanggang.cublog.cn
一、开发环境
- 主 机:VMWare--Fedora 9
- 开发板:Mini2440--64MB Nand, Kernel:2.6.30.4
- 编译器:arm-linux-gcc-4.3.2
二、相关概念
1、平台设备:
通常在Linux中,把SoC系统中集成的独立外设单元(如:I2C、IIS、RTC、看门狗等)都被当作平台设备来处理。在Linux中用platform_device结构体来描述一个平台设备,在2.6.30.4内核中定义在:include/linux/platform_device.h中,如下:
struct platform_device_id *id_entry; |
现在你不必深入理解这个结构体,只要知道在Linux中是用这个结构体来定义一些平台设备的。比如在:arch/arm/plat-s3c24xx/devs.c中就定义了很多平台设备,下面我就只贴出RTC这一种的:
struct platform_device s3c_device_rtc = { //定义了RTC平台设备 EXPORT_SYMBOL(s3c_device_rtc); |
好了,定义了平台设备,那系统是怎么来使用他的呢?我们打开:arch/arm/mach-s3c2440/mach-smdk2440.c这个 ARM 2440平台的系统入口文件,可以看到在系统初始化函数smdk2440_machine_init中是使用platform_add_devices这 个函数将一些平台设备添加到系统中的,如下:(至于系统是如何实现添加平台设备的,这里我们不必研究,这些Linux系统都已经做好了的,我们要研究的是 后面平台设备的驱动是如何实现的)
static void __init smdk2440_machine_init(void) |
2、平台设备驱动:
这里所讲的平台设备驱动是指具体的某种平台设备的驱动,比如上面讲的RTC平台设备,这 里就是指RTC平台设备驱动。在Linux中,系统还为平台设备定义了平台驱动结构体platform_driver,就好比系统为字符设备定义了 file_operations一样,但不要把平台设备跟字符设备、块设备、网络设备搞成了并列的概念,因平台设备也可以是字符设备等其他设备。注意:在 被定义为平台设备的字符设备的驱动中,除了要实现字符设备驱动中file_operations的open、release、read、write等接口 函数外,还要实现平台设备驱动中platform_driver的probe、remove、suspend、resume等接口函数。好了,在我们搞明 白上面这些后,下面我们就来具体详细分析讲解RTC平台设备的驱动现实。
三、实例讲解
1、RTC在Linux中的整体结构:
就个人理解,RTC在Linux中整体结构分为两个部分。第一个是部分就是上面所讲的作为平台设备被 挂接到系统总线中,这里我把他叫做设备层(呵呵,可能不是很准确的叫法);第二部分就是驱动部分,这里叫做驱动层。在Linux中要使一个驱动在不同的平 台中都能够使用似乎是不可能的,所以我们先看2.6.30.4内核驱动中的RTC部分是单独的一个文件夹,在文件夹中包含了很多不同体系结构的RTC驱 动,当然也有S3C2440的RTC驱动,然而在这些驱动中他们都使用了一组文件里面的方法,那么这组文件就是RTC的核心(注意这里的核心不是指对 RTC硬件的操作,指的是对RTC操作的方法。对硬件寄存器的操作还是在具体的驱动中)。好了,我们还是用图来说明这种关系吧!!
2、RTC硬件原理图分析:以下是S3C2440AL内部集成的RTC模块结构图和一个外部的晶振接口图
我们从S3C2440内部RTC模块结构图和数据手册得知,RTC在Linux中主要实现两种功能,分别是系统掉电后的时间日期维持和时间日期报警(类似定时器功能)。
①、时间日期维持功能:
主 要是由RTC实时时钟控制寄存器RTCCON进行功能的使能控制,由节拍时间计数寄存器TICNT来产生节拍时间中断来实现实时操作系统功能相关的时间和 实时同步。其中对时间日期的操作实际上是对BCD码操作,而BCD码则是由一系列的寄存器组成(BCD秒寄存器BCDSEC、BCD分寄存器 BCDMIN、BCD小时寄存器BCDHOUR、BCD日期寄存器BCDDATE、BCD日寄存器BCDDAY、BCD月寄存器BCDMON、BCD年寄存器BCDYEAR)。
②、报警功能:
主要由RTC报警控制寄存器RTCALM进行功能使能控制,并产生报警中断。报警时间日期的设置也是对一系列的寄存器进行操作(报警秒数据寄存器 ALMSEC、报警分钟数据寄存器ALMMIN、报警小时数据寄存器ALMHOUR、报警日期数据寄存器ALMDATE、报警月数据寄存器ALMMON、 报警年数据寄存器ALMYEAR)。
3、RTC驱动实现步骤(建立驱动文件my2440_rtc.c):
注意:在每步中,为了让代码逻辑更加有条理和容易理解,就没有考虑代码的顺序,比如函数要先定义后调用。如果要编译此代码,请严格按照C语言的规范来调整代码的顺序。
①、依然是驱动程序的最基本结构:RTC驱动的初始化和退出部分及其他,如下:
/*RTC平台驱动结构体,平台驱动结构体定义在platform_device.h中,该结构体内的接口函数在第②、④步中实现*/ static int __init rtc_init(void) static void __exit rtc_exit(void) module_init(rtc_init); MODULE_LICENSE("GPL"); |
②、RTC平台驱动结构中探测函数rtc_probe的实现。探测就意味着在系统总线中去检测设备的存在,然后获取设备有用的相关资源信息,以便我们使用这些信息。代码如下:
/*定义了一个用来保存RTC的IO端口占用的IO空间和经过虚拟映射后的内存地址*/ /*定义了两个变量来保存RTC报警中断号和TICK节拍时间中断号,NO_IRQ宏定义在irq.h中*/ /*申明并初始化一个自旋锁rtc_pie_lock,对RTC资源进行互斥访问*/ /*RTC平台驱动探测函数,注意这里为什么要使用一个__devinit,也到rtc_remove实现的地方一起讲*/ /*在系统定义的RTC平台设备中获取RTC报警中断号 //在系统定义的RTC平台设备中获取TICK节拍时间中断号 /*获取RTC平台设备所使用的IO端口资源,注意这个IORESOURCE_MEM标志和RTC平台设备定义中的一致*/ /*申请RTC的IO端口资源所占用的IO空间(要注意理解IO空间和内存空间的区别), /*将RTC的IO端口占用的这段IO空间映射到内存的虚拟地址,ioremap定义在io.h中。 /*好了,通过上面的步骤已经将RTC的资源都准备好了,下面就开始使用啦*/ /*这两个函数开始对RTC寄存器操作,定义都在下面*/ /*device_init_wakeup该函数定义在pm_wakeup.h中,定义如下: /*将RTC注册为RTC设备类,RTC设备类在RTC驱动核心部分中由系统定义好的, /*设置RTC节拍时间计数寄存器TICNT的节拍时间计数值的用户最大相对值, /*将RTC设备类的数据传递给系统平台设备。 return 0; //以下是上面错误处理的跳转点 err_nomap: err_nores: /*该函数主要是初始化或者使能RTC, tmp = readb(rtc_base + S3C2410_TICNT); /*读取TICNT寄存器的值*/ if ((readb(rtc_base + S3C2410_RTCCON) & S3C2410_RTCCON_CNTSEL)) if ((readb(rtc_base + S3C2410_RTCCON) & S3C2410_RTCCON_CLKRST)) /*该函数主要是对RTC的节拍时间计数寄存器TICNT的0-6位进行操作,即:节拍时间计数值的设定*/ if (!is_power_of_2(freq)) /*对freq的值进行检查*/ spin_lock_irq(&rtc_pie_lock); /*获取自旋锁保护临界区资源*/ /*读取节拍时间计数寄存器TICNT的值*/ /*看数据手册得知,节拍时间计数值的范围是1-127, /*将经运算后值写入节拍时间计数寄存器TICNT中,这里主要是改变TICNT的第0-6位的值*/ spin_unlock_irq(&rtc_pie_lock);/*释放自旋锁,即解锁*/ return 0; |
③、RTC设备类的操作。在这一步中,才是对RTC硬件的各种寄存器进行操作,代码如下:
/*rtc_class_ops是RTC设备类在RTC驱动核心部分中定义的对RTC设备类进行操作的结构体, /*RTC设备类打开接口函数*/ /*这里主要的目的是从系统平台设备中获取RTC设备类的数据,和RTC探测函数rtc_probe中 /*申请RTC报警中断服务,中断号rtc_alarmno在RTC探测函数rtc_probe中已经获取得, /*同上面一样,这里申请的是RTC的TICK节拍时间中断服务,服务程序是:rtc_tickirq*/ return ret; tick_err:/*错误处理,注意出现错误后也要释放掉已经申请成功的中断*/ /*RTC报警中断服务程序*/ /*当报警中断到来的时候,去设定RTC中报警的相关信息,具体设定的方法,RTC核心 /*RTC的TICK节拍时间中断服务*/ /*节拍时间中断到来的时候,去设定RTC中节拍时间的相关信息,具体设定的方法,RTC核心 /*RTC设备类关闭接口函数*/ /*请见rtc_setpie接口函数中的解释*/ /*同rtc_open中中断的申请相对应,在那里申请中断,这里就释放中断*/ /*该函数主要是对RTC的节拍时间计数寄存器TICNT的第7位进行操作,即:节拍时间计数的使能功能*/ spin_lock_irq(&rtc_pie_lock);/*获取自旋锁保护临界区资源*/ /*读取节拍时间计数寄存器TICNT的值*/ if (flag) /*将经运算后值写入节拍时间计数寄存器TICNT中,这里主要是改变TICNT的第7位的值*/ spin_unlock_irq(&rtc_pie_lock);/*释放自旋锁,即解锁*/ return 0; /*读取RTC中BCD数中的:分、时、日期、月、年、秒*/ retry_get_time: /*我们知道时间是以60为一个周期的,当时、分、秒达到60后,他们的上一级会加1,而自身又从0开始计数 /*将上面读取的时间日期值保存到RTC核心定义的时间结构体中,该结构体定义在rtc.h中, /*这里为什么要加100年和减1月呢,我们查看数据手册得知原来是为了区别1900年和2000年闰年的因素, return 0; /*和上面的rtc_gettime功能相反,将更改后的分、时、日期、月、年、秒写入RTC中BCD数中*/ /*RTC时钟只支持100年的时间范围*/ /*将上面保存到RTC核心定义的时间结构体中的时间日期值写入对应的寄存器中*/ return 0; /*读取RTC中报警各寄存器的:秒、分、时、月、日期、年的值,保存各值到rtc_time结构体中*/ alm_tm->tm_sec = readb(rtc_base + S3C2410_ALMSEC); /*获取RTC报警控制寄存器RTCALM的值*/ /*判断RTCALM值的第6位,来设置RTC的全局报警使能状态到RTC核心定义的报警状态结构体rtc_wkalrm中*/ /*判断如果RTCALM值的第0位的值(秒报警使能)为1时,就设置报警秒的值到rtc_time结构体中*/ /*判断如果RTCALM值的第1位的值(分报警使能)为1时,就设置报警分的值到rtc_time结构体中*/ /*判断如果RTCALM值的第2位的值(时报警使能)为1时,就设置报警小时的值到rtc_time结构体中*/ /*判断如果RTCALM值的第3位的值(日期报警使能)为1时,就设置报警日期的值到rtc_time结构体中*/ /*判断如果RTCALM值的第4位的值(月报警使能)为1时,就设置报警月的值到rtc_time结构体中*/ /*判断如果RTCALM值的第5位的值(年报警使能)为1时,就设置报警年的值到rtc_time结构体中*/ return 0; /*把上面保存到rtc_time结构体中各值写入RTC中报警各寄存器中*/ /*读取RTC报警控制寄存器RTCALM的第6位,把全局报警使能的状态保存到alrm_en变量中*/ /*把RTC报警控制寄存器RTCALM的值设为0,即将全局报警使能和其他报警使能全部关闭*/ if (tm->tm_sec < 60 && tm->tm_sec >= 0) if (tm->tm_min < 60 && tm->tm_min >= 0) if (tm->tm_hour < 24 && tm->tm_hour >= 0) /*把alrm_en修改过后的值重新写入RTC报警控制寄存器RTCALM中*/ /*请看下面rtc_setaie函数实现部分*/ /*根据全局报警使能的状态来决定是唤醒RTC报警中断还是睡眠RTC报警中断*/ return 0; /*这里主要还是控制RTC报警控制寄存器RTCALM的第6位(全局报警使能状态)*/ tmp = readb(rtc_base + S3C2410_RTCALM) & ~S3C2410_RTCALM_ALMEN; if (flag)/*根据标志flag来使能或禁止全局报警*/ writeb(tmp, rtc_base + S3C2410_RTCALM); |
④、RTC平台驱动的设备移除、挂起和恢复接口函数的实现,代码如下:
platform_set_drvdata(dev, NULL); /*清空平台设备中RTC驱动数据*/ rtc_setpie(&dev->dev, 0); /*禁止RTC节拍时间计数寄存器TICNT的使能功能*/ iounmap(rtc_base); /*释放RTC虚拟地址映射空间*/ return 0; /*对RTC平台设备驱动电源管理的支持。CONFIG_PM这个宏定义在内核中, static int ticnt_save; /*定义一个变量来保存挂起时的TICNT值*/ /*RTC平台驱动的设备挂起接口函数的实现*/ rtc_enable(pdev, 0); /*挂起了之后就禁止RTC控制使能*/ return 0; /*RTC平台驱动的设备恢复接口函数的实现*/ writeb(ticnt_save, rtc_base + S3C2410_TICNT); /*恢复挂起时的TICNT值,RTC节拍时间继续计数*/ return 0; #else /*配置内核时没选上电源管理,RTC平台驱动的设备挂起和恢复功能均无效,这两个函数也就无需实现了*/ |
好了,到此RTC驱动程序编写完成了。在这里不知大家有没有留意,在前面的概念部分中我们讲到过,如果把一个字符设备注册成为一个平台设备,除了要 实现平台设备驱动中platform_driver的接口函数外,还要实现字符设备驱动中file_operations的接口函数,但是从上面的驱动代 码中看,这里并没有对RTC进行file_operations的操作,这是怎么回事啊?原来对RTC进行file_operations的操作在RTC 的核心部分已经由系统提供了。在第② 步的探测函数rtc_probe中,首先用rtc_device_register注册为RTC设备类,我们看rtc_device_register的 实现(在class.c中),又调用了rtc_dev_prepare(rtc),其实现在rtc-dev.c中,那么在这里面才对RTC进行了 file_operations操作,对RTC驱动的设备号也在rtc-dev.c中处理的。
四、回过头再来分析理解具体RTC驱动程序代码的结构
在上面的各步骤中,我已对RTC驱动程序的每行代码的作用都做了详细的讲述,但到结尾部分后,也许你会有点找不着北的感觉。的确,整个代码太长,而 且用文字的方式也确实很难把整个驱动的结构描述清晰。下面,我就用图形的方式来概括上面各步骤之间的关系,使整个驱动程序的结构更加清晰明了。
五、结束语
通过对RTC驱动的实现,我们对平台设备有了进一步的了解,这对我们以后编写I2C、IIS、看门狗等设备的驱动有了很大的帮助。另外,可以看出在对具体硬件操作的时候实际是对该硬件的各种寄存器进行访问读写,所以这就要求我们在编写一个设备驱动之前必须先了解该设备的数据手册,列出所有的寄存器及功能,这样才能在编写驱动时正确的对设备进行访问。