当前位置: 首页 > article >正文

JZ2440开发板——MMU与Cache

以下内容源于韦东山课程的学习与整理,如有侵权请告知删除。

MMU与Cache的作用非常大, 但我们写程序的时候基本不涉及它们(在u-boot代码中好像有涉及到);后面写驱动时,如果你想映射某个寄存器,可以使用内核提供的ioremap函数来映射,根本不需要我们理解内部的机制。但是为了对MMU与Cache有个基本的理解,这里展开说一下。

一、Cache简述

1.1 Cache的框图 

在S3C2440数据手册P35上有以下内容:

由图可知:

(1)对于Cache,有指令Cache(ICaches)和数据Cache(DCaches),它们都是一些高速的、小容量的(16KB)存储器。

(2)对于MMU、Cache,都是通过协处理器CP15来操作的。协处理器,英文是coprocessor,作用是协助主处理器完成某些操作。比如ARM系统中有CP0~CP15一共16个协处理器,其中CP15是负责管理MMU、Cache。

1.2 Cache的作用

下面以一个求和例子说明Cache的作用(在hardware/003_led_c/led.c的main函数之下添加下面内容):

int sum()
{
    int i;
    int sum=0;
    for(i=0;i<100;i++)
        sum+=i;
    return sum;
}

其反汇编代码如下所示。局部变量sum与i保存在栈中,即内存中,假设sum保存在地址A(即[fp,#-20]),i保存在地址B(即[fp,#-16])。

程序执行时,这些指令对应的机器码保存在内存中,CPU根据这些机器码进行操作。由反汇编指令可知,CPU不断进行下面的操作:

1)不断地执行for循环对应的指令。

2)不断地读写地址A、B(即不断地从地址A、B读取数据,不断地写数据到地址A、B)。

现在假设SDRAM非常慢(这是相对于CPU而言的,因为CPU运行在几百MHz,甚至上GHz),那该如何提高程序的执行效率呢?

这里引入“局部性原理”这个概念,包括时间局部性、空间局部性。时间局部性,是指在一段时间内,有极大概率访问同一地址上的指令或数据(比如 for 循环中同一个地址上的指令经常被访问到);空间局部性,是指有极大概率访问到相邻空间的指令或数据(比如地址A和地址B是相邻的,for循环里面的指令的地址也是相邻的)。

上面提到,CPU会反复用到地址A、B上的数据,以及for循环对应的指令,但是它们存储在比较慢的SDRAM中。为了提高程序的执行效率,我们可以在CPU上设置ICaches和Dache,把频繁用到的数据放入DCache,把频繁用到的指令放入ICache。

当CPU执行某条指令时,根据局部性原理,会把它附近的其他指令存入ICache中;读取下一条指令时,会优先从ICache中取得指令,如果ICache中没有那条指令,才去访问SDRAM。

当CPU在访问地址A上的数据时,根据局部性原理,会把它附近的其他数据存入DCache中(比如地址B上的数据)。当需要读取地址B上的数据时,会优先从DCache中取得数据,如果DCache中没有地址B的数据,才去访问SDRAM。

这说明,引入Cache可以提高程序的执行效率。

1.3 Cache的过程 

由于DCache和ICache分别只有16KB,而SDRAM有64MB,因此Cache不可能存储SDRAM中的所有内容,而是只能存储一部分。

下面演示一下Cache的过程,以DCache为例。

1.3.1 读数据时的Cache过程

1)程序要读取地址A的数据,比如“ldr r3,[A]”。

a、首先CPU以地址A查找DCache,一开始DCache无数据,这会导致cache miss

b、CPU把地址A发给SDRAM后,并非只返回地址A的数据,而是返回一系列的数据(这些数据叫做cache line,一个cache line的长度是 8 words,即 32 字节),然后将cache line存入DCache中(这过程称为cache fill),并且把地址A上的数据返回给CPU。

2)当程序再次读取地址A的数据时,CPU以地址A查找DCache,而DCache中有地址A的数据(这称为cache hit),可以直接从DCache返回数据给CPU,不需要再去访问SDRAM了。

3)当程序要读地址B的数据时,CPU以地址B查找DCache。之前读取地址A的数据时,把地址A附近的数据读到了DCache,这其中就包括地址B的数据。因此这里会cache hit,直接从DCache返回数据给CPU。

4)假设DCache的存储空间用完了,现在要读取地址C上的数据,该怎么办呢?这时就需要把DCache中旧数据替换出来,把地址C附近的cache line存入DCache中。

1.3.2 写数据时的Cache过程

写数据时,CPU中有一个write buffer,如下图所示:

