1. 程式人生 > >淺談Linux PCI裝置驅動

淺談Linux PCI裝置驅動

轉自 http://www.uml.org.cn/embeded/201205152.asp

淺談Linux PCI裝置驅動(一)

要弄清楚Linux PCI裝置驅動,首先要明白,所謂的Linux PCI裝置驅動實際包括Linux PCI裝置驅動和裝置本身驅動兩部分。不知道讀者理不理解這句話,本人覺得這句話很重要,對於PCI、USB這樣的驅動來說,必須要理解這個概念,才能明白該如何看待Linux下的PCI和USB以及類似的匯流排型的驅動。理由也很簡單,就是Linux PCI驅動是核心自帶的,或者說核心幫你寫好了!而我們需要完成的就是裝置本身的驅動,比如網絡卡驅動等。當然,並不是說核心幫咱們寫好了Linux PCI驅動我們什麼就不用做了,至少你要明白核心大致都幹了些什麼,這樣你才能明白你該幹什麼,如何完成裝置本身的驅動。這跟我們學習作業系統時要學習很多系統呼叫介面一樣的道理,不知道這些介面,怎麼用作業系統或者說作業系統給你提供的功能呢? 所以這裡我們就來研究下Linux PCI驅動到底都幹了些什麼,以便我們在此基礎上完成我們裝置本身的驅動。

在http://tldp.org/LDP/tlk/dd/pci.html這篇文章裡(整本書叫做The Linux Kernel,中文翻譯見http://oss.org.cn/ossdocs/linux/kernel/ 本文也參考了該中文翻譯) 提到了:

Linux PCI 初始化程式碼邏輯上分為三個部分:

(1)PCI裝置驅動程式

這個偽裝置驅動程式從匯流排0開始查詢PCI系統並且定位系統中所有的PCI裝置和PCI橋。它建立一個可以用來描述這個PCI系統拓樸層次的資料結構連結串列。並且對所有的發現的PCI橋編號。

(2)PCI BIOS

這個軟體層提供在bib-pci-bios歸約中描述的服務。雖然Alpha AXP不提供BIOS服務,在其Linux版本中包含了相應的功能。

(3)PCI Fixup

與特定系統相關的PCI初始化修補程式碼

而這裡主要就是探討Linux PCI裝置驅動,會在最後列出一段包含裝置本身驅動的示例程式碼,僅供參考。

一、概述及簡介

PCI(Periheral Component Interconnect)有三種地址空間:PCI I/O空間、PCI記憶體地址空間和PCI配置空間。其中,PCI I/O空間和PCI記憶體地址空間由裝置驅動程式使用,而PCI配置空間由Linux核心中的PCI初始化程式碼使用,這些程式碼用於配置PCI裝置,比如中斷號以及I/O或記憶體基地址。所以這裡的PCI裝置驅動就是要大致描述對於PCI裝置驅動,Linux核心都幫我們做了什麼(主),接著就是我們應該完成什麼(次)。

(1)Linux核心做了什麼

簡單的說,Linux核心主要就做了對PCI裝置的列舉和配置;這些工作都是在核心初始時完成的。

列舉:對於PCI匯流排,有一個叫做PCI橋的裝置用來將父匯流排與子匯流排連線。作為一種特殊的PCI裝置,PCI橋主要包括以下三種:

(1)Host/PCI橋: 用於連線CPU與PCI根匯流排,第1個根匯流排的編號為0。在PC中,記憶體控制器也通常被整合到Host/PCI橋裝置晶片中,因此Host/PCI橋通常也被稱為“北橋晶片組(North Bridge Chipset)”。

(2)PCI/ISA橋: 用於連線舊的ISA匯流排。通常,PCI中類似i8359A中斷控制器這樣的裝置也會被整合到PCI/ISA橋裝置中。因此,PCI/ISA橋通常也被稱為“南橋晶片組(South Bridge Chipset)”

