业余时间想研究一下RTOS,但是现有的嵌入式系统很多,代码量也很大,厚厚的一本书,又是任务控制块,又是链表又是指针的指来指去,让人不耐心点根本看不下去,也没太多时间去研究。于是就有了自己动手去做的想法,这样可以提高兴趣.比看书有意思。慢慢的发现,操作系统也没有那么神秘。触发软中断,保存堆栈,开始进行任务切换。于是一个多任务就出来了,但是一个完整的操作系统并不简单,涉及到一系列的算法和数据结构的运用,还有系统的引导程序bootloader,内存管理,文件系统,网络管理,IO驱动管理等模块。
有了想法,接下来就是付诸行动。但是还得学习汇编,这成了最大阻碍,工作任务多,下班后,没太多精力去学习它。不过只要能看的懂就可以。于是把ucos/II在stm32上移植部分的汇编代码招搬过来直接利用。这样可以把主要精力放在任务调度和任务间的同步和通讯上。这次任务创建和调度的原理很简单,效率肯定也不高。以后有更好的想法了,打算改进一下任务的调度算法,比如可以利用linux内核中的list_head双向循环链表,加入就绪队列和任务延时队列。利用keilRTX系统中的内存分配机制,动态allox()分配任务的TCB控制块,总之,多学习好的系统中的思想。
以下是汇编的代码OS_CPU_A.ASM,完成任务之间的切换和堆栈的保存。
主要是两个堆栈指针OSCurTCB,OSNewTCB。可以把主要精力放在用c写任务调度和任务间的同步与通信,
利用以下汇编代码,就可以暂时不管汇编部分,该任务调度时调用OSCtxSw(),传递最高优先级任务的堆栈指针给OSNewTCB,完成两个任务切换。
- ;/*********************** (C) COPYRIGHT 2010 Libraworks *************************
- ;* File Name : os_cpu_a.asm
- ;* Author : Librae
- ;* Version : V1.0
- ;* Date : 06/10/2010
- ;* Description : μCOS-II asm port for STM32
- ;*******************************************************************************/
- IMPORT OSCurTCB ; External references
- IMPORT OSNewTCB
- IMPORT OSTaskSwHook
- EXPORT OSStartTask
- EXPORT OSCtxSw
- EXPORT OSIntCtxSw
- EXPORT OS_CPU_SR_Save ; Functions declared in this file
- EXPORT OS_CPU_SR_Restore
- EXPORT PendSV_Handler
- NVIC_PENDSV_PRI EQU 0xFFFF0000 ; PendSV priority value (lowest)
- NVIC_PENDSVSET EQU 0x10000000 ; value to trigger PendSV exception
- NVIC_INT_CTRL EQU 0xE000ED04 ; interrupt control state register
- NVIC_SYSPRI2 EQU 0xE000ED20 ; system priority register (2)
- PRESERVE8
- AREA |.text|, CODE, READONLY
- THUMB
- ;********************************************************************************************************
- ; CRITICAL SECTION METHOD 3 FUNCTIONS
- ;
- ;********************************************************************************************************
- OS_CPU_SR_Save
- MRS R0, PRIMASK ;读取PRIMASK到R0,R0为返回值
- CPSID I ;PRIMASK=1,关中断(NMI和硬件FAULT可以响应)
- BX LR ;返回
- OS_CPU_SR_Restore
- MSR PRIMASK, R0 ;读取R0到PRIMASK中,R0为参数;open interrupt
- BX LR ;返回
- ;/**************************************************************************************
- ;* 函数名称: OSStartTask
- ;*
- ;* 功能描述: 使用调度器运行第一个任务
- ;*
- ;* 参 数: None
- ;*
- ;* 返 回 值: None
- ;**************************************************************************************/
- OSStartTask
- LDR R4, =NVIC_SYSPRI2 ; set the PendSV exception priority
- LDR R5, =NVIC_PENDSV_PRI
- STR R5, [R4]
- MOV R4, #0 ; set the PSP to 0 for initial context switch call
- MSR PSP, R4
- ;切换到最高优先级的任务
- LDR R4, =NVIC_INT_CTRL ;rigger the PendSV exception (causes context switch)
- LDR R5, =NVIC_PENDSVSET ;触发PendSV异常 (causes context switch)
- STR R5, [R4]
- CPSIE I ;enable interrupts at processor level
- OSStartHang
- B OSStartHang ;should never get here
- ;/**************************************************************************************
- ;* 函数名称: OSCtxSw
- ;*
- ;* 功能描述: 任务级上下文切换
- ;*
- ;* 参 数: None
- ;*
- ;* 返 回 值: None
- ;***************************************************************************************/
- OSCtxSw
- PUSH {R4, R5}
- LDR R4, =NVIC_INT_CTRL ;触发PendSV异常 (causes context switch)
- LDR R5, =NVIC_PENDSVSET
- STR R5, [R4]
- POP {R4, R5}
- BX LR
- NOP
- ;/**************************************************************************************
- ;* 函数名称: OSIntCtxSw
- ;*
- ;* 功能描述: 中断级任务切换
- ;*
- ;* 参 数: None
- ;*
- ;* 返 回 值: None
- ;***************************************************************************************/
- OSIntCtxSw
- PUSH {R4, R5}
- LDR R4, =NVIC_INT_CTRL ;触发PendSV异常 (causes context switch)
- LDR R5, =NVIC_PENDSVSET
- STR R5, [R4]
- POP {R4, R5}
- BX LR
- NOP
- ;/**************************************************************************************
- ;* 函数名称: OSPendSV
- ;*
- ;* 功能描述: OSPendSV is used to cause a context switch.
- ;*
- ;* 参 数: None
- ;*
- ;* 返 回 值: None
- ;***************************************************************************************/
- PendSV_Handler
- CPSID I ; Prevent interruption during context switch
- MRS R0, PSP ; PSP is process stack pointer 如果在用PSP堆栈,则可以忽略保存寄存器,参考CM3权威中的双堆栈-白菜注
- CBZ R0, PendSV_Handler_Nosave ; Skip register save the first time
- SUBS R0, R0, #0x20
- STM R0, {R4-R11} ; Save remaining regs r4-11 on process stack
- LDR R1, =OSCurTCB
- LDR R1, [R1]
- STR R0, [R1] ; R0 is SP of process being switched out
- PendSV_Handler_Nosave
- PUSH {R14} ; Save LR exc_return value
- LDR R0, =OSTaskSwHook ; OSTaskSwHook();
- BLX R0
- POP {R14}
- LDR R0, =OSCurTCB ;OSCurTCB=OSNewTCB;
- LDR R1, =OSNewTCB
- LDR R2, [R1]
- STR R2, [R0]
- LDR R0, [R2] ; R0 is new process SP; SP = OSCurTCB;
- LDM R0, {R4-R11} ; Restore r4-11 from new process stac
- ADD R0, R0, #0x20
- MSR PSP, R0
- ORR LR, LR, #0x04
- CPSIE I ; Exception return will restore remaining context
- BX LR
- end
接下来定义一个任务控制块:
- typedef struct taskControlBlock
- {
- /*当前的栈顶指针*/
- OS_STK *pStackTop;
- /*当前优先级*/
- PRIO_TYPE CurPriority;
- /*任务状态*/
- uint8 TaskStat;
- /*等待时间片的个数*/
- int32 TCBDelay;
- } TCB;
- /*当前运行的任务*/
- TCB *OSCurTCB;
- /*当前准备新运行的任务*/
- TCB *OSNewTCB;
- /*当前OS中所有的任务*/
- uint8 TaskNUM=0;
- TCB OSTCBTable[MAX_TASK_NUM];
OSCurTCB和OSNewTCB分别是当前运行任务的堆栈指针和要运行的新任务的堆栈指针。
下一步就是任务的创建了,任务是如何创建的。每个任务都有自己的堆栈空间,就像是单独占用CPU一样,所以创建任务还需完成任务堆栈的初始化。
需要知道CPU是如何出栈压栈的,保存了哪些寄存器,顺序是什么,堆栈的增长方向是什么。参考《cortexM3权威指南》,书中有详细的介绍。
以下是c语言写的任务堆栈的初始化函数,位于文件OS_CPU.c中,如果需要移植,除了汇编部分OS_CPU_A.asm文件修改外,OS_CPU.c和OS_TYPE.h等文件也需要修改。仅这几个文件。
OS_STK实际上就是int32,因为stm32上堆栈指针就是32位长度。第一个参数是任务的地址,即函数的地址,第二个参数是任务的堆栈指针。
- OS_STK *OSTaskStkInit (void (*task),OS_STK *ptos)
- {
- OS_STK *stk;
- stk = ptos; /* get stack point */
- *(stk) = (uint32)0x01000000L; /* xPSR */
- *(--stk) = (uint32)task; /* Entry Point */
- *(--stk) = (uint32)0xFFFFFFFEL; /* R14 (LR) */
- *(--stk) = (uint32)0x12121212L; /* R12 */
- *(--stk) = (uint32)0x03030303L; /* R3 */
- *(--stk) = (uint32)0x02020202L; /* R2 */
- *(--stk) = (uint32)0x01010101L; /* R1 */
- *(--stk) = (uint32)0; /* R0 : argument */
- /* Remaining registers */
- *(--stk) = (uint32)0x11111111L; /* R11 */
- *(--stk) = (uint32)0x10101010L; /* R10 */
- *(--stk) = (uint32)0x09090909L; /* R9 */
- *(--stk) = (uint32)0x08080808L; /* R8 */
- *(--stk) = (uint32)0x07070707L; /* R7 */
- *(--stk) = (uint32)0x06060606L; /* R6 */
- *(--stk) = (uint32)0x05050505L; /* R5 */
- *(--stk) = (uint32)0x04040404L; /* R4 */
- return (stk);
- }
- /*
- * 创建新的任务
- */
- TCB* OSTaskCreate(void* task, OS_STK *stack,PRIO_TYPE prio)
- {
- TCB *pTCB;
- OS_CPU_SR cpu_sr = 0;
- OS_ENTER_CRITICAL();
- pTCB = OSGetFreeTCB(prio);
- if (NULL == pTCB)
- {
- OS_EXIT_CRITICAL();
- return NULL;
- }
- pTCB->pStackTop = OSTaskStkInit(task, stack);
- pTCB->CurPriority = prio;
- pTCB->TCBDelay = 0;
- TaskNUM++;
- OS_EXIT_CRITICAL();
- return pTCB;
- }
创建新任务函数很简单,只要懂了OSGetFreeTCB(prio);这个函数就没啥问题。创建的任务,是一个有序的表,就是一个存储元素为
TCB类型的数组TCB OSTCBTable[MAX_TASK_NUM];在这个数组中,先在OSTCBTable[0]中创建一个任务,如果再创建一个任务,这个任务比上个任务的优先级高,那么OSTCBTable[0]中存储优先级高的任务,那个之前先创建的低优先级的任务搬移到OSTCBTable[1]中。如果再创建一个任务,任务优先级会与前两个任务对比,若比前两个都低,就放在第三个位置OSTCBTable[2]中,否则就重新排序,总之,使OSTCBTable数组中的任务始终是按优先级从高到低的顺序排列。
以下是这种思想实现的OSGetFreeTCB(prio)函数:
- /*在TCB表中取一个空闲的节点,给新任务使用*/
- /*对OSTCBTable表这个有序表进行插入排序*/
- /*将优先级高的任务放在前面*/
- TCB* OSGetFreeTCB(PRIO_TYPE prio)
- {
- TCB *pTCB;
- int32 index=0,orgIndex;
- pTCB = &(OSTCBTable[0]);
- for (index = 0;index < TaskNUM+1;index++)
- {
- pTCB = OSTCBTable+index;
- /*已经是空闲TCB了,就直接使用*/
- if (NULL == pTCB->pStackTop)
- {
- return (TCB*)pTCB;
- }
- /*若新任务优先级比较低或相等,则向后继续找*/
- if (pTCB->CurPriority >= prio)
- {
- continue;
- }
- else /*pTCB->CurPriority < prio 找到了该插入的位置了*/
- {
- /*保存当前位置*/
- orgIndex = index;
- /*从当前节点遍历到最后一个使用了的节点*/
- for( index = TaskNUM ; index > orgIndex ; index-- )
- {
- pTCB = OSTCBTable+index;
- /*将前一个节点的数据,保存到当前节点*/
- _mem_copy((uint8 *)(pTCB),(uint8 *)(pTCB-1),sizeof(TCB));
- }
- _mem_clr((uint8 *)(pTCB-1),sizeof(TCB)) ;
- return (TCB*)(pTCB-1);
- }
- }
- return (TCB*)NULL;
- }
任务创建是基于一个有序表。这种方法虽然简单直观,但也有很大缺点。比如如果建立了100个任务,又准备建立第101个任务,且第101个任务优先级是最高的。那么创建任务就很慢,需要前面一百个任务依次都向后移,第101个任务放在数组的最前面。这是十分耗时的操作,随着任务数的增加,创建任务的时间也随着增加。后面还要讲到,这种方法建立的任务查找最高优先级时,需要遍历数组,在效率上也是不快的,尤其是任务数目多时。
任务创建之后,接下来的事情就是何时需要任务切换,如何查找最高优先级了。先说下何时需要任务切换,每个任务都是一个while(1)死循环,在里面执行到OSTimedly()时就会挂起当前任务,查找最高优先级的任务。每个任务的控制块中都有个延时时间的变量,当这个延时时间变量大于0,说明这个任务还处于睡眠或挂起状态,不能够被执行。因此还需要一个系统时钟函数,作为整个系统的调度中心,每到一个系统时钟中断,让所有任务的延时时间减一。
- /*
- *系统时钟函数,在时钟中断中调用
- */
- void OSTimeTick (void)
- {
- int8 index;
- TCB *pTCB;
- uint8 flagFirstTask=0;
- OS_CPU_SR cpu_sr = 0;
- OS_ENTER_CRITICAL();
- /*初始化*/
- OSNewTCB = NULL;
- /*禁止调度*/
- if (OSScheLock != 0)
- {
- OS_EXIT_CRITICAL();
- return;
- }
- for (index = 0;index < TaskNUM;index++)
- {
- pTCB = OSTCBTable+index;
- /*该任务在睡眠状态,必须将所有时延都--*/
- if (pTCB->TCBDelay > 0)
- {
- pTCB->TCBDelay--;
- continue;
- }
- /*该任务被挂起*/
- if (pTCB->TaskStat == OS_Task_Pend)
- {
- continue;
- }
- /*任务优先级查找算法,以后考虑改进查找速度*/
- /* 是否找到了应该调度进去的就绪任务*/
- if (flagFirstTask==0)
- {
- /*找到了最高优先级的任务,
- 并且比当前任务优先级高*/
- if (OSCurTCB->CurPriority < pTCB->CurPriority)
- {
- flagFirstTask = 1;
- OSNewTCB = pTCB;
- continue;
- }
- /*找到了比当前优先级低的任务*/
- if (OSCurTCB->CurPriority > pTCB->CurPriority)
- {
- if (OSNewTCB == NULL)
- {
- flagFirstTask = 1;
- OSNewTCB = pTCB;
- continue ;
- }
- else
- {
- flagFirstTask = 1;
- continue ;
- }
- }
- /*找到了最高优先级的任务,
- 并且跟当前任务优先级相等*/
- if (OSCurTCB->CurPriority == pTCB->CurPriority)
- {
- /*该任务在当前任务之后*/
- if ((pTCB > OSCurTCB)||(pTCB == OSCurTCB))
- {
- flagFirstTask = 1;
- OSNewTCB = pTCB;
- continue ;
- }
- /*在当前任务之前或者就是当前任务
- 则还需要继续向后搜索第一个同优先级的任务*/
- if ((pTCB < OSCurTCB)&&(OSNewTCB == NULL))
- {
- OSNewTCB = pTCB;
- continue;
- }
- }
- continue;
- }
- }
- OS_EXIT_CRITICAL();
- }
- void SysTick_Handler(void)
- {
- OSIntEnter(); //进入中断
- OSTimeTick();
- OSIntExit(); //触发任务切换软中断
- }
- /*
- *记录中断嵌套层数
- */
- void OSIntEnter (void)
- {
- if (NULL != OSCurTCB)
- {
- if (OSIntNesting < 255u)
- {
- OSIntNesting++; /* Increment ISR nesting level */
- }
- }
- }
- /*
- *中断退出时调用,触发中断级任务切换
- */
- void OSIntExit (void)
- {
- OS_CPU_SR cpu_sr = 0u;
- if (NULL!= OSCurTCB)
- {
- OS_ENTER_CRITICAL();
- if (OSIntNesting > 0u)
- {
- OSIntNesting--;
- }
- if (OSIntNesting == 0u)
- {
- /* 当所有的中断完成候再判断是否调度 */
- if (OSNewTCB != OSCurTCB)
- {
- /* 中断级任务切换 */
- OSIntCtxSw();
- }
- }
- OS_EXIT_CRITICAL();
- }
- }
在这个函数中,比较当前任务指针是否发生了改变,若发生了改变,说明需要进行任务切换了。
下面再看看延时函数OSTimeDly (int32 ticks)
- void OSTimeDly (int32 ticks)
- {
- OS_CPU_SR cpu_sr = 0;
- int8 index;
- TCB *pTCB;
- OS_ENTER_CRITICAL();
- OSCurTCB->TCBDelay = ticks;
- OSNewTCB = NULL;
- /*任务优先级查找算法,从当前任务
- 向后遍历,第一个最大的优先级的任务
- 就是需要调度进去的任务*/
- for (index = 0; index < TaskNUM;index++)
- {
- pTCB = OSTCBTable+index;
- /*跳过睡眠任务*/
- if (pTCB->TCBDelay != 0)
- {
- continue;
- }
- /*跳过挂起任务*/
- if (pTCB->TaskStat == OS_Task_Pend)
- {
- continue;
- }
- /*找到了最高优先级的任务,
- 并且比当前任务优先级高*/
- if (OSCurTCB->CurPriority < pTCB->CurPriority)
- {
- OSNewTCB = pTCB;
- break;
- }
- /*找到了比当前优先级低的任务*/
- if (OSCurTCB->CurPriority > pTCB->CurPriority)
- {
- /*如果当前任务之前有同优先级的就绪任务,
- 则选择该任务,否则就使用*/
- if (OSNewTCB == NULL)
- {
- OSNewTCB = pTCB;
- }
- break;
- }
- /*找到了最高优先级的任务,
- 并且跟当前任务优先级相等*/
- if (OSCurTCB->CurPriority == pTCB->CurPriority)
- {
- /*该任务在当前任务之后*/
- if ((pTCB > OSCurTCB))
- {
- OSNewTCB = pTCB;
- break ;
- }
- /*在当前任务之前或者就是当前任务
- 则还需要继续向后搜索第一个同优先级的任务*/
- if (((pTCB < OSCurTCB)||(pTCB == OSCurTCB))
- &&(OSNewTCB == NULL))
- {
- OSNewTCB = pTCB;
- continue;
- }
- }
- }
- OS_EXIT_CRITICAL();
- OSTaskSche();
- }
以上实现了简单的任务调度和切换。接下来,就是任务间如何进行同步和通讯...
在CSDN资源中可以下载工程的源码,KEILMDK4.23的IDE