1. 程式人生 > 其它 >IO埠和IO記憶體的區別及分別使用的函式介面

IO埠和IO記憶體的區別及分別使用的函式介面

參考---->

https://www.cnblogs.com/reality-soul/p/6126376.html

IO埠和IO記憶體的區別及分別使用的函式介面

每個外設都是通過讀寫其暫存器來控制的。外設暫存器也稱為I/O埠,通常包括:控制暫存器、狀態暫存器和資料暫存器三大類。根據訪問外設暫存器的不同方式,可以把CPU分成兩大類。一類CPU(如M68K,Power PC等)把這些暫存器看作記憶體的一部分,暫存器參與記憶體統一編址,訪問暫存器就通過訪問一般的記憶體指令進行,所以,這種CPU沒有專門用於裝置I/O的指令。這就是所謂的“I/O記憶體”方式。另一類CPU(典型的如X86),將外設的暫存器看成一個獨立的地址空間,所以訪問記憶體的指令不能用來訪問這些暫存器,而要為對外設暫存器的讀/寫設定專用指令,如IN和OUT指令。這就是所謂的“I/O埠

”方式。但是,用於I/O指令的“地址空間”相對來說是很小的,如x86 CPU的I/O空間就只有64KB(0-0xffff)。

結合下圖,我們徹底講述IO埠和IO記憶體以及記憶體之間的關係。主存16M位元組的SDRAM,外設是個視訊採集卡,上面有16M位元組的SDRAM作為緩衝區。

1.CPU是i386架構的情況

在i386系列的處理中,記憶體和外部IO是獨立編址,也是獨立定址的。MEM的記憶體空間是32位可以定址到4G,IO空間是16位可以定址到64K。

Linux核心中,訪問外設上的IO Port必須通過IO Port的定址方式。而訪問IO Mem就比較羅嗦,外部MEM不能和主存一樣訪問,雖然大小上不相上下,可是外部MEM是沒有在系統中註冊的。訪問外部IO MEM必須通過remap對映到核心的MEM空間後才能訪問。為了達到介面的同一性,核心提供了IO Port到IO Mem的對映函式。對映後IO Port就可以看作是IO Mem,按照IO Mem的訪問方式即可。

3. CPU是ARM或PPC架構的情況

在這一類的嵌入式處理器中,IO Port的定址方式是採用記憶體對映,也就是IO bus就是Mem bus。系統的定址能力如果是32位,IO Port+Mem(包括IO Mem)可以達到4G。

1.使用I/O埠

I/O埠是驅動用來和很多裝置通訊的方法。

1.1、分配I/O埠

在驅動還沒獨佔裝置之前,不應對埠進行操作。核心提供了一個註冊介面,以允許驅動宣告其需要的埠:

#include<linux/ioport.h>
/* request_region告訴核心:要使用first開始的n個埠。引數name為裝置名。如果分配成功返回值是非NULL;否則無法使用需要的端 口(/proc/ioports包含了系統當前所有埠的分配資訊,若request_region分配失敗時,可以檢視該檔案,看誰先用了你要的埠) */
structresource*request_region(unsignedlongfirst,unsignedlongn,constchar*name);
/* 用完I/O埠後(可能在模組解除安裝時),應當呼叫release_region將I/O埠返還給系統。引數start和n應與之前傳遞給request_region一致 */
voidrelease_region(unsignedlongstart,unsignedlongn);
/*check_region用於檢查一個給定的I/O端 口集是否可用。如果給定的埠不可用,check_region返回一個錯誤碼。不推薦使用該函式,因為即便它返回0(埠可用),它也不能保證後面的端 口分配操作會成功,因為檢查和後面的埠分配並不是一個原子操作。而request_region通過加鎖來保證操作的原子性,因此是安全的 */
intcheck_region(unsignedlongfirst,unsignedlongn);


1.2、操作I/O埠

在驅動成功請求到I/O埠後,就可以讀寫這些埠了。大部分硬體會將8位、16位和32位埠區分開,無法像訪問記憶體那樣混淆使用。驅動程式必須呼叫不同的函式來訪問不同大小的埠。