(3)PCI-to-PCI橋(以下稱為PCI-PCI橋): 用於連線PCI主匯流排(Primary Bus)和次匯流排(Secondary Bus)。PCI-PCI橋所處的PCI匯流排稱為主匯流排,即次匯流排的父匯流排;PCI-PCI橋所連線的PCI匯流排稱為次匯流排,即主匯流排的子匯流排。

下圖摘自PCI Local Bus Specification Revision 2.1,可以看到PCI-PCI橋的Class Code(見圖3)就是0x060400。

CPU通過Host/PCI橋與一條PCI匯流排相連,處在這種位置上的PCI匯流排稱為根匯流排。PC機中通常只有一個Host/PCI橋,在一條PCI匯流排的基礎上,可以再通過PCI橋連線到其他次一層的匯流排,例如通過PCI-PCI橋可以連線到另一條PCI匯流排,通過PCI-ISA橋可以連線到一條ISA匯流排。事實上,現代PC機中的ISA匯流排正是通過PCI-ISA橋連線在PCI總線上的。這樣,通過使用PCI-PCI橋,就構築起了一個層次的、樹狀的PCI系統結構。對於上層的匯流排而言,連線在這條總線上的PCI橋也是一個裝置。但是這是一種特殊的裝置,它既是上層總線上的一個裝置,實際上又是上層匯流排的延伸。 所謂列舉,就是從Host/PCI橋開始進行探測和掃描,逐個“列舉”連線在第一條PCI總線上的所有裝置並記錄在案。如果其中的某個裝置是PCI-PCI橋,則又進一步再探測和掃描連在這個橋上的次級PCI匯流排。就這樣遞迴下去,直到窮盡系統中的所有PCI裝置。其結果,是在記憶體中建立起一棵代表著這些PCI匯流排和裝置的PCI樹。每個PCI裝置(包括PCI橋裝置)都由一個pci_dev結構體來表示,而每條PCI匯流排則由pci_bus結構來表示。你有通過PCI橋建立起的硬體裝置樹,我有記憶體中通過資料結構構建的軟體樹,多麼和諧 呵呵。

圖1 PCI系統示意圖

配置:PCI裝置中一般都帶有一些RAM和ROM 空間,通常的控制/狀態暫存器和資料暫存器也往往以RAM區間的形式出現,而這些區間的地址在裝置內部一般都是從0開始編址的,那麼當總線上掛接了多個裝置時,對這些空間的訪問就會產生衝突。所以,這些地址都要先對映到系統總線上,再進一步對映到核心的虛擬地址空間。而所謂的配置就是通過對PCI配置空間的暫存器進行操作從而完成地址的對映(只完成內部編址對映到匯流排地址的工作,而對映到核心的虛擬地址空間是由裝置本身的驅動要做的工作)。

(2)Linux核心怎麼做的

這裡首先要說明的是,對於PCI的裝置初始化(即上面提到的列舉和配置工作),PC機的BIOS和Linux核心都可以做。一般而言,只要是採用PCI匯流排的PC機,其BIOS就必須提供對PCI匯流排操作的支援,因而稱為PCI BIOS。而且最早Linux核心也是通過這種BIOS呼叫的方式來獲取系統中的PCI裝置資訊的,但是不是所有的平臺都有BIOS(如某些嵌入式系統),並且在實踐中也發現有些母板上的PCI BIOS存在這樣那樣的問題,所以後來就改由Linux核心自己動手了,自己動手 豐衣足食 呵呵。不過,Linux核心還是很體貼的在make menuconfig的選項裡為我們提供了自己選擇的權利,即PCI access mode,裡面提供了四個選項分別是BIOS、MMconfig、Direct和Any。Direct方式就是拋開BIOS而由核心自己完成初始化工作的意思。

二、開始我們的列舉與配置之路

