手把手,嘴对嘴,讲解UCOSII嵌入式操作系统的运行原理(一)

时间:2022-03-20 20:04:48

 

刚参加工作那几年做MCU程序,由于实现的功能和需求都比较简单,外围功能也很少,所以直接就在裸机上写代码了。当时也没有任务和线程的概念,脑子里想的只有单个函数的调度,变量的控制等等。工作时先把流程图画出来,然后按照一定的逻辑把所有的函数都调用起来,最后实现自己的需求。

随着业务的深入,后来发现在某些比较复杂,或者说是外围功能比较多的项目上,如果依然用裸机的单线程来写,虽然最终也能实现,但是对于软件的架构上就会复杂许多,按照软件定律来说,软件的架构越复杂,bug的个数必然也会大大的增加,所以我慢慢开始接触嵌入式的操作系统。

首先接触的便是在国内很流行的UCOSII,刚开始对于操作系统也只是使用,不求甚解,只要求工程能够跑起来就行,等后来有时间以后深入了研究了一下,随着学习,很多以前困扰自己的问题也迎刃而解,现在把自己的经验分享出来,希望能帮到一些刚刚踏上这条不归路的同志,当然,由于本人能力有限,水平一般,如果文中出现了瑕疵和纰漏,还望不吝赐教。

--------------------------------------------------正文----------------------------------------------------------------------

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

UCOSII是一款适用于低性能MCU的嵌入式实时操作系统,低性能也就是平常所使用的单片机,本文便是基于常用的STM32F103来进行讲解的。

----------------------------------------------------------------------------------------------------------------------------

记得有一次找工作,面试官问我了一个问题:“你既然用过UCOSII实时操作系统,那么请说一下,这款操作系统是如何保证实时性的?”

当时我刚接触操作系统不久,只是知其然而不知其所以然,如果仅仅是移植一下,建立几个任务,让keil工程正常的跑起来还能做到,至于它原理性的东西那就有些懵逼了。

此后,基于这个问题,我抽出了不少时间去学习,现在回想起来,如果这个问题今天再问我,那我应该可以讲出个八九不离十。

--------------------那么UCOSII到底是如何保证它的实时性的呢?

基于这个问题的解答,我用老百姓都能听懂的语言,大胆的讲解一下嵌入式实时操作系统UCOSII的运行原理,希望语言通俗到只要学过C语言的同学就能理解的程度。

 

UCOSII系统最简单的用法

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

1.定义任务名,任务优先级,任务堆栈及大小。

2.从main()中做操作系统的初始化(函数:OSInit()),创建起始任务,并且启动操作系统(函数:OSStart())。

3.在启动任务中,进行MCU硬件的初始化,中断的配置,然后根据自己的需求,创建任意多个任务(64个以下,有些优先级是系统保留,比如统计和空闲,我们可以用的大概有50几个)。

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