如同前面所講的,僅支援單地址空間的計算機體系通過將I/O埠地址重新對映到記憶體地址來偽裝埠I/O。為了提高移植性,核心對驅動隱藏了這些細節。Linux核心標頭檔案(體系依賴的標頭檔案<asm/io.h>)定義了下列行內函數來存取I/O埠:

/*inb/outb:讀/寫位元組埠(8位寬)。有些體系將port引數定義為unsigned long;而有些平臺則將它定義為unsigned short。inb的返回型別也是依賴體系的 */
unsignedinb(unsignedport);
voidoutb(unsignedcharbyte,unsignedport);
/*inw/outw:讀/寫字埠(16位寬)*/
unsignedinw(unsignedport);
voidoutw(unsignedshortword,unsignedport);
/*inl/outl:讀/寫32位埠。longword也是依賴體系的,有的體系為unsigned long;而有的為unsigned int */
unsignedinl(unsignedport);
voidoutl(unsignedlongword,unsignedport);

從現在開始,當我們使用unsigned沒有進一步指定型別時,表示是一個依賴體系的定義。

注意,沒有64位的I/O埠操作函式。即便在64位體系中,埠地址空間使用一個32位(最大)的資料通路。

1.3、從使用者空間訪問I/O埠

1.2節介紹的函式主要是提供給驅動使用,但它們也可在使用者空間使用,至少在PC機上可以。GNUC庫在<sys/io.h>中定義它們。如果在使用者空間使用這些函式,必須滿足下列條件:

1)、程式必須使用-O選項編譯來強制擴充套件行內函數

2)、必須使用ioperm和iopl系統呼叫(#include <sys/perm.h>)來獲得進行操作I/O埠的許可權。ioperm為獲取單個埠的操作許可,iopl為獲取整個I/O空間許可。這2個函式都是x86特有的

3)、程式必須以root來呼叫ioperm或者iopl,或者其父程序(祖先)必須以root獲得的埠操作許可權

如果平臺不支援ioperm和iopl系統呼叫,通過使用/dev/prot裝置檔案,使用者空間仍然可以存取I/O埠。但是要注意的是,這個檔案的定義也是依賴平臺的。

1.4、字串操作

除了一次傳遞一個數據的I/O操作,某些處理器實現了一次傳遞一序列資料(單位可以是位元組、字和雙字)的特殊指令。這些所謂的字串指令,它們完成任務比一個C語言迴圈更快。下列巨集定義實現字串操作,在某些體系上,它們通過使用單個機器指令實現;但如果目標處理器沒有進行字串I/O指令,則通過執行一個緊湊的迴圈實現。

字串函式的原型是:

/* insb:從I/O埠port讀取count個數據(單位位元組)到以記憶體地址addr為開始的記憶體空間*/
voidinsb(unsignedport,void*addr,unsignedlongcount);
/* outsb:將記憶體地址addr開始的count個數據(單位位元組)寫到I/O埠port*/
voidoutsb(unsignedport,void*addr,unsignedlongcount);
/* insw:從I/O埠port讀取count個數據(單位字)到以記憶體地址addr為開始的記憶體空間*/
voidinsw(unsignedport,void*addr,unsignedlongcount);
/* outsw:將記憶體地址addr開始的count個數據(單位字)寫到I/O埠port*/
voidoutsw(unsignedport,void*addr,unsignedlongcount);
/* insl:從I/O埠port讀取count個數據(單位雙字)到以記憶體地址addr為開始的記憶體空間*/
voidinsl(unsignedport,void*addr,unsignedlongcount);
/* outsl:將記憶體地址addr開始的count個數據(單位雙字)寫到I/O埠port*/
voidoutsl(unsignedport,void*addr,unsignedlongcount);

注意:使用字串函式時,它們直接將位元組流從埠中讀取或寫入。當埠和主機系統有不同的位元組序時,會導致不可預期的結果。使用inw讀取埠應在必要時自行轉換位元組序,以匹配主機位元組序。

1.5、暫停式I/O操作函式

