CPU對外設端口實體位址的編址方式有兩種:一種是IO映射方式,另一種是記憶體映射方式。
Linux将基于IO映射方式的和記憶體映射方式的IO端口統稱為IO區域(IO region)。IO region仍然是一種IO資源,是以它仍然可以用resource結構類型來描述。
Linux管理IO region:
1) request_region()
把一個給定區間的IO端口配置設定給一個IO裝置。
2) check_region()
檢查一個給定區間的IO端口是否空閑,或者其中一些是否已經配置設定給某個IO裝置。
3) release_region()
釋放以前配置設定給一個IO裝置的給定區間的IO端口。
Linux中可以通過以下輔助函數來通路IO端口:
inb(),inw(),inl(),outb(),outw(),outl()
“b”“w”“l”分别代表8位,16位,32位。
對IO記憶體資源的通路
注:其實對IO記憶體和IO端口的通路的不同函數,隻是對同一函數的不同調用而已,定義在include/linux/ioport.h:
#define request_region(start,n,name) __request_region(&ioport_resource, (start), (n), (name))
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name))
#define rename_region(region, newname) do { (region)->name = (newname); } while (0)
extern struct resource * __request_region(struct resource *,
resource_size_t start,
resource_size_t n, const char *name);
1) request_mem_region()
請求配置設定指定的IO記憶體資源。
2) check_mem_region()
檢查指定的IO記憶體資源是否已被占用。
3) release_mem_region()
釋放指定的IO記憶體資源。
其中傳給函數的start address參數是記憶體區的實體位址(以上函數參數表已省略)。
驅動開發人員可以将記憶體映射方式的IO端口和外設記憶體統一看作是IO記憶體資源。
ioremap()用來将IO資源的實體位址映射到核心虛位址空間(3GB - 4GB)中,參數addr是指向核心虛位址的指針。
Linux中可以通過以下輔助函數來通路IO記憶體資源:
readb(),readw(),readl(),writeb(),writew(),writel()。
Linux在kernel/resource.c檔案中定義了全局變量ioport_resource和iomem_resource,來分别描述基于IO映射方式的整個IO端口空間和基于記憶體映射方式的IO記憶體資源空間(包括IO端口和外設記憶體)。
1)關于IO與記憶體空間:
在X86處理器中存在着I/O空間的概念,I/O空間是相對于記憶體空間而言的,它通過特定的指令in、out來通路。端口号辨別了外設的寄存器位址。Intel文法的in、out指令格式為:
IN 累加器, {端口号│DX}
OUT {端口号│DX},累加器
目前,大多數嵌入式微控制器如ARM、PowerPC等中并不提供I/O空間,而僅存在記憶體空間。記憶體空間可以直接通過位址、指針來通路,程式和程式運作中使用的變量和其他資料都存在于記憶體空間中。
即便是在X86處理器中,雖然提供了I/O空間,如果由我們自己設計電路闆,外設仍然可以隻挂接在記憶體空間。此時,CPU可以像通路一個記憶體單元那樣通路外設I/O端口,而不需要設立專門的I/O指令。是以,記憶體空間是必須的,而I/O空間是可選的。
(2)inb和outb:
在Linux裝置驅動中,宜使用Linux核心提供的函數來通路定位于I/O空間的端口,這些函數包括:
· 讀寫位元組端口(8位寬)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
· 讀寫字端口(16位寬)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
· 讀寫長字端口(32位寬)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
· 讀寫一串位元組
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
· insb()從端口port開始讀count個位元組端口,并将讀取結果寫入addr指向的記憶體;outsb()将addr指向的記憶體的count個位元組連續地寫入port開始的端口。
· 讀寫一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
· 讀寫一串長字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
上述各函數中I/O端口号port的類型高度依賴于具體的硬體平台,是以,隻是寫出了unsigned
(3)readb和writeb:
在裝置的實體位址被映射到虛拟位址之後,盡管可以直接通過指針通路這些位址,但是工程師宜使用Linux核心的如下一組函數來完成裝置記憶體映射的虛拟位址的讀寫,這些函數包括:
· 讀I/O記憶體
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
與上述函數對應的較早版本的函數為(這些函數在Linux 2.6中仍然被支援):
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
· 寫I/O記憶體
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
與上述函數對應的較早版本的函數為(這些函數在Linux 2.6中仍然被支援):
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
(4)把I/O端口映射到“記憶體空間”:
void *ioport_map(unsigned long port, unsigned int count);
通過這個函數,可以把port開始的count個連續的I/O端口重映射為一段“記憶體空間”。然後就可以在其傳回的位址上像通路I/O記憶體一樣通路這些I/O端口。當不再需要這種映射時,需要調用下面的函數來撤消:
void ioport_unmap(void *addr);
實際上,分析ioport_map()的源代碼可發現,所謂的映射到記憶體空間行為實際上是給開發人員制造的一個“假象”,并沒有映射到核心虛拟位址,僅僅是為了讓工程師可使用統一的I/O記憶體通路接口通路I/O端口。
----------------------------------------------------------------------------------
Linux對I/O端口資源的管理
幾乎每一種外設都是通過讀寫裝置上的寄存器來進行的。外設寄存器也稱為“I/O端口”,通常包括:控制寄存器、狀态寄存器和資料寄存器三大類,而且一個外設的寄存器通常被連續地編址。CPU對外設IO端口實體位址的編址方式有兩種:一種是I/O映射方式(I/O-mapped),另一種是記憶體映射方式(Memory-mapped)。而具體采用哪一種則取決于CPU的體系結構。
有些體系結構的CPU(如,PowerPC、m68k等)通常隻實作一個實體位址空間(RAM)。在這種情況下,外設 I/O端口的實體位址就被映射到CPU的單一實體位址空間中,而成為記憶體的一部分。此時,CPU可以象通路一個記憶體單元那樣通路外設I/O端口,而不需要設立專門的外設I/O指令。這就是所謂的“記憶體映射方式”(Memory-mapped)。
而另外一些體系結構的CPU(典型地如X86)則為外設專門實作了一個單獨地位址空間,稱為“I/O位址空間”或者“I/O端口空間”。這是一個與CPU地RAM實體位址空間不同的位址空間,所有外設的I/O端口均在這一空間中進行編址。CPU通過設立專門的I/O指令(如X86的IN和OUT指令)來通路這一空間中的位址單元(也即 I/O端口)。這就是所謂的“I/O映射方式”(I/O-mapped)。與RAM實體位址空間相比,I/O位址空間通常都比較小,如x86 CPU的I/O空間就隻有64KB(0-0xffff)。這是“I/O映射方式”的一個主要缺點。
Linux将基于I/O映射方式的或記憶體映射方式的I/O端口通稱為“I/O區域”(I/O region)。在讨論對I/O區域的管理之前,我們首先來分析一下Linux是如何實作“I/O資源”這一抽象概念的。
Linux對I/O資源的描述
--------------------------------------
Linux設計了一個通用的資料結構resource來描述各種I/O資源(如:I/O端口、外設記憶體、DMA和IRQ等)。該結構定義在include/linux/ioport.h頭檔案中:
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
Linux對I/O資源的管理
--------------------------------------
Linux是以一種倒置的樹形結構來管理每一類I/O資源(如:I/O端口、外設記憶體、DMA和IRQ)的。每一類I/O資源都對應有一顆倒置的資源樹,樹中的每一個節點都是一個resource結構,而樹的根結點root則描述了該類資源的整個資源空間。
基于上述這個思想,Linux在kernel/resource.c檔案中實作了對資源的申請、釋放及查找等操作。
資源的申請: request_resource()
資源的釋放: release_resource()
尋找可用資源: --find_resource()
配置設定資源: allocate_resource()
管理I/O Region資源
--------------------------------------
Linux将基于I/O映射方式的I/O端口和基于記憶體映射方式的I/O端口資源統稱為“I/O區域”(I/O Region)。I/O Region仍然是一種I/O資源,是以它仍然可以用resource結構類型來描述。下面我們就來看看Linux是如何管理I/O Region的。
I/O Region的配置設定: __request_region()
I/O Region的釋放: __release_region()
檢查指定的I/O Region是否已被占用: __check_region()
管理I/O端口資源
--------------------------------------
我們都知道,采用I/O映射方式的X86處理器為外設實作了一個單獨的位址空間,也即“I/O空間”(I/O Space)或稱為“I/O端口空間”,其大小是64KB(0x0000-0xffff)。Linux在其所支援的所有平台上都實作了“I/O端口空間” 這一概念。
由于I/O空間非常小,是以即使外設總線有一個單獨的I/O端口空間,卻也不是所有的外設都将其I/O端口(指寄存器)映射到“I/O端口空間”中。比如,大多數PCI卡都通過記憶體映射方式來将其I/O端口或外設記憶體映射到CPU的RAM實體位址空間中。而老式的ISA卡通常将其I/O端口映射到I/O端口空間中。
Linux是基于“I/O Region”這一概念來實作對I/O端口資源(I/O-mapped 或 Memory-mapped)的管理的。
資源根節點的定義:
Linux在kernel/resource.c檔案中定義了全局變量ioport_resource和iomem_resource,來分别描述基于I/O映射方式的整個I/O端口空間和基于記憶體映射方式的I/O記憶體資源空間(包括I/O端口和外設記憶體)。其定義如下:
struct resource ioport_resource = {
.name = "PCI IO",
.start -= 0,
.end = IO_SPACE_LIMIT,
.flags -= IORESOURCE_IO,
};
struct resource iomem_resource = {
.name = "PCI mem",
.start -= 0,
.end = -1,
.flags -= IORESOURCE_MEM,
};
其中,宏IO_SPACE_LIMIT表示整個I/O空間的大小,對于X86平台而言,它是0xffff(定義在include/asm-i386/io.h頭檔案中)。顯然,I/O記憶體空間的大小是4GB。
對I/O端口空間的操作:
基于I/O Region的操作函數__xxx_region(),Linux在頭檔案include/linux/ioport.h中定義了三個對I/O端口空間進行操作的接口:
request_region() 請求在I/O端口空間中配置設定指定範圍的I/O端口資源。
check_region() 檢查I/O端口空間中的指定I/O端口資源是否已被占用。
release_region() 釋放I/O端口空間中的指定I/O端口資源。
對I/O記憶體資源的操作:
基于I/O Region的操作函數__xxx_region(),Linux在頭檔案include/linux/ioport.h中定義了三個對I/O記憶體資源進行操作的接口:
request_mem_region() 請求配置設定指定的I/O記憶體資源。
check_mem_region() 檢查指定的I/O記憶體資源是否已被占用。
release_mem_region() 釋放指定的I/O記憶體資源。
通路I/O端口空間(I/O-mapped)
--------------------------------------
在驅動程式請求了I/O端口空間中的端口資源後,它就可以通過CPU的IO指令來讀寫這些I/O端口了。在讀寫I/O端口時要注意的一點就是,大多數平台都區分8位、16位和32位的端口,也即要注意I/O端口的寬度。
inb() outb() inw() outw() inl() outl()
unsigned char inb(unsigned port);
port參數指定I/O端口空間中的端口位址。在大多數平台上(如x86)它都是unsigned short類型的,其它的一些平台上則是unsigned int類型的。顯然,端口位址的類型是由I/O端口空間的大小來決定的。
對I/O端口的字元串操作
除了上述這些“單發”(single-shot)的I/O操作外,某些CPU也支援對某個I/O端口進行連續的讀寫操作,也即對單個I/O端口讀或寫一系列位元組、字或32位整數,這就是所謂的“字元串I/O指令”(String Instruction)。這種指令在速度上顯然要比用循環來實作同樣的功能要快得多。
insb() outsb() insw() outw() insl() outsl()
Pausing I/O
在一些平台上(典型地如X86),對于老式總線(如ISA)上的慢速外設來說,如果CPU讀寫其I/O端口的速度太快,那就可能會發生丢失資料的現象。對于這個問題的解決方法就是在兩次連續的I/O操作之間插入一段微小的時延,以便等待慢速外設。這就是所謂的“Pausing I/O”。
對于Pausing I/O,Linux也在io.h頭檔案中定義了它的I/O讀寫函數,而且都以XXX_p命名,比如:inb_p()、outb_p()等等。
通路I/O記憶體資源(I/O Memory)
--------------------------------------
盡管I/O端口空間曾一度在x86平台上被廣泛使用,但是由于它非常小,是以大多數現代總線的裝置都以記憶體映射方式(Memory-mapped)來映射它的I/O端口(指I/O寄存器)和外設記憶體。基于記憶體映射方式的I/O端口稱為"I/O記憶體"資源(I/O Memory)。因為基于記憶體映射方式的I/O和外設記憶體在硬體實作上的差異對于軟體來說是完全透明的,是以驅動程式開發人員可以将記憶體映射方式的I/O端口和外設記憶體統一看作是“I/O記憶體”資源。
I/O記憶體資源是在CPU的單一記憶體實體位址空間内進行編址的,也即它和系統RAM同處在一個實體位址空間内。是以通過CPU的訪内指令就可以通路I/O記憶體資源。
一般來說,在系統運作時,外設的I/O記憶體資源的實體位址是已知的,這可以通過系統固件(如BIOS)在啟動時配置設定得到,或者通過裝置的硬連線(hardwired)得到。比如,PCI卡的I/O記憶體資源的實體位址通路I/O記憶體資源,而必須将它們映射到核心虛位址空間内(通過頁表),然後才能根據映射所得到的核心虛位址範圍,通過訪内指令通路這些I/O記憶體資源。實體位址就是在系統啟動時由PCI BIOS配置設定并寫到PCI卡的配置空間中的BAR中的。而ISA卡的I/O記憶體資源的實體位址則是通過裝置硬連線映射到640KB-1MB範圍之内的。但是CPU通常并沒有為這些已知的外設I/O記憶體資源的實體位址預定義虛拟位址範圍,因為它們是在系統啟動後才已知的(某種意義上講是動态的),是以驅動程式并不能直接通過
映射I/O記憶體資源
Linux在io.h頭檔案中聲明了函數ioremap(),用來将I/O記憶體資源的實體位址映射到核心虛位址空間(3GB-4GB)中
對于X86體系結構ioremap()定義在/usr/src/linux-2.6.21.5/include/asm-i386/io.h中
讀寫I/O記憶體資源
在将I/O記憶體資源的實體位址映射成核心虛位址後,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O記憶體資源了。但是,由于在某些平台上,對 I/O記憶體和系統記憶體有不同的通路處理,是以為了確定跨平台的相容性,Linux實作了一系列讀寫I/O記憶體資源的函數,這些函數在不同的平台上有不同的實作。但在x86平台上,讀寫I/O記憶體與讀寫RAM無任何差别。如下所示(include/asm-i386/io.h):
readb() readw() readl()
writeb() writew() writel()
memset_io() memcpy_fromio() memcpytoio()
顯然,在x86平台上通路I/O記憶體資源與通路系統主存RAM是無差别的。但是為了保證驅動程式的跨平台的可移植性,我們應該使用上面的函數來通路I/O記憶體資源,而不應該通過指向核心虛位址的指針來通路。