JZ2440 裸机驱动 第7章 内存管理单元MMU
本章目标:
了解虚拟地址和物理地址的关系;
掌握如何通过设置MMU来控制虚拟地址到物理地址的转化;
了解MMU的内存访问权限机制;
了解TLB、Cache、Write buffer的原理,使用时的注意事项;
通过实例深刻掌握上述要点;
7.1 内存管理单元MMU介绍
7.1.1 S3C2410/S3C2440 MMU特性
内存管理单元(Memory Management Unit),简称MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。现代的多用户进程操作系统通
过MMU使得各个用户进程都拥有自己独立的地址空间:地址映射功能使得各进程拥
有“看起来”一样的地址空间,而内存访问权限的检查可以保护每个进程所用的内存不
会被其他进程破坏。
S3C2410/S3C2440有如下特性:
① 与ARM V4兼容的映射长度、域、访问权限检查机制;
② 4种映射长度:段(1MB)、大页(64KB)、小页(4KB)、极小页(1KB);
③ 对每段都可以设置访问权限;
④ 大页、小页的每个子页(sub-page,即被映射页的1/4)都可以单独设置访问权限;
⑤ 硬件实现16个域;
⑥ 指令TLB(含64个条目),数据TLB(含64个条目);
⑦ 硬件访问页表(地址映射、权限检查由硬件自动进行);
⑧ TLB中条目的替换采用round-robin算法(也称cyclic算法);
⑨ 可以使无效整个TLB;
⑩ 可以单独使无效某个TLB条目;
⑪ 可以在TLB中锁定某个条目,指令TLB、数据TLB互相独立。
本章重点在于地址映射;页表的结构与建立、映射的过程,对于访问权限、TLB、
Cache只粗略介绍。
7.1.2 S3C2410/S3C2440 MMU地址变换过程
1.地址的分类
以前的程序是非常小的,可以全部装入内存中。随着技术的发展,出现以下两种情况。
(1)有的程序很大,它所要求的内存空间超过了内存总容量,不能一次性装入内存;
(2)多道系统中有很多程序需要同时执行,它们要求的内存空间超过了内存总容量,
不能把所有程序全部装入内存。
实际上,一个程序在运行之前,没有必要全部载入内存,而仅需要将那些当前需要运行
的部分先装入内存,其余部分在用到时再从磁盘调入,而当内存耗光时再将暂时不同的部
分调出到磁盘。这使得一个大程序可以在较小的内存空间中运行,也使得内存中可以同时
装入更多的程序并发执行,从用户的角度看,该系统所具有的内存容量将比实际内存容量
大得多,人们把这样的存储器称为虚拟存储器。
虚拟存储器从逻辑上对内存容量进行了扩充,用户看到的大容量只是一种感觉,是虚的,
在32位的CPU系统中,这个虚拟内存地址范围为0~0xFFFF FFFF,我们把这个地址范围称
为虚拟地址空间,其中某个地址称为虚拟地址。与虚拟地址空间、虚拟地址对应的是物理地
址空间、物理地址,他们对应实际的内存。
虚拟地址最终需要转换为物理地址才能读写实际的数据,这通过将虚拟地址空间、物理
地址空间划分为同样大小的一块块小空间(称为段或页),然后为这两类小空间建立映射关系。
由于虚拟地址空间远大于物理空间,有可能多块虚拟地址空间映射到同一块物理地址空间。
或者有些虚拟地址空间没有映射到具体的物理地址空间上去(可以在使用到时在映射)。如图
7.1所示为这些映射关系。
ARM CPU的地址转换过程涉及3个概念:虚拟地址(VA, Virtual Address)、变换
后的虚拟地址(MVA,Modified Virtual Address)、物理地址(PA,Physical Address)。
没有启动MMU时,CPU核、cache、MMU、外设等所有部件使用的都是物理地址。
启动MMU后,CPU核对外发出虚拟地址:VA;VA被转换为MVA供cache、MMU使
用,在这里MVA被转换为PA;最后使用PA读写实际设备(S3C2410/S3C2440内部寄存
器或外接的设备):
(1)CPU核看到的、用到的只是虚拟地址VA,至于VA如何最终落实到物理地址PA
上,CPU不理会。
(2)cache和MMU也看不见VA,它们利用由MVA转换得到PA。
(3)实际设备看不到VA,MVA,读写它们时使用的是物理地址PA。
MVA是除CPU核外的其他部分看见的虚拟地址,VA和MVA之间的变换关系如图7.2
所示。
如果VA < 32M,需要使用进程标识号PID(通过读CP15的C13获得)来转换为MVA。
VA和MVA转换方法如下(这是硬件自动完成的):
if(VA < 32M) then
MVA = VA | (PID << 25)
else
MVA = VA
利用PID生成MVA的目的是为了减少切换进程时的代价:
不使用MVA而直接使用VA的话,当两个进程所用的虚拟地址空间(VA)有重叠时,
在切换进程时为了把重叠的VA映射到不同的PA上去,需要重建页表、使无效caches和
TLBS等,代价非常大。使用MVA后,进程切换就省事多了。
假设两个进程1、2运行时的VA都是0~(32M-1),但是它们的MVA并不重叠,分别
是0x0200 0000~0x03ff ffff、0x0400 0000~0x05ff ffff,这样就不必进行重建页表等工作了。
下面说道的虚拟地址,若没有特别指出,就是指MVA。
2.虚拟地址到物理地址的转换过程
将一个虚拟地址转换为物理地址,一般有两种方法:用一个确定的数学公式进行转换或用表格存储虚拟地址对应的物理地址。这类表格称为页表(Page table),页表由一个个条目(Entry)
组成;每个条目存储了一段虚拟地址对应的物理地址及其访问权限,或者下一级页表的地址。
在ARM CPU中使用第二种方法。
S3C2410/S3C2440最多会用到两级页表:
以段(Section,1MB)的方式进行转换时只用到一级页表;
以页(Page,大页(64KB)、小页(4KB)、极小页(1KB))的方式进行转换时用到两级页表。
条目也称为“描述符”(Descriptor),有段描述符、大页描述符、小页描述符、极小页描述符
——它们保存段、大页、小页或极小页的起始物理地址;粗页表描述符、细页表描述符——它
们保存二级页表的物理地址。
大概的转换过程如下:请参考图7.3(通用转换过程)、图7.4(针对ARM CPU细化的转换过程):
(1)根据给定的虚拟地址找到一级页表中的条目;
(2)如果此条目是段描述符,则返回物理地址,转换结束;
(3)否则如果是二级页表描述符,继续利用虚拟地址在此二级页表中找到下一个条目;
(4)如果这第二个条目是页描述符,则返回物理地址,转换结束;
(5)其他情况出错。
图7.3/7.4中“TTB base”代表一级页表的地址,将它写入协处理器CP15的寄存器C2
(称为页表基址寄存器)即可,如图7.5所示,一级页表的地址必须是16K对应的(位[14:0]
为0)。
现在先介绍一级页表。32位CPU的虚拟地址空间达到4GB,一级页表中使用4096个描
述符来表达着4GB空间——每个描述符对应1MB的虚拟地址,要么存储了它对应的1MB
物理空间的起始地址,要么存储了下一级页表的地址。使用MVA[31:20]来索引一级页表,
得到一个描述符,每个描述符占据4字节,格式如图7.6所示。
根据一级描述符的最低两位,可分为以下4种。
(1)0b00:无效。
(2)0b01:粗页表(Coarse page table)。
位[31:10]称为粗页表基地址(Coarse page table base address),此描述符的低10位填充0
后就是一个二级页表的物理地址。此二级页表含256个条目(所以大小为1KB),称为粗页
表(Coarse page table,见图7.4)。其中每个条目表示大小为4KB的物理地址空间,所以一
个粗页表表示1MB的物理空间。
(3)0b10:段(Section)。
位[31:20]称为段基址(Section base),此描述符的低20位填充0后就是一块1MB物理
地址空间的起始地址。MVA[19:0]用来在这1MB空间中寻址。所以,描述符的位[31:20]和
MVA[19:0]就构成了这个虚拟地址:MVA对应的物理地址。
以段的方式进行映射时,虚拟地址MVA到物理地址PA的转换过程如下(参考图7.7)。
① 页表基址寄存器位[31:14]和MVA[31:20]组成一个低2位为0的32位地址,MMU利
用这个地址找到段描述符。
② 取出段描述符的位[31:20]——即段基址,它和MVA[19:0]组成一个32位的物理地
址——这就是MVA对应的PA
(4)0b11:细页表(Fine page table)。
位[31:12]称为细页表基址(Fine page table base address),此描述符的低12位填充0后
就是一个二级页表的物理地址。此二级页表含1024个条目(所以大小为4KB),称为细页表
(Fine page table,如图7.4所示)。其中每个条目表示大小为1KB的物理地址空间,所以一
个细页表表示1MB的物理地址空间。
以大页(64KB)、小页(4KB)或极小页(1KB)进行地址映射时,需要用到两级页表。
二级页表有粗页表、细页表两种,图7.4中“Coarse page table”和“Fine page table”就是这
两种页表。二级页表中描述符的格式如图7.8所示。
根据二级描述符的最低两位,可分为以下4种情况。
(1)0b00:无效
(2)0b01:大页描述符。
位[31:16]称为大页基址(Large page base address),此描述符的低16位填充0后就是一
个64KB物理地址空间的起始地址。粗页表中每个条目只能表示4KB的物理空间,如果大页
描述符保存在粗页表中,则连续16(4KB*16 = 64KB)个条目都保存同一个大页描述符。类似
的,细页表中每个条目只能表示1KB的物理空间,如果大页描述符保存在细页表中,则连续
64(1KB*64 = 64KB)个条目都保存同一个大页描述符。
下面以保存在粗页表中的大页描述符为例,说明地址转换的过程(参考图7.9)。
① 页表基地址寄存器位[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利
用这个地址找到粗页表描述符。
② 取出粗页表描述符的位[31:10]——即粗页表基址,它和MVA[19:12]组成一个低两位
为0的32位物理地址——据此即可找到大页描述符。
③ 取出大页描述符的位[31:16]——即大页基址,它和MVA[15:0]组成一个32位的物理
地址——这就是MVA对应的PA。
上面步骤②和③中,用于在粗页表中索引的MVA[19:12]、用于在大页内寻址的MVA
[15:0]有重合的位:位[15:12]。当[15:12]从0b0000变为0b1111时,步骤②返回的大页描
述符相同——所以,粗页表中连续16个条目都保存同一个大页描述符。
大页描述符保存在细页表中时,地址转换过程与上面类似,如图7.9所示,详细过程不
再赘述。
(3)0b10:小页描述符。
位[31:12]称为小页基址(Small page base address),此描述符的低12位填充0后就是一
块4KB物理地址空间的起始地址。粗页表中每个条目表示4KB的物理空间,如果小页描述符
保存在粗页表中,则只需要用一个条目来保存一个小页描述符。类似的,细页表中每个条目
只能表示1KB的物理空间,如果小页描述符保存在细页表中,则连续4个条目都保存同一个
小页描述符。
下面以保存在粗页表中的小页描述符为例,说明地址转换过程(参考图7.10)。
① 页表基址寄存器位[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利
用这个地址找到粗页表描述符。
② 取出粗页表描述符的位[31:10]——即粗页表基址,它和MVA[19:12]组成一个低两
位为0的32位物理地址——据此即可找到小页描述符。
③ 取出大页描述符的[31:12]——即小页基址,它和MVA[11:0]组成一个32位的物理地
址——这就是MVA对应的PA。
小页描述符保存在细页表中时,地址转换过程与上面类似,不再赘述。
(4)0b11:极小页描述符。
位[31:10]称为极小页基址(Tiny page base address),此描述符的低10位填充0后就是一
块1KB物理地址空间的起始地址。极小页描述符只能保存在细页表中,用一个条目来保存
一个极小页描述符。
下面是极小页的地址转换过程(如图7.11所示)。
① 页表基址寄存器[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利
用这个地址找到细页表描述符。
② 取出细页表描述符的位[31:12]——即细页表基址,它和MVA[19:10]组成一个低
两位为0的32位物理地址——据此即可找到极小页描述符。
③ 取出极小页描述符的位[31:10]——即极小页基址,它和MVA[9:0]组成一个32位
的物理地址——这就是MVA对应的PA。
从段、大页、小页、极小页的地址转换过程可知。
(1)以段进行映射时,通过MVA[31:20]结合页表得到一段(1MB)的起始物理地址,
MVA[19:0]用来在段中寻址。
(2)以大页进行映射时,通过MVA[31:16]结合页表得到一个大页(64KB)的起始物理地址,
MVA[15:0]用来在大页中寻址。
(3)以小页进行映射时,通过MVA[31:12]结合页表得到一个小页(4KB)的起始物理地址,
MVA[11:0]用来在小页中寻址。
(4)以极小页进行映射时,通过MVA[31:10]结合页表得到一个极小页(1KB)的起始物理地址,
MVA[9:0]用来在极小页中寻址。
7.1.3 内存的访问权限检查
内存的访问权限检查是MMU的主要功能之一,简单地说,他就是决定一块内存是否允许读写。这由CP15寄存器C3(域访问控制)、描述符的域(Domain)、CP15寄存器C1的R/S/A
位、描述符的AP位等联合作用。
CP15寄存器的C1中的A位表示是否对地址进行对齐检查。所谓对齐检查就是,访问字
CP15寄存器的C1中的A位表示是否对地址进行对齐检查。所谓对齐检查就是,访问字
(4字节的数据)时,地址是否为4字节对齐,访问半字(2字节的数据)时地址是否2字节对齐,
如果地址不对齐则产生“Alignment fault”异常。无论MMU是否被开启,都可以进行对齐检
查。CPU读取指令时不进行对齐检查,以字节为单位访问时也不进行对齐检查。对齐检查
在MMU的权限检查、地址映射前进行。
内存的访问权限检查可以概括为以下两点。
(1)“域”决定是否对某块内存进行权限检查。
(2)“AP”决定如何对某块内存进行权限检查。
如图7.12所示,S3C2410/S3C2440有16个域,CP15寄存器C3中每两位对应一个域,
用来表示这个域是否进行权限检查。图7.12表示CP15寄存器C3中哪两位对应哪个域,表
7.1给出了CP15寄存器C3中这些“两位的数据”的含义。
图7.13中的“Domain”占据4字节,用来表示这块内存属于上面定义的16个域中国的哪
一个。举例如下:
(1)段描述符中“Domain”为0b0000时,表示这1MB内存属于域0,如果域访问控制寄
存器的位[1:0]等于0b00,则访问这1MB空间时都会产生”Domain fault“的异常;如果域访
问控制寄存器的位[1:0]等于0b11,则使用描述符中的“AP”位进行权限检查。
(2)粗页表中的“Domain”为0b1111时,表示这1MB内存属于域15,如果域访问控制器
的位[31:30]为0b0b00,则访问这1MB空间时都会产生“Domain fault”的异常;如果域访问
控制寄存器的位[31:30]为0b11,则使用二级页表中的大页/小页描述符中的"ap3"、“ap2”、
“ap1”、“ap0”位进行权限检查。
图7.13中的“AP”、"ap3"、“ap2”、“ap1”、“ap0”结合CP15寄存器C1的R/S位,决定如何
进行访问权限检查。
首先说明,段描述符中的“AP”控制整个段(1MB)的访问权限;
大页描述符中的每个“apx”(x为0~3)控制一个大页(64KB)中1/4内存的访问权限,即“ap3”
对应大页高端的16KB;“ap0”对应大页低端的16KB;小页描述符与大页描述符相似,每个
“apx”控制一个小页(4KB)的1/4内存的访问权限;极小页中的"ap"就控制整个极小页(1KB)
的访问权限。
如表7.2所示,AP位、S位和R位的组合,可以产生多种访问权限。需要指出的是,ARM
CPU有7中工作模式,其中6种属于特权模式,1种属于用户模式。在特权模式和用户模式下,
相同的AP位、S位和R位的结合,其访问权限也不相同。
7.1.4 TLB的作用
从虚拟地址到物理地址的转换过程可知:
使用一级页表进行地址转换时,每次读/写数据需要访问两次内存,第一次访问一级页
表获得物理地址,第二次才是真正的读/写数据;
使用二级页表时,每次读/写数据需要访问3次内存,访问两次页表(一级页表和二级页
表)获得物理地址,第三次才是真正的读/写数据。
上述的地址转换过程大大降低了CPU的性能,有没有办法改进呢?
程序执行过程中,所用到的指令、数据的地址往往集中在一个很小的范围内,其中的地址、
数据经常多次使用,这称为程序访问的局限性。由此,通过使用一个高速、容量相对较小的
存储器来存储近期用到的页表条目(段、大页、小页、极小页描述符),以避免每次地址转换时
都到主存去查找,这样可以大幅度地提高性能。这个存储器用来帮助快速地进行地址转换,
称为“转译查找缓存”(Translation Lookaside Buffers,TLB)。
当CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的
描述符,则直接利用此描述符进行地址转换或权限检查;否则MMU访问页表找到描述符后再
进行地址转换和权限检查,并将这个描述符填入TLB中(如果TLB已满,则利用rount-robin算
法找到一个条目,然后覆盖它),下次再使用这个虚拟地址时就可以直接使用TLB中的描述符
了。
使用TLB需要保证TLB中的内容与页表一致,在启动MMU之前、在页表中的内容发生
变化后,尤其要注意这点。S3C2410/S3C2440可以使无效(Invalidate)整个TLB,或者通
过某个虚拟地址使无效TLB中的某个条目。一般的做法是:在启动MMU之前使无效整个
TLB,改变页表时,使无效所涉及的虚拟地址对应的TLB中的条目。
7.1.5 Cache的作用
同样基于程序访问的局限性,在主存和CPU通用寄存器之间设置一个高速、容量相对
较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,
供CPU在一段时间内使用,这对提高程序的运行速度有很大的作用。这个介于主存和CPU
之间的高速小容量存储器称为高速缓冲存储器(Cache)。
启用Cache后,CPU读取数据时,如果Cache中有这个数据的副本,则直接返回,否则
从主存中读入数据,并存入Cache中,下次再使用(读/写)这个数据时,可以直接使用Cache
中的副本。
启用Cache后,CPU写数据时有写穿式和回写式两种方式。
(1)写穿式(Write Through)。
任一从CPU发出的写信号送到Cache的同时,也写入主存,以保证主存的数据能同步地更新。
它的优点是操作简单,但由于主存的慢速,降低了系统的写速度并占用了总线的时间。
(2)回写式(Write Back)。
为了克服写穿式中每次数据写入时都要访问主存,从而导致系统写速度降低并占用总线
时间,尽量减少对主存的访问次数,又有了回写式。
它是这样工作的:数据一般只写到Cache,这样有可能出现Cache中的数据得到更新而
主存中的数据不变(数据陈旧)的情况。但此时可在Cache中设一标志地址和数据陈旧的信息,
只有当Cache中的数据被换出或强制进行“清空”操作时,才将原更新的数据写入主存相应的
单元中。这样保证了Cache和主存中的数据保持一致。
先介绍Cache的两个操作。
(1)“清空”(clean):把Cache或Write Buffer中已经脏的(修改过,但未写入主存)数据写入
主存。
(2)“使无效”(Invalidate):使之不能再使用,并不将脏的数据写入主存。
S3C2410/S3C2440内置了指令Cache(ICaches)、数据Cache(DCaches)、写缓存(Write
buffer)。下面的内容需要用到页表中描述的C、B位,为了方便读者,先把这些描述符用
图7.14表示出来。下文中,描述符的C位称为Ctt,B位称为Btt。
1.指令Cache(ICaches)
ICaches的使用比较简单。系统刚上电或复位时,ICaches中的内容是无效的,并且ICaches
功能是关闭着的。往Icr位(即CP15协处理器中寄存器1)写1可以启动ICaches,写0可以停止ICaches。
ICaches一般在MMU开启之后被使用,此时页表中描述符的C位(称为Ctt)用来表示一段内存
是否可以被Cache。若Ctt = 1,则允许Cache,否则不允许被Cache。但是,即使MMU没有开
启,ICaches也是可以被使用的,这时CPU读取指令(以后简称“取指”)时所涉及的内存都被当做
是允许Cache的。
ICaches被关闭时,CPU每次取指都要读取主存,性能非常低。所以通常尽早启动ICaches。
ICaches被打开时,CPU每次取指都会先在ICache中查看是否能找到所要的的指令,而不管
Ctt是0还是1。如果找到了,称为Cache命中(Cache hit);如果找不到,称为Cache缺失(Cache
miss)。ICaches被开启后,CPU的取值分为如下3种情况。
(1)Cache命中且Ctt为1时,从ICache中取出指令,返回CPU;
(2)Cache缺失且Ctt为1时,CPU从主存中读出指令。同时,一个称为“8-word linefill”的动作
将发生,该动作把该指令所处区域的8个word写进ICache的某个条目中。这有可能会覆盖某个条
目,可以使用Pseudo-random算法或round-robin算法在ICaches中选出某个没有被锁定的条目。
可以通过CP15协处理器中的寄存器1的第14位来选择使用哪种算法。
(3)Ctt为0时,CPU从主存中读出指令。
2.数据Cache(DCaches)
与ICaches相似,系统刚上电或复位时,DCaches中内容也是无效的,并且DCaches功能也是
关闭着的,而Write buffer中的内容也是被废弃不用的。往Ccr位(即CP15协处理器中寄存器1的第
2位)写1可以启动DCaches,写0可以停止DCaches。Write buffer与DCaches紧密结合,没有专门
的控制位来开启、停止它。
与ICaches不同,DCaches功能必须在MMU开启之后才能被使用,因为开启MMU之后,才能
使用页表中的描述符来定义一块内存如何使用DCaches和Write buffer。
DCaches被关闭时,CPU每次读写数据都要操作主存,DCaches和Write buffer被完全忽略。
DCaches被开启后,CPU每次读写数据时都会先在DCaches中查看是否能找到所要的数据,
而不管Ctt是0还是1,。如果找到了,称为Cache命中(Cache hit);如果找不到,称为Cache缺失
(Cache miss)。
通过表7.3可以知道DCaches和Write buffer在CCr、Ctt、和Btt的各种取值下,如何工作。
表中“Ctt and Ccr”一项里面的值是Ctt和Ccr进行逻辑与之后的值(Ctt && Ccr)。
与TLB类似,使用Cache时需要保证Cache、Write buffer的内容和主存内容保持一致,
需要遵守如下两个原则:
(1)清空DCaches,使得主存数据得到更新;
(2)使无效ICaches,使得CPU取值时重新读取主存。
在实际编写程序时,要注意如下几点:
(1)开启MMU前,使无效ICaches、DCaches和Write buffer;
(2)关闭MMU前,清空ICaches、DCaches,即将“脏”数据写到主存上;
(3)如果代码有变,使无效ICaches,这样CPU取值时会重新读写主存;
(4)使用DMA操作可以被Cache的内存时:
将内存的数据发送出去时,要清空Cache;
将内存的数据读入时,要使无效Cache。
(5)改变页表中的地址映射关系时也要慎重考虑;
(6)开启ICaches或DCaches时,要考虑ICaches和DCaches中的内容是否与主存保持一致;
(7)对于I/O地址空间,不使用Cache和Write buffer。所谓I/O地址空间,就是对于其中的地
址连续两次的写操作不能合并在一起,每次读写操作都必须直接访问设备,否则程序的运行结果
无法预测。比如寄存器、非内存的外设(扩展串口、网卡等)。
S3C2410/S3C2440提供了相关指令来操作Cache和Write buffer,可以使无效整个ICaches或
其中的某个条目,可以清空、使无效整个DCaches或其中的某个条目。这些指令在下面介绍。
7.1.6 S3C2410/S3C2440 MMU、TLB、Cache的控制指令
S3C2410/S3C2440中,除了有一个ARM920T的CPU核外,还有若干个协处理器。协处
理器也是一个微处理器,它们被用来帮助主CPU完成一些特殊功能,比如浮点计算等。对MMU、
TLB、Cache等的操作就涉及到协处理器。CPU核与协处理器间传送数据时使用这两条指令:
MRC和MCR,它们的格式如下:
1 <MCR | MRC>{cond} p#, <expression1>,Rd,cn,cm{ ,<expression2>} 2 MRC //从协处理器获取数据,传给CPU核的寄存器 3 MCR //数据从CPU核的寄存器传给协处理器 4 {cond} //执行条件,省略时表示无条件执行 5 p# //协处理器序号 6 <expression1> //一个常数 7 Rd //CPU核的寄存器 8 cn和cm //协处理器中的寄存器 9 <expression2> //一个常数
其中,<expression1>、cn、cm、<expression2>仅供协处理器使用,它们的作用如何取决于
具体的协处理器。
7.2 MMU使用实例:地址映射
7.2.1 程序设计
程序源码位于/work/hardware/mmu目录下。
本开发板SDRAM的物理地址范围出于0x3000 0000 ~ 0x33ff ffff,S3C2410/S3C2440的寄存器
地址范围都处于0x4800 0000 ~ 0x5fff ffff。
第5章通过往GPBCON和GPBDAT这两个寄存器的物理地址0x5600 0010、0x5600 0014写入特
定的数据来驱动4个LED。
本章的实例将开启MMU,并将虚拟地址空间0xA000 0000~0xA010 0000映射到物理地址空间
0x5600 0000~0x5610 0000上,这样,就可以通过操作地址0xA000 0010、0xA000 0014来达到驱
动这4个LED的同样效果。
另外,将虚拟地址空间0xb000 0000~0xb3ff ffff映射到物理地址空间0x3000 0000~0x33ff ffff上,
并在连接程序时将一部分代码的运行地址指定为0xb000 4000(这个数值有点奇怪,看下去就会明白),
看看能否令程序跳转到0xb000 4000处执行。
本章程序只使用一级页表,以段的方式进行地址映射。32位CPU的虚拟地址空间达到4GB,一级
页表中使用4096个描述符来表示这4GB空间(每个描述符对应1MB的虚拟地址),每个描述符占用4字
节,所以一级页表占16KB。本实例使用SDRAM的开始16KB来存放一级页表,剩下的内存开始物理
地址位0x3000 4000。
将程序代码分为两部分:第一部分的运行地址设为0,它用来初始化SDRAM、复制第二部分的代
码到SDRAM中(存放在0x3000 4000处)、设置页表、启动MMU,最后跳转到SDRAM中(地址
0xb000 4000)去继续执行;第二部分的运行地址设为0xb000 4000,它用来驱动LED。
根据上面的叙述,程序流程图如图7.15所示。
7.2.2 代码详解
1. 第一部分代码分析
程序源码分为3个文件:head.S、init.c、leds.c
(1)head.S代码详解。
head.S文件如下:
1 @******************************* 2 @ File:head.S 3 @ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU, 4 @ 然后跳转到SDRAM继续执行。 5 @******************************* 6 7 .text 8 .global _start 9 _start: 10 ldr sp, =4096 @设置栈指针,以下都是C函数,调用前需要设置好栈 11 bl disable_watch_dog @关闭看门狗 12 bl memsetup @设置存储控制器以使用SDRAM 13 bl copy_2th_to_sdram @将第二部分代码复制到SDRAM 14 bl create_page_table @设置页表 15 bl mmu_init @启动MMU 16 ldr sp, =0xB4000000 @重设栈指针,指向SDRAM顶端(使用虚拟地址) 17 ldr pc, =0xB0004000 @跳到SDRAM中继续执行第二部分代码 18 halt_loop: 19 b halt_loop 20
head.S中调用的函数都在init.c中实现。
值的注意的是,在第15行开启MMU之后,无论是CPU取值还是CPU读写数据,使用
的都是虚拟地址。
在第14行设置页表时,在create_page_table函数中令head.S、init.c程序所在内存的虚
拟地址和物理地址一样,这使得head.S和init.c中的代码在开启MMU后能够没有任何障碍
地继续运行。
(2)init.c代码详解。
init.c中的disable_watch_dog、memsetup函数实现的功能在前面两章已经讨论过,不在
重复,下面列出diam以方便读者查阅。
1 /* 2 *init.c:进行一些初始化,在Steppingstone中运行 3 *它和head.S同属第一部分程序,此时MMU未开启,使用物理地址 4 */ 5 6 /*WATCHDOG寄存器*/ 7 #define WTCON (*(volatile unsigned long *)0x53000000) 8 /*存储控制器的寄存器起始地址*/ 9 #define MEM_CTL_BASE 0x48000000 10 11 12 /* 13 *关闭WATCHDOG,否则CPU会不断重启 14 */ 15 void disable_watch_dog(void) 16 { 17 WTCON = 0; //关闭WATCHDOG很简单,往这个寄存器写0即可 18 } 19 20 /* 21 *设置存储控制器以使用SDRAM 22 */ 23 void memsetup(void) 24 { 25 /*SDRAM 13个寄存器的值*/ 26 unsigned long const mem_cfg_val[] = {0x22011110, //BWSCON 27 0x00000700, //BANKCON0 28 0x00000700, //BANKCON1 29 0x00000700, //BANKCON2 30 0x00000700, //BANKCON3 31 0x00000700, //BANKCON4 32 0x00000700, //BANKCON5 33 0x00018005, //BANKCON6 34 0x00018005, //BANKCON7 35 0x008c07a3, //REFRESH 36 0x000000b1, //BANKSIZE 37 0x00000030, //MRSRB6 38 0x00000030, //MRSRB7 39 }; 40 int i = 0; 41 volatile unsigned long *p = (volatile unsigned long *)MEM_CTL_BASE; 42 for(; i < 13; i++) 43 p[i] = mem_cfg_val[i]; 44 }
copy_2th_to_sdram函数用来将第二部分代码(即由leds.c编译得到的代码)从Steppingstone
中复制到SDRAM中。在连接程序时,第二部分代码的加载地址呗指定为2048,重定位地址为
0xB000 4000,。所以系统从NAND Flash启动后,第二部分代码就存储在Steppingstone中地址
2048之后,需要把它复制到0x3000 4000处(此时尚未开启MMU,虚拟地址0xB000 4000对应
的物理地址在后面设为0x3000 4000)。Steppingstone总大小为4KB,不妨把地址2048之后的
所有数据复制到SDRAM中,所以源数据的结束地址为4096。
copy_2th_to_sdram函数的代码如下:
1 /* 2 * 将第二部分代码复制到SDRAM 3 */ 4 void copy_2th_to_sdram(void) 5 { 6 unsigned int * pdwSrc = (unsigned int *)2048; 7 unsigned int * pdwDest = (unsigned int *)0x30004000; 8 9 while(pdwSrc < (unsigned int *)4096) 10 { 11 *pdwDest = *pdwSrc; 12 pdwDest++; 13 pdwSrc++; 14 } 15 }
剩下的create_page_table、mmu_init就是本章的重点了,前者用来设置页表,后者用来
开启MMU。
先看看create_page_table函数。它用于设置3个区域的地址映射关系。
(1)将虚拟地址0~(1M - 1)映射到同样的物理地址去,Steppingstone(从0地址开始
的4KB内存)就处于这个范围中。使虚拟地址等于物理地址,可以让Steppingstone中的
程序(head.s和init.c)在开启MMU前后不需要考虑太多的事情。
(2)GPIO寄存器的起始物理地址范围为0x5600 0000,将虚拟地址0xA000 0000~
(0xA000 0000 + 1M - 1)映射到物理地址0x5600 0000~(0x5600 0000 + 1M -1)。
(3)本开发板中SDRAM的物理地址范围为0x3000 0000~0x33ff ffff,将虚拟地址
0xB000 0000~0xB3FF FFFF映射到物理地址0x3000 0000~0x33FF FFFF。
create_page_table函数代码如下:
1 /* 2 *设置页表 3 */ 4 void create_page_table(void) 5 { 6 7 /* 8 *用于段描述符的一些宏定义 9 */ 10 #define MMU_FULL_ACCESS (3 << 10) /*访问权限*/ 11 #define MMU_DOMAIN (0 << 5) /*属于哪个域*/ 12 #define MMU_SPECIAL (1 << 4) /*必须是1*/ 13 #define MMU_CACHEABLE (1 << 3) /*cacheable*/ 14 #define MMU_BUFFERABLE (1 << 2) /*bufferable*/ 15 #define MMU_SECTION (2) /*表示这是段描述符*/ 16 #define MMU_SECDESC (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \ 17 MMU_SECTION) 18 #define MMU_SECDESC_WB (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \ 19 MMU_CACHEABLE | MMU_BUFFERABLE | MMU_SECTION) 20 #define MMU_SECTION_SIZE 0x00100000 21 22 unsigned long virtualaddr, physicaladdr; 23 unsigned long *mmu_tlb_base = (unsigned long *)0x30000000; 24 25 /* 26 *Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0, 27 *为了在开启MMU后仍然运行第一部分的程序, 28 *将0~1M的虚拟地址映射到同样的物理地址 29 */ 30 virtualaddr = 0; 31 physicaladdr = 0; 32 *(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) |\ 33 MMU_SECDESC_WB; 34 35 /* 36 *0x5600 0000时GPIO寄存器的起始物理地址, 37 *GPFCON和GPFDAT这两个寄存器的物理地址0x56000050、0x56000054, 38 *为了在第二部分程序中能以地址0xA0000050、0xA0000054来操作GPFCON、GPFDAT, 39 *把从0xA000 0000开始的1MB虚拟地址空间映射到从0x5600 0000开始的1MB物理地址空间 40 */ 41 virtualaddr = 0xA0000000; 42 physicaladdr = 0x56000000; 43 *(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) |\ 44 MMU_SECDESC; 45 46 /* 47 *SDRAM的物理地址范围是0x3000 0000~0x33ff ffff, 48 *将虚拟地址0xB000 0000~0xB3ff ffff映射到物理地址0x3000 0000~0x33ff ffff上, 49 *总共64MB,涉及64个段描述符。 50 */ 51 virtualaddr = 0xB0000000; 52 physicaladdr = 0x30000000; 53 while(virtualaddr < 0xB4000000) 54 { 55 *(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) |\ 56 MMU_SECDESC_WB; 57 virtualaddr += 0x100000; 58 physicaladdr += 0x100000; 59 } 60 }
mmu_tlb_base被定义为unsigned long指针,所指向的内存为4字节,刚好是一个描述符
的大小。在SDRAM的开始存放页表——
“unsigned long *mmu_tlb_base = (unsigned long *)0x30000000;”。
其中最能体现页表结构的代码是:
*(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) |\
MMU_SECDESC_WB;
*(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) |\
MMU_SECDESC;
*(mmu_tlb_base + (virtualaddr >> 20)) = (physicaladdr & 0xFFF00000) |\
MMU_SECDESC_WB;
虚拟地址的位[31:20]用于索引一级页表,找到它所对应的描述符,对应于“virtualaddr
>> 20”。
如图7.13所示,段描述符中位[31:20]中保存段的物理地址,对应于“physicaladdr &
0xFFF0 FFFF”。
位[11:0]中用来设置段的访问权限,包括所属的域、AP位、C位(是否可Cache)、B位
(是否使用Write buffer)——这对应“MMU_SECDESC”或“MMU_SECDESC_WB”,它们
的域都被设为0,AP位被设为0b11(根据表7.2可知它所在的域进行权限检查,则读写操作
都被允许)。“MMU_SECDESC”中的C/B位都没有设置,表示不使用Cache和Write buffer,
所以映射寄存器空间时使用“MMU_SECDESC”。“MMU_SECDESC_WB”中C/B位都设置
了,表示使用Cache和Write buffer,即所谓的写回式,在映射Steppingstone和SDRAM等
内存时使用“MMU_SECDESC_WB”。
现在来看看mmu_init函数。create_page_table函数设置好了页表,还需要把页表地址
告诉CPU,并且在开启MMU之前做好一些准备工作,比如使无效ICaches、DCaches,设
置域访问控制寄存器等。代码的注释就可以帮助读者很好地理解mmu_init函数,不再重复。
代码如下:
1 /* 2 * 启动MMU 3 */ 4 void mmu_init(void) 5 { 6 unsigned long ttb = 0x30000000; 7 8 __asm__( 9 "mov r0, #0\n" 10 "mcr p15, 0, r0, c7, c7, 0\n" /*使无效ICaches和DCaches*/ 11 12 "mcr p15, 0, r0, c7, c10, 4\n" /*drain write buffer on v4*/ 13 "mcr p15, 0, r0, c8, c7, 0\n" /*使无效指令、数据TLB*/ 14 15 "mov r4, %0\n" /*r4 = 页表基址*/ 16 "mcr p15, 0, r4, c2, c0, 0\n" /*设置页表基址寄存器*/ 17 18 "mvn r0, #0\n" 19 "mcr p15, 0, r0, c3, c0, 0\n" /*域访问控制寄存器设为0xFFFF FFFF, 20 *不进行权限检查 21 */ 22 /* 23 *对于控制寄存器,先读出其值,在这基础上修改感兴趣的位, 24 *然后再写入 25 */ 26 "mrc p15, 0, r0, c1, c0, 0\n" /*读出控制寄存器的值*/ 27 28 /* 29 *控制寄存器的低16位含义为:.RVI ..RS B... .CAM 30 *R:表示换出Cache中的条目时使用的算法, 31 * 0 = Random replacement; 1= Round robin replacement 32 *V:表示异常向量表所在的位置, 33 * 0 = Low addresses = 0x0000 0000; 1 = High addresses = 0xFFFF 0000 34 *I:0 = 关闭ICaches;1 = 开启ICaches 35 *R、S:用来与页表中的描述符一起确定内存的访问权限 36 *B:0 = CPU为小字节序;1 = CPU为大字节序 37 *C:0 = 关闭DCaches;1 = 开启DCaches 38 *A:0 = 数据访问时不进行地址对齐检查;1 = 进行对齐检查 39 *M:0 = 关闭MMU;1 = 开启MMU 40 */ 41 42 /* 43 *先清除不需要的位,往下若需要则重新设置它们 44 */ 45 /* .RVI ..RS B... .CAM */ 46 "bic r0, r0, #0x3000\n" /*..ll.... .... ....清除V、I位 */ 47 "bic r0, r0, #0x0300\n" /*.... ..ll .... ....清除R、S位 */ 48 "bic r0, r0, #0x0087\n" /* .... .... l... .lll清除B/C/A/M */ 49 50 /* 51 *设置需要的位 52 */ 53 "orr r0, r0, #0x0002\n" /*.... .... .... ..1. 开启对齐检查*/ 54 "orr r0, r0, #0x0004\n" /*.... .... .... .1.. 开启DCaches*/ 55 "orr r0, r0, #0x1000\n" /*...1 .... .... .... 开启ICaches*/ 56 "orr r0, r0, #0x0001\n" /*.... .... .... ...1 使能MMU*/ 57 58 "mcr p15, 0, r0, c1, c0, 0\n" 59 : /*无输出*/ 60 : "r" (ttb); 61 ) 62 }
mmu_init函数在C语言中嵌入了汇编指令。
2. 第二部分代码分析
第二部分代码leds.c中只有两个函数:wait和main。wait函数用来延迟时间,main函数
用来循环点亮LED。与前面两章所用的leds.c有两点不同。
(1)操作GPFCON、GPFDAT两个寄存器时使用虚拟地址0xA0000050、0xA0000054,
在init.c中已经把虚拟地址0xA000 0000~(0xA000 0000 + 1M - 1)映射到物理地址0x5600 0000
~ (0x5600 0000 + 1M - 1);
(2)在定义wait函数时使用了一个小技巧,将它定义成“static inline”类型,原因在源码第
15行给出。
leds.c代码如下:
1 /* 2 * leds.c: 循环点亮3个LED 3 * 属于第二部分程序,此时MMU已开启,使用虚拟地址 4 */ 5 6 #define GPFCON (*(volatile unsigned long *)0xA0000050) // 物理地址0x56000050 7 #define GPFDAT (*(volatile unsigned long *)0xA0000054) // 物理地址0x56000054 8 9 #define GPF4_out (1<<(4*2)) 10 #define GPF5_out (1<<(5*2)) 11 #define GPF6_out (1<<(6*2)) 12 13 /* 14 * wait函数加上“static inline”是有原因的, 15 * 这样可以使得编译leds.c时,wait嵌入main中,编译结果中只有main一个函数。 16 * 于是在连接时,main函数的地址就是由连接文件指定的运行时装载地址。 17 * 而连接文件mmu.lds中,指定了leds.o的运行时装载地址为0xB4004000, 18 * 这样,head.S中的“ldr pc, =0xB4004000”就是跳去执行main函数。 19 */ 20 static inline void wait(unsigned long dly) 21 { 22 for(; dly > 0; dly--); 23 } 24 25 int main(void) 26 { 27 unsigned long i = 0; 28 29 GPFCON = GPF4_out|GPF5_out|GPF6_out; // 将LED1,2,4对应的GPF4/5/6三个引脚设为输出 30 31 while(1){ 32 wait(30000); 33 GPFDAT = (~(i<<4)); // 根据i的值,点亮LED1,2,4 34 if(++i == 8) 35 i = 0; 36 } 37 38 return 0; 39 } 40 注:实验表明,代码功能有问题,但没找到哪里有问题!!!_20171021
3. Makefile 和 连接脚本 mmu.lds
Makefile内容如下:
1 objs := head.o init.o leds.o 2 3 mmu.bin: $(objs) 4 arm-linux-ld -Tmmu.lds -o mmu_elf $^ 5 arm-linux-objcopy -O binary -S mmu_elf $@ 6 arm-linux-objdump -D -m arm mmu_elf > mmu.dis 7 8 %.o:%.c 9 arm-linux-gcc -Wall -O2 -c -o $@ $< 10 11 %.o:%.S 12 arm-linux-gcc -Wall -O2 -c -o $@ $< 13 14 clean: 15 rm -f mmu.bin mmu_elf mmu.dis *.o
在源码目录下执行make命令时,make命令读取Makefile文件发现目标文件mmu.bin
不存在,所以试图使用它的依赖文件head.o、init.o、leds.o来生成mmu.bin;可是,这些
依赖文件也不存在,于是先使用其他规则来生成这些文件;使用第11、12行的规则来编译
head.S以生成head.o,使用第8、9行的命令来生成mmu.bin。
Makefile中第4行命令用来连接程序,它使用连接脚本mmu.lds来控制连接器的行为。
文件mmu.lds内容如下:
1 SECTIONS{ 2 first 0x00000000 : { head.o init.o } 3 second 0xb0004000 : AT(2048) { leds.o } 4 }
连接脚本mmu.lds将程序分成两个段:first和second。前者由head.o和init.o组
成,它的加载地址和运行地址都是0,所以运行前不需要重新移动代码。后者由leds.o组
成,它的加载地址为2048,重定位地址为0xB0004000,这表明段second存放在编译所得
映像文件地址2048处,在运行前需要将它复制到地址0xB0004000处,这由init.c中的
copy_2th_to_sdram函数完成(注意:次函数将代码复制开始地址为0x30004000的内存中,
这是开启MMU后虚拟地址0xB0004000对应的物理地址)。
本实例程序中涉及代码的复制、开启MMU前使用物理地址寻址,开启MMU后使用虚拟
地址寻址,相对复杂。为了更形象地讲解本程序,下面用图7.16延时代码的执行过程。
图7.16 程序复制代码、设置页表、启动MMU的执行过程
7.2.3 实例测试
在源码目录下执行make命令生成可执行程序mmu.bin,使用JTAG工具烧入NAND Flash
后,按复位键启动系统。可以看见3个LED被轮流点亮,用来从0~7重复计数。LED闪烁的速
度比第6章的SDRAM实验快,这是因为开启了Cache。
附:代码:
链接: https://pan.baidu.com/s/1kV24a9L 密码: tfab