一文搞懂树莓派 Pico 任务调度器:基于定时器的 MicroPython 多任务实现

1.任务调度的基本概念和特点

使用定时器最大的便利在于允许我们在特定的时间点或时间间隔执行某些任务,我们可以将其看作与主程序中 while 循环不同的执行流:

我们可以使用 machine.Timer 库完成创建定时器对象、单次或周期运行定时器、执行回调函数和停止定时器,可以说非常便利。

考虑一下这样的情况:

  • 任务 1 定时 500ms 执行 1 次,在程序开始运行 3000ms 后不再执行
  • 任务 2 定时 1000ms 执行 1 次,在程序开始运行 3000ms 后暂停,在程序开始运行 7000ms 后恢复执行
  • 在程序运行开始 3000ms 后,定期 1000ms 执行任务 3
  • 在当前没有任务可以执行时,执行垃圾清理

若是使用多个定时器对象,会引入许多额外的全局变量和判断条件,导致程序复杂且难以扩展,例如:

  • 在任务 1 运行完毕后,我们需要根据任务 1 获取的数据/执行情况决定是否启动任务 3
  • 任务 2 在程序开始运行 7500ms 需要修改定期运行时间并且在程序开始运行 10000ms 时需要再次暂停

这时,我们可以使用 1 个定时器实现任务调度器,任务调度器是一种在嵌入式系统或实时操作系统中常见的编程模式,用于管理和执行定时任务。

它的主要功能包括:

  • 任务管理:
    • 定义任务的数据结构,包括任务回调函数、执行周期、参数等
    • 提供添加、删除、暂停和恢复任务的方法
    • 维护任务列表,跟踪任务的状态和执行情况
  • 任务调度:
    • 根据任务的执行周期和当前时间,决定哪些任务需要执行
    • 执行任务的回调函数,并处理可能出现的异常情况

简单来说,任务调度器就是使用定时器(如微控制器上的硬件定时器或软件定时器)来定期检查任务列表,判断哪些任务需要执行、哪些任务需要暂停,然后进行对应操作。

2. 自定义任务调度类的实现

这里,例程使用代码参考自 MicroPython 中文社区,在其基础上增加了一些注释和小修改: github.com/micropython...

这里,我们在 Scheduler.py 文件中,首先定义一个 Task 类,代码如下:

Python 复制代码
# ======================================== 导入相关模块 ========================================

# 导入const常量标识符
from micropython import const
# 导入硬件相关模块
from machine import Timer

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

# 定义Task任务类
class Task():
    """
​    Task类,用于表示一个定时执行的任务,包含任务的回调函数、参数、执行间隔和任务状态。

​    该类允许任务根据设定的时间间隔重复执行,并支持暂停和恢复任务。可以用于实现定时任务调度等功能。

​    Attributes:
​        TASK_RUN (int): 任务运行状态标识符,值为0,表示任务正在运行。
​        TASK_STOP (int): 任务停止状态标识符,值为1,表示任务已停止。
​        _callback (callable): 执行任务的回调函数,任务执行时调用。
​        _param (tuple): 任务回调函数的参数。
​        _intv (int): 任务执行的时间间隔,单位为毫秒,默认值为1000ms。
​        _state (int): 任务状态,默认为 TASK_RUN,表示任务正在运行。
​        _cnt (int): 任务计数器,表示任务已执行的次数,默认值为10。
​        _rt (int): 任务执行的返回值或其他辅助信息,默认值为0。

​    Methods:
​        __init__(self, callback, *param, interval=1000, state=TASK_RUN) -> None:
​            初始化Task类实例,设置回调函数、参数、间隔时间和任务状态。

​        pause(self) -> None:
​            暂停任务,将任务状态设置为TASK_STOP。

​        resume(self) -> None:
​            恢复任务,将任务状态设置为TASK_RUN。

​        run(self) -> None:
​            执行任务回调函数,并传入相关参数。
​    """