在S3C2410的数据手册P585有以下内容(S3C2440数据手册忽略了这部份内容):

第一种配置方式:NCNB(不使用cache,不使用write buffer)

我们可以设置既不使用cache也不使用write buffer,也就是说,CPU发出的地址会直接到达硬件(注意这里的硬件不单纯是指SDRAM,也可能是其他统一编址的硬件),直接读取硬件上的数据。

这种配置方式,适用于GPIO等硬件。对于GPIO相关的寄存器,我们应该设置它为NCNB。比如说GPFDAT寄存器,CPU通过读这个寄存器得到某引脚的状态,那就不应该从DCache中读取旧的数据,应该直接访问硬件以得到最新的数据。

第二种配置方式: NCB(不使用cache,但是使用write buffer)

读和写的时候,都不使用cache,即读写的时候会直接操作硬件。

当我们要写某个地址时(往某个地址上写入数据),如果这个地址对应的内存被设置为NCB,则CPU直接把数据写到write buffer中,然后CPU就不管了(CPU就可以执行下一条指令了,并不需要等待写操作的的完成),由这个write buffer来进行后续的缓慢的写操作。

第三种配置方式:写通(使用cache,WT方式)

读的时候,会优先从Cache中取得数据或指令,如果Cache中没有则会引起cache fill。

写的操作,先把数据写到write buffer中,然后write buffer会马上将这些数据写到硬件中。

第四种配置方式:写回(使用cache,WB方式)

读的时候,会优先从Cache中取得数据或指令,如果Cache中没有则会引起cache fill。

写数据时也会用到Cache,因为要往某个地址写入数据时,在cache中可能已经存储有这个地址上的数据。

写的时候(要往某个地址写入数据),如果cache miss(cache中没有这个地址的数据),则把数据写到write buffer中,然后write buffer会马上将这些数据写到硬件中;如果cache hit(cache中存有这个地址的数据),则更新Cache中的数据(CPU的数据会写入Cache),并把cache line标记为脏。这些脏的数据在合适的时机会写给write buffer,再由write buffer将这些数据写到硬件中。

什么是合适的时机呢?Cache替换时,或者强制进行“清空”操作时(比如flash cache),此时脏的数据会写给write buffer,再由write buffer将这些数据写到硬件中。

二、编写代码启动ICache

2.1 协处理器指令:mcr、mrc

如下图所示,ARM系统中有CP0~CP15一共16个协处理器,每个协处理器又有C0~C15一共16个寄存器,每个寄存器又有几个备份寄存器,比如对于C7寄存器有C7'、C7''、C7'''…等寄存器。

协处理器和主处理器之间如何传递数据呢?比如如何将主处理器某个寄存器的值,传给协处理器的某个寄存器?使用mcr、mrc指令。

如何记忆这两个指令?首先,这两个指令中的m可以理解为mov,“mov 操作数1,操作数2”表示将操作数2的值赋值给操作数1(或者说从右到左);然后,mcr或mrc中的c表示coprocessor,r表示(主)registerc,那么mcr就表示将r赋值给c(将主处理器某寄存器的值,赋值给协处理器某个寄存器),mrc表示将c赋值给r(将协处理器某寄存器的值,赋值给主处理器某个寄存器)。

在S3C2440手册里面搜索mrc,得到以下内容:

  • p#:表示你要操作哪个协处理器。比如你要操作(写或者读)协处理器15,则写成p15。
  • <expression1> :一般写为0。
  • Rd:表示你要操作主处理器中哪个寄存器。 
  • cn:表示要操作某个协处理器中的哪一个寄存器。比如CP15中的C1寄存器,则写为c1。
  • cm:一般用不着,这里写为c0即可。
  • <expression1>:一般用不着,这里写为0即可。

例如,代码“mcr p15,0,r1,c1,c0,0”表示将寄存器r1的值,赋值给协处理器CP15的C1寄存器。代码后面的"c0,0"用来区分到底是哪一个C1寄存器(因为CP15的C1寄存器有很多同名的备份寄存器?),一般写为"c0,0"。

2.2 CP15中各个寄存器的作用

CP15负责管理MMU与Cache,因此我们想启动ICache的话,需要设置CP15的相关寄存器。

在S3C2410数据手册P532有以下内容,它描述了CP15中各个寄存器的作用:

我们重点关注C1寄存器,它的位含义如下图所示: 

另外上面提到“每个寄存器又有几个备份寄存器,比如C7寄存器有C7'、C7''、C7'''等寄存器”,这些备份寄存器有什么作用呢?在S3C2410数据手册P544有以下内容,由此可知,对于C7寄存器,后面两个选项的不同,就表示选择不同的C7寄存器,也就对应着不同的功能。换句话说,C7寄存器是用来操作Cache的,但单独使用一个C7寄存器是没有办法实现这么多功能的,于是就有许多同名的备份寄存器。

