1. 程式人生 > >I/O埠與I/O記憶體 對外設訪問方式

I/O埠與I/O記憶體 對外設訪問方式

從CPU連出來一把線:資料匯流排、地址匯流排、控制匯流排,這把線上掛著N個介面,有相同的,有不同的,名字叫做儲存器介面、中斷控制介面、DMA介面、並行介面、序列介面、AD介面……一個裝置要想接入,就用自己的介面和總線上的某個匹配介面對接……於是總線上出現了各種裝置:記憶體、硬碟,滑鼠、鍵盤,顯示器……

        對於CPU而言,如果它要發資料到某個裝置,其實是發到對應的介面,介面電路里有多個暫存器(也稱為埠),訪問裝置實際上是訪問相關的埠,所有的資訊會由介面轉給它的裝置。那麼CPU會準備資料到資料匯流排,但是諸多介面,該發給誰呢?這時就須要為各介面分配一個地址,然後把地址放在地址總線上,需要的控制資訊放到控制總線上,就可以和裝置通訊了。

        對一個系統而言,通常會有多個外設,每個外設的介面電路中,又會有多個埠,每個埠都需要一個地址,為他們標識一個具體的地址值,是系統必須解決的事,與此同時,你還有個記憶體條,可能是512M或1G或更大的金士頓、現代DDR2之類,他們的每一個地址也都需要分配一個標識值,另外,很多外設也有自己的記憶體、緩衝區,就像你的記憶體條一樣,你同樣需要為它們分配記憶體……你的CPU可能需要和它們的每一個位元組都打交道,所以:別指望偷懶,它們的每一寸土地都要規劃好!這聽起來就很煩,做起來可能就直接導致腦細胞全部陣亡。但事情總是得有人去做,ARM可能會這樣做:他這次設計的CPU是32位的,最多也就能定址2^32=4G空間,於是把這4GB空間丟給記憶體和埠,讓他們瓜分。但英特爾或許有更好的分配方式…

(一)地址的概念

1)實體地址:CPU地址匯流排傳來的地址,由硬體電路控制其具體含義。實體地址中很大一部分是留給記憶體條中的記憶體的,但也常被對映到其他儲存器上(如視訊記憶體、BIOS等)。在程式指令中的虛擬地址經過段對映和頁面對映後,就生成了實體地址,這個實體地址被放到CPU的地址線上

        實體地址空間,一部分給物理RAM(記憶體)用,一部分給匯流排用,這是由硬體設計來決定的,因此在32 bits地址線的x86處理器中,實體地址空間是2的32次方,即4GB,但物理RAM一般不能上到4GB,因為還有一部分要給匯流排用(總線上還掛著別的許多裝置)。在PC機中,一般是把低端實體地址給RAM用,高階實體地址給匯流排用。

2)匯流排地址:匯流排的地址線或在地址週期上產生的訊號。外設使用的是匯流排地址,CPU使用的是實體地址。

        實體地址與匯流排地址之間的關係由系統的設計決定的。在x86平臺上,實體地址就是匯流排地址,這是因為它們共享相同的地址空間——這句話有點難理解,詳見下面的“獨立編址”。在其他平臺上,可能需要轉換/對映。比如:CPU需要訪問實體地址是0xfa000的單元,那麼在x86平臺上,會產生一個PCI總線上對0xfa000地址的訪問。因為實體地址和匯流排地址相同,所以憑眼睛看是不能確定這個地址是用在哪兒的,它或者在記憶體中,或者是某個卡上的儲存單元,甚至可能這個地址上沒有對應的儲存器。