​    ​# 任务状态标识符
    TASK_RUN  = const(0)
    TASK_STOP = const(1)

    def __init__(self, callback: callable, *param: object, interval: int = 1000, state: int = TASK_RUN) -> None:
        """
​        初始化Task类实例,设置回调函数、参数、执行间隔时间和任务状态。

​        Args:
​            callback (callable): 任务执行时调用的回调函数。
​            *param (object): 传递给回调函数的参数。
​            interval (int, optional): 任务执行的时间间隔,单位为毫秒。默认为1000ms。
​            state (int, optional): 任务的初始状态,默认为TASK_RUN,表示任务正在运行。

​        Returns:
​            None
​        """
​        ​self._callback = callback
        self._param = param
        self._intv = interval
        self._state = state
        self._cnt = 10
        self._rt = 0

    def pause(self) -> None:
        """
​        暂停任务,将任务状态设置为TASK_STOP。

​        Args:
​            None

​        Returns:
​            None
​        """
​        ​self._state = Task.TASK_STOP

    def resume(self) -> None:
        """
​        恢复任务,将任务状态设置为TASK_RUN。

​        Args:
​            None

​        Returns:
​            None
​        """
​        ​self._state = Task.TASK_RUN

    def run(self) -> None:
        """
​        执行任务回调函数,并传入相关参数。

​        Args:
​            None

​        Returns:
​            None
​        """
​        ​self._callback(*self._param)

Task 类表示一个定时任务,包含任务回调函数、参数、执行间隔和任务状态等属性。并且提供了暂停任务、恢复任务以及执行回调函数的方法。任务状态由 TASK_RUN 和 TASK_STOP 两种常量表示。

这里,暂停方法和恢复方法主要实现原理就是改变任务实例的状态,当调度器遍历任务列表中每个任务实例的状态时,若任务状态为 Task.TASK_STOP 则不会执行该任务;反之,则执行。

然后,我们定义了任务调度的 Scheduler 类,Scheduler 类负责管理和执行所有已注册的任务。

Python 复制代码
# 定义Scheduler调度类
class Scheduler():
    """
​    Scheduler 类,用于管理和调度任务的执行。

​    该类通过定时器实现任务的周期性调度,支持任务的添加、删除、暂停、恢复和执行。
​    任务可以是任意可调用对象,调度器会根据设定的时间间隔定期检查并执行任务。
​    此外,调度器还支持任务空闲和任务错误的回调函数,用于处理任务执行过程中的特殊情况。

​    Attributes:
​        tm (machine.Timer): 定时器实例,用于触发任务的调度。
​        interval (int): 定时器的时间间隔,单位为毫秒。
​        task_idle (callable): 任务空闲时调用的回调函数。
​        task_err (callable): 任务执行出错时调用的回调函数。
​        tasks (list): 任务列表,存储所有已注册的任务实例。

​    Methods:
​        __init__(self, tm: Timer, interval: int = 100, task_idle: callable = None, task_err: callable = None):
​            初始化调度器实例,设置定时器、时间间隔和回调函数。

​        _tmrirq(self, t: Timer) -> None:
​            定时器中断回调函数,用于触发任务的调度。

​        _run(self, task: Task) -> None:
​            执行单个任务的回调函数。

​        scheduler(self) -> None:
​            调度器的主循环,负责循环执行所有已注册的任务。

​        find(self, task: Task) -> int:
​            查找指定任务在任务列表中的索引。

​        clear(self) -> None:
​            清空任务列表。

​        add(self, task: Task, state: int = Task.TASK_RUN) -> None:
​            添加任务到任务列表中。

​        delete(self, task: Task) -> None:
​            从任务列表中删除指定任务。

​        pause(self, task: Task) -> None:
​            暂停指定任务的执行。

​        resume(self, task: Task) -> None:
​            恢复指定任务的执行。

​        run(self, task: Task) -> None:
​            执行指定任务的回调函数。
​    """

​    ​def __init__(self, tm: Timer, interval: int = 100, task_idle: callable = None,
                 task_err: callable = None) -> None:
        """
​        初始化调度类实例,设置定时器、定时器间隔、任务空闲回调函数和任务错误回调函数。

​        Args:
​            tm (machine.Timer): 定时器实例,用于调度任务。
​            interval (int, optional): 定时器间隔,单位为毫秒。默认为100ms。
​            task_idle (callable, optional): 任务空闲时调用的回调函数,默认为None。
​            task_err (callable, optional): 任务出现错误时调用的回调函数,默认为None。

​        Returns:
​            None
​        """
​        ​self._tasks     = []
        self._task_idle = task_idle
        self._task_err  = task_err
        self._interval  = interval
        self._tmr       = tm
        self._tmr.init(period=interval, callback=self._tmrirq)

