裸机系列代码地址:链接:http://pan.baidu.com/s/1pLHOd0v 密码:4x5s
内存管理单元(memory management unit),简称MMU,它负责虚拟地址到物理地址的映射
arm采用页表的形式将一个虚拟地址转换成物理地址,以段的方式进行转换时只用到一级页表,以页的方式进行转换则会用到二级页表。
具体的映射过程如下,假设CPU发出一个32位的虚拟地址addr
首先根据页表基值寄存器的[31:14]位找到页表(假设内存地址为P),addr[31:20]位表示一个偏移量(假设值为a),在页表的a这个位置的条目中存放
着一级页表的地址,即P[a]的值是一级页表的地址,现在找到了一级页表的地址PP,那么怎么知道一级页表是用段表示,还是粗页表或是细页表呢,这是根据
P[a]这个值的后两位来判断,其实P[a]的值只有[31:20]表示一级页表的地址,后20位用作其他表示,例如访问限制和说明一级页表的表示法
00:错误,无效
01:粗页表
10:段
11:细页表
假设低两位为01,即一级页表采用段表示法,假设addr[19:0]的值为b,则&PP[b]即是addr对应的物理地址,PP[b]表示要访问的内存单元
上面对CPU发出虚拟地址映射到物理地址的过程做了一个大概的说明(一级页表为段表示法,这种最简单),下面讲讲具体的映射过程。首先要对页表中的条目(一级页表描述符)的格式要了解
当我们从页表中得到了一个条目的数据,我们要对这个数据进行解析,这个格式说明了数据的解析方法,现在我们看看一级页表的几种表示方式
(1)段表示
一个段占用1M内存空间,这样的段在内存中有4096个,页表中每个条目对应一个段的地址,addr[31:20]表示要使用的地址在哪个段中,addr[19:0]
表示在段内的偏移量,表示使用段内的哪个内存单元。
(2)粗页表,粗页表不同于段,段表示一段可以被使用的内存单元的集合,总共1M大小,粗页表和页表相似,其中的内存单元存放的是二级页表的描述符,不是
可使用的内存单元。页表有4096个条目,所以粗页表最多可以有4096个,粗页表中共有256个条目,每个条目对应一个内存页(在二级表示中内存页有
三种形式,大页:64K,小页:4k,极小页:1k)。从粗页表中共有256个条目可知,addr中需要分出8位来存放内存页在一级页表的偏移量。
(3)细页表,细页表共有1024个条目,每个条目对应一个内存页。由于细页表总共有1024个条目,所以addr中需要分出10位来表示一级页表的偏移。
已经讲完了页表,段,和一级页表,下面再来讲讲三种内存页,大页,小页,极小页。
虽然前面讲将段和页表,一级页表放在了一块儿分析,但是其实段的性质和内存页才是相似的,之所以前面将段和页表放在一块儿分析,是因为段在虚拟地址转换
成物理地址的过程中,段处了第二层,和页表处于同一层。
如果我们得到了一级页表,那么我们怎么知道一级页表的条项表示的内存页是大页,小页,还是极小页了。这和从页表的条项中判定其下一级转换是段,粗页表还是
细页表的过程相似,都是从其条项的数据的格式来分析,从条项数据的最后两位来判断
0b00:无效
0b01:大页
0b10:小页
0b11:极小页
(1)大页
大页为64K大小,所以如果大页的描述符存放在粗页表中的话,由于粗页表的每个条目应对应4K字节,所以需要16个粗页表的条目才能完整的表示一个大页
同样由于大页的64k大小,所以addr的最后16位都用来在大页中寻址
(2)小页为4k大小,如果小页的描述符存放在粗页表中的话,刚好没一个条目对应一个小页。如果小页的描述符存放在细页表中的话,则需要四个细页的条目
表示一个完整的小页
(3)极小页为1k大小,所以极小页的描述符只能存放在细页表中,刚好一个细页表条目对应一个极小页。如果将极小页的描述符存放在粗页表中的话,那么
4096*256*1K=256M,即只能表示256M空间大小,粗页表每个条目的能力能表示4k,现在却值使用了1k。
页表,一级页表,内存页之间有如下几种结合方式
1、页表,段
2、页表,粗页表,大页
3、页表,粗页表,小页
4、页表,细页表,大页
5、页表,细页表,小页
6、页表,细页表,极小页
细页表一个条项对应1k,刚好对应极小页
我们先来看看这种刚好对应的情况(例如粗页表对应的小页)
1、首先通过页表基址寄存器得到页表的地址,假设CPU发出的虚拟地址为addr
2、根据addr[31:20]索引页表,得到某个条项,从这个条项的后两位判断一级页表位粗页表,并得到一级页表的基地址
3、根据addr[19:12]得到粗页表中的某个条项,根据这个条项的后两位判断内存页为小页,并得到内存页的基地址
4、根据addr[11:0]得到一个内存页的内存单元,这个内存单元就是虚拟地址寻址得到的物理内存单元,这个内存单元地址的[31:12]位是通过页表和一级
页表转换得到的。
再来看看粗页表对应大页,细页表对应大页,细页表对应小于的情况,这种情况是多个一级页表的条目项指向一个内存页
以细页表对应小页为情况分析
细页表一个条项可以表示1k的内存空间,小页大小为4k,所以可以得知连续四个条项是指向同一个内存页的
1、首先通过页表基址寄存器得到页表的基地址,假设CPU发出的虚拟地址为addr
2、根据addr[31:20]得到某个页表条项,根据条项的后两位判断一级页表位细页表,并得到细页表的基地址
3、根据addr[19:10]得到细页表的某个条项,根据后两位判断内存页为小页,并得到小页的基地址,假设小页的基地址为P[31:12]
4、根据addr[11:0]得到小页内的内存单元,则寻址得到的物理地址为P[31:12]addr[11:0]。
这里涉及到四个条项指向一个内存页的问题
内存访问权限检查
内存的访问权限检查是MMU的主要功能之一,简单的说就是决定一块内存是否允许读写。
内存块的访问权限由以下几个部分联合控制
1、CP15寄存器C3
2、一级页表的描述符的domain域
3、CP15寄存器C1的R/S/A位
4、内存页描述符的AP位
这几个部分具体怎样作用使得内存块得到应有的访问权限参见韦东山老师的《嵌入式Linux应用开发完全手册》
TLB的作用
程序访问的局部性:程序执行过程中,所用到的指令,数据的地址往往集中在一个很小的范围,其中的地址,数据经常多次使用。
基于程序访问的局部性,通过使用一个高速,容量相对较小的存储器来存储近期用到的页表条目(段/大页/小页/极小页),即保持最近使用的内存页的描述符
以避免每次转换时都到主存去查找,这样可以大幅度的提高性能。这个存储器用来帮助快速的进行地质转换,称为“转译查找缓存”(Translation Lookaside Buffers)当CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的描述符,则直接使用此描述符进行地址和权限检查;否则MMU访问页表找到描述符后在进行地址转换和权限检查,并将这个描述符填入TLB中(如果TLB已满,则使用某个算法找到一个条项,覆盖它)
现在来看看代码分为五个部分,先从Makefile入手看看
objs := head.o init.o led.o
mmu.bin : $(objs)
arm-linux-ld -Tmmu.lds -o mmu_linux $^
arm-linux-objcopy -O binary -S mmu_linux $@
arm-linux-objdump -D -m arm mmu_linux > mmu.dis
%.o:%.c
arm-linux-gcc -Wall -O2 -c -o $@ $<
%.o:%.S
arm-linux-gcc -Wall -O2 -c -o $@ $<
clean:
rm -f mmu.bin mmu_linux mmu.dis *.o
接下来是链接文件
SECTIONS {
firtst 0x00000000 : { head.o init.o }
second 0xB0004000 : AT(2048) { led.o }
}
mmu.lds将程序分为两个段,first和second,前者由head.o和init.o组成,它的运行地址和加载地址都是0,,所以运行前不需要移动代码。后者由led.o组成,加载地址为2048
,运行地址为0xB0004000,所以运行前需要将代码复制到0xB0004000.
head.S文件
.equ MEM_CTL_BASE , 0x48000000
.equ SDRAM_BASE, 0X30004000
.text
.global _start
_start:
ldr sp,=4096 /*设置堆栈指针为调用C函数准备堆栈*/
bl disable_watchdog /*关看门狗*/
bl init_sdram /*初始化存储管理器,使得SDRAM可用*/
bl copy_steppingstone_to_sdram /*将代码由加载地址复制到运行地址*/
bl setpagetable /*设置页表,建立虚拟地址到物理地址的映射*/
bl mmu_init /*开启由页表建立的映射关系,即开启MMU*/
ldr sp,=0xB4000000 /*实际为物理地址0x34000000处,即SDRAM的末尾*/
ldr pc,=0xB0004000 /*在mmu.lds中我们看到led.o的运行地址在0xB0004000*/
halt_loop:
b halt_loop
/*关看门狗*/
disable_watchdog:
mov r0,#0x53000000
mov r1,#0x00000000
str r1,[r0]
mov pc,lr
/*将steppingstone中的代码复制到SDRAM中*/
copy_steppingstone_to_sdram:
mov r0,#2048 /*这里从2048开始复制,head.o和init.o不复制到SDRAM中*/
ldr r1,=SDRAM_BASE /*开始地址并不是0x30000000,因为SDRAM开始16k留给了页表*/
mov r3,#4096 /*复制区间为2048~4096,即led.o所在的部分*/
1:
ldr r2,[r0],#4
str r2,[r1],#4
cmp r0,r3
bne 1b
mov pc,lr
/*初始化内存管理器,使得SDRAM可用*/
init_sdram:
ldr r0,=MEM_CTL_BASE /*存储控制器13个寄存器的起始地址*/
adrl r1,sdram_val /*这里必须用地址无关的代码,即不能用ldrl,这和sdram_val
的加载地址有关*/
add r3,r1,#13*4
1:
ldr r2,[r1],#4
str r2,[r0],#4
cmp r1,r3
bne 1b
mov pc,lr
.align 4
sdram_val:
.long 0x22011110
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00018005
.long 0x00018005
.long 0x008c07A3
.long 0x000000b1
.long 0x00000030
.long 0x00000030
init.c文件
void setpagetable(void)
{
/*下面这些为的设置参见一级页表的描述符格式*/
#define MMU_FULL_ACCESS (3<<10) /*AP标示*/
#define MMU_DOMAIN (0<<5) /*Domain域*/
#define MMU_SPECIAL (1<<4) /*必须为1*/
#define MMU_CACHEABLE (1<<3) /*Cache使能的C位*/
#define MMU_BUFFERABLE (1<<2) /*buffer使能的B位*/
#define MMU_SECTION (2) /*标示一级页表的表示法,2=0b10,为粗页表*/
#define MMU_SECDESC (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | MMU_SECTION)
#define MMU_SECTION_WB (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | MMU_CACHEABLE |\
MMU_BUFFERABLE | MMU_SECTION)
//#define MMU_SECTION_SIZE 0X00100000
unsigned long virtualaddr ,physicaladdr;
unsigned long *mmu_tlb_base = (unsigned long *)0x30000000; /*页表的起始地址*/
/*将0地址开始的1M虚拟空间映射到0地址开始的1M物理空间*/
virtualaddr = 0;
physicaladdr = 0; /*(virtualaddr >> 20)为mmu_tlb_base表的第一个条项*/
*(mmu_tlb_base + (virtualaddr >> 20)) = ((physicaladdr & 0xFFF00000) | MMU_SECTION_WB);
/*将0xA0000000开始的1M虚拟空间映射到0x560000000开始的1M物理空间,控制GPFIO和GPGIO寄存器的地址所在*/
virtualaddr = 0xA0000000;
physicaladdr = 0x56000000; /*(virtualaddr >> 20)为mmu_tlb_base表的第0xA00个条项*/
*(mmu_tlb_base + (virtualaddr >> 20)) = ((physicaladdr & 0xFFF00000 )| MMU_SECDESC);
/*将0xB0000000开始的64M虚拟空间映射到0x30000000开始的64M物理空间*/
virtualaddr = 0xB0000000;
physicaladdr = 0x30000000; /*总共占用64个条项,从0xBO开始到0xB40结束*/
while(virtualaddr < 0xB4000000) /*0x30004000映射为虚拟地址0xB0004000,led.o在虚拟地址0xB0004000处*/
{
*(mmu_tlb_base + (virtualaddr >> 20)) = ((physicaladdr & 0XFFF00000) | MMU_SECTION_WB);
virtualaddr += 0x100000;
physicaladdr += 0x100000;
}
}
/*从代码中可以看出,虚拟地址到物理地址的转换过程中,在段的表示法中,只有其前12位会变化,后20为保持不变
后20位是内存单元在内存页中的偏移,这个量不会变动的,变动的位数是标示内存页基地址的那几位*/
/*
1、内存页为大页,64k,则地址转换过程中,寻址内存页地址的[31:16]为转换得到的物理地址
2、内存页为小页,4k,则地址转换过程中,寻址内存页地址的[31:12]为转换得到的物理地址
3、内存页为极小页,1k,则地址转换过程中,寻址内存页地址的[31:10]为转换得到的物理地址
*/
void mmu_init(void)
{
unsigned long ttb = 0x30000000;
__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" /* 读出控制寄存器的值 */
/* 控制寄存器的低16位含义为:.RVI ..RS B... .CAM
* R : 表示换出Cache中的条目时使用的算法,
* 0 = Random replacement;1 = Round robin replacement
* V : 表示异常向量表所在的位置,
* 0 = Low addresses = 0x00000000;1 = High addresses = 0xFFFF0000
* I : 0 = 关闭ICaches;1 = 开启ICaches
* R、S : 用来与页表中的描述符一起确定内存的访问权限
* B : 0 = CPU为小字节序;1 = CPU为大字节序
* C : 0 = 关闭DCaches;1 = 开启DCaches
* A : 0 = 数据访问时不进行地址对齐检查;1 = 数据访问时进行地址对齐检查
* M : 0 = 关闭MMU;1 = 开启MMU
*/
/*
* 先清除不需要的位,往下若需要则重新设置它们
*/
/* .RVI ..RS B... .CAM */
"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" (ttb) );
}
led.c为测试文件,用按键来控制led
#define GPFCON (*(volatile unsigned long *)0xA0000050) /*这些寄存器的地址必须用虚拟地址,因为前面开启了MUU*/
#define GPFDAT (*(volatile unsigned long *)0xA0000054)
#define GPGCON (*(volatile unsigned long *)0xA0000060)
#define GPGDAT (*(volatile unsigned long *)0xA0000064)
#define GPF4_5_6_OUT ((0b01<<12)|(0b01<<10)|(0b01<<8))
#define GPF0_2_IN (~((0x3<<4)&(0x3)))
#define GPG3_IN (~(0X3<<6))
#define GPF4_5_6_OFF ((1<<4)|(1<<5)|(1<<6))
int main()
{
GPFCON|=GPF4_5_6_OUT;
GPFCON&=GPF0_2_IN;
GPGCON&=GPG3_IN;
GPFDAT|=GPF4_5_6_OFF;
unsigned long gpfval;
unsigned long gpgval;
while(1)
{
gpfval=GPFDAT;
if(gpfval&1)
GPFDAT|=(1<<4);
else
GPFDAT&=(~(1<<4));
if((gpfval>>2)&1)
GPFDAT|=(1<<5);
else
GPFDAT&=(~(1<<5));
gpgval=GPGDAT;
if((gpgval>>3)&1)
GPFDAT|=(1<<6);
else
GPFDAT&=(~(1<<6));
}
return 0;
}