近年来,实时操作系统 (RTOS) 非常流行。大多数嵌入式开发工程师会在设计周期的早期选择 RTOS,有时甚至在需求确定之前。RTOS 的一个有趣之处在于,对于许多基于 MCU 的应用程序来说,RTOS 是多余的。当应用程序需要任务抢占(暂时挂起任务以切换到更高优先级的任务并稍后恢复)并且具有严格的实时要求时,RTOS 的魔力才真正发挥作用。在很多情况下,一个更简单的协作调度器也能轻松满足要求。
协作调度器仍然允许通过使用后台周期性定时器来调度任务,该定时器创建系统滴答,就像在RTOS中一样。不同之处在于,协作调度器不具有优先级和抢占权,而是只执行在某个时间周期间隔发生的任务。如果两个任务同时运行,任务列表中位置较高的任务首先运行,然后是第二个任务,依此类推。协作调度器允许软实时行为,但是通过使用中断和其他机制也可以满足硬实时需求。
使用协作调度器的一个很大的优点是,与RTOS相比,它们相当简单和直接。调试RTOS可能极其复杂,而且通常非常痛苦。另一方面,协作式调度器只有很少的几个部分,并且更容易调试。事实上,只需几个简单的步骤就可以设计和实现一个协作调度器。它们也使用很少的闪存和RAM。
步骤1——定义调度器要求
嵌入式开发人员在写任何代码之前,了解将要写的到底是什么是一个好主意。这通常意味着查阅项目需求文档并理解需要什么。对于协作调度器,应该记住一些基本要求:
调度器应使用单一中断驱动定时器来跟踪系统时间
应编写调度器,以便可以从一个项目到下一个项目重复使用
调度器应能够调度周期性和后台任务
调度器应易于通过使用配置表进行配置
步骤2——创建软件架构
当开发可重用软件时,实现一个好的软件架构是至关重要的。当谈到将在多种类型的硬件上使用的调度器时,分层的体系结构可能是永远重用的代码和在第一个项目后丢弃的代码之间的区别。创建一个由硬件相关代码的驱动程序层、调度器核心的应用层和配置应用程序的配置层组成的架构,效果非常好!
步骤3——定义任务的组成部分
为了正确运行周期性任务,调度器至少需要三条信息;任务连续运行之间的时间间隔、任务最后一次执行的系统节拍以及任务到期时应该执行的功能。有了这些信息,嵌入式开发人员就有可能定义一个C结构,用来定义处理器必须执行的每个任务。
步骤4–任务配置表
一旦定义了TaskType结构,现在就可以创建一个TaskType数组,数组中的每个元素定义一个任务。对于小型应用,该表相对较短。Tasks数组应该定义为一个静态变量,如果可能的话,应该定义为const,这将有助于确保任务定义在程序执行期间不会改变。
步骤5——第一个任务功能
在进一步开发调度程序之前,最好确保任务配置表中定义的任务已经定义。这将有助于防止编译器因为函数没有被定义而生气。任务本身可以全部存储在一个模块中,或者按照作者的喜好存储在单独的模块中。任务函数的定义就像任何其他C函数一样。
步骤6——一些支持配置功能
此时,几乎所有与任务配置相关的东西都已经设置好了,准备就绪。唯一缺少的是调度器遍历配置表所需的两个辅助函数。第一个,*Tsk_GetConfig,是一个函数,它返回一个指向 Tasks[] 配置表的指针。这将允许调度程序访问表结构。第二个,Tsk_GetNumTasks,是一个函数,它返回存储在配置表中的任务数。这两个函数之所以存在,是因为使用了良好的数据封装编程实践。该信息仅限于模块范围,嵌入式开发人员使用调度器需要这两个辅助函数来访问数据。
第7步——调度器的诞生
最后,所有部分都准备就绪,可以编写程序的实际调度器部分。协作调度器的调度算法通常直接写在 main 函数中。调度器通过创建指向任务配置表的指针来初始化,还会检索表中的任务数。
有了这两条信息,主循环将从检索当前系统节拍开始。在32位系统中,这是微不足道的工作,因为32位刻度变量的读写是原子性的。接下来,循环检查任务配置表中存在的每个任务条目。如果任务的间隔设置为0(一个不断运行的后台任务),则执行该任务。另一方面,如果间隔不为0,则执行一些数学运算来确定任务运行的最后时间和当前时间之间的差是否大于或等于任务间隔。如果是,那么任务将被执行。
结论
可以看出,协作调度器的实现非常简单直接。没有多少移动部件,一旦构建了调度器,如果做得好,它可以从一个项目重用到下一个项目。嵌入式开发人员唯一需要更改的是系统滴答计时器,然后是任务配置表。虽然这样一个简单的调度器并没有配备当今RTOS的所有花里胡哨,但这个简单的调度器实际上适用于多少应用程序确实令人惊讶。