在初始化时需要传入一个定时器实例(machine.Timer)、定时器间隔、可选的任务空闲回调函数和任务错误回调函数;_tmrirq() 方法是定时器中断回调函数,用于遍历任务列表、判断任务是否正在运行,若任务还处于运行状态则更新任务的执行时间间隔:

Python 复制代码
def _tmrirq(self, t: Timer) -> None:
    """
​    定时器回调中断函数,用于处理定时器中断。

​    Args:
​        t (machine.Timer): 定时器实例,触发中断。

​    Returns:
​        None
​    """
​    ​# 遍历任务列表
    for i in range(len(self._tasks)):
        # 判断任务状态
        if self._tasks[i]._state == Task.TASK_RUN:
            # 任务_tasks[i]的执行时间间隔+1
            self._tasks[i]._rt += 1

_run() 方法用于执行单个任务的回调函数,并处理任务执行过程中可能出现的异常。

Python 复制代码
def _run(self, task: Task) -> None:
    """
​    执行单个任务回调函数。

​    Args:
​        task (Task): 任务实例,包含需要执行的回调函数。

​    Returns:
​        None
​    """
​    ​# 判断任务状态是否为TASK_RUN
    if task._state == Task.TASK_RUN:
        try:
            # 判断任务执行时间间隔是否大于等于任务需要执行的时间间隔
            if task._rt >= task._cnt:
                # 若是,则执行任务回调函数
                task._rt = 0
                task.run()
        except Exception as e:
            # 若是发生异常,则执行任务错误回调函数
            if self._task_err:
                self._task_err(e)

Scheduler 类的关键方法是 scheduler(),它是一个无限循环,负责不断检查任务列表中的任务状态,并根据任务的状态执行相应的操作。具体来说,它会:

  1. 遍历任务列表,对于每个任务:
    1. 如果任务状态为 Task.TASK_RUN,则增加任务的执行时间间隔;
    2. 如果任务的执行时间间隔达到或超过任务需要执行的时间间隔,则执行任务的回调函数,并重置执行时间间隔为 0;
  2. 如果所有任务都处于暂停状态,并且有空闲处理函数,则调用该函数;
  3. 如果遇到任何异常,打印异常信息并继续执行。
Python 复制代码
def scheduler(self) -> None:
    """
​    调度器的主循环,负责循环执行所有已注册的任务。

​    Args:
​        None

​    Returns:
​        None
​    """

​    ​# 轮询检测任务状态,判断是否执行任务
    while True:
        try:
            # 执行到达执行时间间隔的任务
            for i in range(len(self._tasks)):
                task = self._tasks[i]
                self._run(task)
            # 若空闲,则执行任务空闲回调函数
            if self._task_idle:
                self._task_idle()
        except KeyboardInterrupt:
            return
        # 发生异常时,抛出异常位置和类型
        except Exception as e:
            print('except {}'.format(e))

此外,Scheduler 类还提供了其他方法,如 find() 用于查找任务索引,clear() 用于清空任务列表,add() 用于添加新任务,delete() 用于删除任务,pause()resume() 用于暂停和恢复任务,以及 run() 用于立即执行任务。

Python 复制代码
def find(self, task: Task) -> int:
    """
​    查找指定任务在任务列表中的索引。

​    Args:
​        task (Task): 待查找的任务实例。

​    Returns:
​        int: 任务在任务列表中的索引。
​    """
​    ​try:
        return self._tasks.index(task)
    except:
        return None

def clear(self) -> None:
    """
​    清空任务列表。

​    Args:
​        None

​    Returns:
​        None
​    """
​    ​self._tasks.clear()

def add(self, task: Task, state: int = Task.TASK_RUN) -> None:
    """
​    添加任务到任务列表中。

​    Args:
​        task (Task): 任务实例,需要添加的任务。
​        state (int, optional): 任务状态,默认为Task.TASK_RUN,表示任务正在运行。

​    Returns:
​        None
​    """
​    ​if self.find(task) == None:
        self._tasks.append(task)
        # task._cnt为任务需要执行的时间间隔
        # 任务需要执行的时间间隔 = 任务间隔 // 定时器间隔
        task._cnt = task._intv // self._interval
        print('add task:', task._callback.__name__)
    if state == Task.TASK_STOP:
        self.pause(task)

def delete(self, task: Task) -> None:
    """
​    删除任务。

​    Args:
​        task (Task): 任务实例,待删除的任务。

​    Returns:
​        None
​    """
​    ​try:
        # 删除指定任务
        self._tasks.remove(task)
    except:
        print('del task <', task, '> error')