3)虛擬地址:現代作業系統普遍採用虛擬記憶體管理(Virtual Memory Management)機制,這需要MMU(Memory Management Unit)的支援。MMU通常是CPU的一部分(MMU也是硬體級的),如果處理器沒有MMU,或者有MMU但沒有啟用,CPU執行單元發出的記憶體地址將直接傳到晶片引腳上,被記憶體晶片(實體記憶體)接收,這稱為實體地址(Physical Address),如果處理器啟用了MMU,CPU執行單元發出的記憶體地址將被MMU截獲,從CPU到MMU的地址稱為虛擬地址Virtual Address),而MMU將這個地址翻譯成另一個地址發到CPU晶片的外部地址引腳上,也就是將虛擬地址對映成實體地址

        Linux中,程序的4GB(虛擬地址空間)分為使用者空間、核心空間(每個程序都有4GB虛擬地址空間)使用者空間分佈為0~3GB(即PAGE_OFFSET,在0X86中它等於0xC0000000)

剩下的1G為核心空間。程式設計師只能使用虛擬地址。系統中每個程序有各自的私有使用者空間(0~3G),這個空間對系統中的其他程序是不可見的。

        CPU發出取指令請求時的地址是當前上下文的虛擬地址,MMU再從頁表中找到這個虛擬地址的實體地址,完成取指。同樣讀取資料的也是虛擬地址,比如mov ax, var. 編譯時var就是一個虛擬地址,也是通過MMU從也表中來找到實體地址,再產生匯流排時序,完成取資料的。

(二)編址方式

1)對外設的操作全都是通過讀寫裝置上的暫存器來進行的外設暫存器也稱為“I/O埠”,而IO埠有兩種編址方式:獨立編址和統一編制。

        統一編址:外設介面中的IO暫存器(即IO埠)與主存單元一樣看待,每個端口占用一個儲存單元的地址,將主存的一部分劃出來用作IO地址空間,如,在PDP-11中,把最高的4K主存作為IO裝置暫存器地址。I/O端口占用了儲存器的地址空間,使儲存量容量減小(ARM體系結構就是用這種統一編址

        統一編址也稱為“I/O記憶體”方式,外設暫存器位於“記憶體空間”(很多外設有自己的記憶體、緩衝區,外設的暫存器和記憶體統稱“I/O空間”)。

        如,Samsung的S3C2440,是32位ARM處理器,它的4GB地址空間被外設、RAM等瓜分

0x8000 1000    LED 8*8點陣的地址

0x4800 0000 ~ 0x6000 0000  SFR(特殊暫存器)地址空間

0x3800 1002   鍵盤地址

0x3000 0000 ~ 0x3400 0000  SDRAM空間 

0x2000 0020 ~ 0x2000 002e  IDE

0x1900 0300   CS8900

        獨立編址(單獨編址):IO地址與儲存地址分開獨立編址,I/0埠地址不佔用儲存空間的地址範圍這樣,在系統中就存在了另一種與儲存地址無關的IO地址,CPU也必須具有專用與輸入輸出操作的IO指令(IN、OUT等)和控制邏輯。獨立編址下,地址總線上過來一個地址,裝置不知道是給IO埠的、還是給儲存器的,於是處理器通過MEMR/MEMW和IOR/IOW兩組控制訊號來實現對I/O埠和儲存器的不同定址。如,intel 80x86就採用單獨編址,CPU記憶體和I/O是一起編址的,就是說記憶體一部分的地址和I/O地址是重疊的。

        獨立編址也稱為“I/O埠”方式,外設暫存器位於“I/O(地址)空間”。只有獨立編址才有I/O空間的概念

        對於x86架構來說,通過IN/OUT指令訪問(單獨編址)PC架構一共有65536個8bit的I/O埠,組成64K個I/O地址空間,編號從0~0xFFFF,有16位,80x86用低16位地址線A0-A15來定址。連續兩個8bit的埠可以組成一個16bit的埠,連續4個組成一個32bit的埠。I/O地址空間和CPU的實體地址空間是兩個不同的概念,例如I/O地址空間為64K,一個32bit的CPU實體地址空間是4G。如,在Intel 8086+Redhat9.0 下用“more /proc/ioports”可看到:

0000-001f : dma1

0020-003f : pic1

0040-005f : timer

0060-006f : keyboard

0070-007f : rtc

0080-008f : dma page reg

00a0-00bf : pic2

00c0-00df : dma2

00f0-00ff : fpu

0170-0177 : ide1

……

        不過Intel x86平臺普通使用了名為記憶體對映(MMIO)的技術,該技術是PCI規範的一部分,IO裝置埠被對映到記憶體空間,對映後,CPU訪問IO埠就如同訪問記憶體一樣。看Intel TA 719文件給出的x86/x64系統典型記憶體地址分配表:

系統資源  佔用

------------------------------------------------------------------------

BIOS  1M

本地APIC  4K

晶片組保留 2M

IO APIC  4K

PCI裝置  256M

PCI Express裝置 256M

PCI裝置(可選) 256M

顯示幀快取 16M

TSEG  1M

        對於某一既定的系統,它要麼是獨立編址、要麼是統一編址,具體採用哪一種則取決於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()。

2)對外設的訪問