2.3 编写代码启动ICache

注意,S3C2440中的Cache分为DCache和ICache,其中ICache可以随时打开,不需要其他先决条件,而DCache要启动MMU之后才能使用,所以现在只能做ICache的实验。

我们只需要修改start.S文件:

由上面分析可知,C1寄存器是控制寄存器,其bit[12]控制着是否使能ICache,这里设置为1即可:

2.4 测试效果

本次实验是在“008_touchscreen_perfect_018_012”基础上修改的(008是编号,018是指第18节,012是指第12课)。

由实验现象(见链接)可知,修改之后屏幕的刷新速度加快!这说明ICache的确起作用了。

三、MMU与地址映射

3.1 引入MMU的原因 

JZ2440开发板板载64MB的SDRAM,假设上面有若干个应用程序APP11、APP2…同时在运行,分别运行于地址Addr1、Addr2…则我们可以得出以下结论:它们同时运行在内存中;它们的地址各不相同。

之前了解过链接地址,知道它是应用程序运行时的保存地址。根据以前的知识,我们在编译某个应用程序时,需要指定它的链接地址。如果只有几个应用程序时,为每个应用程序单独指定链接地址,这还能够实现;但是对于一个开放式的嵌入式系统而言,则是一个难以完成的任务,因为应用程序可能有成百上千个,你不可能重新编译它们,另外这些应用程序运行时保存在哪个地址,也是不可预料的。

为了解决上述问题,引入“虚拟地址”概念与“MMU”硬件单元。

(1)让APP可以使用同样的链接地址来编译

也就是说,虽然这些应用程序在内存中的位置各不一样,但对于CPU来说,它们运行时都在同一个虚拟地址上。

比如下面的两个应用程序,编译后查看反汇编代码,得知它们的链接地址都是 0x80B4:

这意味着CPU同时运行这两个应用程序时(这里并不是真正的同时运行,在微观上 CPU 是分时操作,即APP1先运行一段时间,然后切换到APP2再运行一段时间,在宏观上就表现为两个应用程序在同时运行),都会去0x80B4地址处读指令,该地址经过MMU映射后,(由于这两个应用程序对页表的设置不同,所以会)分别转换成物理地址Addr1、Addr2。如此一来,不同的应用程序的链接地址可以相同,因为经过MMU地址转换后,在内存上是不同的物理地址,互不干扰。


我们运行多个APP,在切换进程时,需要把0x80B4这个虚拟地址重新对应到不同的物理地址上。这意味着每切换一个进程时,都需要重新修改一下页表,这个开销非常大,有什么办法优化?

可以引入MVA(修改后的虚拟地址),如下所示,这就可以解决切换进程时频繁构造页表的问题。

其中MVA的计算公式如下(当VA<32M时,MVA和进程的PID有关): 

假设现在有两个APP,分别是APP1和APP2,链接地址都是0x80b4(显然小于32M),PID分别是1和2。

(1)当CPU运行APP1时,发出VA,MVA=VA|(1<<25),对应的页表项是PA1(映射到APP1所在的内存);

(2)当CPU运行APP2时,发出VA,MVA=VA|(2<<25),对应的页表项是PA2(映射到APP2所在的内存);

虽然我们发出的都是同一个VA,但因为PID不一样,所对应的页表项也就不一样,也就不需要重新去构造页表,这样进程从APP1切换到APP2时,只需要修改PID即可,不需要去重新创建页表,这样就可以提高切换效率。

我们提到虚拟地址时,如没有特别指出,一般默认就是MVA。

书上原话是这么说的(还是不怎么明白):


(2)让大容量APP可以在资源少的系统上运行

无论是嵌入式系统还是电脑,内存都是有限的。如果某个应用程序所需的内存大于物理内存,系统是否能运行该应用程序呢?比如开发板JZ2440板载的SDRAM就只有64MB,假设有一个APP需要1G的内存。

也是可以运行的。因为应用程序执行时,不是一次性将所有代码都放入内存,而是把要运行的部分依次放入,当放入的代码指令总体积大于64MB时,先将SDRAM中暂时用不到的代码指令置换出来,再放入需要运行的代码指令。这样一来,尽管SDRAM容量很小,也可以运行内存需求很大的应用程序。而这些置换操作,就是由MMU完成的。

