Linux操作系统内核对RTC的编程详解

Linux内核对RTC的编程

MC146818 RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上产生周期性的中断,中断的频率在2HZ~8192HZ之间。与MC146818 RTC对应的设备驱动程序实现在include/linux/rtc.h和drivers/char/rtc.c文件中,对应的设备文件是/dev/rtc(major=10,minor=135,只读字符设备)。因此用户进程可以通过对她进行编程以使得当RTC到达某个特定的时间值时激活IRQ8线,从而将RTC当作一个闹钟来用。

而Linux内核对RTC的唯一用途就是把RTC用作“离线”或“后台”的时间与日期维护器。当Linux内核启动时,它从RTC中读取时间与日期的基准值。然后再运行期间内核就完全抛开RTC,从而以软件的形式维护系统的当前时间与日期,并在需要时将时间回写到RTC芯片中。

Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h头文件中分别定义了mc146818 RTC芯片各寄存器的含义以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口则声明在include/linux/rtc.h头文件中。

7.2.1 RTC芯片的I/O端口操作

Linux在include/asm-i386/mc146818rtc.h头文件中定义了RTC芯片的I/O端口操作。端口0x70被称为“RTC端口0”,端口0x71被称为“RTC端口1”,如下所示:

 

      
      #ifndef RTC_PORT 
#define RTC_PORT(x) (0x70 + (x)) 
#define RTC_ALWAYS_BCD 1 /* RTC operates in binary mode */ 
#endif

显然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。

端口0x70被用作RTC芯片内部寄存器的地址索引端口,而端口0x71则被用作RTC芯片内部寄存器的数据端口。再读写一个RTC寄存器之前,必须先把该寄存器在RTC芯片内部的地址索引值写到端口0x70中。根据这一点,读写一个RTC寄存器的宏定义CMOS_READ()和CMOS_WRITE()如下:

 

      
      #define CMOS_READ(addr) ({ / 
outb_p((addr),RTC_PORT(0)); / 
inb_p(RTC_PORT(1)); / 
}) 
#define CMOS_WRITE(val, addr) ({ / 
outb_p((addr),RTC_PORT(0)); / 
outb_p((val),RTC_PORT(1)); / 
}) 
#define RTC_IRQ 8

在上述宏定义中,参数addr是RTC寄存器在芯片内部的地址值,取值范围是0x00~0x3F,参数val是待写入寄存器的值。宏RTC_IRQ是指RTC芯片所连接的中断请求输入线号,通常是8。

 







7.2.2 对RTC寄存器的定义

Linux在include/linux/mc146818rtc.h这个头文件中定义了RTC各寄存器的含义。

(1)寄存器内部地址索引的定义

Linux内核仅使用RTC芯片的时间与日期寄存器组和控制寄存器组,地址为0x00~0x09之间的10个时间与日期寄存器的定义如下:

 

      
      #define RTC_SECONDS 0 
#define RTC_SECONDS_ALARM 1 
#define RTC_MINUTES 2 
#define RTC_MINUTES_ALARM 3 
#define RTC_HOURS 4 
#define RTC_HOURS_ALARM 5 
/* RTC_*_alarm is always true if 2 MSBs are set */ 
# define RTC_ALARM_DONT_CARE 0xC0 

#define RTC_DAY_OF_WEEK 6 
#define RTC_DAY_OF_MONTH 7 
#define RTC_MONTH 8 
#define RTC_YEAR 9

四个控制寄存器的地址定义如下:

 

      
      #define RTC_REG_A 10 
#define RTC_REG_B 11 
#define RTC_REG_C 12 
#define RTC_REG_D 13

(2)各控制寄存器的状态位的详细定义

控制寄存器A(0x0A)主要用于选择RTC芯片的工作频率,因此也称为RTC频率选择寄存器。因此Linux用一个宏别名RTC_FREQ_SELECT来表示控制寄存器A,如下:

 

      
      #define RTC_FREQ_SELECT RTC_REG_A

RTC频率寄存器中的位被分为三组:①bit[7]表示UIP标志;②bit[6:4]用于除法器的频率选择;③bit[3:0]用于速率选择。它们的定义如下:

 

      
      # define RTC_UIP 0x80 
# define RTC_DIV_CTL 0x70 
/* Periodic intr. / Square wave rate select. 0=none, 1=32.8kHz,... 15=2Hz */ 
# define RTC_RATE_SELECT 0x0F

正如7.1.1.1节所介绍的那样,bit[6:4]有5中可能的取值,分别为除法器选择不同的工作频率或用于重置除法器,各种可能的取值如下定义所示:

 

      
      /* divider control: refclock values 4.194 / 1.049 MHz / 32.768 kHz */ 
