嵌入式linux学习(五):内存管理单元MMU

简单介绍MMU

一、MMU介绍

1、什么是MMU(来自百度)

MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来 管理 虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址 映射 为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。
虚拟存储器:虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有 连续的可用的内存 (一个连续完整的地址空间),而实际上,它通常是被分隔成 多个物理内存碎片,还有部分暂时存储在 外部磁盘存储器 上,在需要时进行数据交换。

2、相关概念(地址范围、虚拟地址映射为物理地址 以及 分页机制)

任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为 地址范围 。这个范围的大小由CPU的位数决定,例如一个32位的CPU,它的地址范围是0~0xFFFFFFFF(4G),而对于一个64位的CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF (16E).这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为 虚拟地址空间 ,该空间中的某一个地址我们称之为 虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的 物理地址空间只是虚拟地址空间的一个子集。这里举一个最简单的例子直观地说明这两者,对于一台内存为256M的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),而物理地址空间范围是0x00000000~0x0FFFFFFF(256M)。
在没有使用虚拟存储器的机器上,地址被直接送到 内存总线 上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到 存储器管理单元MMU,把虚拟地址 映射为物理地址。
大多数使用虚拟存储器的系统都使用一种称为 分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是 页帧(frame).页和页帧的大小必须相同。在这个例子中我们有一台可以生成32位地址的机器,它的虚拟地址范围从0~0xFFFFFFFF(4G),而这台机器只有256M的物理地址,因此他可以运行4G的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放4G程序的 外部存储器(例如磁盘或是FLASH),以保证程序片段在需要时可以被调用。

映射关系
在这个例子中,页的大小为4K,页帧大小与页相同——这点是必须保证的,因为内存和外围存储器之间的传输总是以页为单位的。对应4G的虚拟地址和256M的物理存储器,他们分别包含了1M个页和64K个页帧

二、MMU功能和使用

1、MMU功能

1)权限管理

就windows而言,虽然它是一个多任务操作系统,但是某时某刻只能由一个程序执行。
比如在一个时间轴上,有A、B、C三个任务在进行,它们各自拥有独立的地址空间,如果B是恶意程序,那么权限管理使得B程序不能访问A程序或者C程序的内存。
地址映射:

2)地址映射

现代的多用户多进程操作系统,需要MMU,才能达到每个用户进程都拥有自己 独立的地址空间的目标。使用MMU,操作系统划分出一段地址区域,在这块地址区域中, 每个进程看到的内容都不一定一样。例如MICROSOFT WINDOWS操作系统将地址范围4M-2G划分为用户地址空间,进程A在地址0X400000(4M)映射了可执行文件,进程B同样在地址0X400000(4M)映射了可执行文件,如果A进程读地址0X400000,读到的是A的可执行文件映射到RAM的内容,而进程B读取地址0X400000时,则读到的是B的可执行文件映射到RAM的内容。这就是MMU在当中进行地址转换所起的作用。
这儿贴出一篇博客,对于MMU的地址映射有比较详细的讲解:https://blog.csdn.net/hzrandd/article/details/51028681

2、MMU在2440中的作用

对于CPU而言,它一般只关系两件事情,第一是发出地址,第二是读写数据。至于地址是虚拟地址还是物理地址,这都不是CPU所关心的事情。如果使用了MMU,则是虚拟地址,如果没有使用,那么就是物理地址。在写程序时链接的地址也是如此,没有虚拟地址和物理地址的概念,链接地址是以CPU的角度去看到的地址。

1)在MMU中虚拟地址与物理地址的对应(VA–PA)

在ARM中,VA和PA以表格形式(页表)形成一一对应关系。
以页表形式形成映射也分为几种映射方式,常用的有三种:页式、段式、段页式,这也是三种不同的内存管理方式。
段式(Section)内存管理: MINI2440的虚拟地址空间为2^32=4G。而在Section模式,这4G的虚拟空间被分成一个一个称为段(Section)的单位,每个段的长度是1M。4G的虚拟内存总共可以被分成4096个段(1M4096=4G),因此我们必须用4096个描述符来对这组段进行描述,每个描述符占用4个Byte,故这组描述符的大小为16KB(4byte4096),这4096个描述符构为一个表格,我们称其为Tralaton Table.

描述符结构图

上图是描述符的结构
Section base address:段基地址(相当于页框号首地址)
AP: 访问控制位Access Permission
Domain: 访问控制寄存器的索引。Domain与AP配合使用,对访问权限进行检查
C:当C被置1时为write-through (WT)模式
B: 当B被置1时为write-back (WB)模式(C,B两个位在同一时刻只能有一个被置1)

3、程序说明

目标:用虚拟地址点亮led。

@ File:head.S
@ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU,
@ 然后跳到SDRAM继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text
.global _start
_start:
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
halt_loop:
b halt_loop

链接脚本mmu.lds

1
2
3
4
SECTIONS { 
firtst 0x00000000 : { head.o init.o }
second 0xB0004000 : AT(2048) { leds.o }
}

作用:把程序分为两个段,第一个段的内容为head.oh和init.o,也就是head.s和init.c编译出来的两个文件都放到第一个段,第一个段的链接地址应该位于0地址处;第二个段的内容为leds.o,链接地址应该位于0xB0004000地址。其中AT(2048)指定这个段在编译出来的映像文件(mmu.bin)中的地址——加载地址

init.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
* 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)
{
WTCON = 0; // 关闭WATCHDOG很简单,往这个寄存器写0即可
}

/*
* 设置存储控制器以使用SDRAM
*/
void memsetup(void)
{
/* SDRAM 13个寄存器的值 */
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];
}

/*
* 将第二部分代码复制到SDRAM
*/
void copy_2th_to_sdram(void)
{
unsigned int *pdwSrc = (unsigned int *)2048;
unsigned int *pdwDest = (unsigned int *)0x30004000;

while (pdwSrc < (unsigned int *)4096)
{
*pdwDest = *pdwSrc;
pdwDest++;
pdwSrc++;
}
}

/*
* 设置页表
*/
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 | \
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_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寄存器的起始物理地址,
* GPBCON和GPBDAT这两个寄存器的物理地址0x56000010、0x56000014,
* 为了在第二部分程序中能以地址0xA0000010、0xA0000014来操作GPBCON、GPBDAT,
* 把从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;
}
}

/*
* 启动MMU
*/
void mmu_init(void)
{
unsigned long ttb = 0x30000000;

// ARM休系架构与编程
// 嵌入汇编:LINUX内核完全注释
__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) );
}

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
objs := head.o init.o leds.o

mmu.bin : $(objs)
arm-linux-ld -Tmmu.lds -o mmu_elf $^
arm-linux-objcopy -O binary -S mmu_elf $@
arm-linux-objdump -D -m arm mmu_elf > 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_elf mmu.dis *.o

总结一下上面的程序:
1)从NAND FLASH启动
2)初始化后复制代码到SDRAM中
3)设置页表
4)开启MMU
5)执行第二部分代码