“第021课 MMU和Cache”的版本间的差异
(未显示3个用户的22个中间版本) | |||
第1行: | 第1行: | ||
+ | <div body style="width:800px;"> | ||
=第001节_Cache简述及协处理器指令= | =第001节_Cache简述及协处理器指令= | ||
如果对MMU ICache有所了解或者知道其概念作用,那么这节课可以跳过,我们很少会使用MMU或ICache | 如果对MMU ICache有所了解或者知道其概念作用,那么这节课可以跳过,我们很少会使用MMU或ICache | ||
+ | [[File:chapter21_lesson1_001.png|800px]] | ||
+ | 在2440芯片里面除了CPU之外, | ||
+ | Instruction MMU 指令MMU; | ||
+ | |||
+ | Data MMU 数据MMU; | ||
+ | |||
+ | InstructionC ACHE(16KB) 指令cache; | ||
− | |||
− | |||
− | |||
− | |||
− | |||
Data CACHE (16KB) 数据cache | Data CACHE (16KB) 数据cache | ||
全都通过CP15协处理器来进行操作这些 | 全都通过CP15协处理器来进行操作这些 | ||
− | |||
协处理器的含义作用 | 协处理器的含义作用 | ||
第17行: | 第19行: | ||
写一个程序,0到100求和 | 写一个程序,0到100求和 | ||
+ | <syntaxhighlight lang="c" > | ||
int sum() | int sum() | ||
{ | { | ||
第25行: | 第28行: | ||
return sum; | return sum; | ||
} | } | ||
+ | </syntaxhighlight> | ||
查看反汇编代码 | 查看反汇编代码 | ||
− | + | ||
+ | [[File:chapter21_lesson1_002.png|800px]] | ||
+ | |||
局部变量保存在栈中,也就是内存 | 局部变量保存在栈中,也就是内存 | ||
− | + | <syntaxhighlight lang="c" > | |
70: e50b3014 str r3, [fp,#-20] //这个应该就是sum 假设地址是A | 70: e50b3014 str r3, [fp,#-20] //这个应该就是sum 假设地址是A | ||
第38行: | 第44行: | ||
到 a4 | 到 a4 | ||
指令保存在内存中,CPU根据这些执行进行操作 | 指令保存在内存中,CPU根据这些执行进行操作 | ||
+ | </syntaxhighlight> | ||
1 不断的读写地址A和B | 1 不断的读写地址A和B | ||
+ | |||
2 不断的执行for循环里面代码 | 2 不断的执行for循环里面代码 | ||
+ | |||
2.1 取指令 | 2.1 取指令 | ||
+ | |||
2.2 执行指令 | 2.2 执行指令 | ||
问SDRAM非常慢,那么怎么提高程序执行效率? | 问SDRAM非常慢,那么怎么提高程序执行效率? | ||
− | + | 先引入一个感念,程序局部性原理 | |
− | + | *时间局部性:在同一段时间里,有极大的概率访问同一地址的指令或数据 | |
− | 时间局部性:在同一段时间里,有极大的概率访问同一地址的指令或数据 | ||
(在这个for循环中同一个地址指令经常被访问到) | (在这个for循环中同一个地址指令经常被访问到) | ||
− | 空间局部性: 有极大概率访问到相邻空间的指令/数据 | + | *空间局部性: 有极大概率访问到相邻空间的指令/数据 |
我们在一个比较慢的SDRAM上能不能在CPU上开一个高速缓存,把这些指令放进高速缓存icache | 我们在一个比较慢的SDRAM上能不能在CPU上开一个高速缓存,把这些指令放进高速缓存icache | ||
− | + | [[File:chapter21_lesson1_001.png|800px]] | |
+ | |||
指令cache只有16KB 数据cache也只有16KB | 指令cache只有16KB 数据cache也只有16KB | ||
而我们的SDRAM有64MB空间,显然擦车不可能存储SDRAM中所有的内容,它只能存储一部分 | 而我们的SDRAM有64MB空间,显然擦车不可能存储SDRAM中所有的内容,它只能存储一部分 | ||
− | + | ||
+ | [[File:chapter21_lesson1_003.png|800px]] | ||
cache的示意图 | cache的示意图 | ||
以数据开始为例 | 以数据开始为例 | ||
− | 1 程序要读地址A的数据 | + | |
− | + | *1 程序要读地址A的数据 | |
− | a. cpu以地址A查找cache,一开始cache无数据,导致cache miss | + | ldr r0, [A的数据] |
+ | a. cpu以地址A查找cache,一开始cache无数据,导致cache miss | ||
返回一系列的数据,叫做cache line: 8word 32byte | 返回一系列的数据,叫做cache line: 8word 32byte | ||
第76行: | 第88行: | ||
数据写 | 数据写 | ||
− | write buffer | + | write buffer |
− | 查看2410芯片手册 附录 | + | 查看2410芯片手册 附录 <code> appendix4-caches, write buffer</code> |
585页 | 585页 | ||
− | + | ||
+ | [[File:chapter21_lesson1_004.png|800px]] | ||
+ | |||
设置为NCNB (no cache no buffer)数据直接到达硬件不经过缓冲器 | 设置为NCNB (no cache no buffer)数据直接到达硬件不经过缓冲器 | ||
第85行: | 第99行: | ||
对于这些寄存器应该设置为NCNB | 对于这些寄存器应该设置为NCNB | ||
+ | [[File:chapter21_lesson1_005.png|800px]] | ||
不使用cache但数据写到buffer中,CPU就不管了 由write buffer直接进行写操作 | 不使用cache但数据写到buffer中,CPU就不管了 由write buffer直接进行写操作 | ||
CPU直接操作下一条指令 | CPU直接操作下一条指令 | ||
− | + | [[File:chapter21_lesson1_006.png|800px]] | |
− | 第一种不使用cache buffer 适用于直接硬件操作 gpio 得到最新数据 | + | |
− | 第二种 不使用cache使用write buffer, cpu把写发给buffer,cpu就可以直接下一条指令 | + | *第一种不使用cache buffer 适用于直接硬件操作 gpio 得到最新数据 |
− | 第三种 WT 写通方式 使用cache不使用buffer,马上写硬件 | + | *第二种 不使用cache使用write buffer, cpu把写发给buffer,cpu就可以直接下一条指令 |
+ | *第三种 WT 写通方式 使用cache不使用buffer,马上写硬件 | ||
CPU直接写给write buffer | CPU直接写给write buffer | ||
由write执行缓慢写操作 | 由write执行缓慢写操作 | ||
− | 第四种 写回方式 | + | *第四种 写回方式 |
− | miss: cpu数据直接到达write buffer | + | miss: cpu数据直接到达write buffer |
− | hit: cpu数据写入cache标记为dirty,让后会在合适的时机由write buffer写给硬件 | + | hit: cpu数据写入cache标记为dirty,让后会在合适的时机由write buffer写给硬件 |
+ | |||
+ | *合适的时机 | ||
− | |||
cache替换时dirty会写给write buffer写给硬件 | cache替换时dirty会写给write buffer写给硬件 | ||
+ | |||
或者强制Flash cache 写给write buffer 写给硬件 | 或者强制Flash cache 写给write buffer 写给硬件 | ||
− | |||
下节讲协处理指令 | 下节讲协处理指令 | ||
第108行: | 第125行: | ||
=第002节_协处理器指令_开启ICache代码示例= | =第002节_协处理器指令_开启ICache代码示例= | ||
− | + | [[File:chapter21_lesson1_001.png|800px]] | |
+ | |||
CPU中还有许多协处理器来协助主处理功能 | CPU中还有许多协处理器来协助主处理功能 | ||
比如2440有CP0 ~ CP15一共16个协处理器 | 比如2440有CP0 ~ CP15一共16个协处理器 | ||
第116行: | 第134行: | ||
协处理器指令 | 协处理器指令 | ||
先看硬件结构 | 先看硬件结构 | ||
− | + | ||
+ | [[File:chapter21_lesson1_007.png|800px]] | ||
+ | |||
CP15中也有许多寄存器 C0 ~ C15 启动C7’ 是备份寄存器 | CP15中也有许多寄存器 C0 ~ C15 启动C7’ 是备份寄存器 | ||
第122行: | 第142行: | ||
我们需要引入协处理器指令 | 我们需要引入协处理器指令 | ||
− | mrc | + | |
− | mov r1, r0 | + | mrc |
+ | mov r1, r0 | ||
结果是r0 =传给=> r1 | 结果是r0 =传给=> r1 | ||
− | mrc | + | mrc |
− | c coprocessor =传给=> register | + | c coprocessor =传给=> register |
− | |||
− | |||
第137行: | 第156行: | ||
得到语法格式 | 得到语法格式 | ||
− | + | [[File:chapter21_lesson1_008.png|800px]] | |
− | <MCR|MRC>{cond} p#,<expression1>,Rd,cn,cm{,<expression2>} | + | <MCR|MRC>{cond} p#,<expression1>,Rd,cn,cm{,<expression2>} |
举个例子 | 举个例子 | ||
− | mcr P15, 0, r1,c1 | + | mcr P15, 0, r1,c1 |
把主处理器的值发给协处理器 | 把主处理器的值发给协处理器 | ||
− | expression1 值设置为0,表示用不到 | + | expression1 值设置为0,表示用不到 |
− | r1 是主cpu寄存器里面的值 | + | r1 是主cpu寄存器里面的值 |
− | c1 是cp15寄存器里的值 | + | c1 是cp15寄存器里的值 |
− | + | cm, 用不到,写为c0 | |
− | cm, 用不到,写为c0 | + | expression2 值设置为0,表示用不到 |
− | expression2 值设置为0,表示用不到 | + | cm和expression2用来区分哪一个c1,一般写为c0, 0 |
− | cm和expression2用来区分哪一个c1,一般写为c0, 0 | ||
这条命令表示主cpu中r1 值写入 协处理器cp15 中的c1寄存器 | 这条命令表示主cpu中r1 值写入 协处理器cp15 中的c1寄存器 | ||
− | |||
反过来要从cp15寄存器读到主cpu寄存器 | 反过来要从cp15寄存器读到主cpu寄存器 | ||
− | mrc p15, 0, r1, c1, c0, 0 | + | mrc p15, 0, r1, c1, c0, 0 |
这条命令表示协处理器cp15 c1寄存器的值读出来写入主cpu的r1寄存器 | 这条命令表示协处理器cp15 c1寄存器的值读出来写入主cpu的r1寄存器 | ||
2410手册中有讲cp15寄存器的作用 | 2410手册中有讲cp15寄存器的作用 | ||
− | + | ||
+ | [[File:chapter21_lesson1_009.png|800px]] | ||
+ | |||
其中寄存器1控制寄存器 | 其中寄存器1控制寄存器 | ||
下图为介绍控制寄存器1的功能 | 下图为介绍控制寄存器1的功能 | ||
− | + | ||
+ | [[File:chapter21_lesson1_0010.png|800px]] | ||
bit12位是控制cache指令的开启或者关闭,我们等下把bit 12设置为1 | bit12位是控制cache指令的开启或者关闭,我们等下把bit 12设置为1 | ||
c7里面有许多不同的寄存器,对应不同的功能 | c7里面有许多不同的寄存器,对应不同的功能 | ||
− | + | ||
+ | [[File:chapter21_lesson1_0011.png|800px]] | ||
+ | |||
寄存器7表示用来操作cache,根据语法规则cm{,<expression2>} 来区分选择那个c7 | 寄存器7表示用来操作cache,根据语法规则cm{,<expression2>} 来区分选择那个c7 | ||
第175行: | 第197行: | ||
打开start.s | 打开start.s | ||
+ | <syntaxhighlight lang="c" > | ||
reset: | reset: | ||
/* 关闭看门狗 */ | /* 关闭看门狗 */ | ||
第275行: | 第298行: | ||
mcr p15, 0, r0, c1, c0, 0 //吧修改好的r0写给cp15的c1寄存器 | mcr p15, 0, r0, c1, c0, 0 //吧修改好的r0写给cp15的c1寄存器 | ||
mov pc, lr | mov pc, lr | ||
+ | </syntaxhighlight> | ||
+ | 刷屏效率变快 | ||
+ | |||
+ | = 第003节_MMU及地址映射 = | ||
+ | 对于JZ2440它有64M内存(SDRAM),假设现在有N个APP同时运行,则: | ||
+ | |||
+ | ①它们同时保存在SDRAM里; | ||
+ | |||
+ | ②它们的地址各不相同; | ||
+ | |||
+ | |||
+ | 之前我们讲过链接地址,链接地址就是程序运行时所处地址。 | ||
+ | |||
+ | 假设APP1所处的地址是Addr1,APP2所处的地址是Addr2,APPn所处的地址是Addrn, | ||
+ | |||
+ | 则编译某个App时,需要单独指定它的的链接地址,这是一个不可能完成的任务。 | ||
+ | |||
+ | 因为,假如只有几个程序,为每个程序单独的指定地址还能够实现,但对于一个开放式的嵌入式系统,应用程序可能有成百上千个,你不可能重新编译这成百上千的应用程序,并且这些应用程序运行时保存的地址,也是不可预料的。 | ||
+ | |||
+ | |||
+ | 为了解决上述问题,于是就引入了'''虚拟地址'''。 | ||
+ | |||
+ | 也就是说虽然这些应用程序它们保存在内存中的位置各不一样,但对于CPU,它们运行时,都在同一个虚拟地址上。 | ||
+ | |||
+ | |||
+ | 举个例子,如视频中的两个hello应用程序,编译后查看反汇编代码,可以看到这两个程序的起始地址都是0x80A4。于是CPU运行两个APP时,都会去0x80A4读指令,然后经过MMU转换成Addr1、Addr2。这样,不同的APP可以在任意地址,经过MMU地址转换后,在内存上是不同的地址,互不干扰。 | ||
+ | [[File:chapter21_lesson3_001.jpg|700px]] | ||
+ | |||
+ | 这里说的同时运行,并不是真正的同时运行,CPU是分时操作,APP1先工作很短一段时间,再APP2工作很短的一段时间,宏观的来看就是两个在同时工作。 | ||
+ | |||
+ | '''因此,引入虚拟机地址的原因之一:让APP可以以同样的链接地址来编译;''' | ||
+ | |||
+ | |||
+ | 在电子系统里面,内存都是有限的,无论是嵌入式系统还是电脑,比如我们的JZ2440内存就只有64M,这时假如有一个APP,需要1G的内存。应用程序执行时,不是一次性将所有代码都放入内存,而是将要运行的部分依次放入,当放入的代码指令大于64M后,会先将SDRAM里暂时用不到代码指令先置换出来,再放入需要运行的代码指令。这样尽管SDRAM很小,也可以运行内存需要很大的应用程序,而这个置换管理的工作,就是由MMU完成的。 | ||
+ | [[File:chapter21_lesson3_002.jpg|700px]] | ||
+ | |||
+ | '''因此,引入虚拟机地址的原因之二:让大容量APP可以在资源少的系统上运行;''' | ||
+ | |||
+ | |||
+ | 此外,不同的APP之间应该相互独立,避免APP1能直接访问到APP2,以防止APP1影响APP2。 | ||
+ | |||
+ | '''因此,引入虚拟机地址的原因之三:权限管理,禁止访问其它空间;''' | ||
+ | |||
+ | |||
+ | CPU发出虚拟地址(VA)到达MMU,MMU转换成物理地址(PA)发给硬件,那么MMU怎么根据什么将一个虚拟地址转换成物理地址? | ||
+ | |||
+ | a.表格 | ||
+ | |||
+ | 最简单的方法就是弄一个表格,将VA和PA对应起来,根据VA就能找到PA。这种方法优点是简单,缺点是有点浪费空间,需要同时记录VA和PA的地址。 | ||
+ | |||
+ | |||
+ | b.改进 | ||
+ | |||
+ | 在表格里面,我们只保存PA,PA1对应的VA是0~1M-1,PA2对应的VA是1M~2M-1,以此类推。这样改进后,只需要原来表格容量的一半即可。最后还需要把基地址告诉MMU,启动MMU。 | ||
+ | |||
+ | [[File:chapter21_lesson3_003.jpg|700px]] | ||
+ | |||
+ | |||
+ | '''怎么使用MMU?''' | ||
+ | |||
+ | 1.在内存中创建这些表格(页表); | ||
+ | |||
+ | 2.把页表基地址告诉MMU; | ||
+ | |||
+ | 3.设置CP15,启动MMU; | ||
+ | |||
− | + | 前面图中的是一级页表,对于一级页表,条目/描述符对应的大小是1M,条目/描述符的格式可以参考S3C2410的芯片手册,MMU章节。 | |
+ | [[File:chapter21_lesson3_004.jpg|700px]] | ||
+ | |||
+ | 对于一级页表,我们只需要关系“Section”这一行,里面的PA是物理地址,剩下的AP、Domain、C、B用来进行权限管理。 | ||
+ | |||
+ | |||
+ | 简单插讲一下概念。 | ||
+ | |||
+ | |||
+ | *权限管理: | ||
+ | 权限管理就是是否允许程序访问某块内存,有以下几种情况: | ||
+ | |||
+ | a.完全不允许访问; | ||
+ | |||
+ | b.允许系统模式访问,不允许用户模式访问; | ||
+ | |||
+ | c.用户模式下,根据描述符中的AP决定怎么访问; | ||
+ | |||
+ | |||
+ | *域: | ||
+ | 在CP15寄存器有个C3,用来进行域控制。 | ||
+ | |||
+ | ARM9中,有16个域,每个域用2位来表示4种权限。 | ||
+ | |||
+ | [[File:chapter21_lesson3_005.jpg|700px]] | ||
+ | |||
+ | |||
+ | *条目/描述符(AP): | ||
+ | ①设置domain;查看CP15 C3,确定域权限; | ||
+ | |||
+ | ②如果域权限是01,使用AP来决定; | ||
+ | |||
+ | AP来自页表中的描述符,S、R来自CP15中的C1; | ||
+ | |||
+ | [[File:chapter21_lesson3_006.jpg|700px]] | ||
+ | |||
+ | 最后再来补充一个概念,前面我们运行多个APP,切换进程时,需要重新把0x80B4地址对应到不同的物理地址上,也就是说,每切换一个进程,你都需要重新修改下页表,这个开销非常的大,那有什么办法优化呢? | ||
+ | |||
+ | 引入'''MVA''',也就是修改后的虚拟地址。 | ||
+ | |||
+ | <syntaxhighlight lang="c" > | ||
+ | if (VA<32M) | ||
+ | MVA=VA|(pid<<25); | ||
+ | else | ||
+ | MVA=VA; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 当虚拟地址小于32M时,MVA和进程的PID有关,否则等于VA,这就可以解决切换进程,频繁构造页表的问题。 | ||
+ | 假设现在有两个APP,分别是APP1和APP2,链接地址都是0x80b4,PID分别是1和2。 | ||
+ | |||
+ | ①当CPU运行APP1时,发出VA,MVA=VA(1<<25),对应的页表是PA=APP1所在的内存; | ||
+ | |||
+ | ②当CPU运行APP2时,发出VA,MVA=VA(2<<25),对应的页表是PA=APP2所在的内存; | ||
+ | |||
+ | 虽然我们发出的都是同一个VA,但因为PID不一样,所对应的页表项也就不一样,也就不需要重新去构造页表,这样进程从APP1切换到APP2时,只需要修改PID即可,不需要去重新创建页表,这样就可以提高切换效率。 | ||
+ | |||
+ | |||
+ | = 第004节_MMU代码示例 = | ||
+ | 这节课开始编写MMU代码,从上一个程序的基础上修改。 | ||
+ | |||
+ | 首先打开Start.S,我们需要创建页表,启动MMU,页表是保存在SDRAM里面的,也就在内存初始化之后,创建页表。 | ||
+ | |||
+ | <syntaxhighlight lang="c" > | ||
+ | /* 创建页表 */ | ||
+ | bl create_page_table | ||
+ | |||
+ | /* 启动MMU */ | ||
+ | bl mmu_enable | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 新建一个mmu.c文件。 | ||
+ | |||
+ | 在创建一个一级页表前,我们要先确定要映射哪些虚拟地址(VA),映射到哪个物理地址(VB),类型是否使用Cache和Buffer(CB)。 | ||
+ | |||
+ | 我们程序一开始运行是从0地址开始运行,为了保证使能MMU后,前后的地址保持一致,0地址这段我们需要映射。 | ||
+ | |||
+ | 在做了一些初始化后,会用到栈,如果是nor启动,栈是0x40000000开始。 | ||
+ | <syntaxhighlight lang="c" > | ||
+ | VA PA CB | ||
+ | 0 0 00 | ||
+ | 0x40000000 0x40000000 11 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 然后映射64M的SDRAM: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | 64M sdram: | ||
+ | VA PA CB | ||
+ | 0x30000000 0x30000000 11 | ||
+ | ...... | ||
+ | 0x33f00000 0x33f00000 11 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 接着是映射寄存器,且不应该使用Cache和Buffer: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | register: 0x48000000~0x5B00001C | ||
+ | VA PA CB | ||
+ | 0x48000000 0x48000000 00 | ||
+ | ....... | ||
+ | 0x5B000000 0x5B000000 00 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 涉及LCD的话,还有Framebuffer: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | Framebuffer : 0x33c00000 | ||
+ | VA PA CB | ||
+ | 0x33c00000 0x33c00000 00 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 同时,为了验证映射成功,先修改链接脚本中的链接地址为0xB00000000,再对应的映射0xB00000000到原来的0x300000000: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | link address: | ||
+ | VA PA CB | ||
+ | 0xB0000000 0x30000000 11 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 根据上一节的“Section”格式,将每位的操作定义成宏: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | #define MMU_SECDESC_AP (3<<10) | ||
+ | #define MMU_SECDESC_DOMAIN (0<<5) | ||
+ | #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) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | 设置页表的第一步,就是设置页表保存的位置在哪,随便选择一个没使用过的空间即可,大小为16K: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | /* ttb: translation table base */ | ||
+ | unsigned int *ttb = (unsigned int *)0x32000000; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 第二步就是根据va,pa依次设置页表条目,这里我们写个函数来完成对应关系: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | #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; | ||
+ | else | ||
+ | ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_MEM; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 然后依次映射每个页表条目: | ||
+ | <syntaxhighlight lang="c" > | ||
+ | /* 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); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 至此,我们完成了MMU的设置,还需要使能MMU。 | ||
+ | 在Start.S里面添加mmu_enable,需要做的步骤有: | ||
+ | 1.把页表基址告诉cp15 | ||
+ | 2.设置域为0xffffffff, 不进行权限检查 | ||
+ | 3.使能icache,dcache,mmu | ||
+ | 4.返回到之前位置 | ||
+ | <syntaxhighlight lang="c" > | ||
+ | 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 | ||
+ | </syntaxhighlight> | ||
+ | 最后修改Makefile,添加mmu.o进行测试。 | ||
='''《《所有章节目录》》'''= | ='''《《所有章节目录》》'''= | ||
− | + | </div> | |
<categorytree mode=all background-color:white;">ARM裸机加强版</categorytree> | <categorytree mode=all background-color:white;">ARM裸机加强版</categorytree> | ||
[[Category:ARM裸机加强版]] | [[Category:ARM裸机加强版]] | ||
+ | [[Category:Jz2440]] | ||
+ | [[Category:MMU]] | ||
+ | [[Category:Cache]] |
2018年4月23日 (一) 11:11的最新版本
第001节_Cache简述及协处理器指令
如果对MMU ICache有所了解或者知道其概念作用,那么这节课可以跳过,我们很少会使用MMU或ICache 在2440芯片里面除了CPU之外, Instruction MMU 指令MMU;
Data MMU 数据MMU;
InstructionC ACHE(16KB) 指令cache;
Data CACHE (16KB) 数据cache
全都通过CP15协处理器来进行操作这些
协处理器的含义作用 coprocessor协助主处理器做某些事情, 比如在ARM系统中有cp0 – cp15一共16个协处理器,其中cp15负责管理mmu icache
写一个程序,0到100求和
int sum()
{
int I;
int sum =0;
for(i=0; I <= 100; i++)
sum += I;
return sum;
}
查看反汇编代码
局部变量保存在栈中,也就是内存
70: e50b3014 str r3, [fp,#-20] //这个应该就是sum 假设地址是A
78: e50b3010 str r3, [fp, #-16] //这个应该就是I 假设地址是B
ldr r3, [fp, #-16] //也就是地址B中取出值
cmp r3, #100 //跟100比较
//如果大于100程序跳到 a8 如果小于100则执行下面的for循环
从7c:
到 a4
指令保存在内存中,CPU根据这些执行进行操作
1 不断的读写地址A和B
2 不断的执行for循环里面代码
2.1 取指令
2.2 执行指令
问SDRAM非常慢,那么怎么提高程序执行效率? 先引入一个感念,程序局部性原理
- 时间局部性:在同一段时间里,有极大的概率访问同一地址的指令或数据
(在这个for循环中同一个地址指令经常被访问到)
- 空间局部性: 有极大概率访问到相邻空间的指令/数据
我们在一个比较慢的SDRAM上能不能在CPU上开一个高速缓存,把这些指令放进高速缓存icache
指令cache只有16KB 数据cache也只有16KB 而我们的SDRAM有64MB空间,显然擦车不可能存储SDRAM中所有的内容,它只能存储一部分
以数据开始为例
- 1 程序要读地址A的数据
ldr r0, [A的数据]
a. cpu以地址A查找cache,一开始cache无数据,导致cache miss
返回一系列的数据,叫做cache line: 8word 32byte
b. cpu把地址A发到SDRAM,读入cache line,成为cache file 把地址A上的数据返回给CPU 2 程序再次读取地址A的数据 a cpu以地址A查找cache,cache hit有数据直接从cache返回数据给CPU
3 程序要读地址B的数据,CPU也是以地址B查找数据,cache hint直接返回
4 cache满了,CPU访问C a cache替换,置换老的数据 b 填充新数据
数据写
write buffer
查看2410芯片手册 附录 appendix4-caches, write buffer
585页
设置为NCNB (no cache no buffer)数据直接到达硬件不经过缓冲器
比如GPFDAT寄存器CPU读寄存器的时候想读到引脚状态,不应该从cache读取老的数据,而是不断直接访问硬件返回最新的数据 对于这些寄存器应该设置为NCNB
不使用cache但数据写到buffer中,CPU就不管了 由write buffer直接进行写操作 CPU直接操作下一条指令
- 第一种不使用cache buffer 适用于直接硬件操作 gpio 得到最新数据
- 第二种 不使用cache使用write buffer, cpu把写发给buffer,cpu就可以直接下一条指令
- 第三种 WT 写通方式 使用cache不使用buffer,马上写硬件
CPU直接写给write buffer 由write执行缓慢写操作
- 第四种 写回方式
miss: cpu数据直接到达write buffer hit: cpu数据写入cache标记为dirty,让后会在合适的时机由write buffer写给硬件
- 合适的时机
cache替换时dirty会写给write buffer写给硬件
或者强制Flash cache 写给write buffer 写给硬件
下节讲协处理指令
第002节_协处理器指令_开启ICache代码示例
CPU中还有许多协处理器来协助主处理功能 比如2440有CP0 ~ CP15一共16个协处理器
CP15管理cache mmu 我们启动cache需要操作CP15 协处理器指令 先看硬件结构
CP15中也有许多寄存器 C0 ~ C15 启动C7’ 是备份寄存器
现在主CPU中某一个值R0传给CP15中的某一个寄存器
我们需要引入协处理器指令
mrc mov r1, r0
结果是r0 =传给=> r1
mrc c coprocessor =传给=> register
mcr 是把主处理器的值发给协处理器
register =传给=> coprocessor
查看一下语法格式
在2440中搜索mrc
<MCR|MRC>{cond} p#,<expression1>,Rd,cn,cm{,<expression2>}
举个例子
mcr P15, 0, r1,c1
把主处理器的值发给协处理器
expression1 值设置为0,表示用不到 r1 是主cpu寄存器里面的值 c1 是cp15寄存器里的值 cm, 用不到,写为c0 expression2 值设置为0,表示用不到 cm和expression2用来区分哪一个c1,一般写为c0, 0
这条命令表示主cpu中r1 值写入 协处理器cp15 中的c1寄存器
反过来要从cp15寄存器读到主cpu寄存器
mrc p15, 0, r1, c1, c0, 0
这条命令表示协处理器cp15 c1寄存器的值读出来写入主cpu的r1寄存器
2410手册中有讲cp15寄存器的作用
其中寄存器1控制寄存器 下图为介绍控制寄存器1的功能
bit12位是控制cache指令的开启或者关闭,我们等下把bit 12设置为1
c7里面有许多不同的寄存器,对应不同的功能
寄存器7表示用来操作cache,根据语法规则cm{,<expression2>} 来区分选择那个c7
接下来写程序使能cache
注意2440里有data cache和指令cache 其中data cache要启用地址映射才可以使用,只能使用指令cache
打开start.s
reset:
/* 关闭看门狗 */
ldr r0, =0x53000000
ldr r1, =0
str r1, [r0]
/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
ldr r0, =0x4C000000
ldr r1, =0xFFFFFFFF
str r1, [r0]
/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8 */
ldr r0, =0x4C000014
ldr r1, =0x5
str r1, [r0]
/* 设置CPU工作于异步模式 */
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0
/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0)
* m = MDIV+8 = 92+8=100
* p = PDIV+2 = 1+2 = 3
* s = SDIV = 1
* FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
*/
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]
/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
* 然后CPU工作于新的频率FCLK
*/
/*
使能icache
*/
bl enable_icache
/* 设置内存: sp 栈 */
/* 分辨是nor/nand启动
* 写0到0地址, 再读出来
* 如果得到0, 表示0地址上的内容被修改了, 它对应ram, 这就是nand启动
* 否则就是nor启动
*/
mov r1, #0
ldr r0, [r1] /* 读出原来的值备份 */
str r1, [r1] /* 0->[0] */
ldr r2, [r1] /* r2=[0] */
cmp r1, r2 /* r1==r2? 如果相等表示是NAND启动 */
ldr sp, =0x40000000+4096 /* 先假设是nor启动 */
moveq sp, #4096 /* nand启动 */
streq r0, [r1] /* 恢复原来的值 */
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
/* 复位之后, cpu处于svc模式
* 现在, 切换到usr模式
*/
mrs r0, cpsr /* 读出cpsr */
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0
/* 设置 sp_usr */
ldr sp, =0x33f00000
ldr pc, =sdram
sdram:
bl uart0_init
bl print1
/* 故意加入一条未定义指令 */
und_code:
.word 0xdeadc0de /* 未定义指令 */
bl print2
swi 0x123 /* 执行此命令, 触发SWI异常, 进入0x8执行 */
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr lr, =halt
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
halt:
b halt
如何使能icache 打开2410芯片手册
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 //吧修改好的r0写给cp15的c1寄存器
mov pc, lr
刷屏效率变快
第003节_MMU及地址映射
对于JZ2440它有64M内存(SDRAM),假设现在有N个APP同时运行,则:
①它们同时保存在SDRAM里;
②它们的地址各不相同;
之前我们讲过链接地址,链接地址就是程序运行时所处地址。
假设APP1所处的地址是Addr1,APP2所处的地址是Addr2,APPn所处的地址是Addrn,
则编译某个App时,需要单独指定它的的链接地址,这是一个不可能完成的任务。
因为,假如只有几个程序,为每个程序单独的指定地址还能够实现,但对于一个开放式的嵌入式系统,应用程序可能有成百上千个,你不可能重新编译这成百上千的应用程序,并且这些应用程序运行时保存的地址,也是不可预料的。
为了解决上述问题,于是就引入了虚拟地址。
也就是说虽然这些应用程序它们保存在内存中的位置各不一样,但对于CPU,它们运行时,都在同一个虚拟地址上。
举个例子,如视频中的两个hello应用程序,编译后查看反汇编代码,可以看到这两个程序的起始地址都是0x80A4。于是CPU运行两个APP时,都会去0x80A4读指令,然后经过MMU转换成Addr1、Addr2。这样,不同的APP可以在任意地址,经过MMU地址转换后,在内存上是不同的地址,互不干扰。
这里说的同时运行,并不是真正的同时运行,CPU是分时操作,APP1先工作很短一段时间,再APP2工作很短的一段时间,宏观的来看就是两个在同时工作。
因此,引入虚拟机地址的原因之一:让APP可以以同样的链接地址来编译;
在电子系统里面,内存都是有限的,无论是嵌入式系统还是电脑,比如我们的JZ2440内存就只有64M,这时假如有一个APP,需要1G的内存。应用程序执行时,不是一次性将所有代码都放入内存,而是将要运行的部分依次放入,当放入的代码指令大于64M后,会先将SDRAM里暂时用不到代码指令先置换出来,再放入需要运行的代码指令。这样尽管SDRAM很小,也可以运行内存需要很大的应用程序,而这个置换管理的工作,就是由MMU完成的。
因此,引入虚拟机地址的原因之二:让大容量APP可以在资源少的系统上运行;
此外,不同的APP之间应该相互独立,避免APP1能直接访问到APP2,以防止APP1影响APP2。
因此,引入虚拟机地址的原因之三:权限管理,禁止访问其它空间;
CPU发出虚拟地址(VA)到达MMU,MMU转换成物理地址(PA)发给硬件,那么MMU怎么根据什么将一个虚拟地址转换成物理地址?
a.表格
最简单的方法就是弄一个表格,将VA和PA对应起来,根据VA就能找到PA。这种方法优点是简单,缺点是有点浪费空间,需要同时记录VA和PA的地址。
b.改进
在表格里面,我们只保存PA,PA1对应的VA是0~1M-1,PA2对应的VA是1M~2M-1,以此类推。这样改进后,只需要原来表格容量的一半即可。最后还需要把基地址告诉MMU,启动MMU。
怎么使用MMU?
1.在内存中创建这些表格(页表);
2.把页表基地址告诉MMU;
3.设置CP15,启动MMU;
前面图中的是一级页表,对于一级页表,条目/描述符对应的大小是1M,条目/描述符的格式可以参考S3C2410的芯片手册,MMU章节。
对于一级页表,我们只需要关系“Section”这一行,里面的PA是物理地址,剩下的AP、Domain、C、B用来进行权限管理。
简单插讲一下概念。
- 权限管理:
权限管理就是是否允许程序访问某块内存,有以下几种情况:
a.完全不允许访问;
b.允许系统模式访问,不允许用户模式访问;
c.用户模式下,根据描述符中的AP决定怎么访问;
- 域:
在CP15寄存器有个C3,用来进行域控制。
ARM9中,有16个域,每个域用2位来表示4种权限。
- 条目/描述符(AP):
①设置domain;查看CP15 C3,确定域权限;
②如果域权限是01,使用AP来决定;
AP来自页表中的描述符,S、R来自CP15中的C1;
最后再来补充一个概念,前面我们运行多个APP,切换进程时,需要重新把0x80B4地址对应到不同的物理地址上,也就是说,每切换一个进程,你都需要重新修改下页表,这个开销非常的大,那有什么办法优化呢?
引入MVA,也就是修改后的虚拟地址。
if (VA<32M)
MVA=VA|(pid<<25);
else
MVA=VA;
当虚拟地址小于32M时,MVA和进程的PID有关,否则等于VA,这就可以解决切换进程,频繁构造页表的问题。
假设现在有两个APP,分别是APP1和APP2,链接地址都是0x80b4,PID分别是1和2。
①当CPU运行APP1时,发出VA,MVA=VA(1<<25),对应的页表是PA=APP1所在的内存;
②当CPU运行APP2时,发出VA,MVA=VA(2<<25),对应的页表是PA=APP2所在的内存;
虽然我们发出的都是同一个VA,但因为PID不一样,所对应的页表项也就不一样,也就不需要重新去构造页表,这样进程从APP1切换到APP2时,只需要修改PID即可,不需要去重新创建页表,这样就可以提高切换效率。
第004节_MMU代码示例
这节课开始编写MMU代码,从上一个程序的基础上修改。
首先打开Start.S,我们需要创建页表,启动MMU,页表是保存在SDRAM里面的,也就在内存初始化之后,创建页表。
/* 创建页表 */
bl create_page_table
/* 启动MMU */
bl mmu_enable
新建一个mmu.c文件。
在创建一个一级页表前,我们要先确定要映射哪些虚拟地址(VA),映射到哪个物理地址(VB),类型是否使用Cache和Buffer(CB)。
我们程序一开始运行是从0地址开始运行,为了保证使能MMU后,前后的地址保持一致,0地址这段我们需要映射。
在做了一些初始化后,会用到栈,如果是nor启动,栈是0x40000000开始。
VA PA CB
0 0 00
0x40000000 0x40000000 11
然后映射64M的SDRAM:
64M sdram:
VA PA CB
0x30000000 0x30000000 11
......
0x33f00000 0x33f00000 11
接着是映射寄存器,且不应该使用Cache和Buffer:
register: 0x48000000~0x5B00001C
VA PA CB
0x48000000 0x48000000 00
.......
0x5B000000 0x5B000000 00
涉及LCD的话,还有Framebuffer:
Framebuffer : 0x33c00000
VA PA CB
0x33c00000 0x33c00000 00
同时,为了验证映射成功,先修改链接脚本中的链接地址为0xB00000000,再对应的映射0xB00000000到原来的0x300000000:
link address:
VA PA CB
0xB0000000 0x30000000 11
根据上一节的“Section”格式,将每位的操作定义成宏:
#define MMU_SECDESC_AP (3<<10)
#define MMU_SECDESC_DOMAIN (0<<5)
#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)
设置页表的第一步,就是设置页表保存的位置在哪,随便选择一个没使用过的空间即可,大小为16K:
/* ttb: translation table base */
unsigned int *ttb = (unsigned int *)0x32000000;
第二步就是根据va,pa依次设置页表条目,这里我们写个函数来完成对应关系:
#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;
else
ttb[index] = (pa & 0xfff00000) | MMU_SECDESC_FOR_MEM;
}
然后依次映射每个页表条目:
/* 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的设置,还需要使能MMU。 在Start.S里面添加mmu_enable,需要做的步骤有: 1.把页表基址告诉cp15 2.设置域为0xffffffff, 不进行权限检查 3.使能icache,dcache,mmu 4.返回到之前位置
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
最后修改Makefile,添加mmu.o进行测试。