引用:http://blog.csdn.net/ce123_zhouwei/article/details/7204458編程
1、I/O端口
端口(port)是接口電路中能被CPU直接訪問的寄存器的地址。幾乎每一種外設都是經過讀寫設備上的寄存器來進行的。CPU經過這些地址即端口向接口電路中的寄存器發送命令,讀取狀態和傳送數據。外設寄存器也稱爲「I/O端口」,一般包括:控制寄存器、狀態寄存器和數據寄存器三大類,並且一個外設的寄存器一般被連續地編址。數據結構
2、IO內存
例如,在PC上能夠插上一塊圖形卡,有2MB的存儲空間,甚至可能還帶有ROM,其中裝有可執行代碼。
3、IO端口和IO內存的區分及聯繫
這二者如何區分就涉及到硬件知識,X86體系中,具備兩個地址空間:IO空間和內存空間,而RISC指令系統的CPU(如ARM、PowerPC等)一般只實現一個物理地址空間,即內存空間。
內存空間:內存地址尋址範圍,32位操做系統內存空間爲2的32次冪,即4G。
IO空間:X86特有的一個空間,與內存空間彼此獨立的地址空間,32位X86有64K的IO空間。
IO端口:當寄存器或內存位於IO空間時,稱爲IO端口。通常寄存器也俗稱I/O端口,或者說I/O ports,這個I/O端口能夠被映射在Memory Space,也能夠被映射在I/O Space。
IO內存:當寄存器或內存位於內存空間時,稱爲IO內存。app
4、外設IO端口物理地址的編址方式
CPU對外設IO端口物理地址的編址方式有兩種:一種是I/O映射方式(I/O-mapped),另外一種是內存映射方式(Memory-mapped)。而具體採用哪種則取決於CPU的體系結構。函數
一、統一編址
RISC指令系統的CPU(如,PowerPC、m68k、ARM等)一般只實現一個物理地址空間(RAM)。在這種狀況下,外設I/O端口的物理地址就被映射到CPU的單一物理地址空間中,而成爲內存的一部分。此時,CPU能夠象訪問一個內存單元那樣訪問外設I/O端口,而不須要設立專門的外設I/O指令。
統一編址也稱爲「I/O內存」方式,外設寄存器位於「內存空間」(不少外設有本身的內存、緩衝區,外設的寄存器和內存統稱「I/O空間」)。spa
二、獨立編址
而另一些體系結構的CPU(典型地如X86)則爲外設專門實現了一個單獨地地址空間,稱爲「I/O地址空間」或者「I/O端口空間」。這是一個與CPU地RAM物理地址空間不一樣的地址空間,全部外設的I/O端口均在這一空間中進行編址。CPU經過設立專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元(也即I/O端口)。與RAM物理地址空間相比,I/O地址空間一般都比較小,如x86 CPU的I/O空間就只有64KB(0-0xffff)。這是「I/O映射方式」的一個主要缺點。
獨立編址也稱爲「I/O端口」方式,外設寄存器位於「I/O(地址)空間」。操作系統
三、優缺點
獨立編址主要優勢是:
1)、I/O端口地址不佔用存儲器空間;使用專門的I/O指令對端口進行操做,I/O指令短,執行速度快。
2)、而且因爲專門I/O指令與存儲器訪問指令有明顯的區別,使程序中I/O操做和存儲器操做層次清晰,程序的可讀性強。
3)、同時,因爲使用專門的I/O指令訪問端口,而且I/O端口地址和存儲器地址是分開的,故I/O端口地址和存儲器地址能夠重疊,而不會相互混淆。
4)、譯碼電路比較簡單(由於I/0端口的地址空間通常較小,所用地址線也就較少)。
其缺點是:只能用專門的I/0指令,訪問端口的方法不如訪問存儲器的方法多。
統一編址優勢:
1)、因爲對I/O設備的訪問是使用訪問存儲器的指令,因此指令類型多,功能齊全,這不只使訪問I/O端口可實現輸入/輸出操做,並且還可對端口內容進行算術邏輯運算,移位等等;
2)、另外,能給端口有較大的編址空間,這對大型控制系統和數據通訊系統是頗有意義的。
這種方式的缺點是端口占用了存儲器的地址空間,使存儲器容量減少,另外指令長度比專門I/O指令要長,於是執行速度較慢。
究竟採用哪種取決於系統的整體設計。在一個系統中也能夠同時使用兩種方式,前提是首先要支持I/O獨立編址。Intel的x86微處理器都支持I/O 獨立編址,由於它們的指令系統中都有I/O指令,並設置了能夠區分I/O訪問和存儲器訪問的控制信號引腳。而一些微處理器或單片機,爲了減小引腳,從而減 少芯片佔用面積,不支持I/O獨立編址,只能採用存儲器統一編址。.net
5、Linux下訪問IO端口
對於某一既定的系統,它要麼是獨立編址、要麼是統一編址,具體採用哪種則取決於CPU的體系結構。 如,PowerPC、m68k等採用統一編址,而X86等則採用獨立編址,存在IO空間的概念。目前,大多數嵌入式微控制器如ARM、PowerPC等並不提供I/O空間,僅有內存空間,可直接用地址、指針訪問。但對於Linux內核而言,它可能用於不一樣的CPU,因此它必須都要考慮這兩種方式,因而它採用一種新的方法,將基於I/O映射方式的或內存映射方式的I/O端口通稱爲「I/O區域」(I/O region),不論你採用哪一種方式,都要先申請IO區域:request_resource(),結束時釋放它:release_resource()。
IO region是一種IO資源,所以它能夠用resource結構類型來描述。
訪問IO端口有2種途徑:I/O映射方式(I/O-mapped)、內存映射方式(Memory-mapped)。前一種途徑不映射到內存空間,直接使用 intb()/outb()之類的函數來讀寫IO端口;後一種MMIO是先把IO端口映射到IO內存(「內存空間」),再使用訪問IO內存的函數來訪問 IO端口。設計
一、I/O映射方式
直接使用IO端口操做函數:在設備打開或驅動模塊被加載時申請IO端口區域,以後使用inb(),outb()等進行端口訪問,最後在設備關閉或驅動被卸載時釋放IO端口範圍。
in、out、ins和outs彙編語言指令均可以訪問I/O端口。內核中包含了如下輔助函數來簡化這種訪問:
inb( )、inw( )、inl( )指針
分別從I/O端口讀取一、2或4個連續字節。後綴「b」、「w」、「l」分別表明一個字節(8位)、一個字(16位)以及一個長整型(32位)。
inb_p( )、inw_p( )、inl_p( )
分別從I/O端口讀取一、2或4個連續字節,而後執行一條「啞元(dummy,即空指令)」指令使CPU暫停。 code
outb( )、outw( )、outl( )
分別向一個I/O端口寫入一、2或4個連續字節。
outb_p( )、outw_p( )、outl_p( )
分別向一個I/O端口寫入一、2或4個連續字節,而後執行一條「啞元」指令使CPU暫停。
insb( )、insw( )、insl( )
分別從I/O端口讀入以一、2或4個字節爲一組的連續字節序列。字節序列的長度由該函數的參數給出。
outsb( )、outsw( )、outsl( )
分別向I/O端口寫入以一、2或4個字節爲一組的連續字節序列。
流程以下:
雖然訪問I/O端口很是簡單,可是檢測哪些I/O端口已經分配給I/O設備可能就不這麼簡單了,對基於ISA總線的系統來講更是如此。一般,I/O設備驅動程序爲了探測硬件設備,須要盲目地向某一I/O端口寫入數據;可是,若是其餘硬件設備已經使用這個端口,那麼系統就會崩潰。爲了防止這種狀況的發生,內核必須使用「資源」來記錄分配給每一個硬件設備的I/O端口。資源表示某個實體的一部分,這部分被互斥地分配給設備驅動程序。在這裏,資源表示I/O端口地址的一個範圍。每一個資源對應的信息存放在resource數據結構中:
1 // 代碼1 2 struct resource { 3 resource_size_t start;// 資源範圍的開始 4 resource_size_t end;// 資源範圍的結束 5 const char *name; //資源擁有者的名字 6 unsigned long flags;// 各類標誌 7 struct resource *parent, *sibling, *child;// 指向資源樹中父親,兄弟和孩子的指針 8 };
全部的同種資源都插入到一個樹型數據結構(父親、兄弟和孩子)中;例如,表示I/O端口地址範圍的全部資源都包括在一個根節點爲ioport_resource的樹中。節點的孩子被收集在一個鏈表中,其第一個元素由child指向。sibling字段指向鏈表中的下一個節點。
爲何使用樹?例如,考慮一下IDE硬盤接口所使用的I/O端口地址-好比說從0xf000 到 0xf00f。那麼,start字段爲0xf000 且end 字段爲0xf00f的這樣一個資源包含在樹中,控制器的常規名字存放在name字段中。可是,IDE設備驅動程序須要記住另外的信息,也就是IDE鏈主盤使用0xf000 到0xf007的子範圍,從盤使用0xf008 到0xf00f的子範圍。爲了作到這點,設備驅動程序把兩個子範圍對應的孩子插入到從0xf000 到0xf00f的整個範圍對應的資源下。通常來講,樹中的每一個節點確定至關於父節點對應範圍的一個子範圍。I/O端口資源樹(ioport_resource)的根節點跨越了整個I/O地址空間(從端口0到65535)。
任何設備驅動程序均可以使用下面三個函數,傳遞給它們的參數爲資源樹的根節點和要插入的新資源數據結構的地址:
request_resource( ) //把一個給定範圍分配給一個I/O設備。
allocate_resource( ) //在資源樹中尋找一個給定大小和排列方式的可用範圍;若存在,將這個範圍分配給一個I/O設備(主要由PCI設備驅動程序使用,可使用任意的端口號和主板上的內存地址對其進行配置)。
release_resource( ) //釋放之前分配給I/O設備的給定範圍。
內核也爲以上函數定義了一些應用於I/O端口的快捷函數:request_region( )分配I/O端口的給定範圍,release_region( )釋放之前分配給I/O端口的範圍。當前分配給I/O設備的全部I/O地址的樹均可以從/proc/ioports文件中得到。
二、內存映射方式
將IO端口映射爲內存進行訪問,在設備打開或驅動模塊被加載時,申請IO端口區域並使用ioport_map()映射到內存,以後使用IO內存的函數進行端口訪問,最後,在設備關閉或驅動模塊被卸載時釋放IO端口並釋放映射。
映射函數的原型爲:
void *ioport_map(unsigned long port, unsigned int count);
經過這個函數,能夠把port開始的count個連續的I/O端口重映射爲一段「內存空間」。而後就能夠在其返回的地址上像訪問I/O內存同樣訪問這些I/O端口。但請注意,在進行映射前,還必須經過request_region( )分配I/O端口。
當再也不須要這種映射時,須要調用下面的函數來撤消:
void ioport_unmap(void *addr);
在設備的物理地址被映射到虛擬地址以後,儘管能夠直接經過指針訪問這些地址,可是宜使用Linux內核的以下一組函數來完成訪問I/O內存:·讀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);
流程以下:
6、Linux下訪問IO內存
IO內存的訪問方法是:首先調用request_mem_region()申請資源,接着將寄存器地址經過ioremap()映射到內核空間的虛擬地址,以後就能夠Linux設備訪問編程接口訪問這些寄存器了,訪問完成後,使用ioremap()對申請的虛擬地址進行釋放,並釋放release_mem_region()申請的IO內存資源。
struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
這個函數從內核申請len個內存地址(在3G~4G之間的虛地址),而這裏的start爲I/O物理地址,name爲設備的名稱。注意,。若是分配成功,則返回非NULL,不然,返回NULL。
另外,能夠經過/proc/iomem查看系統給各類設備的內存範圍。
要釋放所申請的I/O內存,應當使用release_mem_region()函數:
void release_mem_region(unsigned long start, unsigned long len)
申請一組I/O內存後, 調用ioremap()函數:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
其中三個參數的含義爲:
phys_addr:與requset_mem_region函數中參數start相同的I/O物理地址;
size:要映射的空間的大小;
flags:要映射的IO空間的和權限有關的標誌;
功能:將一個I/O地址空間映射到內核的虛擬地址空間上(經過release_mem_region()申請到的)
流程以下:
7、ioremap和ioport_map
下面具體看一下ioport_map和ioport_umap的源碼:
1 //代碼2 2 void __iomem *ioport_map(unsigned long port, unsigned int nr) 3 { 4 if (port > PIO_MASK) 5 return NULL; 6 return (void __iomem *) (unsigned long) (port + PIO_OFFSET); 7 } 8 9 void ioport_unmap(void __iomem *addr) 10 { 11 /* Nothing to do */ 12 }
ioport_map僅僅是將port加上PIO_OFFSET(64k),而ioport_unmap則什麼都不作。這樣portio的64k空間就被映射到虛擬地址的64k~128k之間,而ioremap返回的虛擬地址則確定在3G之上。ioport_map函數的目的是試圖提供與ioremap一致的虛擬地址空間。分析ioport_map()的源代碼可發現,所謂的映射到內存空間行爲其實是給開發人員製造的一個「假象」,並無映射到內核虛擬地址,僅僅是爲了讓工程師可以使用統一的I/O內存訪問接口ioread8/iowrite8(......)訪問I/O端口。
最後來看一下ioread8的源碼,其實現也就是對虛擬地址進行了判斷,以區分IO端口和IO內存,而後分別使用inb/outb和readb/writeb來讀寫。
1 //代碼3 2 unsigned int fastcall ioread8(void __iomem *addr) 3 { 4 IO_COND(addr, return inb(port), return readb(addr)); 5 } 6 7 #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET) 8 #define IO_COND(addr, is_pio, is_mmio) do { \ 9 unsigned long port = (unsigned long __force)addr; \ 10 if (port < PIO_RESERVED) { \ 11 VERIFY_PIO(port); \ 12 port &= PIO_MASK; \ 13 is_pio; \ 14 } else { \ 15 is_mmio; \ 16 } \ 17 } while (0)
展開:
1 //代碼4 2 unsigned int fastcall ioread8(void __iomem *addr) 3 { 4 unsigned long port = (unsigned long __force)addr; 5 if( port < 0x40000UL ) { 6 BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET ); 7 port &= PIO_MASK; 8 return inb(port); 9 }else{ 10 return readb(addr); 11 } 12 }
7、總結 外設IO寄存器地址獨立編址的CPU,這時應該稱外設IO寄存器爲IO端口,訪問IO寄存器可經過ioport_map將其映射到虛擬地址空間,但實際上這是給開發人員製造的一個「假象」,並無映射到內核虛擬地址,僅僅是爲了可使用和IO內存同樣的接口訪問IO寄存器;也能夠直接使用in/out指令訪問IO寄存器。 例如:Intel x86平臺普通使用了名爲內存映射(MMIO)的技術,該技術是PCI規範的一部分,IO設備端口被映射到內存空間,映射後,CPU訪問IO端口就如同訪 問內存同樣。 外設IO寄存器地址統一編址的CPU,這時應該稱外設IO寄存器爲IO內存,訪問IO寄存器可經過ioremap將其映射到虛擬地址空間,而後再使用read/write接口訪問。