2016-10-09
其實之前是簡單學習過PCI設備的相關知識,但是總感覺 自己的理解很函數,很多東西說不清楚,正好今天接着寫這篇文章自己重新梳理一下,文章想要分為三部分,首先介紹PCI設備硬件相關的知識,然后介紹LINux內核中對PCI設備的支持。本節講第一部分。
PCI總線在目前計算機總線系統中占據舉足輕重的地位,其良好的擴展性,地址統一分配和總線競爭的處理相對於其他總線而言都具有絕對優勢。

一條PCI總線一般有32個接口,即可以連接32個PCI接口卡,而一個接口卡對應一個外部設備,注意這里的外部設備可以有多個功能(最多八個),每一個功能稱為邏輯設備。每一個邏輯設備對應一個PCI配置空間。對於邏輯設備后面還會詳細解釋,這里先說配置空間的問題。PCI配置空間可以說是記錄了關於此設備的詳細信息。PCI配置空間最大256個字節,其中起先的64個字節的格式是預定義好的。當然並非所有的項都必須填充,位置是固定了,沒有用到可以填充0。而前16個字節的格式是一定的。包含頭部的類型、設備的總類、設備的性質以及制造商等。
PCi配置空間前64個字節格式如下:
需要注意的有一下幾項:
ClassCode 用於將設備分到具體的功能組,該字段分為兩部分,前8個bit表示基類即大類別,后8個比特表示基類的一個子類。比如PCI_BASE_CLASS_STORAGE表示大類大容量存儲器,而PCI_CLASS_STORAGE_IDE表明這個IDE控制器。
HeaderType表明頭部類型。一般區分為0型頭部(PCI設備)1型頭部(PCI橋),注意不同頭部的配置空間格式有差異。這里我們主要先描述0型頭部即普通PCi設備的配置空間。
PCI HeaderType為一個字節的大小,最高位為0表示單功能,最高位為1表示多功能(即前面描述的邏輯設備),單功能情況下一個PCI設備就是一個邏輯設備。低7位表示頭部類型。
前16個字節都是一些基本的信息就不在多說,重點看下接下來的6個BAR空間。每個BAR記錄了該設備映射的一段地址空間。為了區分IO空間和IO內存,這里我們分開描述:
當BAR最后一位為0表示這是映射的IO內存,為1是表示這是IO 端口,當是IO內存的時候1-2位表示內存的類型,bit 2為1表示采用64位地址,為0表示采用32位地址。bit1為1表示區間大小超過1M,為0表示不超過1M.bit3表示是否支持可預取。
而相對於IO內存,當最后一位為1時表示映射的IO地址空間。IO地址空間一般不支持預取,所以這里是29位的地址。
一般情況下,6個BAR是足夠使用的,大部分情況都是3-4個BAR。而這些BAR映射的空間是連續的,即這些BAR共同描述設備的地址空間范圍
除了6個基本的BAR空間們還有一個額外的配置ROM區間,區間最低位表示是否使用ROM區間,高21位表示地址。中間的是保留項。其余原理和上面類似。
接下來需要注意的就是中斷,因為大部分外設和系統交互就是通過中斷的方式。。
由配置空間中的IRQ Pin決定設備是否支持中斷,1表示支持,0表示不支持。假如支持中斷,IRQ Line表記錄下中斷號。
PCI橋的配置空間
PCI橋同樣是連接在PCI總線接口卡上的一個設備,只不過是一個橋設備,連接一條PCI總線。既然同屬於設備,那么它同樣也就有設備的配置空間,只是它的配置空間和普通設備的有些差異。PCI橋的配置空間在系統軟件遍歷PCI總線樹的時候配置,並不需要專門的PCI驅動,故稱為透明橋。PCI橋連接兩條總線,和HOst 橋近的稱為上游總線(Primary Bus ),遠的一條稱為下游總線(Secondry Bus)。PCI橋的配置空間在前16個字節的格式和普通PCI設備並無區別,另外,橋還保留了普通設備的前兩個BAR空間。所以從配置空間的0x18開始有了橋設備自身的配置格式。如前所述,橋設備記錄了上游總線和下游總線,以及橋下最大的總線號。這里還有一個比較重要的概念就是窗口。在PCI橋的配置空間有三個窗口:IO地址區間窗口、存儲器區間窗口、可預取存儲器地址窗口。實際上每個窗口都是一段地址區間,就像一個門,規定了橋下設備映射的區間。從北橋出來的地址,如果在該區間內,就可以穿過該橋到達次級總線,這樣依次尋找設備。反過來,從南橋出來的地址及由設備發出的,只有地址不在該區間范圍內才可以穿過該橋。因為同一條總線上的設備交互不需要外部空間。就像是內網傳輸和公網傳輸一樣的道理。
內核中關於橋配置空間的定義如下:
/* Header type 1 (PCI-to-PCI bridges) */ #define PCI_PRIMARY_BUS 0x18 /* Primary bus number */ #define PCI_SECONDARY_BUS 0x19 /* Secondary bus number */ #define PCI_SUBORDINATE_BUS 0x1a /* Highest bus number behind the bridge */ #define PCI_SEC_LATENCY_TIMER 0x1b /* Latency timer for secondary interface */ #define PCI_IO_BASE 0x1c /* I/O range behind the bridge */ #define PCI_IO_LIMIT 0x1d #define PCI_IO_RANGE_TYPE_MASK 0x0fUL /* I/O bridging type */ #define PCI_IO_RANGE_TYPE_16 0x00 #define PCI_IO_RANGE_TYPE_32 0x01 #define PCI_IO_RANGE_MASK (~0x0fUL) /* Standard 4K I/O windows */ #define PCI_IO_1K_RANGE_MASK (~0x03UL) /* Intel 1K I/O windows */ #define PCI_SEC_STATUS 0x1e /* Secondary status register, only bit 14 used */ #define PCI_MEMORY_BASE 0x20 /* Memory range behind */ #define PCI_MEMORY_LIMIT 0x22 #define PCI_MEMORY_RANGE_TYPE_MASK 0x0fUL #define PCI_MEMORY_RANGE_MASK (~0x0fUL) #define PCI_PREF_MEMORY_BASE 0x24 /* Prefetchable memory range behind */ #define PCI_PREF_MEMORY_LIMIT 0x26 #define PCI_PREF_RANGE_TYPE_MASK 0x0fUL #define PCI_PREF_RANGE_TYPE_32 0x00 #define PCI_PREF_RANGE_TYPE_64 0x01 #define PCI_PREF_RANGE_MASK (~0x0fUL) #define PCI_PREF_BASE_UPPER32 0x28 /* Upper half of prefetchable memory range */ #define PCI_PREF_LIMIT_UPPER32 0x2c #define PCI_IO_BASE_UPPER16 0x30 /* Upper half of I/O addresses */ #define PCI_IO_LIMIT_UPPER16 0x32 /* 0x34 same as for htype 0 */ /* 0x35-0x3b is reserved */ #define PCI_ROM_ADDRESS1 0x38 /* Same as PCI_ROM_ADDRESS, but for htype 1 */ /* 0x3c-0x3d are same as for htype 0 */ #define PCI_BRIDGE_CONTROL 0x3e #define PCI_BRIDGE_CTL_PARITY 0x01 /* Enable parity detection on secondary interface */ #define PCI_BRIDGE_CTL_SERR 0x02 /* The same for SERR forwarding */ #define PCI_BRIDGE_CTL_ISA 0x04 /* Enable ISA mode */ #define PCI_BRIDGE_CTL_VGA 0x08 /* Forward VGA addresses */ #define PCI_BRIDGE_CTL_MASTER_ABORT 0x20 /* Report master aborts */ #define PCI_BRIDGE_CTL_BUS_RESET 0x40 /* Secondary bus reset */
#define PCI_BRIDGE_CTL_FAST_BACK 0x80 /* Fast Back2Back enabled on secondary interface */
下面說說PCI設備的地址空間:
前面也簡單介紹了下PCI設備的地址空間支持PIO和MMIO,即IO端口和IO內存。下面詳細分析下這兩種方式:
PIO
IO端口的編址是獨立於系統的地址空間,其實就是一段地址區域,所有外設的地址都映射到這段區域中。就像是一個進程內部的各個變量,公用進程地址空間一樣。不同外設的IO端口不同。訪問IO端口需要特殊的IO指令,OUT/IN,OUT用於write操作,in用於read操作。在此基礎上,操作系統實現了讀寫不同大小端口的函數。為什么說是不同大小呢??因為前面也說到,IO端口實際上是一段連續的區域,每個端口理論上是字節為單位即8bit,那么要想讀寫16位的端口只能把相鄰的端口進行合並,32位的端口也是如此。
例如下面的匯編指令:
MMIO
IO內存是直接把寄存器的地址空間直接映射到系統地址空間,系統地址空間往往會保留一段內存區用於這種MMIO的映射(當然肯定是位於系統內存區),這樣系統可以直接使用普通的訪存指令直接訪問設備的寄存器,隨着計算機內存容量的日益增大,這種方式更是顯出獨特的優勢,在性能至上的理念下,使用MMIO可以最大限度滿足日益增長的系統和外設存儲的需要。所以當前其實大多數外設都是采用MMIO的方式。
還有一種方式是把IO端口空間映射到內存空間,這樣依然可以通過正常的訪存指令訪問IO端口,但是這種方式下依然受到IO空間大小的制約,可以說並沒有解決實際問題。
但是上述方案只適用於在外設和內存進行小數據量的傳輸時,假如進行大數據量的傳輸,那么IO端口這種以字節為單位的傳輸就不用說了,IO內存雖然進行了內存映射,但是其映射的范圍大小相對於大量的數據,仍然不值一提,所以即使采用IO內存仍然是滿足不了需要,會讓CPU大部分時間處理繁瑣的映射,極大的浪費了CPU資源。那么這種情況就引入了DMA,直接內存訪問。這種方式的傳輸由DMA控制器控制,CPU給DMA控制器下達傳輸指令后就轉而處理其他的事務,然后DMA控制器就開始進行數據的傳輸,在完成數據的傳輸后通過中斷的方式通知CPU,這樣就可以極大的解放CPU。當然本次討論的重點不在DMA,所以對於DMA的討論僅限於此。
那么CPU是怎么訪問這些配置寄存器的呢?要知道配置寄存器指定了設備存儲寄存器的映射方式以及地址區間,但是配置寄存器本身的訪問就是一個問題。為每個設備預留IO端口或者IO內存都是不現實的。暫且不說x86架構下IO端口地址空間只有區區64K,就是內存,雖然現在隨着科技的發展,內存空間越來越大,但是也不可能為每個設備預留空間。那么折中的方式就是為所有設備的配置寄存器使用同一個IO端口,系統在IO地址空間預留了一段地址就是0xCF8~0xCFF一共八個字節,前四個字節做地址端口,后四個字節做數據端口。CPU訪問某個設備的配置寄存器時,先向地址端口寫入地址,然后從數據端口讀寫數據。這里的地址是一個綜合地址,結構如下:
這里就可以解釋我們總線數量和邏輯設備數量的限制了。在尋找某一個邏輯設備時,先根據總線號找到總線,然后根據設備號找到總線上的某個接口,最后在根據功能號定位某一個邏輯設備。
到此,PCI總線架構以及設備的配置空間已經描述的差不多了,下面就是看系統如何探測和初始化設備了。
下篇文章會結合LInux源代碼分析下Linux內核中對PCI設備的配置過程