比如下面示意图中,应用程序中VA1这部份代码映射到PA1中运行,VA2这部份代码映射到PA2中运行;当需要运行VA3这部份代码时,发现物理内存已经用完了,此时可以把PA1中的代码先置换出来以腾出物理内存空间,再把VA3这部份代码映射到PA1中运行。如此一来,64MB的SDRAM也可以运行1GB的应用程序。

(3)引入权限管理,禁止访问其它空间

不同的APP之间应该相互独立,让APP1只能访问自己的内存,避免APP1修改APP2的内存。

总之,MMU提供地址映射、管理访问权限等功能。

3.2 页表的概念 

CPU发出虚拟地址(VA)到达MMU,MMU把它转换成物理地址(PA)发给硬件,那么MMU根据什么将一个虚拟地址转换成物理地址?

最简单的办法:弄一个表格,让一个VA对应一个PA,根据VA就能找到PA。此方法很简单,但非常浪费空间(由于需要同时保存VA和PA,而且是一一对应的关系,所以这个表格的大小好像是寻址范围的两倍)。

改进的办法:同样是弄一个表格(称之为页表),页表中的每一项(称之为条目,或者描述符)是4字节的内容,管理着1MB的虚拟地址对应的物理地址与其访问权限(这里假设是以段的方式进行映射)。对于32bit的系统,其寻址范围是4GB=4096MB,由于页表的每一项管理着1MB的虚拟地址空间,所以页表一共有4096个条目。又由于每个条目大小是4B,所以整个页表大小是4096*4B=16KB。

上面提到的页表是一级页表(了解一级页表即可)。一级页表中每个条目或者说描述符(下面称为“描述符”),管理着或者说对应着大小为1MB的虚拟地址空间(换句话说,对于一级页表,映射时是以1MB为单位进行映射的)。如果你想以更小的单位进行映射,比如1KB或4KB或64KB,则需要用到二级页表(这里不讲二级页表,因为对于MMU,大概了解它怎么使用即可)。

描述符的格式可以参考S3C2410的数据手册P560:

对于一级页表,我们只需要关心“Section”这一行即可。其中bit[31:20]是段基址;C与B用来控制这1MB空间是否使用Cache,是否使用write buffer;AP、Domain用来进行权限管理。

3.3 如何由MVA得到PA

以段的方式进行映射时,虚拟地址MVA是如何转换到物理地址PA的?

在S3C2410的数据手册P562有以下内容:

(1)我们所构建的这个页表是存放在内存中的,它在内存的起始地址叫做页表基址,需要将它存储在协处理器CP15的C2寄存器中(C2寄存器因此也叫页表基址寄存器)。

页表存放在内存哪个位置,貌似没有什么特别要求,放在一段不被打搅的内存中即可。但一级页表的长度是16KB(如果不是将4GB寻址范围全部映射,则其长度是小于16KB的),因此页表基址必须是16KB对齐的,换句话说,页表基址的bit[13:0] 必须为 0。

(2)页表基址寄存器的bit[31:14](共18bit),和虚拟地址MVA的bit[31:20](共12bit),组成一个低两位为0的32位地址。这个合成的地址表示某个描述符的地址,MMU正是利用这个合成的地址来找到MVA对应的段描述符。

可以想象:当进行段映射时,页表基址寄存器的bit[31:14]是不会改变的,那么MVA对应哪个描述符是由MVA的bit[31:20]来决定的。MVA的bit[19:0]无论是怎样的数字也只是在1MB范围内变动的,只有超出1MB时才会改变MVA的bit[31:20](进而改变描述符的地址)。换句话说,1MB的虚拟地址都对应着同一个描述符。反过来说,当我们编程需要填充描述符时,为了明确MVA对应页表哪个描述符时,需要使用MVA/0x100000得到index(也即是将MVA>>20)。MVA的bit[31:20]共12bit,刚好可以用来区分2^12=4096个描述符。

另外注意到,合成地址的低两位是0,这说明每个描述符的地址是4字节对齐的,这是因为每个描述符是32位即4字节的长度。

(3)段描述符的bit[31:20]称为段基址,此描述符的低20位填充0之后得到的地址,就是一块1MB物理地址空间的起始地址。反过来说,当我们编程需要填充描述符时,需要取出1MB物理地址空间的起始地址的高[31:20]位,来填充描述符的bit[31:20]。

(4)那如何由描述符、MVA,得到MVA对应的PA呢?取出段描述符的bit[31:20],即段基址,它和MVA[19:0]组成一个32位的物理地址,它就是MVA对应的PA。

3.4 权限管理 

上面提到,AP、Domain用来进行权限管理。所谓“权限管理”,即是否允许程序访问某块内存,有以下几种情况:

a、某块内存完全不允许访问;

