📄 linux_clock
字号:
#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_ARTC频率寄存器中的位被分为三组:①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)#endif7.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 longmktime (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
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -