PCI設(shè)備驅(qū)動(dòng)(一)

首先要明確兩個(gè)概念:Linux內(nèi)核 PCI設(shè)備驅(qū)動(dòng)和設(shè)備本身驅(qū)動(dòng)兩部分。工作中所謂的編寫設(shè)備驅(qū)動(dòng),其實(shí)就是編寫設(shè)備本身驅(qū)動(dòng)。因?yàn)長(zhǎng)inux 內(nèi)核的PCI驅(qū)動(dòng)是內(nèi)核自帶的。

當(dāng)然,并不是說內(nèi)核幫咱們寫好了Linux PCI驅(qū)動(dòng)我們什么就不用做了,至少你要明白內(nèi)核大致都干了些什么,這樣你才能明白你該干什么,如何完成設(shè)備本身的驅(qū)動(dòng)。我們下面就來研究下Linux PCI驅(qū)動(dòng)到底都干了些什么。

Linux PCI 初始化代碼邏輯上分為三個(gè)部分:

(1)內(nèi)核的PCI設(shè)備驅(qū)動(dòng)程序
這個(gè)偽設(shè)備驅(qū)動(dòng)程序從總線0開始查詢PCI系統(tǒng)并且定位系統(tǒng)中所有的PCI設(shè)備和PCI橋。它建立一個(gè)可以用來描述這個(gè)PCI系統(tǒng)拓樸層次的數(shù)據(jù)結(jié)構(gòu)鏈表。并且對(duì)所有的發(fā)現(xiàn)的PCI橋編號(hào)。
(2)PCI BIOS
這個(gè)軟件層提供在bib-pci-bios歸約中描述的服務(wù)。雖然Alpha AXP不提供BIOS服務(wù),在其Linux版本中包含了相應(yīng)的功能。
(3)PCI Fixup
與特定系統(tǒng)相關(guān)的PCI初始化修補(bǔ)代碼

而這里主要就是探討Linux內(nèi)核 PCI設(shè)備驅(qū)動(dòng),會(huì)在最后列出一段包含設(shè)備本身驅(qū)動(dòng)的示例代碼,僅供參考。

一、概述及簡(jiǎn)介

PCI(Periheral Component Interconnect)有三種地址空間:PCI I/O空間、PCI內(nèi)存地址空間和PCI配置空間。其中,PCI I/O空間和PCI內(nèi)存地址空間由設(shè)備驅(qū)動(dòng)程序(即上面提到的設(shè)備本身驅(qū)動(dòng))使用,而PCI配置空間由Linux PCI初始化代碼使用,這些代碼用于配置PCI設(shè)備,比如中斷號(hào)以及I/O或內(nèi)存基地址。所以這里的PCI設(shè)備驅(qū)動(dòng)就是要大致描述對(duì)于PCI設(shè)備驅(qū)動(dòng),Linux內(nèi)核都幫我們做了什么(主),接著就是我們應(yīng)該完成什么(次)。

(1)Linux內(nèi)核做了什么

簡(jiǎn)單的說,Linux內(nèi)核主要就做了對(duì)PCI設(shè)備的枚舉和配置;這些工作都是在Linux內(nèi)核初始化時(shí)完成的。

枚舉:對(duì)于PCI總線,有一個(gè)叫做PCI橋的設(shè)備用來將父總線與子總線連接。作為一種特殊的PCI設(shè)備,PCI橋主要包括以下三種:

  1. Host/PCI橋: 用于連接CPU與PCI根總線,第1個(gè)根總線的編號(hào)為0。在PC中,內(nèi)存控制器也通常被集成到Host/PCI橋設(shè)備芯片中,因此Host/PCI橋通常也被稱為“北橋芯片組(North Bridge Chipset)”。
  2. PCI/ISA橋: 用于連接舊的ISA總線。通常,PCI中類似i8359A中斷控制器這樣的設(shè)備也會(huì)被集成到PCI/ISA橋設(shè)備中。因此,PCI/ISA橋通常也被稱為“南橋芯片組(South Bridge Chipset)”
  3. PCI-to-PCI橋(以下稱為PCI-PCI橋): 用于連接PCI主總線(Primary Bus)和次總線(Secondary Bus)。PCI-PCI橋所處的PCI總線稱為主總線,即次總線的父總線;PCI-PCI橋所連接的PCI總線稱為次總線,即主總線的子總線。
圖1 PCI系統(tǒng)示意圖

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


圖2 Base class 06h

CPU通過Host/PCI橋與一條PCI總線相連,處在這種配置上的PCI總線稱為根總線。PC機(jī)中通常只有一個(gè)Host/PCI橋,在一條PCI總線的基礎(chǔ)上,可以再通過PCI橋連接到其他次一層的總線,例如通過PCI-PCI橋可以連接到另一條PCI總線,通過PCI-ISA橋可以連接到一條ISA總線。

