CPU地址空間,IO埠和IO記憶體
1)實體地址:CPU地址匯流排傳來的地址,由硬體 電路控制其具體含義。實體地址中很大一部分是留給記憶體條中的記憶體的,但也常被對映到其他儲存器上(如視訊記憶體、BIOS等)。在程式指令中的虛擬地址經過段映 射和頁面對映後,就生成了實體地址,這個實體地址被放到CPU的地址線上。
實體地址空間,一部分給物理RAM(記憶體)用,一部分給匯流排用,這是由硬體設計來決定的,因此在32 bits地址線的x86處理器中,實體地址空間是2的32次方,即4GB,但物理RAM一般不能上到4GB,因為還有一部分要給匯流排用(總線上還掛著別的 許多裝置)。在PC機中,一般是把低端實體地址給RAM用,高階實體地址給匯流排用。
2)匯流排地址:匯流排的地址線或在地址週期上產生的訊號。外設使用的是匯流排地址,CPU使用的是實體地址。
實體地址與匯流排地址之間的關係由系統的設計決定的。在x86平臺上,實體地址就是匯流排地址,這是因為它們共享相同的地址空間——這句話有點難理解,詳見下 面的“獨立編址”。在其他平臺上,可能需要轉換/對映。比如:CPU需要訪問實體地址是0xfa000的單元,那麼在x86平臺上,會產生一個PCI匯流排 上對0xfa000地址的訪問。因為實體地址和匯流排地址相同,所以憑眼睛看是不能確定這個地址是用在哪兒的,它或者在記憶體中,或者是某個卡上的儲存單元, 甚至可能這個地址上沒有對應的儲存器。
3)虛擬地址:現代
,剩下的1G為核心空間。程式設計師只能使用虛擬地址。系統中每個程序有各自的私有使用者空間(0~3G),這個空間對系統中的其他程序是不可見的。
CPU發出取指令請求時的地址是當前上下文的虛擬地址,MMU再從頁表中找到這個虛擬地址的實體地址,完成取指。同樣讀取資料的也是虛擬地址,比如mov ax, var. 編譯時var就是一個虛擬地址,也是通過MMU從也表中來找到實體地址,再產生匯流排時序,完成取資料的。
這 裡要說的是Intel構架下的CPU地址空間佈局,注意這裡沒有說是記憶體地址空間佈局。 我們說的記憶體通常是指DRAM,DRAM相對於CPU也可以算是外部裝置,CPU地址空間是CPU訪問外部裝置過程中的一個概念,CPU除了訪問DRAM 外還會訪問許多其他的裝置。可以粗略的認為CPU地址空間包含DRAM地址空間,但兩者卻是不同的概念。而且DRAM地址空間是由記憶體控制器直接訪問的, 由CPU間接訪問的。 過去很長一段時間Intel CPU是32位的,也就是可以訪問到4GB的地址空間,但是當時的DRAM通常也就是512MB到2GB之間,現在假設DRAM是1GB,那麼就是3GB 的地址空間是空的。在計算機裡面,地址也是資源。這空的地址空間就用來訪問外部裝置IO所用,這部分被稱為MMIO(Memory Mapped I/O)
以上主要說明:假設CPU是32位(i386),其可訪問的地址範圍大小是4G地址空間,在X86平臺上,實體地址和匯流排地址共享這4G地址空間(實體地址就是匯流排地址),但物理RAM一般不能上到4GB,因為還有一部分要給匯流排用(總線上還掛著別的許多裝置),假設RAM是1G,那麼3G的地址空間就是空的,而在計算機裡面,地址也是資源,這空的地址空間就用來訪問外部裝置IO資源所用,及產生記憶體對映時就用到這段地址空間。
幾乎每一種外設都是通過讀寫裝置上的暫存器來進行的,通常包括控制暫存器、狀態暫存器和資料暫存器三大類,外設的暫存器通常被連續地編址。根據CPU體系結構的不同,CPU對IO埠的編址方式有兩種:
(1)I/O對映方式(I/O-mapped)
典型地,如X86處理器為外設專門實現了一個單獨的地址空間,稱為"I/O地址空間"或者"I/O埠空間",CPU通過專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元。
(2)記憶體對映方式(Memory-mapped)
RISC指令系統的CPU(如ARM、PowerPC等)通常只實現一個實體地址空間,外設I/O埠成為記憶體的一部分。此時,CPU可以象訪問一個記憶體單元那樣訪問外設I/O埠,而不需要設立專門的外設I/O指令。
但是,這兩者在硬體實現上的差異對於軟體來說是完全透明的,驅動程式開發人員可以將記憶體對映方式的I/O埠和外設記憶體統一看作是"I/O記憶體"資源。
一般來說,在系統執行時,外設的I/O記憶體資源的實體地址是已知的(通過request_mem_region()),由硬體的設計決定。但是CPU通常並沒有為這些已知的外設I/O記憶體資源的實體地址 預定義虛擬地址範圍,驅動程式並不能直接通過實體地址訪問I/O記憶體資源,而必須將它們對映到核心虛地址空間內(通過頁表),然後才能根據對映所得到的核
心虛地址範圍,通過訪內指令訪問這些I/O記憶體資源。Linux在io.h標頭檔案中聲明瞭函式ioremap(),(在核心驅動程式的初始化階段,通過ioremap()將實體地址對映到核心虛擬空間(3GB-4GB);在驅動程式的mmap系統呼叫中,使用remap_page_range()將該塊ROM對映到使用者虛擬空間。這樣核心空間和使用者空間都能訪問這段被對映後的虛擬地址。)
4GB以下的地址空間的佈局情況
同樣的圖中紅色字型部分為暫存器,這些暫存器與地址空間佈局有著密切的關係。這 些暫存器的詳細說明可以參考spec。比如說“Egress Port Registers”這個4KB的視窗,會根據EPBAR的設定被放置到MMIO的任意一個DMI Interface的位置,但讓該視窗不能與其他任何視窗重疊。另外我也將我所瞭解的情況說明一下。
1.先看TOLUD-4GB的位置,可以看到有幾處都是DMI Interface(Subtractive Decode)。DMI是南橋與北橋的介面,訪問DMI,也就是訪問南橋。
另外要解釋的是Substactive decode,在計算機中地址譯碼有三種形式,當主裝置通過指定地址訪問總線上的從裝置,一個是Positive decode,有從裝置解碼後發現是訪問自己的,於是它就會響應,否則就沒有從裝置響應;一個是Negative decode,從裝置收到該地址經解碼後發現不屬於自己的地址範圍,從裝置就轉發出去;一個是Subtractive decode,在4個時鐘週期內沒有從裝置響應,該地址就會發送到擴充套件的總線上面解碼。
DMI Interface(Subtractive Decode)的意思就是CPU傳送一地址先到北橋上解碼,如果該地址沒有北橋上的裝置佔用,那麼就用該地址就會被傳送到南橋上解碼,,也就是訪問南橋上 的裝置。可以假想為一開始4GB空間都是DMI Interface(Subtractive Decode),然後0-TOLUD被DRAM宣告佔用,TOLUD-4GB也紛紛被各種裝置佔用,於是就剩下了支離破碎的幾個DMI Interface。(目前看上去這樣理解是通順的,但我希望它也是正確的)。
IO Port和IO Mem的區別
在驅動程式編寫過程中,很少會注意到IO Port和IO Mem的區別。雖然使用一些不符合規範的程式碼可以達到最終目的,這是極其不推薦使用的。
結合下圖,我們徹底講述IO埠和IO記憶體以及記憶體之間的關係。主存16M位元組的SDRAM,外設是個視訊採集卡,上面有16M位元組的SDRAM作為緩衝區。
1. CPU是i386架構的情況在i386系列的處理中,記憶體和外部IO是獨立編址,也是獨立定址的。MEM的記憶體空間是32位可以定址到4G,IO空間是16位可以定址到64K。
2. 在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。
訪問這類IO Port時,我們也可以用IO Port專用定址方式。至於在對IO Port定址時,核心是具體如何完成的,這個在核心移植時就已經完成。在這種架構的處理器中,仍然保持對IO Port的支援,完全是i386架構遺留下來的問題,在此不多討論。而訪問IO Mem的方式和i386一致。
注意:linux核心給我提供了完全對IO Port和IO Mem的支援,然而具體去看看driver目錄下的驅動程式,很少按照這個規範去組織IO Port和IO Mem資源。對這二者訪問最關鍵問題就是地址的定位,在C語言中,使用volatile 就可以實現。很多的程式碼訪問IO Port中的暫存器時,就使用volatile關鍵字,雖然功能可以實現,我們還是不推薦使用。就像最簡單的延時莫過於while,可是在多工的系統中是堅決避免的!
RISC指令系統的CPU(如ARM、PowerPC等)通常只實現一個實體地址空間,外設I/O埠成為記憶體的一部分。此時,CPU可以象訪問一個記憶體單元那樣訪問外設I/O埠,而不需要設立專門的外設I/O指令。
但是,這兩者在硬體實現上的差異對於軟體來說是完全透明的,驅動程式開發人員可以將記憶體對映方式的I/O埠和外設記憶體統一看作是"I/O記憶體"資源。
一般來說,在系統執行時,外設的I/O記憶體資源的實體地址是已知的,由硬體的設計決定。但是CPU通常並沒有為這些已知的外設I/O記憶體資源的實體地址預定義虛擬地址範圍,驅動程式並不能直接通過實體地址訪問I/O記憶體資源,而必須將它們對映到核心虛地址空間內(通過頁表),然後才能根據對映所得到的核心虛地址範圍,通過訪內指令訪問這些I/O記憶體資源。Linux在io.h標頭檔案中聲明瞭函式ioremap(),用來將I/O記憶體資源的實體地址對映到核心虛地址空間(3GB-4GB)中,原型如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
iounmap函式用於取消ioremap()所做的對映,原型如下:
void iounmap(void * addr);
這兩個函式都是實現在mm/ioremap.c檔案中。
在將I/O記憶體資源的實體地址對映成核心虛地址後,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O記憶體資源了。為了保證驅動程式的跨平臺的可移植性,我們應該使用Linux中特定的函式來訪問I/O記憶體資源,而不應該通過指向核心虛地址的指標來訪問。如在x86平臺上,讀寫I/O的函式如下所示:
#define readb(addr) (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))
#define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))
#define memset_io(a,b,c) memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
最後,我們要特別強調驅動程式中mmap函式的實現方法。用mmap對映一個裝置,意味著使使用者空間的一段地址關聯到裝置記憶體上,這使得只要程式在分配的地址範圍內進行讀取或者寫入,實際上就是對裝置的訪問。
筆者在Linux原始碼中進行包含"ioremap"文字的搜尋,發現真正出現的ioremap的地方相當少。所以筆者追根索源地尋找I/O操作的實體地址轉換到虛擬地址的真實所在,發現Linux有替代ioremap的語句,但是這個轉換過程卻是不可或缺的。
CPU對外設埠實體地址的編址方式有兩種:
一種是IO對映方式,另一種是記憶體對映方式。
Linux將基於IO對映方式的和記憶體對映方式的IO埠統稱為IO區域(IO region)。
IO region仍然是一種IO資源,因此它仍然可以用resource結構型別來描述。
Linux管理IO region:
1) request_region()
把一個給定區間的IO埠分配給一個IO裝置。
2) check_region()
檢查一個給定區間的IO埠是否空閒,或者其中一些是否已經分配給某個IO裝置。
3) release_region()
釋放以前分配給一個IO裝置的給定區間的IO埠。
Linux中可以通過以下輔助函式來訪問IO埠:
inb(),inw(),inl(),outb(),outw(),outl()
“b”“w”“l”分別代表8位,16位,32位。
對IO記憶體資源的訪問
1) request_mem_region()
請求分配指定的IO記憶體資源。
2) check_mem_region()
檢查指定的IO記憶體資源是否已被佔用。
3) release_mem_region()
釋放指定的IO記憶體資源。
其中傳給函式的start address引數是記憶體區的實體地址(以上函式引數表已省略)。
驅動開發人員可以將記憶體對映方式的IO埠和外設記憶體統一看作是IO記憶體資源。
ioremap()用來將IO資源的實體地址對映到核心虛地址空間(3GB - 4GB)中,引數addr是指向核心虛地址的指標。
Linux中可以通過以下輔助函式來訪問IO記憶體資源:
readb(),readw(),readl(),writeb(),writew(),writel()。
Linux在kernel/resource.c檔案中定義了全域性變數ioport_resource和iomem_resource,來分別描述基於IO對映方式的整個IO埠空間和基於記憶體對映方式的IO記憶體資源空間(包括IO埠和外設記憶體)。
記憶體對映(IO地址和記憶體地址)
ARM體系結構下面記憶體和i/o對映區別
(1)關於IO與記憶體空間:
在X86處理器中存在著I/O空間的概念,I/O空間是相對於記憶體空間而言的,它通過特定的指令in、out來訪問。埠號標識了外設的暫存器地址。Intel語法的in、out指令格式為:
IN 累加器, {埠號│DX}
OUT {埠號│DX},累加器
目前,大多數嵌入式微控制器如ARM、PowerPC等中並不提供I/O空間,而僅存在記憶體空間。記憶體空間可以直接通過地址、指標來訪問,程式和程式執行中使用的變數和其他資料都存在於記憶體空間中。
即便是在X86處理器中,雖然提供了I/O空間,如果由我們自己設計電路板,外設仍然可以只掛接在記憶體空間。此時,CPU可以像訪問一個記憶體單元那樣訪問外設I/O埠,而不需要設立專門的I/O指令。因此,記憶體空間是必須的,而I/O空間是可選的。(2)inb和outb:
在Linux裝置驅動中,宜使用Linux核心提供的函式來訪問定位於I/O空間的埠,這些函式包括:
· 讀寫位元組埠(8位寬)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
· 讀寫字埠(16位寬)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
· 讀寫長字埠(32位寬)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
· 讀寫一串位元組
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
· insb()從埠port開始讀count個位元組埠,並將讀取結果寫入addr指向的記憶體;outsb()將addr指向的記憶體的count個位元組連續地寫入port開始的埠。
· 讀寫一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
· 讀寫一串長字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
上述各函式中I/O埠號port的型別高度依賴於具體的硬體平臺,因此,只是寫出了unsigned。(3)readb和writeb:
在裝置的實體地址被對映到虛擬地址之後,儘管可以直接通過指標訪問這些地址,但是工程師宜使用Linux核心的如下一組函式來完成裝置記憶體對映的虛擬地址的讀寫,這些函式包括:
· 讀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);
(4)把I/O埠對映到“記憶體空間”:
void *ioport_map(unsigned long port, unsigned int count);
通過這個函式,可以把port開始的count個連續的I/O埠重對映為一段“記憶體空間”。然後就可以在其返回的地址上像訪問I/O記憶體一樣訪問這些I/O埠。當不再需要這種對映時,需要呼叫下面的函式來撤消:
void ioport_unmap(void *addr);
實際上,分析ioport_map()的原始碼可發現,所謂的對映到記憶體空間行為實際上是給開發人員製造的一個“假象”,並沒有對映到核心虛擬地址,僅僅是為了讓工程師可使用統一的I/O記憶體訪問介面訪問I/O埠。
11.2.7 I/O 空間的對映
很多硬體裝置都有自己的記憶體,通常稱之為I/O空間。例如,所有比較新的圖形卡都有幾MB的RAM,稱為視訊記憶體,用它來存放要在螢幕上顯示的螢幕影像。
1.地址對映
根據裝置和匯流排型別的不同,PC體系結構中的I/O空間可以在三個不同的實體地址範圍之間進行對映:
(1)對於連線到ISA總線上的大多數裝置
I/O空間通常被對映到從0xa0000到0xfffff的實體地址範圍,這就在640K和1MB之間留出了一段空間,這就是所謂的“洞”。
(2)對於使用VESA本地匯流排(VLB)的一些老裝置
這是主要由圖形卡使用的一條專用匯流排:I/O空間被對映到從0xe00000到0xffffff的地址範圍中,也就是14MB到16MB之間。因為這些裝置使頁表的初始化更加複雜,因此已經不生產這種裝置。
(3)對於連線到PCI匯流排的裝置
I/O空間被對映到很大的實體地址區間,位於RAM實體地址的頂端。這種裝置的處理比較簡單。
2.訪問I/O空間
核心如何訪問一個I/O空間單元?讓我們從PC體系結構開始入手,這個問題很容易就可以解決,之後我們再進一步討論其他體系結構。
不要忘了核心程式作用於虛擬地址,因此I/O空間單元必須表示成大於PAGE_OFFSET的地址。在後面的討論中,我們假設PAGE_OFFSET等於0xc0000000,也就是說,核心虛擬地址是在第4G。
核心驅動程式必須把I/O空間單元的實體地址轉換成核心空間的虛擬地址。在PC體系結構中,這可以簡單地把32位的實體地址和0xc0000000常量進行或運算得到。例如,假設核心需要把實體地址為0x000b0fe4的I/O單元的值存放在t1中,把實體地址為0xfc000000的I/O單元的值存放在t2中,就可以使用下面的表示式來完成這項功能:
t1 = *((unsigned char *)(0xc00b0fe4));
t2 = *((unsigned char *)(0xfc000000));
在第六章我們已經介紹過,在初始化階段,核心已經把可用的RAM實體地址對映到虛擬地址空間第4G的最初部分。因此,分頁機制把出現在第一個語句中的虛擬地址0xc00b0fe4映射回到原來的I/O實體地址0x000b0fe4,這正好落在從640K到1MB的這段“ISA洞”中。這正是我們所期望的。 但是,對於第二個語句來說,這裡有一個問題,因為其I/O實體地址超過了系統RAM的最大實體地址。因此,虛擬地址0xfc000000就不需要與實體地址0xfc000000相對應。在這種情況下,為了在核心頁表中包括對這個I/O實體地址進行對映的虛擬地址,必須對頁表進行修改:這可以通過呼叫ioremap(
)函式來實現。ioremap( )和vmalloc( )函式類似,都呼叫get_vm_area( ) 建立一個新的vm_struct描述符,其描述的虛擬地址區間為所請求I/O空間區的大小。然後,ioremap(
)函式適當地更新所有程序的對應頁表項。
因此,第二個語句的正確形式應該為:
io_mem = ioremap(0xfb000000, 0x200000);
t2 = *((unsigned char *)(io_mem + 0x100000));
第一條語句建立一個2MB的虛擬地址區間,從0xfb000000開始;第二條語句讀取地址0xfc000000的記憶體單元。驅動程式以後要取消這種對映,就必須使用iounmap(
)函式。
現在讓我們考慮一下除PC之外的體系結構。在這種情況下,把I/O實體地址加上0xc0000000常量所得到的相應虛擬地址並不總是正確的。為了提高核心的可移植性,Linux特意包含了下面這些巨集來訪問I/O空間:
readb, readw, readl
分別從一個I/O空間單元讀取1、2或者4個位元組
writeb, writew, writel
分別向一個I/O空間單元寫入1、2或者4個位元組
memcpy_fromio, memcpy_toio
把一個數據塊從一個I/O空間單元拷貝到動態記憶體中,另一個函式正好相反,把一個數據塊從動態記憶體中拷貝到一個I/O空間單元
memset_io
用一個固定的值填充一個I/O空間區域對於0xfc000000 I/O單元的訪問推薦使用這樣的方法: io_mem = ioremap(0xfb000000, 0x200000);
t2 = readb(io_mem + 0x100000);
使用這些巨集,就可以隱藏不同平臺訪問I/O空間所用方法的差異。
從本質上來說是一樣的,IO埠在Linux驅動中是指IO埠的暫存器,通過操作暫存器來控制IO埠。而IO記憶體是指一些裝置把IO暫存器對映到某個記憶體區域,因為訪問記憶體就不要特殊的指令。