b、允许系统模式访问,但不允许用户模式访问(比如驱动程序可以访问某些寄存器,但应用程序不可以访问这些寄存器);

c、用户模式下,根据描述符中的AP决定如何访问。

这里先引入一个“”的概念。在S3C2410的数据手册中搜索“domain”或者配套书籍P108,有以下内容:

ARM9中有16个域,协处理器CP15的C3寄存器用来进行域控制,它的每两位对应一个域,用来表示这个域的访问权限。

  • 如果这个域的权限是00,则表明这块内存无法访问;
  • 如果这个域的权限是01,则表明使用段描述符的AP来决定这个域的访问权限;
  • 如果这个域的权限是11,则表明不检查AP,可以直接访问这块内存。

编程的时候,首先要设置描述符中的domain是哪个(0~15),然后到CP15的C3寄存器中设置相关的位来控制这个域的权限。

上面提到,如果这个域的权限是01,则需要使用段描述符的AP来决定这个域的访问权限。我们再来看一下AP的相关内容,如下所示,AP位来自页表的描述符,而S位、R位来自协处理器CP15的C1寄存器的bit[8]、bit[9](见上面2.2节中的表格)。

对于MMU,我们只关心它的映射,在日常开发中一般不需要理会这些权限。

四、编写代码启动MMU

4.1 如何使用MMU

首先明确一下使用MMU的代码流程:

(1)首先在内存中构建这个页表(你想让某个虚拟地址指向哪个虚拟地址?填充描述符);

(2)然后把页表基地址告诉CP15(将页表基地址写入CP15的C2寄存器);

(3)最后设置CP15,启动MMU。

4.2 创建页表

4.2.1 理论分析

页表是保存在SDRAM里面的,那么我们需要在内存初始化之后,才创建页表。


 

在创建一级页表前,我们需要要确定:将哪些虚拟地址(VA)映射到哪些物理地址(PA)、是否使用Cache(C)和write buffer(B)。

1)我们的应用程序起始运行地址是0,为了保证使能MMU后仍能运行,需要将0~(1M-1)这1MB的虚拟地址,映射到同样的物理地址,即0~(1M-1)这段物理地址。可以C,可以B。

虚拟地址       物理地址CB

0x0000_0000~0x000F_FFFF

0x0000_0000~0x000F_FFFF

11

2)reset函数在做了一些初始化后会用到栈,如下所示。

如果是NOR启动,栈顶是0x40000000+4096,我们可以将从0x40000000开始的1MB的虚拟地址,映射到同样的物理地址,可以C,可以B;如果是Nand启动,栈顶是4096,在1)中已经完成映射。

虚拟地址       物理地址CB

0x4000_0000~0x400F_FFFF

0x4000_0000~0x400F_FFFF

11

3)需要设置各种异常模式下的栈的映射关系。由于这些栈均位于SDRAM中,我们直接将整个SDRAM映射到同样的物理地址即可。也就是从0x30000000开始的64MB的虚拟地址,映射到从0x30000000开始的64MB的物理地址。由于有64MB,而映射是以1MB为单位的,所以需要循环映射64次。另外每个映射都可以C可以B。

虚拟地址物理地址CB
0x3000_0000~0x300F_FFFF,共1MB0x3000_0000~0x300F_FFFF,共1MB11
0x3010_0000~0x301F_FFFF,共1MB0x3010_0000~0x301F_FFFF,共1MB11
………………
0x33F0_0000~0x33FF_FFFF,共1MB0x33F0_0000~0x33FF_FFFF,共1MB11

4)需要将S3C2440的寄存器地址映射到同样的物理地址。在S3C2440数据手册P56,或者配套书籍P88中,提到S3C2440的寄存器地址范围处在0x4800_0000~0x5B00_001C。对于寄存器,我们需要得到它的最新的值,因此这里不使用C,也不使用B。

虚拟地址       物理地址CB
0x4800_0000~0x480F_FFFF0x4800_0000~0x480F_FFFF00
0x4810_0000~0x481F_FFFF0x4810_0000~0x481F_FFFF00
…………
0x5B00_0000~0x5B0F_FFFF0x5B00_0000~0x5B0F_FFFF00

5)涉及LCD的话,还有Framebuffer需要映射。比如 Framebuffer 的地址为0x33C0_0000,我们需要将它映射到同样的物理地址。由于我们需要立即显示,所以不能使用C,也不能使用B。另外在3)中其实已经完成了映射,但是CB的设置不一样,所以要重新设置,以覆盖3)中的设置。

虚拟地址       物理地址CB

0x33C0_0000~0x33CF_FFFF

0x33C0_0000~0x33CF_FFFF00

