IO端口和IO內存的區別及分別使用的函數接口 .

IO端口和IO內存的區別及分別使用的函數接口 

         每個外設都是通過讀寫其寄存器來控制的。外設寄存器也稱爲I/O端口,通常包括:控制寄存器、狀態寄存器和數據寄存器三大類。根據訪問外設寄存器的不同方式,可以把CPU分成兩大類。一類CPU(如M68KPower PC等)把這些寄存器看作內存的一部分,寄存器參與內存統一編址訪問寄存器就通過訪問一般的內存指令進行,所以,這種CPU沒有專門用於設備I/O的指令。這就是所謂的I/O內存方式。另一類CPU(典型的如X86),將外設的寄存器看成一個獨立的地址空間,所以訪問內存的指令不能用來訪問這些寄存器,而要爲對外設寄存器的讀/寫設置專用指令,如INOUT指令。這就是所謂的 I/O端口方式。但是,用於I/O指令的地址空間相對來說是很小的,如x86 CPUI/O空間就只有64KB00xffff)

        結合下圖,我們徹底講述IO端口和IO內存以及內存之間的關係。主存16M字節的SDRAM,外設是個視頻採集卡,上面有16M字節的SDRAM作爲緩衝區。


1.   CPUi386架構的情況

 i386系列的處理中,內存和外部IO是獨立編址,也是獨立尋址的。MEM的內存空間是32位可以尋址到4GIO空間是16位可以尋址到64K

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

3.    CPUARMPPC架構的情況

在這一類的嵌入式處理器中,IO Port的尋址方式是採用內存映射,也就是IO bus就是Mem bus。系統的尋址能力如果是32位,IO PortMem(包括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分配失敗時,可以查看該文件,看誰先用了你要的端口) */
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
/* 用完I/O端口後(可能在模塊卸載時),應當調用release_region將I/O端口返還給系統。參數start和n應與之前傳遞給request_region一致 */
void release_region(unsigned long start, unsigned long n); 
/* check_region用於檢查一個給定的I/O端口集是否可用。如果給定的端口不可用,check_region返回一個錯誤碼。不推薦使用該函數,因爲即便它返回0(端口可用),它也不能保證後面的端口分配操作會成功,因爲檢查和後面的端口分配並不是一個原子操作。而request_region通過加鎖來保證操作的原子性,因此是安全的 */
int check_region(unsigned long first, unsigned long n);


     
 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的返回類型也是依賴體系的 */
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
/* inw/outw:讀/寫字端口(16位寬) */
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
/* inl/outl:讀/32位端口。longword也是依賴體系的,有的體系爲unsigned long;而有的爲unsigned int */
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);

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

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

1.3、從用戶空間訪問I/O端口

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

1)、程序必須使用-O選項編譯來強制擴展內聯函數

2)、必須使用iopermiopl系統調用(#include <sys/perm.h>) 來獲得進行操作I/O端口的權限。ioperm 爲獲取單個端口的操作許可,iopl 爲獲取整個I/O空間許可。這2個函數都是x86特有的

3)、程序必須以root來調用ioperm或者iopl,或者其父進程(祖先)必須以root獲得的端口操作權限

如果平臺不支持iopermiopl系統調用,通過使用/dev/prot設備文件,用戶空間仍然可以存取I/O 端口。但是要注意的是,這個文件的定義也是依賴平臺的。

1.4、字串操作

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

字串函數的原型是:

/* insb:從I/O端口port讀取count個數據(單位字節)到以內存地址addr爲開始的內存空間 */
void insb(unsigned port, void *addr, unsigned long count);
/* outsb:將內存地址addr開始的count個數據(單位字節)寫到I/O端口port */
void outsb(unsigned port, void *addr, unsigned long count);
/* insw:從I/O端口port讀取count個數據(單位字)到以內存地址addr爲開始的內存空間 */
void insw(unsigned port, void *addr, unsigned long count);
/* outsw:將內存地址addr開始的count個數據(單位字)寫到I/O端口port */
void outsw(unsigned port, void *addr, unsigned long count);
/* insl:從I/O端口port讀取count個數據(單位雙字)到以內存地址addr爲開始的內存空間 */
void insl(unsigned port, void *addr, unsigned long count);
/* outsl:將內存地址addr開始的count個數據(單位雙字)寫到I/O端口port */
void outsl(unsigned port, void *addr, unsigned long count);

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

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

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

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