事實(shí)上,現(xiàn)代PC機(jī)中的ISA總線正是通過PCI-ISA橋連接在PCI總線上的。這樣,通過使用PCI-PCI橋,就構(gòu)筑起了一個(gè)層次的、樹狀的PCI系統(tǒng)結(jié)構(gòu)。對(duì)于上層的總線而言,連接在這條總線上的PCI橋也是一個(gè)設(shè)備。但是這是一種特殊的設(shè)備,它既是上層總線上的一個(gè)設(shè)備,實(shí)際上又是上層總線的延伸。

所謂枚舉,就是從Host/PCI橋開始進(jìn)行探測(cè)和掃描,逐個(gè)“枚舉”連接在第一條PCI總線上的所有設(shè)備并記錄在案。如果其中的某個(gè)設(shè)備是PCI-PCI橋,則又進(jìn)一步再探測(cè)和掃描連在這個(gè)橋上的次級(jí)PCI總線。就這樣遞歸下去,直到窮盡系統(tǒng)中的所有PCI設(shè)備。

其結(jié)果,是在內(nèi)存中建立起一棵代表著這些PCI總線和設(shè)備的PCI樹。每個(gè)PCI設(shè)備(包括PCI橋設(shè)備)都由一個(gè)pci_dev結(jié)構(gòu)體來表示,而每條PCI總線則由pci_bus結(jié)構(gòu)來表示。你有通過PCI橋建立起的硬件設(shè)備樹,我有內(nèi)存中通過數(shù)據(jù)結(jié)構(gòu)構(gòu)建的軟件樹,很和諧。

配置:PCI設(shè)備中一般都帶有一些RAM和ROM 空間,通常的控制/狀態(tài)寄存器和數(shù)據(jù)寄存器也往往以RAM區(qū)間的形式出現(xiàn),而這些區(qū)間的地址在設(shè)備內(nèi)部一般都是從0開始編址的,那么當(dāng)總線上掛接了多個(gè)設(shè)備時(shí),對(duì)這些空間的訪問就會(huì)產(chǎn)生沖突。

所以,這些地址都要先映射到系統(tǒng)總線上,再進(jìn)一步映射到內(nèi)核的虛擬地址空間。而所謂的配置就是通過對(duì)PCI配置空間的寄存器進(jìn)行操作從而完成地址的映射(只完成內(nèi)部編址映射到總線地址的工作,而映射到內(nèi)核的虛擬地址空間是由設(shè)備本身的驅(qū)動(dòng)要做的工作)。

(2)Linux內(nèi)核怎么做的

這里首先要說明的是,對(duì)于PCI的設(shè)備初始化(即上面提到的枚舉和配置工作),PC機(jī)的BIOS和Linux內(nèi)核都可以做。一般而言,只要是采用PCI總線的PC機(jī),其BIOS就必須提供對(duì)PCI總線操作的支持,因而稱為PCI BIOS。

而且最早Linux內(nèi)核也是通過這種BIOS調(diào)用的方式來獲取系統(tǒng)中的PCI設(shè)備信息的,但是不是所有的平臺(tái)都有BIOS(比如某些嵌入式系統(tǒng)),并且在實(shí)踐中也發(fā)現(xiàn)有些母板上的PCI BIOS存在這樣那樣的問題,所以后來就改由Linux內(nèi)核自己動(dòng)手了,自己動(dòng)手豐衣足食呵呵。

不過,Linux內(nèi)核還是很體貼的在make menuconfig的選項(xiàng)里為我們提供了自己選擇的權(quán)利,即PCI access mode,里面提供了四個(gè)選項(xiàng)分別是BIOS、MMconfig、Direct和Any。Direct方式就是拋開BIOS而由內(nèi)核自己完成初始化工作的意思。

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

前面提到了PCI有三種地址空間,其中的PCI配置空間是給Linux內(nèi)核中的PCI初始化代碼用的,也就是我們這里的枚舉與配置時(shí)用到的。那么這個(gè)PCI配置空間里放的是什么東西呢,顯然應(yīng)該是寄存器,稱為配置寄存器組。當(dāng)PCI設(shè)備上電時(shí),硬件保持未激活狀態(tài)。即該設(shè)備只會(huì)對(duì)配置事務(wù)做出響應(yīng)。上電時(shí),設(shè)備上不會(huì)有內(nèi)存和I/O端口映射到計(jì)算機(jī)的地址空間;其他設(shè)備相關(guān)的功能,例如中斷報(bào)告,也被禁止。

