目录
1.前言
2.源码分析
2.1 xPortPendSVHandler源码
2.2 pxPortInitialiseStack源码
3. 问题总结
1.前言
以Arm M7核为例,当CPU响应中断异常时,第一件事就是保存现场,进行压栈。如果当前使用的是任务堆栈,则压入PSP;如果使用的是系统主堆栈,则压入MSP。在压栈的过程中,xPSR,PC,LR,R12以及R3~R0是由硬件自动完成压栈的,具体的入栈情况如图1所示:
图1 入栈顺序及入栈后堆栈中的内容(参考M3权威指南)
其中,N为入栈开始时SP的值,在入栈后,新栈顶为N-32,这些硬件自动入栈的寄存器是编译器优先使用来保存中间计算结果的。此外,途中的被保存顺序虽然不是严格按照堆栈操作的顺序来执行,但机器会保证最后入栈的结果是正确的,这么做的主要目的是为了优化指令流的执行。比如,先把PC与xPSR保存,就可以更早的启动服务例程指令的预取(PC入栈后可以指向新的指令),且可以在早期更新xPSR中IPSR位段的值。
此外,如果程序用到了R4~R11,此时会由软件来负责对这些寄存器进行压栈。
本文主要以freertos的xPortPendSVHandler和pxPortInitialiseStack为切入点,简单分析下M7在异常前后和堆栈相关的具体行为。
2.源码分析
2.1 xPortPendSVHandler源码
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
" mrs r0, psp \n"
" isb \n"
" \n"
" ldr r3, pxCurrentTCBConst \n"/* Get the location of the current TCB. */
" ldr r2, [r3] \n"
" \n"
" tst r14, #0x10 \n"/* Is the task using the FPU context? If so, push high vfp registers. */
" it eq \n"
" vstmdbeq r0!, {s16-s31} \n"
" \n"
" stmdb r0!, {r4-r11, r14} \n"/* Save the core registers. */
" str r0, [r2] \n"/* Save the new top of stack into the first member of the TCB. */
" \n"
" stmdb sp!, {r0, r3} \n"
" mov r0, %0 \n"
" cpsid i \n"/* Errata workaround. */
" msr basepri, r0 \n"
" dsb \n"
" isb \n"
" cpsie i \n"/* Errata workaround. */
" bl vTaskSwitchContext \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" ldmia sp!, {r0, r3} \n"
" \n"
" ldr r1, [r3] \n"/* The first item in pxCurrentTCB is the task top of stack. */
" ldr r0, [r1] \n"
" \n"
" ldmia r0!, {r4-r11, r14} \n"/* Pop the core registers. */
" \n"
" tst r14, #0x10 \n"/* Is the task using the FPU context? If so, pop the high vfp registers too. */
" it eq \n"
" vldmiaeq r0!, {s16-s31} \n"
" \n"
" msr psp, r0 \n"
" isb \n"
" \n"
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */
#if WORKAROUND_PMU_CM001 == 1
" push { r14 } \n"
" pop { pc } \n"
#endif
#endif
" \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB \n"
::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
);
}
网上已经有很多博文对xPortPendSVHandler进行了详细的解读,主要过程主要包括包括以下几个步骤:
①通过pxCurrentTCBConst定位到当前TCB,并从其首地址获取对应的任务栈顶;
②对FPU(浮点处理单元)是否使用的相关检测和对应处理器的压栈处理;
③对R4~R11,R14寄存器进行压栈处理(压入任务堆栈);
④将PSP和pxCurrentTCBConst压入系统堆栈,关中断,指令隔离;
⑤调用vTaskSwitchContext切换TCB至当前最高优先级就的就绪任务;
⑥从系统栈弹出PSP和pxCurrentTCBConst,并将PSP指向当前TCB获取新任务堆栈的栈顶;
⑦恢复任务被阻塞前的寄存器内容,即从PSP弹出R4~R11,R14;
⑧跳转到R14,异常返回。
我当时看到这段代码,始终有一个疑惑,既然LR会在响应异常时由硬件自动压栈,那为何会在Save the core registers过程中又对R14进行压栈呢?这个问题后边再分析。
2.2 pxPortInitialiseStack源码
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
* interrupt. */
/* Offset added to account for the way the MCU uses the stack on entry/exit
* of interrupts, and to ensure alignment. */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */
/* Save code space by skipping register initialisation. */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
/* A save method is being used that requires each task to maintain its
* own exec return value. */
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXC_RETURN;
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
这段代码更简单,是在进行任务创建时,进行任务堆栈的初始化,从栈的起始地址按顺序依次在满减栈中对各寄存器的位置进行初始化。其中, PC指向当前任务的主函数,LR则被初始化为portTASK_RETURN_ADDRESS,这其实指向了prvTaskExitError(一个错误处理函数),该函数在任务主函数在错误return的时候,进行相应的异常处理和告警。那么问题来了,既然LR已经被初始化,为何将R0后的位置初始化成了portINITIAL_EXC_RETURN?这个位置和xPortPendSVHandler中最后跳转的R14位置相对应。
3. 问题总结
根据权威指南的说法,CPU在响应异常序列时,首先会进行压栈、取向量、更新寄存器(选择堆栈指针MSP/PSP,更新堆栈指针SP,LR及PC)这三部曲。取向量指的是指令总线从向量表中找到对应的异常向量,并在对应的服务程序的入口处进行取指,而更新寄存器部分则可以解释我的疑惑。入栈和取向量完成后,需要更新以下寄存器:
①SP: 在执行中断服务程序时,将使用系统堆栈,即MSP, 所以xPortPendSVHandler中的PSP指向的是任务栈顶,与当前TCB相对应,而使用的SP则指的是MSP了;
②PSR: IPSR位段会被更新为需要响应的异常号;
③PC: PC指向服务例程的入口地址;
④LR: LR在此时被赋予了新的含义,其值也被更新为EXC_RETURN,用来标识异常返回时要选择的堆栈指针和特权等级(可以参考control寄存器进行理解),更多含义见图2;
图2 EXC_RETURN位段含义
至此,将xPortPendSVHandler和pxPortInitialiseStack对照进行分析,可以发现:
①portINITIAL_EXC_RETURN本身就是初始化给异常返回使用的。如果该任务被异常打断,则CPU在进行异常响应时,首先会将LR进行压栈(栈中位置在PC之后,见图1),再将之更新为portINITIAL_EXC_RETURN(栈中位置在R0之后,值为portINITIAL_EXC_RETURN), 即该位置是专为异常返回使用的;
②xPortPendSVHandler中,在完成任务切换、并从新的PSP中弹出值到R14,实际上弹出的是为异常返回赋予新含义的EXC_RETURN;在freertos中,portINITIAL_EXC_RETURN等于0xfffffffd,标识着返回Thumb状态,异常返回后进行线程模式并使用PSP;文章来源:https://www.toymoban.com/news/detail-825714.html
至此,疑惑终于解开。文章来源地址https://www.toymoban.com/news/detail-825714.html
到了这里,关于FreeRtos(Arm M7)中断压栈分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!