# define RTC_REF_CLCK_4MHZ 0x00 
# define RTC_REF_CLCK_1MHZ 0x10 
# define RTC_REF_CLCK_32KHZ 0x20 
/* 2 values for divider stage reset, others for ”testing purposes only” */ 
# define RTC_DIV_RESET1 0x60 
# define RTC_DIV_RESET2 0x70

寄存器B中的各位用于使能/禁止RTC的各种特性,因此控制寄存器B(0x0B)也称为“控制寄存器”,Linux用宏别名RTC_CONTROL来表示控制寄存器B,它与其中的各标志位的定义如下所示:

 

      
      #define RTC_CONTROL RTC_REG_B 
# define RTC_SET 0x80 /* disable updates for clock setting */ 
# define RTC_PIE 0x40 /* periodic interrupt enable */ 
# define RTC_AIE 0x20 /* alarm interrupt enable */ 
# define RTC_UIE 0x10 /* update-finished interrupt enable */ 
# define RTC_SQWE 0x08 /* enable square-wave output */ 
# define RTC_DM_BINARY 0x04 /* all time/date values are BCD if clear */ 
# define RTC_24H 0x02 /* 24 hour mode - else hours bit 7 means pm */ 
# define RTC_DST_EN 0x01 /* auto switch DST - works f. USA only */

 


寄存器C是RTC芯片的中断请求状态寄存器,Linux用宏别名RTC_INTR_FLAGS来表示寄存器C,它与其中的各标志位的定义如下所示:

 

      
      #define RTC_INTR_FLAGS RTC_REG_C 
/* caution - cleared by read */ 
# define RTC_IRQF 0x80 /* any of the following 3 is active */ 
# define RTC_PF 0x40 
# define RTC_AF 0x20 
# define RTC_UF 0x10

寄存器D仅定义了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也称为RTC的有效寄存器。Linux用宏别名RTC_VALID来表示寄存器D,如下:

 

      
      #define RTC_VALID RTC_REG_D 
# define RTC_VRT 0x80 /* valid RAM and time */

(3)二进制格式与BCD格式的相互转换

由于时间与日期寄存器中的值可能以BCD格式存储,也可能以二进制格式存储,因此需要定义二进制格式与BCD格式之间的相互转换宏,以方便编程。如下:

 

      
      #ifndef BCD_TO_BIN 
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10) 
#endif 

#ifndef BIN_TO_BCD 
#define BIN_TO_BCD(val) ((val)=(((val)/10)<<4) + (val)%10) 
#endif

 

7.2.3 内核对RTC的操作

如前所述,Linux内核与RTC进行互操作的时机只有两个:(1)内核在启动时从RTC中读取启动时的时间与日期;(2)内核在需要时将时间与日期回写到RTC中。为此,Linux内核在arch/i386/kernel/time.c文件中实现了函数get_cmos_time()来进行对RTC的第一种操作。显然,get_cmos_time()函数仅仅在内核启动时被调用一次。而对于第二种操作,Linux则同样在arch/i386/kernel/time.c文件中实现了函数set_rtc_mmss(),以支持向RTC中回写当前时间与日期。下面我们将来分析这二个函数的实现。 在分析get_cmos_time()函数之前,我们先来看看RTC芯片对其时间与日期寄存器组的更新原理。

(1)Update In Progress

当控制寄存器B中的SET标志位为0时,MC146818芯片每秒都会在芯片内部执行一个“更新周期”(Update Cycle),其作用是增加秒寄存器的值,并检查秒寄存器是否溢出。如果溢出,则增加分钟寄存器的值,如此一致下去直到年寄存器。在“更新周期”期间,时间与日期寄存器组(0x00~0x09)是不可用的,此时如果读取它们的值将得到未定义的值,因为MC146818在整个更新周期期间会把时间与日期寄存器组从CPU总线上脱离,从而防止软件程序读到一个渐变的数据。

在MC146818的输入时钟频率(也即晶体增荡器的频率)为4.194304MHZ或1.048576MHZ的情况下,“更新周期”需要花费248us,而对于输入时钟频率为32.768KHZ的情况,“更新周期”需要花费1984us=1.984ms。控制寄存器A中的UIP标志位用来表示MC146818是否正处于更新周期中,当UIP从0变为1的那个时刻,就表示MC146818将在稍后马上就开更新周期。在UIP从0变到1的那个时刻与MC146818真正开始Update Cycle的那个时刻之间时有一段时间间隔的,通常是244us。也就是说,在UIP从0变到1的244us之后,时间与日期寄存器组中的值才会真正开始改变,而在这之间的244us间隔内,它们的值并不会真正改变。如下图所示:

