一、PCI簡介
PCI是一種外設總線規範。我們先來看一下什麼是總線:總線是一種傳輸信號的路徑或信道。典型情況是,總線是連接於一個或多個導體的電氣連線,總線上連接的所有設備可在同一時間收到所有的傳輸內容。總線由電氣接口和編程接口組成。本文討論Linux 下的設備驅動,所以,重點關注編程接口。 PCI是Peripheral Component Interconnect(外圍設備互聯)的簡稱,是普遍使用在桌面及更大型的計算機上的外設總線。PCI架構被設計爲ISA標準的替代品,它有三個主要目標:獲得在計算機和外設之間傳輸數據時更好的性能;儘可能的平臺無關;簡化往系統中添加和刪除外設的工作。 二、PCI尋址 從現在開始,我想儘可能通過一些實際的例子來說明問題,而減少理論方面的問題的描述,因爲,相關的理論的東西,可以在其它地方找到。 我們先來看一個例子,我的電腦裝有1G的RAM,1G以後的物理內存地址空間都是外部設備IO在系統內存地址空間上的映射。/proc/iomem描述了系統中所有的設備I/O在內存地址空間上的映射。我們來看地址從1G開始的第一個設備在/proc/iomem中是如何描述的: 40000000-400003ff : 0000:00:1f.1 這是一個PCI設備,40000000-400003ff是它所映射的內存地址空間,佔據了內存地址空間的1024 bytes的位置,而0000:00:1f.1則是一個PCI外設的地址,它以冒號和逗號分隔爲4個部分,第一個16位表示域,第二個8位表示一個總線編號,第三個5位表示一個設備號,最後是3位,表示功能號。 因爲PCI規範允許單個系統擁有高達256個總線,所以總線編號是8位。但對於大型系統而言,這是不夠的,所以,引入了域的概念,每個PCI域可以擁有最多256個總線,每個總線上可支持32個設備,所以設備號是5位,而每個設備上最多可有8種功能,所以功能號是3位。由此,我們可以得出上述的PCI設備的地址是0號域0號總線上的31號設備上的1號功能。那上述的這個PCI設備到底是什麼呢?下面是我的電腦上的lspci命令的輸出: 00:00.0 Host bridge: Intel Corporation 82845 845 (Brookdale) Chipset Host Bridge (rev 04) 00:01.0 PCI bridge: Intel Corporation 82845 845 (Brookdale) Chipset AGP Bridge(rev 04) 00:1d.0 USB Controller: Intel Corporation 82801CA/CAM USB (Hub #1) (rev 02) 00:1d.1 USB Controller: Intel Corporation 82801CA/CAM USB (Hub #2) (rev 02) 00:1e.0 PCI bridge: Intel Corporation 82801 Mobile PCI Bridge (rev 42) 00:1f.0 ISA bridge: Intel Corporation 82801CAM ISA Bridge (LPC) (rev 02) 00:1f.1 IDE interface: Intel Corporation 82801CAM IDE U100 (rev 02) 00:1f.3 SMBus: Intel Corporation 82801CA/CAM SMBus Controller (rev 02) 00:1f.5 Multimedia audio controller:Intel Corporation 82801CA/CAM AC'97 Audio Controller (rev 02) 00:1f.6 Modem: Intel Corporation 82801CA/CAM AC'97 Modem Controller (rev 02) 01:00.0 VGA compatible controller: nVidia Corporation NV17 [GeForce4 420 Go](rev a3) 02:00.0 FireWire (IEEE 1394): VIA Technologies, Inc. IEEE 1394 Host Controller(rev 46) 02:01.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8139/8139C/8139C+(rev 10) 02:04.0 CardBus bridge: O2 Micro, Inc. OZ6933 Cardbus Controller (rev 01) 02:04.1 CardBus bridge: O2 Micro, Inc. OZ6933 Cardbus Controller (rev 01) lspci沒有標明域,但對於一臺PC而言,一般只有一個域,即0號域。通過這個輸出我們可以看到它是一個IDE interface。由上述的輸出可以看到,我的電腦上共有3個PCI總線(0號,1號,2號)。在單個系統上,插入多個總線是通過橋(bridge)來完成的,橋是一種用來連接總線的特殊PCI外設。所以,PCI系統的整體佈局組織爲樹型,我們可以通過上面的lspci輸出,來畫出我的電腦上的PCI系統的樹型結構: 00:00.0(主橋)--00:01.0(PCI橋)-----01:00:0(nVidia顯卡) | |---00:1d(USB控制器)--00:1d:0(USB1號控制器) | | | |--00:1d:1(USB2號控制器) | |-00:1e:0(PCI橋)--02:00.0(IEEE1394) | | | |-02:01.0(8139網卡) | | | |-02:04(CardBus橋)-02:04.0(橋1) | | | |--02:04.1(橋2) | |-00:1f(多功能板卡)-00:1f:0(ISA橋) | |--00:1f:1(IDE接口) | |--00:1f:3(SMBus) | |--00:1f:5(多媒體聲音控制器) | |--00:1f:6(調制解調器) 由上圖可以得出,我的電腦上共有8個PCI設備,其中0號總線上(主橋)上連有4個,1號總線上連有1個,2號總線上連有3個。00:1f是一個連有5個功能的多功能板卡。 每一個PCI設備都有它映射的內存地址空間和它的I/O區域,這點是比較容易理解的。除此之外,PCI設備還有它的配置寄存器。有了配置寄存器,PCI的驅動程序就不需要探測就能訪問設備。配置寄存器的佈局是標準化的,配置空間的4個字節含有一個獨一無二的功能ID,因此,驅動程序可通過查詢外設的特定 ID來識別其設備。所以,PCI接口標準在ISA之上的主要創新在於配置地址空間。 |
前文已講過,PCI驅動程序不需要探測就能訪問設備,而這得益於配置地址空間。在系統引導階段,PCI硬件設備保持未激活狀態,但每個PCI主板均配備有能夠處理PCI的固件,固件通過讀寫PCI控制器中的寄存器,提供了對設備配置地址空間的訪問。
配置地址空間的前64字節是標準化的,它提供了廠商號,設備號,版本號等信息,唯一標識一個PCI設備。同時,它也提供了最多可多達6個的I/O地址區域,每個區域可以是內存也可以是I/O地址。這幾個I/O地址區域是驅動程序找到設備映射到內存和I/O空間的具體位置的唯一途徑。有了這兩點,PCI驅動程序就完成了相當於探測的功能。關於這64個字節的配置空間的詳細情況,可參閱《Linux設備驅動程序第三版》P306,不再詳述。
下面,我們來看一下8139too網卡設備的配置空間的詳細情況。在2.6內核的系統中,可以在目錄/sys/bus/pci/drivers/下看到很多以PCI設備名命名的目錄,但不是說這些設備都存在於你的系統中。我們進入8139too目錄,其中有一個以它的設備地址0000:02:01.0命名的目錄。在這個目錄下可以找到該網卡設備相關的很多信息。其中resource記錄了它的6個I/O地址區域。內容如下:
0x0000000000003400 0x00000000000034ff 0x0000000000000101
0x00000000e0000800 0x00000000e00008ff 0x0000000000000200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
由該文件可以看出,8139too設備使用了兩個I/O地址區域,第一個是它映射的I/O端口範圍,第二個是它映射的內存地址空間。關於這兩個值可以在/proc/iomem和/proc/ioport中得到驗證。
爲了能看到實際的運行效果,我們選擇8139too網卡作爲示例,從該網卡的linux驅動程序中裁剪相關代碼。
一個PCI設備的驅動程序必須要向內核中的PCI核心描述自己。同時,它也必須告訴PCI核心自己能夠驅動哪些設備。下面,就介紹兩個相關的重要數據結構。
struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
};
struct pci_driver {
struct list_head node;
char *name;
struct module *owner;
const struct pci_device_id *id_table; //驅動所能操縱的設備id列表。
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); //插入新設備
void (*remove)(struct pci_dev *dev); //移除設備。
int (*suspend)(struct pci_dev *dev, pm_message_t state);
int (*resume)(struct pci_dev *dev);
int (*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable);
void (*shutdown) (struct pci_dev *dev);
struct device_driver driver;
struct pci_dynids dynids;
};
pci_device_id唯一標識一個PCI設備。它的幾個成員依次分別表示:廠商號,設備號,子廠商號,子設備號,類別,類別掩碼(類可分爲基類,子類),私有數據。每一個PCI設備的驅動程序都有一個pci_device_id的數組,用於告訴PCI核心自己能夠驅動哪些設備。8139too的驅動程序定義它的pci_device_id數組如下:
static struct pci_device_id rtl8139_pci_tbl[];
該數組被初始化爲8139系列的一組網卡,當PCI核心得到這個數組後,會拿數組中的每一項跟從PCI配置空間中讀取到的數據進行比對,從而爲該驅動程序找到正確的設備。而pci_driver代表一個pci驅動程序。成員id_talbe即是指向pci_device_id數組的指針。name是驅動程序的名字,probe完成探測工作,即拿pci_device_id數組與內核中的數據進行比對。remove完成驅動程序的移除工作。關鍵的成員就這幾個。
驅動程序通過pci_module_init向內核註冊自己(我們有時會看到pci_register_driver函數,其實它們是同一個,在內核代碼中會看到,只是一個簡單的#define):
pci_module_init(&pci_driver);
調用函數後,如果pci_device_id數組中標識的設備存在於系統中,並且該設備恰好還沒有驅動程序,則該驅動程序會被安裝。下面我們來看從8139too驅動代碼中裁剪的pci設備初始化代碼:
pci_driver.h:
/* pci_driver.h
* [email protected]
* 2006-3-5
*/
#ifndef PCI_DRIVER_H
#define PCI_DRIVER_H
#include <linux/mod_devicetable.h> //for struct pci_device_id
#include <linux/module.h> //for MODULE_DEVICE_TABLE
#include <linux/pci.h> //for struct pci_driver
#define DRV_NAME "8139too"
#define DRV_VERSION "0.9.27"
#define RTL8139_DRIVER_NAME DRV_NAME " Fast Ethernet driver " DRV_VERSION
typedef enum{
RTL8139 = 0,
RTL8129,
}board_t;
static struct pci_device_id rtl8139_pci_tbl[] = {
{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1113, 0x1211, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1500, 0x1360, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x4033, 0x1360, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1186, 0x1300, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1186, 0x1340, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x13d1, 0xab06, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1259, 0xa117, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1259, 0xa11e, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x14ea, 0xab06, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x14ea, 0xab07, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x11db, 0x1234, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1432, 0x9130, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x02ac, 0x1012, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x018a, 0x0106, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x126c, 0x1211, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1743, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x021b, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
#ifdef CONFIG_SH_SECUREEDGE5410
/* Bogus 8139 silicon reports 8129 without external PROM :-( */
{0x10ec, 0x8129, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
#endif
#ifdef CONFIG_8139TOO_8129
{0x10ec, 0x8129, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8129 },
#endif
/* some crazy cards report invalid vendor ids like
* 0x0001 here. The other ids are valid and constant,
* so we simply don't match on the main vendor id.
*/
{PCI_ANY_ID, 0x8139, 0x10ec, 0x8139, 0, 0, RTL8139 },
{PCI_ANY_ID, 0x8139, 0x1186, 0x1300, 0, 0, RTL8139 },
{PCI_ANY_ID, 0x8139, 0x13d1, 0xab06, 0, 0, RTL8139 },
{0,}
};
MODULE_DEVICE_TABLE(pci, rtl8139_pci_tbl);
static int __devinit rtl8139_init_one(struct pci_dev *pdev, const struct pci_device_id *id);
static void __devexit rtl8139_remove_one(struct pci_dev *pdev);
static struct pci_driver rtl8139_pci_driver = {
.name = DRV_NAME,
.id_table = rtl8139_pci_tbl,
.probe = rtl8139_init_one,
.remove = __devexit_p(rtl8139_remove_one),
};
#endif //PCI_DRIVER_H
pci_driver.c:
/* pci_driver.c
* [email protected]
* 2006-3-5
*/
#include "pci_driver.h"
#include <linux/init.h>
MODULE_AUTHOR("Linqiang He, Hangzhou China");
MODULE_LICENSE("Dual BSD/GPL");
static int __init rtl8139_init_module(void)
{
/* when we're a module, we always print a version message,
* even if no 8139 board is found.
*/
#ifdef MODULE
printk (KERN_INFO RTL8139_DRIVER_NAME "/n");
#endif
return pci_module_init(&rtl8139_pci_driver);
}
static void __exit rtl8139_cleanup_module (void)
{
pci_unregister_driver(&rtl8139_pci_driver);
}
module_init(rtl8139_init_module);
module_exit(rtl8139_cleanup_module);
int __devinit rtl8139_init_one(struct pci_dev *pdev, const struct pci_device_id *id)
{
//這裏可插入各種調試代碼,下文會有專門描述。
return 0;
}
void __devexit rtl8139_remove_one (struct pci_dev *pdev)
{
}
註冊驅動程序成功後,rtl8139_init_one會被調用,在這個函數中,我們可以通過插入一些打印輸出語句看到PCI的配置地址空間和I/O地址區域的一些情況。
首先,插入以下語句:
u16 vendor, device;
pci_read_config_word(pdev, 0, &vendor);
pci_read_config_word(pdev, 2, &device);
printk(KERN_INFO "%x, %x/n", vendor, device);
這段代碼讀取了網卡設備的配置地址空間的前四位,它正好是設備的廠商號和設備號。下面是輸出:
Mar 9 21:44:39 localhost kernel: 10ec, 8139
10ec和8139就是我的網卡的廠商號和設備號了。
再插入下列代碼:
u32 addr1,addr2,addr3, addr4,addr5,addr6;
pci_read_config_dword(pdev, 16, &addr1);
pci_read_config_dword(pdev, 20, &addr2);
pci_read_config_dword(pdev, 24, &addr3);
pci_read_config_dword(pdev, 28, &addr4);
pci_read_config_dword(pdev, 32, &addr5);
pci_read_config_dword(pdev, 36, &addr6);
printk(KERN_INFO "%x,%x,%x,%x,%x,%x/n",addr1, addr2, addr3, addr4,addr5,addr6);
這段代碼讀取網卡設備的6個I/O地址區域的址始位置。下面是輸出:
Mar 9 21:55:06 localhost kernel: 3401,e0000800,0,0,0,0
可見,該設備只使用了前兩個I/O地址區域,分別標識它的I/O端口區域和內存地址空間。
另外,在這裏,還可直接打印出網卡的MAC地址。不再詳述。
接着上文給出的源代碼,我們可以在rtl8139_init_one中插入一些不同的調試代碼,觀察設備驅動模塊在內核中的一些動作。
8139too網卡設備的設備內存的頭6個字節存放的是該網卡的48位的MAC地址,我們可以通過訪問設備內存得到這個MAC地址。下面通過在 rtl8139_init_one在插入代碼,以四種不同方式訪問設備內存。第一種是通過訪問I/O內存實現,後三種則是通過訪問I/O端口的形式實現。
第一種:
unsigned long mmio_start, addr1, addr2;
void __iomem *ioaddr;
mmio_start = pci_resource_start( pdev, 1);
ioaddr = pci_iomap(pdev, 1, 0);
addr1 = ioread32( ioaddr );
addr2 = ioread32( ioaddr + 4 );
printk(KERN_INFO "mmio start: %lX/n", mmio_start);
printk(KERN_INFO "ioaddr: %p/n", ioaddr);
printk(KERN_INFO "%02lX.%02lX.%02lX.%02lX.%02lX.%02lX/n",
(addr1) & 0xFF,
(addr1 >> 8) & 0xFF,
(addr1 >> 16 ) & 0xFF,
(addr1 >> 24 ) & 0xFF,
(addr2) & 0xFF,
(addr2 >> 8) & 0xFF );
運行結果:
Mar 10 22:34:56 localhost kernel: mmio start: E0000800
Mar 10 22:34:56 localhost kernel: ioaddr: f8aa6800
Mar 10 22:34:56 localhost kernel: 00.02.3F.AC.41.9D
第二種:
unsigned long pio_start, pio_len, addr1, addr2;
void __iomem *ioaddr;
pio_start = pci_resource_start( pdev, 0);
pio_len = pci_resource_len (pdev, 0);
ioaddr = ioport_map(pio_start, pio_len);
addr1 = ioread32( ioaddr );
addr2 = ioread32( ioaddr + 4 );
printk(KERN_INFO "pio start: %lX/n", pio_start);
printk(KERN_INFO "ioaddr: %p/n", ioaddr);
printk(KERN_INFO "%02lX.%02lX.%02lX.%02lX.%02lX.%02lX/n",
(addr1) & 0xFF,
(addr1 >> 8) & 0xFF,
(addr1 >> 16 ) & 0xFF,
(addr1 >> 24 ) & 0xFF,
(addr2) & 0xFF,
(addr2 >> 8) & 0xFF );
運行結果:
Mar 10 22:30:52 localhost kernel: pio start: 3400
Mar 10 22:30:52 localhost kernel: ioaddr: 00013400
Mar 10 22:30:52 localhost kernel: 00.02.3F.AC.41.9D
第三種:
unsigned long pio_start, addr1, addr2;
pio_start = pci_resource_start( pdev, 0 );
addr1 = inl( pio_start );
addr2 = inl( pio_start + 4 );
printk(KERN_INFO "port io start: %lX/n", pio_start);
printk(KERN_INFO "%02lX.%02lX.%02lX.%02lX.%02lX.%02lX/n",
(addr1) & 0xFF,
(addr1 >> 8) & 0xFF,
(addr1 >> 16) & 0xFF,
(addr1 >> 24) & 0xFF,
(addr2) & 0xFF,
(addr2 >> 8) & 0xFF );
運行結果:
Mar 10 22:36:18 localhost kernel: port io start: 3400
Mar 10 22:36:18 localhost kernel: 00.02.3F.AC.41.9D
第四種:
unsigned long pio_start;
u8 addr1, addr2, addr3, addr4, addr5, addr6;
pio_start = pci_resource_start( pdev, 0 );
addr1 = inb( pio_start );
addr2 = inb( pio_start + 1 );
addr3 = inb( pio_start + 2 );
addr4 = inb( pio_start + 3 );
addr5 = inb( pio_start + 4 );
addr6 = inb( pio_start + 5 );
printk(KERN_INFO "port io start: %lX/n", pio_start);
printk(KERN_INFO "%02X.%02X.%02X.%02X.%02X.%02X/n",
addr1, addr2, addr3, addr4, addr5, addr6 );
運行結果:
Mar 10 22:37:19 localhost kernel: port io start: 3400
Mar 10 22:37:19 localhost kernel: 00.02.3F.AC.41.9D
每種外設都通過讀寫寄存器進行控制。大部分外設都有幾個寄存器,不管是在內存地址空間還是在I/O地址空間,這些寄存器的訪問地址都是連續的。x86的處理器爲I/O端口的讀和寫提供了獨立的線路,並且使用特殊的CPU指令訪問端口。但即使這樣,也不是所有的設備都會把寄存器映射到I/O端口中。ISA設備普遍使用I/O端口,而大多數PCI設備則把寄存器映射到某個內存地址區段。
I/O端口是驅動程序與許多設備之間的通信方式,Linux的內核爲我們提供了I/O端口分配的操作接口,但對PCI設備來講,它的配置地址空間已經爲其指定了I/O端口範圍,不需要額外的分配操作。Linux內核
提供瞭如下一些訪問I/O端口的內聯函數:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
下面我們重點來看一下2.6內核引入的ioport_map函數:
void *ioport_map( unsigned long port, unsigned int count );
通過這個函數,可以把port開始的count個連續端口重映射爲一段“內存空間”。然後就可以在其返回的地址上象訪問I/O內存一樣訪問這幾個I/O端口。當不需要這種映射時,需要調用下面的函數來撤消:
void iport_unmap(void *addr);
瀏覽2.6內核的源代碼,我們不難發現,這種所謂的映射其實是“假”的。下面是這兩個函數的實現,及一些相關數據:
#define PIO_OFFSET 0x10000UL
#define PIO_MASK 0x0ffffUL
#define PIO_RESERVED 0x40000UL
void __iomem *ioport_map(unsigned long port, unsigned int nr)
{
if (port > PIO_MASK)
return NULL;
return (void __iomem *) (unsigned long) (port + PIO_OFFSET);
}
void ioport_unmap(void __iomem *addr)
{
/* Nothing to do */
}
它只是簡單地把I/O端口號加上PIO_OFFSET(64K),作爲一個“假”的內存地址返回,而unmap則什麼也不做。之所以這樣做,是基於這樣一個事實:真正的I/O內存地址經過映射成爲虛擬地址後,由於是在內核空間,其值肯定大於3G。而port+PIO_OFFSET不會大於128K。所以,內核不會把這兩種地址搞混。可以分別進行處理,下面看看ioread8函數的實現:
unsigned int fastcall ioread8(void __iomem *addr)
{
IO_COND(addr, return inb(port), return readb(addr));
}
#define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)
#define IO_COND(addr, is_pio, is_mmio) do { /
unsigned long port = (unsigned long __force)addr; /
if (port < PIO_RESERVED) { /
VERIFY_PIO(port); /
port &= PIO_MASK; /
is_pio; /
} else { /
is_mmio; /
} /
} while (0)
展開:
unsigned int fastcall ioread8(void __iomem *addr)
{
unsigned long port = (unsigned long __force)addr;
if( port < 0x40000UL ) {
BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );
port &= PIO_MASK;
return inb(port);
}else{
return readb(addr);
}
}
所以,除了提供一個統一的接口,它並沒有在本質上改變什麼。
除了I/O端口之外,和設備通信的另一種主要機制是通過使用映射到內存的寄存器或設備內存。這兩種都稱爲I/O內存,因爲寄存器和內存的差別對軟件是透明的。
對於分配好的I/O內存,一般不鼓勵直接使用指向I/O內存的指針進行訪問,最好通過頁表,用包裝函數訪問。要通過頁表訪問,那麼需要對分配好的I/O內存進行映射,確保該I/O內存對內核而言是可訪問的。完成I/O內存映射的函數是ioremap:
#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
它把一個總線內存地址映射到CPU空間。
關於這些接口的實際使用,可參考前一篇。