def pause(self, task: Task) -> None:
    """
​    暂停任务。

​    Args:
​        task (Task): 任务实例,待暂停的任务。

​    Returns:
​        None
​    """
​    ​if self.find(task) != None:
        self._tasks[self.find(task)].pause()

def resume(self, task: Task) -> None:
    """
​    恢复任务。

​    Args:
​        task (Task): 任务实例,待恢复的任务。

​    Returns:
​        None
​    """
​    ​if self.find(task) != None:
        self._tasks[self.find(task)].resume()

def run(self, task: Task) -> None:
    """
​    执行任务回调函数。

​    Args:
​        task (Task): 任务实例,包含要执行的回调函数。

​    Returns:
​        None
​    """
​    ​if self.find(task) != None:
        task._rt = task._cnt
        self._run(task)

3.任务调度应用实验

以下实验中所有代码开源在我们提供的资料包中 elegance-devkit v1\Demo\23 TIMER_Scheduler 文件夹找到。

接下来,我们在 main.py 中完成我们的示例程序,首先导入相关的模块:

Python 复制代码
# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/7/11 上午9:43   
# @Author  : 李清水            
# @File    : main.py       
# @Description : 定时器类实验,使用定时器完成任务调度
# 代码参考:https://github.com/micropython-Chinese-Community/micropython-simple-scheduler/tree/main

# ======================================== 导入相关模块 ========================================

# 导入硬件相关的模块
from machine import Timer
# 导入任务调度器
from Scheduler import Scheduler, Task
# 导入时间模块
import time
# 垃圾回收的模块
import gc
# 导入系统相关的模块
import sys

然后定义一些定义了全局变量 time_start 用于记录任务开始时间,以及 RunCnt 用于记录回调函数执行次数:

Python 复制代码
# ======================================== 全局变量 ============================================

# 任务开始时间
time_start = time.ticks_us()
# 方法执行次数
RunCnt = 0

接着定义一个计时装饰器函数,用于测量程序运行时间:

Python 复制代码
# 计时装饰器,用于计算函数运行时间
def timed_function(f: callable, *args: tuple, **kwargs: dict) -> callable:
    """
​    计时装饰器,用于计算并打印函数/方法运行时间。

​    Args:
​        f (callable): 需要传入的函数/方法
​        args (tuple): 函数/方法 f 传入的任意数量的位置参数
​        kwargs (dict): 函数/方法 f 传入的任意数量的关键字参数

​    Returns:
​        callable: 返回计时后的函数
​    """
​    ​myname = str(f).split(' ')[1]

    def new_func(*args: tuple, **kwargs: dict) -> any:
        t: int = time.ticks_us()
        result = f(*args, **kwargs)
        delta: int = time.ticks_diff(time.ticks_us(), t)
        print('Function {} Time = {:6.3f}ms'.format(myname, delta / 1000))
        return result

    return new_func

然后定义了 task_callback 任务回调函数用于在每个任务执行时执行特定操作,函数接收一个参数 task_id,表示当前任务的 ID。函数主要运行逻辑为:

  1. 计算从任务开始时间到现在的时间差,并将其转换为毫秒
  2. 输出任务运行次数、任务运行时间和任务 ID
  3. 将任务运行次数加 1
  4. 根据任务运行次数执行不同的操作:
    1. 如果任务运行次数达到 5 次,暂停任务 2,创建一个新任务 3(每隔 1000 毫秒执行一次),将其添加到调度器中,并删除任务 1
    2. 如果任务运行次数达到 8 次,恢复任务 2
Python 复制代码
# 使用@timed_function装饰器,计算任务运行时间
@timed_function
def task_callback(task_id: int) -> None:
    """
​    任务回调函数,用于打印任务信息并管理任务的状态。

​    Args:
​        task_id (int): 任务ID,用于标识当前任务。

​    Returns:
​        None
​    """
​    ​global time_start, RunCnt, sc, task1, task2
    # 计算从起始时间到现在的时间差
    time_now = (time.ticks_us() - time_start) / 1000

    # 输出任务运行次数、任务运行时间和任务ID
    print('{} - {:.2f} ms: task {} is running'.format(RunCnt, time_now, task_id))

    # 任务运行次数加1
    RunCnt = RunCnt + 1

    # 判断任务运行次数
    if RunCnt == 5 :
        # 任务2暂停
        sc.pause(task2)
        print('pause task 2')
        # 创建任务3,1000ms执行一次
        task3 = Task(task_callback, 3, interval=1000, state=Task.TASK_RUN)
        # 添加任务3
        sc.add(task3)
        print('add task 3')
        # 删除任务1
        sc.delete(task1)
        print('delete task 1')

    # 判断任务运行次数
    if RunCnt == 8:
        # 任务2恢复
        sc.resume(task2)

