, ,
(華中科技大學 自動化學院, 武漢 430074)
隨著社會生產(chǎn)力的發(fā)展,社會、科技等領(lǐng)域傳遞的信息量越來越大。為保證這些信息的可靠傳遞就需要高精度的時間同步。并且隨著某些領(lǐng)域的深入發(fā)展如電力系統(tǒng)、數(shù)字通信系統(tǒng)、軍事領(lǐng)域等,它們對時間同步的同步精度的要求也越來越高。目前實現(xiàn)時間同步的方法主要有3種:無線電波授時、衛(wèi)星授時和網(wǎng)絡(luò)授時[1]。其中無線電授時和網(wǎng)絡(luò)授時的授時精度比較低約為幾毫秒,無法滿足當下的需求。而衛(wèi)星授時具有同步精度相對較高,時刻同步精度可以達到納秒級,并且GPS信號不受地理環(huán)境,地域限制的影響具有準確性和可靠性。
Linux作為開源的操作系統(tǒng)因為其優(yōu)良的穩(wěn)定性,支持多任務(wù)、多使用者,安全性高等特點越來越受到程序員的青睞。PCI總線是當下使用最為廣泛的總線協(xié)議之一,其在Linux下的驅(qū)動程序也備受關(guān)注。Linux操作系統(tǒng)因為其開源的特色,使得驅(qū)動程序在各個平臺間的移植變得簡單。
PCI同步時鐘卡,其系統(tǒng)框圖如圖1所示。該設(shè)備以單片機為整個系統(tǒng)的控制單元,主要用于處理接收到的時間信號并控制雙口RAM數(shù)據(jù)的讀寫??删幊踢壿婥PLD用于產(chǎn)生高精度的同步絕對時標,將信息存儲于雙口RAM中。PC機通過PCI總線訪問雙口RAM中的數(shù)據(jù),同時設(shè)定單片機和PC機訪問雙口RAM的通訊協(xié)議,以防止雙方讀寫雙口RAM產(chǎn)生混亂的情況[2]。PC機獲取雙口RAM時間數(shù)據(jù)有兩種方式:一種是查詢模式,PC機主動讀取雙口RAM時間數(shù)據(jù);另一種是中斷模式,通過外部事件觸發(fā)中斷一旦接收到中斷信號就讀取時間數(shù)據(jù)。本文主要介紹中斷方式。
圖1 同步時鐘卡原理框圖
Linux將I/O設(shè)備分為2類:字符設(shè)備和塊設(shè)備。字符設(shè)備支持按字節(jié)或字符來讀寫數(shù)據(jù),應(yīng)用程序可以順序從設(shè)備中讀取數(shù)據(jù)。塊設(shè)備支持按塊(512字節(jié))讀寫數(shù)據(jù),應(yīng)用程序可以隨機訪問設(shè)備數(shù)據(jù),塊設(shè)備并不支持基于字符的尋址[3]。PCI同步時鐘卡屬于字符設(shè)備。
在Linux中一個cdev結(jié)構(gòu)描述一個字符設(shè)備驅(qū)動程序,當系統(tǒng)調(diào)用模塊加載函數(shù)時,系統(tǒng)就會相應(yīng)實例化一個cdev結(jié)構(gòu)體。該結(jié)構(gòu)體中主要包括設(shè)備號(dev_t)和文件操作集合(file_operations)兩個部分。dev_t中存放有設(shè)備驅(qū)動程序所分配的初始主設(shè)備號和次設(shè)備號。file_operations結(jié)構(gòu)體作為用戶空間與內(nèi)核空間交互的接口,使得用戶空間能夠?qū)inux進行系統(tǒng)調(diào)用。例如在用戶空間中調(diào)用open()、read()等函數(shù)時,Linux會通過file_operations找到對應(yīng)的操作函數(shù)xxx_open()、xxx_read()等函數(shù)完成對設(shè)備的操作。
2.1.1 設(shè)備號申請與釋放
Linux操作系統(tǒng)是基于文件概念的,即它把所有的外部設(shè)備都當成文件來操作,我們把這類文件稱為設(shè)備文件[4]。一個設(shè)備文件對應(yīng)一個唯一確定的設(shè)備號,設(shè)備號由2個部分組成主設(shè)備號和次設(shè)備號。主設(shè)備號它標識了設(shè)備的類型,即同一類設(shè)備具有相同的主設(shè)備號并且他們共享相同的文件操作集合。次設(shè)備號用于標識同類設(shè)備中的一個特定設(shè)備。Linux提供了兩種申請設(shè)備號的方法,register_chrdev_region (dev_t from
,unsigned count, const char *name)和alloc_chrdev_region(dev_t *dev, unsigned
baseminor,unsigned count,const char *name)。第一個屬于靜態(tài)申請設(shè)備號的方式,它需要指定設(shè)備號。第二種屬于動態(tài)申請設(shè)備號的方式,它的設(shè)備號由系統(tǒng)主動分配無需指定。第二種方法能夠有效避免設(shè)備號重復(fù)導(dǎo)致的申請失敗。
相應(yīng)的釋放設(shè)備號函數(shù)為:unregister _chrdev_region(dev_t from,unsigned count)。
2.1.2 file_operations結(jié)構(gòu)體
file_operations是字符設(shè)備驅(qū)動程序設(shè)計的主要部分,它的成員函數(shù)xxx_open()、xxx_read()等會在應(yīng)用程序進行系統(tǒng)調(diào)用時被內(nèi)核調(diào)用到。其主要部分如下所示。
struct file_operations {
struct module *owner;
int (*xxx_open) (struct inode *, struct file *);
ssize_t (*xxx_read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*xxx_write) (struct file *,const char __user *, size_t, loff_t *);
int (*xxx_release) (struct inode *, struct file *);
……
}
file_operations的成員函數(shù)選擇是根據(jù)設(shè)備所需要完成的操作來選擇相應(yīng)的函數(shù)。
Linux內(nèi)核中斷處理機制如下圖所示。為了減少中斷信號的丟失同時提高中斷響應(yīng)的效率,Linux把中斷處理程序分為兩個部分:頂半部和底半部。
頂半部主要起登記的作用,一般情況下它只讀取寄存器中的中斷狀態(tài),并且在清除中斷標志位后把那個設(shè)備發(fā)生的中斷記錄到底半部執(zhí)行隊列中去。這樣,頂半部的工作量會很少執(zhí)行速度就會很快,從而可以服務(wù)更多的中斷請求。
底半部負責完成中斷是需要完成的操作。一般頂半部會被設(shè)計成不可中斷,底半部則可以被新的中斷事件打斷。底半部一般會在CPU空閑的時刻由系統(tǒng)主動調(diào)用執(zhí)行。
2.2.1 申請和釋放中斷
在Linux下若想使用帶有中斷功能的設(shè)備,就應(yīng)當在其設(shè)備驅(qū)動中完成相應(yīng)的操作:申請和釋放中斷資源。Linux內(nèi)核提供request_irq()和free_irq()函數(shù)完成對中斷資源的申請和釋放。
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
irq為要申請的硬件中斷號。在PCI設(shè)備中該中斷號是由系統(tǒng)對PCI初始化時完成分配,為此在系統(tǒng)對PCI設(shè)備探測時應(yīng)該把探測到的中斷號放到對應(yīng)的設(shè)備結(jié)構(gòu)體中去。
handler是中斷發(fā)生時,系統(tǒng)調(diào)用的中斷處理函數(shù)(頂半部)。
flags是中斷觸發(fā)方式以及處理方式。在觸發(fā)方面,可以選擇上升沿(IRQF_TRIGGER)、下降沿(IRQF_FALLING)、高電平(IRQF_TRIGGER _HIGH)、低電平(IRQF_TRIGGER_LOW)等。在處理方面,若是設(shè)置為IRQF_SHARED,表示申請的中斷號支持其它設(shè)備申請即共享中斷號;dev是要傳遞給中斷服務(wù)程序的私有數(shù)據(jù),當設(shè)備申請為共享中斷時一般設(shè)置為這個設(shè)備結(jié)構(gòu)體;當設(shè)備獨占中斷號時可以設(shè)置為NULL。
與request_irq()相對應(yīng)的釋放函數(shù)為
void free_irq(unsigned int irq, void *dev_id);
free_irq()中參數(shù)應(yīng)當與request_irq()相同。
2.2.2 底半部機制
Linux實現(xiàn)底半部的機制主要有tasklet、工作隊列和軟中斷[4]。軟中斷和 tasklet運行于中斷上下文, 因此軟中斷和 tasklet 處理函數(shù)中不能睡眠;而工作隊列運行于內(nèi)核進程上下文,因此工作隊列處理函數(shù)中允許睡眠。本文需要在中斷過程中睡眠為此著重介紹工作隊列。
要使用工作隊列首先要定義一個工作隊列和一個底半部執(zhí)行函數(shù):
struct work_struct pciszk_wq;
void pciszk_do_work(struct work_struct *work);
在初始化函數(shù)中通過INIT_WORK()初始化該隊列并將工作隊列與底半部執(zhí)行函數(shù)綁定:
INIT_WORK(&pciszk_wq,pciszk_do_work);
最后要在頂半部處理函數(shù)中調(diào)度工作隊列,該聲明函數(shù)為:
schedule_work(&pciszk_wq);
在Linux操作系統(tǒng)中將程序執(zhí)行狀態(tài)分為兩級:內(nèi)核空間和用戶空間。在內(nèi)核空間中內(nèi)核程序可以進行任何操作,而在用戶空間中用戶程序不允許直接對硬件和內(nèi)存訪問。用戶態(tài)只能通過系統(tǒng)調(diào)用進入內(nèi)核態(tài)完成對硬件的操作。因此當硬件中斷到來時處于用戶空間的應(yīng)用程序無法直接得知該信號的到來,Linux提供了3種方式來處理:阻塞I/O,非阻塞輪詢I/O和異步通知。
阻塞I/O與非阻塞I/O的區(qū)別在于前者在資源不可操作時系統(tǒng)會將該進程睡眠去執(zhí)行其它進程,等到該資源可操作時再喚醒,而后者則會一直訪問直至資源可獲取。異步通知類似于硬件中斷原理,在設(shè)備資源可獲得時內(nèi)核主動通知應(yīng)用程序,避免應(yīng)用程序查詢設(shè)備狀態(tài)。這3種方法并沒有優(yōu)劣之分,應(yīng)根據(jù)自身需求合理選擇。本文采用阻塞I/O的方法,阻塞I/O主要使用等待隊列實現(xiàn)。
當用戶空間執(zhí)行read()、write()等系統(tǒng)調(diào)用時,如果設(shè)備的資源不能獲取,那么該獲取資源的進程就會進入睡眠,此時CPU將執(zhí)行其它進程,直至資源可以獲取后該進程將被喚醒,這一過程被稱為阻塞操作,如圖2所示[4]。Linux內(nèi)核提供等待隊列這一功能來實現(xiàn)阻塞進程的喚醒。
圖2 阻塞I/O
等待隊列操作如下所示。
定義并初始化一個等待隊列:
wait_queue_head_t wqh;
init_waitqueue_head(&wqh);
設(shè)置等待事件:wait_event_interruptible (queue, condition); 該函數(shù)表示當?shù)却犃衠ueue被喚醒時,若condition為假則繼續(xù)阻塞,若condition為真則喚醒進程。該條件可以自行確立即可以查詢某一硬件狀態(tài)也可以使用軟件的辦法。
喚醒等待事件:wake_up_interruptible (wait_queue_head_t *queue);該函數(shù)一般在某些操作完成后需要喚醒等待進程的地方調(diào)用。如在中斷服務(wù)程序調(diào)用,那么當中斷到來并響應(yīng)時等待進程將被喚醒。
PCI有3種地址空間:PCI I/O空間、PCI內(nèi)存地址空間和PCI配置空間[5]。其中PCI配置空間中存放這PCI設(shè)備的配置信息,如PCI設(shè)備ID號、所使用I/O或內(nèi)存基地址、中斷號等。這些信息是在Linux掃描到PCI設(shè)備后主動寫入到對應(yīng)空間。
PCI設(shè)備總共有6個BAR的基地址寄存器(BAR0~BAR5),一般BAR0與BAR1為配置寄存器,BAR2~BAR5為本地地址空間。訪問這些空間前要判斷它是使用I/O空間還是內(nèi)存空間,兩者之間的訪問方式有所不同。并且如果是內(nèi)存空間還需要將它映射到核心虛地址空間中,然后才能根據(jù)映射得到的核心虛地址范圍,通過訪內(nèi)指令訪問這些I/O內(nèi)存資源。具體使用那個空間可以根據(jù)具體的PCI接口芯片數(shù)據(jù)手冊查詢。例如,本文使用的PCI9052芯片,BAR1使用I/O空間,BAR2使用內(nèi)存空間。
pci_driver結(jié)構(gòu)體中包含著PCI設(shè)備驅(qū)動的重要信息,它的結(jié)構(gòu)如下所示。
struct pci_driver{
char * name;
const struct pci_device_id * id_table;
int (* probe) (struct pci_dev * dev, const struct pci_device_id * id);
void (* remove) (struct pci_dev *dev);
……
}
其中id_table中存放著該設(shè)備驅(qū)動支持的PCI設(shè)備ID號。probe函數(shù)為探測函數(shù),主要用于申請空間、保存探測到的PCI配置信息如中斷號、寄存器基地址等。remove函數(shù)用于注銷設(shè)備。
pci_register_driver(struct pci_driver *driver)用于注冊PCI設(shè)備,當調(diào)用該函數(shù)時系統(tǒng)會掃描所有的PCI設(shè)備,當有設(shè)備的ID號與id_table中的ID號相同時就會調(diào)用一次probe函數(shù)。相對應(yīng)的注銷函數(shù)為pci_unregister_driver(struct pci_driver * driver)。
3.3.1 中斷信號
PCI設(shè)備使用INTA 、INTB 、INTC 和INTD 信號向系統(tǒng)發(fā)送中斷請求,一般對于具有單一功能的PCI設(shè)備僅僅使用INTA 信號。對于PCI9052接口芯片,它的中斷信號是在LINTi1或LINTi2接收到信號時產(chǎn)生的。
3.3.2 中斷注冊與處理
Linux驅(qū)動程序在使用中斷前,需要向系統(tǒng)注冊中斷。使用request_irq(…)函數(shù)完成對中斷號的申請、中斷處理函數(shù)的申明。如果該中斷號被其它設(shè)備使用了,應(yīng)當先釋放該中斷號再向系統(tǒng)申請PCI中斷。再根據(jù)數(shù)據(jù)手冊查看中斷使能位的偏移地址,完成對中斷的使能開啟中斷。例如PCI9052其中斷使能位的偏移地址為0x4ch。
當中斷信號來時,中斷處理函數(shù)除做一些基本操作外應(yīng)當喚醒被阻塞的讀操作。此時用戶空間才能通過系統(tǒng)調(diào)用從內(nèi)核空間中獲取數(shù)據(jù)。如果在等待隊列中使用軟件的方法確立條件,那么在喚醒阻塞進程前應(yīng)該先將喚醒條件置為真。在讀操作完成后應(yīng)該將喚醒條件置為假,等待下一次的中斷到來。
本文在unbuntu14.04操作系統(tǒng)下進行開發(fā),使用unbuntu14.04自帶GCC編譯器編譯。驅(qū)動程序、應(yīng)用程序和Makefile文件可以使用vim或gedit編譯器編寫。
將Makefile文件與驅(qū)動程序放到同一目錄下,在終端下進入該目錄執(zhí)行make命令。該目錄中會得到很多文件,其中后綴為.ko的文件為該驅(qū)動程序的可執(zhí)行文件。執(zhí)行以下步驟:
1)sudo insmod xxx.ko
將模塊加載進內(nèi)核,如果成功可以執(zhí)行cat /proc/device 查看該設(shè)備的設(shè)備號,執(zhí)行l(wèi)s /dev/驅(qū)動名 查看該設(shè)備的設(shè)備文件,執(zhí)行 cat /proc/interrupts 查看該設(shè)備的中斷號。
2)gcc xxx.c -o xxx 編譯應(yīng)用程序得到可執(zhí)行程序xxx
3)su 獲取權(quán)限
4)./xxx 運行應(yīng)用程序
實驗結(jié)果如圖3所示。圖中的第二、三列數(shù)據(jù)表示日期和時間,其中時間信息后面四位表示200 us時間刻度;第四列數(shù)據(jù)表示此時PCI同步時鐘卡與多少衛(wèi)星同步對時,如果顯示為0說明這時此時的時間信息來自于同步時鐘卡的實時時鐘;第五列數(shù)據(jù)表示時鐘源信息的質(zhì)量字節(jié);第六列表示同步時鐘卡的工作模式,有slave和master兩種工作模式;第七列數(shù)據(jù)表示同步時鐘卡的數(shù)據(jù)來源:GPS或者RTC。
圖3 實驗結(jié)果圖
為了測試其中斷功能,使用硬件電路生成一個周期為2.5 kHz的信號替代外部事件作為中斷信號源。從圖3可以看到每隔400 μs的時間刻度就讀取一次時鐘卡的信息并顯示。經(jīng)過長時間的測試沒有發(fā)現(xiàn)丟失中斷信號的現(xiàn)象。
采用PCI9052接口芯片實現(xiàn)對數(shù)據(jù)的傳輸,并且設(shè)計了硬件電路產(chǎn)生時間刻度以及將時間刻度發(fā)送到上位機的數(shù)據(jù)交互電路,以此達到上位機與同步時鐘卡的數(shù)據(jù)傳輸?shù)哪康?。在Linux下通過中斷方式獲取PCI同步時鐘卡的時間信息,經(jīng)測試表明中斷信號未出現(xiàn)丟失現(xiàn)象,數(shù)據(jù)的傳輸也穩(wěn)定可靠。工作在中斷方式下的PCI同步時鐘卡能夠快速有效的判斷電力系統(tǒng)的故障,為故障的排查提供有力的依據(jù)。
本文以Unbuntu14.04操作系統(tǒng)為平臺,以PCI同步時鐘卡為實際應(yīng)用實例,著重描述了Linux下字符設(shè)備驅(qū)動和其中斷機制的開發(fā)過程。Linux因為其開源,安全,可移植性高等優(yōu)勢,有越來越多的公司和個人使用它,這使得在Linux下開發(fā)設(shè)備擁有更加廣闊的市場前景。 基于INTx的PCI中斷一般會由多個設(shè)備共同使用,當某一設(shè)備的中斷信號到來時內(nèi)核不僅需要快速響應(yīng)還要通過輪詢的方式判斷中斷來源并調(diào)用中斷處理函數(shù),這樣大大降低了系統(tǒng)效率。并且PCI設(shè)備通常僅有4個中斷引腳,當PCI設(shè)備具有多種功能時中斷引腳就會被復(fù)用。因此設(shè)備驅(qū)動程序必須查詢設(shè)備產(chǎn)生的具體事件,這就會降低中斷處理速度。為解決以上問題提高中斷運行效率可以采用MSI中斷機制[6]。
[1] 張治煉. 基于GPS授時的本地同步時鐘的設(shè)計[D]. 成都:電子科技大學, 2012.
[2] 黃華華, 魏 豐, 鄧林杰. PCI同步時鐘卡的WDF驅(qū)動程序設(shè)計[J]. 數(shù)字技術(shù)與應(yīng)用, 2015(5):147-150.
[3] DANIEL P. BOVET, MARCO CESATI. 深入理解Linux內(nèi)核(第三版)[M]. 北京:中國電力出版社, 2007.
[4] 宋寶華. Linux設(shè)備驅(qū)動開發(fā)詳解[M]. 北京:機械工業(yè)出版社, 2016.
[5] 楊兵見, 魏 豐, 陳永志. Linux下的PCIE同步時鐘卡的設(shè)備驅(qū)動程序開發(fā)[J]. 計算機測量與控制, 2017, 25(1):98-104.
[6] 王 齊. PCI Express 體系結(jié)構(gòu)導(dǎo)讀[M]. 北京:機械工業(yè)出版社, 2010.