Linux作業系統核心對RTC的編程詳解

來源:互聯網
上載者:User

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更新為目前時間。 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.