引用:http://blog.csdn.net/ce123_zhouwei/article/details/7204458
一、I/O連接埠
連接埠(port)是介面電路中能被CPU直接存取的寄存器的地址。幾乎每一種外設都是通過讀寫裝置上的寄存器來進行的。CPU通過這些地址即連接埠向介面電路中的寄存器發送命令,讀取狀態和傳送資料。外設寄存器也稱為“I/O連接埠”,通常包括:控制寄存器、狀態寄存器和資料寄存器三大類,而且一個外設的寄存器通常被連續地編址。
二、IO記憶體
例如,在PC上可以插上一塊圖形卡,有2MB的儲存空間,甚至可能還帶有ROM,其中裝有可執行代碼。
三、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記憶體。
四、外設IO連接埠物理地址的編址方式
CPU對外設IO連接埠物理地址的編址方式有兩種:一種是I/O映射方式(I/O-mapped),另一種是記憶體映射方式(Memory-mapped)。而具體採用哪一種則取決於CPU的體繫結構。
1、統一編址
RISC指令系統的CPU(如,PowerPC、m68k、ARM等)通常只實現一個物理地址空間(RAM)。在這種情況下,外設I/O連接埠的物理地址就被映射到CPU的單一物理地址空間中,而成為記憶體的一部分。此時,CPU可以象訪問一個記憶體單元那樣訪問外設I/O連接埠,而不需要設立專門的外設I/O指令。
統一編址也稱為“I/O記憶體”方式,外設寄存器位於“記憶體空間”(很多外設有自己的記憶體、緩衝區,外設的寄存器和記憶體統稱“I/O空間”)。
2、獨立編址
而另外一些體繫結構的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(地址)空間”。
3、優缺點
獨立編址主要優點是:
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獨立編址,只能採用儲存空間統一編址。
五、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連接埠。
1、I/O映射方式
直接使用IO連接埠操作函數:在裝置開啟或驅動模組被載入時申請IO連接埠地區,之後使用inb(),outb()等進行連接埠訪問,最後在裝置關閉或驅動被卸載時釋放IO連接埠範圍。
in、out、ins和outs組合語言指令都可以訪問I/O連接埠。核心中包含了以下輔助函數來簡化這種訪問:
inb( )、inw( )、inl( )
分別從I/O連接埠讀取1、2或4個連續位元組。尾碼“b”、“w”、“l”分別代表一個位元組(8位)、一個字(16位)以及一個長整型(32位)。
inb_p( )、inw_p( )、inl_p( )
分別從I/O連接埠讀取1、2或4個連續位元組,然後執行一條“啞元(dummy,即空指令)”指令使CPU暫停。
outb( )、outw( )、outl( )
分別向一個I/O連接埠寫入1、2或4個連續位元組。
outb_p( )、outw_p( )、outl_p( )
分別向一個I/O連接埠寫入1、2或4個連續位元組,然後執行一條“啞元”指令使CPU暫停。
insb( )、insw( )、insl( )
分別從I/O連接埠讀入以1、2或4個位元組為一組的連續位元組序列。位元組序列的長度由該函數的參數給出。
outsb( )、outsw( )、outsl( )
分別向I/O連接埠寫入以1、2或4個位元組為一組的連續位元組序列。
流程如下:
雖然訪問I/O連接埠非常簡單,但是檢測哪些I/O連接埠已經分配給I/O裝置可能就不這麼簡單了,對基於ISA匯流排的系統來說更是如此。通常,I/O裝置驅動程式為了探測硬體裝置,需要盲目地向某一I/O連接埠寫入資料;但是,如果其他硬體裝置已經使用這個連接埠,那麼系統就會崩潰。為了防止這種情況的發生,核心必須使用“資源”來記錄分配給每個硬體裝置的I/O連接埠。資源表示某個實體的一部分,這部分被互斥地分配給裝置驅動程式。在這裡,資源表示I/O連接埠地址的一個範圍。每個資源對應的資訊存放在resource資料結構中:
// 代碼1 resource_size_t start; resource_size_t end; *name; unsigned flags; resource *parent, *sibling, *child; };
所有的同種資源都插入到一個樹型資料結構(父親、兄弟和孩子)中;例如,表示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檔案中獲得。
2、記憶體映射方式
將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);
流程如下:
六、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()申請到的)
流程如下:
七、ioremap和ioport_map
下面具體看一下ioport_map和ioport_umap的源碼:
__iomem *ioport_map(unsigned port, unsigned (port > ( __iomem *) (unsigned ) (port + ioport_unmap( __iomem * }
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來讀寫。
unsigned fastcall ioread8( __iomem * IO_COND(addr, inb(port), VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET) IO_COND(addr, is_pio, is_mmio) do { \ unsigned port = (unsigned (port < port &= } } ()
展開:
unsigned fastcall ioread8( __iomem * unsigned port = (unsigned ( port < BUG_ON( (port & ~PIO_MASK) != port &= } }
七、總結
外設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介面訪問。