一个例子:
int Main(void)
{
TargetInit(); //初始化目标板
OSInit(); //初始化操作系统
OSTaskCreate(Task0,&StackTask0[StackSizeTask0 - 1],PrioTask0); // 创建一个任务
Uart_Printf("Ready to start OS\n");
OSStart(); //运行操作系统
return 0; //程序不会运行至此
}
void Task0(void)
{
TargetStart(); //设置中断向量,启动操作系统的硬件定时器中断
Uart_Printf("Start OS\n");
// 创建其他任务
OSTaskCreate(Task1,&StackTask1[StackSizeTask1 - 1],PrioTask1);
OSTaskCreate(Task2,&StackTask2[StackSizeTask2 - 1],PrioTask2);
OSTaskCreate(Task3,&StackTask3[StackSizeTask2 - 1],PrioTask3);
while(1)
{
Uart_Printf("Task0\n");
OSTimeDly(100); //1 秒运行一次
}
}
void Task1(void)
{
while(1)
{
Uart_Printf("Task1\n");
OSTimeDly(300); //3 秒运行一次
}
}
void Task2(void)
{
while(1)
{
Uart_Printf("Task2\n");
OSTaskSuspend(PrioTask2); // 使自己进入挂起状态
}
}
void Task3(void)
{
while(1)
{
Uart_Printf("Resume Task2\n");
OSTaskResume(PrioTask2); // 恢复任务 2
OSTimeDly(800);
}
}
程序中创建了四个任务,任务 0 每 1 秒运行一次,任务 1 每 3 秒运行一次,任务 2 运行一次即把自己挂起,任务 3 每 8 秒运行一次并把任务 2 恢复。
在多任务系统中,可以同时执行多个并行任务,各个任务之间互相独立。通过操作系统执行任务调度而实现宏观上的“并发运行”。从宏观上不同的任务并发运行,好像每个任务都有自己的 CPU 一样。
其实在单一 CPU 的情况下,是不存在真正的多任务机制的,存在的只有不同的任务轮流使用 CPU,所以本质上还是单任务的。但由于 CPU 执行速度非常快,加上任务切换十分频繁并且切换的很快,所以我们感觉好像有很多任务同时在运行一样。这就是所谓的多任务机制。
多任务的最大好处是充分利用硬件资源,如在单任务时(大循环结构,如大部分 51程序)遇到 delay 函数时,CPU 在空转;而在多任务系统,遇到 delay 或需等待资源时系统会自动运行下一个任务,等条件满足再回来运行先前的任务,这样就充分利用了 CPU,提高了效率。
任务
任务有下面的特性:
动态性
任务并不是随时都可以运行的,而一个已经运行的任务并不能保证一直占有 CPU 直到运行完。一般有就绪态,运行态,挂起态等。
- 运行态。一个运行态的任务是一个正在使用 CPU 的任务。任何时刻有且只有一个运行着的任务。
- 就绪态。一个就绪态任务是可运行的,等待占有 CPU 的任务释放 CPU。
- 挂起态。某些条件不满足而挂起不能运行的状态。
如何实现状态的转换?
其实就和排值日表一样,在不同的表里面跳来跳去。
INT32U OSRdyTbl; /* 就绪任务表 */
/* 在就绪表中登记就绪任务 */
#define OSSetPrioRdy(prio) \
{ \
OSRdyTbl |= 0x01<<prio; \
}
/* 从就绪表中删除任务 */
#define OSDelPrioRdy(prio) \
{ \
OSRdyTbl &= ~(0x01<<prio); \
}
上面定义一个 32 位变量,每一位代表一个任务,0 表示挂起状态,1 表示就绪状态。它记录了各任务的就绪与否状态,称它为就绪表。OSRdyTbl 定义为 32 位变量,对应 32 个任务。当然,定义为 64 位的话,便最多能支持 64 个任务。
独立性
任务之间互相独立,不存在互相调用的关系。所有任务在逻辑上都是平等的。
由于任务之间互相看不见,所以他们之间的信息传输就无法当面完成。这就需要各种通信机制如信号量,消息邮箱,队列等来实现。
并发性
由同一个处理器轮换地运行多个程序。或者说是由多个程序轮班地占用处理器这个资源。且在占用这个资源期间,并不一定能够把程序运行完毕。
抢占式调度
调度的概念,通俗的说就是系统在多个任务中选择合适的任务执行。系统如何知道何时该执行哪个任务?可以为每个任务安排一个唯一的优先级别,当同时有多个任务就绪时,优先运行优先级较高的任务。
同时,任务的优先级也作为任务的唯一标识号。代码中都是对标识号来完成对任务的操作的。如 OSDelPrioRdy(prio),OSSetPrioRdy(prio)等。
不同的优先级对应就绪表中的每一位。低位对应高优先级。优先级 0 的优先权最高,优先级 31 的优先权最低。
所以在程序一开始就要先为每一个任务分配一个唯一的优先级:
///**************定义任务优先级*************///
#define PrioTask0 0
#define PrioTask1 1
#define PrioTask2 2
#define PrioTask3 3
所谓“抢占式调度”是指:一旦就绪状态中出现优先权更高的任务,便立即剥夺当前任务的运行权,把 CPU 分配给更高优先级的任务。这样 CPU 总是执行处于就绪条件下优先级最高的任务。
在程序中查找最高优先级的任务代码如下:
/* 在就绪表中查找更高级的就绪任务 */
#define OSGetHighRdy()
{
for( OSNextTaskPrio = 0; (OSNextTaskPrio < OS_TASKS) && (!(OSRdyTbl & (0x01<<OSNextTaskPrio))); OSNextTaskPrio ++ );
OSPrioHighRdy = OSNextTaskPrio;
}
多任务系统的时间管理
多任务系统自身也是需要一个时钟的,滴答定时器经常被拿来充当这一角色。
与人一样,多任务系统也需要一个“心跳”来维持其正常运行,这个心跳叫做时钟节拍,通常由定时器产生一个固定周期的中断来充当,频率一般为 50-100Hz。在TargetInit.c 文件中有下面的定时器 0 初始化函数,T0 用作系统心跳计时,产生时钟节拍。
#define OS_TICKS_PER_SEC 100 /* 设置一秒内的时钟节拍数*/
void StartTicker(INT32U TicksPerSec)
{
rTCFG0 = 99; //Prescaler0 = 99
rTCFG1 = 0x03; //Select MUX input for PWM Timer0:divider=16
rTCNTB0 = 31250 / TicksPerSec; //设置中断频率
rTCON |= (1<<1); //Timer 0 manual update
rTCON = 0x09; //Timer 0 auto reload on
//Timer 0 output inverter off
//清"Timer 0 manual update"
//Timer 0 start */
BIT_CLR(rINTMSK, BIT_TIMER0); // Enable WatchDog interrupts
}
OSTimeDly 函数就是以时钟节拍为基准来延时的。这个函数完成功能很简单,就是先挂起当起当前任务,设定其延时节拍数,然后进行任务切换,在指定的时钟节拍数到来之后,将当前任务恢复为就绪状态。任务必须通过 OSTimeDly 或 OSTaskSuspend 让出 CPU的使用权,使更低优先级任务有机会运行。
void OSTimeDly(INT32U ticks)
{
if( ticks > 0 ) /* 当延时有效 */
{
OS_ENTER_CRITICAL();
OSDelPrioRdy(OSPrioCur); /* 把任务从就绪表中删去 */
TCB[OSPrioCur].OSTCBDly = ticks; /* 设置任务延时节拍数 */
OS_EXIT_CRITICAL();
OSSched(); /* 重新调度 */
}
}
在 T0 的中断服务函数中,依次对各个延时任务的延时节拍数减 1。若发现某个任务的延时节拍数变为 0,则把它从挂起态置为就绪态。
void TickInterrupt(void)
{
static INT8U i;
OSTime ++;
//Uart_SendByte('I');
for(i = 0; i < OS_TASKS; i++) /* 刷新各任务时钟 */
{
if(TCB[i].OSTCBDly )
{
TCB[i].OSTCBDly --;
if(TCB[i].OSTCBDly == 0) /* 当任务时钟到时,必须是由定时
器减时的才行*/
{
OSSetPrioRdy(i); /* 使任务可以重新运行 */
}
}
}
rSRCPND |= BIT_TIMER0;
rINTPND |= BIT_TIMER0;
}
系统自身创建了一个空闲任务,并设它为最低优先级,当系统没有任何任务就绪时,则运行这个任务,让 CPU“有事可干”。用户程序可以在这个任务中加入一些“无关紧要”的功能,如统计 CPU 使用率等。
void IdleTask(void)
{
IdleCount = 0;
while(1)
{
IdleCount++;
//Uart_Printf("IdleCount %d\n",IdleCount);
}
}
多任务除了需要优先级还需要什么?
只有一个 CPU,如何在同一时间实现多个独立程序的运行?要实现多任务,条件是每个任务互相独立。人如何才能独立,有自己的私有财产。任务也一样,如果一个任务有自己的 CPU,堆栈,程序代码,数据存储区,那这个任务就是一个独立的任务。
下面我们来看看是任务是如何“独立”的。
任务程序怎么写的?
void task ( )
{
//initialize 代码
while(1)
{
//your code
}
}
每个任务的程序代码与函数一样,与 51 的裸奔程序一样,每个任务都是一个大循环。
如果一个任务正在运行某个公共函数时(如 Printf),被另一个高优先级的任务抢占,那么当这个高优先级的任务也调用同一个公共函数时,极有可能破坏原任务的数据。因为两个任务可能共用一套数据。为了防止这种情况发生,常采用两种措施:可重入设计和互斥调用。
可重入函数中所有的变量均为局部变量,局部变量在调用时临时分配空间,所以不同的任务在不同的时刻调用该函数时,它们的同一个局部变量所分配的存储空间并不相同(任务私有栈中),互不干扰。另外,如果可重入函数调用了其他函数,则这些被调用的函数也必须是可重入函数。
数据存储
然后是数据存储区,由于全局变量是系统共用的,各个任务共享,不是任务私有,所以这里的数据存储区是指任务的私有变量,如何变成私有?局部变量也。编译器是把局部变量保存在栈里的,所以好办,只要任务有个私有的栈就行。
临界资源
临界资源是一次仅允许一个任务使用的共享资源。每个任务中访问临界资源的那段程序称为临界区。
在多任务系统中,为保障数据的可靠性和完整性,共享资源要互斥(独占)访问,所以全局变量(只读的除外)不能同时有多个任务访问,即一个任务访问的时候不能被其他任务打断。共享资源是一种临界资源。
实现互斥(独占)访问的方法有关中断,关调度,互斥信号量,计数信号量等。
定义了如下处理临界资源访问的代码, 主要用于在进入临界区之前关闭中断,在退出临界区后恢复原来的中断状态。
INT32U cpu_sr; /* 进入临界代码区时保存 CPU 状态*/
/* 进入临界资源代码区 */
#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR()) /* 关总中断 */
/* 退出临界代码区 */
#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr)) /* 恢复原来中断状态*/
OSCPUSaveSR
mrs r0,CPSR ; 保存当前中断状态,参阅汇编子程序调用 ATPCS 规则,
; r0 保存传递的参数
orr r1,r0,#NOINT ; 关闭所有中断
msr CPSR_c,r1
mov pc,lr
OSCPURestoreSR
msr CPSR_c,r0 ; 恢复原来的中断状态
mov pc,lr
为了不影响程序当前的中断状态,先当前的 CPSR 保存起来,退出临界区后再恢复。
注意 OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL()要成对使用。
示例:
OS_ENTER_CRITICAL();
Printf(…);//临界代码
OS_EXIT_CRITICAL();
这样,在临界区内,只要任务不主动放弃 CPU 使用权,别的任务就没有占用 CPU 的机会,相当与独占 CPU 了。临界区的代码要尽量短,因为它会使系统响应性能降低。
私有栈
私有栈的作用是存放局部变量,函数的参数,它是一个线性的空间,所以可以申请一个静态数组,把栈顶指针 SP 指向栈的数组的首元素(递增栈)或最后一个元素(递减栈)。即可打造一个人工的栈出来。
///******************任务堆栈大小定义***********///
#define StackSizeTask0 512
///******************建立任务堆栈***************///
INT32U StackTask0[StackSizeTask0];
若在任务中使用 Printf 函数,栈要设得大些,否则栈越界溢出,会有意想不到的问题。
任务控制块
每个任务还要有记录自己栈顶指针的变量,保存在任务控制块(TCB)中。
什么是任务控制块?系统中的每个任务具有一个任务控制块,任务控制块记录任务执行的环境,这里的任务控制块比较简单,只包含了任务的堆栈指针和任务延时节拍数。
struct TaskCtrBlock /* 任务控制块数据结构 */
{
INT32U OSTCBStkPtr; /* 保存任务的堆栈顶 */
INT32U OSTCBDly; /* 任务延时时钟 */
};
struct TaskCtrBlock TCB[OS_TASKS + 1]; /* 定义任务控制块 */
任务控制块是任务的身份证。它把任务的程序与数据联系起来,找到它就可以得到任务的所有资源。
这里把 OSTCBStkPtr 放在结构体的最前面是有原因的,目的是使得在汇编中访问这个变量比较容易。因为结构体的地址就是它的首元素的地址,要在汇编中访问 OSTCBStkPtr这个变量,只需取得结构体的地址即可。
在 C 语言中不能直接实现如 TCB. TCB[OSPrioCur].OSTCBStkPtr = SP 的功能,只能用汇编语言完成。
定义如下指向结构体的指针变量:
struct TaskCtrBlock *p_OSTCBCur ; /* 指向当前任务控制块的指针 */
在 C 语言中,先让 p_OSTCBCur 指向当前运行任务的 TCB。
p_OSTCBCur = &TCB[OSPrioCur]; /* 目的是在汇编中引用任务的 TCB 地址取得栈顶指针 */
这样,运行下面的汇编代码即可把当前任务的堆顶指针取出到 SP 中。
SaveSPToCurTcb ; 保存当前任务的堆顶指针到它的
ldr r4,=p_OSTCBCur ; 取出当前任务的 PCB 地址
ldr r5,[r4]
str sp,[r5] ; 保存当前任务的堆顶指针到它的
; TCB(因为 TaskCtrBlock 地址亦即
; OSTCBStkPtr 的地址)
任务切换
最后来看看任务是如何“拥有”自己的 CPU 的。只有一个 CPU,各个任务共享,轮流使用。如何才能实现?我们先来看看中断的过程,当中断来临时,CPU 把当前程序的运行地址,寄存器等现场数据保存起来(一般保存在栈里),然后跳到中断服务程序执行。待执行完毕,再把先前保存的数据装回 CPU 又回到原来的程序执行。这样就实现了两个不同程序的交叉运行。
借鉴这种思想不就能实现多任务了吗?模仿中断的过程就可以实现任务切换运行。
任务切换时,把当前任务的现场数据保存在自己的任务栈里面,再把待运行的任务的数据从自己的任务栈装载到 CPU 中,改变 CPU 的 PC,SP,寄存器等。
可以说,任务的切换是任务运行环境的切换。而任务的运行环境保存在任务栈中,也就是说,任务切换的关键是把任务的私有堆栈指针赋予处理器的堆栈指针 SP。
程序调用下面函数进行任务切换:
void OSSched (void)
{
OS_ENTER_CRITICAL();
OSGetHighRdy(); /* 找出就绪表中优先级最高的任务 */
if(OSPrioHighRdy != OSPrioCur) /* 如果不是当前运行的任务,进行任务调度 */
{
p_OSTCBCur = &TCB[OSPrioCur]; /* 目的是在汇编中引用任务的 TCB 地址取得栈顶指针 */
p_OSTCBHighRdy = &TCB[OSPrioHighRdy];
OSPrioCur = OSPrioHighRdy; /* 更新 OSPrioCur */
OS_TASK_SW(); /* 调度任务 */
}
OS_EXIT_CRITICAL();
}
OS_TASK_SW ; 任务级的任务切换
;
stmfd sp!,{lr} ; PC 入栈
stmfd sp!,{r0-r12,lr} ; r0-r12,lr 入栈
PUAH_PSR
mrs r4,cpsr
stmfd sp!,{r4} ; cpsr 入栈
SaveSPToCurTcb ; 保存当前任务的堆顶指针到它的 TCB.
; TCB[OSPrioCur].OSTCBStkPtr = SP;
ldr r4,=p_OSTCBCur ; 取出当前任务的 PCB 地址
ldr r5,[r4]
str sp,[r5] ; 保存当前任务的堆顶指针到它的 TCB(因为
;TaskCtrBlock 地址亦即 OSTCBStkPtr 的地址)
GetHigTcbSP ; 取出更高优先级任务的堆顶指针到 SP ,
; SP = TCB[OSPrioCur].OSTCBStkPtr
ldr r6,=p_OSTCBHighRdy ; 取出更高优先级就绪任务的 PCB 的地址
ldr r6,[r6]
ldr sp,[r6] ; 取出更高优先级任务的堆顶指针到 SP
b POP_ALL ; 根据设定的栈结构顺序出栈
如何建立任务?
OSTaskCreate(Task0,&StackTask0[StackSizeTask0 - 1],PrioTask0); // 创建一个任务
任务未运行或被剥夺运行权后,它的运行环境以一定的结构保存在私有栈中。这个规定的顺序是十分重要的,以后任务切换时都要按这个顺序入栈出栈。建立任务时栈结构的初始化如下:
上面函数的作用就是创建一个任务。它接收三个参数,分别是任务的入口地址,任务堆栈的首地址和任务的优先级。
调用本函数后,系统会根据用户给出的参数初始化任务栈,并把栈顶指针保存到任务控制块中,在任务就绪表标记该任务为就绪状态。最后返回,这样一个任务就创建成功了。
可见,初始化后的任务堆栈空间由高到低将依次保存着 PC,LR,R12…R0,CPSR。当一个任务将要运行时,便通过取得它的堆栈指针(保存在任务控制块中)将这些寄存器出栈装入 CPU 相应的位置即可。
挂起/恢复任务
挂起
通过 OSTaskSuspend()可以主动挂起一个任务。OSTaskSuspend()会把任务从任务就绪表中移出,最后重新启动系统调度。这个函数可以挂起任务本身也可以挂起其他任务。
void OSTaskSuspend(INT8U prio)
{
OS_ENTER_CRITICAL();
TCB[prio].OSTCBDly = 0;
OSDelPrioRdy(prio); /* 从任务就绪表上去除标志位 */
OS_EXIT_CRITICAL();
if(OSPrioCur == prio) /* 当要挂起的任务为当前任务 */
{
OSSched(); /* 重新调度 */
}
}
恢复
可以让被 OSTaskSuspend 或 OSTimeDly 挂起的任务恢复就绪态,然后进行任务调度。
void OSTaskResume(INT8U prio)
{
OS_ENTER_CRITICAL();
OSSetPrioRdy(prio); /* 从任务就绪表上重置标志位 */
TCB[prio].OSTCBDly = 0; /* 将时间计时设为 0,延时到 */
OS_EXIT_CRITICAL();
if(OSPrioCur > prio) /* 当前任务的优先级低于重置位的任务的优先级 */
{
OSSched(); /* 重新调度 */
}
}