对技术执着
分类: 嵌入式
2015-03-14 14:04:47
原文地址:ARM学习笔记--内存管理单元MMU 作者:mutes
段地址转换过程
0b11:细页表(Fine page table)
[31:12] 为细页表基址(Fine page table base address),此描述符的低12位填充0后,就是一个二级页表的物理地址。此二级页表含1024个条目(使用[11:2],10位),其中每个条目表示大小1kb的物理地址空间,一个细页表表示1MB物理地址空间
以大页(64KB),小页(4KB)或极小页(1KB)进行地址映射时,需要用到二级页表,二级页表有粗页表、细页表两种,二级页表描述符格式如下:
大页的地址转换过程(大页描述符保存在粗页表中)
0b10:小页描述符
[31:12] 为小页基址(Small page base address),此描述符的低12位填充0后就是一块4kb([11:0],一共12位,2^12=4096)物理地址空间的起始地址。粗页表中每个条目表示4kb的物理空间,如果小页描述符保存在粗页表中,则只需要用一个条目来保存一个小页描述符。类似的,细页表中每个条目只能表示1kb的物理空间,如果小页保存在细页表中,则连续4个条目都保存同一个小页描述符。
下面以保存在粗页表中的小页描述符为例,说明地址转换过程:
①页表基址[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到粗页表描述符
②取出粗页表描述符[31:10](即粗页表基址),它和MVA[19:12]组成一个低两位为0的32位物理地址,用这个地址找到小页描述符
③取出小页描述符的位[31:12](即小页基址),它和MVA[11:0]组成一个32位物理地址(即MVA对应的PA)
小页描述符保存在细页表中,地址转换过程和上面类似。
小页的地址转换过程(小页描述符保存在粗页表中)
0b11:极小页描述符
[31:10]为极小页基址(Tiny page base address),此描述符的低10位填充0后就是一块1KB物理地址空间的起始地址。极小页描述符只能保存在细页表中,用一个条目来保存一耳光极小页描述符
下面是极小页的地址转换过程:
①页表基址寄存器[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU通过这个地址找到细页表描述符
②取出细页表描述符[31:12](即细页表基址),它和MVA[19:10]组成一个低两位为0的32位物理地址,通过这个地址即可找到极小页描述符
③取出极小页描述符[31:10](即极小页基址),它和MVA[9:0]组成一个32位的物理地址(即MVA对应的PA)
极小页的地址转换过程(极小页描述符保存在粗页表中)
从段、大页、小页、极小页的地址转换过程可知
①以段进行映射时,通过MVA[31:20]结合页表得到一段(1MB)的起始物理地址,MVA[19:0]用来在段中寻址
②以大页进行映射时,通过MVA[31:16]结合页表得到一个大页(64KB)的起始物理地址,MVA[15:0]用来在小页中寻址
③以小页进行映射时,通过MVA[31:12]结合页表得到一个小页(4KB)的起始物理地址,MVA[11:0]用来在小页中寻址
④以极小页进行映射时,通过MVA[31:10]结合页表得到一个极小页(1KB)的起始物理地址,MVA[9:0]用来在极小页中寻址
2、内存的访问权限检查
它决定一块内存是否允许读、是否允许写。这由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,哪一个域
例如:
①段描述符中的“Domain”为0b0010,表示1MB内存属于域2,如果域访问控制寄存器的[5:4]等于0b00,则访问这1MB空间都会产生“Domain fault”异常,如果等于0b01,则使用描述符中的“Ap”位进行权限检查
② 粗页表中的“Domain”为0b1010,表示1MB内存属于域10,如果域访问控制寄存器的[21:20]等于0b01,则使用二级页表中的大页/小页描述符中的"ap3"、"ap2"、"ap1"、"ap0"位进行权限检查,如果等于0b11,则允许任何访问,不进行权限检查。
如下图:
AP | S | R | 特权模式 | 用户模式 | 说明 |
00 | 0 | 0 | 无访问权限 | 无访问权限 | 任何访问将产生“Permission fault”异常 |
00 | 1 | 0 | 只读 | 无访问权限 | 在超级权限下可以进行读操作 |
00 | 0 | 1 | 只读 | 只读 | 任何写操作将产生”Permission fault“异常 |
00 | 1 | 1 | 保留 | - | - |
01 | x | x | 读/写 | 无访问权限 | 只允许在超级模式下访问 |
10 | x | x | 读/写 | 只读 | 在用户模式下进行写操作将产生"Permission fault"异常 |
11 | x | x | 读/写 | 读/写 | 在所有模式下允许任何访问 |
xx | 1 | 1 | 保留 | - | - |
3、TLB的作用
从MVA到PA的转换需要访问多次内存,大大降低了CPU的性能,有没有办法改进呢?
程序执行过程中,用到的指令和数据的地址往往集中在一个很小的范围内,其中的地址、数据经常使用,这是程序访问的局部性。
由此,通过使用一个高速、容量相对较小的存储器来存储近期用到的页表条目(段、大页、小页、极小页描述符),避免每次地址转换都到主存中查找,这样就大幅提高性能。这个存储器用来帮助快速地进行地址转换,成为转译查找缓存(Translation Lookaside Buffers, TLB)
当 CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查,否则MMU 访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB中,下次再使用这个虚拟地址时就直接使用TLB用的描述符。
使用TLB需要保证TLB中的内容与页表一致,在启动MMU之前,页表中的内容发生变化后,尤其要注意。一般的做法是在启动MMU之前使整个TLB无效,改变页表时,使所涉及的虚拟地址对应的TLB中条目无效。
4、Cache的作用
同样基于程序访问的局部性,在主存和CPU通用寄存器之间设置一个高速的、容量相对较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,供CPU在一段时间内使用,对提高程序的运行速度有很大作用。这个cache一般称为高速缓存。
①写穿式(Write Through)
任一CPU发出写信号送到Cache的同时,也写入主存,保证主存的数据同步更新。优点是操作简单,但由于主存速度慢,降低了系统的写速度并占用了总线的时间。
②回写式(Write Back)
数据一般只写到Cache,这样可能出现Cache中的数据得到更新而主存中的数据不变(数据陈旧)的情况。此时可在Cache中设一个标志地址及数据陈旧的信息,只有当Cache中的数据被换出或强制进行”清空“操作时,才将原更新的数据写入主存响应的单元中,保证了Cache和主存中数据一致。
Cache有以下两个操作:
①”清空“(clean):把Cache或Write buffer中已经脏的(修改过,但未写入主存)数据写入主存
②”使无效“(Invalidate):使之不能再使用,并不将脏的数据写入主存。
S2C2440内置了指令Cache(ICaches)、数据Cache(DCaches)、写缓存(Write buffer),需要用到描述符中的C位(Ctt)和B位(Btt)
1)指令Cache(ICaches)
系统刚上电或复位时,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从主存中取指
2)数据Cache(DCaches)
与 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丢失。
通过下表可知DCaches和Write buffer在Ccr,Ctt,Btt各种取值下,如何工作,Ctt and Ccr 意为 Ctt与Ccr进行逻辑与后的值
Ctt and Ccr | Btt | DCaches、Write buffer 和主存的访问方式 |
0 | 0 |
Non-cached,non-buffered(NCNB) 读写数据时都是直接操作主存,并且可以被外设中止; 写数据时不使用Write buffer,CPU会等待写操作完成; 不会出现Cache命中 |
0 | 1 |
Non-Cached buffered(NCB) 读数据时都是直接操作主存; 不会出现Cache命中; 写数据时,数据线存入Write buffer,并在随后写入主存; 数据存入Write buffer后,CPU立即继续执行; 读数据时,可以被外设中止; 写数据时,无法被外设中止 |
1 | 0 |
Cached,write-through(写通)mode 读数据时,如果Cache命中则从Cache中返回数据,不读取主存; 读数据时,如果Cache丢失则从读主存中返回数据,并导致“linefill”的动作; 写数据时,数据先存入Write buffer,并在随后写入主存; 数据存入Write buffer后,CPU立即继续执行; 写数据时,如果Cache命中则新数据也写入Cache中; 写数据时,无法被外设中止 |
1 | 1 |
Cached,write-back(写回) mode 读数据时,如果Cache命中则从Cache中返回数据,不读取主存; 读数据时,如果Cache丢失则从读主存中返回数据,并导致“linefile”的动作; 写数据时,如果Cache丢失则将数据先存入Write buffer,存储完毕后CPu立即继续执行,这些数据在随后写入主存; 写数据时,如果Cache命中则在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
5、S3C2440 MMU、TLB、Cache的控制指令
S3C2440除了ARM920T的CPU核心外,还有若干个协处理器,用来帮助主CPu完成一些特殊功能。对MMU、TLB、Cache等的操作涉及到协处理器。
MRC //从协处理器获得数据,传给ARM920T CPU核心寄存器
MCR //数据从ARM920T CPU核心寄存器传给协处理器
{cond} //执行条件,省略时表示无条件执行
p# //协处理器序号
Rd //ARM920T CPU核心的寄存器
cn和cm //协处理器中的寄存器
其中,
二、MMU使用实例:地址映射
这个实例将开启MMU,并将虚拟地址0xA0000000-0xA0100000映射到物理地址0x56000000-0x56100000(GPBCON物理地址为0x56000010,GPBDAT物理地址为0x56000014),来驱动LED。
将虚拟地址0xB0000000-0xB3FFFFFF映射到物理地址0x30000000-0x33FFFFFF,在连接程序时,将一部分代码的运行地址指定为0xB0004000.
这个程序只使用一级页表,以段的方式进行地址映射,32位CPU虚拟地址空间达到4G,一级页表使用4096个描述符来表示4G空间(每个描述符对应 1MB),每个描述符占4字节,所以一级页表占16KB。这个程序使用SDRAM的开始16KB存放一级页表,所以剩下的内存开始地址就为 0x30004000,这个地址最终会对应虚拟地址0xB0004000(所以代码运行地址为0xB0004000)
程序分为两部分:第一部分的运行地址为0,它用来初始化SDRAM,复制第二部分的代码到SDRAM中(存放在0x30004000)、设置页表、启动MMU,最后跳到SDRAM中(地址0xB0004000),第二部分运行地址设为0xB0004000,用来驱动LED
先看连接文件mmu.lds
SECTIONS {
firtst 0x00000000 : { head.o init.o }
second 0xB0004000 : AT(2048) { leds.o }
}
程序分两个段:first和second。first由head.o和init.o组成,加载和运行地址都是0,second由leds.o组成,加载地址为2048,重定位地址为0xB0004000。
@*************************************************************************
@ File:head.S
@ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU,
@ 然后跳到SDRAM继续执行
@*************************************************************************
.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中继续执行第二部分代码
halt_loop:
b 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)
{
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]; //循环复制13个寄存器到内存控制器基址
}
/*
* 将第二部分代码复制到SDRAM
*/
void copy_2th_to_sdram(void)
{
unsigned int *pdwSrc = (unsigned int *)2048; //第二段代码加载地址2048
unsigned int *pdwDest = (unsigned int *)0x30004000; //0x30004000前放页表
while (pdwSrc < (unsigned int *)4096) //4kb最大4096
{
*pdwDest = *pdwSrc;
pdwDest++;
pdwSrc++;
}
}
/*
* 设置页表
*/
void create_page_table(void)
{
/*
* 用于段描述符的一些宏定义
*[31:20]段基址,[11:10]AP,[8:5]Domain,[3]C,[2]B,[1:0]0b10为段描述符
*/
#define MMU_FULL_ACCESS (3 << 10) /* 访问权限AP */
#define MMU_DOMAIN (0 << 5) /* 属于哪个域 Domain*/
#define MMU_SPECIAL (1 << 4) /* 必须是1 */
#define MMU_CACHEABLE (1 << 3) /* cacheable C位*/
#define MMU_BUFFERABLE (1 << 2) /* bufferable B位*/
#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 /*每个段描述符对应1MB大小空间*/
unsigned long virtuladdr, physicaladdr;
unsigned long *mmu_tlb_base = (unsigned long *)0x30000000; /*SDRAM开始地址存放页表*/
/*
* Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0,
* 为了在开启MMU后仍能运行第一部分的程序,
* 将0~1M的虚拟地址映射到同样的物理地址
*/
virtuladdr = 0;
physicaladdr = 0;
//虚拟地址[31:20]用于索引一级页表,找到它对应的描述符,对应于(virtualaddr>>20)
//段描述符中[31:20]保存段的物理地址,对应(physicaladdr & 0xFFF00000)
*(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; //右移20位就是1
physicaladdr += 0x100000; //右移20位就是1
}
}
/*
* 启动MMU
*/
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) );
}
最后是Makefile
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