IO端口和IO內(nèi)存的區(qū)別及分別使用的函數(shù)接口
每個(gè)外設(shè)都是通過讀寫其寄存器來控制的。外設(shè)寄存器也稱為I/O端口,通常包括:控制寄存器、狀態(tài)寄存器和數(shù)據(jù)寄存器三大類。根據(jù)訪問外設(shè)寄存器的不同方式,可以把CPU分成兩大類。一類CPU(如M68K,Power PC等)把這些寄存器看作內(nèi)存的一部分,寄存器參與內(nèi)存統(tǒng)一編址,訪問寄存器就通過訪問一般的內(nèi)存指令進(jìn)行,所以,這種CPU沒有專門用于設(shè)備I/O的指令。這就是所謂的“I/O內(nèi)存”方式。另一類CPU(典型的如X86),將外設(shè)的寄存器看成一個(gè)獨(dú)立的地址空間,所以訪問內(nèi)存的指令不能用來訪問這些寄存器,而要為對(duì)外設(shè)寄存器的讀/寫設(shè)置專用指令,如IN和OUT指令。這就是所謂的“I/O端口”方式。但是,用于I/O指令的“地址空間”相對(duì)來說是很小的,如x86 CPU的I/O空間就只有64KB(0-0xffff)。
本文引用地址:http://m.butianyuan.cn/article/201611/320405.htm結(jié)合下圖,我們徹底講述IO端口和IO內(nèi)存以及內(nèi)存之間的關(guān)系。主存16M字節(jié)的SDRAM,外設(shè)是個(gè)視頻采集卡,上面有16M字節(jié)的SDRAM作為緩沖區(qū)。
1.CPU是i386架構(gòu)的情況
在i386系列的處理中,內(nèi)存和外部IO是獨(dú)立編址,也是獨(dú)立尋址的。MEM的內(nèi)存空間是32位可以尋址到4G,IO空間是16位可以尋址到64K。
在Linux內(nèi)核中,訪問外設(shè)上的IO Port必須通過IO Port的尋址方式。而訪問IO Mem就比較羅嗦,外部MEM不能和主存一樣訪問,雖然大小上不相上下,可是外部MEM是沒有在系統(tǒng)中注冊(cè)的。訪問外部IO MEM必須通過remap映射到內(nèi)核的MEM空間后才能訪問。為了達(dá)到接口的同一性,內(nèi)核提供了IO Port到IO Mem的映射函數(shù)。映射后IO Port就可以看作是IO Mem,按照IO Mem的訪問方式即可。
3. CPU是ARM或PPC架構(gòu)的情況
在這一類的嵌入式處理器中,IO Port的尋址方式是采用內(nèi)存映射,也就是IO bus就是Mem bus。系統(tǒng)的尋址能力如果是32位,IO Port+Mem(包括IO Mem)可以達(dá)到4G。
1.使用I/O端口
I/O端口是驅(qū)動(dòng)用來和很多設(shè)備通訊的方法。
1.1、分配I/O端口
在驅(qū)動(dòng)還沒獨(dú)占設(shè)備之前,不應(yīng)對(duì)端口進(jìn)行操作。內(nèi)核提供了一個(gè)注冊(cè)接口,以允許驅(qū)動(dòng)聲明其需要的端口:
#include |
1.2、操作I/O端口
在驅(qū)動(dòng)成功請(qǐng)求到I/O端口后,就可以讀寫這些端口了。大部分硬件會(huì)將8位、16位和32位端口區(qū)分開,無法像訪問內(nèi)存那樣混淆使用。驅(qū)動(dòng)程序必須調(diào)用不同的函數(shù)來訪問不同大小的端口。
如同前面所講的,僅支持單地址空間的計(jì)算機(jī)體系通過將I/O端口地址重新映射到內(nèi)存地址來偽裝端口I/O。為了提高移植性,內(nèi)核對(duì)驅(qū)動(dòng)隱藏了這些細(xì)節(jié)。Linux內(nèi)核頭文件(體系依賴的頭文件)定義了下列內(nèi)聯(lián)函數(shù)來存取I/O端口:
/*inb/outb:讀/寫字節(jié)端口(8位寬)。有些體系將port參數(shù)定義為unsigned long;而有些平臺(tái)則將它定義為unsigned short。inb的返回類型也是依賴體系的 */ |
從現(xiàn)在開始,當(dāng)我們使用unsigned沒有進(jìn)一步指定類型時(shí),表示是一個(gè)依賴體系的定義。
注意,沒有64位的I/O端口操作函數(shù)。即便在64位體系中,端口地址空間使用一個(gè)32位(最大)的數(shù)據(jù)通路。
1.3、從用戶空間訪問I/O端口
1.2節(jié)介紹的函數(shù)主要是提供給驅(qū)動(dòng)使用,但它們也可在用戶空間使用,至少在PC機(jī)上可以。GNUC庫(kù)在
1)、程序必須使用-O選項(xiàng)編譯來強(qiáng)制擴(kuò)展內(nèi)聯(lián)函數(shù)
2)、必須使用ioperm和iopl系統(tǒng)調(diào)用(#include
3)、程序必須以root來調(diào)用ioperm或者iopl,或者其父進(jìn)程(祖先)必須以root獲得的端口操作權(quán)限
如果平臺(tái)不支持ioperm和iopl系統(tǒng)調(diào)用,通過使用/dev/prot設(shè)備文件,用戶空間仍然可以存取I/O端口。但是要注意的是,這個(gè)文件的定義也是依賴平臺(tái)的。
1.4、字串操作
除了一次傳遞一個(gè)數(shù)據(jù)的I/O操作,某些處理器實(shí)現(xiàn)了一次傳遞一序列數(shù)據(jù)(單位可以是字節(jié)、字和雙字)的特殊指令。這些所謂的字串指令,它們完成任務(wù)比一個(gè)C語言循環(huán)更快。下列宏定義實(shí)現(xiàn)字串操作,在某些體系上,它們通過使用單個(gè)機(jī)器指令實(shí)現(xiàn);但如果目標(biāo)處理器沒有進(jìn)行字串I/O指令,則通過執(zhí)行一個(gè)緊湊的循環(huán)實(shí)現(xiàn)。
字串函數(shù)的原型是:
/* insb:從I/O端口port讀取count個(gè)數(shù)據(jù)(單位字節(jié))到以內(nèi)存地址addr為開始的內(nèi)存空間*/ |
注意:使用字串函數(shù)時(shí),它們直接將字節(jié)流從端口中讀取或?qū)懭?。?dāng)端口和主機(jī)系統(tǒng)有不同的字節(jié)序時(shí),會(huì)導(dǎo)致不可預(yù)期的結(jié)果。使用inw讀取端口應(yīng)在必要時(shí)自行轉(zhuǎn)換字節(jié)序,以匹配主機(jī)字節(jié)序。
1.5、暫停式I/O操作函數(shù)
由于處理器的速率可能與外設(shè)(尤其是低速設(shè)備)的并不匹配,當(dāng)處理器過快地傳送數(shù)據(jù)到或自總線時(shí),這時(shí)可能就會(huì)引起問題。解決方法是:如果在I/O指令后面緊跟著另一個(gè)相似的I/O指令,就必須插入一個(gè)小的延時(shí)。為此,Linux提供了暫停式I/O操作函數(shù),這些函數(shù)的名子只是在非暫停式I/O操作函數(shù)(前面提到的那些I/O操作函數(shù)都是非暫停式的)名后加上_p,如inb_p、outb_p等。大部分體系都支持這些函數(shù),盡管它們常常被擴(kuò)展為與非暫停I/O同樣的代碼,因?yàn)槿绻w系使用一個(gè)合理的現(xiàn)代外設(shè)總線,沒有必要額外暫停。
以下是ARM體系暫停式I/O宏的定義:
#defineoutb_p(val,port)outb((val),(port)) |
因?yàn)锳RM使用內(nèi)部總線,就沒有必要額外暫停,所以暫停式的I/O函數(shù)被擴(kuò)展為與非暫停式I/O同樣的代碼。
1.6、平臺(tái)依賴性
由于自身的特性,I/O指令高度依賴于處理器,非常難以隱藏各體系間的不同。因此,大部分的關(guān)于端口I/O的源碼是平臺(tái)依賴的。以下是x86和ARM所使用函數(shù)的總結(jié):
IA-32(x86)
x86_64
這個(gè)體系支持本章介紹的所有函數(shù);port參數(shù)的類型為unsignedshort。
ARM
端口映射到內(nèi)存,并且支持本章介紹的所有函數(shù);port參數(shù)的類型為unsignedint;字串函數(shù)用C語言實(shí)現(xiàn)。
2、使用I/O內(nèi)存
盡管I/O端口在x86世界中非常流行,但是用來和設(shè)備通訊的主要機(jī)制是通過內(nèi)存映射的寄存器和設(shè)備內(nèi)存,兩者都稱為I/O內(nèi)存,因?yàn)榧拇嫫骱蛢?nèi)存之間的區(qū)別對(duì)軟件是透明的。
I/O內(nèi)存僅僅是一個(gè)類似于RAM的區(qū)域,處理器通過總線訪問該區(qū)域,以實(shí)現(xiàn)對(duì)設(shè)備的訪問。同樣,讀寫這個(gè)區(qū)域是有邊際效應(yīng)。
根據(jù)計(jì)算機(jī)體系和總線不同,I/O內(nèi)存可分為可以或者不可以通過頁表來存取。若通過頁表存取,內(nèi)核必須先重新編排物理地址,使其對(duì)驅(qū)動(dòng)程序可見,這就意味著在進(jìn)行任何I/O操作之前,你必須調(diào)用ioremap;如果不需要頁表,I/O內(nèi)存區(qū)域就類似于I/O端口,你可以直接使用適當(dāng)?shù)腎/O函數(shù)讀寫它們。
由于邊際效應(yīng)的緣故,不管是否需要ioremap,都不鼓勵(lì)直接使用I/O內(nèi)存指針,而應(yīng)使用專門的I/O內(nèi)存操作函數(shù)。這些I/O內(nèi)存操作函數(shù)不僅在所有平臺(tái)上是安全,而且對(duì)直接使用指針操作I/O內(nèi)存的情況進(jìn)行了優(yōu)化。
2.1、I/O內(nèi)存分配和映射
I/O內(nèi)存區(qū)在使用前必須先分配。分配內(nèi)存區(qū)的函數(shù)接口在
/* request_mem_region分配一個(gè)開始于start,len字節(jié)的I/O內(nèi)存區(qū)。分配成功,返回一個(gè)非NULL指針;否則返回NULL。系統(tǒng)當(dāng)前所有I/O內(nèi)存分配信息都在/proc/iomem文件中列出,你分配失敗時(shí),可以看看該文件,看誰先占用了該內(nèi)存區(qū) */ |
在訪問I/O內(nèi)存之前,分配I/O內(nèi)存并不是唯一要求的步驟,你還必須保證內(nèi)核可存取該I/O內(nèi)存。訪問I/O內(nèi)存并不只是簡(jiǎn)單解引用指針,在許多體系中,I/O內(nèi)存無法以這種方式直接存取。因此,還必須通過ioremap函數(shù)設(shè)置一個(gè)映射。
#include /* ioremap_nocache為ioremap的無緩存版本。實(shí)際上,在大部分體系中,ioremap與ioremap_nocache的實(shí)現(xiàn)一樣的,因?yàn)樗?I/O 內(nèi)存都是在無緩存的內(nèi)存地址空間中 */ |
經(jīng)過ioremap(和iounmap)之后,設(shè)備驅(qū)動(dòng)就可以存取任何I/O內(nèi)存地址。注意,ioremap返回的地址不可以直接解引用;相反,應(yīng)當(dāng)使用內(nèi)核提供的訪問函數(shù)。
2.2、訪問I/O內(nèi)存
訪問I/O內(nèi)存的正確方式是通過一系列專門用于實(shí)現(xiàn)此目的的函數(shù):
#include |
2.3、像I/O內(nèi)存一樣使用端口
一些硬件有一個(gè)有趣的特性:有些版本使用I/O端口;而有些版本則使用I/O內(nèi)存。不管是I/O端口還是I/O內(nèi)存,處理器見到的設(shè)備寄存器都是相同的,只是訪問方法不同。為了統(tǒng)一編程接口,使驅(qū)動(dòng)程序易于編寫,2.6內(nèi)核提供了一個(gè)ioport_map函數(shù):
/*ioport_map重新映射count個(gè)I/O端口,使它們看起來I/O內(nèi)存。此后,驅(qū)動(dòng)程序可以在ioport_map返回的地址上使用ioread8和同類函數(shù)。這樣,就可以在編程時(shí),消除了I/O端口和I/O 內(nèi)存的區(qū)別*/ |
注意,I/O端口在重新映射前必須使用request_region分配所需的I/O端口。
3、ARM體系的I/O操作接口
s3c24x0處理器使用的是I/O內(nèi)存,也就是說:s3c24x0處理器使用統(tǒng)一編址方式,I/O寄存器和內(nèi)存使用的是單一地址空間,并且讀寫I/O寄存器和讀寫內(nèi)存的指令是相同的。所以推薦使用I/O內(nèi)存的相關(guān)指令和函數(shù)。但這并不表示I/O端口的指令在s3c24x0中不可用。如果你注意過s3c24x0關(guān)于I/O方面的內(nèi)核源碼,你就會(huì)發(fā)現(xiàn):其實(shí)I/O端口的指令只是一個(gè)外殼,內(nèi)部還是使用和I/O內(nèi)存一樣的代碼。
下面是ARM體系原始的I/O操作函數(shù)。其實(shí)后面I/O端口和I/O內(nèi)存操作函數(shù),只是對(duì)這些函數(shù)進(jìn)行再封裝。從這里也可以看出為什么我們不推薦直接使用I/O端口和I/O內(nèi)存地址指針,而是要求使用專門的I/O操作函數(shù)——專門的I/O操作函數(shù)會(huì)檢查地址指針是否有效是否為IO地址(通過__iomem或__chk_io_ptr)
#include /* |
關(guān)于__force和__iomem
#include /* __force表示所定義的變量類型是可以做強(qiáng)制類型轉(zhuǎn)換的 */ |
I/O端口
#include #defineoutb(v,p)__raw_writeb(v,__io(p)) |
I/O內(nèi)存
#include #defineioread8(p)({unsignedint__v=__raw_readb(p);__v;}) |
注意:
1)、所有的讀寫指令(I/O操作函數(shù))所賦的地址必須都是虛擬地址,你有兩種選擇:使用內(nèi)核已經(jīng)定義好的地址,如在include/asm-arm/arch-s3c2410/regs-xxx.h中定義了s3c2410處理器各外設(shè)寄存器地址(其他處理器芯片也可在類似路徑找到內(nèi)核定義好的外設(shè)寄存器的虛擬地址;另一種方法就是使用自己用ioremap映射的虛擬地址。絕對(duì)不能使用實(shí)際的物理地址,否則會(huì)因?yàn)閮?nèi)核無法處理地址而出現(xiàn)oops。
2)、在使用I/O指令時(shí),可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因?yàn)閞equest的功能只是告訴內(nèi)核端口被誰占用了,如再次request,內(nèi)核會(huì)制止(資源busy)。但是不推薦這么做,這樣的代碼也不規(guī)范,可能會(huì)引起并發(fā)問題(很多時(shí)候我們都需要獨(dú)占設(shè)備)。
3)、在使用I/O指令時(shí),所賦的地址數(shù)據(jù)有時(shí)必須通過強(qiáng)制類型轉(zhuǎn)換為unsigned long,不然會(huì)有警告。
4)、在includeasm-armarch-s3c2410hardware.h中定義了很多io口的操作函數(shù),有需要可以在驅(qū)動(dòng)中直接使用,很方便。
評(píng)論