由於處理器的速率可能與外設(尤其是低速裝置)的並不匹配,當處理器過快地傳送資料到或自匯流排時,這時可能就會引起問題。解決方法是:如果在I/O指令後面緊跟著另一個相似的I/O指令,就必須插入一個小的延時。為此,Linux提供了暫停式I/O操作函式,這些函式的名子只是在非暫停式I/O操作函式(前面提到的那些I/O操作函式都是非暫停式的)名後加上_p,如inb_p、outb_p等。大部分體系都支援這些函式,儘管它們常常被擴充套件為與非暫停I/O同樣的程式碼,因為如果體系使用一個合理的現代外設匯流排,沒有必要額外暫停。

以下是ARM體系暫停式I/O巨集的定義:

#defineoutb_p(val,port)outb((val),(port))
#defineoutw_p(val,port)outw((val),(port))
#defineoutl_p(val,port)outl((val),(port))
#defineinb_p(port)inb((port))
#defineinw_p(port)inw((port))
#defineinl_p(port)inl((port))
#defineoutsb_p(port,from,len)outsb(port,from,len)
#defineoutsw_p(port,from,len)outsw(port,from,len)
#defineoutsl_p(port,from,len)outsl(port,from,len)
#defineinsb_p(port,to,len)insb(port,to,len)
#defineinsw_p(port,to,len)insw(port,to,len)
#defineinsl_p(port,to,len)insl(port,to,len)

因為ARM使用內部匯流排,就沒有必要額外暫停,所以暫停式的I/O函式被擴充套件為與非暫停式I/O同樣的程式碼。

1.6、平臺依賴性

由於自身的特性,I/O指令高度依賴於處理器,非常難以隱藏各體系間的不同。因此,大部分的關於埠I/O的原始碼是平臺依賴的。以下是x86和ARM所使用函式的總結:

IA-32(x86)

x86_64

這個體系支援本章介紹的所有函式;port引數的型別為unsignedshort。

ARM

埠對映到記憶體,並且支援本章介紹的所有函式;port引數的型別為unsignedint;字串函式用C語言實現。

2、使用I/O記憶體

儘管I/O埠在x86世界中非常流行,但是用來和裝置通訊的主要機制是通過記憶體對映的暫存器和裝置記憶體,兩者都稱為I/O記憶體,因為暫存器和記憶體之間的區別對軟體是透明的。

I/O記憶體僅僅是一個類似於RAM的區域,處理器通過匯流排訪問該區域,以實現對裝置的訪問。同樣,讀寫這個區域是有邊際效應。

根據計算機體系和匯流排不同,I/O記憶體可分為可以或者不可以通過頁表來存取。若通過頁表存取,核心必須先重新編排實體地址,使其對驅動程式可見,這就意味著在進行任何I/O操作之前,你必須呼叫ioremap;如果不需要頁表,I/O記憶體區域就類似於I/O埠,你可以直接使用適當的I/O函式讀寫它們。

由於邊際效應的緣故,不管是否需要ioremap,都不鼓勵直接使用I/O記憶體指標,而應使用專門的I/O記憶體操作函式。這些I/O記憶體操作函式不僅在所有平臺上是安全,而且對直接使用指標操作I/O記憶體的情況進行了優化。

2.1、I/O記憶體分配和對映

I/O記憶體區在使用前必須先分配。分配記憶體區的函式介面在<linux/ioport.h>定義中:

/* request_mem_region分配一個開始於start,len位元組的I/O記憶體區。分配成功,返回一個非NULL指標;否則返回NULL。系統當前所有I/O記憶體分配資訊都在/proc/iomem檔案中列出,你分配失敗時,可以看看該檔案,看誰先佔用了該記憶體區 */
structresource*request_mem_region(unsignedlongstart,unsignedlonglen,char*name);
/* release_mem_region用於釋放不再需要的I/O記憶體區*/
voidrelease_mem_region(unsignedlongstart,unsignedlonglen);
/* check_mem_region用於檢查I/O記憶體區的可用性。同樣,該函式不安全,不推薦使用 */
intcheck_mem_region(unsignedlongstart,unsignedlonglen);

在訪問I/O記憶體之前,分配I/O記憶體並不是唯一要求的步驟,你還必須保證核心可存取該I/O記憶體。訪問I/O記憶體並不只是簡單解引用指標,在許多體系中,I/O記憶體無法以這種方式直接存取。因此,還必須通過ioremap函式設定一個對映。

