手把手,嘴对嘴,讲解UCOSII嵌入式操作系统的任务调度策略(一)

前言

所谓操作系统,便是隔绝硬件层与应用层的平台,让工程师可以最大限度的忽视硬件,直接进行逻辑开发,它最大的特点,便是可以让多任务并发执行,但并非是同时执行,形象点来说,假如我有4个任务(LED点灯,喇叭鸣叫,串口通信,数据计算),让每个任务都执行几十个毫秒,虽然实际上在任何一个时间点,都有且只有一个任务的一条代码在执行,但是从宏观上看来,这4个任务几乎是同时执行的,这4个任务的调度,就是切换是由操作系统根据自身的策略来完成,程序员所关注的,只是任务中实际的处理部分,不需要在意框架,这样便可以大大减少开发的难度和工作量。

正文

UCOSII系统最简单的用法

对于一个刚接触ucosii的同学而言,用法其实比较简单,如果工程是完备的,那么建立一个能跑起来的工程的步骤如下:

  1. 定义任务名,任务优先级,任务堆栈及大小。
  2. 从main()中做操作系统的初始化(函数:OSInit()),创建起始任务,并且启动操作系统(函数:OSStart())。
  3. 在启动任务中,进行MCU硬件的初始化,中断的配置,然后根据自己的需求,创建任意多个任务(64个以下,有些优先级是系统保留,比如统计和空闲,我们可以用的大概有50几个)。

这个起始任务只执行一遍,因为它的作用仅仅是启动别的任务,执行完毕以后将它挂起。

代码如下:

/* Includes ------------------------------------------------------------------*/
#include "app.h"
#include "includes.h"
#include "delay.h"
/////////////////////////UCOSII任务设置/////////////////////////////////////////
//START 任务
//设置任务优先级
#define START_TASK_PRIO                 (8) //开始任务的优先级设置为最低
//设置任务堆栈大小
#define START_STK_SIZE                  (256)
//任务堆栈
OS_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *pdata);

//APP0任务
//设置任务优先级
#define APP0_TASK_PRIO                  (0)
//设置任务堆栈大小
#define APP0_STK_SIZE                   (256)
//任务堆栈
OS_STK APP0_TASK_STK[APP0_STK_SIZE];
//任务函数
void App0_task(void *pdata);


//APP1任务
//设置任务优先级
#define APP1_TASK_PRIO                  (1)
//设置任务堆栈大小
#define APP1_STK_SIZE                   (256)
//任务堆栈
OS_STK APP1_TASK_STK[APP1_STK_SIZE];
//任务函数
void App1_task(void *pdata);


//APP2任务
//设置任务优先级
#define APP2_TASK_PRIO                  (2)
//设置任务堆栈大小
#define APP2_STK_SIZE                   (256)
//任务堆栈
OS_STK APP2_TASK_STK[APP2_STK_SIZE];
//任务函数
void App2_task(void *pdata);


//APP3任务
//设置任务优先级
#define APP3_TASK_PRIO                  (3)
//设置任务堆栈大小
#define APP3_STK_SIZE                   (64)
//任务堆栈
OS_STK APP3_TASK_STK[APP3_STK_SIZE];
//任务函数
void App3_task(void *pdata);


/*******************************************************************************
*  函 数 名: main
*  功    能: 系统初始化 + 启动起始线程
*  输    入: 无
*  输    出: 无
*  返 回 值: 无
*  备    注: 无
*******************************************************************************/
int main(void)
{

    OSInit();//操作系统初始化
    /* 创建起始任务 */
    OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO );
    OSStart();//操作系统启动 开始任务调度
}

/*******************************************************************************
*  函 数 名: start_task
*  功    能: 起始线程
*  输    入: 无
*  输    出: 无
*  返 回 值: 无
*  备    注: 创建任务线程
*******************************************************************************/
void start_task(void *pdata)
{
    OS_CPU_SR cpu_sr=0;

    pdata = pdata;

    /* 设置中断优先级分组为组2:2位抢占优先级,2位响应优先级 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    /* 延时函数初始化 */
    delay_init();
    /* 启动统计任务,便于统计CPU的利用率以及负荷 */
    OSStatInit();
    /* 系统初始化 其中分为硬件初始化和变量初始化 */
    System_Init();
    /* 进入临界区(无法被中断打断) */
    OS_ENTER_CRITICAL();
    /* 创建线程 */

    OSTaskCreate(App0_task,(void *)0,(OS_STK*)&APP0_TASK_STK[APP0_STK_SIZE-1],APP0_TASK_PRIO);
    OSTaskCreate(App1_task,(void *)0,(OS_STK*)&APP1_TASK_STK[APP1_STK_SIZE-1],APP1_TASK_PRIO);
    OSTaskCreate(App2_task,(void *)0,(OS_STK*)&APP2_TASK_STK[APP2_STK_SIZE-2],APP2_TASK_PRIO);
    OSTaskCreate(App3_task,(void *)0,(OS_STK*)&APP3_TASK_STK[APP3_STK_SIZE-2],APP3_TASK_PRIO);
    /* 删除起始任务 */
    OSTaskDel(OS_PRIO_SELF);
    /* 退出临界区(可以被中断打断) */
    OS_EXIT_CRITICAL();
}