注:為了更清晰,簡單的描述PCI裝置的初始化過程(因為2.4.18中還沒有引入裝置驅動模型,這樣可以讓我們專心研究PCI裝置驅動本身)。這裡是對Linux-2.4.18的核心進行的分析,主要原因大家從參考資料中也應該能明白,這裡很多就是參考了[1]中的資料來分析的。如果想學PCI裝置驅動,那麼應該好好看看[1]的第八章中的PCI匯流排一節。然後再能找到一個驅動的例子程式碼看看,就可以說算是對PCI裝置驅動入門了,當然,前提是都看懂了 呵呵。

廢話少說,下面進入正題。前面提到了PCI有三種地址空間,其中的PCI配置空間是給Linux核心中的PCI初始化程式碼用的,也就是我們這裡的列舉與配置時用到的。那麼這個PCI配置空間裡放的是什麼東西呢,顯然應該是暫存器,稱為配置暫存器組。當PCI裝置上電時,硬體保持未啟用狀態。即該裝置只會對配置事務做出響應。上電時,裝置上不會有記憶體和I/O埠對映到計算機的地址空間;其他裝置相關的功能,例如中斷報告,也被禁止。

PCI標準規定每個裝置的配置暫存器組最多可以有256位元組的連續空間,其中開頭的64位元組的用途和格式是標準的,稱為配置暫存器的頭部。系統中提供一些與硬體有關的機制,使得PCI配置程式碼可以檢測在一個給定的PCI總線上所有可能的PCI配置暫存器頭部,從而知道哪個PCI插槽上目前有裝置,哪個插槽上暫無裝置。這是通過讀PCI配置暫存器頭部上的某個域完成的(一般是“Vendor ID" 域)。如果一個插槽上為空,上述操作會返回一些錯誤返回值,如0xFFFFFFFF。這種頭部(指64位元組頭部)又有三種,其中“0型”(type 0)頭部用於一般的PCI裝置,“1型”頭部用於各種PCI-PCI橋, “2型”頭部是用於PCI-CardBus橋的,CardBus是膝上型電腦中使用的匯流排,我們不關心。而64位元組頭部中的16個位元組中又包含著有關頭部的型別、裝置的種類、裝置的一些性質、由誰製造等等資訊。根據這16個位元組中提供的資訊,來確定應該怎樣進一步解釋和處理剩餘頭部中的48個位元組。對於這16個位元組的地址,include/linux/pci.h中定義了這樣一些常數。

#define PCI_VENDOR_ID 0x00 /* 16 bits */

#define PCI_DEVICE_ID 0x02 /* 16 bits */

#define PCI_COMMAND 0x04 /* 16 bits */

#define PCI_STATUS 0x06 /* 16 bits */

#define PCI_CLASS_REVISION 0x08 /* High 24 bits are class, low 8 revision */

#define PCI_REVISION_ID 0x08 /* Revision ID */

#define PCI_CLASS_PROG 0x09 /* Reg. Level Programming Interface */

#define PCI_CLASS_DEVICE 0x0a /* Device class */

#define PCI_CACHE_LINE_SIZE 0x0c /* 8 bits */

#define PCI_LATENCY_TIMER 0x0d /* 8 bits */

#define PCI_HEADER_TYPE 0x0e /* 8 bits */

對應我們的圖3(見下)中的前16位元組。而且我們也看到了緊挨著PCI_HEADER_TYPE (即存放頭部型別的暫存器)下面定義的就是上面提到的三種類型的頭部:

#define PCI_HEADER_TYPE_NORMAL 0

#define PCI_HEADER_TYPE_BRIDGE 1

#define PCI_HEADER_TYPE_CARDBUS 2

在Linux系統上,可以通過cat /proc/pci 等命令檢視系統中所有PCI裝置的類別、型號以及廠商等等資訊,那就是從這些暫存器來的。下面是在虛擬機器中用lspci -x命令的資訊擷取(lspci命令也是使用/proc檔案作為其資訊來源):

00:00.0 Host bridge: Intel Corp. 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)

