IO端口和IO內存的區(qū)別及分別使用的函數接口每個外設都是通過讀寫其寄存器來控制的。外設寄存器也稱為I/O端口,通常包括:控制寄存器、狀態(tài)寄存器和數據寄存器三大類。根據訪問外設寄存器的不同方式,可以把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)。
本文引用地址:http://www.ex-cimer.com/article/201611/320405.htm結合下圖,我們徹底講述IO端口和IO內存以及內存之間的關系。主存16M字節(jié)的SDRAM,外設是個視頻采集卡,上面有16M字節(jié)的SDRAM作為緩沖區(qū)。
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 /* 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位端口區(qū)分開,無法像訪問內存那樣混淆使用。驅動程序必須調用不同的函數來訪問不同大小的端口。
如同前面所講的,僅支持單地址空間的計算機體系通過將I/O端口地址重新映射到內存地址來偽裝端口I/O。為了提高移植性,內核對驅動隱藏了這些細節(jié)。Linux內核頭文件(體系依賴的頭文件)定義了下列內聯函數來存取I/O端口:
/*inb/outb:讀/寫字節(jié)端口(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節(jié)介紹的函數主要是提供給驅動使用,但它們也可在用戶空間使用,至少在PC機上可以。GNUC庫在中定義它們。如果在用戶空間使用這些函數,必須滿足下列條件:
1)、程序必須使用-O選項編譯來強制擴展內聯函數
2)、必須使用ioperm和iopl系統調用(#include )來獲得進行操作I/O端口的權限。ioperm為獲取單個端口的操作許可,iopl為獲取整個I/O空間許可。這2個函數都是x86特有的
3)、程序必須以root來調用ioperm或者iopl,或者其父進程(祖先)必須以root獲得的端口操作權限
如果平臺不支持ioperm和iopl系統調用,通過使用/dev/prot設備文件,用戶空間仍然可以存取I/O端口。但是要注意的是,這個文件的定義也是依賴平臺的。
1.4、字串操作
除了一次傳遞一個數據的I/O操作,某些處理器實現了一次傳遞一序列數據(單位可以是字節(jié)、字和雙字)的特殊指令。這些所謂的字串指令,它們完成任務比一個C語言循環(huán)更快。下列宏定義實現字串操作,在某些體系上,它們通過使用單個機器指令實現;但如果目標處理器沒有進行字串I/O指令,則通過執(zhí)行一個緊湊的循環(huán)實現。
字串函數的原型是:
/* insb:從I/O端口port讀取count個數據(單位字節(jié))到以內存地址addr為開始的內存空間*/ voidinsb(unsignedport,void*addr,unsignedlongcount); /* outsb:將內存地址addr開始的count個數據(單位字節(jié))寫到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); |
注意:使用字串函數時,它們直接將字節(jié)流從端口中讀取或寫入。當端口和主機系統有不同的字節(jié)序時,會導致不可預期的結果。使用inw讀取端口應在必要時自行轉換字節(jié)序,以匹配主機字節(jié)序。
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內存,因為寄存器和內存之間的區(qū)別對軟件是透明的。
I/O內存僅僅是一個類似于RAM的區(qū)域,處理器通過總線訪問該區(qū)域,以實現對設備的訪問。同樣,讀寫這個區(qū)域是有邊際效應。
根據計算機體系和總線不同,I/O內存可分為可以或者不可以通過頁表來存取。若通過頁表存取,內核必須先重新編排物理地址,使其對驅動程序可見,這就意味著在進行任何I/O操作之前,你必須調用ioremap;如果不需要頁表,I/O內存區(qū)域就類似于I/O端口,你可以直接使用適當的I/O函數讀寫它們。
由于邊際效應的緣故,不管是否需要ioremap,都不鼓勵直接使用I/O內存指針,而應使用專門的I/O內存操作函數。這些I/O內存操作函數不僅在所有平臺上是安全,而且對直接使用指針操作I/O內存的情況進行了優(yōu)化。
2.1、I/O內存分配和映射
I/O內存區(qū)在使用前必須先分配。分配內存區(qū)的函數接口在定義中:
/* request_mem_region分配一個開始于start,len字節(jié)的I/O內存區(qū)。分配成功,返回一個非NULL指針;否則返回NULL。系統當前所有I/O內存分配信息都在/proc/iomem文件中列出,你分配失敗時,可以看看該文件,看誰先占用了該內存區(qū) */ structresource*request_mem_region(unsignedlongstart,unsignedlonglen,char*name); /* release_mem_region用于釋放不再需要的I/O內存區(qū)*/ voidrelease_mem_region(unsignedlongstart,unsignedlonglen); /* check_mem_region用于檢查I/O內存區(qū)的可用性。同樣,該函數不安全,不推薦使用 */ intcheck_mem_region(unsignedlongstart,unsignedlonglen); |
在訪問I/O內存之前,分配I/O內存并不是唯一要求的步驟,你還必須保證內核可存取該I/O內存。訪問I/O內存并不只是簡單解引用指針,在許多體系中,I/O內存無法以這種方式直接存取。因此,還必須通過ioremap函數設置一個映射。
#include /*ioremap用于將I/O內存區(qū)映射到虛擬地址。參數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 /*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表示要讀寫的數據個數,而不是字節(jié)大小 */ 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 內存的區(qū)別*/ 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方面的內核源碼,你就會發(fā)現:其實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 /* * 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 /* __force表示所定義的變量類型是可以做強制類型轉換的 */ #define__force __attribute__((force)) /* __iomem是用來修飾一個變量的,這個變量必須是非解引用(no dereference)的,即這個變量地址必須是有效的,而且變量所在的地址空間必須是2,即設備地址映射空間。0表示normal space,即普通地址空間,對內核代碼來說,當然就是內核空間地址了。1表示用戶地址空間,2表示是設備地址映射空間 */ #define__iomem __attribute__((noderef,address_space(2))) |
I/O端口
#include #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 #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)。但是不推薦這么做,這樣的代碼也不規(guī)范,可能會引起并發(fā)問題(很多時候我們都需要獨占設備)。
3)、在使用I/O指令時,所賦的地址數據有時必須通過強制類型轉換為unsigned long,不然會有警告。
4)、在includeasm-armarch-s3c2410hardware.h中定義了很多io口的操作函數,有需要可以在驅動中直接使用,很方便。
評論