ARM  MMU 理解(基于ARM 920T

MMU简介

嵌入式系统中,存储系统差别很大,可包含多种类型的存储器件,如FLASHSRAMSDRAMROM等,这些不同类型的存储器件速度和宽度等各不相同;在访问存储单元时,可能采取平板式的地址映射机制对其操作,或需要使用虚拟地址对其进行读写;系统中,需引入存储保护机制,增强系统的安全性。为适应如此复杂的存储体系要求,ARM处理器中引入了存储管理单元来管理存储系统。

ARM存储系统中,使用MMU实现虚拟地址到实际物理地址的映射。为何要实现这种映射?首先就要从一个嵌入式系统的基本构成和运行方式着手。系统上电时,处理器的程序指针从0x0(或者是由0Xffff_0000处高端启动)处启动,顺序执行程序,在程序指针(PC)启动地址,属于非易失性存储器空间范围,如ROMFLASH等。然而与上百兆的嵌入式处理器相比,FLASHROM等存储器响应速度慢,已成为提高系统性能的一个瓶颈。而SDRAM具有很高的响应速度,为何不使用SDRAM来执行程序呢?为了提高系统整体速度,可以这样设想,利用FLASHROM对系统进行配置,把真正的应用程序下载到SDRAM中运行,这样就可以提高系统的性能。然而这种想法又遇到了另外一个问题,当ARM处理器响应异常事件时,程序指针将要跳转到一个确定的位置,假设发生了IRQ中断,PC将指向0x18(如果为高端启动,则相应指向0vxffff_0018),而此时0x18处仍为非易失性存储器所占据的位置,则程序的执行还是有一部分要在FLASH或者ROM中来执行的。那么我们可不可以使程序完全都SDRAM中运行那?答案是肯定的,这就引入了MMU,利用MMU,可把SDRAM的地址完全映射到0x0起始的一片连续地址空间,而把原来占据这片空间的FLASH或者ROM映射到其它不相冲突的存储空间位置。例如,FLASH的地址从0x0000_00000x00ff_ffff,SDRAM的地址范围是0x3000_00000x31ff_ffff,则可把SDRAM地址映射为0x0000_00000x1fff_ffffFLASH的地址可以映射到0x9000_00000x90ff_ffff(此处地址空间为空闲,未被占用)。映射完成后,如果处理器发生异常,假设依然为IRQ中断,PC指针指向0x18处的地址,而这个时候PC实际上是从位于物理地址的0x3000_0018处读取指令。通过MMU的映射,则可实现程序完全运行在SDRAM之中。
在实际的应用中,可能会把两片不连续的物理地址空间分配给SDRAM。而在操作系统中,习惯于把SDRAM的空间连续起来,方便内存管理,且应用程序申请大块的内存时,操作系统内核也可方便地分配。通过MMU可实现不连续的物理地址空间映射为连续的虚拟地址空间。
操作系统内核或者一些比较关键的代码,一般是不希望被用户应用程序所访问的。通过MMU可以控制地址空间的访问权限,从而保护这些代码不被破坏。
MMU的实现过程,实际上就是一个查表映射的过程。建立页表(translate table)是实现MMU功能不可缺少的一步。页表是位于系统的内存中,页表的每一项对应于一个虚拟地址到物理地址的映射。每一项的长度即是一个字的长度(在ARM中,一个字的长度被定义为4字节)。页表项除完成虚拟地址到物理地址的映射功能之外,还定义了访问权限和缓冲特性等。
MMU的映射分为两种,一级页表的变换和二级页表变换。两者的不同之处就是所实现的变换地址空间大小不同。一级页表变换支持1M大小的存储空间的映射,而二级可以支持64KB4KB1KB大小地址空间的映射。
要实现从虚拟地址到物理地址的映射,必然会遇到一个问题,如何找到这个页表。对于表的查找,要知道这个表的基地址和偏移地址,在具有MMU功能的处理器中,集成了一个被称为CP15的协处理器,该协处理器的C2寄存器中用于保存页表的基地址,下面以一级页表变换为例说明MMU实现地址变换的过程。
如图1.1所示,当处理器访问一个虚拟地址时,该虚拟地址的[3120]作为偏移地址与页基地址结合(基地址必须是64KB对齐的,因此基地址的[130]位都为0),得到一个32位的页表项地址(因为页表项为4字节对齐,[10]两位为0)。通过这个页表项地址可以检索到该页表项。页表项的格式如下:

                           查找到页表项后,根据页表项的访问特性(缓冲以及是否允许访问等)协处理器决定是否允许访问。如不允许访问,则协处理器向CPU报告出错信息;反之,由页表项的[3120]位与虚拟地址的[190]一起组成实际的物理地址,实现从虚拟地址到物理地址的映射。

对于实际编程工作而言,主要是确定如何编写页表中的内容并如何确定页表项地址。现举例如下:
假设物理地址为0x36B0_0000~0x36Bf_ffff1M空间)的一块连续空间需映射为0x0100_0000~0x010f_ffff的一块连续空间:
1.确定页表项中的内容:把物理地址的基地址作为页表项的高12位(31bit21bit),填写访问属性。假设可以读写,可以读缓存、写缓冲,这样该页表项内容为0x36B0_0C00E
2.确定页表基地址,填写页表基地址到CP15寄存器的C2中。页表的基地址要为64KB对齐;
3.计算出偏移地址,把内容填写到页表项地址中。页表项地址=页表基地址+(物理地址基地址>>18,如页表基地址为0xA100_0000,那么,页表项地址=0xA100_0DAC
4.将页表项数值写到对应的页表项地址中。上例中,需要向地址0xA100_0DAC中写入0x36B0_0C00E


ARM920T的MMUCache

Cache是高性能CPU解决总线访问速度瓶颈的方法,然而它的使用却是需要权衡的,因为缓存本身的动作,如块拷贝和替换等,也是很消耗CPU时间的。MMU的重要性勿庸置疑,ARM920T(和ARM720T)集成了MMU是其最大的卖点;有了MMU,高级的操作系统(虚拟地址空间,平面地址,进程保护等)才得以实现。二者都挺复杂,并且在920T中又高度耦合,相互配合操作,所以需要结合起来研究。同时,二者的操作对象都是内存,内存的使用是使用MMU/Cache的关键。另外,MMUCache的控制寄存器不占用地址空间,CP15是操纵MMU/Cache的唯一途径。

Cache/Write Buffer的功能

Cache通过预测CPU即将要访问的内存地址(一般都是顺序的),预先读取大块内存供CPU访问,来减少后续的内存总线上的读写操作,以提高速度。然而,如果程序中长跳转的次数很多,Cache的命中率就会显著降低,随之而来,大量的替换操作发生,于是,过多的内存操作反而降低了程序的性能。

ARM920T内部采用哈佛结构,将内部指令总线和数据总线分开,分别连接到ICacheDCache,再通过AMBA总线接口连接到ASB总线上去访问内存。CacheLine组成,LineCache进行块读取和替换的单位。

Writer Buffer是和DCache相逆过程的一块硬件,目的也是通过减少memory bus的访问来提高性能。

MMU的功能

在内存中维护一张或几张表,就看你怎么给内存划分pagesection了。通过CP15指定好转换表的位置,920T的硬件会自动将转换表的一部分读到TLB中。CPU每次进行内存读写时,发出虚拟地址,参照TLB中的转换表转换到物理地址,并读取相应entry中的信息,以决定是否可以有权限读写和缓存。

mmugen这个工具就是帮你构造这个表的,省的自己写程序了。

操作MMU,实际上就是如何分配和使用你的内存,并记录在translationtable里。

ARM920T中,MMU的每条entry包括CachableBuffable位来指定相应的内存是否可以用Cache缓存。此处就是MMUCache的交互作用处。


实际上,MMUCache的使用是操作系统设计者根据系统软硬件配置而考虑的事情。操作系统针对分配给应用程序的地址空间作内存保护和缓存优化。在没有操作系统的情况下,就需要我们自己来掌控它们了。其中,主要是合理分配内存。

我认为,以下几点需要着重考虑:

1) 安全第一! -- 避免MMUCache的副作用。

