PCI是一種普遍採用的總線標準,它提供了許多優於其它總線標準(如EISA)的新特性,目前已經成爲計算機系統中應用最爲普遍,而且最爲通用的總 線標準。Linux的內核能較好地支持PCI總線,本文以Intel 386體系結構爲主,探討了在Linux下開發PCI設備驅動程序的基本框架。html
1、PCI總線系統體系結構node
PCI是外圍設備互連(Peripheral Component Interconnect)的簡稱,做爲一種通用的總線接口標準,它在目前的計算機系統中獲得了很是普遍的應用。PCI提供了一組完整的總線接口規範,其 目的是描述如何將計算機系統中的外圍設備以一種結構化和可控化的方式鏈接在一塊兒,同時它還刻畫了外圍設備在鏈接時的電氣特性和行爲規約,而且詳細定義了計 算機系統中的各個不一樣部件之間應該如何正確地進行交互。 linux
不管是在基於Intel芯片的PC機中,或是在基於Alpha芯片的工做站上,PCI毫無疑問都是目前使用最普遍的一種總線接口標準。同舊式的 ISA總線不一樣,PCI將計算機系統中的總線子系統與存儲子系統徹底地分開,CPU經過一塊稱爲PCI橋(PCI-Bridge)的設備來完成同總線子系 統的交互,如圖1所示。數組
因爲使用了更高的時鐘頻率,所以PCI總線可以得到比ISA總線更好的總體性能。PCI總線的時鐘頻率通常在25MHz到33MHz範圍內,有些甚 至可以達到66MHz或者133MHz,而在64位系統中則最高能達到266MHz。儘管目前PCI設備大多采用32位數據總線,但PCI規範中已經給出 了64位的擴展實現,從而使PCI總線可以更好地實現平臺無關性,如今PCI總線已經可以用於IA-3二、Alpha、PowerPC、SPARC64和 IA-64等體系結構中。 數據結構
PCI總線具備三個很是顯著的優勢,使得它可以完成最終取代ISA總線這一歷史使命: app
圖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系統示意圖async
在此我只對PCI總線系統體系結構做了歸納性介紹,若是讀者想進一步瞭解,David A Rusling在The Linux Kernel(http://tldp.org/LDP/tlk/dd/pci.html)中對Linux的PCI子系統有比較詳細的介紹。函數
Linux將全部外部設備當作是一類特殊文件,稱之爲「設備文件」,若是說系統調用是Linux內核和應用程序之間的接口,那麼設備驅動程序則能夠 當作是Linux內核與外部設備之間的接口。設備驅動程序嚮應用程序屏蔽了硬件在實現上的細節,使得應用程序能夠像操做普通文件同樣來操做外部設備。
Linux抽象了對硬件的處理,全部的硬件設備均可以像普通文件同樣來看待:它們可使用和操做文件相同的、標準的系統調用接口來完成打開、關閉、 讀寫和I/O控制操做,而驅動程序的主要任務也就是要實現這些系統調用函數。Linux系統中的全部硬件設備都使用一個特殊的設備文件來表示,例如,系統 中的第一個IDE硬盤使用/dev/hda表示。每一個設備文件對應有兩個設備號:一個是主設備號,標識該設備的種類,也標識了該設備所使用的驅動程序;另 一個是次設備號,標識使用同一設備驅動程序的不一樣硬件設備。設備文件的主設備號必須與設備驅動程序在登陸該設備時申請的主設備號一致,不然用戶進程將沒法 訪問到設備驅動程序。
在Linux操做系統下有兩類主要的設備文件:一類是字符設備,另外一類則是塊設備。字符設備是以字節爲單位逐個進行I/O操做的設備,在對字符設備 發出讀寫請求時,實際的硬件I/O緊接着就發生了,通常來講字符設備中的緩存是無關緊要的,並且也不支持隨機訪問。塊設備則是利用一塊系統內存做爲緩衝 區,當用戶進程對設備進行讀寫請求時,驅動程序先查看緩衝區中的內容,若是緩衝區中的數據能知足用戶的要求就返回相應的數據,不然就調用相應的請求函數來 進行實際的I/O操做。塊設備主要是針對磁盤等慢速設備設計的,其目的是避免耗費過多的CPU時間來等待操做的完成。通常說來,PCI卡一般都屬於字符設 備。
全部已經註冊(即已經加載了驅動程序)的硬件設備的主設備號能夠從/proc/devices文件中獲得。使用mknod命令能夠建立指定類型的設備文件,同時爲其分配相應的主設備號和次設備號。例如,下面的命令:
將創建一個主設備號爲6,次設備號爲0的字符設備文件/dev/lp0。當應用程序對某個設備文件進行系統調用時,Linux內核會根據該設備文件 的設備類型和主設備號調用相應的驅動程序,並從用戶態進入到核心態,再由驅動程序判斷該設備的次設備號,最終完成對相應硬件的操做。
Linux中的I/O子系統向內核中的其餘部分提供了一個統一的標準設備接口,這是經過include/linux/fs.h中的數據結構file_operations來完成的:
當應用程序對設備文件進行諸如open、close、read、write等操做時,Linux內核將經過file_operations結構訪問 驅動程序提供的函數。例如,當應用程序對設備文件執行讀操做時,內核將調用file_operations結構中的read函數。
Linux下的設備驅動程序能夠按照兩種方式進行編譯,一種是直接靜態編譯成內核的一部分,另外一種則是編譯成能夠動態加載的模塊。若是編譯進內核的話,會增長內核的大小,還要改動內核的源文件,並且不能動態地卸載,不利於調試,全部推薦使用模塊方式。
從本質上來說,模塊也是內核的一部分,它不一樣於普通的應用程序,不能調用位於用戶態下的C或者C++庫函數,而只能調用Linux內核提供的函數,在/proc/ksyms中能夠查看到內核提供的全部函數。
在以模塊方式編寫驅動程序時,要實現兩個必不可少的函數init_module( )和cleanup_module( ),並且至少要包含<linux/krernel.h>和<linux/module.h>兩個頭文件。在用gcc編譯內核模塊 時,須要加上
這幾個參數,編譯生成的模塊(通常爲.o文件)可使用命令insmod載入Linux內核,從而成爲內核的一個組成部分,此時內核會調用模塊中的 函數init_module( )。當不須要該模塊時,可使用rmmod命令進行卸載,此進內核會調用模塊中的函數cleanup_module( )。任什麼時候候均可以使用命令lsmod來查看目前已經加載的模塊以及正在使用該模塊的用戶數。
瞭解設備驅動程序的基本結構(或者稱爲框架),對開發人員而言是很是重要的,Linux的設備驅動程序大體能夠分爲以下幾個部分:驅動程序的註冊與註銷、設備的打開與釋放、設備的讀寫操做、設備的控制操做、設備的中斷和輪詢處理。
向系統增長一個驅動程序意味着要賦予它一個主設備號,這能夠經過在驅動程序的初始化過程當中調用register_chrdev( )或者register_blkdev( )來完成。而在關閉字符設備或者塊設備時,則須要經過調用unregister_chrdev( )或unregister_blkdev( )從內核中註銷設備,同時釋放佔用的主設備號。
打開設備是經過調用file_operations結構中的函數open( )來完成的,它是驅動程序用來爲從此的操做完成初始化準備工做的。在大部分驅動程序中,open( )一般須要完成下列工做:
釋放設備是經過調用file_operations結構中的函數release( )來完成的,這個設備方法有時也被稱爲close( ),它的做用正好與open( )相反,一般要完成下列工做:
字符設備的讀寫操做相對比較簡單,直接使用函數read( )和write( )就能夠了。但若是是塊設備的話,則須要調用函數block_read( )和block_write( )來進行數據讀寫,這兩個函數將向設備請求表中增長讀寫請求,以便Linux內核能夠對請求順序進行優化。因爲是對內存緩衝區而不是直接對設備進行操做 的,所以能很大程度上加快讀寫速度。若是內存緩衝區中沒有所要讀入的數據,或者須要執行寫操做將數據寫入設備,那麼就要執行真正的數據傳輸,這是經過調用 數據結構blk_dev_struct中的函數request_fn( )來完成的。
除了讀寫操做外,應用程序有時還須要對設備進行控制,這能夠經過設備驅動程序中的函數ioctl( )來完成。ioctl( )的用法與具體設備密切關聯,所以須要根據設備的實際狀況進行具體分析。
對於不支持中斷的硬件設備,讀寫時須要輪流查詢設備狀態,以便決定是否繼續進行數據傳輸。若是設備支持中斷,則能夠按中斷方式進行操做。
PCI設備上有三種地址空間:PCI的I/O空間、PCI的存儲空間和PCI的配置空間。CPU能夠訪問PCI設備上的全部地址空間,其中I/O空 間和存儲空間提供給設備驅動程序使用,而配置空間則由Linux內核中的PCI初始化代碼使用。內核在啓動時負責對全部PCI設備進行初始化,配置好全部 的PCI設備,包括中斷號以及I/O基址,並在文件/proc/pci中列出全部找到的PCI設備,以及這些設備的參數和屬性。
Linux驅動程序一般使用結構(struct)來表示一種設備,而結構體中的變量則表明某一具體設備,該變量存放了與該設備相關的全部信息。好的 驅動程序都應該能驅動多個同種設備,每一個設備之間用次設備號進行區分,若是採用結構數據來表明全部能由該驅動程序驅動的設備,那麼就能夠簡單地使用數組下 標來表示次設備號。
在PCI驅動程序中,下面幾個關鍵數據結構起着很是核心的做用:
這個數據結構在文件include/linux/pci.h裏,這是Linux內核版本2.4以後爲新型的PCI設備驅動程序所添加的,其中最主要的是用於識別設備的id_table結構,以及用於檢測設備的函數probe( )和卸載設備的函數remove( ):
這個數據結構也在文件include/linux/pci.h裏,它詳細描述了一個PCI設備幾乎全部的硬件信息,包括廠商ID、設備ID、各類資源等:
在用模塊方式實現PCI設備驅動程序時,一般至少要實現如下幾個部分:初始化設備模塊、設備打開模塊、數據讀寫和控制模塊、中斷處理模塊、設備釋放模塊、設備卸載模塊。下面給出一個典型的PCI設備驅動程序的基本框架,從中不難體會到這幾個關鍵模塊是如何組織起來的。
上面這段代碼給出了一個典型的PCI設備驅動程序的框架,是一種相對固定的模式。須要注意的是,同加載和卸載模塊相關的函數或數據結構都要在前面加 上__init、__exit等標誌符,以使同普通函數區分開來。構造出這樣一個框架以後,接下去的工做就是如何完成框架內的各個功能模塊了。
在Linux系統下,想要完成對一個PCI設備的初始化,須要完成如下工做:
當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探測例程將負責完成對硬件的檢測工做。
在這個模塊裏主要實現申請中斷、檢查讀寫模式以及申請對設備的控制權等。在申請控制權的時候,非阻塞方式遇忙返回,不然進程主動接受調度,進入睡眠狀態,等待其它進程釋放對設備的控制權。
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傳送到系統內存中。
PC的中斷資源比較有限,只有0~15的中斷號,所以大部分外部設備都是以共享的形式申請中斷號的。當中斷髮生的時候,中斷處理程序首先負責對中斷進行識別,而後再作進一步的處理。
釋放設備模塊主要負責釋放對設備的控制權,釋放佔用的內存和中斷等,所作的事情正好與打開設備模塊相反:
卸載設備模塊與初始化設備模塊是相對應的,實現起來相對比較簡單,主要是調用函數pci_unregister_driver( )從Linux內核中註銷設備驅動程序:
PCI總線不只是目前應用普遍的計算機總線標準,並且是一種兼容性最強、功能最全的計算機總線。而Linux做爲一種新的操做系統,其發展前景是無 法估量的,同時也爲PCI總線與各類新型設備互連成爲可能。因爲Linux源碼開放,所以給鏈接到PCI總線上的任何設備編寫驅動程序變得相對容易。本文 介紹如何編譯Linux下的PCI驅動程序,針對的內核版本是2.4。
引用於"隨點BBS" "2beanet":www.2beanet.com/bbs http://www.2beanet.com/