出处:http://www.100ask.org/bbs/forum.php?mod=viewthread&tid=11580&fromuid=5490
正文黑色,代码蓝色,重点标红。 第4课 MMU实验,很多人反映看不懂里面的代码。没关系,这说明你很正常。如果光看视频就能把第4课的代码完全看懂,那你看视频也没什么意义了。
先说几个前提:
1,这一课对新手来说看不看其实关系不大,以后用到的地方也很少;
2,再次强调,光看视频就想把嵌入式Linux学好是不可能的,牵涉到的很多技术、原理、协议需要自己通过查找资料来弄懂,视频只能引你入门;
3,在阅读这篇帖子之前,强烈建议把第4课视频、代码再看一遍,带着自己的问题来看这篇帖子!
4,看完这篇帖子之后,强烈建议把第4课的代码看一遍,并自己写出来,修改里面的映射关系,能够做到这一点说明你至少明白了虚拟地址是如何映射到物理地址的。
先说下参考资料: 《ARM 嵌入式系统开发——软件设计与优化》,英文原版的名字是"ARM System Developer's
Guide: Designing and Optimizing System
Software"。建议买一本来看看,尊重知识版权!相关内容在第14章。
本文分为3部分:
1,What? 是什么
2,How? 如何映射
3,Code 。代码分析
会穿插其中讲,最终的目的是能够分析MMU裸板代码。
What is MMU?
MMU,Memory Management Unit,内存管理单元(存储器管理单元)。还有个东西叫做MPU,Memory
Protection
Unit,内存保护单元(存储器保护单元)。MMU可以把它看做是MPU加上虚拟存储器的功能,MPU不是这篇文章的重点。虚拟存储器说的通俗点就是管理
虚拟地址空间,并负责把这些虚拟地址映射到物理地址。这些概念可以去看上文提到的那本书,而且一定要看:为什么要有MMU?为什么不直接访问物理地
址?MMU跟多任务有什么关系?这篇文章的重点是如何完成虚拟地址到物理地址的映射。
虚拟地址------->MMU-------->物理地址
为什么使用0xA0000050这个地址就能够访问GPFCON寄存器,而GPFCON寄存器的物理地址是0x56000050?
为什么代码中创建页表的语句看上去这么奇怪:
*(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) |
MMU_SECDESC_WB;
MMU_SECDESC_WB......这些宏是什么意思?
How?
如何映射?话说很久很久以前......其实我也不知道如何来组织这篇文章的逻辑,非常头疼。
ARM MMU有2类映射方式:一级(L1)、二级(L2)。
并且可以细分为3种映射方式,一级映射类型说的是段页表,二级映射类型里面又分细页表和粗页表。看到这里是不是越来越晕了?
我们只研究最简单的映射方式:一级映射。也就是说从虚拟地址到物理地址只经过了1级转换,而二级映射比这个复杂的多。
这里说到了页表,什么是页表?什么是页表项呢?相信每个人都看过书,拿到书之后要先看目录。“第一章 第1节 XXXXXXX
.................... 1”、“第三章 第2节 XXXXXXX ....................
88”......读者通过查找目录里面的这些条目就能够找到自己所关心的内容的章节在那一页。这些包含了章节名称并附有页码的条目就相当于页表项,所有的这些条目就构成了页表。虚拟地址到物理地址就通过一个个页表项来映射。
那么我们这里要说的段页表有多少项、每一项又有多大呢?
段页表一共有4096项,总共映射4GB大小的地址空间;每一个页表项本身是4字节大小,映射1MB的地址空间。那么一个段页表本身就需要4096*4=16KB大小的空间来存储。
下面来说说页表项的基本格式。
“一个段页表项指向一个1MB的存储段。页表项的高12位代替虚拟地址的高12位来产生物理地址。段页表项还包含域属性、cache属性、缓冲器属性和访问权限属性......”这段话摘抄自上文提到的书。 ①
Section entry:段页表项
Base address:基地址
Domain:域
SBZ: should be zero
从上图可以看到,1个页表项由32位构成,所以1个页表项本身是4个字节大小。这里高12位代表的基地址其实就是真实物理地址的高12位(见 ①)。
Bit19~Bit12都为0,所以是SBZ。
AP位决定了该页表项代表的虚拟空间的访问权限由Bit11~Bit10来表示:
Privileged mode:特权模式,又叫系统模式;
User mode:用户模式;
以上这两种模式是ARM7种工作模式中的2种(连这个都不知道的话......)。
System bit、Rom bit分别由CP15:C1的bit8和bit9来决定(见书的P494)。 Domain用于选择域,一共16个域,所以用Bit5~Bit8一共4个bit来选择。
C、B用来表示cache和写缓冲器的属性。
最低两位用来表示段页表项、粗页表、细页表、错误项。
页表项的格式介绍只需要了解个大概,其中只需要记住:页表项的高12位就是物理地址的高12位!
上文我们提到了,虚拟地址到物理地址的映射是通过一个个的页表项来完成的,而且页表项的高12位就是物理地址的高12位,那剩下的20位呢?
该来的总会来的!
这张图是用手机从书上拍下来的。
下面来分析这张图:
32位的虚拟地址被分解成了图中上方的“基值”、“偏移值”。至于基值为什么用12位来表示,偏移值用20位来表示,现在先不解释。
图片的中间部分,L1 主页表由第0~第4095一共4096个页表项构成。
最下方32位的物理地址也被分解成了“基值”、“偏移值”。看到“偏移值”上方的箭头了吗?知道这个箭头是什么意思嘛?
虚拟地址的低20位就是物理地址的低20位!
可能有人不明白物理地址“基值”上方的箭头以及从虚拟地址的“基值”指向页表项的箭头是什么意思。
虚拟地址的“基值”一共12位,用来索引4096个页表项,4096等于2的12次方。这句话可能还有人没看懂。
之前我们说过,虚拟地址的低20位就是物理地址的低20位,而页表项的高12位其实就是物理地址的高12位,那么我们怎么通过虚拟地址找到这个页表项呢?
---->通过虚拟地址的高12索引L1页表中的4096个页表项。
到现在为止我们知道了:由页表项的高12位构成了物理地址的高12位,由虚拟地址的低20位构成了物理地址的低20位。
到现在为止还有一个问题:这4096个页表项放在哪里?我拿着虚拟地址的高12位要去找页表项,上哪儿去找?你总得把第0个页表项的地址告诉我吧?(这几个问题问的真好!)
图中的“转换表基地址”就记录了第0个页表项的地址,也就是页表的基址。translation table base address,简称ttb。这个地址由CP15:C2寄存器来保存,所以需要写这个寄存器来告知ttb的基地址。
下面是写CP15:C2寄存器的代码:
各位,眼熟吗?
下面来梳理一遍映射的全部逻辑:
1,MMU首先知道你创建的这些页表项都放在哪里;
2,MMU通过虚拟地址的高12位可以找到对应的页表项,并且这个页表项的高12位就是物理地址的高12位;
3,虚拟地址的低20位就是物理地址的低20位;
4,页表项的高12位+虚拟地址的低20位==物理地址。
请把以上4点逻辑看懂。
下面来分析代码。
head.S
@*************************************************************************
@ 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
我们只分析里面的2条跳转语句:
bl create_page_table
bl mmu_init
bl create_page_table 跳转到create_page_table函数去创建页表,这个函数在init.c中。
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这两个寄存器的物理地址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;
}
}
经过一开始的页表项格式介绍,相信应该能够看懂create_page_table函数中的那几个宏了吧。
unsigned long *mmu_tlb_base = (unsigned long *)0x30000000;
这个地址是存放页表项的首地址,其实变量的名字应该改成mmu_ttb更好。
/*
* Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0,
* 为了在开启MMU后仍能运行第一部分的程序,
* 将0~1M的虚拟地址映射到同样的物理地址
*/
virtuladdr = 0;
physicaladdr = 0;
*(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) |
MMU_SECDESC_WB;
这条赋值语句过于简单,我就不侮辱大家的智商了。
/*
* 0x56000000是GPIO寄存器的起始物理地址,
* GPBCON和GPBDAT这两个寄存器的物理地址0x56000050、0x56000054,
* 为了在第二部分程序中能以地址0xA0000050、0xA0000054来操作GPFCON、GPFDAT,
* 把从0xA0000000开始的1M虚拟地址空间映射到从0x56000000开始的1M物理地址空间
*/
virtuladdr = 0xA0000000;
physicaladdr = 0x56000000;
*(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) |
MMU_SECDESC;
(virtualaddr >> 20 ) = A00 ②
(pysicaladdr & 0xFFF00000)| MMU_SECDESC = 0x56000c12 ③
由于mmu_tlb_base是unsigned long类型的变量,所以(mmu_tlb_base+(virtualaddr >> 20 ) )指向了(0x30000000+0xA00*4) = 0x30002800的地方 ④
综合②③④可以得到:
*(0x30002800) = 0x56000c12; ⑤
/*
* 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;
}
这个while循环一共创建了64个页表项(段描述符)。
第一次循环:
(virtuladdr >> 20) = 0xB00 ⑥
(physicaladdr & 0xFFF00000)| MMU_SECDESC_WB = (0x30000C1E) ⑦
(mmu_tlb_base + (virtuladdr >> 20)) = (0x30002C00) ⑧
综合⑥⑦⑧,可以得到:
*(0x30002c00) = 0x30000C1E; ⑨ 在创建了第一个页表项之后虚拟地址和物理地址都增加1MB大小,
第二个页表项就是像这样创建的:
*(0x30002c04) = 0x30100C1E;
第三个页表项就是像这样创建的:
*(0x30002c08) = 0x30200C1E;
......
第64个页表项是像这样创建的:
*(0x30002CFC) = 0x33F00C1E; ⑩
综合以上代码可以得到:
0x30000000是页表的首地址,也是存放第一个页表项的地址。
到这一步,页表项已经创建好了。
bl create_page_table 这句代码分析完毕
接下来是bl mmu_init,mmu_init函数也在init.c中实现。这里面的代码需要对协处理器非常了解,这部分内容也可以参考我推荐的那本书。
我只分析这3句代码:
unsigned long ttb = 0x30000000;
"mov r4, %0
" /* r4 = 页表基址 */
"mcr p15, 0, r4, c2, c0, 0
" /* 设置页表基址寄存器 */
第1步,把页表的首地址赋给一个变量ttb。
下面2句是嵌入汇编代码,ttb是这个函数中的第0个变量,用0%表示,那么mov r4, 0%这句话就不用再解释了吧。
然后这句:
mcr p15, 0, r4, c2, c0, 0
这句话把页表的首地址赋给了CP15:C2寄存器,各位,眼熟吗?
剩下的语句是对MMU的其他功能进行初始化,并且使能MMU。
从现在开始你就可以使用0xA0000050访问GPFCON寄存器了。
若有谬误,请指正:125707942@qq.com
转载请注明出处。
参考资料:
韦东山嵌入式Linux视频第一期第4课 MMU实验;
《ARM 嵌入式系统开发——软件设计与优化》/ "ARM System Developer's Guide: Designing and Optimizing System Software"
2014年9月30日