#include<asm/io.h>
/*ioremap用於將I/O記憶體區對映到虛擬地址。引數phys_addr為要對映的I/O記憶體起始地址,引數size為要對映的I/O記憶體的大小,返回值為被對映到的虛擬地址 */
void*ioremap(unsignedlongphys_addr,unsignedlongsize);

/* ioremap_nocache為ioremap的無快取版本。實際上,在大部分體系中,ioremap與ioremap_nocache的實現一樣的,因為所有 I/O 記憶體都是在無快取的記憶體地址空間中 */
void*ioremap_nocache(unsignedlongphys_addr,unsignedlongsize);
/* iounmap用於釋放不再需要的對映 */
voidiounmap(void*addr);

經過ioremap(和iounmap)之後,裝置驅動就可以存取任何I/O記憶體地址。注意,ioremap返回的地址不可以直接解引用;相反,應當使用核心提供的訪問函式。

2.2、訪問I/O記憶體

訪問I/O記憶體的正確方式是通過一系列專門用於實現此目的的函式:

#include<asm/io.h>
/*I/O記憶體讀函式。引數addr應當是從ioremap獲得的地址(可能包含一個整型偏移); 返回值是從給定I/O記憶體讀取到的值 */
unsignedintioread8(void*addr);
unsignedintioread16(void*addr);
unsignedintioread32(void*addr);
/*I/O記憶體寫函式。引數addr同I/O記憶體讀函式,引數value為要寫的值 */
voidiowrite8(u8 value,void*addr);
voidiowrite16(u16 value,void*addr);
voidiowrite32(u32 value,void*addr);
/* 以下這些函式讀和寫一系列值到一個給定的 I/O 記憶體地址,從給定的buf讀或寫count個值到給定的addr。引數count表示要讀寫的資料個數,而不是位元組大小 */
voidioread8_rep(void*addr,void*buf,unsignedlongcount);
voidioread16_rep(void*addr,void*buf,unsignedlongcount);
voidioread32_rep(void*addr,void*buf,unsignedlongcount);
voidiowrite8_rep(void*addr,constvoid*buf,unsignedlongcount);
voidiowrite16_rep(void*addr,constvoid*buf,unsignedlongcount);
voidiowrite32_rep(void*addr,,onstvoid*buf,,nsignedlongcount);
/* 需要操作一塊I/O地址時,使用下列函式(這些函式的行為類似於它們的C庫類似函式): */
voidmemset_io(void*addr,u8 value,unsignedintcount);
voidmemcpy_fromio(void*dest,void*source,unsignedintcount);
voidmemcpy_toio(void*dest,void*source,unsignedintcount);
/* 舊的I/O記憶體讀寫函式,不推薦使用 */
unsignedreadb(address);
unsignedreadw(address);
unsignedreadl(address);
voidwriteb(unsignedvalue,address);
voidwritew(unsignedvalue,address);
voidwritel(unsignedvalue,address);

2.3、像I/O記憶體一樣使用埠

一些硬體有一個有趣的特性:有些版本使用I/O埠;而有些版本則使用I/O記憶體。不管是I/O埠還是I/O記憶體,處理器見到的裝置暫存器都是相同的,只是訪問方法不同。為了統一程式設計介面,使驅動程式易於編寫,2.6核心提供了一個ioport_map函式:

/*ioport_map重新對映count個I/O埠,使它們看起來I/O記憶體。此後,驅動程式可以在ioport_map返回的地址上使用ioread8和同類函式。這樣,就可以在程式設計時,消除了I/O埠和I/O 記憶體的區別*/
void*ioport_map(unsignedlongport,unsignedintcount);
/* ioport_unmap用於釋放不再需要的對映 */
voidioport_unmap(void*addr);

注意,I/O埠在重新對映前必須使用request_region分配所需的I/O埠。


3、ARM體系的I/O操作介面

