mini2440 貌似複雜的mmu

在《嵌入式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吧。

相關文章
相關標籤/搜索