PCI是一種廣泛採用的總線標準,它提供了許多優於其它總線標準(如EISA)的新特性,目前已經成爲計算機系統中應用最爲廣泛,並且最爲通用的總線標準。Linux的內核能較好地支持PCI總線,本文以Intel 386體系結構爲主,探討了在Linux下開發PCI設備驅動程序的基本框架。
一、PCI總線系統體系結構
PCI是外圍設備互連(Peripheral Component Interconnect)的簡稱,作爲一種通用的總線接口標準,它在目前的計算機系統中得到了非常廣泛的應用。PCI提供了一組完整的總線接口規範,其目的是描述如何將計算機系統中的外圍設備以一種結構化和可控化的方式連接在一起,同時它還刻畫了外圍設備在連接時的電氣特性和行爲規約,並且詳細定義了計算機系統中的各個不同部件之間應該如何正確地進行交互。
無論是在基於Intel芯片的PC機中,或是在基於Alpha芯片的工作站上,PCI毫無疑問都是目前使用最廣泛的一種總線接口標準。同舊式的ISA總線不同,PCI將計算機系統中的總線子系統與存儲子系統完全地分開,CPU通過一塊稱爲PCI橋(PCI-Bridge)的設備來完成同總線子系統的交互,如圖1所示。
圖1 PCI子系統的體系結構
由於使用了更高的時鐘頻率,因此PCI總線能夠獲得比ISA總線更好的整體性能。PCI總線的時鐘頻率一般在25MHz到33MHz範圍內,有些甚至能夠達到66MHz或者133MHz,而在64位系統中則最高能達到266MHz。儘管目前PCI設備大多采用32位數據總線,但PCI規範中已經給出了64位的擴展實現,從而使PCI總線能夠更好地實現平臺無關性,現在PCI總線已經能夠用於IA-32、Alpha、PowerPC、SPARC64和IA-64等體系結構中。
PCI總線具有三個非常顯著的優點,使得它能夠完成最終取代ISA總線這一歷史使命:
在計算機和外設間傳輸數據時具有更好的性能;
能夠儘量獨立於具體的平臺;
可以很方便地實現即插即用。
圖2是一個典型的基於PCI總線的計算機系統邏輯示意圖,系統的各個部分通過PCI總線和PCI-PCI橋連接在一起。從圖中不難看出,CPU和RAM需要通過PCI橋連接到PCI總線0(即主PCI總線),而具有PCI接口的顯卡則可以直接連接到主PCI總線上。PCI-PCI橋是一個特殊的PCI設備,它負責將PCI總線0和PCI總線1(即從PCI主線)連接在一起,通常PCI總線1稱爲PCI-PCI橋的下游(downstream),而PCI總線0則稱爲PCI-PCI橋的上游(upstream)。圖中連接到從PCI總線上的是SCSI卡和以太網卡。爲了兼容舊的ISA總線標準,PCI總線還可以通過PCI-ISA橋來連接ISA總線,從而能夠支持以前的ISA設備。圖中ISA總線上連接着一個多功能I/O控制器,用於控制鍵盤、鼠標和軟驅。
圖2 PCI系統示意圖
在此我只對PCI總線系統體系結構作了概括性介紹,如果讀者想進一步瞭解,David A Rusling在The Linux Kernel(http://tldp.org/LDP/tlk/dd/pci.html)中對Linux的PCI子系統有比較詳細的介紹。
3. 設備驅動程序結構
瞭解設備驅動程序的基本結構(或者稱爲框架),對開發人員而言是非常重要的,Linux的設備驅動程序大致可以分爲如下幾個部分:驅動程序的註冊與註銷、設備的打開與釋放、設備的讀寫操作、設備的控制操作、設備的中斷和輪詢處理。
驅動程序的註冊與註銷
向系統增加一個驅動程序意味着要賦予它一個主設備號,這可以通過在驅動程序的初始化過程中調用register_chrdev( )或者register_blkdev( )來完成。而在關閉字符設備或者塊設備時,則需要通過調用unregister_chrdev( )或unregister_blkdev( )從內核中註銷設備,同時釋放佔用的主設備號。
設備的打開與釋放
打開設備是通過調用file_operations結構中的函數open( )來完成的,它是驅動程序用來爲今後的操作完成初始化準備工作的。在大部分驅動程序中,open( )通常需要完成下列工作:
1.檢查設備相關錯誤,如設備尚未準備好等。
2.如果是第一次打開,則初始化硬件設備。
3.識別次設備號,如果有必要則更新讀寫操作的當前位置指針f_ops。
4.分配和填寫要放在file->private_data裏的數據結構。
5.使用計數增1。
釋放設備是通過調用file_operations結構中的函數release( )來完成的,這個設備方法有時也被稱爲close( ),它的作用正好與open( )相反,通常要完成下列工作:
1.使用計數減1。
2.釋放在file->private_data中分配的內存。
3.如果使用計算爲0,則關閉設備。
設備的讀寫操作
字符設備的讀寫操作相對比較簡單,直接使用函數read( )和write( )就可以了。但如果是塊設備的話,則需要調用函數block_read( )和block_write( )來進行數據讀寫,這兩個函數將向設備請求表中增加讀寫請求,以便Linux內核可以對請求順序進行優化。由於是對內存緩衝區而不是直接對設備進行操作的,因此能很大程度上加快讀寫速度。如果內存緩衝區中沒有所要讀入的數據,或者需要執行寫操作將數據寫入設備,那麼就要執行真正的數據傳輸,這是通過調用數據結構blk_dev_struct中的函數request_fn(
)來完成的。
設備的控制操作
除了讀寫操作外,應用程序有時還需要對設備進行控制,這可以通過設備驅動程序中的函數ioctl( )來完成。ioctl( )的用法與具體設備密切關聯,因此需要根據設備的實際情況進行具體分析。
設備的中斷和輪詢處理
對於不支持中斷的硬件設備,讀寫時需要輪流查詢設備狀態,以便決定是否繼續進行數據傳輸。如果設備支持中斷,則可以按中斷方式進行操作。
三、PCI驅動程序實現
1. 關鍵數據結構
PCI設備上有三種地址空間:PCI的I/O空間、PCI的存儲空間和PCI的配置空間。CPU可以訪問PCI設備上的所有地址空間,其中I/O空間和存儲空間提供給設備驅動程序使用,而配置空間則由Linux內核中的PCI初始化代碼使用。內核在啓動時負責對所有PCI設備進行初始化,配置好所有的PCI設備,包括中斷號以及I/O基址,並在文件/proc/pci中列出所有找到的PCI設備,以及這些設備的參數和屬性。
Linux驅動程序通常使用結構(struct)來表示一種設備,而結構體中的變量則代表某一具體設備,該變量存放了與該設備相關的所有信息。好的驅動程序都應該能驅動多個同種設備,每個設備之間用次設備號進行區分,如果採用結構數據來代表所有能由該驅動程序驅動的設備,那麼就可以簡單地使用數組下標來表示次設備號。
在PCI驅動程序中,下面幾個關鍵數據結構起着非常核心的作用:
pci_driver
這個數據結構在文件include/linux/pci.h裏,這是Linux內核版本2.4之後爲新型的PCI設備驅動程序所添加的,其中最主要的是用於識別設備的id_table結構,以及用於檢測設備的函數probe( )和卸載設備的函數remove( ):
|
pci_dev
這個數據結構也在文件include/linux/pci.h裏,它詳細描述了一個PCI設備幾乎所有的硬件信息,包括廠商ID、設備ID、各種資源等:
|
2. 基本框架
在用模塊方式實現PCI設備驅動程序時,通常至少要實現以下幾個部分:初始化設備模塊、設備打開模塊、數據讀寫和控制模塊、中斷處理模塊、設備釋放模塊、設備卸載模塊。下面給出一個典型的PCI設備驅動程序的基本框架,從中不難體會到這幾個關鍵模塊是如何組織起來的。
|
上面這段代碼給出了一個典型的PCI設備驅動程序的框架,是一種相對固定的模式。需要注意的是,同加載和卸載模塊相關的函數或數據結構都要在前面加上__init、__exit等標誌符,以使同普通函數區分開來。構造出這樣一個框架之後,接下去的工作就是如何完成框架內的各個功能模塊了。
3. 初始化設備模塊
在Linux系統下,想要完成對一個PCI設備的初始化,需要完成以下工作:
檢查PCI總線是否被Linux內核支持;
檢查設備是否插在總線插槽上,如果在的話則保存它所佔用的插槽的位置等信息。
讀出配置頭中的信息提供給驅動程序使用。
當Linux內核啓動並完成對所有PCI設備進行掃描、登錄和分配資源等初始化操作的同時,會建立起系統中所有PCI設備的拓撲結構,此後當PCI驅動程序需要對設備進行初始化時,一般都會調用如下的代碼:
|
驅動程序首先調用函數pci_present( )檢查PCI總線是否已經被Linux內核支持,如果系統支持PCI總線結構,這個函數的返回值爲0,如果驅動程序在調用這個函數時得到了一個非0的返回值,那麼驅動程序就必須得中止自己的任務了。在2.4以前的內核中,需要手工調用pci_find_device( )函數來查找PCI設備,但在2.4以後更好的辦法是調用pci_register_driver( )函數來註冊PCI設備的驅動程序,此時需要提供一個pci_driver結構,在該結構中給出的probe探測例程將負責完成對硬件的檢測工作。
|
4. 打開設備模塊
在這個模塊裏主要實現申請中斷、檢查讀寫模式以及申請對設備的控制權等。在申請控制權的時候,非阻塞方式遇忙返回,否則進程主動接受調度,進入睡眠狀態,等待其它進程釋放對設備的控制權。
|
5. 數據讀寫和控制信息模塊
PCI設備驅動程序可以通過demo_fops 結構中的函數demo_ioctl( ),嚮應用程序提供對硬件進行控制的接口。例如,通過它可以從I/O寄存器裏讀取一個數據,並傳送到用戶空間裏:
|
事實上,在demo_fops裏還可以實現諸如demo_read( )、demo_mmap( )等操作,Linux內核源碼中的driver目錄裏提供了許多設備驅動程序的源代碼,找那裏可以找到類似的例子。在對資源的訪問方式上,除了有I/O指令以外,還有對外設I/O內存的訪問。對這些內存的操作一方面可以通過把I/O內存重新映射後作爲普通內存進行操作,另一方面也可以通過總線主DMA(Bus Master DMA)的方式讓設備把數據通過DMA傳送到系統內存中。
6. 中斷處理模塊
PC的中斷資源比較有限,只有0~15的中斷號,因此大部分外部設備都是以共享的形式申請中斷號的。當中斷髮生的時候,中斷處理程序首先負責對中斷進行識別,然後再做進一步的處理。
|
7. 釋放設備模塊
釋放設備模塊主要負責釋放對設備的控制權,釋放佔用的內存和中斷等,所做的事情正好與打開設備模塊相反:
|
8. 卸載設備模塊
卸載設備模塊與初始化設備模塊是相對應的,實現起來相對比較簡單,主要是調用函數pci_unregister_driver( )從Linux內核中註銷設備驅動程序:
|
四、小結
PCI總線不僅是目前應用廣泛的計算機總線標準,而且是一種兼容性最強、功能最全的計算機總線。而Linux作爲一種新的操作系統,其發展前景是無法估量的,同時也爲PCI總線與各種新型設備互連成爲可能。由於Linux源碼開放,因此給連接到PCI總線上的任何設備編寫驅動程序變得相對容易。本文介紹如何編譯Linux下的PCI驅動程序,針對的內核版本是2.4。
參考資料: David A Rusling在 The Linux Kernel中對Linux的PCI子系統進行了比較詳細的介紹。
Linux PCI-HOWTO是瞭解Linux下PCI設備的最好讀物。
毛德操,胡希明,Linux內核源代碼情景分析,杭州:浙江大學出版社,2001
Alessandro Rubini,,Linux Device Drivers(2nd Edition) USA:O’Reilly,2001
Tomshanley,DonAderson,PCI系統結構(第四版),北京:電子工業出版社,2000
關於作者:肖文鵬,北京理工大學計算機系的一名碩士研究生,主要從事操作系統和分佈式計算環境的研究,喜愛Linux和Python。你可以通過 [email protected]與他取得聯繫。