arm9之MMU
一、MMU地址转换:
1.首先弄清除为什么要使用MMU纳?MMU即内存管理单元,直白一点的讲,就像食堂的餐具,所有的学生一起吃饭时不够用,但食堂又不想再出资购买新的餐具(原因很明显:一方面要成本,另一方面又占地方。这就像增加内存一样),那么有没有解决办法?根据以往经验得知不可能全学校的学习一起都到食堂吃饭,于是食堂就找几个人负责餐具的管理(相当于MMU),他们一方面发放餐具,保证来的同学有餐具可用,另一方面又回收用完的餐具(这就相当于虚拟地址到物理地址之间建立了一个映射一样,内存还是那么多,但从任意单个程序角度都好像用不完一样)。当然如果有同学一个人拿好几套餐具肯定不允许的(这就相当于内存的权限检查)。MMU在地址转换过程中涉及到三种地址:(VA---Virtual Address,虚拟地址)---这个就相当于餐具存放的地方(大家都可以领到餐具)。CPU核心看到和用到的只是虚拟地址VA,至于VA如果去对应物理地址PA,CPU核心不理会,大家也不会去关心总共有多少餐具吧;(MVA---Modified Virtual Address,变换后的虚拟地址)---这个相当于放假的时候,人很少,只发餐具好了,用过的就不先回收了,节省人员了。Caches和MMU看不到VA,他们利用MVA转换得到PA,放假了回收餐具的人也不需要一直寻找用完的餐具;(PA---Physical Address,物理地址)---实际的餐具量,就那些。实际设备看不到VA、MVA,读写它们使用的是物理地址PA,同学们就餐一般会领到餐具。
2.虚拟地址到物理地址的转换过程。ARM使用页表来进行转换,S3C2410最多会用到两级页表,以段(Section,1M)的方式进行转换时只用到一级页表,以页(Page)的方式进行转换时用到两级页表。页的大小有3种:大页(64KB)、小页(4KB)和极小页(1KB)。本文只是以段地址转换过程为例来讲解一下,页的转换大同小异。
★首先有个页表基址寄存器(位置为协处理器CP15的寄存器C2),它里面写入的就是一级页表的地址,通过读取它就可以找到一级页表存放的起始位置。一级页表的地址是16K对齐(所以[13:0]为0,使用[31:14]存储页表基址)。一级页表使用4096个描述符来表示4GB空间,所以每个描述符对应1MB的虚拟地址,存储它对应的1MB物理空间的起始地址,或者存储下一级页表的地址。使用MVA[31:20]来索引一级页表(31-20一共12位,2^12=4096,所以是4096个描述符),得到一个描述符,每个描述符占4个字节。
★描述符最后两位为0B10时,即是段的方式映射。[31:20]为段基址,此描述符低20位填充0后就是一块1MB物理地址空间的起始地址。MVA[19:0]用来在这1MB空间中寻址。描述符的位[31:20]和MVA[19:0]构成了这个虚拟地址MVA对应的物理地址。以段的方式进行映射时,虚拟地址MVA到物理地址PA的转换过程如下:①页表基址寄存器位[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到段描述符;②取出段描述符的位[31:20](段基址),它和MVA[19:0]组成一个32位的物理地址(这就是MVA对应的PA)。
二. 关于地址
要知道虚拟内存机制必须了解ARM9中的3种地址:VA(虚地址),MVA(修正后虚地址),PA(物理地址)
1)VA,是程序中的逻辑地址,0x00000000~0xFFFFFFFF。
2)MVA,由于多个进程执行,逻辑地址会重合。所以,跟据进程号将逻辑地址分布到整个内存中。MVA = (PID << 25) | VA
3)PA,MVA通过MMU转换后的地址。
由2可知,地址位共32位,PID占7位,所以最多只能有 128 个进程。而每个进程可访问的地址位为25位,故只能分到 32MB 的地址空间。(注:不是物理内存空间)
PID是存放在CP15协处理器的C13寄存器的高7位。
2. 关于虚拟内存转换
CP15从C2中获得页基址(TTB)。将 MVA 的高12位作为页表索引值。获得页表项:TTB [ MVA >> 20 ]。注意:页表项是32位的。
从上可知,一个页表最多有4096个页表项,也就是4K。那么,每个页表项可以表示1MB的地址空间。
得来的项表项分三种:
1)段页描述符,直接指向1MB的内存空间。
2)粗页描述符,有256个二级页表项,每个二级页表项指向4KB的内存空间。
3)细页描述符,有1024个二级页表项,每个二级页表项指向1KB的内存空间。
## 粗页描述符中存放的是粗页表二级表的基址。 将MVA的[19~12]位用来进行二级页表查寻。粗页表二级表分两种:
1)大页描述符,一个描述符可以对应64KB的内存地址,但16个二级描述符对应同一块内存。
2)小页描述符,一个描述符只对应4KB的内存地址,每个二级描述符只对应一块内存。
## 细页描述符中存放的是细页表二级表的基址。将MVA的[19~10],共计10位用于进行二级页表索引。二级页表共1024个描述符。剩下的10位作为基址,可访问空间为1024B。
如下是内存转换图:
思考:
通过上面的学习,了解到ARM将4GB的地址访问空间分成128个32MB,每份供一个进程使用。如此以来,一个进程的地址访问空间只有32MB。如进程1的地址空间为[0x02000000~0x03FFFFFF]。如果超出这个范围,地址访问就是非法的。
那ARM9在设计CP15时为什么不为每一个进程指定一个单独的页表。这样以来,每个进程就可以独地拥有4GB的地址空间。
如此以来,进程数就不再受限于128个,可以多达1024个进程。而每一个进程的虚拟地址的空间可以扩展到4GB。
三、内存的访问权限检查
内存的访问权限检查决定一块内存是否允许读/写。这由CP15寄存器C3(域访问控制)、描述符的域(Domain)、CP15寄存器C1的R/S/A位和描述符的AP位共同决定。“域”决定是否对某块内存进行权限检查,"AP"决定如何对某块内容进行权限检查。S3C2440有16个域,CP15寄存器C3中每两位对应一个域(一共32位),用来表示这个域是否进行权限检查。
每两位数据的含义:00---无访问权限(任何访问都将导致"Domain fault"异常);01---客户模式(使用段描述符、页描述符进行权限检查);10---保留(保留,目前相当于“无访问权限”);11---管理模式(不进行权限检查,允许任何访问)。"Domain"占用4位,用来表示内存属于0-15哪一个域。
四、TLB和Cache
首先说两者都是利用程序访问的局部性原理,通过设置高速、小容量的存储器来提高性能。
1.(TLB---Translation Lookaside Buffers,转译查找缓存):由于从MVA到PA的转换需要访问多次内存,大大降低了CPU的性能,故提出TLB办法改进。当CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查,否则MMU访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB中,下次再使用这个虚拟地址时就直接使用TLB用的描述符。使用TLB需要保证TLB中的内容与页表一致,在启动MMU之前,页表中的内容发生变化后,尤其要注意。一般的做法是在启动MMU之前使整个TLB无效,改变页表时,使所涉及的虚拟地址对应的TLB中条目无效。
2.(Cache,高速缓存):为提高程序的运行速度,在主存和CPU通用寄存器之间设置一个高速的、容量相对较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,供CPU在一段时间内使用。
★写数据的两种方式:①(Write Through,写穿式)---任一CPU发出写信号送到Cache的同时,也写入主存,保证主存的数据同步更新。优点是操作简单,但由于主存速度慢,降低了系统的写速度并占用了总线的时间。②(Write Back,回写式)---数据一般只写到Cache,这样可能出现Cache中的数据得到更新而主存中的数据不变(数据陈旧)的情况。此时可在Cache中设一个标志地址及数据陈旧的信息,只有当Cache中的数据被换出或强制进行”清空“操作时,才将原更新的数据写入主存响应的单元中,保证了Cache和主存中数据一致。
★Cache有以下两个操作:①(Clean,清空)---把Cache或Write buffer中已经脏的(修改过,但未写入主存)数据写入主存。②(Invalidate,使无效)---使之不能再使用,并不将脏的数据写入主存。
★S2C2440内置了(ICaches,指令Cache)、(DCaches,数据Cache)和(Write buffer,写缓存),操作时需要用到描述符中的C位(Ctt)和B位(Btt)。①(ICaches,指令Cache)---系统刚上电或复位时,ICaches中的内容是无效的,并且ICaches功能关闭。往Icr位(CP15协处理器中寄存器1的第12位)写1可以启动ICaches,写0停止ICaches。ICaches一般在MMU开启后使用,此时描述符的C位用来表示一段内存是否可以被Cache。若Ctt=1,允许Cache,否则不允许。如果MMU没有开启,ICaches也可以被使用,此时CPU读取指令时所涉及的内存都被当做允许Cache。ICaches关闭时,CPU每次取指都要读取主存,性能低,所以通常尽早启动ICaches。ICaches开启后,CPU每次取指时都会先在ICaches中查看是否能找到所用指令,而不管Ctt是0还是1。如果找到成为Cache命中,找不到称为Cache丢失,ICaches被开启后,CPU的取指有如下三种情况:Cache命中且Ctt为1时,从ICaches中取指,返回CPU;Cache丢失且Ctt为1时,CPU从主存中取指,并且把指令缓存到Cache中;Ctt为0时,CPU从主存中取指。②(DCaches,数据Cache)---与ICaches相似,系统刚上电或复位时,DCaches中的内容无效,并且DCaches功能关闭,Write buffer中的内容也是被废弃不用的。往Ccr位(CP15协处理器 中寄存器1的第二位)写1启动DCaches,写0停止DCaches。Write buffer和DCaches紧密结合,额米有专门的控制来开启和停止它。与ICaches不同,DCaches功能必须在MMU开启之后才能被使用。DCaches被关闭时,CPU每次都去内存取数据。DCaches被开启后,CPU每次读写数据时都会先在DCaches中查看是否能找到所要的数据,不管Ctt是0还是1,找到了称为Cache命中,找不到称为Cache丢失。
★使用Cache时需要保证Cache、Write buffer的内容和主存内容一致,保证下面两个原则:①清空DCaches,使主存数据得到更新。②使无效ICaches,使CPU取指时重新读取主存。
在实际编写程序时,要注意如下几点:①开启MMU前,使无效ICaches,DCaches和Write buffer。②关闭MMU前,清空ICaches、DCaches,即将“脏”数据写到主存上。③如果代码有变,使无效ICaches,这样CPU取指时会从新读取主存。④使用DMA操作可以被Cache的内存时:将内存的数据发送出去时,要清空Cache;将内存的数据读入时,要使无效Cache。⑤改变页表中地址映射关系时也要慎重考虑。⑥开启ICaches或DCaches时,要考虑ICaches或DCaches中的内容是否与主存保持一致。⑦对于I/O地址空间,不使用Cache和Write buffer。
源码:
1.head.s
1 @************************************************************************* 2 @ File:head.S 3 @ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU, 4 @ 然后跳到SDRAM继续执行 5 @************************************************************************* 6 7 .text 8 .global _start 9 _start: 10 ldr sp, =4096 @ 设置栈指针,以下都是C函数,调用前需要设好栈 11 bl disable_watch_dog @ 关闭WATCHDOG,否则CPU会不断重启 12 bl memsetup @ 设置存储控制器以使用SDRAM 13 bl copy_2th_to_sdram @ 将第二部分代码复制到SDRAM 14 bl create_page_table @ 设置页表 15 bl mmu_init @ 启动MMU 16 ldr sp, =0xB4000000 @ 重设栈指针,指向SDRAM顶端(使用虚拟地址) 17 ldr pc, =0xB0004000 @ 跳到SDRAM中继续执行第二部分代码 18 halt_loop: 19 b halt_loop
2.init.c
1 /* 2 * init.c: 进行一些初始化,在Steppingstone中运行 3 * 它和head.S同属第一部分程序,此时MMU未开启,使用物理地址 4 */ 5 6 /* WATCHDOG寄存器 */ 7 #define WTCON (*(volatile unsigned long *)0x53000000) 8 /* 存储控制器的寄存器起始地址 */ 9 #define MEM_CTL_BASE 0x48000000 10 11 12 /* 13 * 关闭WATCHDOG,否则CPU会不断重启 14 */ 15 void disable_watch_dog(void) 16 { 17 WTCON = 0; // 关闭WATCHDOG很简单,往这个寄存器写0即可 18 } 19 20 /* 21 * 设置存储控制器以使用SDRAM 22 */ 23 void memsetup(void) 24 { 25 /* SDRAM 13个寄存器的值 */ 26 unsigned long const mem_cfg_val[]={ 0x22011110, //BWSCON 27 0x00000700, //BANKCON0 28 0x00000700, //BANKCON1 29 0x00000700, //BANKCON2 30 0x00000700, //BANKCON3 31 0x00000700, //BANKCON4 32 0x00000700, //BANKCON5 33 0x00018005, //BANKCON6 34 0x00018005, //BANKCON7 35 0x008C07A3, //REFRESH 36 0x000000B1, //BANKSIZE 37 0x00000030, //MRSRB6 38 0x00000030, //MRSRB7 39 }; 40 int i = 0; 41 volatile unsigned long *p = (volatile unsigned long *)MEM_CTL_BASE; 42 for(; i < 13; i++) 43 p[i] = mem_cfg_val[i]; 44 } 45 46 /* 47 * 将第二部分代码复制到SDRAM 48 */ 49 void copy_2th_to_sdram(void) 50 { 51 unsigned int *pdwSrc = (unsigned int *)2048; 52 unsigned int *pdwDest = (unsigned int *)0x30004000; 53 54 while (pdwSrc < (unsigned int *)4096) 55 { 56 *pdwDest = *pdwSrc; 57 pdwDest++; 58 pdwSrc++; 59 } 60 } 61 62 /* 63 * 设置页表 64 */ 65 void create_page_table(void) 66 { 67 68 /* 69 * 用于段描述符的一些宏定义 70 */ 71 #define MMU_FULL_ACCESS (3 << 10) /* 访问权限 */ 72 #define MMU_DOMAIN (0 << 5) /* 属于哪个域 */ 73 #define MMU_SPECIAL (1 << 4) /* 必须是1 */ 74 #define MMU_CACHEABLE (1 << 3) /* cacheable */ 75 #define MMU_BUFFERABLE (1 << 2) /* bufferable */ 76 #define MMU_SECTION (2) /* 表示这是段描述符 */ 77 #define MMU_SECDESC (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \ 78 MMU_SECTION) 79 #define MMU_SECDESC_WB (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \ 80 MMU_CACHEABLE | MMU_BUFFERABLE | MMU_SECTION) 81 #define MMU_SECTION_SIZE 0x00100000 82 83 unsigned long virtuladdr, physicaladdr; 84 unsigned long *mmu_tlb_base = (unsigned long *)0x30000000; 85 86 /* 87 * Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0, 88 * 为了在开启MMU后仍能运行第一部分的程序, 89 * 将0~1M的虚拟地址映射到同样的物理地址 90 */ 91 virtuladdr = 0; 92 physicaladdr = 0; 93 *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \ 94 MMU_SECDESC_WB; 95 96 /* 97 * 0x56000000是GPIO寄存器的起始物理地址, 98 * GPBCON和GPBDAT这两个寄存器的物理地址0x56000050、0x56000054, 99 * 为了在第二部分程序中能以地址0xA0000050、0xA0000054来操作GPFCON、GPFDAT, 100 * 把从0xA0000000开始的1M虚拟地址空间映射到从0x56000000开始的1M物理地址空间 101 */ 102 virtuladdr = 0xA0000000; 103 physicaladdr = 0x56000000; 104 *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \ 105 MMU_SECDESC; 106 107 /* 108 * SDRAM的物理地址范围是0x30000000~0x33FFFFFF, 109 * 将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF上, 110 * 总共64M,涉及64个段描述符 111 */ 112 virtuladdr = 0xB0000000; 113 physicaladdr = 0x30000000; 114 while (virtuladdr < 0xB4000000) 115 { 116 *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \ 117 MMU_SECDESC_WB; 118 virtuladdr += 0x100000; 119 physicaladdr += 0x100000; 120 } 121 } 122 123 /* 124 * 启动MMU 125 */ 126 void mmu_init(void) 127 { 128 unsigned long ttb = 0x30000000; 129 130 __asm__( 131 "mov r0, #0\n" 132 "mcr p15, 0, r0, c7, c7, 0\n" /* 使无效ICaches和DCaches */ 133 134 "mcr p15, 0, r0, c7, c10, 4\n" /* drain write buffer on v4 */ 135 "mcr p15, 0, r0, c8, c7, 0\n" /* 使无效指令、数据TLB */ 136 137 "mov r4, %0\n" /* r4 = 页表基址 */ 138 "mcr p15, 0, r4, c2, c0, 0\n" /* 设置页表基址寄存器 */ 139 140 "mvn r0, #0\n" 141 "mcr p15, 0, r0, c3, c0, 0\n" /* 域访问控制寄存器设为0xFFFFFFFF, 142 * 不进行权限检查 143 */ 144 /* 145 * 对于控制寄存器,先读出其值,在这基础上修改感兴趣的位, 146 * 然后再写入 147 */ 148 "mrc p15, 0, r0, c1, c0, 0\n" /* 读出控制寄存器的值 */ 149 150 /* 控制寄存器的低16位含义为:.RVI ..RS B... .CAM 151 * R : 表示换出Cache中的条目时使用的算法, 152 * 0 = Random replacement;1 = Round robin replacement 153 * V : 表示异常向量表所在的位置, 154 * 0 = Low addresses = 0x00000000;1 = High addresses = 0xFFFF0000 155 * I : 0 = 关闭ICaches;1 = 开启ICaches 156 * R、S : 用来与页表中的描述符一起确定内存的访问权限 157 * B : 0 = CPU为小字节序;1 = CPU为大字节序 158 * C : 0 = 关闭DCaches;1 = 开启DCaches 159 * A : 0 = 数据访问时不进行地址对齐检查;1 = 数据访问时进行地址对齐检查 160 * M : 0 = 关闭MMU;1 = 开启MMU 161 */ 162 163 /* 164 * 先清除不需要的位,往下若需要则重新设置它们 165 */ 166 /* .RVI ..RS B... .CAM */ 167 "bic r0, r0, #0x3000\n" /* ..11 .... .... .... 清除V、I位 */ 168 "bic r0, r0, #0x0300\n" /* .... ..11 .... .... 清除R、S位 */ 169 "bic r0, r0, #0x0087\n" /* .... .... 1... .111 清除B/C/A/M */ 170 171 /* 172 * 设置需要的位 173 */ 174 "orr r0, r0, #0x0002\n" /* .... .... .... ..1. 开启对齐检查 */ 175 "orr r0, r0, #0x0004\n" /* .... .... .... .1.. 开启DCaches */ 176 "orr r0, r0, #0x1000\n" /* ...1 .... .... .... 开启ICaches */ 177 "orr r0, r0, #0x0001\n" /* .... .... .... ...1 使能MMU */ 178 179 "mcr p15, 0, r0, c1, c0, 0\n" /* 将修改的值写入控制寄存器 */ 180 : /* 无输出 */ 181 : "r" (ttb) ); 182 }
3.leds.c
1 /* 2 * leds.c: 循环点亮4个LED 3 * 属于第二部分程序,此时MMU已开启,使用虚拟地址 4 */ 5 6 #define GPFCON (*(volatile unsigned long *)0xA0000050) // 物理地址0x56000050 7 #define GPFDAT (*(volatile unsigned long *)0xA0000054) // 物理地址0x56000054 8 9 #define GPF4_out (1<<(4*2)) 10 #define GPF5_out (1<<(5*2)) 11 #define GPF6_out (1<<(6*2)) 12 13 /* 14 * wait函数加上“static inline”是有原因的, 15 * 这样可以使得编译leds.c时,wait嵌入main中,编译结果中只有main一个函数。 16 * 于是在连接时,main函数的地址就是由连接文件指定的运行时装载地址。 17 * 而连接文件mmu.lds中,指定了leds.o的运行时装载地址为0xB4004000, 18 * 这样,head.S中的“ldr pc, =0xB4004000”就是跳去执行main函数。 19 */ 20 static inline void wait(volatile unsigned long dly) 21 { 22 for(; dly > 0; dly--); 23 } 24 25 int main(void) 26 { 27 unsigned long i = 0; 28 29 GPFCON = GPF4_out|GPF5_out|GPF6_out; // 将LED1,2,4对应的GPF4/5/6三个引脚设为输出 30 31 while(1){ 32 wait(30000); 33 GPFDAT = (~(i<<4)); // 根据i的值,点亮LED1,2,4 34 if(++i == 8) 35 i = 0; 36 } 37 38 return 0; 39 }
4.mmu.lds
1 SECTIONS { 2 firtst 0x00000000 : { head.o init.o } 3 second 0xB0004000 : AT(2048) { leds.o } 4 } 5
5.makefile
1 objs := head.o init.o leds.o 2 3 mmu.bin : $(objs) 4 arm-linux-ld -Tmmu.lds -o mmu_linux $^ 5 arm-linux-objcopy -O binary -S mmu_linux $@ 6 arm-linux-objdump -D -m arm mmu_linux > mmu.dis 7 8 %.o:%.c 9 arm-linux-gcc -Wall -O2 -c -o $@ $< 10 11 %.o:%.S 12 arm-linux-gcc -Wall -O2 -c -o $@ $< 13 14 clean: 15 rm -f mmu.bin mmu_linux mmu.dis *.o