目录
1、寄存器组
2、堆栈指针 R13
2.1、堆栈
2.2、堆栈区的操作
2.3、Cortex-M3 堆栈的实现
3、连接寄存器 R14
4、程序计数器 R15
5、特殊功能寄存器组
5.1、程序状态寄存器(PSRs 或曰 PSR)
5.2、PRIMASK, FAULTMASK 和 BASEPRI
5.3、控制寄存器(CONTROL)
5.3.1、操作模式
5.3.2、Cortex-M3 的双堆栈机制
6、异常和中断
6.1、 向量表
7、复位操作
本小节通过Cortex-M3 的寄存器组进行展开,分别介绍了CM3的堆栈、处理器模式、异常以及上电复位的等知识。本小节需要特别掌握的知识是处理器的模式和堆栈指针的使用,特别是双堆的使用方式,虽然在工作过程中不需要我们操作这些寄存器,但是对于需要移植操作系统的工程师来说,理解这部分的知识是非常有必要的。为什么CM3要有两个堆栈指针。这么设计的好处是什么?可以带着这个问题阅读下面内容。
1、 寄存器组
如上图所示,CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通用目的”的,但是绝大多数的 16 位指令只能使用 R0‐R7(低组寄存器),而 32 位的 Thumb‐2指令则可以访问所有通用寄存器。特殊功能寄存器有预定义的能,而且必须通过专用的指令来访问。
R0‐R7 也被称为低组寄存器。所有指令都能访问它们。它们的字长全是 32 位,复位后的初始值是不可预料的。
R8‐R12 也被称为高组寄存器。只有很少的 16 位 Thumb 指令能访问它们,32位的指令则不受限制。它们也是 32 位字长,且复位后的初始值是不可预料的。
2、堆栈指针 R13
R13 是堆栈指针。在 CM3 处理器内核中有两个堆栈指针,这两个堆栈指针分别是:
主堆栈指针(MSP),或写作 SP_main。这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
进程堆栈指针(PSP),或写作 SP_process。用于常规的应用程序代码(不处于异常服用例程中时)
要注意的是,当引用 R13(或写作 SP)时,引用到的是当前正在使用的那一个,另一个必须用特殊的指令来访问(MRS,MSR 指令)。并不是每个应用都必须用齐两个堆栈指针。简单的应用程序只使用 MSP就够了。
堆栈指针用于访问堆栈,PUSH 指令和 POP 指令默认使用 SP。
PUSH指令:寄存器入栈指令
POP指令:数据出栈指令
它俩的汇编语言语法,如下例所演示:
PUSH {R0} ; *(--R13)=R0。R13 是 long*的指针 POP {R0} ; R0= *R13++
2.1、堆栈
在介绍CM3的堆栈数据结构之前,有必要先描述堆栈的概念,不然很容易混淆,堆和栈都是对一段连续内存的统称,在程序中堆和栈都是用来存储数据的,不同的地方就是,堆用在动态分配的内存,如使用malloc函数申请的一段空间实际分配的是堆的空间,在操作系统的内存管理算法中,如内存池等都是堆空间的管理。而栈以“先进后出”的缓存方式保存函数调用过程中的中间数据,如函数临时变量等。因为栈是被内核动态管理的,所以被调用函数退出后,中间变量也就失去意义。在内核层面栈保存的是寄存器中的数据。本文将栈统称为堆栈。读者在这里一定要清楚,堆和栈在操作系统的层面是两个概念,不能混为一谈。
言归正传,下面回到介绍PUSH和POP指令对堆栈的操作形式。
如上图,内存地址从上到下依次增大的,所以PUSH和POP指令操作的是向下生长的满堆栈。即数据入栈时,堆栈指针先减一个单元,数据出栈时,先将数据POP出去然后SP自增一个单元。通常在进入一个子程序后,第一件事就是把寄存器的值先PUSH 入堆栈中,在子程序退出前再 POP 曾经 PUSH 的那些寄存器。子程序进入后汇编代码如下所示:
subroutine_1 PUSH {R0-R7, R12, R14} ; 保存寄存器列表 … ; 执行处理 POP {R0-R7, R12, R14} ; 恢复寄存器列表 BX R14 ; 返回到主调函数
可以使用 SP 表示 R13。在程序代码中,MSP 和 PSP 都被称为 R13/SP。可以通过 MRS/MSR 指令来指名道姓地访问具体的堆栈指针。
寄存器的 PUSH 和 POP 操作永远都是 4 字节对齐的——也就是说他们的地址必须是0x4,0x8,0xc,……。即R13 的最低两位被硬线连接到0,并且总是读出0。
2.2、堆栈区的操作
笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由SP给出。寄存器的数据通过PUSH操作存入堆栈,以后用POP操作从堆栈中取回。在PUSH与POP的操作中,SP的值会按堆栈的使用法则自动调整,以保证后续的PUSH不会破坏先前PUSH进去的内容。堆栈的功能就是把寄存器的数据放入内存,以便将来能恢复,当一个任务或一段子程序执行完毕后恢复。正常情况下,PUSH与POP 必须成对使用,而且参与的寄存器,不论是身份还是先后顺序都必须完全一致。当PUSH/POP指令执行时,SP指针的值也根着自减/自增。示例代码如下所示:
如果参与的寄存器比较多,可以使用PUSH和POP操作多个寄存器。像这样:
PUSH {R0-R2} ;压入 R0-R2 PUSH {R3-R5,R8, R12} ;压入 R3-R5,R8,以及 R12
在POP时,可以如下操作:
POP {R0-R2} ;弹出 R0-R2 POP {R3-R5,R8, R12} ;弹出 R3-R5,R8,以及 R12
注意:不管在寄存器列表中,寄存器的序号是以什么顺序给出的,汇编器都将把它们升序排序。然后PUSH指令按照从大到小的顺序依次入栈,POP则按从小到大的顺序依次出栈。如果不按升序写寄存器,有些汇编器可能会给出一个语法错误。
PUSH/POP 还有这样一种特殊形式,形如
PUSH {R0-R3, LR} POP {R0-R3, PC}
注意:POP的最后一个寄存器是PC,并不是先前PUSH的LR。这其实是一个返回的小技巧。因为总要把先前LR的值弹出来,再使用此值返回,干脆绕过LR,直接传给PC!因为 LR 在子程序返回时的唯一用处就是提供返回地址,在返回后,先前保存的返回地址就没有利用价值了,所以只要PC得到了正确的值,不恢复也没关系。
PUSH 指令等效于与使用 R13 作为地址指针的STMDB指令,而POP指令则等效于使用R13作为地址指针的LDMIA指令——STMDB/LDMIA。
2.3、Cortex-M3 堆栈的实现
Cortex‐M3使用的是“向下生长的满栈”模型。堆栈指针SP指向最后一个被压入堆栈的 32位数值。在下一次压栈时,SP先自减4,再存入新的数值。
POP 操作刚好相反:先从SP指针处读出上一次被压入的值,再把SP指针自增4。
重点:在进入ISR时,CM3会自动把一些寄存器压栈,这里使用的是进入ISR之前使用的SP指针(MSP或者是PSP)。离开ISR后,只要ISR没有更改过CONTROL[1],就依然使用先前的SP指针来执行出栈操作。
入栈操作一般发生在以下两种情况:
1)、子程序调用之前对LR有入栈操作
2)、进入子程序之后根据实际情况做现场保护的入栈操作
3)、进入ISR服务程序之前入栈操作(硬件自动执行)
出栈操作一般发生在以下两种情况:
1)、子程序调用结束前恢复现场的出栈操作
2)、退出子程序调用后LR出栈操作
2)、推出中断服务程序之后出栈操作(硬件自动执行)
3、连接寄存器 R14
R14 是连接寄存器(LR)。在一个汇编程序中,可以把它写作 LR 或者 R14。LR 在调用函数或者子程序时存储返回地址。当函数或者子程序执行完成后加载LR到PC寄存器返回调用程序处继续执行。例如,使用 BL(分支并连接,Branch and Link)指令时,就自动填充 LR 的值。子程序调用示例如下:
main ;主程序 … BL function1 ; 使用“分支并连接”指令呼叫 function1 ; PC= function1,并且 LR=main 的下一条指令地址 … Function1 … ; function1 的代码 BX LR ; 函数返回(如果 function1 要使用 LR,必须在使用前 PUSH, ; 否则返回时程序就可能跑飞了)
当函数或者子程序被调用时,LR的值由硬件自动更新,如果被调函数或者子程序还需要调用其他函数或者子程序,在调用函数或者子程序之前需要对LR进行入栈操作,否则被调函数的返回地址会被丢失。
当系统发生异常时,进入异常时,LR被硬件自动更新为EXC_RETURN的值。这个用于异常的退出。
4、程序计数器 R15
R15 是程序计数器,在汇编代码中也可以使用名字“PC”来访问它。如果向PC中写数据,就会引起一次程序的分支(但是不更新 LR 寄存器)。CM3中的指令至少是半字对齐的,所以PC的LSB总是读回0。然而,在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到PC的数值是奇数(即LSB=1),用以表明这是在Thumb 状态下执行。倘若写了0,则视为企图转入ARM模式,CM3将产生一个fault异常。
5、特殊功能寄存器组
Cortex‐M3 中的特殊功能寄存器包括:
程序状态寄存器组(PSRs 或曰 xPSR)
中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及 BASEPRI)
控制寄存器(CONTROL)
它们只能被专用的 MSR 和 MRS 指令访问,指令格式如下所示。
MRS <gp_reg>, <special_reg> ;读特殊功能寄存器的值到通用寄存器 MSR <special_reg>, <gp_reg> ;写通用寄存器的值到特殊功能寄存器
5.1、程序状态寄存器(PSRs 或曰 PSR)
程序状态寄存器如上图所示,在其内部又被分为三个子状态寄存器:
应用程序 PSR(APSR)
中断号 PSR(IPSR)
执行 PSR(EPSR)
通过 MRS/MSR 指令,这 3 个 PSRs 即可以单独访问,也可以组合访问(2 个组合,3 个组合都可以)。当使用三合一的方式访问时,应使用名字“xPSR”或者“PSR”。
个人理解 IPSR使用记录当前的中断编号,CM3根据中断向量基地址和中断编号进行异常入口地址计算,且这个操作有硬件操作完成,提高了异常响应速度。参考STM32的IPSR寄存器定义如下:
5.2、PRIMASK, FAULTMASK 和 BASEPRI
这三个寄存器用于控制异常的使能和除能。
要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:
MRS R0, BASEPRI ;读取 BASEPRI 到 R0 中 MRS R0, FAULTMASK ;似上 MRS R0, PRIMASK ;似上 MSR BASEPRI, R0 ;写入 R0 到 BASEPRI 中 MSR FAULTMASK, R0 ;似上 MSR PRIMASK, R0 ;似上
只有在特权级下,才允许访问这 3 个寄存器。
5.3、控制寄存器(CONTROL)
控制寄存器用于定义特权级别,还用于选择当前使用哪个堆栈指针。
CONTROL[1]
在Cortex‐M3的handler模式中,CONTROL[1]总是0,在线程模式中则可以为0或1。仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。改变处理器的模式也有其它的方式:在异常返回时,通过修改LR的位2,也能实现模式切换。
CONTROL[0]
仅当在特权级下操作时才允许写该位。一旦进入了用户级,唯一返回特权级的途径,就是触发一个(软)中断,再由服务例程改写该位。CONTROL 寄存器也是通过 MRS 和 MSR 指令来操作的,示例如下:
MRS R0, CONTROL
MSR CONTROL, R0
注意:异常进入和退出不会修改CONTROL的值。
5.3.1、操作模式
Cortex‐M3 支持 2 个模式和两个特权等级。
当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面,handler模式总是特权级的。在复位后,处理器进入线程模式+特权级。
在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级。系统特权级变更如下图所示:
把代码按特权级和用户极分开对待,有利于使架构更加安全和健壮。例如,当某个用户代码出问题时,不会让它成为害群之马,因为用户级的代码是禁止写特殊功能寄存器和 NVIC中寄存器的。另外,如果还配有 MPU,保护力度就更大,甚至可以阻止用户代码访问不属于它的内存区域。
为了避免系统堆栈因应用程序的错误使用而毁坏,可以给应用程序专门配一个堆栈,不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用 PSP,而异常服务例程则使用 MSP。这两个堆栈指针的切换是全自动的,就在出入异常服务例程时由硬件处理。
如前所述,特权等级和堆栈指针的选择均由 CONTROL 负责。当 CONTROL[0]=0 时,在异常处理的始末,只发生了处理器模式的转换,如下图所示:
但若 CONTROL[0]=1(线程模式+用户级),则在中断响应的始末,both 处理器模式和特权等极都要发生变化,如下图所示:
CONTROL[0]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使用一条“系统服务呼叫指令(SVC)”来触发“SVC 异常”,该异常的服务例程可以选择修改CONTROL[0]。
5.3.2、Cortex-M3 的双堆栈机制
已经知道了CM3的堆栈是分为两个:主堆栈和进程堆栈,CONTROL[1]决定如何选择。
当 CONTROL[1]=0 时,只使用MSP,此时用户程序和异常handler共享同一个堆栈。这也是复位后的缺省使用方式。
当 CONTROL[1]=1 时,线程模式将不再使用MSP,而改用PSP(handler 模式永远使用MSP)。
在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下:
MRS R0, MSP ; 读取主堆栈指针到 R0 MSR MSP, R0 ; 写入 R0 的值到主堆栈中 MRS R0, PSP ; 读取进程堆栈指针到 R0 MSR PSP, R0 ; 写入 R0 的值到进程堆栈中
通过读取PSP的值,OS就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(使用STMDB和LDMIA的书写形式)。OS还可以修改PSP,用于实现多任务中的任务上下文切换。
6、异常和中断
Cortex‐M3 支持大量异常,包括 16‐4‐1=11 个系统异常,和最多 240 个外部中断——简称 IRQ。具体使用了这 240 个中断源中的多少个,则由芯片制造商决定。由外设产生的中断信号,除了 SysTick 的之外,全都连接到 NVIC 的中断输入信号线。典型情况下,处理器一般支持 16 到 32 个中断,当然也有在此之外的。
作为中断功能的强化,NVIC 还有一条 NMI 输入信号线。NMI 究竟被拿去做什么,还要视处理器的设计而定。在多数情况下,NMI 会被连接到一个看门狗定时器,有时也会是电压监视功能块,以便在电压掉至危险级别后警告处理器。NMI 可以在任何时间被激活,甚至是在处理器刚刚复位之后。异常描述如下表所示:
上表列出了 Cortex‐M3 可以支持的所有异常。有一定数量的系统异常是用于 fault 处理,它们可以由多种错误条件引发。NVIC 还提供了一些 fault 状态寄存器,以便于 fault 服务例程找出导致异常的具体原因。
6.1、 向量表
当一个发生的异常被 CM3 内核接受,对应的异常 handler 就会执行。为了决定 handler 的入口地址,CM3 使用了“向量表查表机制”。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该异常 handler 的入口地址。向量表的存储位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 处必须包含一张向量表,用于初始时的异常分配。向量表结构如下图所示:
举个例子,如果发生了异常 11(SVC),则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。0 号异常的功能则是个另类,它并不是什么入口地址,而是给出了复位后 MSP 的初值。
7、复位操作
CM3在上电是首先进入复位状态,在离开复位状态后,CM3 做的第一件事就是读取下列两个 32 位整数的值(如上图MDK反汇编之后向量表的值):
从地址 0x0000,0000 处取出 MSP 的初始值。
从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量,LSB 必须是 1。然后从这个值所对应的地址处取指。
如上图是CM3退出复位后的过程,取出0地址的数据给到MSP初始化堆栈,同时将0x4的地址给到PC,而0x4的地方就是复位函数的入口地址,熟悉启动文件的同学就知道,CM3是从复位函数进入最后调到C环境的main函数的。
请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。在 CM3中,0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令。
关于STM32的启动知识的可以参考下面的链接。
STM32启动文件介绍