1 : MMU的作用
MMU:内存管理机制单元, 主要作用是:把虚拟地址转换为物理地址,为各硬件访问内存提供权限机制。
VA ( virtual Address) 虚拟地址,VMA(Modified VA)修改后的虚拟地址。 PA(phisical address)
三者之间的关系:
MMU把虚拟后地址换成物理地址的过程:
1:在系统初始化的时候,会先把页表的内容写内存中,设置MMU,并告诉MMU页表存在内存中的地址。
2:设置启动MMU之后,CPU每次执行访问内存的指令的时候都会引发MMU做查表和地址转换的动作。地址转换由硬件完成,不需要指令控制MMU去操作
没有启动MMU时,CPU核心,cache,MMU,外设等所有部件使用的都是物理地址。
启动MMU后,CPU核心对外发出虚拟地址VA;VA被转换为MVA供cache,MMU使用,在这里MVA被转换成PA;最后使用PA读取实际设备
CPU核心看到和用到的只是虚拟地址VA,至于VA如果去对应物理地址PA,CPU核心不理会
caches和MMU看不到VA,他们利用MVA转换得到PA
实际设备看不到VA、MVA,读写它们使用的是物理地址PA
页表:
页表是由一个个表项组成的物理表,每个表项或是物理页(一块物理内存为1K,4K大小不等)的起始地址。或是一个二级页表的起始的地址(当表项为二级页表的起始地址时,又可以称该页为页描述符)所有的页表都用物理地址访问。
表项的分类:
1:一是直接保存的物理页的地址或是段的起始地址。如段的描述符,大小页,极小页的描述等
2:另一类是保存二级页表的物理地址,如粗页表描述符,细页表描述符等
一级映射与二级映射
相关代码解释如下:
ldr sp, =4096
@设置栈指针,以下都是C函数,调用前需要设好栈
bl
disable_watch_dog
@关闭WATCHDOG,否则CPU会不断重启
bl
memsetup
@设置存储控制器以使用SDRAM
bl
copy_2th_to_sdram
@将第二部分代码复制到SDRAM
bl
create_page_table
@ 设置页表
bl
mmu_init
@ 启动MMU,可以使用虚拟地址了
ldr sp, =0xB4000000
@重设栈指针,指向SDRAM顶端(使用虚拟地址)
ldr pc, =0xB0004000
@ 跳到SDRAM中继续执行第二部分代码
@ ldr pc, =main
b
halt_loop
/ init.c: 进行一些初始化,在Steppingstone中运行
WTCON = 0;
// 关闭WATCHDOG很简单,往这个寄存器写0即可
// SDRAM13个寄存器的值
unsigned long
const
mem_cfg_val[]={ 0x22011110,
//BWSCON
0x00000700,
//BANKCON0
0x00000700,
//BANKCON1
0x00000700,
//BANKCON2
0x00000700,
//BANKCON3
0x00000700,
//BANKCON4
0x00000700,
//BANKCON5
0x00018005,
//BANKCON6
0x00018005,
//BANKCON7
0x008C07A3,
//REFRESH
0x000000B1,
//BANKSIZE
0x00000030,
//MRSRB6
0x00000030,
//MRSRB7
};
int
i = 0;
volatile unsigned long*p = (volatile unsigned long *)MEM_CTL_BASE;
for(; i < 13;i++)
p[i] = mem_cfg_val[i];
//参考链接文件,第二部分代码放在2048地址
unsigned int *pdwSrc
= (unsigned int *)2048;
//此地址为实际的物理地址,先拷贝到实际的SDRAM中,初始化MMU之后才能使用虚拟地址进行访问
unsigned int *pdwDest =(unsigned int *)0x30004000;
while (pdwSrc <(unsigned int *)4096)
// SRAM的最大地址4M
{
*pdwDest = *pdwSrc;
pdwDest++;
pdwSrc++;
}
MMU_SECTION)
MMU_CACHEABLE | MMU_BUFFERABLE |MMU_SECTION)
unsigned longvirtuladdr, physicaladdr;
unsigned long*mmu_tlb_base = (unsigned long *)0x30000000;
//Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0,
//为了在开启MMU后仍能运行第一部分的程序,
//将0~1M的虚拟地址映射到同样的物理地址
virtuladdr = 0;
physicaladdr = 0;
*(mmu_tlb_base +(virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) |\
MMU_SECDESC_WB;
//0x56000000是GPIO寄存器的起始物理地址,
//GPFCON和GPFDAT这两个寄存器的物理地址0x56000050、0x56000054,
//为了在第二部分程序中能以地址0xA0000050、0xA0000054来操作GPFCON、GPFDAT,
//把从0xA0000000开始的1M虚拟地址空间映射到从0x56000000开始的1M物理地址空间
virtuladdr =0xA0000000;
physicaladdr =0x56000000;
*(mmu_tlb_base +(virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) |\
MMU_SECDESC;
//SDRAM的物理地址范围是0x30000000~0x33FFFFFF,
//将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF上,
// 总共64M,涉及64个段描述符
virtuladdr =0xB0000000;
physicaladdr =0x30000000;
while (virtuladdr <0xB4000000)
{
*(mmu_tlb_base + (virtuladdr >> 20)) =(physicaladdr & 0xFFF00000) | \
MMU_SECDESC_WB;
virtuladdr += 0x100000;
physicaladdr += 0x100000;
}
unsigned long ttb =0x30000000;
"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 robinreplacement
// V :表示异常向量表所在的位置,
//
0 = Low addresses = 0x00000000;1 = Highaddresses = 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) );
for(; dly > 0;dly--);
arm-linux-ld -Tmmu.lds-o mmu_elf $^
arm-linux-objcopy -Obinary -S mmu_elf $@
arm-linux-objdump -D -marm mmu_elf > mmu.dis
arm-linux-gcc -Wall -O2-c -o $@ $<</div>
arm-linux-gcc -Wall -O2-c -o $@ $<</div>
rm -f mmu.bin mmu_elfmmu.dis *.o
firtst
0x00000000: { head.o init.o }
//第一段链接地址为0
second
0xB0004000 : AT(2048) { leds.o }
//第二段存放地址为2048,链接地址为虚拟地址
0xB0004000
1:在系统初始化的时候,会先把页表的内容写内存中,设置MMU,并告诉MMU页表存在内存中的地址。
2:设置启动MMU之后,CPU每次执行访问内存的指令的时候都会引发MMU做查表和地址转换的动作。地址转换由硬件完成,不需要指令控制MMU去操作
没有启动MMU时,CPU核心,cache,MMU,外设等所有部件使用的都是物理地址。
启动MMU后,CPU核心对外发出虚拟地址VA;VA被转换为MVA供cache,MMU使用,在这里MVA被转换成PA;最后使用PA读取实际设备
CPU核心看到和用到的只是虚拟地址VA,至于VA如果去对应物理地址PA,CPU核心不理会
caches和MMU看不到VA,他们利用MVA转换得到PA
实际设备看不到VA、MVA,读写它们使用的是物理地址PA
页表:
页表是由一个个表项组成的物理表,每个表项或是物理页(一块物理内存为1K,4K大小不等)的起始地址。或是一个二级页表的起始的地址(当表项为二级页表的起始地址时,又可以称该页为页描述符)所有的页表都用物理地址访问。
表项的分类:
1:一是直接保存的物理页的地址或是段的起始地址。如段的描述符,大小页,极小页的描述等
2:另一类是保存二级页表的物理地址,如粗页表描述符,细页表描述符等
一级映射与二级映射
一级映射,是指以段(Section,大小1MB)的方式进行转换。
二级映射,是以页的方式进行转换。ARM页大小由三种:1k,4k,64k
4.地址转换的过程
(1)CPU发出一个VA,是怎么转换为MVA的
这在上图可以看到,是通过一个硬件电路转换的。
在ARM9里面,如果VA<32M,利用进程标识号PID转换得到MVA
过程如下:
if(VA < 32M)
MVA = VA | ( PID<<25 )
else
MVA = VA
只是上面的过程是由硬件实现的。
这样为VA进行了一级映射,为什么呢?在linux系统里,每个进程的地址空间0-4G,0-3G是进程独有的,称为用户空间,3G-4G是系统的,称为内核空间,所有进程共享。如果两个进程所用的VA有重叠,在切换进程时,为了把重叠的VA映射到不同的PA上,需要重建页表、使无效caches和TLBS。使用了MVA,使进程在VA相同的情况下,使用不同的MVA,进而PA也不同。这就是在VA与PA之间加上一次到MVA的映射的意义。
相关代码解释如下:
.text
.global _start
_start:
halt_loop:
/ init.c: 进行一些初始化,在Steppingstone中运行
// 它和head.S同属第一部分程序,此时MMU未开启,使用物理地址
// WATCHDOG寄存器
#define WTCON
(*(volatile unsigned long *)0x53000000)
// 存储控制器的寄存器起始地址
#define MEM_CTL_BASE
0x48000000
// 关闭WATCHDOG,否则CPU会不断重启
void disable_watch_dog(void)
{
}
// 设置存储控制器以使用SDRAM
void memsetup(void)
{
}
// 将第二部分代码复制到SDRAM
void copy_2th_to_sdram(void)
{
}
// 设置页表
void create_page_table(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 | \
#define MMU_SECDESC_WB
(MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL |\
#define MMU_SECTION_SIZE
0x00100000
}
// 启动MMU
void mmu_init(void)
{
// ARM休系架构与编程
// 嵌入汇编:LINUX内核完全注释
__asm__(
}
====================================================================
leds.c源码:
// leds.c: 循环点亮4个LED
// 属于第二部分程序,此时MMU已开启,使用虚拟地址
#define GPFCON
(*(volatile unsigned long *)0xA0000050)
// 物理地址0x56000050
#define GPFDAT
(*(volatile unsigned long *)0xA0000054)
// 物理地址0x56000054
#define GPF4_out (1<<(4*2))
#define GPF5_out (1<<(5*2))
#define GPF6_out (1<<(6*2))
// wait函数加上“static inline”是有原因的,
// 这样可以使得编译leds.c时,wait嵌入main中,编译结果中只有main一个函数。
// 于是在连接时,main函数的地址就是由连接文件指定的运行时装载地址。
// 而连接文件mmu.lds中,指定了leds.o的运行时装载地址为0xB4004000,
// 这样,head.S中的“ldr pc, =0xB4004000”就是跳去执行main函数。
static inline void wait(unsigned long dly)
{
}
int main(void)
{
unsigned long i = 0;
GPFCON = GPF4_out|GPF5_out|GPF6_out; //将LED1,2,4对应的GPF4/5/6三个引脚设为输出
while(1){
wait(30000);
GPFDAT = (~(i<<4)); // 根据i的值,点亮LED1,2,4
if(++i == 8)
i = 0;
}
return 0;
}
====================================================================
Makefile文件;
objs := head.o init.o leds.o
mmu.bin : $(objs)
%.o:%.c
%.o:%.S
clean:
===================================================================
mmu.lds文件:
SECTIONS {
}