uCOS-III 学习记录(3)——空闲任务和阻塞延时
参考内容:《[野火]uCOS-III内核实现与应用开发实战指南——基于STM32》第 8 章。
1. 空闲任务
不知道有没有注意到这样一个问题:我在学习 x86 汇编语言的时候,曾详细研读过系统内核的代码,内核本身也是有自己的 TCB 的,内核可作为一个“任务管理器”来对其他任务进行创建、删除等操作。那么 uCOS 作为一个实时操作系统,内核也应该需要自己的任务和 TCB,因此我们需要创建一个空闲任务给内核,这样,在没有任务的时候,内核空转运行空闲任务,同时检查各个任务的状态以做出相应的操作(因此 uCOS 内核也可视为“任务管理器”)。
1.1 数据类型定义
空闲任务虽然与其他任务的作用不同,但它的本质依然是一个任务,TCB、任务栈一个都不能少。
1.1.1 空闲任务 TCB (os.h)
在 os.h 中定义 TCB:
1 | OS_EXT OS_TCB OSIdleTaskTCB; |
1.1.2 空闲任务栈 (os_cfg_app.c)
有关空闲任务栈的定义,并不在 os.h 中。
首先在 os_cfg_app.h 中定义了空闲任务栈的大小(感觉叫栈尺寸更合适?栈粒度应该是 4 字节):
1 | /* 空闲任务栈大小 */ |
然后,在 os_cfg_app.c 中定义栈和栈大小:
1 | CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE]; /* 空闲任务栈 */ |
1.2 空闲任务函数 OS_IdleTask (os_core.c)
作为一个任务,任务主体是不能缺的。
按照书上的教程,它定义了一个全局变量(OSIdleTaskCtr),用来计数。我不知道这个计数是干什么用的,我觉得在目前的学习阶段而言,空闲任务函数可以什么都不做。可能是作者觉得内核干一些无意义的事也好过什么事也不干吧。
1 | typedef CPU_INT32U OS_IDLE_CTR; |
注意:
- 本函数不允许用户调用。
以下是空闲任务函数,用来无聊的计数:
1 | /* 空闲任务 */ |
1.3 空闲任务初始化函数 (os_core.c)
很简单,就完成两个功能:
- 创建空闲任务:调用 OSTaskCreate 即可。
- 计数清零:将 OSIdleTaskCtr 清零,为无聊的计数做准备。
注意:
- 本函数不允许用户调用。
1 | /* 空闲任务初始化函数 */ |
那么,在 OS 初始化的时候,调用该函数即可完成空闲任务的创建:
1 | /* OS 系统初始化,用于初始化全局变量 */ |
2 阻塞延时
很多时候,某些任务运行到某处就需要延时一段时间,什么都不做。比如驱动某外设,需要按照时序,延时一段时间,再去访问接口。为了能榨干 CPU 的性能,不让它空转,在延时的这段时间内,还是要去完成其他的任务。
任务自行发起的延时,会使得任务自身发生阻塞,不再运行下去,这种延时就叫阻塞延时。而在阻塞延时期间,我们不让 CPU 闲着干等着,所以就有了任务切换(任务调度)。如果没有别的任务可以切换(调度),那就切换到内核的空闲任务。无论怎样,只要别让 CPU 闲着就行了。
2.1 数据类型定义
在 TCB 中加入了记录延时长度的成员,最小单位为 1 个 Tick,之前在上一篇笔记中我们初始化一个 Tick 为 10ms,因此最小单位为 10ms。
1 | /* TCB 重命名为大写字母格式 */ |
下面来实现阻塞延时的函数。
2.2 阻塞延时函数 OSTimeDly (os_time.c)
一旦我们在任务中发起延时,那么在阻塞延时函数中完成两件事:
- 延时长度记录到当前运行任务的 TCB 中,方便系统查看。
- 发起任务切换(更准确的说法是任务调度)。
1 | /* 阻塞延时 */ |
特别需要注意,阻塞延时函数跟软延时 Delay 的本质是不同的。软延时是 CPU 真的会卡在 for 循环里,什么事都干不了;而阻塞延时函数的实质是 CPU 设置了一下延时时间,然后就去忙别的了。那谁负责计时?当然是我们的 SysTick 了。
2.3 SysTick 发起中断后调用 OSTimeTick (os_time.c)
之前已提及,阻塞延时函数负责设置延时时间,书承上节内容,在 SysTick 发起一次中断时,表明一次 Tick 已经到来(在本案例中是 已经过去了 10ms),此时,把 TCB 中记录延时的数值减去 1,表示已经过去了一个 Tick(即过去了 10ms)。之后,还要发起一次任务调度,看看有没有任务已经延时结束的。
因此,实际上该函数起了这么一个作用:每隔 10ms,我帮你计数一次,起到了一个延时计数的功能。
1 | void OSTimeTick (void) |
2.4 任务调度 OSChed (os_core.c)
从现在起,该函数不再叫任务切换了,而是叫任务调度器,因为已经有三个任务了。
我们实现的任务调度的算法很简单,也很朴素,就是去一个个检查其他其他任务是否延时结束,如果某个任务延时结束了,那么就切换到这个任务去运行。如果找不到一个任务延时结束,那么就维持当前任务运行。
在这里,我们实现了两个用户任务,一个空闲任务。那么,SysTick 每发起一次中断就会调用本函数,检查的步骤为:
- 如果当前任务为空闲任务,那么检查任务 1 和任务 2 是否延时结束。如果任务 1 延时结束,那么就运行任务 1 (OSTCBHighRdyPtr 指向任务 1 的 TCB);如果任务 2 延时结束,那么就运行任务 2 (OSTCBHighRdyPtr 指向任务 2 的 TCB)。
- 如果当前任务为任务 1,那么检查任务 1 自己和任务 2 是否延时结束。如果任务 2 延时结束,那么就运行任务 2 (OSTCBHighRdyPtr 指向任务 2 的 TCB);如果任务 1 自己未延时结束,那么就运行空闲任务(OSTCBHighRdyPtr 指向空闲任务的 TCB)。
- 如果当前任务为任务 2,那么检查任务 2 自己和任务 1 是否延时结束。如果任务 1 延时结束,那么就运行任务 1 (OSTCBHighRdyPtr 指向任务 1 的 TCB);如果任务 2 自己未延时结束,那么就运行空闲任务(OSTCBHighRdyPtr 指向空闲任务的 TCB)。
- 最后,触发 PendSV 异常,保存上文,使 OSTCBCurPtr 获得 OSTCBHighRdyPtr,切换下文。
以上,就是我们实现的简单的调度算法。
1 | /* 任务调度 */ |
最后一个困惑:为什么要将 TCB 指针赋值给 OSTCBHighRdyPtr 而不是 OSTCBCurPtr 呢?我个人理解:因为 uCOS 是抢占式多任务,将任务 A 赋值给最高优先级时,当前任务会被切换为该任务 A。而且可以看看 PendSV 异常汇编程序,你会发现它实现的最核心的东西其实是 OSTCBCurPtr = OSTCBHighRdyPtr。我现在不是很懂,也许学到后面会逐渐明白的吧。
3 实验:阻塞延时的运用
3.1 主函数 (app.c)
在任务中,实现了阻塞延时,去掉了软件延时和手动切换任务。
1 |
|
(1)初始化流程如下:
- OS 初始化:完成就绪列表和空闲任务的初始化。
- 关闭中断。
- SysTick 初始化:设置一个滴答为 10ms。
- 创建任务。
- 将任务加入就绪列表。
- OS 启动:启动任务切换,先运行就绪列表里的第一个任务 Task1。
(2)在第一个任务 Task1 中,执行 Flag1 = 1 后,执行到 OSTimeDly:
- 将当前任务 TCB 的记录延时成员设置好。
- 由 OSSched 进行任务调度。
- 因为此时没有别的任务运行,所以会被切换到 Task2。
(3)在 Task2 中,执行 Flag2 = 1 后,也执行到 OSTimeDly:
- 将当前任务 TCB 的记录延时成员设置好。
- 由 OSSched 进行任务调度。
- 因为此时 Task1 仍处于阻塞状态,所以会被切换到空闲任务。
(4)在运行空闲任务的同时,SysTick 也在工作中:
- 发现一个 Tick 到来了,说明 10ms 间隔已到,发起中断 SysTick_Handler。
- 执行函数 OSTimeTick,将所有任务 TCB 中的延时时长全部减 1,表示已过去了 10ms。
- 发起 OSSched 进行任务调度。
- 因为这时两个任务仍在阻塞中,所以继续切换到空闲任务。
- 这样,每发起一次 Tick,都会减去 1,同时去检查两个任务是否还在阻塞中,CPU 一直运行空闲任务。直到,当两个任务同时阻塞完毕的时候(因为我们设置的两个任务阻塞时间相同),又是一次 Tick 的到来,此时空闲任务将切换到 Task1(因为任务调度函数中判断 Task1 是否阻塞是写在前面的)。此时你发现又回到了第(2)个步骤,只不过这一次是 Flag1 = 0,后面的运行过程就不多说了。
3.2 实验现象
可以发现,因为我们延时的长度一样,所以两个任务几乎是同时进行的,按照上面的分析,确实就是这个样子,看起来就像是并行线程,很神奇吧!



