从0到1教你写UCOS-III 第十一部分:支持多优先级

版权声明: https://blog.csdn.net/qq_38351824/article/details/89791851

       在本章之前, OS 还没有到优先级,只支持两个任务互相切换,从本章开始, 任务中我们开始加入优先级的功能。在 uC/OS-III 中,数字优先级越小,逻辑优先级越高。


11.1 定义优先级相关全局变量:

       在支持任务多优先级的时候,需要在 os.h 头文件添加两个优先级相关的全局变量,具体定义见代码清单 11-1。
代码清单 11-1 定义优先级相关全局变量

/* 在 os.h 中定义 */
/* 当前优先级 */
OS_EXT OS_PRIO OSPrioCur;
/* 最高优先级 */
OS_EXT OS_PRIO OSPrioHighRdy;

11.2 修改 OSInit()函数:

        刚刚新添加的优先级相关的全部变量,需要在 OSInit()函数中进行初始化,具体见代码清单 11-2 ,其实 OS 中定义的所有的全局变量都是在 OSInit()中初始化的。
代码清单 11-2 OSInit()函数
 

void OSInit (OS_ERR *p_err)
{
/* 配置 OS 初始状态为停止态 */
OSRunning = OS_STATE_OS_STOPPED;
/* 初始化两个全局 TCB,这两个 TCB 用于任务切换 */
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
/* 初始化优先级变量 */
OSPrioCur = (OS_PRIO)0;
OSPrioHighRdy = (OS_PRIO)0;
/* 初始化优先级表 */
OS_PrioInit();
/* 初始化就绪列表 */
OS_RdyListInit();
/* 初始化空闲任务 */
OS_IdleTaskInit(p_err);
if (*p_err != OS_ERR_NONE) {
return;
}
}

11.3 修改任务控制块 TCB:

       在任务控制块中,加入优先级字段 Prio,具体见代码清单 11-3 。优先级Prio 的数据类型为 OS_PRIO,宏展开后是 8 位的整型,所以只支持 255 个优先级。
代码清单 11-3 在 TCB 中加入优先级
 

struct os_tcb {
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
/* 任务延时周期个数 */
OS_TICK TaskDelayTicks;
/* 任务优先级 */
OS_PRIO Prio;
/* 就绪列表双向链表的下一个指针 */
OS_TCB *NextPtr;
/* 就绪列表双向链表的前一个指针 */
OS_TCB *PrevPtr;
};

11.4 修改 OSTaskCreate()函数:

       修改 OSTaskCreate()函数,在里面加入优先级相关的处理,具体见代码清单 11-4 。
代码清单 11-4 OSTaskCreate()函数 加入优先级处理
 

void OSTaskCreate (OS_TCB *p_tcb,
OS_TASK_PTR p_task,
void *p_arg,
OS_PRIO prio,                       (1)
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_size,
OS_ERR *p_err)
{
CPU_STK *p_sp;
CPU_SR_ALLOC();                     (2)
/* 初始化 TCB 为默认值 */
OS_TaskInitTCB(p_tcb);              (3)
/* 初始化堆栈 */
p_sp = OSTaskStkInit( p_task,
p_arg,
p_stk_base,
stk_size );
p_tcb->Prio = prio;                (4)
p_tcb->StkPtr = p_sp;
p_tcb->StkSize = stk_size;
/* 进入临界段 */ 
OS_CRITICAL_ENTER();               (5)
/* 将任务添加到就绪列表 */           (6)
OS_PrioInsert(p_tcb->Prio);
OS_RdyListInsertTail(p_tcb);
/* 退出临界段 */
OS_CRITICAL_EXIT();                (7)
*p_err = OS_ERR_NONE;
}

       代码清单 11-4(1): 在函数形参中,加入优先级字段。任务的优先级由用户在创建任务的时候通过形参 Prio 传进来。
       代码清单 11-4(2): 定义一个局部变量,用来存 CPU 关中断前的中断状态,因为接下来将任务添加到就绪列表这段代码属于临界短代码,需要关中断。
       代码清单 11-4(3): 初始 化 TCB 为默认值,其实就是全部初始化为 0,OS_TaskInitTCB()函数在 os_task.c 的开头定义,具体见代码清单 11-5。
代码清单 11-5 OS_TaskInitTCB()函数