#define outb_p(val,port)    outb((val),(port))
#define outw_p(val,port)    outw((val),(port))
#define outl_p(val,port)    outl((val),(port))
#define inb_p(port)        inb((port))
#define inw_p(port)        inw((port))
#define inl_p(port)        inl((port))
#define outsb_p(port,from,len)    outsb(port,from,len)
#define outsw_p(port,from,len)    outsw(port,from,len)
#define outsl_p(port,from,len)    outsl(port,from,len)
#define insb_p(port,to,len)    insb(port,to,len)
#define insw_p(port,to,len)    insw(port,to,len)
#define insl_p(port,to,len)    insl(port,to,len)

因爲ARM使用內部總線,就沒有必要額外暫停,所以暫停式的I/O函數被擴展爲與非暫停式I/O同樣的代碼。

1.6、平臺依賴性

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

IA-32 (x86)

x86_64

這個體系支持本章介紹的所有函數;port參數的類型爲unsigned short

ARM

端口映射到內存,並且支持本章介紹的所有函數;port參數的類型爲unsigned int;字串函數用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.1I/O 內存分配和映射

I/O 內存區在使用前必須先分配。分配內存區的函數接口在<linux/ioport.h>定義中:

/* request_mem_region分配一個開始於start,len字節的I/O內存區。分配成功,返回一個非NULL指針;否則返回NULL。系統當前所有I/O內存分配信息都在/proc/iomem文件中列出,你分配失敗時,可以看看該文件,看誰先佔用了該內存區 */
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
/* release_mem_region用於釋放不再需要的I/O內存區 */
void release_mem_region(unsigned long start, unsigned long len); 
/* check_mem_region用於檢查I/O內存區的可用性。同樣,該函數不安全,不推薦使用 */
int check_mem_region(unsigned long start, unsigned long len);

在訪問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(unsigned long phys_addr, unsigned long size);

/* ioremap_nocache爲ioremap的無緩存版本。實際上,在大部分體系中,ioremap與ioremap_nocache的實現一樣的,因爲所有 I/O 內存都是在無緩存的內存地址空間中 */
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
/* iounmap用於釋放不再需要的映射 */
void iounmap(void * addr);

經過 ioremap (iounmap)之後,設備驅動就可以存取任何I/O內存地址。注意,ioremap返回的地址不可以直接解引用;相反,應當使用內核提供的訪問函數。

2.2、訪問I/O內存

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

#include <asm/io.h>
/* I/O內存讀函數。參數addr應當是從ioremap獲得的地址(可能包含一個整型偏移); 返回值是從給定I/O內存讀取到的值 */
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
/* I/O內存寫函數。參數addr同I/O內存讀函數,參數value爲要寫的值 */
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
/* 以下這些函數讀和寫一系列值到一個給定的 I/O 內存地址,從給定的buf讀或寫count個值到給定的addr。參數count表示要讀寫的數據個數,而不是字節大小 */
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr,,onst void *buf,,nsigned long count);
/* 需要操作一塊I/O 地址時,使用下列函數(這些函數的行爲類似於它們的C庫類似函數): */
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);
/* 舊的I/O內存讀寫函數,不推薦使用 */
unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 

2.3、像I/O 內存一樣使用端口

一些硬件有一個有趣的特性有些版本使用 I/O 端口;而有些版本則使用 I/O 內存。不管是I/O 端口還是I/O 內存,處理器見到的設備寄存器都是相同的,只是訪問方法不同。爲了統一編程接口,使驅動程序易於編寫,2.6內核提供了一個ioport_map函數:

/* ioport_map重新映射countI/O端口,使它們看起來I/O內存。此後,驅動程序可以在ioport_map返回的地址上使用ioread8和同類函數。這樣,就可以在編程時,消除了I/O端口和I/O 內存的區別 */
void *ioport_map(unsigned long port, unsigned int count); 
/* ioport_unmap用於釋放不再需要的映射 */
void ioport_unmap(void *addr);

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


       3ARM體系的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.
 */
