每个 MCU 开发工程师一定都了解寄存器这个东西,以 STM32 为例,其拥有非常多的外设模块,如串口、SPI、IIC 等等,如果要使用这些外设,使其按照我们的要求工作,就需要配置这些外设的寄存器,往这些寄存器中写入对应的配置数据,从而使其工作在我们所需要的模式中。
上述寄存器都是工程师日常编程会操作的寄存器,可以说和工程师的关系非常紧密。但有这么一组寄存器,与大多数工程师的关系就很疏远,甚至一些初学者完全不知道。
这,就是内核寄存器组。
在大多数比较基础的日常编程中你完全无需关注内核寄存器,但实际上你写的每一行代码,最终都是去操作内核寄存器,编译器会负责完成这中间的转换。并且,如果你要进行一些高级编程,如操作系统的编写,或者做一些非常底层的优化,你就不得不去人为操作这些寄存器。
下面我们就基于 STM32F103 这款芯片来探一探其内核寄存器的究竟。
STM32F103 属于 Cortex-M3 内核,其内核寄存器组中拥有 16 个寄存器,其中 13 个为 32 位通用寄存器,另外 3 个则有特殊用途:
R0 ~ R12
寄存器 R0 ~ R12 为通用寄存器,其中前 8 个 (R0 ~ R7)也被称为低寄存器。由于指令中的可用空间有限,许多 16 位的指令只能访问低寄存器。
高寄存器 R8 ~ R12 则可以用于 32 位指令和几个 16 位指令,如 MOV。
R0 ~ R12 的初始值是未定义的。
R13 栈指针 SP
R13 为栈指针,可以通过 PUSH 和 POP 指令实现栈的访问。实际在物理上存在两个栈指针:主栈指针 MSP(Main Stack Pointer)和进程栈指针 PSP(Process Stack Pointer)。
主栈指针(MSP)为默认的栈指针,在复位后或处理器处于特权模式(如进中断)时会被使用。通常用于芯片初始化代码和中断服务函数中。
进程栈指针(PSP)只能用于线程模式。在使用支持任务或线程切换的实时操作系统(RTOS)中,PSP 用于管理用户级任务的堆栈。这允许操作系统进行有效而稳定的任务调度,因为每个任务可以有自己的堆栈,在切换任务时只需要切换 PSP 的值即可。
大多数情况下,如果你是裸机编程,即没有用到嵌入式操作系统,那么 PSP 也没有必要使用。许多简单的应用可以完全依赖于 MSP,一般在操作系统中才会使用到 PSP,因其各个任务(或线程)的栈是要求相互独立的。
PSP 的初始值未定义,而 MSP 的初始值则会在复位流程中从存储器的第一个字中取出。
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 状态。
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 作为基地址寄存器,而地址偏移则由指令中的立即数生成。
总结
以上就是 Cortex-M3 内核寄存器组中所有的寄存器。虽然在日常编程过程中我们很少或者可以说几乎不会直接接触并操作这些寄存器,一旦程序出现问题崩溃,或者运行过程中出现异常输出或逻辑,此时这些内核寄存器就能够为我们提供非常重要且直观的调试分析依据,以此快速定位问题并解决。如果你不理解这些内核寄存器组,那你的问题分析过程将会变得异常艰辛,甚至最终从调试到放弃。