前言:
目前针对ARM Cortex-A7裸机开发文档及视频进行了二次升级持续更新中,使其内容更加丰富,讲解更加细致,全文所使用的开发平台均为华清远见FS-MP1A开发板(STM32MP157开发板)
针对对FS-MP1A开发板,除了Cortex-A7裸机开发篇外,还包括其他多系列教程,包括Cortex-M4开发篇、FreeRTOS篇、Linux基础及应用开发篇、Linux系统移植篇、Linux驱动开发篇、硬件设计篇、人工智能机器视觉篇、Qt应用编程篇、Qt综合项目实战篇等。除此之外计划针对Linux系统移植篇、Linux驱动开发篇均会进行文档及视频的二次升级更新敬请关注!
开发板更多资料可在评论区下方留言领取~~
ARM 的存储器系统是由多级构成的,可以分为内核级、芯片级、板卡级、外设级,如下图所示
每级都有特定的存储介质,下面对比各级系统中特定存储介质的存储性能。
⚫ 内核级的寄存器。处理器寄存器组可看做是存储器层次的顶层。这些寄存器被集成在处理器内核中,在系统中提供最快的存储器访问。典型的 ARM 处理器有多个 32 位寄存器,其访问时间为ns 量级。
⚫ 片级的紧耦合存储器(TCM,部分处理器有)是为弥补 Cache 访问的不确定性增加的存储器。TCM 是一种快速 SDRAM,它紧挨内核,并且保证取指和数据操作的时钟周期数,这一点对一些要求确定行为的实时算法是很重要的。TCM 位于存储器地址映射中,可作为快速存储器来访问。
⚫ 芯片级的片上 Cache 存储器的容量在 8KB~32KB 之间,访问时间大约为 10ns 级别。高性能的
ARM 结构中,可能存在第二级片外 Cache,容量为几百 KB,访问时间为几十 ns 级别。
⚫ 板卡级的 DRAM。主存储器可能是几 MB 到几十 MB 的动态存储器,访问时间大约为100ns。
⚫ 外设级的后援存储器,通常是硬盘,可能从几百 MB 到几十个 GB,访问时间约为几十 ms 级别。
处理器核内部的存储管理单元主要包含有:Cache、MMU、Write Buffer(写缓存)等部分,以及可以控制相关存储单元的协处理 CP15。下图是一个简易结构图。
上个章节已经了解了 Cortex-A7 内核的寄存器的情况,本章节将整体了解 STM32MP1 的 Cortex-A7 内核的存储结构,如下图所示。
可以看出 Cortex-A7 内核有两级 Cache,而且是哈佛结构的 Cache(早期 ARM7 为冯洛伊曼结构),指令和数据可以同时和 Icache、Dache 交互(这也是 5 级以上流水线的要求,在流水线章节会讲解)。可以描述为如下结构:
在创建多任务嵌入式系统时,最好用一个简单的方式来编写、装载及运行各自独立的任务。目前大多数的嵌入式系统不再使用自己定制的控制系统,而使用操作系统来简化这个过程。较高级的操作系统采用基于硬件的存储管理单元(MMU)来实现上述操作。
MMU 提供的一个关键服务是使各个任务作为各自独立的程序在自己的私有存储空间中运行。在带 MMU的操作系统控制下,运行的任务无须知道其他与之无关的任务的存储需求情况,这就简化了各个任务的设计。
MMU 提供了一些资源以允许使用虚拟存储器(将系统物理存储器重新编址,可将其看成一个独立于系统物理存储器的存储空间)。MMU 作为转换器,将程序和数据的虚拟地址(编译时的连接地址)转换成实际的物理地址,即在物理主存中的地址。这个转换过程允许运行的多个程序使用相同的虚拟地址,而各自存储在物理存储器的不同位置。
这样存储器就有两种类型的地址:虚拟地址和物理地址。虚拟地址由编译器和连接器在定位程序时分配;物理地址用来访问实际的主存硬件模块(物理上程序存在的区域)。
MMU 的开启指令:
1 mrc p15, 0, r1, c1, c0, 0 //Read control register
2 orr r1, #0x1 //Set M bit
3 mcr p15, 0,r1,c1, c0,0 //Write control register and enable MMU
MMU 的关闭指令:
1 mrc p15, 0, r1, c1, c0, 0 //Read control register
2 bic r1, r1, #0x1 //Clr M bit
3 mcr p15, 0,r1,c1, c0,0 //Write control register and enable MMU
Cache 是一个容量小但存取速度非常快的存储器,它保存最近用到的存储器数据副本。对于程序员来说,Cache 是透明的。它自动决定保存哪些数据、覆盖哪些数据。现在 Cache 通常与处理器在同一芯片上实现。Cache 能够发挥作用是因为程序具有局部性。所谓局部性就是指在任何特定的时间,处理器趋于对相同区域的数据(如堆栈)多次执行相同的指令(如循环)。
Cache 经常与写缓存器(write buffer)一起使用。写缓存器是一个非常小的先进先出(FIFO)存储器,位于处理器核与主存之间。使用写缓存的目的是,将处理器核和 Cache 从较慢的主存写操作中解脱出来。当 CPU 向主存储器做写入操作时,它先将数据写入到写缓存区中,由于写缓存器的速度很高,这种写入操作的速度也将很高。写缓存区在 CPU 空闲时,以较低的速度将数据写入到主存储器中相应的位置。
通过引入 Cache 和写缓存区,存储系统的性能得到了很大的提高,但同时也带来了一些问题。例如,由于数据将存在于系统中不同的物理位置,可能造成数据的不一致性;由于写缓存区的优化作用,可能有些写操作的执行顺序不是用户期望的顺序,从而造成操作错误。所以在后续学习驱动开发时,要注意,针对外设硬件寄存器空间的 MMU 内存管理,在页表的 C、B 位设置权限,小心设置,一般选择不支持 Cache、不支持 write buffer。注意:内存管理是分段或页面管理的,不同页面的权限设置不一样。
当然可以可以整体开启或关闭 Cache。
ICache 是在整体 Cache 打开后,可以单独设置开关,MMU 不开启时,也可以使用。
DCache 是依赖于 MMU,只有开启 MMU 后,Dcache 才有效,并且受 MMU 控制。
指令 Cache 的打开和关闭,可以用通用寄存器和 CP15 协处理中的 C1 寄存器进行交互,设置指令Cache 的开关。下图为 C1 寄存器。
我们仍然在导入的 c_led 工程的 start.S 中增加这段代码,调试,观察现象(因为 r0 已经被程序占用,可以使用 r1 寄存器)。
1 /******Cache Test*******/
2 mrc p15,0,r1,c1,c0,0
3 orr r1, r1, #(1 << 2) // Set C bit 整体使能Cache
4 orr r1, r1, #(1 << 12) //Set I bit 使能ICache
5 mcr p15,0,r1,c1,c0,0
6 /******End Test******/
通过设置的断点,可以看到 r1 的值为 0x5187f,对应的 C 位和 I 位为 1,说明 ICache 原本已经是打开的,也就是说增加的代码不影响原来的结果。接下来运行程序,记住 FS-MP1A 开发板上的 Led 灯闪烁频率。注意因为 M 位也为 1,说明
接下来测试关闭 ICache 对程序的影响。还是在刚刚的位置,增加关闭 ICache 的指令。
1 /******Cache Test*******/
2 mrc p15,0,r1,c1,c0,0
3 orr r1, r1, #(1 << 2) // Set C bit 整体使能Cache
4 orr r1, r1, #(1 << 12) //Set I bit 使能Cache
5 bic r1, r1, #(1 << 12)//关闭ICache
6 //bic r1, r1, #(1 << 2)//关闭Cache
7 mcr p15,0,r1,c1,c0,0
8 /******End Test******/
编译执行,可以观察到关闭了 ICache 的代码,点灯速度慢了很多。
处理器按照一系列步骤来执行每一条指令,典型的步骤如下:
1、从存储器读取指令(fetch)。
2、译码以鉴别它属于哪一条指令(decode)。
3、从指令中提取指令的操作数(这些操作数往往存在于寄存器 reg 中)。
4、将操作数进行组合以得到结果或存储器地址(ALU)。
5、如果需要,则访问存储器以存储数据(mem)。
6、将结果写回到寄存器堆(res)
并不是所有的指令都需要上述每一个步骤,但是,多数指令需要其中的多个步骤。这些步骤往往使用不同的硬件功能,如 ALU 可能只在第 4 步中用到。因此,如果一条指令不是在前一条指令结束之前就开始,那么在每一步骤内处理器只有少部分的硬件在使用。
有一种方法可以明显改善硬件资源的使用率和处理器的吞吐量,这就是在当前一条指令结束之前就开始执行下一条指令,即通常所说的流水线(Pipeline)技术。流水线是 RISC 处理器执行指令时采用的机制。使用流水线,可在取下一条指令的同时译码和执行其他指令,从而加快执行的速度。可以把流水线看做是汽车生产线,每个阶段只完成专门的处理器任务。
采用上述操作顺序,处理器可以这样来组织:当一条指令刚刚执行完步骤(1)并转向步骤(2)时,下一条指令就开始执行步骤(1)。从原理上说,这样的流水线应该比没有重叠的指令执行快 6 倍,但由于硬件结构本身的一些限制,实际情况会比理想状态差一些。
到 ARM7 为止的 ARM 处理器使用简单的 3 级流水线,它包括下列流水线级。
1、取指令(fetch):从寄存器装载一条指令。
2、译码(decode):识别被执行的指令,并为下一个周期准备数据通路的控制信号。在这一级,指令占有译码逻辑,不占用数据通路。
3、执行(excute):处理指令并将结果写回寄存器。
如下图所示为:3 级流水线指令的执行过程。
我们仍然在导入的 c_led 工程的 start.S 中增加下面的代码,调试,观察。
1 /****pipeline test begin****/
2 mov r1,pc
3 /****pipeline test end****/
运行结果如下:
可以发现,当指令执行完成后,R1 中的值是在执行阶段赋予的,即 R1=0xc20000a8 是执行阶段 PC 的值,而这条指令本身的地址=0xc20000a0。说明执行阶段的这条指令的地址=PC-8。(注:后文会有描述,虽然 A7 处理器是 8 级流水线,但也符合这个规则)
当处理器执行简单的数据处理指令时,流水线使得平均每个时钟周期能完成 1 条指令。但 1 条指令需要 3 个时钟周期来完成,因此,有 3 个时钟周期的延时(latency),但吞吐率(throughput)是每个周期 1 条指令。下面的情况是三级流水线的最佳情况。
该例中用 6 个时钟周期执行了 6 条指令,所有的操作都在寄存器中(单周期执行),指令周期数(CPI) = 1。
在三级流水线中,如果遇到 LDR、STR 命令,需要访问内存时,如下图所示:
该例中,用 6 周期执行了 4 条指令,指令周期数 (CPI) = 1.5。
所有的处理器都要满足对高性能的要求,直到 ARM7 为止,在 ARM 核中使用的 3 级流水线的性价比是很高的。但是,为了得到更高的性能,需要重新考虑处理器的组织结构。有两种方法来提高性能。
提高时钟频率。时钟频率的提高,必然引起指令执行周期的缩短,所以要求简化流水线每一级的逻辑,流水线的级数就要增加。
减少每条指令的平均指令周期数 CPI。这就要求重新考虑 3 级流水线 ARM 中多于 1 个流水线周期的实现方法,以便使其占有较少的周期,或者减少因指令相关造成的流水线停顿,也可以将两者结合起来。
3 级流水线 ARM 核在每一个时钟周期都访问存储器,或者取指令,或者传输数据。只是抓紧存储器不用的几个周期来改善系统性能,效果并不明显。为了改善 CPI,存储器系统必须在每个时钟周期中给出多于一个的数据。方法是在每个时钟周期从单个存储器中给出多于 32 位数据,或者为指令或数据分别设置存储器。
基于以上原因,较高性能的 ARM 核使用了 5 级流水线,而且具有分开的指令和数据存储器。把指令的执行分割为 5 部分而不是 3 部分,进而可以使用更高的时钟频率,分开的指令和数据存储器使核的 CPI明显减少。
在 ARM9TDMI 中使用了典型的 5 级流水线,5 级流水线包括下面的流水线级。
1、取指令(fetch):从存储器中取出指令,并将其放入指令流水线。
2、译码(decode):指令被译码,从寄存器堆中读取寄存器操作数。在寄存器堆中有 3 个操作数读端口,因此,大多数 ARM 指令能在 1 个周期内读取其操作数。
3、执行(execute):将其中 1 个操作数移位,并在 ALU 中产生结果。如果指令是 Load 或 Store 指令,则在 ALU 中计算存储器的地址。
4、缓冲/数据(buffer/data):如果需要则访问数据存储器,否则 ALU 只是简单地缓冲 1 个时钟周期。
5、回写(write-back):将指令的结果回写到寄存器堆,包括任何从寄存器读出的数据。
如下图所示列出了 5 级流水线指令的执行过程。
在程序执行过程中,PC 值是基于 3 级流水线操作特性的。5 级流水线中提前 1 级来读取指令操作数,得到的值是不同的(PC+4 而不是 PC+8)。这里产生代码不兼容是不容许的。但 5 级流水线 ARM 完全仿真 3 级流水线的行为。在取指级增加的 PC 值被直接送到译码级的寄存器,穿过两级之间的流水线寄存器。下一条指令的 PC+4 等于当前指令的 PC+8,因此,未使用额外的硬件便得到了正确的 R15。
在 Cortex-A7 中有一条 8 级的流水线,但没有找到相关的细节资料,这里只能简单介绍一下。从经典 ARM 系列到现在的 Cortex 系列,ARM 处理器的结构在向复杂的阶段发展,但没改变的是 CPU 的取指指令和地址关系,不管是几级流水线,都可以按照最初的 3 级流水线的操作特性来判断其当前的 PC 位置。这样做主要还是为了软件兼容性上的考虑,由此可以判断的是,后面 ARM 所推出的处理核心都想满足这一特点,感兴趣的读者可以自行查阅相关资料。
在典型的程序处理过程中,经常会遇到这样的情形,即一条指令的结果被用做下一条指令的操作数。例如,有如下指令序列:
LDR R4,[R7]
ORR R8,R3,R4 ;在 5 级流水线上产生互锁
从例子可以看出,流水线的操作产生中断,因为第 1 条指令的结果在第 2 条指令取数时还没有产生。第 2 条指令必须停止,直到结果产生为止。本例中,用了 7 个时钟周期执行 6 条指令, CPI = 1.2 。
但如果将 ORR R8,R3,R4 和 AND R6,R3,R1 调换顺序,则在不影响程序结果的情况下,得到了更好的流水线效果。大家以后学习 Linux 内核时,会遇到内存屏障的概念,有时就是为了防止编译器做这种指令顺序上的优化。
本例中,用了 6 个时钟周期执行 6 条指令, CPI = 1。
跳转指令也会破坏流水线的行为,因为后续指令的取指步骤受到跳转目标计算的影响,因而必须推迟。但是,当跳转指令被译码时,在它被确认是跳转指令之前,后续的取指操作已经发生。这样一来,已经被预取进入流水线的指令不得不被丢弃。如果跳转目标的计算是在 ALU 阶段完成的,那么在得到跳转目标之前已经有两条指令按原有指令流读取。
显然,只有当所有指令都依照相似的步骤执行时,流水线的效率达到最高。如果处理器的指令非常复杂,每一条指令的行为都与下一条指令不同,那么就很难用流水线实现。
Cache 和 MMU 部分,目前只做功能介绍,和简单的测试实验,大家目前需要理解它们的作用。等打好基础后,可以继续了解 Cache 内部的结构,以及控制方法,比如:Cache 的覆盖机制、锁定机制。MMU 的一级页表和二级页表编写,权限管理等。这些内容对应以后嵌入式 Linux 驱动、内核优化、内存管理的学习、实时性的优化都很有意义。编写驱动时遇到的一些疑难问题也可能和这部分存储系统有关系。