(2)get_cmos_time()函数

该函数只被内核的初始化例程time_init()和内核的APM模块所调用。其源码如下:

 

      
      /* not static: needed by APM */ 
unsigned long get_cmos_time(void) 
{ 
unsigned int year, mon, day, hour, min, sec; 
int i; 

/* The Linux interpretation of the CMOS clock register contents: 
* When the Update-In-Progress (UIP) flag goes from 1 to 0, the 
* RTC registers show the second which has precisely just started. 
* Let''s hope other operating systems interpret the RTC the same way. 
*/ 
/* read RTC exactly on falling edge of update flag */ 
for (i = 0 ; i < 1000000 ; i++) /* may take up to 1 second... */ 
if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP) 
break; 
for (i = 0 ; i < 1000000 ; i++) /* must try at least 2.228 ms */ 
if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)) 
break; 
do { /* Isn''t this overkill ? UIP above should guarantee consistency */ 
sec = CMOS_READ(RTC_SECONDS); 
min = CMOS_READ(RTC_MINUTES); 
hour = CMOS_READ(RTC_HOURS); 
day = CMOS_READ(RTC_DAY_OF_MONTH); 
mon = CMOS_READ(RTC_MONTH); 
year = CMOS_READ(RTC_YEAR); 
} while (sec != CMOS_READ(RTC_SECONDS)); 
if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD) 
{ 
BCD_TO_BIN(sec); 
BCD_TO_BIN(min); 
BCD_TO_BIN(hour); 
BCD_TO_BIN(day); 
BCD_TO_BIN(mon); 
BCD_TO_BIN(year); 
} 
if ((year += 1900) < 1970) 
year += 100; 
return mktime(year, mon, day, hour, min, sec); 
}

对该函数的注释如下:

(1)在从RTC中读取时间时,由于RTC存在Update Cycle,因此软件发出读操作的时机是很重要的。对此,get_cmos_time()函数通过UIP标志位来解决这个问题:第一个for循环不停地读取RTC频率选择寄存器中的UIP标志位,并且只要读到UIP的值为1就马上退出这个for循环。第二个for循环同样不停地读取UIP标志位,但他只要一读到UIP的值为0就马上退出这个for循环。这两个for循环的目的就是要在软件逻辑上同步RTC的Update Cycle,显然第二个for循环最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)

 

(2)从第二个for循环退出后,RTC的Update Cycle已经结束。此时我们就已经把当前时间逻辑定准在RTC的当前一秒时间间隔内。也就是说,这是我们就可以开始从RTC寄存器中读取当前时间值。但是要注意,读操作应该保证在244us内完成(准确地说,读操作要在RTC的下一个更新周期开始之前完成,244us的限制是过分偏执的:-)。所以,get_cmos_time()函数接下来通过CMOS_READ()宏从RTC中依次读取秒、分钟、小时、日期、月份和年分。这里的do{}while(sec!=CMOS_READ(RTC_SECOND))循环就是用来确保上述6个读操作必须在下一个Update Cycle开始之前完成。

(3)接下来判定时间的数据格式,PC机中一般总是使用BCD格式的时间,因此需要通过BCD_TO_BIN()宏把BCD格式转换为二进制格式。

(4)接下来对年分进行修正,以将年份转换为“19XX”的格式,如果是1970以前的年份,则将其加上100。

(5)最后调用mktime()函数将当前时间与日期转换为相对于1970-01-01 00:00:00的秒数值,并将其作为函数返回值返回。

函数mktime()定义在include/linux/time.h头文件中,它用来根据Gauss算法将以year/mon/day/hour/min/sec(如1980-12-31 23:59:59)格式表示的时间转换为相对于1970-01-01 00:00:00这个UNIX时间基准以来的相对秒数。其源码如下:

 

      
      static inline unsigned long 
mktime (unsigned int year, unsigned int mon, 
unsigned int day, unsigned int hour, 
unsigned int min, unsigned int sec) 
{ 
if (0 >= (int) (mon -= 2)) { /* 1..12 -> 11,12,1..10 */ 
mon += 12; /* Puts Feb last since it has leap day */ 
year -= 1; 
} 

return ((( 
(unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) + 
year*365 - 719499 
)*24 + hour /* now have hours */ 
)*60 + min /* now have minutes */ 
)*60 + sec; /* finally seconds */ 
}

(3)set_rtc_mmss()函数

