是否在玩arduino过程中出现按键控制带来不灵敏问题,是否在为只有一个循环loop()而烦恼,不否认可以使用中断解决问题,但我觉得,多任务处理起来更香。
本文将介绍arduino协作多任务的轻量级实现,让arduino实现类似操作系统(比如FreeRTOS、uC/OS-II)般的任务调度功能,不再尴尬倮奔。延时带来的不灵敏、数据刷新(比如温湿度、光照强度等数据同时实时获取)等问题迎刃而解。
定期任务执行,动态执行周期以毫秒(默认值)或微秒(如果明确启用)-执行频率
迭代次数(有限或无限次迭代)
按预定顺序执行任务
任务执行参数的动态变化(频率、迭代次数、回调方法)
在未计划运行任务时,通过进入空闲睡眠模式来节省电源
通过状态请求对象支持事件驱动的任务调用
支持任务ID和错误处理控制点以及看门狗定时器
支持本地任务存储指针(允许对多个任务使用相同的回调代码)
支持分层任务优先级划分
支持std::函数(仅限ESP8266和ESP32)
支持总体任务超时
任务调度功能适用于Arduino Uno R3、Arduino Nano、ESP8266、ESP32等。每个调度过程的开销在15us~18us,属于单调度。
每个任务都通过回调方法执行其功能。调度器定期调用任务的回调方法,直到任务被禁用或迭代结束。除了“常规”回调方法外,每个任务还可以使用另外两种方法:一种是每次启用任务时调用的回调方法,另一种是在禁用任务时调用的回调方法。这两种特殊方法允许任务正确启动以执行,并在执行结束后进行清理(例如,在启用时设置管脚模式,并始终在结束时将管脚级别降低)。
任务通过成为好邻居来支持协同多任务处理,即以非阻塞方式快速运行其回调方法,并尽快将控制权释放回调度程序。
调度程序按照任务添加到链中的顺序从头到尾执行任务的回调方法。调度器在处理链一次后停止并存在,以便允许loop()方法的主代码中的其他语句运行。这被称为“计划通行证”。通常,除了调度程序的execute()方法外,loop()方法中不需要任何其他语句。相当于loop()里只有execute()。如下所示:
void loop () {
runner.execute();
}
这个execute()让我想到uC/OS-II的OSStart()。说这么多大家肯定是一头雾水没啥概念,直接上案例吧,细品。
创建两个任务,分别是t1和t2。
t1每隔两秒执行一次打印,执行10次后自我毁灭(销毁任务)。
t2每隔3秒执行一次打印,不会自我销毁。
如果t1任务创建成功,在t1里边创建任务t3,t3每5秒执行一次打印。
在第二点里说了,t1执行10次后销毁。之后让任务t3也跟着销毁,同时让仅剩的任务t2变为0.5秒执行一次打印,一直下去。
Arduino使用的库是TaskScheduler.h,库压缩包已上传到资源,可前往下载。
库使用的编程语言基本是C++,把功能都封装起来,有各种类以及成员提供给arduino的IDE里去编程调用。关于C++内容,可以参考该系列内容 C++学习.
其中主要的类有两个:Task和Scheduler
Task负责任务初始化(定义任务的一些特性)。
案例要求有三个任务,分别是t1、t2、t3,t1是只能执行10次的自我销毁型任务,t2和t3是长久型任务。在类Task里有个成员原型是:
#ifdef _TASK_OO_CALLBACKS
Task::Task( unsigned long aInterval, long aIterations, Scheduler* aScheduler, bool aEnable ) {
reset();
set(aInterval, aIterations);
#else
Task::Task( unsigned long aInterval, long aIterations, TaskCallback aCallback, Scheduler* aScheduler, bool aEnable, TaskOnEnable aOnEnable, TaskOnDisable aOnDisable ) {
reset();
set(aInterval, aIterations, aCallback, aOnEnable, aOnDisable);
#endif
if (aScheduler) aScheduler->addTask(*this);
#ifdef _TASK_WDT_IDS
iTaskID = ++__task_id_counter;
#endif // _TASK_WDT_IDS
if (aEnable) enable();
}
所以可以通过类Task创建该成员对象,参数分别是(延时ms,循环次数,回调函数)
Task t1(2000, 10, &t1Callback);//延时2s,重复10次,回调函数名为t1Callback
Task t2(3000, TASK_FOREVER, &t2Callback);//延时3s,永远重复,回调函数名为t2Callback
Task t3(5000, TASK_FOREVER, &t3Callback);//延时5s,永远重复,回调函数名为t3Callback
这里的回调函数就是用来实现功能的函数,定义成void t1Callback、void t2Callback、void t3Callback
void t1Callback() {
Serial.print("t1: ");
Serial.println(millis());//millis()函数返回开发板运行程序的毫秒数数值,该数值约50天后溢出而从零开始。
}
Scheduler负责处理任务(任务添加、删除等)。
所以先用类声明对象(本例对象名为runner)
Scheduler runner;
然后根据对象去访问类成员函数,有
runner.init();
runner.addTask(name);//添加name任务,name为上面的t1或t2或t3
runner.deleteTask(name);
runner.execute();
除了以上这些,函数句柄还有disableAll、currentTask、allowSleep等。
#include
// Callback methods prototypes
void t1Callback();
void t2Callback();
void t3Callback();
//Tasks (delay_ms,times,func)
Task t4();
Task t1(2000, 10, &t1Callback);
Task t2(3000, TASK_FOREVER, &t2Callback);
Task t3(5000, TASK_FOREVER, &t3Callback);
Scheduler runner;
void t1Callback() {
Serial.print("t1: ");
Serial.println(millis());
if (t1.isFirstIteration()) {
runner.addTask(t3);
t3.enable();
Serial.println("t1: enabled t3 and added to the chain");
}
if (t1.isLastIteration()) {
t3.disable();
runner.deleteTask(t3);
t2.setInterval(500);
Serial.println("t1: disable t3 and delete it from the chain. t2 interval set to 500");
}
}
void t2Callback() {
Serial.print("t2: ");
Serial.println(millis());
}
void t3Callback() {
Serial.print("t3: ");
Serial.println(millis());
}
void setup () {
Serial.begin(115200);
Serial.println("Scheduler TEST");
runner.init();
Serial.println("Initialized scheduler");
runner.addTask(t1);
Serial.println("added t1");
runner.addTask(t2);
Serial.println("added t2");
t1.enable();
Serial.println("Enabled t1");
t2.enable();
Serial.println("Enabled t2");
}
void loop () {
runner.execute();
}
执行结果:
Scheduler TEST
Initialized scheduler
added t1
added t2
Enabled t1
Enabled t2
t1: 1
t1: enabled t3 and added to the chain
t2: 5
t3: 6
t1: 2000
t2: 3000
t1: 4000
t3: 5002
t1: 6000
t2: 6000
t1: 8000
t2: 9000
t1: 10000
t3: 10002
t1: 12000
t2: 12000
t1: 14000
t2: 15000
t3: 15002
t1: 16000
t2: 18000
t1: 18000
t1: disable t3 and delete it from the chain. t2 interval set to 500
t2: 18500
t2: 19000
t2: 19500
t2: 20000
t2: 20500
t2: 21000
t2: 21500
t2: 22000
t2: 22500
t2: 23000
t2: 23500
t2: 24000
t2: 24500
t2: 25000
t2: 25500
结果分析:
Scheduler TEST ------------------->从void setup()开始,Serial.println("Scheduler TEST");
Initialized scheduler --------------->Serial.println("Initialized scheduler");
added t1 ------------------>Serial.println("added t1");
added t2
Enabled t1 ------------------>Serial.println("Enabled t1");t1被激活,跳去执行t1Callback()
Enabled t2
t1: 1 ------------------>t1Callback()函数里的Serial.println(millis());时间数值代表毫秒
t1: enabled t3 and added to the chain
----->Serial.println("t1: enabled t3 and added to the chain");
t2: 5 -------------->t2抢在了t3前面显示,说明t1Callback()还没执行完,证明了多任务的存在
t3: 6
t1: 2000 -------------->现在才第二秒刚结束,于是t1打印
t2: 3000 -------------->t2是隔三秒打印一次
t1: 4000
t3: 5002 -------------->t3是隔五秒
t1: 6000
t2: 6000
t1: 8000
t2: 9000
t1: 10000
t3: 10002
t1: 12000
t2: 12000
t1: 14000
t2: 15000
t3: 15002
t1: 16000
t2: 18000
t1: 18000 -------------->t1从0、2、...到18共执行了10次,然后自我销毁
t1: disable t3 and delete it from the chain. t2 interval set to 500
------------->t3跟着t1销毁,然后t2变为每半秒打印一次
t2: 18500
t2: 19000
t2: 19500
t2: 20000
t2: 20500
t2: 21000
t2: 21500
t2: 22000
t2: 22500
t2: 23000
t2: 23500
t2: 24000
t2: 24500
t2: 25000
t2: 25500 -------------->后续会一直剩下t2不断半秒循环下去
案例总结:
这个例子已经把基本的任务框架和执行流程体现出来,多任务最简单直接方式理解,好处就是提供多个循环,然后各循环执行自己的功能,相当于存在多个while()死循环体。而且有些情况下还可以实现定义执行几次,比较常规,和for循环类似