之前讲到UCOSIII默认有5个系统任务:
- 空闲任务:UCOSIII创建的第一个任务,UCOSIII必须创建的任务,此任务有UCOSIII自动创建,不需要用户手动创建;
- 时钟节拍任务:此任务也是必须创建的任务;
- 统计任务:可选任务,用来统计CPU使用率和各个任务的堆栈使用量。此任务是可选任务,由宏OS_CFG_STAT_TASK_EN控制是否使用此任务;
- 定时任务:用来向用户提供定时服务,也是可选任务,由宏OS_CFG_TMR_EN控制是否使用此任务;
- 中断服务管理任务:可选任务,由宏OS_CFG_ISR_POST_DEFERRED_EN控制是否使用此任务。
UCOSIII空闲任务
我们首先来看一下空闲任务:OS_IdleTask(),在os_core.c文件中定义。任务OS_IdleTask()是必须创建的,不过不需要手动创建,在调用OS_Init()初始化UCOS的时候就会被创建。打开OS_Init()函 数 , 可 以 看 到 , 在OS_Init()中 调 用 了 函 数OS_IdleTaskInit(),打开函数OS_IdleTaskInit(),函数代码如下:
void OS_IdleTaskInit (OS_ERR *p_err)
{
#ifdef OS_SAFETY_CRITICAL
if (p_err == (OS_ERR *)0) {
OS_SAFETY_CRITICAL_EXCEPTION();
return;
}
#endif
OSIdleTaskCtr = (OS_IDLE_CTR)0;
/* ---------------- CREATE THE IDLE TASK ---------------- */
OSTaskCreate((OS_TCB *)&OSIdleTaskTCB,
(CPU_CHAR *)((void *)"uC/OS-III Idle Task"),
(OS_TASK_PTR)OS_IdleTask,
(void *)0,
(OS_PRIO )(OS_CFG_PRIO_MAX - 1u),
(CPU_STK *)OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_IdleTaskStkLimit,
(CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
(OS_MSG_QTY )0u,
(OS_TICK )0u,
(void *)0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR | OS_OPT_TASK_NO_TLS),
(OS_ERR *)p_err);
}
从上面的代码可以看出,函数OS_IdleTaskInit()很简单,只是调用了OSTaskCreate()来创建一 个 任 务 , 这 个 任 务 就 是 空 闲 任 务 。 任 务 优 先 级 为OS_CFG_PRIO_MAX –1,OS_CFG_PRIO_MAX是一个宏,在文件os_cfg.h中定义,OS_CFG_PRIO_MAX定义了UCOSIII可用的任务数。
空闲任务堆栈大小为OSCfg_IdleTaskStkSize,OSCfg_IdleTaskStkSize也是一个宏,在os_cfg_app.c文件中定义,默认为128,则空闲任务堆栈默认为128*4=512字节。
空闲任务的任务函数为任务函数为OS_IdleTask(),OS_IdleTask()函数代码如下:
void OS_IdleTask (void *p_arg)
{
CPU_SR_ALLOC();
p_arg = p_arg; /* 消除警告 */
while (DEF_ON) {
CPU_CRITICAL_ENTER(); //进入临界代码保护
OSIdleTaskCtr++;
#if OS_CFG_STAT_TASK_EN > 0u //开启了统计任务
OSStatTaskCtr++;
#endif
CPU_CRITICAL_EXIT();
OSIdleTaskHook(); /* 钩子函数 */
}
}
可以看到:每进入一次空闲任务,OSIdleTaskCtr就加一。我们可以通过查看OSIdleTaskCtr变量的递增速度来判断CPU执行应用任务的繁忙程度,如果递增的快的话说明应用任务花费时间少,很快就执行完了。
空闲任务特点:
- 空闲任务是UCOSIII创建的第一个任务;
- 空闲任务是UCOSIII必须创建的;
- 空闲任务优先级总是为OS_CFG_PRIO_MAK-1;
- 空闲任务中不能调用任何可使空闲任务进入等待态的函数!
UCOSIII时钟节拍任务
另一个必须创建的任务时钟节拍任务:OS_Ticktask(),在OS_Init()中调用了一个函数OS_TickTaskInit(),函数代码如下:
void OS_TickTaskInit (OS_ERR *p_err)
{
#ifdef OS_SAFETY_CRITICAL
if (p_err == (OS_ERR *)0) {
OS_SAFETY_CRITICAL_EXCEPTION();
return;
}
#endif
OSTickCtr = (OS_TICK)0u; /* Clear the tick counter */
OSTickTaskTimeMax = (CPU_TS)0u;
OS_TickListInit(); /* Initialize the tick list data structures */
/* ---- CREATE THE TICK TASK ---- */
if (OSCfg_TickTaskStkBasePtr == (CPU_STK *)0) {
*p_err = OS_ERR_TICK_STK_INVALID;
return;
}
if (OSCfg_TickTaskStkSize < OSCfg_StkSizeMin) {
*p_err = OS_ERR_TICK_STK_SIZE_INVALID;
return;
}
if (OSCfg_TickTaskPrio >= (OS_CFG_PRIO_MAX - 1u)) { /* Only one task at the 'Idle Task' priority */
*p_err = OS_ERR_TICK_PRIO_INVALID;
return;
}
OSTaskCreate((OS_TCB *)&OSTickTaskTCB,
(CPU_CHAR *)((void *)"uC/OS-III Tick Task"),
(OS_TASK_PTR )OS_TickTask,
(void *)0,
(OS_PRIO )OSCfg_TickTaskPrio,
(CPU_STK *)OSCfg_TickTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_TickTaskStkLimit,
(CPU_STK_SIZE)OSCfg_TickTaskStkSize,
(OS_MSG_QTY )0u,
(OS_TICK )0u,
(void *)0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR | OS_OPT_TASK_NO_TLS),
(OS_ERR *)p_err);
}
可以看到在函数OS_TickTaskInit()的最后调用OSTaskCreate()来创建了一个任务,任务函数为OS_TickTask(),所以说时钟节拍任务是UCOSIII必须创建的,同样,不需要我们手工创建。时钟节拍任务的任务优先级为OSCfg_TickTaskPrio,时钟节拍任务的优先级尽可能的高一点,一般设置时钟节拍任务的任务优先级为1。
时钟节拍任务的作用是跟踪正在延时的任务,以及在指定时间内等待某个内核对象的任务,OS_TickTask()任务函数代码如下:
void OS_TickTask (void *p_arg)
{
OS_ERR err;
CPU_TS ts;
p_arg = p_arg; /* Prevent compiler warning */
while (DEF_ON) {
(void)OSTaskSemPend((OS_TICK )0, //请求信号量
(OS_OPT )OS_OPT_PEND_BLOCKING,
(CPU_TS *)&ts,
(OS_ERR *)&err); /* Wait for signal from tick interrupt */
if (err == OS_ERR_NONE) {
if (OSRunning == OS_STATE_OS_RUNNING) {
OS_TickListUpdate(); /* 信号量请求成功的话就调用 */
}
}
}
}
我们先来了解时钟节拍任务中一个重要的概念:时钟节拍列表。
时钟节拍列表是由一个数据表OSCfg_TickWheel(在os_cfg_app.c中定义)和一个计数器OSTickCtr组 成 , 表OSCfg_TickWheel是 一 个 数 组 , 数 组 元 素 个 数 由 宏OS_CFG_TICK_WHEEL_SIZE定义,宏OS_CFG_TICK_WHEEL_SIZE在os_cfg_app.h中定义了。表OSCfg_TickWheel中的元素为os_tick_spoke类型的,os_tick_spoke是一个结构体,结构体定义如下:
struct os_tick_spoke {
OS_TCB *FirstPtr; /* 指针变量,在表头上并属于该表 */
OS_OBJ_QTY NbrEntries; /* 表示在该表项上等待的任务的数目 */
OS_OBJ_QTY NbrEntriesMax; /* 表示在该表项上等待的任务的最大数目 */
};
在使用时钟节拍列表时需要先初始化时钟节拍列表,在OS_TickTaskInit()函数中会调用OS_TickListInit()来初始化时钟节拍列表,函数OS_TickListInit()代码如下:
void OS_TickListInit (void)
{
OS_TICK_SPOKE_IX i;
OS_TICK_SPOKE *p_spoke;
for (i = 0u; i < OSCfg_TickWheelSize; i++) {
p_spoke = (OS_TICK_SPOKE *)&OSCfg_TickWheel[i];
p_spoke->FirstPtr = (OS_TCB *)0;
p_spoke->NbrEntries = (OS_OBJ_QTY )0u;
p_spoke->NbrEntriesMax = (OS_OBJ_QTY )0u;
}
}
初始化时钟节拍列表十分简单,就是将OSCfg_TickWheel[]中的每个变量都清零。
UCOSIII统计任务
在UCOSIII中统计任务可用来统计CPU的使用率、各个任务的CPU使用率和各任务的堆栈使用情况,默认情况下统计任务是不会创建的,如果要使能统计任务的话需要将宏OS_CFG_STAT_TASK_EN置1,宏OS_CFG_STAT_TASK_EN在os_cfg.h文件中有定义。
当我们将宏OS_CFG_STAT_TASK_EN置1以后,OSinit()函数中有关统计任务的代码就可以编译了。OS_StatTaskInit()函数用来创建统计任务,统计任务的优先级通过宏OS_CFG_STAT_TASK_PRIO设 置 ,一般将统计任务的优先级设置为OS_CFG_PRIO_MAX-2,也就是倒数第二。
如果要使用统计任务的话就需要在main()函数创建的第一个也是唯一一个应用任务中调用OSStatTaskCPUUsageInit()函数。也就是在start_task()任务中调用函数:
void start_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
p_arg = p_arg;
CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); //统计任务
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了测量中断关闭时间
CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN //当使用时间片轮转的时候
//使能时间片轮转调度功能,时间片长度为1个系统时钟节拍,既1*5=5ms
OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);
#endif
OS_CRITICAL_ENTER(); //进入临界区
... //函数省略
OS_TaskSuspend((OS_TCB*)&StartTaskTCB,&err); //挂起开始任务
OS_CRITICAL_EXIT(); //进入临界区
}
CPU的总的使用率会保存在变量OSStatTaskCPUUsage中,我们可以通过读取这个值来获取CPU的使用率。从V3.03.00版本起,CPU的使用率用一个0~10000之间的整数表示(对应0.00~100.00%),在这之前的版本,CPU使用率是0~100之间的整数表示。
如果将宏OS_CFG_STAT_TASK_STK_CHK_EN置1 的话表示检查任务堆栈使用情况,那么统计任务就会调用OSTaskStkChk()函数来计算所有已创建任务的堆栈使用量,并将检测结果写入到每个任务的OS_TCB中的StkFree和StkUsed中。
UCOSIII定时任务
UCOSIII提供软件定时器功能,定时任务是可选的,将宏OS_CFG_TMR_EN设置为1就会使能定时任务,在OSInit()中将会调用函数OS_TmrInit()来创建定时任务。定时任务的优先级通过宏OS_CFG_TMR_TASK_PRIO定义,一般将定时器任务优先级设置为2。
UCOSIII中断服务管理任务
当把os_cfg.h文件中的宏OS_CFG_ISR_POST_DEFERRED_EN置1就会使能中断服务管理任务,UCOSIII会创建一个名为OS_IntQTask()的任务,该任务负责“延迟”在ISR中调用的系统post服务函数的行为。中断服务管理任务的任务优先级永远是最高的,为0!
在UCOS中可以通过关闭中断和任务调度器上锁两种方式来管理临界段代码,如果采用后一种,即调度器上锁的方式来管理临界段代码的话,那么在中断服务函数中调用的“post”类函数就不允许操作诸如任务就绪表、等待表等系统内部数据结构。
当ISR(中断服务函数)调用UCOSIII提供的“post”函数时,要发送的数据和发送的目的地都会存入一个特别的缓冲队列中,当所有嵌套的ISR都执行完成以后UCOSIII会做任务切换,运行中断服务管理任务,该任务会把缓存队列中存放的信息重发给相应的任务。这样做的好处就是可以减少中断关闭的时间,否则,在ISR中还需要把任务从等待列表中删除,并把任务放入就绪表,以及做一些其他的耗时操作。
UCOSIII钩子函数
钩子函数一般主要是用来扩展其他函数(任务)功能的,钩子函数有如下几个:
- OSIdleTaskHook(),空闲任务调用这个函数,可以用来让CPU进入低功耗模式;
- OSInitHook(),系统初始化函数OSInit()调用此函数;
- OSStatTaskHook(),统计任务每秒中都会调用这个函数,此函数允许你向统计任务中添加自己的应用函数;
- OSTaskCreateHook(),任务创建的钩子函数;
- OSTaskDelHook(),任务删除的钩子函数;
- OSTaskReturnHook(),任务意外返回时调用的钩子函数,比如删除某个任务;
- OSTaskSwHook(),任务切换时候调用的钩子函数;
- OSTimeTickHook(),滴答定时器调用的钩子函数。
本质上讲:钩子函数实际就是事件,就是你可以不改动UCOSIII源代码结构,在需要的地方加入自己代码!
下面以OSIdleTaskHook()为例介绍一下:
void OS_IdleTask (void *p_arg)
{
CPU_SR_ALLOC();
p_arg = p_arg; /* 消除警告 */
while (DEF_ON) {
CPU_CRITICAL_ENTER(); //进入临界代码保护
OSIdleTaskCtr++;
#if OS_CFG_STAT_TASK_EN > 0u //开启了统计任务
OSStatTaskCtr++;
#endif
CPU_CRITICAL_EXIT();
OSIdleTaskHook(); /* 钩子函数 */
}
}
函数OSIdleTaskHook(),代码如下:
void OSIdleTaskHook (void)
{
#if OS_CFG_APP_HOOKS_EN > 0u
if (OS_AppIdleTaskHookPtr != (OS_APP_HOOK_VOID)0) {
(*OS_AppIdleTaskHookPtr)();
}
#endif
}
从 上 面 的 函 数 代 码 中 可 以 看 出 要 使 用 空 闲 任 务 钩 子 函 数 的 话 需 要 将 宏OS_CFG_APP_HOOKS_EN置1,即允许使用空闲任务的钩子函数。当时使能空闲任务的钩子函数以后每次进入空闲任务就会调用指针OS_AppIdleTaskHookPtr,而这个指针是一个指向函数的指针。OS_AppIdleTaskHookPtr是何方神圣?
打开os_app_hooks.c文 件 , 在 文 件 中 有 个 函 数App_OS_SetAllHooks(),函数代码如下:
void App_OS_SetAllHooks (void)
{
#if OS_CFG_APP_HOOKS_EN > 0u
CPU_SR_ALLOC();
CPU_CRITICAL_ENTER();
OS_AppTaskCreateHookPtr = App_OS_TaskCreateHook;
OS_AppTaskDelHookPtr = App_OS_TaskDelHook;
OS_AppTaskReturnHookPtr = App_OS_TaskReturnHook;
OS_AppIdleTaskHookPtr = App_OS_IdleTaskHook;
OS_AppStatTaskHookPtr = App_OS_StatTaskHook;
OS_AppTaskSwHookPtr = App_OS_TaskSwHook;
OS_AppTimeTickHookPtr = App_OS_TimeTickHook;
CPU_CRITICAL_EXIT();
#endif
}
显示将App_OS_IdleTaskHook赋值给OS_AppIdleTaskHookPtr。那么问题来了,OS_AppIdleTaskHookPtr又是何方神圣?我们仔细查看os_app_hooks.c文件,会发现App_OS_IdleTaskHook是一个函数,代码如下:
void App_OS_IdleTaskHook (void)
{
}
到这里我们基本就懂了空闲任务的钩子函数OSIdleTaskHook()是怎么工作了,在OSIdleTaskHook中最终调用的是函数App_OS_IdleTaskHook(),也就是说如果我们要想在空闲任务的钩子函数中做一些其他处理的话就需要将程序代码写在App_OS_IdleTaskHook()函数中。
注意:在空闲任务的钩子函数中不能调用任何可以使空闲进入等待态的代码!
原因很简单,CPU总是在不停的运行,需要一直工作,不能让CPU停下来,哪怕是执行一些对应用没有任何用的代码,比如简单的将一个变量加一。在UCOS中为了让CPU一直工作,在所有应用任务都进入等待态的时候CPU会执行空闲任务,我们可以从空闲任务的任务函数OS_IdleTask()看出,在OS_IdleTask()中没有任何可以让空闲任务进入等待态的代码。如果在OS_IdleTask()中有可以让空闲任务进入等待态的代码的话有可能会在同一时刻所有任务(应用任务和空闲任务)同时进入等待态,此时CPU就会无所事事了,所以在空闲任务的钩子函数OSIdleTaskHook()中不能出现可以让空闲任务进入等待态的代码!这点很重要,一定要谨记!