void OS_TaskInitTCB (OS_TCB *p_tcb)
{
p_tcb->StkPtr = (CPU_STK *)0;
p_tcb->StkSize = (CPU_STK_SIZE )0u;
p_tcb->TaskDelayTicks = (OS_TICK )0u;
p_tcb->Prio = (OS_PRIO )OS_PRIO_INIT; (1)
p_tcb->NextPtr = (OS_TCB *)0;
p_tcb->PrevPtr = (OS_TCB *)0;
}

       代码清单 11-5 (1): OS_PRIO_INIT 是任务 TCB 初始化的时候给的默认的一个优先级 , 宏 展 开 等 于 OS_CFG_PRIO_MAX , 这 是 一 个 不 会 被 OS 使 用 到 的 优 先 级 。OS_PRIO_INIT 具体在 os.h 中定义。
       代码清单 11-4(4): 将形参传进来的优先级存到任务控制块 TCB 的优先级字段。
       代码清单 11-4(5): 进入临界段。
       代码清单 11-4(6): 将任务插入到就绪列表,这里需要分成两步来实现: 1、根据优先级置位优先级表中的相应位置; 2、将任务 TCB 放到 OSRdyList[优先级]中,如果同一个优先级有多个任务,那么这些任务的 TCB 就会被放到 OSRdyList[优先级]串成一个双向链表。
       代码清单 11-4(7): 退出临界段。

11.5 修改 OS_IdleTaskInit()函数:

       修改 OS_IdleTaskInit()函数,是因为该函数调用了任务创建函数 OSTaskCreate(),OSTaskCreate()我们刚刚加入了优先级,所以这里我要跟空闲任务分配一个优先级,具体见。 代码清单 11-6。
代码清单 11-6 OS_IdleTaskInit()函数
 

/* 空闲任务初始化 */
void OS_IdleTaskInit(OS_ERR *p_err)
{
/* 初始化空闲任务计数器 */
OSIdleTaskCtr = (OS_IDLE_CTR)0;
/* 创建空闲任务 */
OSTaskCreate( (OS_TCB *)&OSIdleTaskTCB,
(OS_TASK_PTR )OS_IdleTask,
(void *)0,
(OS_PRIO)(OS_CFG_PRIO_MAX - 1u),              (1)
(CPU_STK *)OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
(OS_ERR *)p_err );
}

       代码清单 11-6(1):空闲任务是 uC/OS-III 的内部任务,在 OSInit()中被创建,在系统没有任何用户任务运行的情况下,空闲任务就会被运行,优先级最低,即等于OS_CFG_PRIO_MAX - 1u。

11.6 修改 OSStart()函数:

       加入优先级之后, OSStart()函数需要修改,具体哪一个任务最先运行,由优先级决定,新加入的代码具体见代码清单 11-7 。
代码清单 11-7 OSStart()函数

/* 启动 RTOS,将不再返回 */
void OSStart (OS_ERR *p_err)
{
if ( OSRunning == OS_STATE_OS_STOPPED ) {
#if 0
/* 手动配置任务 1 先运行 */
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
#endif
/* 寻找最高的优先级 */ 
OSPrioHighRdy = OS_PrioGetHighest();                    (1)
OSPrioCur = OSPrioHighRdy;
/* 找到最高优先级的 TCB */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;     (2)
OSTCBCurPtr = OSTCBHighRdyPtr;
/* 标记 OS 开始运行 */
OSRunning = OS_STATE_OS_RUNNING;
/* 启动任务切换,不会返回 */
OSStartHighRdy();
/* 不会运行到这里,运行到这里表示发生了致命的错误 */
*p_err = OS_ERR_FATAL_RETURN;
} else {
*p_err = OS_STATE_OS_RUNNING;
}
}

       代码清单 11-7(1): 调取 OS_PrioGetHighest()函数从全局变量优先级表 OSPrioTbl[]获取最高的优先级,放到 OSPrioHighRdy 这个全局变量中,然后把 OSPrioHighRdy 的值再赋给当前优先级 OSPrioCur 这个全局变量。 在任务切换的时候需要用到 OSPrioHighRdy OSPrioCur 这两个全局变量。
       代码清单 11-7(2) :根据 OSPrioHighRdy 的值,作为全局变量 OSRdyList[]的下标索引 找 到 最 高 优 先 级 任 务 的 TCB , 传 给 全 局 变 量 OSTCBHighRdyPtr , 然 后 再 将OSTCBHighRdyPtr 赋 值 给 OSTCBCurPtr 。 在 任 务 切 换 的 时 候 需 要 使 用 到OSTCBHighRdyPtr OSTCBCurPtr 这两个全局变量。

11.7 修改 PendSV_Handler()函数:

       PendSV_Handler()函数中添加了优先级相关的代码,具体见代码清单 11-8 中加粗部分。有关 PendSV_Handler()这个函数的具体讲解要参考《任务的定义与任务切换的实现》这个章节,这里不再赘述。
