手把手,嘴对嘴,讲解UCOSII嵌入式操作系统的任务调度策略(一)
刚参加工作那几年做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,那么二进制是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位又有什么关系?