该函数用来更新RTC中的时间,它仅有一个参数nowtime,是以秒数表示的当前时间,其源码如下:

 

      
      static int set_rtc_mmss(unsigned long nowtime) 
{ 
int retval = 0; 
int real_seconds, real_minutes, cmos_minutes; 
unsigned char save_control, save_freq_select; 

/* gets recalled with irq locally disabled */ 
spin_lock(&rtc_lock); 
save_control = CMOS_READ(RTC_CONTROL); /* tell the clock it''s being set */ 
CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL); 

save_freq_select = CMOS_READ(RTC_FREQ_SELECT); /* stop and reset prescaler */ 
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2), RTC_FREQ_SELECT); 

cmos_minutes = CMOS_READ(RTC_MINUTES); 
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) 
BCD_TO_BIN(cmos_minutes); 

/* 
* since we''re only adjusting minutes and seconds, 
* don''t interfere with hour overflow. This avoids 
* messing with unknown time zones but requires your 
* RTC not to be off by more than 15 minutes 
*/ 
real_seconds = nowtime % 60; 
real_minutes = nowtime / 60; 
if (((abs(real_minutes - cmos_minutes) + 15)/30) & 1) 
real_minutes += 30; /* correct for half hour time zone */ 
real_minutes %= 60; 

if (abs(real_minutes - cmos_minutes) < 30) { 
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) { 
BIN_TO_BCD(real_seconds); 
BIN_TO_BCD(real_minutes); 
} 
CMOS_WRITE(real_seconds,RTC_SECONDS); 
CMOS_WRITE(real_minutes,RTC_MINUTES); 
} else { 
printk(KERN_WARNING 
”set_rtc_mmss: can''t update from %d to %d/n”, 
cmos_minutes, real_minutes); 
retval = -1; 
} 

/* The following flags have to be released exactly in this order, 
* otherwise the DS12887 (popular MC146818A clone with integrated 
* battery and quartz) will not reset the oscillator and will not 
* update precisely 500 ms later. You won''t find this mentioned in 
* the Dallas Semiconductor data sheets, but who believes data 
* sheets anyway ... -- Markus Kuhn 
*/ 
CMOS_WRITE(save_control, RTC_CONTROL); 
CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT); 
spin_unlock(&rtc_lock); 

return retval; 
}

对该函数的注释如下:

(1)首先对自旋锁rtc_lock进行加锁。定义在arch/i386/kernel/time.c文件中的全局自旋锁rtc_lock用来串行化所有CPU对RTC的操作。

(2)接下来,在RTC控制寄存器中设置SET标志位,以便通知RTC软件程序随后马上将要更新它的时间与日期。为此先把RTC_CONTROL寄存器的当前值读到变量save_control中,然后再把值(save_control | RTC_SET)回写到寄存器RTC_CONTROL中。

(3)然后,通过RTC_FREQ_SELECT寄存器中bit[6:4]重启RTC芯片内部的除法器。为此,类似地先把RTC_FREQ_SELECT寄存器的当前值读到变量save_freq_select中,然后再把值(save_freq_select | RTC_DIV_RESET2)回写到RTC_FREQ_SELECT寄存器中。

(4)接着将RTC_MINUTES寄存器的当前值读到变量cmos_minutes中,并根据需要将它从BCD格式转化为二进制格式。

(5)从nowtime参数中得到当前时间的秒数和分钟数。分别保存到real_seconds和real_minutes变量。注意,这里对于半小时区的情况要修正分钟数real_minutes的值。

(6)然后,在real_minutes与RTC_MINUTES寄存器的原值cmos_minutes二者相差不超过30分钟的情况下,将real_seconds和real_minutes所表示的时间值写到RTC的秒寄存器和分钟寄存器中。当然,在回写之前要记得把二进制转换为BCD格式。

(7)最后,恢复RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原来的值。这二者的先后次序是:先恢复RTC_CONTROL寄存器,再恢复RTC_FREQ_SELECT寄存器。然后在解除自旋锁rtc_lock后就可以返回了。

最后,需要说明的一点是,set_rtc_mmss()函数尽可能在靠近一秒时间间隔的中间位置(也即500ms处)左右被调用。此外,Linux内核对每一次成功的更新RTC时间都留下时间轨迹,它用一个系统全局变量last_rtc_update来表示内核最近一次成功地对RTC进行更新的时间(单位是秒数)。该变量定义在arch/i386/kernel/time.c文件中:

 

      
      /* last time the cmos clock got updated */ 
static long last_rtc_update;

每一次成功地调用set_rtc_mmss()函数后,内核都会马上将last_rtc_update更新为当前时间。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值