PCI標(biāo)準(zhǔn)規(guī)定每個(gè)設(shè)備的配置寄存器組最多可以有256字節(jié)的連續(xù)空間,其中開頭的64字節(jié)的用途和格式是標(biāo)準(zhǔn)的,稱為配置寄存器的頭部。系統(tǒng)中提供一些與硬件有關(guān)的機(jī)制,使得PCI配置代碼可以檢測(cè)在一個(gè)給定的PCI總線上所有可能的PCI配置寄存器頭部,從而知道哪個(gè)PCI插槽上目前有設(shè)備,哪個(gè)插槽上暫無設(shè)備。這是通過讀PCI配置寄存器頭部上的某個(gè)域完成的(一般是“Vendor ID" 域)。如果一個(gè)插槽上為空,上述操作會(huì)返回一些錯(cuò)誤返回值,如0xFFFFFFFF。

這種頭部(指64字節(jié)頭部)又有三種,其中“0型”(type 0)頭部用于一般的PCI設(shè)備,“1型”頭部用于各種PCI-PCI橋, “2型”頭部是用于PCI-CardBus橋的,CardBus是筆記本電腦中使用的總線,我們不關(guān)心。

而64字節(jié)頭部中的16個(gè)字節(jié)中又包含著有關(guān)頭部的類型、設(shè)備的種類、設(shè)備的一些性質(zhì)、由誰制造等等信息。根據(jù)這16個(gè)字節(jié)中提供的信息,來確定應(yīng)該怎樣進(jìn)一步解釋和處理剩余頭部中的48個(gè)字節(jié)。對(duì)于這16個(gè)字節(jié)的地址,include/linux/pci.h中定義了這樣一些常數(shù):

#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 */

對(duì)應(yīng)我們的圖3(見下)中的前16字節(jié)。而且我們也看到了緊挨著PCI_HEADER_TYPE(即存放頭部類型的寄存器)下面定義的就是上面提到的三種類型的頭部:

#define PCI_HEADER_TYPE_NORMAL 0

#define PCI_HEADER_TYPE_BRIDGE 1

#define PCI_HEADER_TYPE_CARDBUS 2

在Linux系統(tǒng)上,可以通過cat /proc/pci 等命令查看系統(tǒng)中所有PCI設(shè)備的類別、型號(hào)以及廠商等信息,那就是從這些寄存器來的。下面是在虛擬機(jī)中用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寄存器是小端字節(jié)序格式的。那么根據(jù)最下面的PCI配置寄存器組的結(jié)構(gòu)(圖4),顯然這個(gè)Host bridge的Vendor ID是0x8086,我不說你也能猜到這個(gè)Vendor就是Intel。

這里有個(gè)問題要先說清楚,就是這些寄存器的地址問題,不然往后就進(jìn)行不下去了。配置寄存器可以讓我們來進(jìn)行配置以便完成PCI設(shè)備上的存儲(chǔ)空間的訪問,但這些配置寄存器本身也位于PCI設(shè)備地址空間中,如何訪問這部分空間也就成了我們整個(gè)初始化工作的一個(gè)入口點(diǎn),就像每個(gè)可執(zhí)行程序都要有入口點(diǎn)一樣。

PCI采用的辦法是讓所有設(shè)備的配置寄存器組都采用相同的地址,由所在總線的PCI橋在訪問時(shí)附加上其他條件來區(qū)分。而CPU則通過一個(gè)統(tǒng)一的入口地址向“宿主--PCI橋”發(fā)出命令,由相應(yīng)的PCI橋間接的完成具體的讀寫。對(duì)于i386結(jié)構(gòu)的處理器,PCI總線的設(shè)計(jì)者在I/O地址空間保留了8個(gè)字節(jié)用于這個(gè)目的,那就是0xCF8~0xCFF。

這8個(gè)字節(jié)構(gòu)成了兩個(gè)32位的寄存器,第一個(gè)是“地址寄存器”0xCF8,第二個(gè)是“數(shù)據(jù)寄存器”0xCFC。要訪問某個(gè)設(shè)備中的某個(gè)配置寄存器時(shí),CPU先往地址寄存器中寫入目標(biāo)地址,然后通過數(shù)據(jù)寄存器讀寫數(shù)據(jù)。不過,寫入地址寄存器的目標(biāo)地址是一種總線號(hào)、設(shè)備號(hào)、功能號(hào)以及設(shè)備寄存器地址在內(nèi)的綜合地址。格式如下圖:
圖3 寫入地址寄存器0xCF8的綜合地址

這里的總線號(hào)、設(shè)備號(hào)和功能號(hào)是對(duì)配置寄存器地址的擴(kuò)充,就是上面提到的附加的其他條件。

首先每個(gè)PCI總線都有個(gè)總線號(hào),主總線的總線號(hào)為0,其余的則由CPU在枚舉階段每當(dāng)探測(cè)到一個(gè)PCI橋時(shí)便為其指定一個(gè),依次遞增。設(shè)備號(hào)一般代表著一塊PCI接口卡(更確切的說是PCI總線接口芯片),通常取決于插槽的位置。PCI接口卡上可以有若干個(gè)功能模塊,這些功能模塊共用一個(gè)PCI總線接口芯片,包括其中用于地址映射的電子線路,以降低成本。

從邏輯的角度說,每個(gè)“功能”實(shí)際上就是一個(gè)設(shè)備(看過USB設(shè)備驅(qū)動(dòng)的人很眼熟吧 ,呵呵),所以設(shè)備號(hào)和功能號(hào)合在一起又可以稱作“邏輯設(shè)備號(hào)”,而每塊卡上最多可以容納8個(gè)設(shè)備。

顯然,這些字段(指整個(gè)32bit)結(jié)合在一起就惟一確定了系統(tǒng)中的一項(xiàng)PCI邏輯設(shè)備。開始時(shí),只有0號(hào)總線可以訪問,在掃描0號(hào)總線時(shí)如果發(fā)現(xiàn)上面某個(gè)設(shè)備是PCI橋,就為之指定一個(gè)新的總線號(hào),例如1,這樣1號(hào)總線就可以訪問了,這就是枚舉階段的任務(wù)之一。

現(xiàn)在請(qǐng)讀者考慮一個(gè)問題:當(dāng)我們拿到一塊PCI網(wǎng)卡,把它插到PC的主板上,打算寫個(gè)這個(gè)網(wǎng)卡的驅(qū)動(dòng)。那么第一步該干什么呢?讀者可以回顧前面的內(nèi)容,既然我們說Linux內(nèi)核幫我們做了設(shè)備的枚舉和配置工作,那么我在寫網(wǎng)卡驅(qū)動(dòng)之前是不是可以先看看Linux內(nèi)核對(duì)我們的這個(gè)PCI網(wǎng)卡設(shè)備完成的枚舉工作的結(jié)果呢?或者直白些說,我把網(wǎng)卡插上了,現(xiàn)在Linux內(nèi)核有沒有識(shí)別出這塊設(shè)備呢? 注意識(shí)別出設(shè)備跟能正常使用設(shè)備是不同的概念,這很好理解。

安裝過PC網(wǎng)卡驅(qū)動(dòng)的人都知道,當(dāng)設(shè)備的驅(qū)動(dòng)沒有安裝時(shí),我們?cè)谠O(shè)備管理器中是可以看到這個(gè)設(shè)備的,不過上面是一個(gè)黃色的大問號(hào)。而在Linux系統(tǒng)中,我們可以通過lspci命令來查看。

