首先要明確兩個概念:Linux內核 PCI設備驅動和設備本身驅動兩部分。工作中所謂的編寫設備驅動,其實就是編寫設備本身驅動。因為Linux 內核的PCI驅動是內核自帶的。
當然,并不是說內核幫咱們寫好了Linux PCI驅動我們什么就不用做了,至少你要明白內核大致都干了些什么,這樣你才能明白你該干什么,如何完成設備本身的驅動。我們下面就來研究下Linux PCI驅動到底都干了些什么。
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設備的枚舉和配置;這些工作都是在Linux內核初始化時完成的。
枚舉:對于PCI總線,有一個叫做PCI橋的設備用來將父總線與子總線連接。作為一種特殊的PCI設備,PCI橋主要包括以下三種:
Host/PCI橋: 用于連接CPU與PCI根總線,第1個根總線的編號為0。在PC中,內存控制器也通常被集成到Host/PCI橋設備芯片中,因此Host/PCI橋通常也被稱為“北橋芯片組(North Bridge Chipset)”。
PCI/ISA橋: 用于連接舊的ISA總線。通常,PCI中類似i8359A中斷控制器這樣的設備也會被集成到PCI/ISA橋設備中。因此,PCI/ISA橋通常也被稱為“南橋芯片組(South Bridge Chipset)”
PCI-to-PCI橋(以下稱為PCI-PCI橋): 用于連接PCI主總線(Primary Bus)和次總線(Secondary Bus)。PCI-PCI橋所處的PCI總線稱為主總線,即次總線的父總線;PCI-PCI橋所連接的PCI總線稱為次總線,即主總線的子總線。
圖1 PCI系統示意圖
下圖摘自PCI Local Bus Specification Revision 2.1,可以看到PCI-PCI橋的Class Code(見圖3)就是0x060400。
圖2 Base class 06h
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橋建立起的硬件設備樹,我有內存中通過數據結構構建的軟件樹,很和諧。
配置: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有三種地址空間,其中的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配置寄存器組的結構(圖4),顯然這個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先往地址寄存器中寫入目標地址,然后通過數據寄存器讀寫數據。不過,寫入地址寄存器的目標地址是一種總線號、設備號、功能號以及設備寄存器地址在內的綜合地址。格式如下圖:
圖3 寫入地址寄存器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-3000000.0 Host bridge 000000.1 RAM memory 000000.2 RAM memory 000002.0 USB Controller 000004.0 Multimedia audio controller 000006.0 Bridge 000007.0 ISA bridge 000009.0 USB Controller 000009.1 USB Controller 000009.2 USB Controller 00000c.0 CardBus bridge 00000f.0 IDE interface 000010.0 Ethernet controller 000012.0 Network controller 000013.0 FireWire (IEEE 1394) 000014.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/ |-- 000000.0 -> ../../../devices/pci0000:00/000000.0 |-- 000000.1 -> ../../../devices/pci0000:00/000000.1 |-- 000000.2 -> ../../../devices/pci0000:00/000000.2 |-- 000002.0 -> ../../../devices/pci0000:00/000002.0 |-- 000004.0 -> ../../../devices/pci0000:00/000004.0 |-- 000006.0 -> ../../../devices/pci0000:00/000006.0 |-- 000007.0 -> ../../../devices/pci0000:00/000007.0 |-- 000009.0 -> ../../../devices/pci0000:00/000009.0 |-- 000009.1 -> ../../../devices/pci0000:00/000009.1 |-- 000009.2 -> ../../../devices/pci0000:00/000009.2 |-- 00000c.0 -> ../../../devices/pci0000:00/00000c.0 |-- 00000f.0 -> ../../../devices/pci0000:00/00000f.0 |-- 000010.0 -> ../../../devices/pci0000:00/000010.0 |-- 000012.0 -> ../../../devices/pci0000:00/000012.0 |-- 000013.0 -> ../../../devices/pci0000:00/000013.0 |--000014.0->../../../devices/pci0000:00/000014
所有的 3 個設備列表都以相同順序排列, 因為 lspci 使用 /proc 文件作為它的信息源。拿 VGA 視頻控制器作一個例子, 0x00a0 意思是 000014.0 當劃分為域(16位), 總線(8位), 設備(5位)和功能(3位).為什么0x00a0對應的是000014.0呢,這就要看圖2中的內容了,根據圖2中的寄存器對應0x00a0就代表著總線(8位), 設備(5位)和功能(3位).
0x00a0=0000000010100000,很容易看出高8位是總線號也就是0。剩下的0xa0=10100000,可以看出如果低3位表示功能號,那么剩下的10100就是設備號,補全成8位的值就是00010100即0x14.
圖4 PCI配置寄存器組
-
Linux
+關注
關注
87文章
11232瀏覽量
208941 -
PCI
+關注
關注
4文章
662瀏覽量
130189 -
PCI設備
+關注
關注
0文章
9瀏覽量
8125
原文標題:PCI設備驅動(一)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論