当以上的初始化部分执行完后,代码就能自己的跳进自己写的任务中,然后开始根据优先级实现调度。

/*******************************************************************************
* 函数名  : App0_task0
* 描述    : 任务
* 输入    : 无
* 返回    : 无
* 说明    : 无
*******************************************************************************/
void App0_task(void *pdata)
{

    while(1)
    {
#if SYSTEM_IWDG_ENABLE==1
        /* 清除看门狗 */
        IWDG_ReloadCounter();
#endif

        delay_ms(100);
    };
}

/*******************************************************************************
* 函数名  : App1_task
* 描述    : 任务
* 输入    : 无
* 返回    : 无
* 说明    : 无
*******************************************************************************/
void App1_task(void *pdata)
{
    while(1)
    {
#if SYSTEM_IWDG_ENABLE==1
        /* 清除看门狗 */
        IWDG_ReloadCounter();
#endif
        delay_ms(100);
    };
}


/*******************************************************************************
* 函数名  : App1_task
* 描述    : 任务
* 输入    : 无
* 返回    : 无
* 说明    : 无
*******************************************************************************/
void App2_task(void *pdata)
{54     while(1)
    {
#if SYSTEM_IWDG_ENABLE==1
        /* 清除看门狗 */
        IWDG_ReloadCounter();
#endif
        delay_ms(100);
    };
}


/*******************************************************************************
* 函数名  : App3_task
* 描述    : 任务
* 输入    : 无
* 返回    : 无
* 说明    : 无
*******************************************************************************/
void App3_task(void *pdata)
{
    while(1)
    {
#if SYSTEM_IWDG_ENABLE==1
        /* 清除看门狗 */
        IWDG_ReloadCounter();
#endif
        delay_ms(100);
    };
}

我新建了4个任务,他们会按照优先级(0,1,2,3)从APP0→APP3的顺序开始调用,现在它们都是空的,如果需要加入功能,只需要在while(1)里面加入自己的代码便可。

UCOSII任务调度的时机,也就是切换任务的时间点,我知道的大概有以下几处:

  1. 当前任务进入了延时。
  2. 当前任务被挂起或者杀死。
  3. 当前任务执行时,发生了某些中断。

现在分别讲解一下在以上3种情况下,任务调度的来龙去脉。

1、当前任务进入了延时

我们从代码运行的流程梳理一下,忽略操作系统本身,代码从APP0开始执行,当执行完它需要执行的任务后,会进入一个延时函数delay_ms()。

现在看一下这个函数体:

//延时nms
//nms:要延时的ms数
void delay_ms(u16 nms)
{
    if(delay_osrunning && delay_osintnesting==0)//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)
    {
        if(nms>=fac_ms)                         //延时的时间大于OS的最少时间周期
        {
            OSTimeDly(nms/fac_ms);              //OS延时
        }
        nms%=fac_ms;                            //OS已经无法提供这么小的延时了,采用普通方式延时
    }
    delay_us((u32)(nms*1000));                  //普通方式延时
}

这个函数是自己写的,其他的不重要,重点看第9行的OSTimeDly()函数,这个函数可是系统自带的,从现在开始进入系统。

void  OSTimeDly (INT32U ticks)
{
    INT8U      y;
#if OS_CRITICAL_METHOD == 3u
    OS_CPU_SR  cpu_sr = 0u;
#endif

    if (OSIntNesting > 0u) {                     /* 查看延时函数是否在中断中调用,如果在中断中调用,不能切换任务 */
        return;
    }
    if (OSLockNesting > 0u) {                    /* 查看当前任务调度是否被系统锁住,当系统被锁住,不能切换任务 */
        return;
    }
    if (ticks > 0u) {                            /* 延时参数是否为0 */
        OS_ENTER_CRITICAL();            /* 禁止中断 */
        y            =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;
        OS_EXIT_CRITICAL();              /* 开启中断 */
        OS_Sched();
    }
}

当代码进入这个函数以后,首先进行两个判定,1.是否在中断中,2.任务调度是否属于允许状态,如果两个都不满足,才执行下面的代码。OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()这两个宏分别是禁止中断和重启中断,一般是成对出现,用来保证一些重要的代码在执行期间,不会被打断。

前面的不重要,重点是if (ticks > 0u)里面的东西,他里面到底实现了些什么?

if (ticks > 0u) {                            /* 延时参数是否为0 */
        OS_ENTER_CRITICAL();            /* 禁止中断 */
        y =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;
        OS_EXIT_CRITICAL();              /* 开启中断 */
        OS_Sched();
    }

问:OSTCBCur->OSTCBY这个变量到底是代表着什么?

答:这个变量明显是属于一个指向结构体的指针,我们可以跟踪它去看看它的定义。