下面是在LDD3的PCI驅(qū)動(dòng)那一章截取的一段內(nèi)容: lspci 的輸出( pciutils 的一部分, 在大部分發(fā)布中都有)和在 /proc/pci 和 /porc/bus/pci 中的信息排布. PCI 設(shè)備的 sysfs 表示也顯示了這種尋址方案, 還有 PCI 域信息,當(dāng)顯示硬件地址時(shí), 它可被顯示為 2 個(gè)值( 一個(gè) 8-位總線號(hào)和一個(gè) 8-位 設(shè)備和功能號(hào)), 作為 3 個(gè)值( bus, device, 和 function), 或者作為 4 個(gè)值(domain, bus, device, 和 function); 所有的值常常用 16 進(jìn)制顯示.

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

$ 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

所有的 3 個(gè)設(shè)備列表都以相同順序排列, 因?yàn)?lspci 使用 /proc 文件作為它的信息源。 拿 VGA 視頻控制器作一個(gè)例子, 0x00a0 意思是 0000:00:14.0 當(dāng)劃分為域(16位), 總線(8位), 設(shè)備(5位)和功能(3位).為什么0x00a0對(duì)應(yīng)的是0000:00:14.0呢,這就要看圖2中的內(nèi)容了,根據(jù)圖2中的寄存器對(duì)應(yīng)0x00a0就代表著總線(8位), 設(shè)備(5位)和功能(3位).

0x00a0=0000000010100000,很容易看出高8位是總線號(hào)也就是0。剩下的0xa0=10100000,可以看出如果低3位表示功能號(hào),那么剩下的10100就是設(shè)備號(hào),補(bǔ)全成8位的值就是00010100即0x14.
圖4 PCI配置寄存器組
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容