1、任务上下文
μCOS-II V2.83的任务上下文保存在任务的栈里面,任务创建时,将任务的参数、运行地址(任务入口)、任务栈等信息保存在任务栈里面,OS_TCB->OSTCBStkPtr指向任务栈顶(任务创建及挂起时,OS_TCB->OSTCBStkPtr即指向任务上下文)。
ARM栈是递减栈(栈底地址高、栈顶地址低,地址由低到高为:栈顶->栈数据->栈底)!
1.1、 任务创建时任务栈设置
以下为os_cpu_c.c任务创建时任务栈初始化函数。
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
opt = opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
*(stk) = (OS_STK)task; /* Entry Point */
*(--stk) = (INT32U)0x14141414L; /* R14 (LR) (init value will cause fault if ever used) */
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
*(--stk) = (INT32U)0x00000013L; /* CPSR (Enable both IRQ and FIQ interrupts) */
return (stk);
}
从第一个*(--stk)看,是先将栈地址减一在存储数据,因此该栈是个满栈(即栈指针指向的地址有数据,前置递减可以看出栈是个递减栈,栈的增长是从高地址往低地址增长);入栈顺序依次是{task, r14-r0, cpsr},task在内存高地址,cpsr在内存低地址。
栈中几个关键的寄存器:r14(lr),这里设置的是一个无意义的值,因此这种情况下程序结束时返回地址会异常(其他嵌入式实时系统会设置返回地址指向一个任务退出函数);r0任务运行参数指针;cpsr任务运行时的cpsr,这里设置任务运行在SVC模式,另外任务运行时默认必须允许中断,否则中断处理器没法处理任务调度定时器中断信号。
寄存器在栈中的顺序主要与上下文保存及恢复有关。
1.2、 第一个任务的运行
第一个任务是由main函数执行的,main函数没有运行在任务上下文,操作系统运行之后也不会再返回到被中断的main函数继续执行,因此,第一个任务执行时没有保存main函数上下文的必要。
.text
OSStartHighRdy:
#if (OS_TASK_SW_HOOK >= 1)
BL OS_TaskSwHook // OSTaskSwHook();
#endif
MSR CPSR_c, #(I_BIT | F_BIT | ARM_MODE_SVC) // Switch to SVC mode with IRQ and FIQ disabled
LDR R4, =OSRunning // OSRunning = TRUE;
MOV R5, #1
STRB R5, [R4]
LDR R0, =OSTCBCur // SWITCH TO HIGHEST PRIORITY TASK:
LDR R0, [R0] // Get stack pointer,
LDR SP, [R0] // Switch to the new stack,
LDMFD SP!, {R0} // Pop new task's CPSR,
MSR SPSR_cxsf, R0
LDMFD SP!, {R0-R12, LR, PC}^ // Pop new task's context.
OSStartHighRdy函数主要步骤是:
1、禁止中断;
2、OSRunning = TRUE设置操作系统运行状态(只有操作系统运行时,调度定时器才有必要调用);
3、恢复任务栈;
获取任务栈顶
LDR R0, =OSTCBCur // 获取任务控制块地址(汇编语言 "=变量名称"用于获取该变量的地址,等价于c语言的 r0 = &OSTCBCur)
LDR R0, [R0] // 获取OSTCBStkPtr的地址(r0 = OSTCBCur)
LDR SP, [R0] // 获取OSTCBStkPtr的值(即任务栈顶)(sp = OSTCBStkPtr->OSTCBStkPtr
任务栈恢复
LDMFD SP!, {R0} // 出栈(栈顶元素pop到r0,栈顶此时是任务创建时的cpsr)
MSR SPSR_cxsf, R0 // 任务的cpsr保存到spsr
LDMFD SP!, {R0-R12, LR, PC}^ // 寄存器出栈(1、ldmfd带有pc及^标志,指令会将spsr备份程序状态寄存器的值恢复到cpsr程序状态寄存器;2、r0-r12, lr, pc恢复,此次主要时将任务入口的参数恢复并将程序寄存器pc设置为task入口函数指针;3、ldmfd之前的cpu已经切换到SVC模式,任务也是运行在SVC模式,因此sp寄存器并没有在ldmfd恢复寄存器列表里面,ldmfd sp!, ... 本身就是在操作任务的栈,任务切换前后的sp是同一个sp)
2、中断处理
中断处理主要保存任务/中断上下文,恢复任务/中断上下文。中断处理汇编代码如下:
/*
* IRQ interrupt
*/
.align 5
irq:
stmfd sp!, {r1-r3} // We will use r1-r3 as temporary registers
mov r1, sp // sp -> r1
add sp, sp, #12 // Adjust IRQ stack pointer
sub r2, lr, #4 // Adjust pc for return address to task
mrs r3, spsr // Copy spsr (task's cpsr)
/*
* save task's context onto old task's stack
*/
msr cpsr_cxsf, #(I_BIT | F_BIT | ARM_MODE_SVC) // Change to SVC mode
stmfd sp!, {r2} // Push task's pc
stmfd sp!, {r4-r12, lr} // Push task's lr, r12-r4
ldmfd r1!, {r4-r6} // Load task's r1-r3 from IRQ stack
stmfd sp!, {r4-r6} // Push task's r1-r3 to SVC stack
stmfd sp!, {r0} // Push task's r0 to SVC stack
stmfd sp!, {r3} // Push task's cpsr
ldr r0, =OSIntNesting // OSIntNesting++
ldrb r1, [r0]
add r1, r1, #1
strb r1, [r0]
cmp r1, #1 // if (OSIntNesting == 1) {
bne 1f
ldr r4, =OSTCBCur // OSTCBCur->OSTCBStkPtr = sp;
ldr r5, [r4]
str sp, [r5] // }
1:
msr cpsr_c, #(I_BIT | F_BIT | ARM_MODE_IRQ) // Change to IRQ mode to use IRQ stack to handle interrupt
bl do_IRQ
msr cpsr_c, #(I_BIT | F_BIT | ARM_MODE_SVC) // Change to SVC mode
bl OSIntExit // Call OSIntExit
ldmfd sp!, {r4} // Pop the task's cpsr
msr spsr_cxsf, r4
ldmfd sp!, {r0-r12, lr, pc}^ // Pop new task's context
2.1、中断上下文保存
stmfd sp!, {r1-r3} // 通用寄存器r1-r3保存到IRQ栈里面(r1-r3将作为临时寄存器使用,即寄存器将被改变,sp = sp - 4 * 3)
mov r1, sp // r1保存IRQ模式的栈顶(中断前任务/中断处理程序r1-r3保存的地址)
add sp, sp, #12 // 恢复IRQ的栈指针(sp = sp + 4 * 3),保存的寄存器仍在内存中,只是移动了IRQ的栈指针而已,后续代码不会操作IRQ的sp值,此处如果不恢复的话将会造成IRQ模式栈内存的泄露!
sub r2, lr, #4 // 中断返回地址保存到r2中(中断时,硬件会将中断前的pc寄存器的值减去4然后保存到IRQ的lr寄存器里面,中断前的pc指向的是正在取指的指令地址,pc之前有正在指向的指令、正在译码的指令,pc - 8才是正在执行指令的地址,正在执行的指令被中断了,因此需要重新执行)
mrs r3, spsr // 中断前的cpsr寄存器的值保存在spsr里面,中断前的cpsr保存到r3
目前已经保存的寄存器有,r1指向的内存(r1-r3),IRQ寄存器r2(pc)、r3(cpsr)。
2.2、任务/中断处理上下文保存到任务栈
前一小结中的中断前的部分寄存器保存到了IRQ的栈里面,再中断重入的情况下,会造成保存的上下文被覆盖,因此IRQ栈里面只是临时保存了部分上下文数据而已,在再次允许中断前,需要将IRQ栈里面的数据保存到其他地方(任务的栈)。
msr cpsr_cxsf, #(I_BIT | F_BIT | ARM_MODE_SVC) // 切换到SVC模式并禁止中断
stmfd sp!, {r2} // r2(pc)入栈(如同任务创建时一样,栈底保存任务运行地址),此处已经是SVC模式了,因为任务也运行在SVC模式,因此,此处操作的是任务的栈! 栈的内容为: pc(低地址到高地址)
stmfd sp!, {r4-r12, lr} // {r4-r12, lr}入栈,栈的内容为:r4-r12,lr,pc(低地址到高地址)
ldmfd r1!, {r4-r6} // r1指向IRQ栈保存上下文的地址,IRQ保存的栈数据(r1-r3)恢复到寄存器r4-r6
stmfd sp!, {r4-r6} // r4-r6(r1-r3)入栈,栈的内容为:r1-r12,lr,pc(低地址到高地址)
stmfd sp!, {r0} // r0入栈(r0为通用寄存器,在这之前的中断处理一直没有改变r0的值),栈的内容为:r0-r12,lr,pc(低地址到高地址)
stmfd sp!, {r3} // cpsr入栈(r3为通用寄存器,此前已经保存了中断前的cpsr),栈的内容为:cpsr,r0-r12,lr,pc(低地址到高地址)
至此,中断前任务/中断处理函数的所有寄存器都保存到了任务栈里面了!
2.3、任务栈设置
中断可能导致任务被重新调度,因此需要将任务上下文的地址保存到任务控制块里面,任务重新执行的时候才可能找到上下文。
ldr r0, =OSIntNesting // 中断嵌套计数器OSIntNesting加1
ldrb r1, [r0]
add r1, r1, #1
strb r1, [r0]
cmp r1, #1 // 判断中断嵌套计数器OSIntNesting是否为1,如果为1表示当前是第一次进入中断,即中断前是在执行操作系统的任务,需要更新被中断任务的栈指针OSTCBStkPtr
bne 1f
ldr r4, =OSTCBCur // r4 = &OSTCBCur
ldr r5, [r4] // r5 = OSTCBCur
str sp, [r5] // OSTCBCur->OSTCBStkPtr = sp
2.4、中断退出
(中断处理的c函数略过,中断处理c函数不会切换上下文,执行完成后会回到汇编代码!)
bl OSIntExit // 调用中断退出函数(操作系统调度定时器更新可能会唤醒某些等待超时的任务,当前任务优先级可能不再是最高优先级了)
// OSIntExit没有执行任务切换(中断嵌套/当前任务优先级最高),此处代码将继续执行,后续代码仅恢复被中断的任务/中断处理函数(do_IRQ)
ldmfd sp!, {r4} // r4 = cpsr
msr spsr_cxsf, r4 // spsr = cpsr
ldmfd sp!, {r0-r12, lr, pc}^ // 从SVC栈恢复寄存器
3、任务抢占
中断处理函数除了调度定时器计数导致任务时间片用完、等到超时任务定时器超时外,其他硬件中断处理也可能使某个睡眠任务被唤醒,因此当前任务可能被高优先级任务抢占。OSIntExit在中断退出时进行抢占检查,判断是否有更高优先级任务。
3.1、OSIntExit中断退出
主要是对中断嵌套计数器OSIntNesting减1,如果是中断嵌套,则返回中断汇编代码继续处理,直到所有中断退出,如果中断都处理完成(OSIntNesting为0),则调用OS_SchedNew更新就绪任务的最高优先级OSPrioHighRdy。
如果被中断的任务的优先级最高,则返回中断汇编代码,中断汇编代码负责恢复中断上下文;
如果有更高优先级的任务就绪,则更新OSTCBHighRdy、OSTCBHighRdy->OSTCBCtxSwCtr、OSCtxSwCtr,并调用OSIntCtxSw执行任务切换,OSIntCtxSw不同于任务主动睡眠的切换,OSIntCtxSw函数调用前已经保存了被切换出去的任务的上下文,因此OSIntCtxSw仅恢复下一任务的上下文即可。
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
if (OSRunning == OS_TRUE) {
OS_ENTER_CRITICAL();
if (OSIntNesting > 0) { /* Prevent OSIntNesting from wrapping */
OSIntNesting--;
}
if (OSIntNesting == 0) { /* Reschedule only if all ISRs complete ... */
if (OSLockNesting == 0) { /* ... and not locked. */
OS_SchedNew();
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
#if OS_TASK_PROFILE_EN > 0
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Keep track of the number of ctx switches */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
}
OS_EXIT_CRITICAL();
}
}
3.2、OSIntCtxSw任务上下文恢复
前面已经讲了,中断上下文已经保存,OSIntCtxSw仅恢复下一任务上下文。
OSIntCtxSw主要执行以下任务:
1、设置OSPrioCur、OSTCBCur,这两个值用在不同的地方,OSPrioCur主要用于与最高就绪任务优先级比较,OSTCBCur主要用于任务上下文的切换及任务统计;
2、获取下一任务的sp(栈顶保存了任务的上下文),恢复任务上下文。
OSIntCtxSw:
LDR R0, =OSPrioHighRdy // OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioCur
LDRB R0, [R0]
STRB R0, [R1]
LDR R0, =OSTCBHighRdy // OSTCBCur = OSTCBHighRdy;
LDR R1, =OSTCBCur
LDR R0, [R0]
STR R0, [R1]
LDR R0, =OSTCBHighRdy // SP = OSTCBHighRdy->task_stack;
LDR R0, [R0]
LDR SP, [R0]
LDMFD SP!, {R0} // RESTORE NEW TASK'S CONTEXT:
MSR SPSR_cxsf, R0 // Pop new task's CPSR,
LDMFD SP!, {R0-R12, LR, PC}^ // Pop new task's context.