参考内容:《[野火]uCOS-III内核实现与应用开发实战指南——基于STM32》第 8 章。
不知道有没有注意到这样一个问题:我在学习 x86 汇编语言的时候,曾详细研读过系统内核的代码,内核本身也是有自己的 TCB 的,内核可作为一个“任务管理器”来对其他任务进行创建、删除等操作。那么 uCOS 作为一个实时操作系统,内核也应该需要自己的任务和 TCB,因此我们需要创建一个空闲任务给内核,这样,在没有任务的时候,内核空转运行空闲任务,同时检查各个任务的状态以做出相应的操作(因此 uCOS 内核也可视为“任务管理器”)。
空闲任务虽然与其他任务的作用不同,但它的本质依然是一个任务,TCB、任务栈一个都不能少。
在 os.h 中定义 TCB:
OS_EXT OS_TCB OSIdleTaskTCB;
有关空闲任务栈的定义,并不在 os.h 中。
首先在 os_cfg_app.h 中定义了空闲任务栈的大小(感觉叫栈尺寸更合适?栈粒度应该是 4 字节):
/* 空闲任务栈大小 */
#define OS_CFG_IDLE_TASK_STK_SIZE 128u
然后,在 os_cfg_app.c 中定义栈和栈大小:
CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE]; /* 空闲任务栈 */
CPU_STK *const OSCfg_IdleTaskStkBasePtr = (CPU_STK *) &OSCfg_IdleTaskStk[0]; /* 空闲任务栈的起始地址 */
CPU_STK const OSCfg_IdleTaskStkSize = (CPU_STK_SIZE) OS_CFG_IDLE_TASK_STK_SIZE; /* 空闲任务栈的大小 */
作为一个任务,任务主体是不能缺的。
按照书上的教程,它定义了一个全局变量(OSIdleTaskCtr),用来计数。我不知道这个计数是干什么用的,我觉得在目前的学习阶段而言,空闲任务函数可以什么都不做。可能是作者觉得内核干一些无意义的事也好过什么事也不干吧。
typedef CPU_INT32U OS_IDLE_CTR;
OS_EXT OS_IDLE_CTR OSIdleTaskCtr;
注意:
以下是空闲任务函数,用来无聊的计数:
/* 空闲任务 */
void OS_IdleTask (void *p_arg)
{
p_arg = p_arg;
/* 空闲任务什么都不做,只对全局变量OSIdleTaskCtr ++ 操作 */
for (;;)
{
OSIdleTaskCtr++;
}
}
很简单,就完成两个功能:
注意:
/* 空闲任务初始化函数 */
void OS_IdleTaskInit (OS_ERR *p_err)
{
OSIdleTaskCtr = (OS_IDLE_CTR) 0; /* 计数器清零 */
OSTaskCreate ((OS_TCB*) &OSIdleTaskTCB,
(OS_TASK_PTR) OS_IdleTask,
(void *) 0,
(CPU_STK *) OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE) OSCfg_IdleTaskStkSize,
(OS_ERR *) &p_err); /* 创建空闲任务 */
}
那么,在 OS 初始化的时候,调用该函数即可完成空闲任务的创建:
/* OS 系统初始化,用于初始化全局变量 */
void OSInit (OS_ERR *p_err)
{
/* 系统用一个全局变量 OSRunning 来指示系统的运行状态。系统初始化时,默认为停止状态,即 OS_STATE_OS_STOPPED */
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *) 0; /* 指向当前正在运行的任务的 TCB 指针 */
OSTCBHighRdyPtr = (OS_TCB *) 0; /* 指向就绪任务中优先级最高的任务的 TCB */
OS_RdyListInit(); /* 初始化就绪列表 */
OS_IdleTaskInit(p_err); /* ----> 初始化空闲任务 */
if (*p_err != OS_ERR_NONE) {
return;
}
}
很多时候,某些任务运行到某处就需要延时一段时间,什么都不做。比如驱动某外设,需要按照时序,延时一段时间,再去访问接口。为了能榨干 CPU 的性能,不让它空转,在延时的这段时间内,还是要去完成其他的任务。
任务自行发起的延时,会使得任务自身发生阻塞,不再运行下去,这种延时就叫阻塞延时。而在阻塞延时期间,我们不让 CPU 闲着干等着,所以就有了任务切换(任务调度)。如果没有别的任务可以切换(调度),那就切换到内核的空闲任务。无论怎样,只要别让 CPU 闲着就行了。
在 TCB 中加入了记录延时长度的成员,最小单位为 1 个 Tick,之前在上一篇笔记中我们初始化一个 Tick 为 10ms,因此最小单位为 10ms。
/* TCB 重命名为大写字母格式 */
typedef struct os_tcb OS_TCB;
/* TCB 数据类型声明 */
struct os_tcb{
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks; /* 任务延时多少个 Ticks,注意 1 个 Ticks 为 10ms */
};
下面来实现阻塞延时的函数。
一旦我们在任务中发起延时,那么在阻塞延时函数中完成两件事:
/* 阻塞延时 */
void OSTimeDly (OS_TICK dly)
{
/* 延时时间 */
OSTCBCurPtr->TaskDelayTicks = dly;
/* 任务切换 */
OSSched();
}
特别需要注意,阻塞延时函数跟软延时 Delay 的本质是不同的。软延时是 CPU 真的会卡在 for 循环里,什么事都干不了;而阻塞延时函数的实质是 CPU 设置了一下延时时间,然后就去忙别的了。那谁负责计时?当然是我们的 SysTick 了。
之前已提及,阻塞延时函数负责设置延时时间,书承上节内容,在 SysTick 发起一次中断时,表明一次 Tick 已经到来(在本案例中是 已经过去了 10ms),此时,把 TCB 中记录延时的数值减去 1,表示已经过去了一个 Tick(即过去了 10ms)。之后,还要发起一次任务调度,看看有没有任务已经延时结束的。
因此,实际上该函数起了这么一个作用:每隔 10ms,我帮你计数一次,起到了一个延时计数的功能。
void OSTimeTick (void)
{
OS_PRIO i;
/* 遍历整个就绪列表,如果延时未到时,则减 1 */
for ( i = 0u; i < OS_CFG_PRIO_MAX; i++)
{
if ( OSRdyList[i].HeadPtr->TaskDelayTicks > 0u )
{
OSRdyList[i].HeadPtr->TaskDelayTicks --;
}
}
/* 任务调度 */
OSSched();
}
从现在起,该函数不再叫任务切换了,而是叫任务调度器,因为已经有三个任务了。
我们实现的任务调度的算法很简单,也很朴素,就是去一个个检查其他其他任务是否延时结束,如果某个任务延时结束了,那么就切换到这个任务去运行。如果找不到一个任务延时结束,那么就维持当前任务运行。
在这里,我们实现了两个用户任务,一个空闲任务。那么,SysTick 每发起一次中断就会调用本函数,检查的步骤为:
以上,就是我们实现的简单的调度算法。
/* 任务调度 */
void OSSched (void)
{
if ( OSTCBCurPtr == &OSIdleTaskTCB ) /* (a) 假如现在的任务是空闲任务 */
{
if ( OSRdyList[0].HeadPtr->TaskDelayTicks == 0 ) /* 如果任务 1 延时结束 */
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if ( OSRdyList[1].HeadPtr->TaskDelayTicks == 0 ) /* 如果任务 2 延时结束 */
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else
{
return;
}
}
else if ( OSTCBCurPtr == OSRdyList[0].HeadPtr ) /* (b) 假如现在的任务是任务 1 */
{
if ( OSRdyList[1].HeadPtr->TaskDelayTicks == 0 ) /* 如果任务 2 延时结束 */
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else if ( OSTCBCurPtr->TaskDelayTicks != 0 ) /* 如果任务 1 自己还在延时 */
{
OSTCBHighRdyPtr = &OSIdleTaskTCB;
}
else
{
return;
}
}
else if ( OSTCBCurPtr == OSRdyList[1].HeadPtr ) /* (c) 假如现在的任务是任务 2 */
{
if ( OSRdyList[0].HeadPtr->TaskDelayTicks == 0 ) /* 如果任务 1 延时结束 */
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if ( OSTCBCurPtr->TaskDelayTicks != 0 ) /* 如果任务 2 自己还在延时 */
{
OSTCBHighRdyPtr = &OSIdleTaskTCB;
}
else
{
return;
}
}
OS_TASK_SW();
}
最后一个困惑:为什么要将 TCB 指针赋值给 OSTCBHighRdyPtr 而不是 OSTCBCurPtr 呢?我个人理解:因为 uCOS 是抢占式多任务,将任务 A 赋值给最高优先级时,当前任务会被切换为该任务 A。而且可以看看 PendSV 异常汇编程序,你会发现它实现的最核心的东西其实是 OSTCBCurPtr = OSTCBHighRdyPtr。我现在不是很懂,也许学到后面会逐渐明白的吧。
在任务中,实现了阻塞延时,去掉了软件延时和手动切换任务。
#include "ARMCM3.h"
#include "os.h"
#define TASK1_STK_SIZE 20
#define TASK2_STK_SIZE 20
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;
uint32_t flag1;
uint32_t flag2;
void Task1 (void *p_arg);
void Task2 (void *p_arg);
void delay(uint32_t count);
int main (void)
{
OS_ERR err;
/* 初始化相关的全局变量,创建空闲任务 */
OSInit(&err);
/* 关中断,因为此时 OS 未启动,若开启中断,那么 SysTick 将会引发中断 */
CPU_IntDis();
/* 初始化 SysTick,配置 SysTick 为 10ms 中断一次,Tick = 10ms */
OS_CPU_SysTickInit(10);
/* 创建任务 */
OSTaskCreate ((OS_TCB*) &Task1TCB,
(OS_TASK_PTR) Task1,
(void *) 0,
(CPU_STK*) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_ERR *) &err);
/* 将任务加入到就绪列表 */
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;
/* 启动OS,将不再返回 */
OSStart(&err);
}
void Task1 (void *p_arg)
{
for (;;)
{
flag1 = 1;
OSTimeDly (2); // 20ms
flag1 = 0;
OSTimeDly (2);
}
// 不用手动任务切换
}
void Task2 (void *p_arg)
{
for (;;)
{
flag2 = 1;
OSTimeDly (2);
flag2 = 0;
OSTimeDly (2);
}
// 不用手动任务切换
}
(1)初始化流程如下:
(2)在第一个任务 Task1 中,执行 Flag1 = 1 后,执行到 OSTimeDly:
(3)在 Task2 中,执行 Flag2 = 1 后,也执行到 OSTimeDly:
(4)在运行空闲任务的同时,SysTick 也在工作中:
所以,你发现了一个什么问题?就是,任务调度(OSSched)会发生在设置延时时间(OSTimeDly)和中断发起(SysTick_Handler)的时候,这意味这两个函数都有可能引发 PendSV 异常。
可以发现,因为我们延时的长度一样,所以两个任务几乎是同时进行的,按照上面的分析,确实就是这个样子,看起来就像是并行线程,很神奇吧!