嵌入式系统学习——STM32之UCOS-III任务管理

时间:2021-01-20 20:23:08

什么是任务?

    生活中我们处理一个大问题的时候通常都是将这个问题“分而治之”,把大问题分成多个小问题,小问题被逐步的解决掉,大问题也就随之解决了。那么这些小问题就可以看成是很多个小任务。
    在我们设计复杂、大型程序的时候也是一样的,将这些负责的程序分割成许多个简单的小程序,这些小程序就是单个的任务,所有的小任务和谐的工作,最终完成复杂的功能。在操作系统中这些小任务可以并发执行,从而提高CPU的使用效率。

    UCOSIII就是一个可剥夺的多任务系统,我们使用UCOSIII的一个重要的原因就是它的多任务处理能力。  


UCOSIII中的任务

在UCOSIII中任务是以何种面貌存在的呢?

在UCOSIII中任务就是程序实体,UCOSIII能够管理和调度这些小任务(程序)。
UCOSIII中的任务由三部分组成:任务堆栈、任务控制块和任务函数。
任务堆栈: 上下文切换的时候用来保存任务的工作环境,就是STM32的内部寄存器值。
任务控制块:任务控制块用来记录任务的各个属性。

任务函数: 由用户编写的任务处理代码,是实实在在干活的,一般写法如下:

UCOSIII中的任务函数模板:

 void XXX_task(void *p_arg)
{
while(1)
{
。。。。。//任务处理过程
}
}
   可以看出用任务函数通常是一个无限循环,当然了,也可以是一个只执行一次的任务。任务的参数是一个void类型的,这么做的目的是可以可以传递不同类型的数据甚至是函数。
   可以看出任务函数其实就是一个C语言的函数,但是在使用UCOIII的情况下这个函数不能有用户自行调用,任务函数何时执行执行,何时停止完全有操作系统来控制。


UCOSIII系统任务:

UCOSIII默认有5个系统任务:
1、空闲任务:UCOSIII创建的第一个任务,UCOSIII必须创建的任务,此任务有UCOSIII自动创建,不需要用户手动创建。
2、时钟节拍任务:此任务也是必须创建的任务。
3、统计任务:可选任务,用来统计CPU使用率和各个任务的堆栈使用量。此任务是可选任务,由宏OS_CFG_STAT_TASK_EN控制是否使用此任务。
4、定时任务:用来向用户提供定时服务,也是可选任务,由宏OS_CFG_TMR_EN控制是否使用此任务。
5、中断服务管理任务:可选任务,由宏OS_CFG_ISR_POST_DEFERRED_EN控制是否使用此任务。


从用户的角度看,UCOSIII的任务一共有5种状态:

1、休眠态:任务已经在CPU的flash中了,但是还不受UCOSIII管理。
2、就绪态:系统为任务分配了任务控制块,并且任务已经在就绪表中登记,这时这个任务就具有了运行的条件,此时任务的状态就是就绪态。
3、运行态:任务获得CPU的使用权,正在运行。
4、等待态:正在运行的任务需要等待一段时间,或者等待某个事件,这个任务就进入了等待态,此时系统就会把CPU使用权转交给别的任务。
5、中断服务态:当发送中断,当前正在运行的任务会被挂起,CPU转而去执行中断服务函数,此时任务的任务状态叫做中断服务态。


任务堆栈的创建:

       任务堆栈是任务的重要部分,堆栈是在RAM中按照“先进先出(FIFO)”的原则组织的一块连续的存储空间。为了满足任务切换和响应中断时保存CPU寄存器中的内容及任务调用其它函数时的需要,每个任务都应该有自己的堆栈。
    任务堆栈创建很简单:
#define START_STK_SIZE 512//堆栈大小
CPU_STK START_TASK_STK[START_STK_SIZE];//定义一个数组来作为任务堆栈
任务堆栈的大小是多少呢?
    CPU_STK为CPU_INT32U类型,也就是unsigned int类型,为4字节的,那么任务堆栈START_TASK_STK的大小就为:512 X 4=2048字节!