代码清单 11-8 PendSV_Handler()函数

;*******************************************************************
; PendSVHandler 异常
;*******************************************************************
OS_CPU_PendSVHandler_nosave
; OSPrioCur = OSPrioHighRdy
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
; OSTCBCurPtr = OSTCBHighRdyPtr
LDR R0, = OSTCBCurPtr
LDR R1, = OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2]
LDMIA R0!, {R4-R11}
MSR PSP, R0
ORR LR, LR, #0x04
CPSIE I
BX LR
NOP
ENDP

11.8 修改 OSTimeDly()函数:

       任务调用 OSTimeDly()函数之后,任务就处于阻塞态,需要将任务从就绪列表中移除,具体修改的代码见代码清单 11-9 的加粗部分。
代码清单 11-9 OSTimeDly()函数
 

/* 阻塞延时 */
void OSTimeDly(OS_TICK dly)
{
#if 0
/* 设置延时时间 */
OSTCBCurPtr->TaskDelayTicks = dly;
/* 进行任务调度 */
OSSched();
#endif
CPU_SR_ALLOC();                         (1)
/* 进入临界区 */
OS_CRITICAL_ENTER();                    (2)
/* 设置延时时间 */
OSTCBCurPtr->TaskDelayTicks = dly;
/* 从就绪列表中移除 */
//OS_RdyListRemove(OSTCBCurPtr);
OS_PrioRemove(OSTCBCurPtr->Prio);       (3)
/* 退出临界区 */
OS_CRITICAL_EXIT();                     (4)
/* 任务调度 */
OSSched();
}

       代码清单 11-9(1): 定义一个局部变量,用来存 CPU 关中断前的中断状态,因为接下来将任务从就绪列表移除这段代码属于临界短代码,需要关中断。
       代码清单 11-9(2): 进入临界段
       代码清单 11-9(3): 将任务从就绪列表移除,这里只需将任务在优先级表中对应的位清除即可,暂时不需要把任务 TCB 从 OSRdyList[]中移除,因为接下来 OSTimeTick()函数还是通过扫描 OSRdyList[]来判断任务的延时时间是否到期。当我们加入了时基列表之后,当任务调用 OSTimeDly()函数进行延时,就可以把任务的 TCB 从就绪列表删除,然后把任务 TCB 插入时基列表, OSTimeTick()函数判断任务的延时是否到期只需通过扫描时基列表即可, 时基列表在下一个章节实现。所以这里暂时不能把 TCB 从就绪列表中删除,只是将任务优先级在优先级表中对应的位清除来达到任务不处于就绪态的目的。
       代码清单 11-9(4): 退出临界段。

11.9 修改 OSSched()函数:

       任务调度函数 OSSched()不再是之前的两个任务轮流切换,需要根据优先级来调度,具体修改部分见代码清单 11-10 ,被迭代的代码已经通过条件编译屏蔽。
代码清单 11-10 OSSched()函数

void OSSched(void)
{
#if 0
/* 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,
看看他们的延时时间是否结束,如果任务的延时时间均没有到期,
那就返回继续执行空闲任务 */
if ( OSTCBCurPtr == &OSIdleTaskTCB ) {
if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0) {
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
} else if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0) {
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
} else {
return; /* 任务延时均没有到期则返回,继续执行空闲任务 */
}
} else {
/*如果是 task1 或者 task2 的话,检查下另外一个任务,
如果另外的任务不在延时中,就切换到该任务,
否则,判断下当前任务是否应该进入延时状态,
如果是的话,就切换到空闲任务。否则就不进行任何切换 */
if (OSTCBCurPtr == OSRdyList[0].HeadPtr) {
if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0) {
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
} else if (OSTCBCurPtr->TaskDelayTicks != 0) {
OSTCBHighRdyPtr = &OSIdleTaskTCB;
} else {
/* 返回,不进行切换,因为两个任务都处于延时中 */
return;
}
} else if (OSTCBCurPtr == OSRdyList[1].HeadPtr) {
if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0) {
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
} else if (OSTCBCurPtr->TaskDelayTicks != 0) {
OSTCBHighRdyPtr = &OSIdleTaskTCB;
} else {
/* 返回,不进行切换,因为两个任务都处于延时中 */
return;
}
}
}
/* 任务切换 */
OS_TASK_SW();
#endif
CPU_SR_ALLOC();                                                      (1)
/* 进入临界区 */
OS_CRITICAL_ENTER();                                                 (2)
/* 查找最高优先级的任务 */                                             (3)
OSPrioHighRdy = OS_PrioGetHighest();
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
/* 如果最高优先级的任务是当前任务则直接返回,不进行任务切换 */            (4)
if (OSTCBHighRdyPtr == OSTCBCurPtr) {
/* 退出临界区 */
OS_CRITICAL_EXIT();
return;
}
/* 退出临界区 */
OS_CRITICAL_EXIT();                                                  (5)
/* 任务切换 */
OS_TASK_SW();                                                        (6)
}

       代码清单 11-10(1): 定义一个局部变量,用来存 CPU 关中断前的中断状态,因为接下来查找最高优先级这段代码属于临界短代码,需要关中断。
       代码清单 11-10(2): 进入临界段。
       代码清单 11-10(3): 查找最高优先级任务。

       代码清单 11-10(4): 判断最高优先级任务是不是当前任务,如果是则直接返回,否则将继续往下执行,最后执行任务切换。
       代码清单 11-10(5): 退出临界段。
       代码清单 11-10(6): 任务切换。

