接着上篇文章:【ARMv8M Cortex-M33 系列 7.2 – HardFault 问题定位 1】,后面定位到是在cortex-m33/context_gcc.S
执行完 BX Lr
之后就发生了 HardFault,通过JLink 发现 LR
的值为0xfffffffd
所以又继续调查了EXC_RETURN
的具体含义。
pendsv_exit:
/* restore interrupt */
MSR PRIMASK, r2
ORR lr, lr, #0x04
BX lr
在 ARM Cortex-M33(以及其他Cortex-M系列)处理器中,异常返回值(EXC_RETURN
)用于指示当处理器完成异常处理(例如中断或系统调用)后应如何返回到异常发生前的状态。这个值被自动加载到LR
(链接寄存器)中,在异常返回时由处理器使用。
当发生异常时,处理器会自动将当前的程序状态保存到堆栈中,包括程序计数器(PC)的值和其他重要寄存器,并将特定的值加载到LR
。这个LR
中的值称为EXC_RETURN
,它决定了异常返回时处理器的行为,如使用哪个堆栈指针(MSP或PSP),以及是否返回到Thread或Handler模式。
EXC_RETURN 的值如下:
0xFFFFFFF1
:返回到Handler模式,使用MSP(主堆栈指针)作为堆栈指针。0xFFFFFFF9
:返回到Thread模式,使用MSP作为堆栈指针。0xFFFFFFFD
:返回到Thread模式,使用PSP(进程堆栈指针)作为堆栈指针。0xFFFFFFBC
的设置表示返回时,处理器将:
此外,当在硬件浮点单元(FPU)支持的设备上编译代码时,还会有以下EXC_RETURN值:
0xFFFFFFE1
:返回到Handler模式,使用MSP作为堆栈指针,并且需要恢复浮点状态。0xFFFFFFE9
:返回到Thread模式,使用MSP作为堆栈指针,并且需要恢复浮点状态。0xFFFFFFED
:返回到Thread模式,使用PSP作为堆栈指针,并且需要恢复浮点状态。在异常处理完毕后,处理器将执行一个异常返回操作,这个操作会根据LR
中的EXC_RETURN
值来从堆栈恢复之前保存的状态,并将控制权交回到异常发生前的代码。这个恢复过程包括将之前保存在堆栈上的PC值重新加载到PC寄存器,这样程序就会从中断前被中断的点继续执行。
在实践中,异常处理函数通常不需要显式地处理这些细节,因为处理器硬件和操作系统的异常管理机制会自动处理这些过程。当开发裸机应用或自己的操作系统时,理解EXC_RETURN
和异常返回机制将非常重要。在使用操作系统(如FreeRTOS或其他RTOS)时,上下文切换和异常返回通常由操作系统管理。
在异常处理函数中,你可以检查LR
的值来确定异常发生时的上下文。例如,在异常处理函数中,你可以使用类似于以下的代码:
uint32_t lr_value;
asm("MOV %0, lr" : "=r" (lr_value));
// lr_value 现在包含了 EXC_RETURN 值
或者,如果你需要在你的异常处理函数中手动操作LR
以改变返回行为,你也可以编写相应的汇编代码,但通常这样的操作是不必要的,而且需要对ARM架构有深入的理解。
由于上面问题涉及到了入栈出栈的问题,所以又温习了一遍栈的内容:
在 ARM Cortex-M33 微控制器上,从异常(如中断)返回时,处理器会从堆栈中出栈(pop)寄存器的值来恢复到异常发生之前的状态。这一过程发生在异常处理完成后,当执行异常返回序列时。异常返回过程是自动的,由处理器硬件管理。
当异常发生时,处理器会将当前的上下文(某些寄存器的值)压入当前使用的堆栈中(MSP 或 PSP)。在从异常返回时,处理器将这些值出栈到寄存器中。这些寄存器包括程序计数器(PC)、链接寄存器(LR)、程序状态寄存器(xPSR)以及可能的一些通用寄存器。
异常返回时的出栈顺序与入栈顺序相反。入栈顺序通常如下:
因此,异常返回时寄存器的出栈顺序是:
在异常返回序列结束时,处理器从堆栈恢复了这些寄存器的值,并跳转到返回地址(之前的PC)继续执行程序。
值得注意的是,这里所说的 LR 寄存器在异常发生时会被自动设置为一个特殊的值(EXC_RETURN),该值包含了关于返回时所需使用的堆栈指针(MSP 或 PSP)以及是否使用了浮点寄存器等信息。异常返回时,处理器会检查 LR 中的 EXC_RETURN 值,以决定如何恢复上下文并从哪个堆栈指针出栈。
此外,浮点寄存器(如果有)的保存和恢复,取决于浮点单元(FPU)的使用以及处理器的配置。如果在异常发生时使用了 FPU,则会额外保存和恢复 S16-S31 寄存器。如果 FPU 未启用或未使用,则不会保存和恢复这些浮点寄存器。
在 ARM Cortex-M33 微控制器(以及所有 ARM Cortex-M 系列处理器)中,栈是向下增长的。这意味着,当数据被压入栈(push)时,栈指针(SP)会递减;相应地,当数据从栈中弹出(pop)时,栈指针会递增。
这个行为符合大多数现代处理器的常见约定,即栈空间的起始地址通常较高,随着数据的增加,栈指针向着较低的内存地址方向移动。这种设计可以有效利用内存空间,因为栈的最大尺寸通常是不确定的,而向下增长可以确保栈不会与静态分配或动态分配的内存空间发生冲突。
在thread.c
中可以看到对栈的具体使用选择:
static rt_err_t _rt_thread_init(struct rt_thread *thread,
const char *name,
void (*entry)(void *parameter),
void *parameter,
void *stack_start,
rt_uint32_t stack_size,
rt_uint8_t priority,
rt_uint32_t tick)
{
/* init thread list */
rt_list_init(&(thread->tlist));
thread->entry = (void *)entry;
thread->parameter = parameter;
/* stack init */
thread->stack_addr = stack_start;
thread->stack_size = stack_size;
/* init thread stack */
rt_memset(thread->stack_addr, '#', thread->stack_size);
#ifdef ARCH_CPU_STACK_GROWS_UPWARD
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(void *)((char *)thread->stack_addr),
(void *)_rt_thread_exit);
#else
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
(void *)_rt_thread_exit);
所以宏ARCH_CPU_STACK_GROWS_UPWARD
处的内容不会编译进去,使用的是#else
分支。