内存管理单元MMU负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。
4种映射长度:段(1MB)、大页(64KB)、小页(4KB)、极小页(1KB)。
对每个段都可以设置访问权限。
大页、小页的每个子页(sub-page,即被映射页的1/4)都可以单独设置访问权限。
没有启动MMU时,CPU核、cache、MMU、外设等所有部件使用的都是物理地址。
理论知识我就不多写了,毕竟这里是做实验而不是讲原理的,但是代码的注释会很清楚
主要的c代码:
- #include"memmap.h"
- void creat_page_table()
- {
- //我们建立段描述符 相对简单些 便于理解
- //权限设置 它们占描述符的低12bit【11-0】
- #define MMU_AP (3<<10) //ap 位111 访问权限
- #define MMU_DOMAIN (0<<5) //选择0域
- #define MMU_SPECIAL (1<<4) // 必须1 why?
- #define MMU_CACHEEN (1<<3) //cache使能
- #define MMU_BUFFEN (1<<2) //buffer 使能
- #define MMU_SECTION 2 //0b10 表示这个是段描述符
- //段描述符低12bit 没有开启cache和buff,因为它 将用于前1M的映射,不能开启cache
- #define MMU_SEC_DESC MMU_AP|MMU_DOMAIN|MMU_SPECIAL|MMU_SECTION
- //段描述符低12bit 开启cache和buff,它将用于 后面sdram的映射,开启cache buffer
- #define MMU_SEC_DESC_WB MMU_AP|MMU_DOMAIN|MMU_SPECIAL|MMU_SECTION|MMU_CACHEEN|MMU_BUFFEN
- unsigned long vir_address,phy_address;
- unsigned long *mmu_table_base=(unsigned long *)0x30000000;//地址转化为指针
- //映射前1M的物理地址到虚拟地址的前1M,为了能够在开启mmu 之后0地址的前4k内的程序依然可以执行,所以它们是一一对应的关系
- /*
- (vir_address>>20可以理解取虚拟地址高 12bit 也可以理解为虚拟地址除于1MB,就得到了段地址,所以要映射的虚拟地址必须是1M的倍数。学过汇编的话你应该很容易知道段地址是怎么一回 事,比如这里我们的是按照1M分段的,那么段1就是1M=0x100000。段大页小页极小页地址,也就是单位不一样。
- phy_address&0xfff00000也是保存高 12bit 这样做取来的结果是1M的倍数,也是段对应的物理地址,低12bit保存权限,中间的8bit保留
- */
- vir_address=0;
- phy_address=0;
- *(mmu_table_base+(vir_address>>20))=((phy_address&0xfff00000) | MMU_SEC_DESC);
- /*
- mmu_table_base+(vir_address>>20)取得段描述符的地址(指针),这4个字 节保存着一个32bit的段描述符(((phy_address&0xfff00000) | MMU_SEC_DESC)),段描述符的 【31-20】bit保存着段的物理地址(带上单位就是实实在在的地址了,比如12M,12M=0x1200000)
- MMU_SEC_DESC即是上面定义的权限了
- */
- /* 将0x56000000 之后1M映射到0xA0000000
- 外部 IO存储设备也是不能开启cache和buff的
- */
- vir_address=0xa0000000;
- phy_address=0x56000000;
- *(mmu_table_base+(vir_address>>20))=((phy_address&0xfff00000) | MMU_SEC_DESC);
- /*将0x30000000 之后64M映射到0xB0000000*/
- vir_address=0xb0000000;
- phy_address=0x30000000;
- while(phy_address<0x34000000)
- {
- *(mmu_table_base+(vir_address>>20))=((phy_address&0xfff00000) | MMU_SEC_DESC_WB);
- vir_address+=0x100000; //累加1m,到下一个段描述符
- phy_address+=0x100000;
- }
- }
- /*以上就是我们建的描述符表了,但是cpu还不知道这是怎么回事, 下面我们就需要告诉cpu这样一个虚拟地址映射物理地址的规则了,由于处理开启mmu以后的所有的虚拟地址转换*/
- void mmu_init()
- {
- unsigned long ttb=0x30000000; //描述符表table的表头地址
- __asm__(
- "mov r0,#0/n" //操作协处理器得仔细看其结构了
- "mcr p15,0,r0,c7,c7,0/n" //mcr是写 将r0 写入c7
- "mcr p15,0,r0,c7,c10,4/n"
- "mcr p15,0,r0,c8,c7,0/n"
- "mov r4,%0/n"
- "mcr p15,0,r4,c2,c0,0/n"
- "mvn r0,#0/n"
- "mcr p15,0,r0,c3,c0,0/n"
- /*对于控制寄存器 先读出控制寄存器的值 修改之 再保存进去*/
- "mrc p15,0,r0,c1,c0,0/n"
- /*先清除不需要的位 如果需要用到 之后再设置*/
- "bic r0,r0,#0x3000/n"
- "bic r0,r0,#0x0300/n"
- "bic r0,r0,#0x0087/n" //清除1 2 3 7bit
- /* 设置需要的位*/
- "orr r0,r0,#0x2/n" //开启对齐检查
- "orr r0,r0,#0x4/n" //开启dcache
- "orr r0,r0,#0x1000/n" //开启icache
- "orr r0,r0,#0x1/n" //使能mmu
- "mcr p15,0,r0,c1,c0,0/n"
- :
- :"r" (ttb)
- );
- }
对于协处理器的结构,我也很模糊,以后专门研究下
其次就是定义那些寄存器的物理地址需要修改一下,因为mmu开启之后 就只认虚拟地址了
- //led fanction
- #define GPBCON (* (volatile unsigned long *)0xa0000010) //先将地址值其转化为指针,GPBCON其实就变成了这个地址储存的值 鸟 哈哈
- #define GPBDAT (*(volatile unsigned long *)0xa0000014)
- #define GPBUP (*(volatile unsigned long *)0xa0000018)
- //key addr
- #define GPFCON (*(volatile unsigned long *)0xa0000050)
- #define GPFDAT (*(volatile unsigned long *)0xa0000054)
- #define GPFUP (*(volatile unsigned long *)0xa0000058)
然后就是start.s加的一点内容了
- @led start
- @2010-01-12
- @jay
- .globl _start
- _start:
- b reset
- @预留着以后扩展中断向量表
- reset:
- /*因为下面有c函数 要先设置个sp*/
- ldr sp,=4096
- @disable watchdog
- ldr r0,=0x53000000
- mov r1,#0
- str r1,[r0]
- @init SDRAM
- bl memsetup
- @cope the code to sdram
- bl cp_to_SDRAM
- @setup pagetable
- bl creat_page_table
- @init and enable mmu
- bl mmu_init
- @reset stack 开启了mmu,我们要重设下堆栈指针,指向虚拟地址映射的顶部
- ldr sp,=0xb4000000
- @跳到虚拟地址
- /*这2句指令是从uboot学来的 注意这一点的理解,_led_on是个标签,标签的实质就是个地址,这个标签存储 的内容就是另外一个标签led_test(是不是很熟悉的感觉?对了,就是c语言的指针的指针),其实就是将_led_on的内容led_text赋给了 pc*/
- ldr pc,_led_on
- _led_on:.word led_test
- load_sd:
- ldr sp,=0x34000000
- bl led_test
- @copy to sdram
- cp_to_SDRAM:
- @ adr r0,_start
- mov r0,#0
- add r1,r0,#4096
- ldr r2,=0x30004000 @注意这里不要指定到0x30000000了
- lp:
- ldmia r0!,{r3-r11}
- stmia r2!,{r3-r11}
- cmp r0,r1
- ble lp
- mov pc,lr
最后就是连接脚本了
- OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
- OUTPUT_ARCH(arm)
- ENTRY(_start)
- SECTIONS
- {
- . =0x30004000;
- . =ALIGN(4);
- .text :
- {
- start.o (.text)
- low_init.o (.text)
- *(.text)
- }
- . = ALIGN(4);
- .data :{ *(.data) }
- . =ALIGN(4);
- .rodata : { *(.rodata) }
- . =ALIGN(4);
- __bss_start = .;
- .bss :{*(.bss)}
- _end = . ;
- }
这个也是从uboot学来的
ALIGN(4)是4字节对齐,32bitcpu一次处理32bit的嘛
.是指当前地址 这里指定的知识编译地址,跟运行地址会不一样的。
两个.指令要用;隔开的,注意. =0x30004000 这里有空格的啊 否则会出错
makefile
- CC=arm-linux-gcc
- LD=arm-linux-ld
- CP=arm-linux-objcopy
- DP=arm-linux-objdump
- objs:=start.o low_init.o memmap.o led.o
- mmu.bin:$(objs)
- $(LD) -Tboot.lds -g -o mmu_elf $^
- $(CP) -O binary -S mmu_elf $@
- $(DP) -D -m arm mmu_elf > mmu.asm
- #makefile 注意 .s不可以大写 -T不可以少-
- %.o:%.c
- $(CC) -g -Wall -c -o $@ $<
- %.o:%.s
- $(CC) -g -Wall -c -o $@ S<
- clean:
- rm -f *.asm *.bin *_elf *.o
内容虽然不多,却花费了我很多时间去理解,其他的代码与之前的一样 没有改动
终于是可以成功开启mmu了