代码如下:

 

  1 /* Includes ------------------------------------------------------------------*/
  2 #include "app.h"
  3 #include "includes.h"
  4 #include "delay.h"
  5 /////////////////////////UCOSII任务设置/////////////////////////////////////////
  6 //START 任务
  7 //设置任务优先级
  8 #define START_TASK_PRIO                 (8) //开始任务的优先级设置为最低
  9 //设置任务堆栈大小
 10 #define START_STK_SIZE                  (256)
 11 //任务堆栈
 12 OS_STK START_TASK_STK[START_STK_SIZE];
 13 //任务函数
 14 void start_task(void *pdata);
 15 
 16 //APP0任务
 17 //设置任务优先级
 18 #define APP0_TASK_PRIO                  (0)
 19 //设置任务堆栈大小
 20 #define APP0_STK_SIZE                   (256)
 21 //任务堆栈
 22 OS_STK APP0_TASK_STK[APP0_STK_SIZE];
 23 //任务函数
 24 void App0_task(void *pdata);
 25 
 26 
 27 //APP1任务
 28 //设置任务优先级
 29 #define APP1_TASK_PRIO                  (1)
 30 //设置任务堆栈大小
 31 #define APP1_STK_SIZE                   (256)
 32 //任务堆栈
 33 OS_STK APP1_TASK_STK[APP1_STK_SIZE];
 34 //任务函数
 35 void App1_task(void *pdata);
 36 
 37 
 38 //APP2任务
 39 //设置任务优先级
 40 #define APP2_TASK_PRIO                  (2)
 41 //设置任务堆栈大小
 42 #define APP2_STK_SIZE                   (256)
 43 //任务堆栈
 44 OS_STK APP2_TASK_STK[APP2_STK_SIZE];
 45 //任务函数
 46 void App2_task(void *pdata);
 47 
 48 
 49 //APP3任务
 50 //设置任务优先级
 51 #define APP3_TASK_PRIO                  (3)
 52 //设置任务堆栈大小
 53 #define APP3_STK_SIZE                   (64)
 54 //任务堆栈
 55 OS_STK APP3_TASK_STK[APP3_STK_SIZE];
 56 //任务函数
 57 void App3_task(void *pdata);
 58 
 59 
 60 /*******************************************************************************
 61 *  函 数 名: main
 62 *  功    能: 系统初始化 + 启动起始线程
 63 *  输    入: 无
 64 *  输    出: 无
 65 *  返 回 值: 无
 66 *  备    注: 无
 67 *******************************************************************************/
 68 int main(void)
 69 {
 70 
 71     OSInit();//操作系统初始化
 72     /* 创建起始任务 */
 73     OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO );
 74     OSStart();//操作系统启动 开始任务调度
 75 }
 76 
 77 /*******************************************************************************
 78 *  函 数 名: start_task
 79 *  功    能: 起始线程
 80 *  输    入: 无
 81 *  输    出: 无
 82 *  返 回 值: 无
 83 *  备    注: 创建任务线程
 84 *******************************************************************************/
 85 void start_task(void *pdata)
 86 {
 87     OS_CPU_SR cpu_sr=0;
 88 
 89     pdata = pdata;
 90 
 91     /* 设置中断优先级分组为组2:2位抢占优先级,2位响应优先级 */
 92     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
 93     /* 延时函数初始化 */
 94     delay_init();
 95     /* 启动统计任务,便于统计CPU的利用率以及负荷 */
 96     OSStatInit();
 97     /* 系统初始化 其中分为硬件初始化和变量初始化 */
 98     System_Init();
 99     /* 进入临界区(无法被中断打断) */
100     OS_ENTER_CRITICAL();
101     /* 创建线程 */
102 
103     OSTaskCreate(App0_task,(void *)0,(OS_STK*)&APP0_TASK_STK[APP0_STK_SIZE-1],APP0_TASK_PRIO);
104     OSTaskCreate(App1_task,(void *)0,(OS_STK*)&APP1_TASK_STK[APP1_STK_SIZE-1],APP1_TASK_PRIO);
105     OSTaskCreate(App2_task,(void *)0,(OS_STK*)&APP2_TASK_STK[APP2_STK_SIZE-2],APP2_TASK_PRIO);
106     OSTaskCreate(App3_task,(void *)0,(OS_STK*)&APP3_TASK_STK[APP3_STK_SIZE-2],APP3_TASK_PRIO);
107     /* 删除起始任务 */
108     OSTaskDel(OS_PRIO_SELF);
109     /* 退出临界区(可以被中断打断) */
110     OS_EXIT_CRITICAL();
111 }

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

 1 /*******************************************************************************
 2 * 函数名  : App0_task0
 3 * 描述    : 任务
 4 * 输入    : 无
 5 * 返回    : 无
 6 * 说明    : 无
 7 *******************************************************************************/
 8 void App0_task(void *pdata)
 9 {
10 
11     while(1)
12     {
13 #if SYSTEM_IWDG_ENABLE==1
14         /* 清除看门狗 */
15         IWDG_ReloadCounter();
16 #endif
17 
18         delay_ms(100);
19     };
20 }
21 
22 /*******************************************************************************
23 * 函数名  : App1_task
24 * 描述    : 任务
25 * 输入    : 无
26 * 返回    : 无
27 * 说明    : 无
28 *******************************************************************************/
29 void App1_task(void *pdata)
30 {
31     while(1)
32     {
33 #if SYSTEM_IWDG_ENABLE==1
34         /* 清除看门狗 */
35         IWDG_ReloadCounter();
36 #endif
38         delay_ms(100);
39     };
40 }
41 
42 
43 /*******************************************************************************
44 * 函数名  : App1_task
45 * 描述    : 任务
46 * 输入    : 无
47 * 返回    : 无
48 * 说明    : 无
49 *******************************************************************************/
50 void App2_task(void *pdata)
51 {54     while(1)
55     {
57 #if SYSTEM_IWDG_ENABLE==1
58         /* 清除看门狗 */
59         IWDG_ReloadCounter();
60 #endif
62         delay_ms(100);
63     };
64 }
65 
66 
67 /*******************************************************************************
68 * 函数名  : App3_task
69 * 描述    : 任务
70 * 输入    : 无
71 * 返回    : 无
72 * 说明    : 无
73 *******************************************************************************/
74 void App3_task(void *pdata)
75 {
76     while(1)
77     {
78 #if SYSTEM_IWDG_ENABLE==1
79         /* 清除看门狗 */
80         IWDG_ReloadCounter();
81 #endif
83         delay_ms(100);
84     };
85 }

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

现在回到刚才的问题,

  “你用过UCOSII实时操作系统,那么请说一下,这款操作系统是如何保证实时性的?”

   用老百姓都听懂的语言翻译一下就是:为啥程序会从APP0开始执行?为啥APP0的优先级就比APP3的优先级高?大家都是一张键盘打出来的代码,它就凭什么?

  我们所给任务定义的优先级,也就是那几个数字(0,1,2,3),到底是怎么影响任务调度顺序的?

------------------------------------------------------------------------------------------------------------------------------------------------------------

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

  1.当前任务进入了延时。

  2.当前任务被挂起或者杀死。

  3.当前任务执行时,发生了某些中断。

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

  1.当前任务进入了延时

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

      现在看一下这个函数体:

  

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

  这个函数是自己写的,其他的不重要,重点看第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();
    }

 

        y  =  OSTCBCur->OSTCBY;这一句话表示什么意思?当然是把一个变量的值赋给另一个变量……废话!

  那么,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,那么二进制是0x00001100,高3位就是0x001,不过现在我们的APP0的优先级是0,那么高3位也就是0x000。

在这行代码的旁边,同时还可以看到另一行代码:

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

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

这么做的目的是什么?保存这两个玩意儿有啥用?

容我以后慢慢讲,现在回到刚才的函数,终于明白了那个变量表示什么意思了,就是当前任务优先级的高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,那么二进制是0x00001100,那么ptcb->OSTCBX变量应该是0x100,那么ptcb->OSTCBBitX应该是1向左移动4个位置,结果是0x10。

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

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

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

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

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

那么……!@#*()&¥*()&¥……气的我想骂人,到底是什么意思?

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

待续……