红联Linux门户
Linux帮助

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

发布时间:2006-11-15 09:55:41来源:红联作者:Informix
  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。
文章评论

共有 5 条评论

  1. chunyu 于 2006-11-16 16:16:12发表:

    讲解的很详细,牛人啊,佩服

  2. Informix 于 2006-11-15 09:59:04发表:

      (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更新为当前时间。

  3. Informix 于 2006-11-15 09:58:03发表:

      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)

  4. Informix 于 2006-11-15 09:57:22发表:

      寄存器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

  5. Informix 于 2006-11-15 09:56:41发表:

      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 */