11.10 修改 OSTimeTick()函数:

       OSTimeTick()函数在 SysTick 中断服务函数中被调用, 是一个周期函数,具体用于扫描就绪列表 OSRdyList[],判断任务的延时时间是否到期,如果到期则将任务在优先级表中对应的位置位,修改部分的代码见代码清单 11-11 的加粗部分,被迭代的代码则通过条件编译屏蔽。
代码清单 11-11 OSTimeTick()函数

void OSTimeTick (void)
{
unsigned int i;
CPU_SR_ALLOC();                                      (1)
/* 进入临界区 */
OS_CRITICAL_ENTER();                                 (2)
/* 扫描就绪列表中所有任务的 TaskDelayTicks,如果不为 0,则减 1 */
#if 0
for (i=0; i<OS_CFG_PRIO_MAX; i++) {
if (OSRdyList[i].HeadPtr->TaskDelayTicks > 0) {
OSRdyList[i].HeadPtr->TaskDelayTicks --;
}
}
#endif
for (i=0; i<OS_CFG_PRIO_MAX; i++) {                  (3)
if (OSRdyList[i].HeadPtr->TaskDelayTicks > 0) {
OSRdyList[i].HeadPtr->TaskDelayTicks --;
if (OSRdyList[i].HeadPtr->TaskDelayTicks == 0) {
/* 为 0 则表示延时时间到,让任务就绪 */
//OS_RdyListInsert (OSRdyList[i].HeadPtr);
OS_PrioInsert(i);
}
}
}
/* 退出临界区 */
OS_CRITICAL_EXIT();                                  (4)
/* 任务调度 */
OSSched();
}

       代码清单 11-11(1):定义一个局部变量,用来存 CPU 关中断前的中断状态,因为接下来扫描就绪列表 OSRdyList[]这段代码属于临界短代码,需要关中断。
       代码清单 11-11(2): 进入临界段。
       代码清单 11-11(3): 扫描就绪列表 OSRdyList[],判断任务的延时时间是否到期,如果到期则将任务在优先级表中对应的位置位。
       代码清单 11-11(4): 退出临界段。

11.11 main 函数:

        main 函数具体见代码清单 11-12。
代码清单 11-12 main()函数
 