1、訪問I/O記憶體的流程是:request_mem_region() -> ioremap() -> ioread8()/iowrite8() -> iounmap() -> release_mem_region() 。

        前面說過,IO記憶體是統一編址下的概念,對於統一編址,IO地址空間是物理主存的一部分,對於程式設計而言,我們只能操作虛擬記憶體,所以,訪問的第一步就是要把裝置所處的實體地址對映到虛擬地址,Linux2.6下用ioremap():

        void *ioremap(unsigned long offset, unsigned long size);

然後,我們可以直接通過指標來訪問這些地址,但是也可以用Linux核心的一組函式來讀寫:

ioread8(), iowrite16(), ioread8_rep(), iowrite8_rep()......

2、訪問I/O埠

        訪問IO埠有2種途徑I/O對映方式(I/O-mapped)、記憶體對映方式(Memory-mapped)。前一種途徑不對映到記憶體空間,直接使用intb()/outb()之類的函式來讀寫IO埠後一種MMIO是先把IO埠對映到IO記憶體(“記憶體空間”),再使用訪問IO記憶體的函式來訪問IO埠。

        void ioport_map(unsigned long port, unsigned int count);

通過這個函式,可以把port開始的count個連續的IO埠對映為一段“記憶體空間”,然後就可以在其返回的地址是像訪問IO記憶體一樣訪問這些IO埠。

二、linux I/O埠與I/O記憶體

IO埠:當一個暫存器或者記憶體位於IO空間時;
IO記憶體:當一個記憶體或者暫存器位於記憶體空間時;

  在一些CPU製造商在其晶片上實現了一個單地址空間(統一編址)的同時,其它的CPU製造商認為外設不同於記憶體,應該有一個獨立的地址空間給外設(單獨編址),其生產處理器(特別是x86家族)的I/O埠有自己的讀寫訊號線和特殊的CPU指令來存取埠。因為外設要與外設匯流排相匹配,並且大部分流行的I/O匯流排都是以個人計算機(主要是x86家族)作為模型,所以即便那些沒有單獨地址空間給I/O埠的處理器,也必須在訪問外設時模擬成讀寫埠。這通常通過外部晶片組(PC中的南北橋)或者在CPU核中附加額外電路來實現(基於嵌入式應用的處理器)。
  由於同樣的理由,Linux在所有計算機平臺上都實現了I/O埠,甚至在那些單地址空間的CPU平臺上(模擬I/O埠)。但並不是所有的裝置都會將其暫存器對映到I/O埠。雖然ISA裝置普遍使用I/O埠,但大部分PCI裝置將暫存器對映到某個記憶體地址區。這種I/O記憶體方法通常是首選的,因為它無需使用特殊的處理器指令,CPU存取記憶體也更有效率,並且編譯器在存取記憶體時在暫存器分配和定址模式的選擇上有更多自由。

