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吧。