[Arduino 库] 适用于 Arduino Uno 的多任务调度程序

一般情况下,处理 Arduino 的多个任务,是把所有任务放在 void loop() 里,然后用 delay() 控制时间。不过,任务一多,这种方法就不太方便了。

最近刚刚看了一本书:《时间触发嵌入式系统设计模式》,里面介绍的调度器,可以以特定的周期执行特定的任务,值得在 Arduinio 项目中借鉴。我也刚刚把这个调度器移植到 Arduino 中:https://github.com/blanboom/Arduino-Task-Scheduler

基本使用方法

这是一个使用调度器的例子,各个函数的功能都已在注释中标出:

// Arduino 任务调度器 演示程序
// Created by Blanboom
// 2013.7.27
// https://blanboom.org

#include "TaskScheduler.h"  //包含此头文件,才能使用调度器

// 用于储存 LED 状态
boolean g_led1State=1;
boolean g_led2State=0;

void setup()
{
    // 第12、13脚接有 LED
    pinMode(13,OUTPUT);
    pinMode(12,OUTPUT);

    Sch.init(); //初始化调度器

    //向调度器中添加任务
    //第一个参数为要添加任务的函数名
    //第二个参数为任务第一次执行的时间,
    //    合理设置有利于防止任务重叠,有利以提高任务执行的精度
    //第三个参数是任务执行的周期
    //第二、三个参数的单位均为毫秒,也可配置定时器修改其单位
    //第四个参数代表任务是合作式还是抢占式
    //    一般取1就可以,更多用法请参考下文
    Sch.addTask(led1Update,0,1000,1);  //从第 0 毫秒开始闪烁 LED,每隔 1s, LED 状态改变一次
    Sch.addTask(led2Update,20,500,1);  //从第 20 毫秒开始闪烁 LED,每隔 0.5s, LED 状态改变一次

    Sch.start();//启动调度器
}

void loop()
{
    Sch.dispatchTasks();  // 执行被调度的任务,用调度器时放上这一句即可
}

// 把要调度的任务函数放下面

// 闪烁第 13 脚的 LED
void led1Update()
{
    if(g_led1State==0)
    {
        g_led1State=1;
        digitalWrite(13,HIGH);
    }
    else
    {
        g_led1State=0;
        digitalWrite(13,LOW);
    }
}

// 闪烁第 12 脚的 LED
void led2Update()
{
    if(g_led2State==0)
    {
        g_led2State=1;
        digitalWrite(12,HIGH);
    }
    else
    {
        g_led2State=0;
        digitalWrite(12,LOW);
    }
}

程序执行后,两个 LED 分别会以程序中指定的周期和时间闪烁。

更多功能

1. 添加抢占式任务

抢占式任务,简单说,就是优先级比正常任务(合作式任务)高的任务。在这个调度器中,抢占式任务可以打断正常任务,优先执行。

对于一些对时间精度要求较高的任务,可以将任务模式改为抢占式。

修改方法:

在添加任务的函数 Sch.addTask(任务名称,开始时间,执行周期,1) 函数中,将最后一个参数由 1 改为 0,即:

Sch.addTask(任务名称,开始时间,执行周期,1)

这样,该任务就成了抢占式任务。

2. 添加单次执行的任务

可以添加只执行一次的任务,在一段时间后执行。

只需把 Sch.addTask(任务名称,开始时间,执行周期,1) 中的执行周期改为 0 即可。

3. 删除任务

使用函数 Sch.addTask(任务名称,开始时间,执行周期,1) 时,会返回这个任务的 ID,将这个 ID 赋给一个变量。需要删除任务时,用删除任务函数 Sch.deleteTask(任务ID) ,就能把任务删除。

4. 调整被调度的任务数量

打开 TaskScheduler.h,找到 #define MAX_TASKS (10) ,将 10 修改为需要被调度的任务的数量。

5. 自动进入空闲模式

这个调度器能在没有任务的情况下自动进入空闲模式,以节省电量。不需要对程序进行其他修改。

6. 错误报告