任务堆栈初始化:

    任务如何才能切换回上一个任务并且还能接着从上次被中断的地方开始运行?  恢复现场即可,现场就是CPU的内部各个寄存器。因此在创建一个新任务时,必须把系统启动这个任务时所需的CPU各个寄存器初始值事先存放在任务堆栈中。这样当任务获得CPU使用权时,就把任务堆栈的内容复制到CPU的各个寄存器,从而可以任务顺利地启动并运行。

任务堆栈初始化:
       把任务初始数据存放到任务堆栈的工作就叫做任务堆栈的初始化,UCOSIII提供了完成堆栈初始化的函数:OSTaskStkInit()。
CPU_STK  *OSTaskStkInit (OS_TASK_PTR    p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK *p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
       用户一般不会直接操作堆栈初始化函数,任务堆栈初始化函数由任务创建函数OSTaskCreate()调用。不同的CPU对于的寄存器和对堆栈的操作方式不同,因此在移植UCOSIII的时候需要用户根据各自所选的CPU来编写任务堆栈初始化函数。

怎么使用创建的任务堆栈?

前面我们创建了一个任务堆栈,怎么使用这个任务堆栈?
作为任务创建函数OSTaskCreate()的参数,函数OSTaskCreate()如下:
void  OSTaskCreate (OS_TCB        *p_tcb,
CPU_CHAR *p_name,
OS_TASK_PTR p_task,
void *p_arg,
OS_PRIO prio,
CPU_STK *p_stk_base, //任务堆栈基地址
CPU_STK_SIZE stk_limit,//任务堆栈栈深
CPU_STK_SIZE stk_size,//任务堆栈大小
OS_MSG_QTY q_size,
OS_TICK time_quanta,
void *p_ext,
OS_OPT opt,
OS_ERR *p_err)


堆栈增长方式:

函数OSTaskCreate()中的参数p_stk_base如何确定?
根据堆栈的增长方式,堆栈有两种增长方式:
向上增长:堆栈的增长方向从低地址向高地址增长。
向下增长:堆栈的增长方向从高地址向低地址增长。
函数OSTaskCreate()中的参数p_stk_base是任务堆栈基地址,那么 如果CPU的堆栈是向上增长的话,那么基地址就&START_TASK_STK[0], 如果CPU的堆栈是向下增长的话,基地址就是&START_TASK_STK[START_STK_SIZE-1]  STM32的堆栈是向下增长的!

任务控制块结构:

   任务控制块是用来记录与任务相关的信息的数据结构,每个任务都要有自己的任务控制块。任务控制块由用户自行创建,如下代码为创建一个任务控制块:
 OS_TCB StartTaskTCB;  //创建一个任务控制块
    OS_TCB为一个结构体,描述了任务控制块,任务控制块中的成员变量用户不能直接访问,更不可能改变他们。
 OS_TCB为一个结构体,其中有些成员采用了条件编译的方式来确定
struct os_tcb 
{
CPU_STK *StkPtr;
void *ExtPtr;
CPU_STK *StkLimitPtr
OS_TCB *NextPtr;
OS_TCB *Prev
……//此处省略N个成员变量
#if OS_CFG_DBG_EN > 0u
OS_TCB *DbgPrevPtr;
OS_TCB *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
}


任务控制块初始化:

   函数OSTaskCreate()在创建任务的时候会对任务的任务控制块进行初始化。
   函数OS_TaskInitTCB()用与初始化任务控制块。用户不需要自行初始化任务控制块。

优先级:
    UCOSIII中任务优先级数由宏OS_CFG_PRIO_MAX来配置,UCOSIII中数值越小,优先级越高,最低可用优先级就是OS_CFG_PRIO_MAX-1。

就绪表:
    UCOSIII中就绪表由2部分组成:
    1、优先级位映射表OSPrioTbl[]:用来记录哪个优先级下有任务就绪。
    2、就绪任务列表OSRdyList[]:用来记录每一个优先级下所有就绪的任务。


就绪任务列表:
    通过上一步我们已经知道了哪个优先级的任务已经就绪了,但是UCOSIII支持时间片轮转调度,同一个优先级下可以有多个任务,因此我们还需要在确定是优先级下的哪个任务就绪了
struct  os_rdy_list {
OS_TCB *HeadPtr //用于创建链表,指向链表头
OS_TCB *TailPtr; //用于创建链表,指向链表尾
OS_OBJ_QTY NbrEntries; //此优先级下的任务数量
};
  同一优先级下如果有多个任务的话最先运行的永远是HeadPtr所指向的任务!


可剥夺型任务调度:

       任务调度就是中止当前正在运行的任务转而去执行其他的任务。
       UCOSIII是可剥夺型内核,因此当一个高优先级的任务准备就绪,并且此时发生了任务调度,那么这个高优先级的任务就会获得CPU的使用权!
        UCOSIII中的任务调度是由任务调度器来完成的,任务调度器有2种:任务级调度器和中断级调度器。
    任务级调度器为函数OSSched()。
    中断级调度器为函数OSIntExit(),当退出外部中断服务函数的时候使用中断级任务调度。

任务调度点:

  1、释放信号量或者发送消息,也可通过配置相应的参数不发生任务调度。
  2、使用延时函数OSTimeDly()或者OSTimeDlyHMSM()。
  3、任务等待的事情还没发生(等待信号量,消息队列等)。
  4、任务取消等待。
  5、创建任务。
  6、删除任务。
  7、删除一个内核对象。
  8、任务改变自身的优先级或者其他任务的优先级。
  9、任务通过调用OSTaskSuspend()将自身挂起。
  10、任务解挂某个挂起的任务。
  11、退出所有的嵌套中断。
  12、通过OSSchedUnlock()给调度器解锁。
  13、任务调用OSSchedRoundRobinYield()放弃其执行时间片。
  14、用户调用OSSched()。

调度器上锁和解锁:
       有时候我们并不希望发生任务调度,因为始终有一些代码的执行过程是不能被打断的。此时我们就可以使用函数OSSchedLock()对调度器加锁,当我们想要恢复任务调度的时候就可以使用函数OSSchedUnlock()给已经上锁的任务调度器解锁

时间片轮转调度:
    UCOSIII允许一个优先级下有多个任务,每个任务可以执行指定的时间(时间片),然后轮到下一个任务,这个过程就是时间片轮转调度,当一个任务不想在运行的时候就可以放弃其时间片。
    时间片轮转调度器为:OS_SchedRoundRobin()。


什么是任务切换?

       当UCOSIII需要切换到另外一个任务时,它将保存当前任务的现场到当前任务的堆栈中,主要是CPU寄存器值,然后恢复新的现场并且执行新的任务,这个过程就是任务切换。
任务切换分为两种:任务级切换和中断级切换。
任务级切换函数为:OSCtxSw()。
中断级切换函数为:OSIntCtxSw()。

任务控制块结构:
   任务控制块是用来记录与任务相关的信息的数据结构,每个任务都要有自己的任务控制块。任务控制块由用户自行创建,如下代码为创建一个任务控制块:
OS_TCB StartTaskTCB;  //创建一个任务控制块
    OS_TCB为一个结构体,描述了任务控制块,任务控制块中的成员变量用户不能直接访问,更不可能改变他们。
    OS_TCB为一个结构体,其中有些成员采用了条件编译的方式来确定
UCOSIII系统初始化:    在使用UCOSIII之前我们必须先初始化UCOSIII,函数OSInit()用来完成UCOSIII的初始化,而且OSInit()必须先于其他UCOSIII函数调用,包括OSStart()。
int main(void)
{
OS_ERR err;
……
//其他函数,一般为外设初始化函数
……
OSInit(&err);
……
//其他函数,一般为创建任务函数
……
OSStart(&err);
}


参考:开源电子网   STM32中文参考手册