在《嵌入式Linux应用开发完全手册》中把MMU放在第7章,硬件顺序上仅在 GPIO 和 存储控制器之后,可见它基本到和内存差不多了。有了MMU,CPU 和 SDRAM之间不再直接通话了,CPU的寻址改用虚拟地址VA,VA地址范围等于CPU总线宽度,4GB那是基本的,高端大气上档次。这MMU就一翻译,领导说话跑的是灰机,她传话过去用什么要看实际SDRAM寻址范围了,像mini2440这64MB就改用拖拉机了。这翻译仅仅就是为了让领导能跑灰机吗?
曾经尝试把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 等。这些技能乘在一起确实百花齐放了。
正规点说就是,
TBL 提供4096*4B 空间存一级页表项指针,一级页表最终提供 1MB 空间,可能是直接提供(段式)也可能是分成256*4KB(粗页)配合二级页表,或者分成1024*1KB(细页)配合二级页表提供。每个页表项可以指定所在 Domain和 AP/C/B 属性,详细的介绍还是看《嵌入式Linux应用开发完全手册》吧。用的时候如同老板挑人,能满足要求的最简单的那个。
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吧。