一般情况下,处理 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 和需要优化的地方。
看起来很不错 通过什么原理实现的?
通过定时器产生中断,每隔一毫秒时间加一,并检查一下当前时间有没有任务需要运行。
我试了多次,编译时出错,能不能把库打包一下,做个下载连接,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 ,下面的内容就是
感谢楼主的分享,不过我试着套用,发现有三个问题解决不了:
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 配合使用。
似乎评论系统出了点问题,一直没收到通知邮件,这么晚才回复,抱歉。
添加抢占式任务,会不会影响CPU 处理其他的任务呢? 干扰主程序的运行时间?
会影响,不过返回后主任务继续执行。
使用这样的合作式调度器,需要合理设计每个任务的运行时间,尽量将一个大的任务分解成多个子任务来执行。避免一个任务占用过长时间。
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.
Ok, thanks for your quick reply and the tip. I will reasearch more about RTOS on Teensy.
楼主 我用的是Arduino UNO 能使用您编写的这个任务调度器的库吗
UNO 可以用
为什么我同样是Arduino UNO头文件调用不了?
把 TaskScheduler 文件夹放入 Arduino 的 libraries 文件夹里,经测试,Arduino 1.6.5 可以正常编译。
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 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()对于间隔时间好像也特别敏感。我还真有点犯愁对于多任务的长间隔时间怎样处理。有限状态机不是所有情况都适用的。
可以改下与时间相关的变量/参数的数据类型,改成 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 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
需要把 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灯有问题,其中一个常亮,一个不亮。不知道怎么回事,代码完全是按照你原有的代码扩展的。
可以在 Gist 或者 Pastbin.com 上贴一下现在的代码,我看下出什么原因。
亲测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