[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 的多任务调度程序”的60个回复

    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 配合使用。

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

        1. 会影响,不过返回后主任务继续执行。
          使用这样的合作式调度器,需要合理设计每个任务的运行时间,尽量将一个大的任务分解成多个子任务来执行。避免一个任务占用过长时间。

  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. 把 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/sandeepmistry/esp8266-Arduino/blob/master/esp8266com/esp8266/libraries/Ticker/Ticker.cpp 程序里关于时间的数据类型是 uint32_t, 如果用的就是这个,应该没问题。或者如果时间间隔比较大时,在数字后面加上 UL, 例如 3600000UL.

    1. 可以用,需要直接操作 AVR 的寄存器。最好看看 AVR 的手册。

  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 均可使用。

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

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

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

  11. 我希望机械臂上的关节电机转角不同,希望同时运转,可以用这个函数吧,我把电机脉冲数信息全放到定义的led2Update函数的if语句里,为什么还是不同时转呢?

  12. 这个语句报错
    Sch.dispatchTasks();
    显示的是这个
    ‘Sch’ was not declared in this scope
    arduino版本1.8.8
    用的UNO

  13. 程序:#include “TaskScheduler.h” // include this file to use this library

    // the state of LEDs
    boolean g_led1State=1;
    boolean g_led2State=0;

    void setup()
    {
    pinMode(13,OUTPUT);
    pinMode(12,OUTPUT);

    Sch.init(); // Initialize task scheduler

    /*
    * use Sch.addTask(task, start_time, period, priority) to add tasks
    * task – tasks to be scheduled
    * start_time – when the task starts (ms)
    * period – repeat period of the task (ms)
    * priority – 1: mormal priority, 0: high priority
    */
    Sch.addTask(led1Update,0,1000,1); //从第 0 毫秒开始闪烁 LED,每隔 1s, LED 状态改变一次
    Sch.addTask(led2Update,20,500,1); //从第 20 毫秒开始闪烁 LED,每隔 0.5s, LED 状态改变一次

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

    void loop()
    {
    Sch.dispatchTasks();
    }

    // Put task to be scheduled below

    // Blink LED on pin 13
    void led1Update()
    {
    if(g_led1State==0)
    {
    g_led1State=1;
    digitalWrite(13,HIGH);
    }
    else
    {
    g_led1State=0;
    digitalWrite(13,LOW);
    }
    }

    // Blink LED on pin 12
    void led2Update()
    {
    if(g_led2State==0)
    {
    g_led2State=1;
    digitalWrite(12,HIGH);
    }
    else
    {
    g_led2State=0;
    digitalWrite(12,LOW);
    }
    }
    报错:
    exit status 1
    ‘Sch’ was not declared in this scope
    报错代码:Sch.dispatchTasks();

    1. 我试了一下,是能编译通过的。可以确认一下这个库有没有放到合适的位置。

      如果放到了合适的位置,编译输出里面会有如下字样:

      使用库 TaskScheduler 在文件夹: /Users/blanboom/Documents/Arduino/libraries/TaskScheduler (legacy)

    1. 我把 FastLED.h,以及相关代码删掉是可以编译通过的。建议再检查下 FastLED.h 相关的 lib 使用方式是否正确。

  14. 我用了五个LED灯,发现有两个LED灯有问题,其中一个常亮,一个不亮。不知道怎么回事,代码完全是按照你原有的代码扩展的。

  15. 从第几秒开始这个参数只能是写好的吗?,我发现只能在编译或上电后计算几秒后开始闪烁。我想接收串口信息后再开始闪烁应该怎么实现呢?我尝试了将Sch.addTask和Sch.start放到loop里的判断句里,结果没有反应

    1. 今天看了一个 学长的代码,他把循环检查信号放进一个task,就可以实现信号进来后才开始闪烁,耶耶耶

      1. 对的,这种情况可以拆分成两个任务,一个用于检查串口信息,一个用于闪烁。

        平时使用的时候,尽可能把一个大任务拆分成多个独立的子任务就可以啦。

    1. 各个任务都是保存在数组中的,修改这个宏就能修改支持任务的数量:MAX_TASKS

  16. 不知道为什么之前的评论发不出。。。我的程序中,DHT11读取到的温湿度数据会被多个合作式任务用到,所以我的DHT11读取温湿度的任务需要使用抢占式,不然DHT11的温湿度刷新率会被其他合作式任务因为Delay而变得巨慢。但是我将DHT读取温湿度的任务使用抢占式时,其输出结果一直是nan,也就是无法读取到DHT11的数据,而使用合作式时是正常读取的。下面是使用抢占式的DHT11代码,麻烦楼主帮忙看看!

    #include
    #define DHTPIN 24//定义针脚
    #define DHTTYPE DHT11//定义类型,DHT11或者其它
    DHT dht(DHTPIN, DHTTYPE);//进行初始设置

    #include “TaskScheduler.h” //包含此头文件,才能使用调度器
    void setup() {
    Sch.init(); //初始化调度器
    Sch.addTask(led1Update,0,1000,0);
    Sch.start();//启动调度器
    Serial.begin(9600);
    dht.begin(); //DHT开始工作
    }

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

    #include
    #define DHTPIN 24//定义针脚
    #define DHTTYPE DHT11//定义类型,DHT11或者其它
    DHT dht(DHTPIN, DHTTYPE);//进行初始设置
    #include “TaskScheduler.h” //包含此头文件,才能使用调度器

    void setup() {
    Sch.init(); //初始化调度器
    Sch.addTask(dhtvalue,0,1000,0);
    Sch.start();//启动调度器
    Serial.begin(9600);
    dht.begin(); //DHT开始工作
    }

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

    void dhtvalue(){
    //delay(2000); // 两次检测之间,要等几秒钟,这个传感器有点慢。
    // 读温度或湿度要用250毫秒
    float h = dht.readHumidity();//读湿度
    float t = dht.readTemperature();//读温度,默认为摄氏度
    Serial.print(“Humidity: “);//湿度
    Serial.println(h);
    Serial.print(“Temperature: “);//温度
    Serial.print(t);
    Serial.println(” ℃ “);
    }

    1. 抢占式任务是直接在中断中运行的。可能你用的 DHT11 库用到了硬件定时器或者中断。可以试下这个:https://github.com/adidax/dht11
      不过一般情况下,中断里面的代码最好不要执行太长时间,否则会对其他任务有影响。如果要执行的任务比较多,又对任务的实时性有要求,建议还是自己写读取 DHT11 的代码,避免软件延迟占用太多时间。

      1. 你好 我尝试了一下新的库文件也不行,当时我用同样的操作试了一下MQ135传感器,MQ135传感器读取数据的程序以抢占模式运行,其输出值能直接作为全局变量给其他合作模式下的程序使用。应该是DHT的问题,我尝试下iic协议下的sht系列下。谢谢解答!

    1. 目前只支持 Arduino Uno。适配 ESP8266 的话,需要修改 TaskScheduler.cpp 里面设置定时器和中断的部分。

      不需要支持抢占式任务的话,也可以考虑使用这个调度器:https://github.com/arkhipenko/TaskScheduler

发表回复

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

退出移动版