IO端口和IO內存的區別 轉

 

 

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

         每一個外設都是經過讀寫其寄存器來控制的。外設寄存器也稱爲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。app

      

1.使用I/O 端口函數

I/O 端口是驅動用來和不少設備通信的方法。

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.二、操做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.三、從用戶空間訪問I/O端口

1.2節介紹的函數主要是提供給驅動使用,但它們也可在用戶空間使用,至少在PC機上能夠。GNU C 庫在 <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.四、字串操做

除了一次傳遞一個數據的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.五、暫停式I/O操做函數

因爲處理器的速率可能與外設(尤爲是低速設備)的並不匹配,當處理器過快地傳送數據到或自總線時,這時可能就會引發問題。解決方法是:若是在I/O 指令後面緊跟着另外一個類似的I/O 指令,就必須插入一個小的延時。爲此,Linux提供了暫停式I/O操做函數,這些函數的名子只是在非暫停式I/O操做函數(前面提到的那些I/O操做函數都是非暫停式的)名後加上_p ,如inb_p、outb_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.六、平臺依賴性

因爲自身的特性,I/O指令高度依賴於處理器,很是難以隱藏各體系間的不一樣。所以,大部分的關於端口 I/O的源碼是平臺依賴的。如下是x86和ARM所使用函數的總結:

IA-32 (x86)

x86_64

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

ARM

端口映射到內存,而且支持本章介紹的全部函數;port參數的類型爲unsigned int;字串函數用C語言實現。

 

       二、使用 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.一、I/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.二、訪問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.三、像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(unsigned long port, unsigned int count); 
/* ioport_unmap用於釋放再也不須要的映射 */
void ioport_unmap(void *addr);

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


       三、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.
 */
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_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

 

 

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內存。

 

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空間」)。

二、獨立編址

        而另一些體系結構的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獨立編址,只能採用存儲器統一編址。

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暫停。

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數據結構中:

[plain]  view plain copy
 
  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文件中得到。

二、內存映射方式

         將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()申請到的)

流程以下:

6、ioremap和ioport_map

下面具體看一下ioport_map和ioport_umap的源碼:

[plain]  view plain copy
 
  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 plain copy
 
  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. }  

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

相關文章
相關標籤/搜索