1.IO暫存器和常規記憶體
  I/O暫存器和RAM的主要不同是I/O操作有邊際效應(side effect),而記憶體操作沒有:訪問記憶體只是在記憶體某一位置儲存數值。因為記憶體存取速度嚴重影響CPU的效能,編譯器可能會對原始碼進行優化,主要是:使用快取記憶體和重排讀/寫指令的順序。對於傳統記憶體(至少在單處理器系統)這些優化是透明有益的,但是對於I/O 暫存器,這可能是致命錯誤,因為它們干擾了那些"邊際效應"(驅動程式存取I/O 暫存器就是為了獲取邊際效應)。因此,驅動程式必須確保在存取暫存器時,不能使用快取記憶體並且不能重新編排讀寫指令的順序。
  side effect 是指:訪問I/O暫存器時,不僅僅會像訪問普通記憶體一樣影響儲存單元的值,更重要的是它可能改變CPU的I/O埠電平、輸出時序或CPU對I/O埠電平的反應等等,從而實現CPU的控制功能。CPU在電路中的意義就是實現其side effect 。舉個例子,有些裝置的中斷狀態暫存器只要一讀取,便自動清零。

  硬體緩衝的問題是最易解決的:只要將底層硬體配置(或者自動地或者通過Linux 初始化程式碼)為當存取I/O區時,禁止任何硬體緩衝(不管是I/O 記憶體還是I/O 埠)。

  編譯器優化和硬體重編排讀寫指令順序的解決方法是:在硬體或處理器必須以一個特定順序執行的操作之間安放一個記憶體屏障(memory barrier)。

2.操作IO埠(申請,訪問,釋放)
  I/O 埠是驅動用來和很多裝置通訊的方法。
(1)申請I/O 埠
  在驅動還沒獨佔裝置之前,不應對埠進行操作。核心提供了一個註冊介面,以允許驅動宣告其需要的埠:

/* request_region告訴核心:要使用first開始的n個埠。引數name為裝置名。如果分配成功返回值是非NULL;否則無法使用需要的埠(/proc/ioports包含了系統當前所有埠的分配資訊,若request_region分配失敗時,可以檢視該檔案,看誰先用了你要的埠) */
struct resource *request_region(unsigned long first, unsigned long n, const char *name);

(2)訪問IO埠:

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

(3)釋放IO埠:

/* 用完I/O埠後(可能在模組解除安裝時),應當呼叫release_region將I/O埠返還給系統。引數start和n應與之前傳遞給request_region一致 */
void release_region(unsigned long start, unsigned long n);

3.操作IO記憶體(申請,對映,訪問,釋放):
  儘管 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 記憶體的情況進行了優化。

(1)申請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);

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

/* ioremap用於將I/O記憶體區對映到虛擬地址。引數phys_addr為要對映的I/O記憶體起始地址,引數size為要對映的I/O記憶體的大小,返回值為被對映到的虛擬地址 */
void *ioremap(unsigned long phys_addr, unsigned long size);

(3)訪問IO記憶體:
  經過 ioremap之後,裝置驅動就可以存取任何I/O記憶體地址。注意,ioremap返回的地址不可以直接解引用;相反,應當使用核心提供的訪問函式。訪問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);
複製程式碼

(4)釋放IO記憶體步驟:

void iounmap(void * addr); /* iounmap用於釋放不再需要的對映 */
void release_mem_region(unsigned long start, unsigned long len); /* iounmap用於釋放不再需要的對映 */

4、像IO記憶體一樣使用埠

  一些硬體有一個有趣的特性: 有些版本使用 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);

void ioport_unmap(void *addr);/* ioport_unmap用於釋放不再需要的對映 */

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

5、ARM體系的IO操作介面
  s3c24x0處理器使用的是I/O記憶體,也就是說:s3c24x0處理器使用統一編址方式,I/O暫存器和記憶體使用的是單一地址空間,並且讀寫I/O暫存器和讀寫記憶體的指令是相同的。所以推薦使用I/O記憶體的相關指令和函式。但這並不表示I/O埠的指令在s3c24x0中不可用。如果你注意過s3c24x0關於I/O方面的核心原始碼,你就會發現:其實I/O埠的指令只是一個外殼,內部還是使用和I/O記憶體一樣的程式碼。注意以下幾點:
  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口的操作函式,有需要可以在驅動中直接使用,很方便。

轉載地址:http://www.cnblogs.com/geneil/archive/2011/12/08/2281367.html