目录
Cache简述
对于ARM芯片中包含这指令Cache和数据Cache以及MMU,这些MMU和Cache是通过协处理器(coprocessor)CP15来操作的,协助主处理器,在ARM9系统里面有CP0到CP15总共16个协处理器
下面有一段程序,假设sum为地址A,i为地址B,根据反汇编我们可以发现会不断地读写地址A和B,不断地执行for循环中的代码,取指令和执行指令,在JZ2440中SDRAM非常慢,怎么提高程序执行效率
#include <stdio.h>
int sum()
{
int i;
int sum = 0;
for(i = 0; i <= 100; i++)
sum += i;
return sum;
}
int main()
{
int num;
num = sum();
printf("num = %d\n", num);
return 0;
}
这涉及到程序局部性原理:
- 时间局部性:在同一段时间里,有极大的概率访问同一地址的指令/数据
- 空间局部性:有极大概率访问到相邻空间的指令/数据
把小段空间的程序全部读到指令Cache,执行程序时优先从指令Cache中取指令,如果没有指令再去内存读指令,CPU访问数据时,将数据读到数据Cache,以后读数据时,优先从数据Cache中读取,没有数据再去访问内存,这样就可以加快了速度,在S3C2440中对于指令Cache和数据Cache都只有16KB
如果开启了Cache,对于该程序执行过程概述如下:
1.程序要读取地址A的数据"ldr r0,[A的地址]"
- CPU以地址A查找Cache,一开始Cache没有任何数据,导致Cache miss
- CPU把地址A发到SDRAM (并不止返回地址A的数据,而是返回一系列数据即Cache line,在ARM9中一个Cache line就是8个word即32byte),数据读入Cacheline称为cachefill(填充Cache),并且把A的数据返回给CPU
2.程序再次读取地址A的数据
- CPU以地址A查找Cache,Cache中有数据,即Cachehit(Cache命中),直接从Cache返回数据给CPU
3.程序读地址B的数据,CPU以地址B查找Cache, 之前读地址A的数据中Cache line包含着附近的内容,而B就在A附近,因此也会导致Cachehit,直接返回(空间局部性)
4.假设Cache满了,想访问新数据C时,a.导致Cache替换把老数据置换出去 b.填充新数据
这过程只涉及到数据的读,并没有涉及到数据的写,对于数据Cache和指令Cache的操作是差不多的,对于写的过程涉及到writebuffer,在上图中有体现到,如果我们想读取GPIO的引脚状态,就不应该使用Cache,因为数据需要立马返回,而不应该存在Cache中,CPU应该直接访问硬件,所以对于寄存器我们需要设置为non cache non buffered(即不用Cache和writebuffer),读的时候读硬件,写的时候写硬件,而不是缓冲到cache中去
对于Cache和writebuffer就有四种方式,如下图所示
模式一:不使用Cache和writebuffer
模式二:NCB模式,读和写都会操作到硬件,写的时候CPU直接把数据给writebuffer,然后就去执行下一条指令,有writebuffer来执行缓慢的写操作过程
模式三:WT模式(写通,即马上写硬件),读的时候优先执行Cache的内容,Cache有数据的话直接返回数据,如果没数据会linefill,所有的写操作都会先写入writebuffer,writebuffer会马上把数据写到硬件上去,CPU写给writebuffer就马上执行下一条指令,不等待写操作完成
模式四:WB模式(写回,即使用Cache和writebuffer),读操作是类似的,都优先从Cache里面读数据,写的时候,分为两种情况,一种是Cache miss的话,会直接把数据写给writebuffer,writebuffer会马上写到硬件上去,CPU不等待写操作完成;如果Cachehit的话,CPU会把数据写在Cache并标记为dirty,表示此数据需要更新,在以后合适的时机会写给writebuffer,由writebuffer把数据写到硬件上,写操作是以后的事,CPU只是标记数据
协处理器指令
对ARM芯片除了CPU外还有很多协处理器,协处理器就是协助主处理器,对于ARM9有CP0-CP15总共16个,而CP15用来管理Cache和MMU,想启动Cache,问题是怎么把数据给CP15,或者再总CP15中得到数据,对于CP15里面也有各种寄存器C0-C15,对于C?还含有备份寄存器,例如C7里面有备份寄存器 C7'、C7''、C7'''等 ,假设想访问C7'',需要引入协处理器指令mrc和mcr指令,对于mrc指令是把协处理器里面的值传给CPU寄存器,而mcr指令是把CPU寄存器的值写给协处理器,下面有该指令的介绍
后面两个参数用来区分哪一个C1,一般写为"C0,0",这句话的一是主CPU的r1写入CP15的c1中
mcr P15, 0, r1, c1, c0, 0
CP15的c1的值写入CPU的r1
mrc P15, 0, r1, c1, c0, 0
对于指令Cache需要将CP15中的C1寄存器的bit12设置为1,对于数据Cache需要我们使能MMU后才能开启
程序示例,运行程序,会发现程序执行速度明显增快
enable_icache:
/* 设置协处理器使能icache */
mrc p15, 0, r0, c1, c0, 0
orr r0, r0, #(1<<12) /* r0 = r0 or (1<<12) */
mcr p15, 0, r0, c1, c0, 0
mov pc, lr
MMU及地址映射
对于JZ2440来说有64M的SDRAM,假设有N个APP同时运行,它们保存同时保存在SDRAM中,并且地址各不相同,链接地址为程序运行时所处地址,假设APP1为地址1,APP2为地址2,APPN为地址n,则编译的某个APP的时候,需要单独指定它们的链接地址,这是不可能完成的任务,如果一两个还可以指定,N个APP就实现不了,APP多不可能重新编译,也不可能预测它所处的位置
引入虚拟地址的概念:
下面有两个APP,其反汇编初始运行地址都是0x80b4,APP都处于同一个虚拟地址,CPU都以虚拟地址0x80b4去读取指令,这些虚拟地址(VA)会转换为物理地址(PA),即VA到PA,处理转换的过程就是MMU(负责地址转换),APP同时运行只是对人类来说,而实际上是先让APP1运行若干毫秒,再让APP2运行若干毫秒等轮流运行的,当运行到APP1时,可以让同一块虚拟地址指向APP1所在的物理地址,当运行到APP2时,可以让同一块虚拟地址指向APP2所在的物理地址
$ cat hello1.c
#include <stdio.h>
int main()
{
while(1);
return 0;
}
$ cat hello2.c
#include <stdio.h>
int main()
{
printf("hello,world\n\r");
while(1);
return 0;
}
引入虚拟地址的原因:
- 让APP以同样的链接地址来编译
- 让大容量APP可在资源少的系统上运行,VA到PA的映射都是MMU实现的(SDRAM有限,对于JZ2440来说只有64M,APP假设要求内存需要1G,需要1G内存的应用可否运行在64M的内存上,答案是可以运行的,执行应用程序的时候,只会执行其中的某一段代码,然后如果跳转再去执行另一段代码,不可能一下子执行完1G的指令,所以一开始可以用其中一块VA映射到某一块PA,依次执行并映射,如果SDRAM满了,就先置换出PA的内容然后再让新的VA映射到新的PA上,这样就可以让1G的应用程序在64M的内存运行起来,因此中间有很多置换和映射,这些操作是操作系统完成的)
- MMU不止有地址映射的功能,还有权限管理的功能(APP1、APP2、APPN,如果APP1写得很烂,访问的内存越界了,想去修改APP2的内容,就有可能把APP2给干掉了,因此有权限管理,让APP1只能够访问自己的内存,因此可以禁止APP来访问其他空间)
因此MMU有两个功能,一是地址映射,而是权限管理
CPU发出VA到达MMU,MMU转换为PA然后访问硬件,MMU怎么转换,有一个表格,里面放VA和PA,假设VA1对应PA2,VA2对于PA2...,这样就浪费空间,通过改进表格中只放PA,其中PA1对应0~1M-1,PA2对应1M~2M-1,PA3对应2M~3M-1...,只需要之前表格的一半内存,构建这个表格后需要把表格基地址告诉MMU,然后就可以启动MMU
怎么使用MMU:
- 在内存中创建这个表格,称为页表,(前面说的页表为一级页表,里面的每一项称为条目或者描述符,每一个条目对应1M,1M的物理地址对应1M的虚拟地址,如果想映射更小的范围,需要二级页表,这里不用二级页表,知道MMU的概念就可以了)
- 把页表基址告诉MMU
- 设置CP15启动MMU
对于条目的格式如下图所示,用一级页表只需要关心其中的Section段描述符,其中"Section base address"就是物理地址(PA),还有后面的" AP Domain C B"用来进行权限管理的,其中的C和B是用来表示这块空间是否使用Cache和writebuffer
权限管理功能:
- 完全不允许访问
- 允许系统模式访问,不允许用户模式访问
- 用户模式下,根据描述符中AP的值决定怎么访问
这里需要引入"域(domain)"的概念,对ARM9中CP15的C3有16个域,每个域用2位来表示4中权限,bit31、30设置为00则表示无法访问,即域15,将条目中的domain设置为域15,就用域15的权限控制,这块内存就访问不了
因此对于页表中的条目或者称为描述符的设置过程为:
- 设置domain,查看CP15's C3确定域权限,如果是00就无法访问
- 如果域权限是01,则使用AP来决定权限,AP来自页表中的描述符,而S、R是CP15中控制寄存器C1,根据这些组合来管理权限
对于学习者,我们只关心映射,权限管理可以设置为管理模式,这里补充一个概念,APP处于同一块虚拟地址映射到不同的PA上,每切换一下进程都需要重新修改页表,开销大,解法办法,引入MVA的概念,即修改后的虚拟地址,CPU发出VA到 CP15's C13(含有进程PID) 经过修改后得到MVA,再进入MMU,MMU根据MVA查表页表得到PA,使用PA访问硬件,下面有段程序其中的机制,MMU和Cache看到的都是MVA,在程序中不会去区分VA和MVA,我们提到虚拟地址VA时默认指的是MVA
- 当虚拟地址小于32M时,MVA跟PID有关,这就可以解决切换进程频繁构造页表的问题 ,假设有两个进程 APP1 APP2 链接地址都是0x80b4开始
- 假设PID分别是1和2,首先CPU运行APP1时,发出VA,MVA = VA | (1 << 25) 对应页表PA1即APP1所在物理地址,然后CPU运行APP2时,发出VA,MVA = VA | (2 << 25) 对应页表PA2即APP2所在物理地址
- 因此使用同一块VA,由于PID不一样,对于的页表项也不一样,就不需要重新去构造页表,这样APP1切换到APP2时,只需要修改PID就行了,这样就是MVA引入的原因
if(VA < 32M)
MVA = VA | (PID << 25);
else
MVA = VA;
MMU代码示例
需要创建页表然后启动MMU,页表是保存在SDRAM,对于JZ2440需要先初始化SDRAM才能创建页表,其中create_page_table为C函数
...
bl sdram_init
/* 创建页表 */
bl create_page_table
/* 创建页表 */
bl create_page_table
/* 启动MMU */
bl mmu_enable
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
...
对于创建页表,根据其中的条目描述符定义宏,对于寄存器我们需要设置为不用cache和writebuffer,因此有两种模式IO模式和MEM模式,MEM模式cache和writebuffer都使用,在32位系统中VA虚拟地址为0-4G,条目数为4G除以1M即4096个,每一个条目4字节,因此页表的大小为4096*4即16K
#define MMU_SECDESC_AP (3<<10)
#define MMU_SECDESC_DOMAIN (0<<5) //用域0
#define MMU_SECDESC_NCNB (0<<2)
#define MMU_SECDESC_WB (3<<2)
#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
void create_secdesc(unsigned int *ttb, unsigned int va, unsigned int pa, int io)
{
int index;
index = va / 0x100000;//得到条目位置
if (io)
ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_IO;//对于PA只保留最高的16位
else
ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_MEM;
}
程序从0地址开始运行,为了保证使能MMU,前后地址一致,0地址需要映射,0地址设置为IO模式,是为了对于JZ2440来说为了支持NOR或者NAND启动,其中ttb为SDRAM中一块没有占用的内存,最后需要告诉MMU,将链接地址设置为0xB0000000,因此需要先创建页表启动MMU后才能重定位,对于LCD的framebuffer我们需要设置其地址为IO模式,对于JZ2440的寄存器是从0x48000000~0x5B00001C开始因此都使用IO模式
/* 创建一级页表
* VA PA CB
* 0 0 00
* 0x40000000 0x40000000 11
*
* 64M sdram:
* 0x30000000 0x30000000 11
* ......
* 0x33f00000 0x33f00000 11
*
* register: 0x48000000~0x5B00001C
* 0x48000000 0x48000000 00
* .......
* 0x5B000000 0x5B000000 00
*
* Framebuffer : 0x33c00000
* 0x33c00000 0x33c00000 00
*
* link address:
* 0xB0000000 0x30000000 11
*/
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/nor flash */
create_secdesc(ttb, 0, 0, IO);
/* 2.2 for sram when nor boot */
create_secdesc(ttb, 0x40000000, 0x40000000, MEM);
/* 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;)
{
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 */
create_secdesc(ttb, 0xB0000000, 0x30000000, MEM);
}
启动MMU,需要把地址告诉CP15的C2寄存器,设置所有的域为不进行权限检查,上面用的是域0,由于数据Cache需要再启动MMU才能使能,因此使能之后,程序跑的飞快,比只使能指令Cache还快
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