s3c24x0處理器使用的是I/O記憶體,也就是說:s3c24x0處理器使用統一編址方式,I/O暫存器和記憶體使用的是單一地址空間,並且讀寫I/O暫存器和讀寫記憶體的指令是相同的。所以推薦使用I/O記憶體的相關指令和函式。但這並不表示I/O埠的指令在s3c24x0中不可用。如果你注意過s3c24x0關於I/O方面的核心原始碼,你就會發現:其實I/O埠的指令只是一個外殼,內部還是使用和I/O記憶體一樣的程式碼。

下面是ARM體系原始的I/O操作函式。其實後面I/O埠和I/O記憶體操作函式,只是對這些函式進行再封裝。從這裡也可以看出為什麼我們不推薦直接使用I/O埠和I/O記憶體地址指標,而是要求使用專門的I/O操作函式——專門的I/O操作函式會檢查地址指標是否有效是否為IO地址(通過__iomem或__chk_io_ptr)

#include<asm-arm/io.h>

/*
* Generic IO read/write. These perform native-endian accesses. Note
* that some architectures will want to re-define __raw_{read,write}w.
*/
externvoid__raw_writesb(void__iomem*addr,constvoid*data,intbytelen);
externvoid__raw_writesw(void__iomem*addr,constvoid*data,intwordlen);
externvoid__raw_writesl(void__iomem*addr,constvoid*data,intlonglen);
externvoid__raw_readsb(constvoid__iomem*addr,void*data,intbytelen);
externvoid__raw_readsw(constvoid__iomem*addr,void*data,intwordlen);
externvoid__raw_readsl(constvoid__iomem*addr,void*data,intlonglen);
#define__raw_writeb(v,a)(__chk_io_ptr(a),*(volatileunsignedchar__force*)(a)=(v))
#define__raw_writew(v,a)(__chk_io_ptr(a),*(volatileunsignedshort__force*)(a)=(v))
#define__raw_writel(v,a)(__chk_io_ptr(a),*(volatileunsignedint__force*)(a)=(v))
#define__raw_readb(a)(__chk_io_ptr(a),*(volatileunsignedchar__force*)(a))
#define__raw_readw(a)(__chk_io_ptr(a),*(volatileunsignedshort__force*)(a))
#define__raw_readl(a)(__chk_io_ptr(a),*(volatileunsignedint__force*)(a))

關於__force和__iomem

#include<linux/compiler.h>

/* __force表示所定義的變數型別是可以做強制型別轉換的 */
#define__force __attribute__((force))
/* __iomem是用來修飾一個變數的,這個變數必須是非解引用(no dereference)的,即這個變數地址必須是有效的,而且變數所在的地址空間必須是2,即裝置地址對映空間。0表示normal space,即普通地址空間,對核心程式碼來說,當然就是核心空間地址了。1表示使用者地址空間,2表示是裝置地址對映空間 */
#define__iomem __attribute__((noderef,address_space(2)))

I/O埠

#include<asm-arm/io.h>

#defineoutb(v,p)__raw_writeb(v,__io(p))
#defineoutw(v,p)__raw_writew((__force __u16)\
cpu_to_le16(v),__io(p))
#defineoutl(v,p)__raw_writel((__force __u32)\
cpu_to_le32(v),__io(p))
#defineinb(p)({__u8 __v=__raw_readb(__io(p));__v;})
#defineinw(p)({__u16 __v=le16_to_cpu((__force __le16)\
__raw_readw(__io(p)));__v;})
#defineinl(p)({__u32 __v=le32_to_cpu((__force __le32)\
__raw_readl(__io(p)));__v;})
#defineoutsb(p,d,l)__raw_writesb(__io(p),d,l)
#defineoutsw(p,d,l)__raw_writesw(__io(p),d,l)
#defineoutsl(p,d,l)__raw_writesl(__io(p),d,l)
#defineinsb(p,d,l)__raw_readsb(__io(p),d,l)
#defineinsw(p,d,l)__raw_readsw(__io(p),d,l)
#defineinsl(p,d,l)__raw_readsl(__io(p),d,l)

I/O記憶體

#include<asm-arm/io.h>