6)为了验证映射是成功的,我们故意将链接脚本中的链接地址由0x3000_0000修改为0xB000_0000,并且把0xB000_0000开始的1MB虚拟地址映射到0x3000_0000开始的1MB物理地址,这块空间可以C也可以B。

虚拟地址       物理地址CB

0xB000_0000~0xB00F_FFFF

0x3000_0000~0x300F_FFFF11

既然你把链接地址设置为 0xB000_0000,如果想使用0xB000_0000,必须是使能MMU之后才能使用。则start.S文件需要修改一下:

也就是在重定位之前,就必须创建页表与使能MMU,否则0xB开头的地址无法使用。如何理解呢?重定位代码copy2sdram中,需要用到由链接脚本提供的地址,如下图所示。既然你修改后的链接地址0xB000_0000需要经过映射后才是SDRAM的物理地址,如果你想把代码重定位到SDRAM中,则必须先开启MMU。如果你不开始MMU,则0xB000_0000这个地址自身也不是SDRAM的物理地址,无法完成重定位的(如果你将链接地址改为0x3100_0000,则无论开启MMU与否都可以完成重定位,因为这个地址是SDRAM的一个物理地址。然而这就无法验证映射是否成功了)。

4.2.2 编写创建页表函数

我们新建一个mmu.c文件,在里面实现crear_page_table函数。

所谓创建页表,也就是把虚拟地址对应的段描述符填充好。在3.2节中,我们知道段描述符的格式如下:

(1)首先,根据“Section”格式,将每位的操作定义成宏:

#define MMU_SECDESC_AP      (3<<10)
#define MMU_SECDESC_DOMAIN  (0<<5)
#define MMU_SECDESC_NCNB    (0<<2) //不使用C不使用B
#define MMU_SECDESC_WB      (3<<2) //使用C使用B,则是1.3.2提到的WB方式,或者P110
#define MMU_SECDESC_TYPE    ((1<<4) | (1<<1))

#define MMU_SECDESC_FOR_IO   (MMU_SECDESC_AP | MMU_SECDESC_DOMAIN | MMU_SECDESC_NCNB | MMU_SECDESC_TYPE)
#define MMU_SECDESC_FOR_MEM  (MMU_SECDESC_AP | MMU_SECDESC_DOMAIN | MMU_SECDESC_WB   | MMU_SECDESC_TYPE)

#define IO  1
#define MEM 0

1)将AP设置为11。由3.4节可知,设置为11表示“在所有模式下允许任何访问”,即任何模式下都是可读可写的。不过AP的设置,是以domain的权限值被设置为“01”为前提的。

2)将domain为0(注意这里是明确哪一个域,而非某个域的权限)。由3.4节可知,将domain设置为0,则这1MB的内存属于域0。后续我们通过CP15的C3寄存器的bit[1:0],将域0的访问权限设置为11(那么上面将AP设置为11就没有啥意义了),表示不进行权限检查,直接访问。

3)MMU_SECDESC_NCNB、MMU_SECDESC_WB 的含义,见代码注释。

4)MMU_SECDESC_TYPE表示一些固定设置:bit[4]、bit[1]必须设置为1,bit[0]必须为0。

5)MMU_SECDESC_FOR_IO(对于GPIO,我们使用NCNB方式)、MMU_SECDESC_FOR_MEM(对于内存,我们使用WB方式)。

(2)接下来设置将页表(由3.2节的分析可知页表大小为16KB)保存在内存的哪个位置。随便选择一个没使用过的空间即可,这里选择0x32000000。

void create_page_table(void)
{
	/* 1. 页表在哪? 0x32000000(占据16KB) */
	/* ttb: translation table base,即页表基地址 */
	unsigned int *ttb = (unsigned int *)0x32000000;

(3)然后根据MVA、PA,依次设置页表的描述符。

比如对于0~(1M-1)这1MB的虚拟地址,我们要把它映射到相同的物理地址,那如何设置它对应的描述符呢?

首先根据VA得知它对应页表的哪一项(你可以把页表看作一个数组,而页表中的某一项描述符相当于数组的某一元素,只不过这个元素的大小是4字节):index=(VA / 0x100000)= 0这项(相当于数组下标为0的这一个元素)。

然后如何建立映射关系呢?我们只需要根据描述符的格式来填充描述符,就建立好了映射关系(我们人眼虽然觉得描述符所体现的映射关系很不直观,但描述符它是给MMU看的,MMU能从中得出映射关系,然后给定一个MVA,它就能返回一个PA)。描述符的格式如下:

所以有以下代码(为什么这样写的原因,见上面3.3第(3)点的描述,以及本节(1)的5)):