00: 86 80 90 71 06 00 00 02 01 00 00 06 00 00 00 00

10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

20: 00 00 00 00 00 00 00 00 00 00 00 00 ad 15 76 19

30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

首先要說明的是PCI暫存器是小端位元組序格式的。那麼根據最下面的PCI配置暫存器組的結構(圖 3),顯然這個Host bridge的Vendor ID是0x8086,我不說你也能猜到這個Vendor就是Intel了。

這裡有個問題要先說清楚,就是這些暫存器的地址問題,不然往後就進行不下去了。配置暫存器可以讓我們來進行配置以便完成PCI裝置上的儲存空間的訪問,但這些配置暫存器本身也位於PCI裝置地址空間中,如何訪問這部分空間也就成了我們整個初始化工作的一個入口點,就像每個可執行程式都要有入口點一樣。PCI採用的辦法是讓所有裝置的配置暫存器組都採用相同的地址,由所在匯流排的PCI橋在訪問時附加上其他條件來區分。而CPU則通過一個統一的入口地址向“宿主--PCI橋”發出命令,由相應的PCI橋間接的完成具體的讀寫。對於i386結構的處理器,PCI匯流排的設計者在I/O地址空間保留了8個位元組用於這個目的,那就是0xCF8~0xCFF。這8個位元組構成了兩個32位的暫存器,第一個是“地址暫存器”0xCF8,第二個是“資料暫存器”0xCFC。要訪問某個裝置中的某個配置暫存器時,CPU先往地址暫存器中寫入目標地址,然後通過資料暫存器讀寫資料。不過,寫入地址暫存器的目標地址是一種匯流排號、裝置號、功能號以及裝置暫存器地址在內的綜合地址。格式如圖2:

圖2 寫入地址暫存器0xCF8的綜合地址

這裡的匯流排號、裝置號和功能號是對配置暫存器地址的擴充,就是上面提到的附加的其他條件。首先每個PCI匯流排都有個匯流排號,主匯流排的匯流排號為0,其餘的則由CPU在列舉階段每當探測到一個PCI橋時便為其指定一個,依次遞增。裝置號一般代表著一塊PCI介面卡(更確切的說是PCI匯流排介面晶片),通常取決於插槽的位置。每塊PCI介面卡上可以有若干個功能模組,這些功能模組共用一個PCI匯流排介面晶片,包括其中用於地址對映的電子線路,以降低成本。從邏輯的角度說,每個“功能”實際上就是一個裝置(看過USB裝置驅動的人很眼熟吧 呵呵),所以裝置號和功能號合在一起又可以稱作“邏輯裝置號”,而每塊卡上最多可以容納8個裝置。顯然,這些欄位(指整個32bit)結合在一起就惟一確定了系統中的一項PCI邏輯裝置。開始時,只有0號匯流排可以訪問,在掃描0號匯流排時如果發現上面某個裝置是PCI橋,就為之指定一個新的匯流排號,例如1,這樣1號匯流排就可以訪問了,這就是列舉階段的任務之一。

現在請讀者考慮一個問題:當我們拿到一塊PCI網絡卡,我們把它插到PC的主機板上,打算寫個這個網絡卡的驅動。那麼第一步該幹什麼呢?讀者可以回顧前面的內容,既然我們說Linux核心幫我們做了裝置的列舉和配置工作,那麼我在寫網絡卡驅動之前是不是可以先看看Linux核心對我們的這個PCI網絡卡裝置完成的列舉工作的結果呢?或者直白些說,我把網絡卡插上了,現在Linux核心有沒有識別出這塊裝置呢? 注意識別出裝置跟能正常使用裝置是不同的概念,這很好理解。安裝過PC網絡卡驅動的人都知道,當裝置的驅動沒有安裝時,我們在裝置管理器中是可以看到這個裝置的,不過上面是一個黃色的大問號。而在Linux系統中,我們可以通過lspci命令來檢視。