/*
*******************************************************************
* 全局变量
*******************************************************************
*/
uint32_t flag1;
uint32_t flag2;
uint32_t flag3;
/*
*******************************************************************
* TCB & STACK & 任务声明
*******************************************************************
*/
#define TASK1_STK_SIZE 128
#define TASK2_STK_SIZE 128
#define TASK3_STK_SIZE 128
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;
static OS_TCB Task3TCB;
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
static CPU_STK Task3Stk[TASK2_STK_SIZE];
void Task1( void *p_arg );
void Task2( void *p_arg );
void Task3( void *p_arg );
/*
*******************************************************************
* 函数声明
*******************************************************************
*/
void delay(uint32_t count);
/*
*******************************************************************
* main 函数
*******************************************************************
*/
/*
* 注意事项: 1、该工程使用软件仿真, debug 需选择 Ude Simulator
* 2、在 Target 选项卡里面把晶振 Xtal(Mhz)的值改为 25,默认是 12,
* 改成 25 是为了跟 system_ARMCM3.c 中定义的__SYSTEM_CLOCK 相同,
* 确保仿真的时候时钟一致
*/
int main(void)
{
OS_ERR err;
/* CPU 初始化: 1、初始化时间戳 */
CPU_Init();
/* 关闭中断 */
CPU_IntDis();
/* 配置 SysTick 10ms 中断一次 */
OS_CPU_SysTickInit (10);
/* 初始化相关的全局变量 */
OSInit(&err);                                       (1)
/* 创建任务 */
OSTaskCreate( (OS_TCB*)&Task1TCB,
(OS_TASK_PTR )Task1,
(void *)0,
(OS_PRIO)1,                                         (2)
(CPU_STK*)&Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_ERR *)&err );
OSTaskCreate( (OS_TCB*)&Task2TCB,
(OS_TASK_PTR )Task2,
(void *)0,
(OS_PRIO)2,                                         (3)
(CPU_STK*)&Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_ERR *)&err );
OSTaskCreate( (OS_TCB*)&Task3TCB,
(OS_TASK_PTR )Task3,
(void *)0,
(OS_PRIO)3,                                          (4)
(CPU_STK*)&Task3Stk[0],
(CPU_STK_SIZE) TASK3_STK_SIZE,
(OS_ERR *)&err );
#if 0
/* 将任务加入到就绪列表 */                              (5)
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;
#endif
/* 启动 OS,将不再返回 */
OSStart(&err);
}
/*
*******************************************************************
* 函数实现
*******************************************************************
*/
/* 软件延时 */
void delay (uint32_t count)
{
for (; count!=0; count--);
}
void Task1( void *p_arg )
{
for ( ;; ) {
flag1 = 1;
OSTimeDly(2);
flag1 = 0;
OSTimeDly(2);
}
}
void Task2( void *p_arg )
{
for ( ;; ) {
flag2 = 1;
OSTimeDly(2);
flag2 = 0;
OSTimeDly(2);
}
}
void Task3( void *p_arg )
{
for ( ;; ) {
flag3 = 1;
OSTimeDly(2);
flag3 = 0;
OSTimeDly(2);
}
}

       代码清单 11-12(1): 加入了优先级相关的全局变量 OSPrioCur 和 OSPrioHighRdy 的初始化
       代码清单 11-12(2)、(3)和(4): 为每个任务分配了优先级,任务 1 的优先级为1,任务 2 的优先级为 2,任务 3 的优先级为 3。
       代码清单 11-12(5): 将任务插入到就绪列表这部分功能由 OSTaskCreate()实现,这里通过条件编译屏蔽掉。

11.12 实验现象:

       进入软件调试,全速运行程序,从逻辑分析仪中可以看到三个任务的波形是完全同步,就好像 CPU 在同时干三件事情,具体仿真的波形图见图 11-1。 任务开始的启动过程具体见
图 11-2,这个启动过程要认真的理解下。

图 11-1 实验现象(宏观)

图 11-2 任务的启动过程(微观)

       图 11-2 是任务 1、 2 和 3 刚开始启动时的软件仿真波形图,系统从启动到任务 1 开始运行前花的时间为 TIME1,等于 0.26MS。 任务 1 开始运行,然后调用 OSTimeDly(1)进入延时, 随后进行任务切换,切换到任务 2 开始运行,从任务 1 切换到任务 2 花费的时间等于 TIME2-TIME1,等于 0.01MS。任务 2 开始运行,然后调用 OSTimeDly(1)进入延时,随后进行任务切换,切换到任务 3 开始运行,从任务 2 切换到任务 3 花费的时间等于 TIME3-TIME1,等于 0.01MS。任务 3 开始运行,然后调用 OSTimeDly(1)进入延时,随后进行任务切换,这个时候我们创建的 3 个任务都处于延时状态,那么系统就切换到空闲任务,在三个任务延时未到期之前,系统一直都是在运行空闲任务。当第一个 SysTick 中断产生,中断服务函数会调用 OSTimeTick() 函数扫描每个任务的延时是否到期,因为是延时 1 个SysTick 周期,所以第一个 SysTick 中断产生就意味着延时都到期,任务 1、 2 和 3 依次进入就绪态, 再次回到任务本身接着运行, 将自身的 Flag 清 0,然后任务 1、 2 和 3 又依次调用 OSTimeDly(1)进入延时状态,直到下一个 SysTick 中断产生前,系统都处在空闲任务中,一直这样循环下去。
       但是,有些同学肯定就会问图 11-1 中任务 1、 2 和 3 的波形图是同步的, 而图 11-2 中任务的波形就不同步,有先后顺序? 答案是图 11-2 是将两个任务切换花费的时间 0.01ms进行放大后观察的波形,就好像我们用放大镜看微小的东西一样,如果不用放大镜,在宏观层面观察就是图 11-1 的实验现象。

猜你喜欢

转载自blog.csdn.net/qq_38351824/article/details/89791851