利用MMU透过访问虚拟内存来实现流水灯
实验目的:
利用MMU通过访问虚拟内存来实现流水灯
预备知识:
(1):熟悉协处理器的作用,CP15中的十六个寄存器,以及寄存器中的位格式和含义,相关的指令(MRC和MCR);
(关于协处理器的指令,在《arm体系结构与编程》这本书上说的比较好)
(2): 熟悉嵌入式汇编语法 (这方面的知识,在我上面的博客里已经有过介绍,这里不再赘述);
实验原理:
内存管理单元(Memory Management Unit)简称MMU。它的功能是:(1)将虚拟内存转化为物理内存;(2)设置访问权限;(3)设置Cache和Write Buffer。 若没有MMU,即程序只能直接使用物理内存,若几个应用程序同时运行,这样会导致内存泄漏,造成程序的混乱,同时有了MMU,对于CPU,其发出的虚拟地址将 会变大(对于arm9,CPU可以发出4G的虚拟内存地址) 。
在基于ARM的嵌入式应用系统中,存储系统通常是通过系统控制协处理器CP15完成的,在具体的各种存储管理机制中,可能还会用到其他的一些技术,如在MMU中除了CP15,还有使用页表和高速缓存(很重要,注意理解)等
在MMU中的地址变换中,有四种地址映射方法:段(section)、大页(large page)、小页(small page)、极小页(tiny page)。
虚拟地址到物理地址的索引,涉及到两种索引:一级页表索引、二级页表索引。
其中段用的一级页表索引,大页、小页和极小页用的的二级索引。
虚拟地址到物理地址的大概转换过程如下:
a. 根据给定的虚拟地址找到一级页表中的条目;
b.如果此条目是段描述符,则返回物理地址,转换结束;
c,否则如果此条目是二级页表描述符,继续利用虚拟地址在此二级页表中找到下一个条目;
d,如果这第二个条目是页描述符,则返回物理地址,转换结束;
e,其他情况出错。
补充:这里地址转换图我不在此补充,希望大家自己参考arm体系结构与编程,上面的图解已经很详细了。
注意:条目中的每个位的含义:
例如 在一级页表中描述符的格式(段的条目)
31 12 11 10 9 8 5 4 3 2 1 0
[section base adress| 00 00 0 0 0 ] [ AP ] 1 [ DOMAIN ] 1 C B 1 0
段的物理地址的基址 AP 规定怎样进行检查 domain规定对某个内存是否检查 C规定是否启动cache B规定是否启动Write Buffer ,最后两位(10)
代表的是段映射。 在一级页表中,最后两位01代表的是粗页条目, 11代表的是细页条目。
例如 在一级页表中描述符的格式(粗页的条目)
31 12 11 10 9 8 5 4 3 2 1 0
[Corse page table base address ] [ AP ] 1 [ DOMAIN ] 1 C B 0 1
二级页表的虚拟页表的基址
例如 在一级页表中描述符的格式(细页的条目)
31 12 11 10 9 8 5 4 3 2 1 0
[Fine page table base address ] [ AP ] 1 [ DOMAIN ] 1 C B 0 1
二级虚拟页表的基址
注意:在地址转换工程中,如有二级页表转换(粗页和细页中的条目是跟一级页表是差不多的)。通过MMU来实现虚拟内存的访问一般步骤:
(1)建表格;(2)将表格的基址(TTB)告诉MMU (即将TTP保存到CP15的寄存器C2中) (2)启动MMU
实验代码:第一部分代码:head.S 和 init.c主要是完成一些初始化化工作,第二部分代码:leds.c 是通过虚拟内存的访问,来实现点亮一个灯
head.S:
@*************************************************************************
@ File:head.S
@ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU,
@ 然后跳到SDRAM继续执行
@*************************************************************************
.text
.global _start
_start:
ldr sp, =4096 @ 设置栈指针,,以下都是C函数,调用前需要设好栈,将sp指向stepping的顶部,
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
@问题一:这里sp为什么是0xB4000000
@分析:SDRAM的物理内存地址范围是0x30000000~0x33ffffff,要将虚拟地址0xB0000000~0xBffffff映射到物理地址0x30000000~0x33ffffff
@CPU在执行第二块代码的时候,pc就在SDRAM内了,故将sp重设为指向SDRAM的顶部
@问题二:ldr pc, =0xB0004000 ,这里为什么是0xB0004000
@分析:本程序是使用段映射,只用到一级页表。32位CPU的虚拟地址空间达到4GB,一级页表中使用4096个描述符来表示这4GB空间
@(每个描述符对应1MB的虚拟地址空间)每个描述符占用4个字节,所以一级页表占16KB。本例使用SDRAM的开始16KB来存放一级页表。
@所以剩下的内存开始物理地址为0x30004000,然而在支持第二部分代码(即leds.c)的时候MMU已经启动,故应该使用虚拟内存.由于
@虚拟地址0xB0000000~0xBffffff映射到物理地址0x30000000~0x33ffffff,虚拟内存0xB0004000对应的物理内存是0x30004000.所以
@PC这个时候应该指向0xB0004000
init.c:
/*
* 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
* 将steppingstone中2048到4096空间中(leds.c就存在这一部分中)
*从mmu.lds中的AT(2048)中可以知道,第二部分代码leds.c从steppingstone处开始存放
*应该运行的位置从0xB0004000开始
*/
void copy_2th_to_sdram(void) //将steppingstone的2048~4096(不包括4096)的代码复制到SDRAM
{
//因为前64MB的内存被一级页表占用了
//段的一个条目是1M 对于cpu的4GB寻址范围,需要4GB/1M=4096个一级页表条目。 一个条目占4B 所以4096*4B=16KB。
//所以剩下的内存开始地址是0x30004000
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)
{
/*
* 用于段描述符的一些宏定义
*/
/*设置 例如 在一级页表中描述符的格式(段的条目)
31 12 11 10 9 8 5 4 3 2 1 0
[section base adress ] [ AP ] 1 [ DOMAIN ] 1 C B 1 0
段的物理地址的基址 AP 规定怎样进行检查
domain规定对某个内存是否检查 C规定是否启动cache B规定是否启动Write Buffer ,最后两位(10)
*/
#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后仍能运行第一部分的程序,(同时也是保证在启动MMU之前和启动MMU瞬间,虚拟地址连续并一致)
* 将0~1M的虚拟地址映射到同样的物理地址
*/
//建立第一个段条目:将虚拟地址映射到物理地址,这里虚拟和物理地址都是0开始,一样
// 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物理地址空间
*/
//建立第二个段条目:将虚拟地址0xA0000000映射到物理地址0x56000000
//不设置cache和write Buffer
virtuladdr = 0xA0000000;
physicaladdr = 0x56000000;
*(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \
MMU_SECDESC;
/*
* SDRAM的物理地址范围是0x30000000~0x33FFFFFF,
* 将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF上,
* 总共64M,涉及64个段描述符
*/
/ /建立若干个段条目:将虚拟地址0xA0000000映射到物理地址0x56000000
//设置cache和write Buffer
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;
// MRC和MCR中的讲解:在《ARM休系架构与编程》中
// 在我的博客中,嵌入式汇编已经讲解了
/*
*/
__asm__(
"mov r0, #0\n"
"mcr p15, 0, r0, c7, c7, 0\n" /* 使无效ICaches和DCaches */
/*在CP15中,第一个常数都是0,最后一个协处理寄存器和最后一个参数(option2)的组合决定了该命令的具体功能,
不过默认情况下是C0,0*/
"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 = 页表基址 */
/*这里的%0是引用第0个“占位符” “占位符”是输入变量和输出变量,其是这些变量时第几个由他们在输入和输出变量中的位置决定*/
/*入式汇编程序规定把输出和输入寄存器按统一顺序编号,顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为%0、%1···%9
/*从下面两个冒号开始,由于输出变量没有,按照从输出寄存器序列开始从左到右从上到下的原则,TTB是第0个参数*/
/*所以这里%0代表的就是0x30000000*/
"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) );
}
leds.c:
#define GPFCON (*(volatile unsigned long*)0xA0000050)
#define GPFDAT (*(volatile unsigned long*)0xA0000054)
/*
上面是要使用虚拟地址的
在执行led.c时,就启动MMU了,使用了虚拟地址,所以在对GPFCON和GPFDAT访问的时候,CPU要通过MMU。
*/
/*
* delay函数加上“static inline”是有原因的,
* 这样可以使得编译leds.c时,wait嵌入main中,编译结果中只有main一个函数。
* 于是在连接时,main函数的地址就是由连接文件指定的运行时装载地址。
* 而连接文件mmu.lds中,指定了leds.o的运行时装载地址为0xB4004000,
* 这样,head.S中的“ldr pc, =0xB4004000”就是跳去执行main函数。
* volatile 是防止该代码被优化
*/
static inline void delay (unsigned long loops)
{
__asm__ volatile (
"1:\n"
"subs %0, %1, #1\n" @loops=loops-1;
"bne 1b":"=r" (loops):"0" (loops));
}
/*
上面是个延时函数,再次涉及到嵌入式汇编的语法知识
即loops=loops
1:
subs loops,,loops,#1
bne 1b
// :"=r" (loops)
// :"0" (loops)
*/
int main()
{
unsigned long i=0;
while(1)
{
GPFCON=0x00000100;
for(i=0;i<3;i++)
{
delay(300000);
delay(300000);
GPFCON=GPFCON<<2;
GPFDAT=0x00000000;
}
}
return 0;
}
在未经本人允许的情况下,拒绝转载!!