下面是在LDD3的PCI驅動那一章擷取的一段內容: lspci 的輸出( pciutils 的一部分, 在大部分發布中都有)和在 /proc/pci 和 /porc/bus/pci 中的資訊排布. PCI 裝置的 sysfs 表示也顯示了這種定址方案, 還有 PCI 域資訊. 當顯示硬體地址時, 它可被顯示為 2 個值( 一個 8-位匯流排號和一個 8-位 裝置和功能號), 作為 3 個值( bus, device, 和 function), 或者作為 4 個值(domain, bus, device, 和 function); 所有的值常常用 16 進位制顯示.

例如, /proc/bus/pci/devices 使用一個單個16位欄位(來便於分析和排序), 而 /proc/bus/busnumber 劃分地址為3個欄位. 下面內容顯示了這些地址如何顯示, 只顯示了輸出行的開始:

$ lspci | cut -d: -f1-3

0000:00:00.0 Host bridge

0000:00:00.1 RAM memory

0000:00:00.2 RAM memory

0000:00:02.0 USB Controller

0000:00:04.0 Multimedia audio controller

0000:00:06.0 Bridge

0000:00:07.0 ISA bridge

0000:00:09.0 USB Controller

0000:00:09.1 USB Controller

0000:00:09.2 USB Controller

0000:00:0c.0 CardBus bridge

0000:00:0f.0 IDE interface

0000:00:10.0 Ethernet controller

0000:00:12.0 Network controller

0000:00:13.0 FireWire (IEEE 1394)

0000:00:14.0 VGA compatible controller

$ cat /proc/bus/pci/devices | cut -f1

0000

0001

0002

0010

0020

0030

0038

0048

0049

004a

0060

0078

0080

0090

0098

00a0

$ tree /sys/bus/pci/devices/

/sys/bus/pci/devices/

|-- 0000:00:00.0 -> ../../../devices/pci0000:00/0000:00:00.0

|-- 0000:00:00.1 -> ../../../devices/pci0000:00/0000:00:00.1

|-- 0000:00:00.2 -> ../../../devices/pci0000:00/0000:00:00.2

|-- 0000:00:02.0 -> ../../../devices/pci0000:00/0000:00:02.0

|-- 0000:00:04.0 -> ../../../devices/pci0000:00/0000:00:04.0

|-- 0000:00:06.0 -> ../../../devices/pci0000:00/0000:00:06.0

|-- 0000:00:07.0 -> ../../../devices/pci0000:00/0000:00:07.0

|-- 0000:00:09.0 -> ../../../devices/pci0000:00/0000:00:09.0

|-- 0000:00:09.1 -> ../../../devices/pci0000:00/0000:00:09.1

|-- 0000:00:09.2 -> ../../../devices/pci0000:00/0000:00:09.2

|-- 0000:00:0c.0 -> ../../../devices/pci0000:00/0000:00:0c.0

|-- 0000:00:0f.0 -> ../../../devices/pci0000:00/0000:00:0f.0

|-- 0000:00:10.0 -> ../../../devices/pci0000:00/0000:00:10.0

|-- 0000:00:12.0 -> ../../../devices/pci0000:00/0000:00:12.0

|-- 0000:00:13.0 -> ../../../devices/pci0000:00/0000:00:13.0