#defineioread8(p)({unsignedint__v=__raw_readb(p);__v;})
#defineioread16(p)({unsignedint__v=le16_to_cpu((__force __le16)__raw_readw(p));__v;})
#defineioread32(p)({unsignedint__v=le32_to_cpu((__force __le32)__raw_readl(p));__v;})
#defineiowrite8(v,p)__raw_writeb(v,p)
#defineiowrite16(v,p)__raw_writew((__force __u16)cpu_to_le16(v),p)
#defineiowrite32(v,p)__raw_writel((__force __u32)cpu_to_le32(v),p)
#defineioread8_rep(p,d,c)__raw_readsb(p,d,c)
#defineioread16_rep(p,d,c)__raw_readsw(p,d,c)
#defineioread32_rep(p,d,c)__raw_readsl(p,d,c)
#defineiowrite8_rep(p,s,c)__raw_writesb(p,s,c)
#defineiowrite16_rep(p,s,c)__raw_writesw(p,s,c)
#defineiowrite32_rep(p,s,c)__raw_writesl(p,s,c)

注意:

1)、所有的讀寫指令(I/O操作函式)所賦的地址必須都是虛擬地址,你有兩種選擇:使用核心已經定義好的地址,如在include/asm-arm/arch-s3c2410/regs-xxx.h中定義了s3c2410處理器各外設暫存器地址(其他處理器晶片也可在類似路徑找到核心定義好的外設暫存器的虛擬地址;另一種方法就是使用自己用ioremap對映的虛擬地址。絕對不能使用實際的實體地址,否則會因為核心無法處理地址而出現oops。

2)、在使用I/O指令時,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因為request的功能只是告訴核心埠被誰佔用了,如再次request,核心會制止(資源busy)。但是不推薦這麼做,這樣的程式碼也不規範,可能會引起併發問題(很多時候我們都需要獨佔裝置)。

3)、在使用I/O指令時,所賦的地址資料有時必須通過強制型別轉換為unsigned long,不然會有警告。

4)、在include\asm-arm\arch-s3c2410\hardware.h中定義了很多io口的操作函式,有需要可以在驅動中直接使用,很方便。

Linux系統對IO埠和IO記憶體的管理

http://blog.csdn.net/ce123/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資料結構中:

[plain]view plaincopy
  1. structresource{
  2. resource_size_tstart;//資源範圍的開始
  3. resource_size_tend;//資源範圍的結束
  4. constchar*name;//資源擁有者的名字
  5. unsignedlongflags;//各種標誌
  6. structresource*parent,*sibling,*child;//指向資源樹中父親,兄弟和孩子的指標
  7. };


所有的同種資源都插入到一個樹型資料結構(父親、兄弟和孩子)中;例如,表示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的原始碼:

[plain]view plaincopy
  1. void__iomem*ioport_map(unsignedlongport,unsignedintnr)
  2. {
  3. if(port>PIO_MASK)
  4. returnNULL;
  5. return(void__iomem*)(unsignedlong)(port+PIO_OFFSET);
  6. }
  7. voidioport_unmap(void__iomem*addr)
  8. {
  9. /*Nothingtodo*/
  10. }

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來讀寫。

[plain]view plaincopy
  1. unsignedintfastcallioread8(void__iomem*addr)
  2. {
  3. IO_COND(addr,returninb(port),returnreadb(addr));
  4. }
  5. #defineVERIFY_PIO(port)BUG_ON((port&~PIO_MASK)!=PIO_OFFSET)
  6. #defineIO_COND(addr,is_pio,is_mmio)do{\
  7. unsignedlongport=(unsignedlong__force)addr;\
  8. if(port<PIO_RESERVED){\
  9. VERIFY_PIO(port);\
  10. port&=PIO_MASK;\
  11. is_pio;\
  12. }else{\
  13. is_mmio;\
  14. }\
  15. }while(0)
  16. 展開:
  17. unsignedintfastcallioread8(void__iomem*addr)
  18. {
  19. unsignedlongport=(unsignedlong__force)addr;
  20. if(port<0x40000UL){
  21. BUG_ON((port&~PIO_MASK)!=PIO_OFFSET);
  22. port&=PIO_MASK;
  23. returninb(port);
  24. }else{
  25. returnreadb(addr);
  26. }
  27. }

七、總結

外 設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介面訪問。