在《嵌入式Linux應用開發徹底手冊》中把MMU放在第7章,硬件順序上僅在 GPIO 和 存儲控制器以後,可見它基本到和內存差很少了。有了MMU,CPU 和 SDRAM之間再也不直接通話了,CPU的尋址改用虛擬地址VA,VA地址範圍等於CPU總線寬度,4GB那是基本的,高端大氣上檔次。這MMU就一翻譯,領導說話跑的是灰機,她傳話過去用什麼要看實際SDRAM尋址範圍了,像mini2440這64MB就改用拖拉機了。這翻譯僅僅就是爲了讓領導能跑灰機嗎?linux
曾經嘗試把MMU地址變換過程描述的更簡潔一些,想一想要作flash/gif算了,仍是參考韋東山先生的圖文吧。大致就是,
1. 把翻譯們放到 TBL 處;
2. 領導說出VA給某個翻譯;
3. 該翻譯盡職盡責的把 (M)VA翻譯成 PA 到總線去,若是領導那句沒說好還要先告訴領導一聲,顧全大局嘛;
上面的1. 是由軟件作的,把TBL放到 ram 的某處"隨意",之後別被沖掉就行。對 mini2440 這 TBL = 4096 * 4B = 16KB,即共4096個翻譯,每一個翻譯有個4B長的名字,顯然片內 ram是不夠用的,只能在配置了 sdram 後放到其中。爲何名字要是4B長呢,其實對 cpu 來講這名字也就是指針,32位的cpu一個指針固然是4B也就是一個 int* 的大小了。
上面的2. 是硬件完成的,VA 會先簡單的轉換爲MVA,用於尋找翻譯,尋找的過程很簡單至關於在一個 int* 數組中取個值。拿這這個值(或者叫名字)找到翻譯,而後把MVA告訴翻譯,下來領導就不用管啥了。
上面的3. 是硬件完成的,翻譯各顯其能喊個 PA 給總線聽。說他們「各顯其能」不光由於有的細緻有的粗獷,還要看本身門派(Domain)限制,讀、寫模式,是否使用 Cache/Buffer 等。這些技能乘在一塊兒確實百花齊放了。c++
正規點說就是,
TBL 提供4096*4B 空間存一級頁表項指針,一級頁表最終提供 1MB 空間,多是直接提供(段式)也多是分紅256*4KB(粗頁)配合二級頁表,或者分紅1024*1KB(細頁)配合二級頁表提供。每一個頁表項能夠指定所在 Domain和 AP/C/B 屬性,詳細的介紹仍是看《嵌入式Linux應用開發徹底手冊》吧。用的時候如同老闆挑人,能知足要求的最簡單的那個。shell
MMU 和 cache 不是綁定關係,但一般相互影響,提到 Cache 又有了 TLB 的概念,依次:
Cache 用來把 PA 的左鄰右舍存起來,TLB 只是把當前用到的頁表項存起來。既然是緩存,就有同步問題,同步策略又按讀、寫分出不一樣模式。若是用到 DMA 這種不經緩存的訪問內存模式,則要保證讀mem前已同步,寫mem後需同步,也就是在DMA設備發起讀的前一條指令把 Cache 同步到 sdram 去,在DMA設備寫完sdram的第一條指令把Cache無效掉以保證Cache重取sdram。
數組
下面是代碼及測試過程,按編譯順序,首先看的是 Makefile:緩存
all: clean mmu.elf mmu.elf : arm-linux-gcc -g -c -O2 -o head.o head.s arm-linux-g++ -g -c -O2 -o init.o init.cpp arm-linux-g++ -g -c -O0 -o main.o main.cpp arm-linux-ld -Tmmu.lds -o mmu.elf head.o init.o main.o arm-linux-objcopy -O binary -S mmu.elf mmu.bin arm-linux-objdump -D -m arm mmu.elf > mmu.dis clean: rm -f *.o *.elf *.dis *.bin
其中涉及3個文件,1個是彙編,2個是cpp。注意編譯 main.cpp 時使用 -O0 而非日常用的 -O2,緣由後面說。下來head.s:框架
.text .global _start _start: mov sp, #0x00001000 bl kill_dog bl control_mem bl copy2sdram bl start_mmu mov sp, #0xC4000000 ldr r4, =main mov lr, pc bx r4 _end: b _end
一個簡單的入口函數,設置棧指針以便能調用 c/c++ 中的函數,在start_mmu 後把棧頂設在了0xC4000000可見此時MMU已經在工做了,由於存儲控制器尋址只有1GB,即PA不可能大於0x40000000,因此sp 中只能是 VA。下來是 init.c:函數
extern "C" void kill_dog( void ) { unsigned long* pWatchDog = reinterpret_cast<unsigned long*>(0x53000000); *pWatchDog = 0; } extern "C" void control_mem( void ) { unsigned long* pMemControlBase = reinterpret_cast<unsigned long*>(0x48000000); unsigned long aulRegisters[] = { 0x22111112, 0x00000700, 0x00000700, 0x00000700, 0x00000700, 0x00000700, 0x00000700, 0x00018009, 0x00018009, 0x008e04eb, 0x000000b2, 0x00000030, 0x00000030, 0x00000000, 0x00000000 }; for ( int i = 0; i < sizeof(aulRegisters)/sizeof(aulRegisters[0]); i++ ) pMemControlBase[i] = aulRegisters[i]; } extern "C" void copy2sdram( void ) { unsigned long* pSdram = reinterpret_cast<unsigned long*>(0x30010000); unsigned long* pAppCode = reinterpret_cast<unsigned long*>(0x00000800); unsigned long* pAppEnd = reinterpret_cast<unsigned long*>(0x00001000); while ( pAppCode != pAppEnd ) { *pSdram = *pAppCode; pSdram ++; pAppCode ++; } } extern "C" void start_mmu( void ) { #define MMU_FULL_ACCESS (3 << 10) /* 訪問權限 */ #define MMU_DOMAIN (0 << 5) /* 屬於哪一個域 */ #define MMU_SPECIAL (1 << 4) /* 必須是1 */ #define MMU_CACHEABLE (1 << 3) /* cacheable */ #define MMU_BUFFERABLE (1 << 2) /* bufferable */ #define MMU_SECTION (2) /* 表示這是段描述符 */ #define MMU_SECDESC (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \ MMU_SECTION) #define MMU_SECDESC_WB (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \ MMU_CACHEABLE | MMU_BUFFERABLE | MMU_SECTION) #define MMU_SECTION_SIZE 0x00100000 unsigned long virtuladdr, physicaladdr; unsigned long *mmu_tbl_base = (unsigned long *)0x30000000; /* 片內ram PA=VA */ virtuladdr = 0; physicaladdr = 0; *(mmu_tbl_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \ MMU_SECDESC_WB; /* GPIO 地址不變 */ virtuladdr = 0xA0000000; physicaladdr = 0x56000000; *(mmu_tbl_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \ MMU_SECDESC; /* sdram PA=VA-0x90000000 */ virtuladdr = 0xC0000000; physicaladdr = 0x30000000; while (virtuladdr < 0xC4000000) { *(mmu_tbl_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \ MMU_SECDESC_WB; virtuladdr += MMU_SECTION_SIZE; physicaladdr += MMU_SECTION_SIZE; } __asm__( "mov r0, #0\n" "mcr p15, 0, r0, c7, c7, 0\n" /* 使無效ICaches和DCaches */ "mcr p15, 0, r0, c7, c10, 4\n" /* drain write buffer on v4 */ "mcr p15, 0, r0, c8, c7, 0\n" /* 使無效指令、數據TLB */ "mov r4, %0\n" /* r4 = 頁表基址 */ "mcr p15, 0, r4, c2, c0, 0\n" /* 設置頁表基址寄存器 */ "mvn r0, #0\n" "mcr p15, 0, r0, c3, c0, 0\n" /* 域訪問控制寄存器設爲0xFFFFFFFF, * 不進行權限檢查 */ "mrc p15, 0, r0, c1, c0, 0\n" /* 讀出控制寄存器的值 */ "bic r0, r0, #0x3000\n" /* ..11 .... .... .... 清除V、I位 */ "bic r0, r0, #0x0300\n" /* .... ..11 .... .... 清除R、S位 */ "bic r0, r0, #0x0087\n" /* .... .... 1... .111 清除B/C/A/M */ "orr r0, r0, #0x0002\n" /* .... .... .... ..1. 開啓對齊檢查 */ "orr r0, r0, #0x0004\n" /* .... .... .... .1.. 開啓DCaches */ "orr r0, r0, #0x1000\n" /* ...1 .... .... .... 開啓ICaches */ "orr r0, r0, #0x0001\n" /* .... .... .... ...1 使能MMU */ "mcr p15, 0, r0, c1, c0, 0\n" /* 將修改的值寫入控制寄存器 */ : /* 無輸出 */ : "r" (mmu_tbl_base) ); }
用 c++ 編譯出的函數會被編譯器重命名,且格式不一。想讓head.s 能調用這些函數,或者readelf -s看看編譯器給起了什麼名字再改到head.s中,或者簡單粗暴的用 extern "C" 前綴一下。copy2sdram 中把片內ram的後2KB拷貝到sdram的0x30001000,這是把前64KB留給TBL用。start_mmu 是把《嵌入式Linux應用開發徹底手冊》中的 create_page_table和mmu_init簡單合併修改了一下,詳細解說仍是書上更清楚。根據上面介紹的段映射規則,可見當cpu訪問VA爲0xC0000000開始的64MB上的數據時會被翻譯到PA=0x30000000上去;想訪問GPIO的0x56000000所在的那1MB空間,cpu要說0xA0000000。開始以爲有點脫褲子放屁,但這亮點就在褲子上:除了以前說的能夠給每一個映射設置不一樣屬性外,沒有TBL表項映射的VA還將被視做異常,試試把GPIO的映射注掉,看看LED還會亮嗎。下來是main.cpp:測試
#define GPBCON (*(volatile unsigned long *)0xA0000010) // 物理地址0x56000010 #define GPBDAT (*(volatile unsigned long *)0xA0000014) // 物理地址0x56000014 #define GPB5_out (1<<(5*2)) #define GPB6_out (1<<(6*2)) #define GPB7_out (1<<(7*2)) #define GPB8_out (1<<(8*2)) static inline void wait(unsigned long dly) { for(; dly > 0; dly--); } int main(void) { unsigned long i = 0; GPBCON = GPB5_out|GPB6_out|GPB7_out|GPB8_out; while(1){ wait(30000); GPBDAT = (~(i<<5)); // 根據i的值,點亮LED1-4 if(++i == 16) i = 0; } return 0; }
只有同樣可說的:wait函數體其實啥都沒幹,-O2會把它優化掉,也就說wait(30000)編譯出來會消失掉。優化
最後是 mmu.lds 了:翻譯
ENTRY(_start) SECTIONS { . = 0x00000000; loader : AT(0) { head.o } .loader.extab ALIGN(4) : { init.o (.ARM.extab) } .loader.exidx ALIGN(4) : { init.o (.ARM.exidx) } init : { init.o } . = 0xC0010000; .ARM.extab ALIGN(4) : AT(2048) { main.o(.ARM.extab) } .ARM.exidx ALIGN(4) : AT(2048) { main.o(.ARM.exidx) } runner ALIGN(4) : AT(2064) { main.o } }
同一個c文件用g++編譯會多獲得一些段例如.ARM.extab和.ARM.exidx,並且它們不能被合到一個段中,ordered和unordered互斥?具體請高手講解。總之要把它們分出來。我從nor啓動,片內ram在0x00000000處,並且知道經mmu後cpu要訪問內存只能用0xC0000000之上的VA,故把main.cpp裏的內容放在 0xC0010000(別侵佔了TBL)。在bin文件裏則是把main的內容放在2048日後的地方,並且要保證最終文件小於4KB。那個2064是經過readelf -S main.o:
[10] .ARM.extab PROGBITS 00000000 00021d 000000 00 A 0 0 1 [11] .ARM.exidx ARM_EXIDX 00000000 000220 000010 00 AL 1 0 4
獲得.ARM.extabl 尺寸爲0,.ARM.exidx 尺寸爲0x10,在根據 2048算出來的。現實中這些能夠自動的,寫個makefile調用readelf 再用sed改改框架文件就能夠了。至於2048,也就是上面說的「片內ram的後2KB」,這個要根據前面段的長度實際選取,不能重疊最好也不要浪費。總之,4K片內逐漸成爲限制因素了,早點啓用 nand flash吧。