然后定义了两个回调函数:task_idle_callback()task_err_callback()

  • ​空闲任务回调函数:​task_idle_callback 函数在调度器中没有任务需要执行时被调用。在这个函数中,首先检查当前可用堆 RAM 的字节数是否小于 230000。如果是,说明内存可能不足,因此手动触发垃圾回收功能(gc.collect()),以释放不再使用的内存。这个函数的作用是帮助系统在内存紧张时进行自我优化。
  • ​异常回调函数:​task_err_callback 函数在任务执行过程中发生异常时被调用。在这个函数中,使用 sys.print_exception(e) 打印异常的详细信息。然后,函数进入一个无限循环,不断打印"task run error",以便在出现异常时能够及时发现并处理问题。这个函数的作用是在任务执行出错时提供错误处理和调试信息。
Python 复制代码
# 空闲任务回调函数
def task_idle_callback() -> None:
    """
​    空闲任务回调函数,用于在内存不足时手动触发垃圾回收功能。

​    Args:
​        None

​    Returns:
​        None
​    """
​    ​# 当可用堆 RAM 的字节数小于 230000 时,手动触发垃圾回收功能
    if gc.mem_free() < 230000:
        # 手动触发垃圾回收功能
        gc.collect()

# 异常回调函数
def task_err_callback(e: Exception) -> None:
    """
​    异常回调函数,用于打印异常信息并处理任务错误。

​    Args:
​        e (Exception): 捕获到的异常对象。

​    Returns:
​        None
​    """
​    ​while True:
        sys.print_exception(e)
        print('task run error')

接着,我们创建任务和调度器,将任务添加到调度器后开启任务调度:

Python 复制代码
# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试信息
print("FreakStudio : Using Timer to implement a simple task scheduler")

# 创建任务1,500ms执行一次
task1 = Task(task_callback, 1, interval=500, state=Task.TASK_RUN)
# 创建任务2,1000ms执行一次
task2 = Task(task_callback, 2, interval=1000, state=Task.TASK_RUN)

# 创建任务调度器,定时周期为100ms
sc = Scheduler(Timer(-1), interval=100, task_idle=task_idle_callback, task_err=task_err_callback)

# 添加任务
sc.add(task1)
sc.add(task2)

# ========================================  主程序  ============================================

# 延时2s,等待烧录完成程序后打开终端
time.sleep(2)
# 开启调度
sc.scheduler()

烧录代码,打开终端我们可以看到任务按照设定的顺序运行:

任务 1、2、3 和空闲任务的执行时序图如下:

相关推荐
花 满 楼17 小时前
国芯科技CCFC3008PCS BIST (内建自测试) 分析总结
mcu·嵌入式·bist
虎王物联21 小时前
ESP32 FreeRTOS多任务优先级翻转与互斥量保护实战
物联网·嵌入式·esp32·freertos
与你4032 天前
CLion 中为什么要重写 _write,而不是 fputc?
嵌入式
Freak嵌入式2 天前
MicroPython 开发避坑:用 Signal 类解决不同电路的电平兼容问题
嵌入式
智码看视界3 天前
Edge AI 全栈1:ESP32 传感器采集到云端大模型推理的完整链路
人工智能·物联网·嵌入式·esp32·aiot·全栈开发·edgeai
Freak嵌入式3 天前
告别硬件差异!MicroPython Signal 类:让你的 GPIO 代码跨板运行无压力
嵌入式
v_for_van3 天前
C语言__attribute__
服务器·c语言·开发语言·mcu·嵌入式·嵌入式实时数据库
xx~t3 天前
嵌入式——进程与线程1
linux·c语言·学习·嵌入式·进程与线程
潘潘的嵌入式日记4 天前
协议旧指令该不该删?4 个检查决定删除还是保留兼容
重构·嵌入式·协议·经验·代码维护