上面写成数组的形式,比较直观形象一些,但也可以像配套书籍P118那样以指针的形式来写代码(VA>>20其实就是/0x100000):

我们可以遵照上面的方式,依次设置页表的描述符,但为了简洁,这里写一个函数来填充描述符:

void create_secdesc(unsigned int *ttb, unsigned int va, unsigned int pa, int io)
{                  //页表基址     //1MB虚拟地址的起始地址  //1MB物理地址的起始地址
	int index;
	index = va / 0x100000;

	if (io)
		ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_IO;
	else
		ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_MEM;
}

如此一来,create_page_table函数的完整代码如下:

void create_page_table(void)
{
	/* 1. 页表在哪? 0x32000000(占据16KB) */
	/* ttb: translation table base */
	unsigned int *ttb = (unsigned int *)0x32000000;

	unsigned int va, pa;
	int index;

	/* 2. 根据va,pa设置页表条目 */

	/* 2.1 for sram(NAND启动时)或者 nor flash(NOR启动时) */ 
	create_secdesc(ttb, 0, 0, MEN);// 0~(1M-1)

	/* 2.2 for sram when nor boot */ 
	create_secdesc(ttb, 0x40000000, 0x40000000, MEM);//Nor启动时,栈需要映射

	/* 2.3 for 64M sdram */
	va = 0x30000000;
	pa = 0x30000000;
	for (; va < 0x34000000;)
	{
		create_secdesc(ttb, va, pa, MEM);
		va += 0x100000;
		pa += 0x100000;
	}

	/* 2.4 for register: 0x48000000~0x5B00001C */
	va = 0x48000000;
	pa = 0x48000000;
	for (; va <= 0x5B000000;)//这个地址好像有点问题,不需要改为0x5B10_0000么?
	{
		create_secdesc(ttb, va, pa, IO);
		va += 0x100000;
		pa += 0x100000;
	}

	/* 2.5 for Framebuffer : 0x33c00000 */
	create_secdesc(ttb, 0x33c00000, 0x33c00000, IO);

	/* 2.6 for link address */
    //我们的程序应该不会超过1MB,所以我们只需要映射一个页表项即可。
	create_secdesc(ttb, 0xB0000000, 0x30000000, IO);
}

(1)注意一下,TTB是“Translation Table Base”即“转换表(也叫页表)基址”的英文首字母。

(2)从上面可以看出,要映射的虚拟地址很恰巧地都是1MB对齐的(地址末尾起码有20个0),这是为何呢?因为虚拟地址到物理地址的映射,是以1MB为单位进行映射的,这1MB虚拟地址的起始地址必须是1MB对齐的。 

4.3 启动MMU

在start.S文件最后添加以下代码(配套书籍P120使用内嵌汇编的方式,语法相对复杂且很少用到,因此这里直接以汇编语言编写):

  1. 把页表基址告诉MMU;
  2. 设置域为0xffffffff, 不进行权限检查;
  3. 使能ICache、DCache、MMU;
  4. 函数返回“mov pc,lr”。
mmu_enable: 
	/* 把页表基址告诉cp15 */
	ldr r0, =0x32000000
	mcr p15, 0, r0, c2, c0, 0

	/* 设置域为0xffffffff, 不进行权限检查 */
	ldr r0, =0xffffffff
	mcr p15, 0, r0, c3, c0, 0

	/* 使能icache,dcache,mmu */
	mrc p15, 0, r0, c1, c0, 0
	orr r0, r0, #(1<<12)  /* enable icache */
	orr r0, r0, #(1<<2)  /* enable dcache */
	orr r0, r0, #(1<<0)  /* enable mmu */
	mcr p15, 0, r0, c1, c0, 0	

	mov pc, lr

以下是对这段代码的解释说明:

(1)在4.2节中我们创建了页表,页表基址是0x32000000。我们需要把页表基址告知CP15,如何告知呢?在S3C2410的数据手册上搜索TTB,在P540有以下内容,这说明把页表基址写入协处理器CP15的C2寄存器中即可,另外如何写入C2寄存器也给出了指令用法。

(2)在2.2小节中我们知道CP15中各个寄存器的作用,其中C1寄存器是控制寄存器,它的位含义如下所示:

如何使能ICache、DCache、MMU呢?我们只需要“读改写”,即先读出C1的内容,然后修改C1的内容,最后将修改后的内容写回C1即可。这里使能ICache、DCache、MMU,于是有以下代码:

	/* 使能icache,dcache,mmu */
    //先读出C1寄存器的内容,存储到r0中
	mrc p15, 0, r0, c1, c0, 0  
    //根据需要,修改r0的内容
	orr r0, r0, #(1<<12)  /* enable icache */
	orr r0, r0, #(1<<2)  /* enable dcache */
	orr r0, r0, #(1<<0)  /* enable mmu */
    //将修改后的r0,写入C1寄存器中
	mcr p15, 0, r0, c1, c0, 0	

