二、uCOS-II移植
嵌入式实时操作系统uCOS-II移植的核心在于任务切换时上下文环境的保存及恢复,针对Cortex-M3内核的单片机,其采用了PendSVHandler中断处理的方式解决这一核心问题。我们要做的就是在任务切换及中断任务切换过程中开启触发PendSV中断,并在PendSVHandler中实现上下文环境的切换,即保存CPU内部寄存器值和恢复切换任务的相关信息。
为什么要使用PendSV中断实现上下文切换呢?参阅Cortex-M3的一段话。
另一个相关的异常是PendSV(可悬起的系统调用),它和SVC协同使用。一方面,SVC异常是必须在执行SVC指令后立即得到响应的(对于SVC异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬fault——译者注),应用程序执行SVC时都是希望所需的请求立即得到响应。另一方面,PendSV则不同,它是可以像普通的中断一样被悬起的(不像SVC那样会上访)。OS可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬起PendSV 的方法是:手工往NVIC的PendSV悬起寄存器中写1。悬起后,如果优先级不够高,则将缓期等待执行。
PendSV的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
- 执行一个系统调用
-系统滴答定时器(SYSTICK)中断,(轮转调度中需要)
让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过SysTick异常启动上下文切换。如图所示。
上图是两个任务轮转调度的示意图。但若在产生SysTick 异常时正在响应一个中断,则SysTick异常会抢占其ISR。在这种情况下,OS是不能执行上下文切换的,否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这种事。因此,在CM3中也是严禁没商量——如果OS在某中断活跃时尝试切入线程模式,将触犯用法fault异常。
为解决此问题,早期的OS大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了IRQ,则本次SysTick在执行后不得作上下文切换,只能等待下一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”,使上下文切换迟迟不能进行。
现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行。为实现这个机制,需要把PendSV编程为最低优先级的异常。如果OS检测到某IRQ正在活动并且被SysTick抢占,它将悬起一个PendSV异常,以便缓期执行上下文切换。如图所示
个中事件的流水账记录如下:
1. 任务A呼叫SVC来请求任务切换(例如,等待某些工作完成)
2. OS接收到请求,做好上下文切换的准备,并且悬起一个PendSV异常。
3. 当CPU退出SVC后,它立即进入PendSV,从而执行上下文切换。
4. 当PendSV执行完毕后,将返回到任务B,同时进入线程模式。
5. 发生了一个中断,并且中断服务程序开始执行
6. 在ISR执行过程中,发生SysTick异常,并且抢占了该ISR。
7. OS执行必要的操作,然后悬起PendSV异常以作好上下文切换的准备。
8. 当SysTick退出后,回到先前被抢占的ISR中,ISR继续执行
9. ISR执行完毕并退出后,PendSV服务例程开始执行,并且在里面执行上下文切换
10. 当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。
1、任务堆栈初始化
创建一个新任务时需要对该任务的堆栈进行初始化,其程序如下
/*
*********************************************************************************************************
* INITIALIZE A TASK'S STACK
*
* Description: This function is called by either OSTaskCreate() or OSTaskCreateExt() to initialize the
* stack frame of the task being created. This function is highly processor specific.
*
* Arguments : task is a pointer to the task code
*
* p_arg is a pointer to a user supplied data area that will be passed to the task when the task first executes.
*
* ptos is a pointer to the top of stack. It is assumed that 'ptos' points to a 'free' entry on the task stack. If OS_STK_GROWTH is set to 1 then
* 'ptos' will contain the HIGHEST valid address of the stack. Similarly, if OS_STK_GROWTH is set to 0, the 'ptos' will contains the LOWEST valid address of the stack.
*
* opt specifies options that can be used to alter the behavior of OSTaskStkInit(). (see uCOS_II.H for OS_TASK_OPT_xxx).
*
* Returns : Always returns the location of the new top-of-stack once the processor registers have been placed on the stack in the proper order.
*
* Note(s) : 1) Interrupts are enabled when your task starts executing.
* 2) All tasks run in Thread mode, using process stack.
*********************************************************************************************************
*/
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
(void)opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(--stk) = (INT32U)task; /* Entry Point */
*(--stk) = (INT32U)0xFFFFFFFEL;
/* R14 (LR) (init value will cause fault if ever used)*/
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(--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 */
return (stk);
}
任务堆栈的初始化主要是对CPU内部寄存器进行入栈操作,由于M3内核堆栈方向是高地址向低地址生长的,故堆栈地址是递减的,且须按照程序中的顺序操作。具体原因,请研读Cortex-M3权威指南。
2、上下文环境切换
上文中已经介绍过,一般发生系统调用时,例如使用系统函数OSTimeDly 、OSTimeDlyHMSM 需要调用OS_Sched的OSCtxSw进行任务级别的上下文切换。systick轮询中断产生时,需调用OSIntExit 中的OSIntCtxSw() 进行中断级的任务切换,在这两个函数中我们只需开启PendSV中断,在PendSV函数中实现上下文环境的的保存及恢复。
OSCtxSw
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
PendSV_Handler
CPSID I
MRS R0, PSP
CBZ R0, OS_CPU_PendSVHandler_nosave
SUBS R0, R0, #0x20
STM R0, {R4-R11}
LDR R1, =OSTCBCur
LDR R1, [R1]
STR R0, [R1]
OS_CPU_PendSVHandler_nosave
PUSH {R14}
LDR R0, =OSTaskSwHook
BLX R0
POP {R14}
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2]
LDM R0, {R4-R11}
ADDS R0, R0, #0x20
MSR PSP, R0
ORR LR, LR, #0x04
CPSIE I
BX LR
在这之前我们需要设置PendSV中断的优先级,以及在系统初始化的时候,启动最高优先级别的任务,这主要是由OSStartHighRdy 函数来实现的。
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OSRunning ; OSRunning = TRUE
MOVS R1, #1
STRB R1, [R0]
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
OSStartHang
B OSStartHang
SYSTICK中断服务函数:
void SysTick_Handler(void)
{
OSIntEnter();
OSTimeTick();
OSIntExit();
}
3、其它设置
临界代码宏定义
#define OS_CRITICAL_METHOD 3
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
#endif
OS_CPU_SR_Save
MRS R0, PRIMASK
CPSID I
BX LR
OS_CPU_SR_Restore
MSR PRIMASK, R0
BX LR
在移植过程中,一定要注意修改uncomment掉stmf10x_it.c文件中的pendsv handler和systick handler,并修改os_cpu_asm.asm文件中的中断处理函数名,并使用 EXPORT PendSV_Handler,否则移植不能通过。
4、测试
测试代码如下:
OS_STK Task1Stack[TASK1STACKSIZE];
OS_STK Task2Stack[TASK2STACKSIZE];
void TaskCreate(void)
{
OSInit();
OSTaskCreate(Task1,(void *)0,(OS_STK*)&Task1Stack[TASK1STACKSIZE-1],TASK1PRIO);
OSTaskCreate(Task2,(void *)0,(OS_STK*)&Task2Stack[TASK2STACKSIZE-1],TASK2PRIO);
OSStart();
}
void Task1(void *pdata)
{
pdata = pdata;
for(;;)
{
printf("Task1 is running!\n");
OSTimeDly(10);
}
}
void Task2(void *pdata)
{
pdata = pdata;
for(;;)
{
printf("Task2 is running!\n");
OSTimeDly(10);
}
}
测试输出: