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

淺談Linux PCI裝置驅動(上)

有學員建議寫寫PCI驅動,今天就找到一篇,文章很長,這基本上是全網對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這篇文章裡提到了:

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

(1)PCI裝置驅動程式(即上面提到的Linux 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橋主要包括以下三種:

(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匯流排稱為次匯流排,即主匯流排的子匯流排。

在這裡插入圖片描述
圖1,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橋建立起的硬體裝置樹,我有記憶體中通過資料結構構建的軟體樹,很和諧。

配置: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配置暫存器組

參考資料:

[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

[8] http://www.ibm.com/developerworks/cn/linux/l-pci/

本文大概有2萬字,所以分二次發,未完待續…

技術交流可以加個人威信13266630429,驗證:CSDN部落格