目录
2.2、PRIMASK, FAULTMASK 和 BASEPRO
1、内核寄存器
1.1、简介
每个 MCU 开发工程师一定都了解寄存器这个东西,以 STM32 为例,其拥有非常多的外设模块,如串口、SPI、IIC 等等,如果要使用这些外设,使其按照我们的要求工作,就需要配置这些外设的寄存器,往这些寄存器中写入对应的配置数据,从而使其工作在我们所需要的模式中。
上述寄存器都是工程师日常编程会操作的寄存器,可以说和工程师的关系非常紧密。但有这么一组寄存器,与大多数工程师的关系就很疏远,甚至一些初学者完全不知道。这,就是内核寄存器组。
在大多数比较基础的日常编程中你完全无需关注内核寄存器,但实际上你写的每一行代码,最终都是去操作内核寄存器,编译器会负责完成这中间的转换。并且,如果你要进行一些高级编程,如操作系统的编写,或者做一些非常底层的优化,你就不得不去人为操作这些寄存器。
STM32F103 属于 Cortex-M3 内核,其内核寄存器组中拥有 16 个寄存器,其中 13 个为 32 位通用寄存器,另外 3 个则有特殊用途:
1.2、R0 ~ R12
寄存器 R0 ~ R12 为通用寄存器,其中前 8 个 (R0 ~ R7)也被称为低寄存器。由于指令中的可用空间有限,许多 16 位的指令只能访问低寄存器。
高寄存器 R8 ~ R12 则可以用于 32 位指令和几个 16 位指令,如 MOV。
R0 ~ R12 的初始值是未定义的。
1.3、R13 栈指针 SP
R13 为栈指针,可以通过 PUSH 和 POP 指令实现栈的访问。实际在物理上存在两个栈指针:主栈指针 MSP(Main Stack Pointer)和进程栈指针 PSP(Process Stack Pointer)。
主栈指针(MSP)为默认的栈指针,在复位后或处理器处于特权模式(如进中断)时会被使用。通常用于芯片初始化代码和中断服务函数中。
进程栈指针(PSP)只能用于线程模式。在使用支持任务或线程切换的实时操作系统(RTOS)中,PSP 用于管理用户级任务的堆栈。这允许操作系统进行有效而稳定的任务调度,因为每个任务可以有自己的堆栈,在切换任务时只需要切换 PSP 的值即可。
大多数情况下,如果你是裸机编程,即没有用到嵌入式操作系统,那么 PSP 也没有必要使用。许多简单的应用可以完全依赖于 MSP,一般在操作系统中才会使用到 PSP,因其各个任务(或线程)的栈是要求相互独立的。
PSP 的初始值未定义,而 MSP 的初始值则会在复位流程中从存储器的第一个字中取出。
1.4、R14 链接寄存器 LR
R14 也被称作链接寄存器 LR,用于函数或子程序调用时返回地址的保存。在函数或子程序被调用时,返回地址(即调用指令后面那条指令的地址)被保存到 LR 寄存器中。这样,在函数或子程序结束时,处理器可以通过 LR 寄存器中的值返回到正确的地址继续执行。
当执行了函数或子程序的调用后,LR 的数值会自动更新。若该函数或子程序内还要调用其他函数或子程序(如 A 调用 B,B 中还调用了 C),则此时需要将 LR 的数值保存在栈中,否则,一旦执行了 C 调用,LR 就会更新为 B 函数中调用 C 这条函数的下一条指令的地址,原本 A 调 B 的下一条指令地址就会被覆盖丢失,从而无法再恢复到 A 函数中继续运行。
在异常处理期间,LR 也会被自动更新为特殊的 EXC_RETURN(异常返回)数值,之后该数值会在异常处理结束时触发异常返回,从而实现从异常处理返回到正确的用户程序中 。
需要注意的是,尽管 Cortex-M 处理器中的返回地址总是偶数(由于指令会对齐到半字地址上,因此,最低位即第 0 位为 0),LR 的第 0 位是可读可写的,有些跳转/调用操作需要将 LR(或正在使用的任何寄存器)的第 0 位置 1 来表示当前处理器处于 Thumb 状态。
1.5、R15 程序计数器 PC
R15 为程序计数器 PC,可读可写。读操作返回当前指令地址加 4(这是由于流水线特性及同 ARM7TDMI 处理器兼容的需要),而对 PC 的写操作(例如使用数据传输/处理指令)则会引起程序的跳转(不会更新 LR 寄存器)。
由于指令必须要对齐到半字或字地址,PC 的最低位(LSB)为 0.不过,在使用一些跳转或读存储器指令更新 PC 时,需要将新 PC 值的 LSB 置 1 来表示 Thumb 状态,否则就会由于试图使用不支持的 ARM 指令(如 ARM7TDMI 中的 32 位 ARM 指令)而触发错误异常。对于高级编程语言(包括 C 和 C++),编译器会自动将跳转目标的 LSB 置位。
多数情况下,跳转和调用由专门的指令实现,利用数据处理指令更新 PC 的情况较为少见。不过,在访问位于程序存储器的字符数据时,PC的数值非常有用,因此,会经常发现存储器的读操作将 PC 作为基地址寄存器,而地址偏移则由指令中的立即数生成。
2、特殊寄存器
实际上除了内核寄存器组外,CM3 处理器中还存在多个特殊寄存器,它们分别为程序状态寄存器,中断/异常屏蔽寄存器和控制寄存器。
需要注意的是,特殊寄存器未经过存储器映射,即没有对应的存储器地址,也只能使用专门的 MSR 和 MRS 等特殊寄存器访问指令来进行访问:
MRS <reg> <special_reg> ;将特殊寄存器读入寄存器
MSR <special_reg> <reg> ;写入特殊寄存器
CMSIS-Core 也提供了几个用于访问特殊寄存器的 C 函数,其本质也是以上两个指令的封装。如在 gcc 环境下(cmsis_gcc.h),对 CONTROL 寄存器的操作如下:
__STATIC_FORCEINLINE uint32_t __get_CONTROL(void)
{
uint32_t result;
__ASM volatile ("MRS %0, control" : "=r" (result) );
return(result);
}
__STATIC_FORCEINLINE void __set_CONTROL(uint32_t control)
{
__ASM volatile ("MSR control, %0" : : "r" (control) : "memory");
}
2.1、程序状态寄存器(PSRs 或 xPSR)
程序状态寄存器包含以下三个状态寄存器:
应用 PSR(APSR)
执行 PSR(EPSR)
中断 PSR(IPSR)
通过上面提到的 MRS 和 MSR 指令,这三个 PSRs 可以单独访问:
MRS r0, APSR ;将应用状态读入 R0
MRS r0, IPSR ;将中断/异常状态读入 R0
MSR APSR, R0 ;写应用状态
也可以组合访问(两个组合或三个组合都可以)。当使用三合一方式访问时,应使用 “xPSR” 或 “PSR” 这两个名字。
MRS r0, PSR ;读组合程序状态字
MSR PSR, r0 ;写组合程序状态字
需要注意的是,软件代码无法直接使用 MRS (读出为 0)或 MSR 直接访问 EPSR。同时 IPSR 为只读,可以从组合 PSR 中读出。
这三个寄存器的位域结构如下:
组合形式:
其中每个位域字段的含义如下:
N:负标志。
Z:零标志。
C:进位(或非借位)标志。
V:溢出标志。
Q:饱和标志(ARMv6-M 中不存在)。
ICI/IT:中断继续指令状态位(ICI),用于条件执行的 IF-THEN 指令状态位(ARMv6-M 中不存在)。
T:Thumb 状态,总是 1,尝试清除此位会引起错误异常。
Exception Number:表示处理器正在处理的异常对应的编号。
2.2、PRIMASK, FAULTMASK 和 BASEPRO
PRIMASK、FAULTMASK 和 BASEMASK 寄存器都用于异常或中断的屏蔽,每个异常(包括中断)都有一个优先等级,数值越小优先级越高,反之数值越大优先级越低。以上三个特殊寄存器可以基于优先级屏蔽异常,只有在特权访问等级才能对它们进行操作(非特权状态下的写操作会被忽略,而读取则会返回 0)。它们的默认值都为 0,即不屏蔽任何异常或中断。这些寄存器的编程模型如下图所示:
PRIMASK 寄存器是位宽为 1 的中断屏蔽寄存器。在置位时,它会阻止不可屏蔽中断(NMI)和 HardFault 异常之外的所有异常(包括中断)。实际上,它的原理是将当前异常优先级提升为 0,这也是可编程异常/中断的最高优先级。PRIMASK 最常见的用途是在一些时间要求很严格的进程中禁止所有中断,在该进程完成后,需要将 PRIMASK 清除以重新使能中断。
FAULTMASK 和 PRIMASK 非常相似,不过它还能屏蔽 HardFault 异常,它实际上是将异常优先级提升到了 -1。错误处理代码可以使用 FAULTMASK 以免在错误处理期间再次触发其他错误(只有几种)。例如, FAULTMASK 可用于旁路 MPU 或屏蔽总线错误(这些都是可配置的),这样,错误处理代码执行修复措施也就更容易了。与 PRIMASK 不同, FAULTMASK 在异常返回时会被自动清除。
BASEPRI 会根据优先级屏蔽异常或中断。BASEPRI 的宽度取决于设计中实际实现的优先级数量,这通常是由微控制器供应商决定的。大多数 Cortex-M3 或 Cortex-M4 微控制器都有 8 个或 16 个可编程的异常优先级,此时 BASEPRI 的宽度就相应地为 3 位或者 4 位。BASEPRI 为 0 时不会起作用,当被设置为非 0 数值时,他就会屏蔽具有相同或更低优先级的异常(包括中断),而更高优先级的则仍然会被处理器接受。
CMSIS-Core 提供了多个 C 函数用于访问 PRIMASK、FAULTMASK、及 BASEPRI 寄存器。(这些寄存器只能在特权等级下访问)
x = __get_BASEPRI(); // 读 BASEPRI 寄存器
x = __get_PRIMARK(); // 读 PRIMASK 寄存器
x = __get_FAULTMASK(); // 读 FAULTMASK 寄存器
__set_BASEPRI(x); // 设置 BASEPRI
__set_PRIMASK(x); // 设置 PRIMASK
__set_FAULTMASK(x); // 设置 FAULTMASK
__disable_irq(); // 设置 PRIMASK, 禁用 IRQ
__enable_irq(); // 清除 PRIMASK, 使能 IRQ
同时也可以使用汇编代码访问这些寄存器:
MRS r0, BASEPRI ; 将 BASEPRI 寄存器的值读入 R0
MRS r0, PRIMASK ; 将 PRIMASK 寄存器的值读入 R0
MRS r0, FAULTMASK ; 将 FAULTMASK 寄存器的值读入 R0
MSR BASEPRI, r0 ; 将 R0 寄存器的值写入 BASEPRI
MSR PRIMASK, r0 ; 将 R0 寄存器的值写入 PRIMASK
MSR FAULTMASK, r0 ; 将 R0 寄存器的值写入 FAULTMASK
此外,利用修改处理器状态(CPS)指令,可以非常方便地设置或清除 PRIMASK 和 FAULTMASK 的值:
CPSIE i ; 使能 interrupt (清除 PRIMASK)
CPSID i ; 禁用 interrupt (设置 PRIMASK)
CPSIE f ; 使能 interrupt (清除 FAULTMASK)
CPSID f ; 禁用 interrupt (设置 FAULTMASK)
2.3、CONTROL 寄存器
CONTROL 寄存器中包含了如下两个主要信息:
栈指针的选择(主栈指针 MSP 和 进程栈指针 PSP)。
线程模式的访问等级(特权级和非特权级)。
其编程模型如下:
具体的位域描述为:
复位后,CONTROL 寄存器默认为 0,这意味着处理器此时处于线程模式,具有特权访问权限并且使用主栈指针。通过写 CONTROL 寄存器,特权线程模式的程序可以切换栈指针的选择或进入非特权访问等级,如下图所示:
不过,nPRIV(CONTROL[0])置位后,运行在线程模式的程序就不能访问 CONTROL 寄存器了。
运行在非特权等级的程序一般情况下无法再切换回特权访问等级,这样就提供了一个基本安全的模型。例如,嵌入式系统中可能会具有运行在非特权等级且不受信任的应用,这些应用的访问权限就需要受到限制,以免不可靠的程序引起系统的崩溃。
若需要在线程模式切换回特权访问等级,则需要借助于异常机制。在异常处理期间,处理程序可以清除 nPRIV 位。在返回到线程模式后,处理器就会进入特权访问等级。
若使用嵌入式 OS,每次上下文切换时都可以重新编程 CONTROL 寄存器,以满足应用间不同特权访问等级的需要。
nPRIV 和 SPSEL 的设置有 4 种组合方式,其中 3 种在实际应用中较为常见:
对于未使用嵌入式 OS 的多数简单应用,无须修改 CONTROL 寄存器的数值,整个应用可以运行在特权访问等级并且只使用 MSP:
要利用 C 语言访问 CONTROL 寄存器,可以使用符合 CMSIS 的设备驱动库提供的以下函数:
x = __get_CONTROL(); // 读取当前 CONTROL 寄存器的值
__set_CONTROL(x); // 设置 CONTROL 寄存器的值为 x
如果使用汇编,则可以借助于 MRS 和 MSR 指令:
MRS r0, CONTROL ;将 CONTROL 寄存器的值读到 r0
MSR CONTROL, r0 ;将 r0 中的值写到 CONTROL 寄存器
最后,你可以通过检查 CONTROL 和 IPSR 的数值来确定当前是否为特权等级:
int in_privileged(void) {
if (__get_IPSR() != 0)
return 1; // True
else if ((__get_CONTROL() & 0x1) == 0)
return 1; // True
else
return 0; // False
}
3、Cortex-M3的操作模式
Cortex-M3 有两种操作状态和两个操作模式,此外,处理器还支持特权访问等级和非特权访问等级。
特权访问等级能够访问处理器的所有资源,自然在非特权访问等级下,有些存储器区域是不能访问的,并且也无法执行某些指令。
3.1、操作状态
调试状态:当处理器被暂停后(如通过调试器或触发断点后),就会进入调试状态并停止指令的执行。
Thumb 状态:当处理器在执行程序代码(Thumb 指令),其就会处在 Thumb 状态。与 ARM7TDMI 等经典的 ARM 处理器不同,由于 Cortex-M 处理器不支持 ARM 指令集,所以不存在 ARM 状态。
3.2、操作模式
处理模式(Handle Mode):执行中断服务程序(ISR)等异常处理。在处理模式下,处理器总是拥有特权访问等级。
线程模式(Thread Mode):在执行普通应用程序代码时,处理器可以处于特权访问等级,也可以处于非特权访问等级。实际的访问等级会由特殊寄存器 CONTROL 控制。
关于操作模式和特权等级的关系可以总结为下图:
需要注意的是,软件可以将处理器从特权线程模式切换到非特权线程模式,但无法将自身从非特权模式切换到特权模式。如果已处在非特权模式,并且想切换到特权模式,此时只能借助异常机制。
由于区分了特权和非特权等级,嵌入式系统设计人员可以提供对关键区域访问的保护机制及基本的安全模型。从而有助于开发出健壮的嵌入式系统。
例如,系统中可能会包含运行在特权访问等级的嵌入式 OS 内核,以及运行在非特权访问等级的应用任务。按照这种方式,可以通过存储器保护单元(MPU)设置存储器访问权限,避免应用任务破坏 OS 内核以及其他任务使用的存储器和外设。在这种情况下若其中一个任务崩溃,其余的任务和 OS 内核仍然可以继续运行。
除了存储器访问权限和几个指令访问的差异,特权访问等级和非特权访问等级的编程模型基本是一致的。此处注意几乎所有的 NVIC 寄存器都只能在特权模式下进行访问。
同样,线程模式和处理模式的编程模型也非常类似。但在线程模式下可以使用独立的栈指针(PSP),这使得应用任务的栈空间和 OS 内核的栈空间相互独立,从而提高了系统的可靠性。
Cortex-M 处理器启动后默认处于特权模式以及 Thumb 状态。实际上对于大部分简单应用,非特权线程模式和独立的栈指针根本就用不上。这种情况下的系统运行状态图如下:
调试状态仅用于调试操作,可以通过两种方式进入调试状态:
调试器发起暂停请求。
处理器中的调试模块产生调试事件。
在此状态下,调试器可以访问或修改处理器寄存器的数值。无论是在 Thumb 状态还是调试状态,调试器都可以访问处理器内外的外设等系统存储器。