前言
所谓操作系统,便是隔绝硬件层与应用层的平台,让工程师可以最大限度的忽视硬件,直接进行逻辑开发,它最大的特点,便是可以让多任务并发执行,但并非是同时执行,形象点来说,假如我有4个任务(LED点灯,喇叭鸣叫,串口通信,数据计算),让每个任务都执行几十个毫秒,虽然实际上在任何一个时间点,都有且只有一个任务的一条代码在执行,但是从宏观上看来,这4个任务几乎是同时执行的,这4个任务的调度,就是切换是由操作系统根据自身的策略来完成,程序员所关注的,只是任务中实际的处理部分,不需要在意框架,这样便可以大大减少开发的难度和工作量。
正文
UCOSII系统最简单的用法
对于一个刚接触ucosii的同学而言,用法其实比较简单,如果工程是完备的,那么建立一个能跑起来的工程的步骤如下:
- 定义任务名,任务优先级,任务堆栈及大小。
- 从main()中做操作系统的初始化(函数:OSInit()),创建起始任务,并且启动操作系统(函数:OSStart())。
- 在启动任务中,进行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任务调度的时机,也就是切换任务的时间点,我知道的大概有以下几处:
- 当前任务进入了延时。
- 当前任务被挂起或者杀死。
- 当前任务执行时,发生了某些中断。
现在分别讲解一下在以上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位又有什么关系?