Build Your Own RTOS Part6:延时
Build Your Own RTOS 系列文章:
1 让出CPU
在我们前几期所搭建出来的简易系统中,虽然已经完成了任务调度和时间片轮转,可以并发执行任务,但是当一个任务不需要运作,想要让出CPU给别的任务时,我们好像没有一个办法让其完成。
我们这一期就来解决这个问题:当任务不需要CPU时,它会主动进入“睡眠”状态,不再参与调度,直到时间到达。配合空闲任务,我们的系统就初步具备了资源管理的功能。这才像一个真正的RTOS。
我们先来构思一下具体的实现路径,就以大家都熟悉的硬件阻塞延时(比如ST的HAL库提供的HAL_Delay())为例。HAL有一个时基变量uwTick,用于存储当前“系统时间”,实现方法是在定期触发的SysTick_Handler()中断中,让uwTick++。HAL_Delay()的完整实现如下:
1 | __weak void HAL_Delay(uint32_t Delay) |
可以看到,HAL_Delay()的原理就是,如果规定的时间没到我就卡在这死循环,直到规定时间结束。这就是我们常说的阻塞延时的阻塞所在。
类似的,我们的延时也使用这个思路进行。
1.1 延时时间
首先我们要为每一个任务分配一个变量,用于存储延时的时间。这个变量当然是放在TCB里了。
1 | typedef struct Task_Control_Block |
不要忘记初始化的时候,将TCB中的DelayTicks设置为0。修改OS_TaskCreate函数,添加一行tcb->DelayTicks = 0;。
1.2 中断函数
接着我们要改造OS_Tick_Handler(),遍历一遍任务列表,如果一个任务的延时时间>0,就将其减1。这里涉及到一点算法的知识:如何遍历链表?
1 | void OS_Tick_Handler(void) |
2 OS_Delay()
好了,我们基于以上的内容开始编写我们的延时函数void OS_Delay(uint32_t ticks)。它的主要逻辑就是,将当前任务的延时变量设置成ticks,然后触发PendSV,让出CPU。
1 | void OS_Delay(uint32_t ticks) |
就这么简单结束了吗?当然不是。我们知道,除了OS_Delay()这个函数外,SysTick_Handler()中断也会更新任务延时的值,如果就这么写的话它们俩说不定会在程序里打架,导致系统错乱了。所以我们要在前后添加临界区。注意,为了解耦,我们还是要把开关中断的函数在os_cpu.c中封装起来。
1 | /* in os_cpu.c 不要忘记在os_cpu.h中声明 */ |
3 调度器
接下来我们还要修改调度器。我们之前的调度器是只看任务链表,不看延时;我们就要修改调度的逻辑,让它考虑到正在延时中的任务,将它们排除在外。我们的调度器逻辑实际上是写在OS_Tick_Handler()里的,所以我们又要修改它。
首先我们先在os_core.c里封装一个私有函数FindNextTask(),用于寻找下一个处于就绪态的任务。
1 | OS_TCB* FindNextTask(void) |
然后我们修改OS_Tick_Handler()。
1 | void OS_Tick_Handler(void) |
问题又来了:如果所有任务都在睡觉,会发生什么?答案是会卡在寻找下一个可用任务的循环中出不来。所以,我们在启用调度器时,需要先创建一个空闲任务,内容可以是一个死循环,或者是执行__WFI()(一个ARM架构下的宏,代表进入Wait For Interrupt的低功耗模式),这样即使所有任务都在休眠,系统也能正常运作。我们在os_core.c里创建这个任务函数,这里就用死循环。
1 | void IdleTask(void) |
然后我们在OS_StartScheduler()函数的最开头添加这个任务。由于任务链表中必有一个任务,所以原来的防御编程可以省略了。
1 | void OS_StartScheduler(void) |
4 “启动”
好了,我们可以开始实验。这次实验推荐使用两个GPIO,这样能直观的看到延时的效果。
我们的任务就设置的简单一点:
1 | void Task1(void) |
插板子,编译、下载。看看这个效果。。。好像有点不对劲?

这就是RTOS开发中典型的“假切换”问题。实际上,问题出在我们的OS_Delay()代码中。
当任务调用OS_Delay()时,虽然任务修改了自己的延时变量,也触发了PendSV,但是它没有指定下一个任务是谁(很有可能,Next还是自己),直到下一次的SysTick中断里才会重新寻找下一个就绪任务。所以就导致“相位”在漂移。
我们只需要修改OS_Delay():
1 | void OS_Delay(uint32_t ticks) |
在交出CPU的时候就指定下一个任务,这样调度就正常了。

5 总结
本期我们完成了任务阻塞延时,现在我们的系统已经基本可以运行多个独立的任务了。接下来我们就要讨论如何让多个任务“通信”。下期见!
.jpg)