extern void __raw_writesb(void __iomem *addr, const void *data, int bytelen);
extern void __raw_writesw(void __iomem *addr, const void *data, int wordlen);
extern void __raw_writesl(void __iomem *addr, const void *data, int longlen);
extern void __raw_readsb(const void __iomem *addr, void *data, int bytelen);
extern void __raw_readsw(const void __iomem *addr, void *data, int wordlen);
extern void __raw_readsl(const void __iomem *addr, void *data, int longlen);
#define __raw_writeb(v,a)    (__chk_io_ptr(a), *(volatile unsigned char __force *)(a) =(v))
#define __raw_writew(v,a)    (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) =(v))
#define __raw_writel(v,a)    (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) =(v))
#define __raw_readb(a)        (__chk_io_ptr(a), *(volatile unsigned char __force *)(a))
#define __raw_readw(a)        (__chk_io_ptr(a), *(volatile unsigned short __force *)(a))
#define __raw_readl(a)        (__chk_io_ptr(a), *(volatile unsigned int __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>

#define outb(v,p)        __raw_writeb(v,__io(p))
#define outw(v,p)        __raw_writew((__force __u16) \
                    cpu_to_le16(v),__io(p))
#define outl(v,p)        __raw_writel((__force __u32) \
                    cpu_to_le32(v),__io(p))
#define inb(p)    ({ __u8 __v = __raw_readb(__io(p)); __v; })
#define inw(p)    ({ __u16 __v = le16_to_cpu((__force __le16) \
            __raw_readw(__io(p))); __v; })
#define inl(p)    ({ __u32 __v = le32_to_cpu((__force __le32) \
            __raw_readl(__io(p))); __v; })
#define outsb(p,d,l)        __raw_writesb(__io(p),d,l)
#define outsw(p,d,l)        __raw_writesw(__io(p),d,l)
#define outsl(p,d,l)        __raw_writesl(__io(p),d,l)
#define insb(p,d,l)        __raw_readsb(__io(p),d,l)
#define insw(p,d,l)        __raw_readsw(__io(p),d,l)
#define insl(p,d,l)        __raw_readsl(__io(p),d,l)

I/O內存

#include <asm-arm/io.h>

#define ioread8(p)    ({ unsigned int __v = __raw_readb(p); __v; })
#define ioread16(p)    ({ unsigned int __v = le16_to_cpu((__force __le16)__raw_readw(p));__v; })
#define ioread32(p)    ({ unsigned int __v = le32_to_cpu((__force __le32)__raw_readl(p));__v; })
#define iowrite8(v,p)    __raw_writeb(v, p)
#define iowrite16(v,p)    __raw_writew((__force __u16)cpu_to_le16(v), p)
#define iowrite32(v,p)    __raw_writel((__force __u32)cpu_to_le32(v), p)
#define ioread8_rep(p,d,c)    __raw_readsb(p,d,c)
#define ioread16_rep(p,d,c)    __raw_readsw(p,d,c)
#define ioread32_rep(p,d,c)    __raw_readsl(p,d,c)
#define iowrite8_rep(p,s,c)    __raw_writesb(p,s,c)
#define iowrite16_rep(p,s,c)    __raw_writesw(p,s,c)
#define iowrite32_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_regionrequest_mem_region,而直接使用outbioread等指令。因爲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. struct resource {  
  2.          resource_size_t start;// 資源範圍的開始  
  3.          resource_size_t end;// 資源範圍的結束  
  4.          const char *name; //資源擁有者的名字  
  5.          unsigned long flags;// 各種標誌  
  6.          struct resource *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(unsigned long port, unsigned int nr)  
  2. {  
  3.     if (port > PIO_MASK)  
  4.         return NULL;  
  5.     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);  
  6. }  
  7.   
  8. void ioport_unmap(void __iomem *addr)  
  9. {  
  10.     /* Nothing to do */  
  11. }  

          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. unsigned int fastcall ioread8(void __iomem *addr)  
  2. {  
  3.     IO_COND(addr, return inb(port), return readb(addr));  
  4. }  
  5.   
  6. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  
  7. #define IO_COND(addr, is_pio, is_mmio) do { \  
  8.     unsigned long port = (unsigned long __force)addr; \  
  9.         if (port < PIO_RESERVED) { \  
  10.             VERIFY_PIO(port); \  
  11.             port &= PIO_MASK; \  
  12.             is_pio; \  
  13.         } else { \  
  14.             is_mmio; \  
  15.         } \  
  16. } while (0)  
  17.   
  18. 展開:  
  19. unsigned int fastcall ioread8(void __iomem *addr)  
  20. {  
  21.     unsigned long port = (unsigned long __force)addr;  
  22.     if( port < 0x40000UL ) {  
  23.         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );  
  24.         port &= PIO_MASK;  
  25.         return inb(port);  
  26.     }else{  
  27.         return readb(addr);  
  28.     }  
  29. }  

七、總結

         外設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接口訪問。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章