一般情况下,处理 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 和需要优化的地方。

最后修改日期: 2021-05-14

留言

看起来很不错 通过什么原理实现的?

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

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

    编译时有没有错误信息之类的?能不能发一下错误信息或截图?应该不需要下载链接就行,GitHub 里面有 Download ZIP ( https://github.com/blanboom/Arduino-Task-Scheduler/archive/master.zip ),可以直接下载。

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

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

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

TaskScheduler.h: No such file or directory

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

怎么配置定时器修改单位啊

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

Torah Awesome 

感谢楼主的分享,不过我试着套用,发现有三个问题解决不了:1、Sch.addTask(lightUpdate,0,21600000,1); //从第 0 毫秒开始闪烁 LED,每隔 6h, LED 状态改变一次这里,我想把周期间隔的时间放到天这样的水平,但是尝试后发现实际效果不对。2、这句代码如果把间隔时间放大后,LED的闪烁时间极短,怎样改可以让LED持续一段时间再灭掉呢?3、发现在这个文件里面添加#include 以后,编译通不过,总会提示编译出错,但具体原因不明。

    1. 任务执行周期是 uint16_t (unsigned int) 型的,最大支持 65535. 不过可以修改定时器 1 的初值来修改时间单位,例如修改成每隔 5 毫秒中断一次。2. 是不是也是因为时间间隔超过 65535 导致的?不是的话能不能吧代码贴出来?3. 为了驱动舵机,需要通过定时器产生 PWM 波。servo 库在有些情况下也用到了定时器 1,导致两者冲突。这时候可能要通过其他方式来实现任务调度器或舵机驱动,或者通过两片 Arduino 配合使用。似乎评论系统出了点问题,一直没收到通知邮件,这么晚才回复,抱歉。

      tity_zhang 

      添加抢占式任务,会不会影响CPU 处理其他的任务呢? 干扰主程序的运行时间?

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

Cleiton Souza 

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?

    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.

      Cleiton Souza 

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

楼主 我用的是Arduino UNO 能使用您编写的这个任务调度器的库吗

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

宇文杨峰 

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

    任务 ID 为 uint8_t 类型。

      宇文杨峰 

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

        高优先级能打断低优先级任务,例如 B (低优先级) 在执行的过程中,检测到 A (高优先级) 需要执行,则先执行 A,当 A 执行完毕后,**继续**执行 B 的**剩余部分**。如果需要在 A 执行完之后,B 从头开始执行,我现在想到两种思路,还不知道可行性如何:1. 在 A 执行后,清除堆栈中的相关信息。这样,一旦 A 运行结束,就不会再接着执行 B. 此时可以通过一定的方式让 B 再次执行。2. 如果有执行完 A 后重新执行 B 的需要,我感觉有可能是你对 B 的执行时间/时序方面有较高的要求。可以在 B 的关键部分加上时间判断的语句,如果发现超时,则回到 B 的开头重新执行 B。或者在 B 执行到关键部分时,通过禁用中断等方法(当然,要考虑这些方法的副作用),防止任务被 A 抢占。————————————————————————————————不过,这只是一个简单的任务调度器,如果有比较复杂的需求,最好还是直接用操作系统。

宇文杨峰 

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

hi,你好!你的思路很好!但是我在使用时发现间隔时间如果长一点就完全不行了。比如#include // include this file to use this library#define PUMPPIN 2void setup(){ pinMode(PUMPPIN,OUTPUT); Sch.init(); // Initialize task schedulerSch.addTask(pump,0,3600000,1); Sch.start(); // Start the task scheduler}void loop(){ Sch.dispatchTasks();}void pump(){****}另外,官方有个ticker()的库好像和这个类似,但是没有优先级。不知道是不是?ticker()对于间隔时间好像也特别敏感。我还真有点犯愁对于多任务的长间隔时间怎样处理。有限状态机不是所有情况都适用的。

    可以改下与时间相关的变量/参数的数据类型,改成 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.

arduino 中,AD转换能否用中断?

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

牟老师 

arduino1.5.2;1.6.5都是如此报错,请教楼主Build options changed, rebuilding allTaskSchedulerSketch.ino:3:69: error: TaskScheduler.h: No such file or directoryTaskSchedulerSketch.ino: In function ‘void setup()’:TaskSchedulerSketch:14: error: ‘Sch’ was not declared in this scopeTaskSchedulerSketch.ino: In function ‘void loop()’:TaskSchedulerSketch:31: error: ‘Sch’ was not declared in this scope

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

      牟老师 

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

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

    两个库使用相同的定时器,无法同时使用。9 和 10 脚的 analogWrite() 也不能和这个库一起使用。

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

    用舵机的话,需要检查一下你用的舵机库,和这个库里的定时器是否冲突

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

程序:#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();

    我试了一下,是能编译通过的。可以确认一下这个库有没有放到合适的位置。 如果放到了合适的位置,编译输出里面会有如下字样: 使用库 TaskScheduler 在文件夹: /Users/blanboom/Documents/Arduino/libraries/TaskScheduler (legacy)

    我之前也出现了这个问题,是我用别的TaskScheduler库,名字都一样不知道为什么不能用。 解决办法:按楼主提供的连接下压缩包 https://github.com/blanboom/Arduino-Task-Scheduler/archive/master.zip ) 解压缩后将里面TaskScheduler文件夹复制到arduino IDE里的libraries文件夹里就行啦

我这个也不能编译通过,提示invalid use of void expression 我贴一下代码 https://shimo.im/docs/QjyKRV3rRt3y6GJC/ 《代码》,可复制链接后用石墨文档 App 或小程序打开

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

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

亲测arduino mega2586可用

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

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

      对的,这种情况可以拆分成两个任务,一个用于检查串口信息,一个用于闪烁。 平时使用的时候,尽可能把一个大任务拆分成多个独立的子任务就可以啦。

请问一下,最多可以有多少条线程,这个受哪些条件的制约?谢谢!

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

不知道为什么之前的评论发不出。。。我的程序中,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(” ℃ “); }

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

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

HI blanboom 老师,想请问这个项目能否挪到ESP8266上进行使用呢?

    目前只支持 Arduino Uno。适配 ESP8266 的话,需要修改 TaskScheduler.cpp 里面设置定时器和中断的部分。 不需要支持抢占式任务的话,也可以考虑使用这个调度器:https://github.com/arkhipenko/TaskScheduler

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。