当你在无OS的裸机上开发程序时,初始化运行环境的代码很重要,比如:各种模式堆栈指针的初始化;将代码和RW dataROM拷贝到RAM;初始化.bss段(zero initialized)空间等。此时会有大量的内存操作,如果你enableCache,那么在拷贝完代码之后,一定要invalidate ICacheflush DCache。否则将会出现缓存中的代码或数据与内存中的不一致,程序跑飞。

另外,有时候我们需要自己作loader来直接运行ELF文件,情况也是一样,拷贝完代码后一定要刷新Cache,以免不测。

还有,对硬件的操作要小心。很多寄存器值都是被硬件改变的,读写时,要保证确实访问到它的地址。首先,在C语言代码中声明为volatile变量,以防止内存读写被编译器优化掉;另外,设置好TLB,使得寄存器映射的地址空间不被缓存。

总之,缓存和内存中代码的不一致,是一定要避免的。

2) 弄巧成拙! -- 只对频繁访问的地址空间进行Cache优化。

我们很清楚自己的程序中,那里有大量的运算,哪里有无数的循环或递归,而这正是Cache的用武之地,我们将这些空间进行缓存将大大提高运行速度。但是,很多函数或子程序往往仅仅运行很少几次,若是对它们也缓存,只会捡了芝麻丢了西瓜,造成不必要的缓存和替换操作,反而增加了系统负担,降低了整体性能。

3) 断点哪儿去了? -- 如何调试“加速”了的代码?

据我所知,一般,debugger都是通过扫描地址总线,在断点处暂停CPUARM9TDMI中集成的JTAG调试口,也是这样。

当我们调试使用Cache的代码时,将会出现问题。比如:CPU访问某断点所在地址之前的地址时,发生缓存操作,断点处代码被提前读入Cache,此时地址总线上出现了断点地址,CPUdebugger暂停,并且断点之后的指令也被Cache缓存。于是,当你从断点处step时,程序却停不了了,因为地址总线上不再出现断点之后的下一个地址了。
再举个例子:
int i,a;
for (i=0; i<100; i++) {
-> a++; 
}

当地址总线上第一次出现断点地址时,CPU暂停;之后,就再也不会停了。因为,之后CPU会从cache中直接去代码了。(当然,后来,Cache的代码有可能会被替换掉,断点又可到达。) 所幸的是,我用的debugger提供JTAG Monitor,允许断点跟踪使用cache的程序。

全部参考来源于ARM的手册,建议详细阅读1)PartB及其在2)中的具体实现。
1) DDI0100E_ARM_ARM.pdf (ARM Architecture Reference Manual)
2) DDI0151C_920T_TRM.pdf (ARM920T Technical Reference Manual)