RTC 實時時鐘驅(qū)動 收藏RTC 實時時鐘驅(qū)動 -------I2C軟件模擬通信
內(nèi)核版本: linux-2.4.21 文檔設(shè)計: 侯輝華 版 本: 1.01 時 間: 2007/06/10
內(nèi)容簡介: 介紹接在I2C總線上RTC實時時鐘設(shè)備的驅(qū)動, 使用軟件模擬的方法完成I2C的通信; 介紹了Linux下的時鐘系統(tǒng), 以及I2C的層次結(jié)構(gòu).
目錄索引:
一. Linux下的時鐘系統(tǒng)簡介. 二. Linux對時間的表示. 三. Linux時鐘中斷的初始化及處理. 四. RTC設(shè)備驅(qū)動程序. 五. I2C總線讀寫. 六. Linux下的I2C驅(qū)動層次結(jié)構(gòu)..
實際上,linux系統(tǒng)有兩個時鐘:一個是由主板電池驅(qū)動的“Real Time Clock”也叫做RTC或者叫CMOS時鐘,硬件時鐘。當操作系統(tǒng)關(guān)機的時候,用這個來記錄時間,但是對于運行的系統(tǒng)是不用這個時間的。另一個時間是 “System clock”也叫內(nèi)核時鐘或者軟件時鐘,是由軟件根據(jù)時間中斷來進行計數(shù)的,內(nèi)核時鐘在系統(tǒng)關(guān)機的情況下是不存在的,所以,當操作系統(tǒng)啟動的時候,內(nèi)核時鐘是要讀取RTC時間來進行時間同步.
二. Linux對時間的表示 通常,操作系統(tǒng)可以使用三種方法來表示系統(tǒng)的當前時間與日期:①最簡單的一種方法就是直接用一個64位的計數(shù)器來對時鐘滴答進行計數(shù)。②第二種方法就是用一個32位計數(shù)器來對秒進行計數(shù),同時還用一個32位的輔助計數(shù)器對時鐘滴答計數(shù),之子累積到一秒為止。因為2的32次方超過136年,因此這種方法直至22世紀都可以讓系統(tǒng)工作得很好。③第三種方法也是按時鐘滴答進行計數(shù),但是是相對于系統(tǒng)啟動以來的滴答次數(shù),而不是相對于相對于某個確定的外部時刻;當讀外部后備時鐘(如RTC)或用戶輸入實際時間時,根據(jù)當前的滴答次數(shù)計算系統(tǒng)當前時間。 Linux通常都采用第三種方法來維護系統(tǒng)的時間與日期, 通過時鐘點滴進行計時的基礎(chǔ)原理, 可以參看下面介紹的參考文檔, 主要原理是通過硬件的中斷累積來計時, 但必要要設(shè)置硬件中斷一次所須的時間, 一般具體的不同的芯片都不同, EP9302系統(tǒng)具體設(shè)置如下: 1. EP93xx系列芯片有四個Timer計時器, 使用的是Timer1, 與具體芯片相關(guān)的內(nèi)容在如下兩個文件: linux-2.4.21\arch\arm\mach-ep93xx\time.c, linux-2.4.21\arch\arm\mach-ep93xx\time.h 2. 針對整個Arm體系的時鐘相關(guān)文件為: linux-2.4.21\arch\arm\kernel\time.c 時鐘中斷計時的主要相關(guān)函數(shù)為如下兩個: 1. ep93xx_gettimeoffset()的作用就是返回距最近一次時鐘中斷發(fā)生后, Timer已經(jīng)累積的時間(但還未滿足引發(fā)一次時鐘中斷), 這個時間值單位為微秒, 獲取當前時間時, 就是累計已經(jīng)發(fā)生的Timer中斷次數(shù)所經(jīng)歷的時間, 然后加上這個即將要發(fā)生中斷所過去的時間, 這樣取得當前時間的精度是相當高的. 2. LATCH的含義是指一次時鐘中斷要經(jīng)過多少個Timer時鐘周期, TIMER1LOAD寄存器設(shè)置的就是這個值, ep93xx的計時器Timer1會將這個值一直遞減直至0, 如此就引發(fā)一次時鐘中斷, 然后又重LATCH重頭開始遞減. 3. xtime記載的即為系統(tǒng)自開機以來的當前時間, 單位為秒, 精確度為微秒. 因此在開機時必須從RTC當中取得真實的時間來賦此初值, EP93xx系列直接初始此值為其自身所帶RTC模塊的時間值, RTCDR寄存器是EP93xx所自帶的RTC模塊的寄存器, 其值單位為秒,基準為相對1970年, 此處即為我們要改動的地方.將其值從i2c的RTC實時芯片中取回賦給它. static unsigned long ep93xx_gettimeoffset(void) { unsigned long hwticks; hwticks = LATCH - (inl(TIMER1VALUE) & 0xffff); return ((hwticks * tick) / LATCH); } void __init ep93xx_setup_timer(void) //初始化Timer,設(shè)定時鐘中斷周期… { gettimeoffset = ep93xx_gettimeoffset; outl(0, TIMER1CONTROL); outl(LATCH - 1, TIMER1LOAD); //設(shè)定Timer經(jīng)多少個Timer時鐘周期后產(chǎn)生中斷… outl(0xc8, TIMER1CONTROL); xtime.tv_sec = inl(RTCDR); //從ep93xx內(nèi)部RTC模塊讀取時間,后將重新從cmos讀. } 以下詳細介紹一下時鐘點滴計時的幾個基本參數(shù), 以下定義的出處, 除非特別指出, 一般是位于各自不同的平臺的文件夾下定義: linux-2.4.21\include\asm-arm\arch-ep93xx linux-2.4.21\arch\arm\mach-ep93xx\
1. 時鐘周期(clock cycle)的頻率:計時器Timer晶體振蕩器在1秒時間內(nèi)產(chǎn)生的時鐘脈沖個數(shù)就是時鐘周期的頻率, 要注意這個Timer的時鐘周期頻率要與時鐘中斷的頻率區(qū)別開來, Linux用宏CLOCK_TICK_RATE來表示計時器的輸入時鐘脈沖的頻率(此值在EP93xx上是508KHZ),該宏定義在timex.h頭文件中: #define CLOCK_TICK_RATE 508000 /* Underlying HZ */ 2. 時鐘中斷(clock tick):我們知道當計數(shù)器減到0值時,它就在IRQ0上產(chǎn)生一次時鐘中斷,也即一次時鐘中斷, 計數(shù)器的初始值決定了要過多少時鐘周期才產(chǎn)生一次時鐘中斷,因此也就決定了一次時鐘滴答的時間間隔長度. 在EP93xx系統(tǒng)中, Timer1的TIMER1LOAD 的值決定過多少時鐘周期后產(chǎn)生一次時鐘中斷. 3. 時鐘中斷的頻率(HZ):也即1秒時間內(nèi)Timer所產(chǎn)生的時鐘中斷次數(shù)。確定了時鐘中斷的頻率值后也就可以確定Timer的計數(shù)器初值。Linux內(nèi)核用宏HZ來表示時鐘中斷的頻率,而且在不同的平臺上HZ有不同的定義值。對于SPARC、MIPS、ARM和i386等平臺HZ的值都是100。該宏在ARM平臺上的定義如下(param.h): #ifndef HZ #define HZ 100 #endif 據(jù)HZ值,可知每隔(1000ms/HZ)=10ms發(fā)生一次時鐘中斷.
4. 時鐘中斷的時間間隔: Linux用全局變量tick來表示時鐘中斷的時間間隔長度,其實定義了HZ之后, 即決定了此間隔值, tick變量的單位是微妙(μs), 該變量定義在kernel/timer.c文件中,如下: long tick = (1000000 + HZ/2) / HZ; /* timer interrupt period */. 5. 宏LATCH:Linux用宏LATCH來定義要設(shè)置到Timer中的值,它表示TImer將沒隔多少個時鐘周期產(chǎn)生一次時鐘中斷。顯然LATCH應(yīng)該由下列公式計算: LATCH=(1秒之內(nèi)的Timer時鐘周期個數(shù))÷(1秒之內(nèi)的時鐘中斷次數(shù))=(CLOCK_TICK_RATE)÷(HZ).
三. Linux時鐘中斷的初始化及處理
以下著重描述一下時鐘中斷時與RTC相關(guān)的修改:
文件:linux-2.4.21\arch\arm\kernel\time.c 描述:時鐘初始化化, 在start_kernel()當中調(diào)用. void __init time_init(void) { xtime.tv_usec = 0; xtime.tv_sec = 0; setup_timer(); }
文件:linux-2.4.21\arch\arm\mach-ep93xx\time.h 描述:安裝時鐘中斷服務(wù)程序,從RTC更新初始化系統(tǒng)時鐘, 在time_init()當中調(diào)用. /* * Set up timer interrupt, and return the current time in seconds. */ static inline void setup_timer(void) { ep93xx_setup_timer(); //houhh 20070713... ///////// xtime.tv_sec = get_cmos_time(); //從RTC設(shè)備中讀取當前時間… set_rtc = set_cmos_time; //初始化更新系統(tǒng)時間到RTC的函數(shù), 在do_set_rtc()中調(diào)用… ///////// timer_irq.handler = ep93xx_timer_interrupt; //時鐘中斷服務(wù)程序… setup_arm_irq(IRQ_TIMER1, &timer_irq); //安裝時鐘中斷處理… }
文件:linux-2.4.21\arch\arm\mach-ep93xx\time.h 描述:讀寫I2C的RTC設(shè)備,軟件模擬方式, 引自i2c-ep93xx.c, 將在下面詳細介紹. extern uchar pcf8563_readdata(uchar address); extern int pcf8563_writedata(uchar address, uchar mdata); #define CMOS_READ(addr) pcf8563_readdata(addr) #define CMOS_WRITE(data, addr) pcf8563_writedata(addr, data)
文件:linux-2.4.21\arch\arm\mach-ep93xx\time.h 描述:設(shè)置及讀寫RTC, 主要通過宏CMOS_READ/ CMOS_WRITE完成功能. unsigned long get_cmos_time(void) static int set_cmos_time(unsigned long nowtime)
文件: linux-2.4.21\arch\arm\mach-ep93xx\time.h 描述:時鐘中斷服務(wù)程序,并檢測更新系統(tǒng)時鐘到RTC, 在setup_timer()當中調(diào)用. /* * IRQ handler for the timer */ static void ep93xx_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) { outl( 1, TIMER1CLEAR ); do_leds(); do_set_rtc(); //houhh 20070713, 檢測是否要更新系統(tǒng)當前時間到cmos rtc設(shè)備... do_timer(regs); do_profile(regs); }
一般認為時鐘中斷計時的精度較高,所以在時鐘中斷服務(wù)程序中會每隔11分鐘(660秒)就檢測一次是否須要將此時的系統(tǒng)時間寫回到RTC當中, 所以在ep93xx的時鐘中斷服務(wù)程序中,須要加上do_set_rtc(),關(guān)于這個函數(shù)具體的功能請具體參見源碼.
四. RTC設(shè)備驅(qū)動程序
主要指出RTC設(shè)備及相關(guān)操作,這一塊相當簡單,RTC時鐘處理成一簡單字符設(shè)備.
基礎(chǔ)文件結(jié)構(gòu): static struct file_operations pcf8563_fops = { owner: THIS_MODULE, ioctl: pcf8563_ioctl, open: pcf8563_open, release: pcf8563_release, }; 文件: linux-2.4.21\drivers\char\pcf8563_rtc.c 描述: RTC設(shè)備的文件結(jié)構(gòu),指文件打開及釋放操作,其實最核心的還是ioctl,這里可以進行時間讀取以及時間設(shè)置操作, 具體使用示例可以參考rtctest.c示例文件.
文件: linux-2.4.21\drivers\char\pcf8563_rtc.c 描述:時間設(shè)置與讀取. get_rtc_time(struct rtc_time *tm); Set_rtc_time(struct rtc_time *tm);
文件: linux-2.4.21\drivers\charpcf8563_rtc.c 描述: I2C讀寫, 這兩個函數(shù)是從文件i2c-ep93xx.c中引用的,定義成宏. extern int pcf8563_writedata(uchar address, uchar mdata); extern uchar pcf8563_readdata(uchar address); #define rtc_reg_read(x) pcf8563_readdata(x) #define rtc_reg_write(x,y) pcf8563_writedata(x, y)
文件: linux-2.4.21\drivers\charpcf8563_rtc.c 描述: 模塊初始化,負責注冊/注消RTC字符設(shè)備. int __init pcf8563_init(void) void __exit pcf8563_exit(void)
五. I2C總線讀寫
i2c總線讀寫方式為軟件模擬方式,因為ep93xx沒有相關(guān)的i2c總線控制器,因此只能通過軟件方式來模擬I2C總線的讀寫, 在調(diào)試過程中遇到如下問題,注意如下即可:
1. 注意延時的時間,I2C的開始條件與讀寫時都須要一定的延時,根據(jù)主設(shè)備(即ep93xx cpu)的運行速度,此延時必須是一個穩(wěn)定的時間,通常采取讀取特定外設(shè)i/o以達此目的. 2. 注意在改變I2C的SDA數(shù)據(jù)線狀態(tài)時,必須是在SCL時鐘線為低的時候,因為根據(jù)開始與結(jié)束條件的要求,開始與結(jié)束條件是在SCL時鐘線高的時候SDA拉高或者拉低,所以如果在傳送數(shù)據(jù)時,SCL為高,則會被當成開始或結(jié)束條件,通信失敗. 3. 在讀寫I2C總線時,每傳送或接收一BYTE數(shù)據(jù),必須要進行回應(yīng). Ø 寫回應(yīng):主設(shè)備寫完八位數(shù)據(jù)后,在第九個周期等待從設(shè)備來拉低SDA作為回應(yīng),因此須先將SDA在第八周期SCL低時拉高,之后拉高SCL等從設(shè)備回應(yīng),等到從設(shè)備回應(yīng)后拉低SCL,第九周期結(jié)束,一個BYTE傳送完成. Ø 讀回應(yīng):主設(shè)備讀從設(shè)備八位數(shù)據(jù)后,也應(yīng)該在第九周期進行回應(yīng),分如下兩種情況: 連續(xù)讀n個字節(jié)時,前n-1個字節(jié)以拉低作回應(yīng),第n個字節(jié)則為拉高SDA回應(yīng),因此如若是每次只讀一個字節(jié),則回應(yīng)為拉高. 4. 操作SDA/SCL PIN時,注意在讀SDA時,將其設(shè)置成輸入狀態(tài).
文件: linux-2.4.21\drivers\i2ci2c-ep93xx.c 描述: SDA/SCL PIN腳定義, 為EP93xx的GPIO口G口的第0, 1位. #define I2C_SDA_PORT GPIO_PGDR #define I2C_SDA_DIR GPIO_PGDDR #define I2C_SDA_MASK 0x2 //EEDAT... #define I2C_SCL_PORT GPIO_PGDR #define I2C_SCL_DIR GPIO_PGDDR #define I2C_SCL_MASK 0x1 //EECLK...
文件: linux-2.4.21\drivers\i2c\ i2c-ep93xx.c 描述: SDA/SCL輸入輸出. static void bit_ep93xx_setscl(void* data, int state) { unsigned long flags; save_flags(flags); outl(inl(I2C_SCL_DIR) | I2C_SCL_MASK, I2C_SCL_DIR); // tristate pin if (state){ cli(); outl(inl(I2C_SCL_PORT) | I2C_SCL_MASK, I2C_SCL_PORT); // drive pin } else{ cli(); outl(inl(I2C_SCL_PORT) & ~I2C_SCL_MASK, I2C_SCL_PORT); // drive pin } restore_flags(flags); } //=========================================================================== /// write SCL pin //=========================================================================== static void bit_ep93xx_setsda(void* data, int state) { unsigned long flags; save_flags(flags); outl(inl(I2C_SDA_DIR) | I2C_SDA_MASK, I2C_SDA_DIR); // output... if (state){ cli(); outl(inl(I2C_SDA_PORT) | I2C_SDA_MASK, I2C_SDA_PORT); // drive pin } else{ cli(); outl(inl(I2C_SDA_PORT) & ~I2C_SDA_MASK, I2C_SDA_PORT); // drive pin } restore_flags(flags); }
具體有關(guān)I2C總線軟件模擬通信部分,參考源碼.
六. Linux下的I2C驅(qū)動層次結(jié)構(gòu).
Linux最大的特點,就是將一系列的驅(qū)動中共有的東西抽象出來,大大提高代碼的共享與利用率,這樣使具體硬件設(shè)備的驅(qū)動作者無須關(guān)注驅(qū)動中共性的部分,I2C驅(qū)動同樣如此,這里我不打算詳細的分析I2C的層次結(jié)構(gòu),只是簡單的陪析一下其大略的層次, 并指出幾個不易理解的地方。
1. 適配器層(adapters) 描述: 這一層簡單的理解,可以理解成是I2C總線的抽象,它提供訪問I2C總線的方法規(guī)則,并包含在物理總線上適用此規(guī)則進行I2C通信的I2C設(shè)備, 而且I2C設(shè)備并不須是在同一條總線上,只須滿足相同的訪問I2C總線方法規(guī)則,且設(shè)備地址不重重疊. 2. I2C設(shè)備驅(qū)動層(i2c client driver) 描述:具體到每個掛在I2C上的設(shè)備, 針對特定的設(shè)備可以有不同的驅(qū)動, 諸如設(shè)備標志ID, 設(shè)備名稱, 設(shè)備屬性志Flag, 還有就是設(shè)備連接及拔除的相應(yīng)處理. 3. I2C數(shù)據(jù)傳送規(guī)則層(algorithms) 描述: 針對具體的I2C總線, 可以有不同的數(shù)據(jù)傳輸規(guī)則, 分為三種: ü 大體來說如果本身就有I2C控制器的, I2C使用起來就比較簡單, 傳輸入數(shù)據(jù)時就是讀寫一些寄存器即可. ü 通地軟件模擬方式進行數(shù)據(jù)傳輸, I2C中這個規(guī)則被稱為algorithms for bit-shift, 這種編程起來稍微麻煩一些, 要用軟件模擬I2C的通信協(xié)議規(guī)則. ü 通過每三方總線間接的連接I2C總線的, 如ISA總線, 此時I2C是間接連接在系統(tǒng)上, 這種方式通常與具體的所連接上的總線相關(guān). 另外, 還有一種規(guī)則是針對SMBus總線, 這個總線是Intel推出的兼容I2C協(xié)議的, 可能一次傳送一個字,兩個字節(jié),多個字節(jié)等等, 這里沒有用到,并不詳述, 由此我們可以理會I2C設(shè)計者設(shè)計的結(jié)構(gòu)的靈活程度.
以上簡述了I2C驅(qū)動層次結(jié)構(gòu), 現(xiàn)在具體的就PCF8563 RTC來描述每一層具體由哪些文件組成, 結(jié)合實際的驅(qū)動文件說明:
1. 適配器層----------------- i2c-core.c, i2c-dev.c 2. I2C設(shè)備驅(qū)動層---------pcf8563-rtc.c 3. I2C數(shù)據(jù)傳送規(guī)則層--- i2c-algo-bit.c, i2c-ep93xx
大體對照I2C驅(qū)動的層次結(jié)構(gòu), 結(jié)合以上文件來看, 可以理解I2C的驅(qū)動.
I2C驅(qū)動代碼中不易理解的幾點:
結(jié)合在代碼閱讀時我所經(jīng)歷的過程, 說明以下幾點不易明白的地方:
1. i2c設(shè)備的識別 描述:每個I2C設(shè)備有自己特定的設(shè)備地址,通常為讀地址及寫地址; 2.4版當中是采取遍歷的方式來查找I2C設(shè)備應(yīng)該, 每個i2c設(shè)備都會標明自己所處的地址, 描述i2c設(shè)備所處地址范圍的結(jié)構(gòu)比較復(fù)雜, 它包括描述i2c設(shè)備所處地址范圍, 要忽略的地址范圍, 應(yīng)當強制檢測的范圍等, 個人感覺些結(jié)構(gòu)設(shè)計過于重復(fù), 在2.6版中已經(jīng)簡化. 具體的I2C設(shè)備檢測時, 即根據(jù)這些地址, 從0~0x7f開始檢測, 檢測時會跳出過無須檢測的范圍. 比如一i2c設(shè)備寫地址為0xa0, 則在此描述時給出的地址值是0x50, 因此在真實寫此設(shè)備時, 必須將地址左移兩位得真實地址.
2.4版i2c地址描述結(jié)構(gòu): struct i2c_client_address_data { unsigned short *normal_i2c; unsigned short *normal_i2c_range; unsigned short *probe; unsigned short *probe_range; unsigned short *ignore; unsigned short *ignore_range; unsigned short *force; }; 2.6版i2c地址描述結(jié)構(gòu): struct i2c_client_address_data { unsigned short *normal_i2c; unsigned short *probe; unsigned short *ignore; unsigned short **forces; };
2. i2c設(shè)備設(shè)備的讀與寫 描述:設(shè)備地址一般為7位, 此地址是字節(jié)的高七位, 最低的一位用于描述是讀設(shè)備還是寫設(shè)備, 因此可知I2C設(shè)備讀寫地址必然相連, 因此讀寫地址轉(zhuǎn)換通常為最低位或上1, 或者清除1, 另外還記得這里描述的地址必須左移兩位, 因為給出的設(shè)備地址是以低七位形式給出. 寫地址一般是小于讀地址, 所以通常讀地址都是寫地址或上1即得, 因此在設(shè)備驅(qū)動結(jié)構(gòu)i2c_client中, 僅用一addr成員來描述設(shè)備地址. 具體來說, 看i2c-algo-bit.c文件下的bit_doAddress()函數(shù), 如果是七位地址的I2C總線, 其處理如下, 將設(shè)備地址左移兩位得寫地址, 然后或上1, 即得讀地址. addr = ( msg->addr << 1 ); if (flags & I2C_M_RD ) addr |= 1; 3. i2c adapters 及algorithms的管理. 描述: 每一個adapters 都會有一個唯一的標志, 如我們的EP93xx則定義為I2C_HW_B_EP93XX, 每一種algorithms都也有一個ID標志, bit-shift algorithms的標志即為: I2C_ALGO_BIT., 關(guān)于這些標志的定義, 可以查看i2c-id.h文件. 具體的, 添加一個bit-shift algorithms的adapters , 可以調(diào)用i2c_bit_add_bus()函數(shù), 這個調(diào)用由用戶發(fā)出, 一般在模塊加載的init函數(shù)中調(diào)用, 每當加入一個adapters, 都會檢測已經(jīng)注冊的i2c driver設(shè)備是否適用此adapters, 如果適用則調(diào)用該i2c設(shè)備driver下的attach_adapter, 通知設(shè)備發(fā)現(xiàn)其相應(yīng)適配器, 只有找到相應(yīng)適配器, 該設(shè)備才能使用i2c總線. |
|
|