打开 TaskScheduler.h,找到

//#define REPORT_ERRORS // Remove “//” to enable error report,

将前面的 // 去掉,打开错误报告功能。

然后,这条语句的下面,定义了相关错误代码,可根据情况修改。

最后,打开 TaskScheduler.cpp,找到函数 void Schedule::_reportStatus(void),在里面添加合适的错误报告代码即可。

欢迎大家对这个调度器进行测试,找出 bug 和需要优化的地方。

“[Arduino 库] 适用于 Arduino Uno 的多任务调度程序”的37个回复

    1. 通过定时器产生中断,每隔一毫秒时间加一,并检查一下当前时间有没有任务需要运行。

  1. 我试了多次,编译时出错,能不能把库打包一下,做个下载连接,1.5.5版本里有一个Scheduler编译时一样提示错误。

    1. 1.5.5 的 Scheduler 只能在 Arduino Due 上使用,好像 AVR 芯片的 Arduino 还不能用。

  2. 在TaskScheduler.cpp里引用的是Scheduler.h,改成TaskScheduler.h就可以用了,可能是版主没改到吧!

    1. 谢谢提醒。
      Arduino 新版也有让 Arduino DUE 用的 Scheduler 库,头文件也叫 Scheduler.h,为了防止重名,就把文件名改了。
      结果忘记改源码了…

    1. 检查一下 TaskScheduler.cpp 和 TaskScheduler.h 是不是已经放在了正确的位置。

    1. 在 TaskScheduler.cpp 中找到 //Set up timer1. 1ms per interrupt ,下面的内容就是

  3. 感谢楼主的分享,不过我试着套用,发现有三个问题解决不了:

    1、Sch.addTask(lightUpdate,0,21600000,1); //从第 0 毫秒开始闪烁 LED,每隔 6h, LED 状态改变一次

    这里,我想把周期间隔的时间放到天这样的水平,但是尝试后发现实际效果不对。
    2、这句代码如果把间隔时间放大后,LED的闪烁时间极短,怎样改可以让LED持续一段时间再灭掉呢?
    3、发现在这个文件里面添加#include 以后,编译通不过,总会提示编译出错,但具体原因不明。

    1. 1. 任务执行周期是 uint16_t (unsigned int) 型的,最大支持 65535. 不过可以修改定时器 1 的初值来修改时间单位,例如修改成每隔 5 毫秒中断一次。

      2. 是不是也是因为时间间隔超过 65535 导致的?不是的话能不能吧代码贴出来?

      3. 为了驱动舵机,需要通过定时器产生 PWM 波。servo 库在有些情况下也用到了定时器 1,导致两者冲突。这时候可能要通过其他方式来实现任务调度器或舵机驱动,或者通过两片 Arduino 配合使用。

      似乎评论系统出了点问题,一直没收到通知邮件,这么晚才回复,抱歉。

  4. Hello Blanboom, My name is Cleiton, I’m from Brazil and I want thanks a lot for your sharing! Your library is simple and very useful!
    One Question, Would I need some modification to use it on Teensy 3.1?

    1. The library uses some special registers on AVR CPU to set up timer and interrupt. However, Teensy 3.1 is based on a Freescale Cortex-M4 Microcontroller. So, to use this library on it, additional changes are needed.

      Cortex-M4 is much more powerful than AVR, you can also consider using an RTOS.

      1. Ok, thanks for your quick reply and the tip. I will reasearch more about RTOS on Teensy.

      1. 把 TaskScheduler 文件夹放入 Arduino 的 libraries 文件夹里,经测试,Arduino 1.6.5 可以正常编译。

  5. Bingo.这个调度器 确实很简单实用,之前在一些单片机系统里面用,还想着我也试试写一下,看来源码,发现“志同道合”呀。

  6. 如何删除调度任务没有太看懂,请问你提到的会返回ID是什么数据类型呢?就删除调度任务能否举个例子。谢谢。

      1. 我想向您询问:举例说明,有两个任务函数,A任务优先级0(高),B任务优先级1(相对较低)。如果在执行A任务的时候,B任务的开启时间已到,那么会不会从A任务跳出呢?
        建议2:可否加入一个指令,比如从B任务跳到执行A任务,A任务执行完后再回到B任务时候能否从B任务的从头开始执行。

        1. 高优先级能打断低优先级任务,例如 B (低优先级) 在执行的过程中,检测到 A (高优先级) 需要执行,则先执行 A,当 A 执行完毕后,**继续**执行 B 的**剩余部分**。

          如果需要在 A 执行完之后,B 从头开始执行,我现在想到两种思路,还不知道可行性如何:

          1. 在 A 执行后,清除堆栈中的相关信息。这样,一旦 A 运行结束,就不会再接着执行 B. 此时可以通过一定的方式让 B 再次执行。
          2. 如果有执行完 A 后重新执行 B 的需要,我感觉有可能是你对 B 的执行时间/时序方面有较高的要求。可以在 B 的关键部分加上时间判断的语句,如果发现超时,则回到 B 的开头重新执行 B。或者在 B 执行到关键部分时,通过禁用中断等方法(当然,要考虑这些方法的副作用),防止任务被 A 抢占。

          ————————————————————————————————

          不过,这只是一个简单的任务调度器,如果有比较复杂的需求,最好还是直接用操作系统。

  7. 纠正,您再3.删除任务说明:Sch.DeleteTask(任务ID)
    实际库函数中是:boolean deleteTask(uint_8)
    所以正确调用删除线程是Sch.deleteTask(任务ID)

  8. hi,你好!你的思路很好!但是我在使用时发现间隔时间如果长一点就完全不行了。比如

    #include // include this file to use this library

    #define PUMPPIN 2

    void setup()

    {

    pinMode(PUMPPIN,OUTPUT);

    Sch.init(); // Initialize task scheduler

    Sch.addTask(pump,0,3600000,1);

    Sch.start(); // Start the task scheduler

    }

    void loop()

    {

    Sch.dispatchTasks();

    }

    void pump(){

    ****

    }

    另外,官方有个ticker()的库好像和这个类似,但是没有优先级。不知道是不是?ticker()对于间隔时间好像也特别敏感。我还真有点犯愁对于多任务的长间隔时间怎样处理。有限状态机不是所有情况都适用的。

    1. 可以改下与时间相关的变量/参数的数据类型,改成 uint32_t 应该就可以,最大支持 4294967295. 我现在用的是 uint16_t, 最大支持 65535.

      有没有 ticker() 的链接?在网上只找到了这个:https://github.com/sandeepm… 程序里关于时间的数据类型是 uint32_t, 如果用的就是这个,应该没问题。或者如果时间间隔比较大时,在数字后面加上 UL, 例如 3600000UL.

  9. arduino1.5.2;1.6.5都是如此报错,请教楼主

    Build options changed, rebuilding all

    TaskSchedulerSketch.ino:3:69: error: TaskScheduler.h: No such file or directory

    TaskSchedulerSketch.ino: In function ‘void setup()’:

    TaskSchedulerSketch:14: error: ‘Sch’ was not declared in this scope

    TaskSchedulerSketch.ino: In function ‘void loop()’:

    TaskSchedulerSketch:31: error: ‘Sch’ was not declared in this scope

    1. 需要把 TaskScheduler 文件夹放到 Arduino 的 libraries 文件夹。经过我测试,Arduino 1.0, 1.5, 1.6 均可使用。

      1. 可以了。呵呵,下载了库直接解压全部拖进去了。现在好了,改过来了

  10. 为甚么和舵机库一起编译就会报错:collect2.exe: error: ld returned 1 exit status,和舵机库的参数TIMER1_COMPA_vect 是重的

    1. 两个库使用相同的定时器,无法同时使用。

      9 和 10 脚的 analogWrite() 也不能和这个库一起使用。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

单击“Verify me”,验证后即可评论(了解更多
验证码加载中......
如果验证码无法加载,请关闭广告过滤软件,或打开浏览器中的 JavaScript