(3)在4.2.2小节中,我们已经将众多的1MB地址空间的域都设置为域0(每个描述符中domain位都是0):

#define MMU_SECDESC_DOMAIN  (0<<5)

这里进一步设置域0的权限值为“11”,表示不进行权限检查,允许任何访问;而某个域的权限值,是通过CP15的C3寄存器来设置的。 

 

于是有以下代码:

	/* 设置域为0xffffffff, 不进行权限检查 */
	ldr r0, =0xffffffff
	mcr p15, 0, r0, c3, c0, 0

4.4 测试效果

由于新添加了mmu.c文件,需要在Makefile文件中进行修改:

重新编译烧写运行,发现程序可以运行,但执行效率比较低,甚至比不上2.4小节的测试效果,这是显然有问题的。

我们查看代码,修正了以下错误:之前我们以IO方式(即NCNB)来映射,那肯定是不行的,需要MEN方式映射。此时虚拟地址0xB0000000对应的物理空间才会使用Cache和write buffer,提高执行效率。

重新编译烧写运行,发现程序可以运行,且执行效率比2.4小节中只使能ICache的情形更快(实验现象见链接)。这是因为本次实验同时启动了ICache和DCache,可以极大地提高程序的执行效率。值得注意的是,DCache只有使能MMU之后才能使用。

4.5 解决程序不支持NOR启动

本次实验中,发现程序不支持NOR启动。这是什么原因呢?

NAND启动和NOR启动的唯一区别,在于代码重定位时,代码的来源不一样。

代码重定位copy2sdram函数中,调用isBootFromNorFlash函数来判断是哪种启动方式。该函数内容如下:它通过往0地址写入数字0x12345678,然后读出0地址的内容,根据读到的内容是否为0x12345678(是否修改成功)来判断是NAND启动还是NOR启动。

这会存在什么问题呢?对于虚拟地址0开始的1MB地址空间,我们是以MEN方式进行映射的,如下所示。

这意味着虚拟地址0开始的1MB地址空间,它所对应的物理空间是使用Cache、使用write buffer的。那么isBootFromNorFlash函数中,往0地址写入数据时,它会写到Cache里面去,在读0地址时,会从Cache中得到数据(而不是直接去访问Nor Flash),也就是说Cache的读写总是成功的,于是程序会一直以为是NAND启动。

这显然是不对的,我们需要修改映射方式,将MEN改为“IO”(NCNB),这样设置后,读写时才会去访问硬件,才能得到正确的结果。


http://www.kler.cn/news/362293.html

相关文章:

  • 基于opencv的人脸闭眼识别疲劳监测
  • JMeter使用不同方式传递接口参数
  • spring boot 集成 dynamic-datasource-spring-boot-starter
  • JavaScript 中怎么判断前端各种运行环境
  • 极狐GitLab 发布安全补丁版本 17.4.2, 17.3.5, 17.2.9
  • uniapp结合uview-ui创建项目
  • 如何使用Git推送本地搭建的仓库以及远程克隆的仓库
  • golang中的上下文
  • 滚雪球学Redis[7.4讲]:Redis在分布式系统中的应用:微服务与跨数据中心策略
  • 016_基于python+django网络爬虫及数据分析可视化系统2024_kyz52ks2
  • Python 应用可观测重磅上线:解决 LLM 应用落地的“最后一公里”问题
  • python如何基于numpy pandas完成复杂的数据分析操作?
  • 华企盾对当前网络安全挑战与应对策略探讨
  • LeetCode102. 二叉树的层序遍历(2024秋季每日一题 43)
  • 毕业设计项目系统:基于Springboot框架的心理咨询评估管理系统,完整源代码+数据库+毕设文档+部署说明
  • python将1格式化为01
  • 思科网络设备命令
  • 9个用于测试自动化的最佳AI测试工具(2024)
  • NoSQL数据库分类简述
  • DSVPN简介与应用
  • Stable Diffusion Web UI 大白话术语解释 (二)
  • 中小型医院网站:Spring Boot开发技巧
  • 【Jmeter】jmeter指定jdk版本启动
  • 利用grid sample优化BevDet
  • ACM CCS 2024现场直击:引爆通信安全新纪元
  • 通过conda install -c nvidia cuda=“11.3.0“ 安装低版本的cuda,但是却安装了高版本的12.4.0