本文的目的是希望读者能够通过本文的内容掌握移植uCOS-II 的规范方法。如果只是需要移植文件,可以直接去Micriμm的官网上下载。
移植uCOS-II,主要的移植工作是编写如下三个文件:
OS_CPU.H
OS_CPU_C.C
OS_CPU_A.ASM
下面就按照这三个文件的顺序来介绍。本文以STM32F107+RealView Compiler 开发环境为例。如果使用的其他的开发环境,个别代码可能需要做些小修改。
OS_CPU.H 的第一部分是定义了一个宏OS_CPU_EXT。这一部分暂时可以先不去管。
#ifdef OS_CPU_GLOBALS
#define OS_CPU_EXT
#else
#define OS_CPU_EXT extern
#endif
接下来是一系列的类型定义。这一部分的移植需参考RealView Compiler Reference Guide的如下章节:
RealView Compiler Reference Guide->C and C++ Implementation Details->Basic data types
从这里可以得到如下信息。
Type |
Size in bits |
Natural alignment in bytes |
char |
8 |
1 (byte-aligned) |
short |
16 |
2 (halfword-aligned) |
int |
32 |
4 (word-aligned) |
long |
32 |
4 (word-aligned) |
long long |
64 |
8 (doubleword-aligned) |
float |
32 |
4 (word-aligned) |
double |
64 |
8 (doubleword-aligned) |
long double |
64 |
8 (doubleword-aligned) |
All pointers |
32 |
4 (word-aligned) |
_Bool |
8 |
1 (byte-aligned) |
根据上面的信息,形成下面的代码:
typedef unsigned char BOOLEAN; typedef unsigned char INT8U; typedef signed char INT8S; typedef unsigned short INT16U; typedef signed short INT16S; typedef unsigned int INT32U; typedef signed int INT32S; typedef float FP32; typedef double FP64; typedef unsigned int OS_STK; typedef unsigned int OS_CPU_SR;
上面代码中OS_STK 表示堆栈出栈、入栈的基本数据长度。我们知道Cortex-M3 的所有堆栈操作都是以字为单位的,所以这里为 unsigned int 型。OS_CPU_SR 对应的是程序状态寄存器PSRs,自然也是unsigned int 型。
然后是关于临界区的处理,一般来说我们都喜欢使用第三种方法来实现临界区,这里也不例外。这里多说几句,第一种直接开关中断的实现临界区的方法很少采用,因为这种方法可能将原本关闭了的中断意外的打开。第二种方法是最高效的实现方法,但是这种方法调整了堆栈指针,对于需要利用堆栈指针间接寻址局部变量的系统并不适用。(x86通常采用第二种方法,因为它有单独的寄存器来做局部变量的寻址)第三种方法最保险,虽然效率比第二种方法略低一点。
#define OS_CRITICAL_METHOD 3 #define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();} #define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);} #if OS_CRITICAL_METHOD == 3 OS_CPU_SR OS_CPU_SR_Save(void); void OS_CPU_SR_Restore(OS_CPU_SR cpu_sr); #endif
EXPORT OS_CPU_SR_Save EXPORT OS_CPU_SR_Restore OS_CPU_SR_Save MRS R0, PRIMASK ; Set prio int mask to mask all (except faults) CPSID I BX LR OS_CPU_SR_Restore MSR PRIMASK, R0 BX LR
__asm OS_CPU_SR OS_CPU_SR_Save(void) { MRS R0, PRIMASK ; Set prio int mask to mask all (except faults) CPSID I BX LR } __asm void OS_CPU_SR_Restore(OS_CPU_SR cpu_sr) { MSR PRIMASK, R0 BX LR }
上面的代码利用的RealView Compiler 的特殊功能(Embedded assembler),如需进一步的信息,可以参考RealView Compiler Reference Guide中Using the Inline and Embedded Assemblers这一章的内容。
然后是堆栈增长方向,ARM Cortex-M3 的堆栈是倒生的:
#define OS_STK_GROWTH 1
任务切换,OSCtxSw()在OS_CPU_A.ASM 中定义:
#define OS_TASK_SW() OSCtxSw()
最后是一些函数原型声明:
void OSCtxSw(void); void OSIntCtxSw(void); void OSStartHighRdy(void); void OS_CPU_PendSVHandler(void); void OS_CPU_SysTickHandler(void);
在原本uCOS-II 的移植代码中是没有这个文件的。由于下面这9个函数的函数体基本都是空的,并且移植时几乎不需要更改,所以我就将其拿出到一个单独的文件中来了。
OSInitHookBegin()
OSInitHookEnd()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskIdleHook()
OSTaskStatHook()
OSTaskSwHook()
OSTCBInitHook()
OSTimeTickHook()
这9个函数的代码都很简单,下面是代码,不多介绍。
#if OS_CPU_HOOKS_EN > 0 && OS_VERSION > 203 void OSInitHookBegin (void) { #if OS_TMR_EN > 0 OSTmrCtr = 0; #endif } #endif #if OS_CPU_HOOKS_EN > 0 && OS_VERSION > 203 void OSInitHookEnd (void) { } #endif #if OS_CPU_HOOKS_EN > 0 void OSTaskCreateHook (OS_TCB *p_tcb) { #if OS_VIEW_MODULE > 0 OSView_TaskCreateHook(p_tcb); #else (void)p_tcb ; /* Prevent compiler warning */ #endif } #endif #if OS_CPU_HOOKS_EN > 0 void OSTaskDelHook (OS_TCB *p_tcb) { (void)p_tcb ; /* Prevent compiler warning */ } #endif #if OS_CPU_HOOKS_EN > 0 && OS_VERSION >= 251 extern volatile unsigned long wdg_clr_flag; void OSTaskIdleHook (void) { } #endif #if OS_CPU_HOOKS_EN > 0 void OSTaskStatHook (void) { } #endif #if (OS_CPU_HOOKS_EN > 0) && (OS_TASK_SW_HOOK_EN > 0) void OSTaskSwHook (void) { #if OS_VIEW_MODULE > 0 OSView_TaskSwHook(); #endif } #endif #if OS_CPU_HOOKS_EN > 0 && OS_VERSION > 203 void OSTCBInitHook (OS_TCB *ptcb) { (void) ptcb; /* Prevent Compiler warning */ } #endif #if (OS_CPU_HOOKS_EN > 0) && (OS_TIME_TICK_HOOK_EN > 0) void OSTimeTickHook (void) { #if OS_VIEW_MODULE > 0 OSView_TickHook(); #endif #if OS_TMR_EN > 0 OSTmrCtr++; if (OSTmrCtr >= (OS_TICKS_PER_SEC / OS_TMR_CFG_TICKS_PER_SEC)) { OSTmrCtr = 0; OSTmrSignal(); } #endif }
重要的移植工作都在这两个文件中提供,由于RealView Compiler 支持在C文件中插入汇编代码,所以OS_CPU_A.ASM 文件实际上可以去掉。所有的函数都在OS_CPU_C.C 中实现。下面分别介绍。
OSTaskStkInit 的移植是比较有难度的。这个函数是用来初始化各个任务的堆栈,使各个任务的堆栈就像是刚才中断处理函数中返回那样。
OS_STK *OSTaskStkInit (void (*p_task)(void *p_arg), void *p_arg, OS_STK *p_tos, INT16U opt) { OS_STK *stk; (void)opt; /* 'opt' is not used, prevent warning */ stk = p_tos; /* Load stack pointer */ /* Registers stacked as if auto-saved on exception */ *(stk) = (INT32U)0x01000000L; /* xPSR, the ‘T’ bit is set */ *(--stk) = (INT32U)p_task; /* Entry Point of the task */ *(--stk) = (INT32U)OS_TaskReturn; /* the return address of the task */ *(--stk) = (INT32U)0x12121212L; /* R12 */ *(--stk) = (INT32U)0x03030303L; /* R3 */ *(--stk) = (INT32U)0x02020202L; /* R2 */ *(--stk) = (INT32U)0x01010101L; /* R1 */ *(--stk) = (INT32U)p_arg; /* R0 : 1st argument to the task */ /* 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); }
想要理解上面的代码需要知道Cortex-M3在响应外部中断时对寄存器的压栈顺序,还需知道函数的第一个参数是通过R0来传递的。建议阅读ARM Cotex M3 权威指南,里面有详细的介绍。这里我只说一处,就是OS_TaskReturn 位置对应的是任务的返回地址。我们知道,uCOS-II 中任务就是简单的函数。普通的函数执行完成后会返回到调用它的地方的下一条语句处继续执行。这个位置就记录在堆栈中,也就是OS_TaskReturn所在的位置。uCOS-II要求任务必须是无限的循环,不允许退出。所以理论上永远不会跳转到OS_TaskReturn处执行。OS_TaskReturn的作用是当程序异常退出时不至于程序跑飞。在现在的移植代码中OS_TaskReturn 也是个简单的函数,没有加入额外的保护代码。
void OS_TaskReturn(void) { while(1); }
这个函数只被OSStart()调用。用来运行最高优先级的任务。代码如下。
__asm void OSStartHighRdy(void) { 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 }
其中如下三行代码是用来设置PendSV异常的优先级为最低。
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority LDR R1, =NVIC_PENDSV_PRI STRB R1, [R0]
相当于如下的C代码:
NVIC->IP[14] = 0xFF;
下面两行代码的作用是使线程堆栈指针 PSP = 0。PendSV_Handler 中需要根据它来判断是否是OSStartHighRdy 引起的PendSV,因为这时要特殊处理一下。
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call MSR PSP, R0
最后四行的作用是引起一次 PendSV。相当于下面的C代码:
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
在其他处理器的移植代码中,这两个函数还是有些工作要做的。但是对于Cortex-M3 就简单的多了,只要引起一次PendSV 就行了,具体的任务切换由PendSV来处理。
void OSCtxSw(void) { SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; } void OSIntCtxSw(void) { SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; }
也可以写为汇编代码,写为汇编的好处是两个函数可以共用一个函数体:
OSCtxSw OSIntCtxSw LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority LDR R1, =NVIC_PENDSV_PRI STRB R1, [R0]
SysTick 用来处理操作系统的计时。代码很简单,无需多说。
void SysTick_Handler(void) { OS_CPU_SR cpu_sr; OS_ENTER_CRITICAL(); /* Tell uC/OS-II that we are starting an ISR */ OSIntNesting++; OS_EXIT_CRITICAL(); OSTimeTick(); /* Call uC/OS-II's OSTimeTick() */ OSIntExit(); }
最终的任务切换工作都在这里完成。下面先给出伪代码。从这里就可以看出OSStartHighRdy 中将PSP 写为0 的作用了。
OS_CPU_PendSVHandler { if (PSP != NULL) { Save R4-R11 onto task stack; OSTCBCur->OSTCBStkPtr = SP; } OSTaskSwHook(); OSPrioCur = OSPrioHighRdy; OSTCBCur = OSTCBHighRdy; PSP = OSTCBHighRdy->OSTCBStkPtr; Restore R4-R11 from new task stack; Return from exception; }
下面给出真实的代码,可以看出与伪代码是对应的:
__asm void PendSV_Handler(void) { EXTERN OSPrioCur EXTERN OSPrioHighRdy EXTERN OSTCBCur EXTERN OSTCBHighRdy EXTERN OSTaskSwHook CPSID I ; Prevent interruption during context switch MRS R0, PSP ; PSP is process stack pointer CBZ R0, PendSVHandler_nosave ; Skip register save the first time SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack STM R0, {R4-R11} LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP; LDR R1, [R1] STR R0, [R1] ; R0 is SP of process being switched out ; At this point, entire context of process has been saved PendSVHandler_nosave PUSH {R14} ; Save LR exc_return value LDR R0, =OSTaskSwHook ; OSTaskSwHook(); BLX R0 POP {R14} LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy; LDR R1, =OSPrioHighRdy LDRB R2, [R1] STRB R2, [R0] LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy; LDR R1, =OSTCBHighRdy LDR R2, [R1] STR R2, [R0] LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr; LDM R0, {R4-R11} ; Restore r4-11 from new process stack ADDS R0, R0, #0x20 MSR PSP, R0 ; Load PSP with new process SP ORR LR, LR, #0x04 ; Ensure exception return uses process stack CPSIE I BX LR ; Exception return will restore remaining context }
至此,移植工作完成。uCOS-II 在 Cortex-M3上的移植与其他单片机上移植代码的最大区别在于所有的任务切换工作都放到了 PendSV 中进行,而PendSV 中断的优先级被设为最低,这样就能保证更高优先级的中断能够及时被处理。不过,在PendSV 中断处理代码中第一条语句就是关中断,这时如果来了更高优先级的中断,也是无法响应的。能否进一步改善中断响应性能还需再思考。个人认为应该还有进一步优化的可能,不过具体该如何优化,暂时还没有头绪。