OS_EXT  OS_TCB           *OSTCBCur;           /* Pointer to currently running TCB */

从注释便可知道,这个结构体指针,指向当前正在运行的任务,继续跟踪……

typedef struct os_tcb {
    OS_STK          *OSTCBStkPtr;           /* Pointer to current top of stack                         */

    struct os_tcb   *OSTCBNext;             /* Pointer to next     TCB in the TCB list                 */
    struct os_tcb   *OSTCBPrev;             /* Pointer to previous TCB in the TCB list                 */

    INT32U           OSTCBDly;              /* Nbr ticks to delay task or, timeout waiting for event   */
    INT8U            OSTCBStat;             /* Task      status                                        */
    INT8U            OSTCBStatPend;         /* Task PEND status                                        */
    INT8U            OSTCBPrio;             /* Task priority (0 == highest)                            */

    INT8U            OSTCBX;                /* Bit position in group  corresponding to task priority   */
    INT8U            OSTCBY;                /* Index into ready table corresponding to task priority   */
    OS_PRIO          OSTCBBitX;             /* Bit mask to access bit position in ready table          */
    OS_PRIO          OSTCBBitY;             /* Bit mask to access bit position in ready group          */

} OS_TCB;

这个结构体有很多成员,是用来记录任务的基本信息的,这样的结构体有很多,可以简单的理解为有多少个任务就有多少个这样的结构体,它的原本的定义很大,我剔除了一些干扰信息,结果如上,这明显是一个双向链表,我们需要的OSTCBCur->OSTCBY到底是什么意思呢?从注释上看,应该是与任务优先级对应的就绪索引表有关,听不懂没关系,记在脑子里,下面解释。

继续跟踪变量,发现它是在当前任务创建的时候赋值的。也就是起始任务中的start_task()→OSTaskCreate()→OS_TCBInit()的里面。

ptcb->OSTCBY             = (INT8U)(prio >> 3u);

从字面意思上看,它的值应该是优先级的高3位,如果我们的优先级是12,那么二进制是00001100,高3位就是001,不过现在我们的APP0的优先级是0,那么高3位也就是000。在这行代码的旁边,同时还可以看到另一行代码:

ptcb->OSTCBX             = (INT8U)(prio & 0x07u);

这个变量里面保存的优先级的低三位,如果我们的优先级是12,那么二进制是00001100,低3位也就是0x100,不过现在我们的APP0的优先级是0,那么低3位也就是000。

把优先级的高3位和低3位分开,这么做的目的是什么?保存这两个玩意儿有啥用?

详细的东西容我以后慢慢讲,现在回到刚才的函数,终于明白了那个变量表示什么意思了,就是当前任务优先级的高3位,意义为何,虽然现在还不清楚,默默的记住有这么一个东西就行。

if (ticks > 0u) {                            /* 延时参数是否为0 */
        OS_ENTER_CRITICAL();            /* 禁止中断 */
        y            =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;
        OS_EXIT_CRITICAL();              /* 开启中断 */
        OS_Sched();
    }

现在看第二句代码:OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX根据上面那个结构体,我们可以找到这个成员的定义以及赋值。它也是在那个OS_TCBInit函数里赋值的。

ptcb->OSTCBBitY          = (OS_PRIO)(1uL << ptcb->OSTCBY);
ptcb->OSTCBBitX          = (OS_PRIO)(1uL << ptcb->OSTCBX);

从算法上看,ptcb->OSTCBBitX是1向左移动本任务优先级的低3位。

举个例子:假如我们当前的优先级是12,那么二进制是00001100,那么ptcb->OSTCBX变量应该是二进制100,那么ptcb->OSTCBBitX应该是1向左移动4个位置,结果是0x10。

同理,ptcb->OSTCBBitY便是1向左移动优先级的高3位。

举个例子:加入我们当前的优先级是12,那么二进制是00001100,那么ptcb->OSTCBtY变量应该是001,那么ptcb->OSTCBBittY应该是1向左移动1个位置,结果是0x02。

ptcb->OSTCBY,ptcb->OSTCBX,ptcb->OSTCBBitY,ptcb->OSTCBBitX……这四个变量和优先级有关的变量都是在任务创建的时候就赋值好了,以后也不会改变,至于它们的用法,便是需要重点讲解的地方。

现在结合那两句代码一起看:

y            =  OSTCBCur->OSTCBY;
OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;

还是举个例子:假如我当前任务的优先级是12,那么Y = 001,OSTCBCur->OSTCBBitX = 0x10,结合起来看,第二句有&符号,也有~符号,只要是稍微有点C语言基础的同学,那么都很容易看出这两句话的意思,把数组OSRdyTbl[1]的第4位清空……

那么,清空又是什么意思?

优先级12的任务和数组OSRdyTbl[1]有什么关系,和OSRdyTbl[1]的第4位又有什么关系?

待续......

猜你喜欢

转载自blog.csdn.net/SammySum/article/details/103508672