`-- 0000:00:14.0 -> ../../../devices/pci0000:00/0000:00:14.0

所有的 3 個裝置列表都以相同順序排列, 因為 lspci 使用 /proc 檔案作為它的資訊源. 拿 VGA 視訊控制器作一個例子, 0x00a0 意思是 0000:00:14.0 當劃分為域(16位), 匯流排(8位), 裝置(5位)和功能(3位).為什麼0x00a0對應的是0000:00:14.0呢,這就要看圖2中的內容了,根據圖2中的暫存器對應0x00a0就代表著匯流排(8位), 裝置(5位)和功能(3位).0x00a0=0000000010100000,很容易看出高8位是匯流排號也就是0。剩下的0xa0=10100000,可以看出如果低3位表示功能號,那麼剩下的10100就是裝置號,補全成8位的值就是00010100即0x14.

圖3 PCI配置暫存器組

淺談Linux PCI裝置驅動(二)

我們在 淺談Linux PCI裝置驅動(一)中(以下簡稱 淺談(一) )介紹了PCI的配置暫存器組,而Linux PCI初始化就是使用了這些暫存器來進行的。後面我們會舉個例子來說明Linux PCI裝置驅動的主要工作內容(不是全部內容),這裡只做文字性的介紹而不會涉及具體程式碼的分析,因為要分析程式碼的話,基本就是對 Linux核心原始碼情景分析(下冊)第八章的解讀,讀者若想分析程式碼,可以參考該書的內容,我們這裡就不去深入分析這些程式碼了。

Linux PCI裝置驅動程式碼必須掃描系統中所有的PCI匯流排,尋找系統中所有的PCI裝置(包括PCI-PCI橋裝置)。系統中的每條PCI匯流排都有個編號number,根PCI匯流排的編號為0。系統當前存在的所有根匯流排(因為可能存在不止一個Host/PCI橋,那麼就可能存在多條根匯流排) 都通過其pci_bus結構體中的node成員連結成一個全域性的根匯流排連結串列,其表頭由struct list_head型別的全域性變數pci_root_buses來描述,我們在/linux-2.4.18/linux/drivers/pci/pci.c的38行可以看到如下定義:

LIST_HEAD(pci_root_buses);

而根匯流排下面的所有下級匯流排則都通過其pci_bus結構體中的node成員連結到其父匯流排的children連結串列中。這樣,通過這兩種PCI匯流排連結串列,Linux核心就將所有的pci_bus結構體以一種倒置樹的方式組織起來。

另外,每個PCI裝置都由一個pci_dev結構體表示,每個pci_dev結構體都同時連入兩個佇列,一方面通過其成員global_list掛入一個總的pci_dev結構佇列(佇列頭是pci_devices);同時又通過成員bus_list掛入其所在匯流排的pci_dev結構佇列devices(佇列頭是pci_bus.devices,即該pci裝置所在的pci匯流排的devices佇列),並且使指標bus(指pci_dev結構體裡的bus成員)指向代表著其所在匯流排的pci_bus結構。如果具體的裝置是PCI-PCI橋,則還要使其指標subordinate指向代表著另一條PCI匯流排的pci_bus結構。同樣我們在/linux-2.4.18/linux/drivers/pci/pci.c的39行可以看到如下定義:

LIST_HEAD(pci_devices);

對於PCI裝置連結串列,我們可以通過圖1來理解。

注:該圖摘自Linux裝置驅動開發詳解 第21章 PCI裝置驅動。

圖1 Linux PCI裝置連結串列

而對於我們在淺談(一)中貼出的圖1的PCI系統結構示意圖,Linux核心中對應的資料結構如這裡的圖2所示。

圖2 Linux核心PCI資料結構

Linux PCI初始化程式碼從PCI匯流排0開始掃描,它通過讀取"Vendor ID"和"Device ID"來試圖發現每一個插槽上的裝置。如果發現了一個PCI-PCI橋,則建立一個pci_bus資料結構並且連入到由pci_root_buses指向的pci_bus和pci_dev資料結構組成的樹中。PCI初始化程式碼通過裝置類程式碼0x060400來判斷一個PCI裝置是否是PCI-PCI橋。然後,Linux核心開始構造這個橋裝置另一端的PCI匯流排和其上的裝置。如果還發現了橋裝置,就以同樣的步驟來進行構建。這個處理過程稱之為深度優先演算法。PCI-PCI橋橫跨在兩條匯流排之間,暫存器PCI_PRIMARY_BUS和PCI_SECONDARY_BUS的內容就說明了其上下兩端的匯流排號,其中PCI_SECONDARY_BUS就是該PCI-PCI橋所連線和控制的匯流排,而PCI_SUBORDINATE_BUS則說明自此以下、在以此為根的子樹中最大的匯流排號是什麼。

我們可以在/linux-2.4.18/linux/include/linux/pci.h看到如下定義:

112: /* Header type 1 (PCI-to-PCI bridges) */

113: #define PCI_PRIMARY_BUS 0x18 /* Primary bus number */

114: #define PCI_SECONDARY_BUS 0x19 /* Secondary bus number */

115: #define PCI_SUBORDINATE_BUS 0x1a /* Highest bus number behind the bridge */

由於在列舉階段做的是深度優先掃描,所以子樹中的匯流排號總是連續遞增的。當CPU往I/O暫存器0xCF8中寫入一個綜合地址以後,從0號匯流排開始,每個PCI-PCI橋會把綜合地址中的匯流排號與自身的匯流排號相比,如果相符就用邏輯裝置號在本總線上尋訪目標裝置;否則就進一步把這個匯流排號與PCI_SUBORDINATE_BUS中的內容相比,如果目標匯流排號落在當前子樹範圍中,就把綜合地址傳遞給其下的各個次層PCI-PCI橋,要不然就不予理睬。這樣,最終就會找到目標裝置。當然,這個過程只是在PCI裝置的配置階段需要這樣做,一旦配置完成,CPU就直接通過有關的匯流排地址訪問目標裝置了。

PCI-PCI橋要想正確傳遞對PCI I/O,PCI Memory或PCI Configuration地址空間的讀和寫請求,必須知道下列資訊:

(1)Primary Bus Number(主匯流排號)

該PCI-PCI橋所處的PCI匯流排稱為主匯流排。

(2)Secondary Bus Number(子匯流排號)

該PCI-PCI橋所連線的PCI匯流排稱為子匯流排/次匯流排號。

(3)Subordinate Bus Number

PCI匯流排的下屬PCI匯流排的匯流排編號最大值。有點繞,看後面的分析就明白了。

PCI I/O 和 PCI Memory 視窗

PCI橋的配置暫存器與一般的PCI裝置不同。一般PCI裝置可以有6個地址區間,外加一個ROM區間,代表著裝置上實際存在的儲存器或暫存器區間。而PCI橋,則本身並不一定有儲存器或暫存器區間,但是卻有三個用於地址過濾的區間。每個地址過濾區間決定了一個地址視窗,從CPU一側發出的地址,如果落在PCI橋的某個視窗內,就可以穿過PCI橋而到達其所連線的總線上。此外,PCI橋的命令暫存器中還有”memory access enable”和”I/O access enable ”的兩個控制位,當這兩個控制位為0時,這些視窗就全都關上了。在未完成對PCI匯流排的初始化之前,還沒有為PCI裝置上的各個區間分配合適的匯流排地址時,正是因為這兩個控制位為0,才不會對CPU一側造成干擾。例如, 對於淺談(一)的 PCI系統示意圖 ,僅當讀和寫請求中的PCI I/O或PCI memory地址屬於SCSI或Ethernet裝置時,PCI-PCI橋才將這些總線上的請求從PCI匯流排0傳遞到PCI匯流排1。這種過濾機制可以避免地址在系統中沒必要的繁衍。為了做到這點,每個PCI-PCI橋必須正確地被設定好它所負責的PCI I/O或PCI memory的起始地址和大小。當一個讀或寫請求地址落在其負責的範圍之內,這個請求將被對映到次級的PCI總線上。系統中的PCI-PCI橋一旦設定完畢,如果Linux中的裝置驅動程式存取的PCI I/O和PCI memory地址落在在這些視窗之內,那麼這些PCI-PCI橋就是透明的。這是個很重要的特性,使得Linux PCI裝置驅動程式開發者的工作容易些。

問題是配置一個PCI-PCI橋的時候,並不知道這個PCI-PCI橋的subordinate bus number。那麼就不知道該PCI橋下面是否還有其他的PCI-PCI橋。即使你知道,也不清楚如何對它們賦值。解決方法是利用上述的深度掃描演算法來掃描每個匯流排。每當發現PCI-PCI橋就對它進行賦值。當發現一個PCI-PCI橋時,可以確定它的secondary bus number。然後我們暫時先將其subordinate bus number賦值為0xFF。緊接著,開始掃描該PCI-PCI橋的downstream橋。這個過程看起來有點複雜,下面的例子將給出清晰的解釋:

圖3 配置PCI系統 第一步

PCI-PCI橋編號--第一步

以圖3的拓撲結構為例,掃描時首先發現的橋是Bridge1。Bridge 1的downstream PCI匯流排號碼被賦值1。自然該橋的secondary bus number也是1。其subordinate bus number暫時賦值為0xFF。上述賦值的含義是所有型別1的含有PCI匯流排1或更高(<255)的號碼的PCI配置地址將被Bridge 1傳遞到PCI匯流排1上。如果PCI匯流排號是1,Bridge 1 還負責將配置地址的型別轉換成型別0(對於這裡說的型別0和型別1,請參考淺談(一))。否則,就不做轉換。上述動作就是開始掃描匯流排1時Linux PCI初始化程式碼所完成的對匯流排0的配置工作。

圖4 配置PCI系統 第二步

PCI-PCI橋編號--第二步

由於Linux PCI裝置驅動使用深度優先演算法進行掃描,所以初始化程式碼開始掃描匯流排1。從而Bridge 2被發現。因為在Bridge 2下面發現不再有PCI-PCI橋,所以Bridge 2的subordinate bus number是2,等於它的secondary bus number。圖4顯示了在這個時刻匯流排和PCI-PCI橋的賦值情況。

圖5 配置PCI系統 第三步

PCI-PCI橋編號--第三步

Linux PCI裝置驅動程式碼從匯流排2的掃描中回來接著進行掃描匯流排1,發現Bridge 3。它的primary bus number被賦值為1,secondary bus number為3。因為匯流排3上還發現了PCI-PCI橋,所以Bridge 3的subordinate bus number暫時賦值0xFF。圖5顯示了這個時刻系統配置的狀態。到目前為止,含有匯流排號1,2,3的型別1的PCI配置都可以正確地傳送到相應的總線上。

圖6 配置PCI系統 第四步

PCI-PCI橋編號--第四步

現在Linux開始掃描PCI匯流排3,Bridge 3的downstream。PCI匯流排3上有另外一個PCI-PCI橋,Bridge 4。因此Bridge 4的primary bus number的值為3,secondary bus number為4。由於Bridge 4下面沒有別的橋裝置,所以Bridge 4的subordinate bus number為4。然後回到PCI-PCI Bridge 3。這時就將Bridge 3的subordinate bus number從0xFF改為4,表示匯流排4是從Bridge 3往下走的最遠的PCI-PCI橋。最後,Linux PCI裝置驅動程式碼將4以同樣的道理賦值給Bridge 1的subordinate bus number。圖6反映了系統最後的狀態。

注:淺談Linux PCI裝置驅動(二)暫時的整體結構就是這樣了,後續可能還會有些細節上的修補和新增。在此強烈推薦想學Linux PCI裝置驅動的朋友結合《Linux核心原始碼情景分析下冊》第八章和《Linux裝置驅動開發詳解》第21章 來學習。感謝您關注本文。

參考資料:

[1]Linux核心原始碼情景分析(下冊)

[2]Linux裝置驅動開發詳解

[3]Linux裝置驅動(第三版)

[4]核心Documentation下的pci.txt

[5]精通Linux裝置驅動開發

[6]http://tldp.org/LDP/tlk/dd/pci.html

[7]http://linux.die.net/man/8/lspci