Arduino 调度器






4.81/5 (8投票s)
一个在 Arduino 和 ESP32 板上事件驱动的函数
引言
本文旨在介绍在 Arduino IDE 环境中使用 Arduino 和 ESP32 板进行应用程序开发。这是之前文章的更新,反映了软件所做的更改。
这类卡的一个典型用途是控制传感器和/或通过无限循环的软件激活动作;下面是 Arduino 脚本(在 Arduino 术语中称为 sketch)的骨架。
// includes, defines and everywhere accessible variables
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
本文介绍了一个用于调度 Arduino 和 ESP32 板上活动的类:`loop` 函数包含对调度器的调用,开发人员只需描述事件、实例化类(在 `setup` 函数之前)并编写函数来处理事件。
背景
读者必须对自动化板中运行的程序的组织方式、所使用的语言(Arduino 的 C++ 方言)以及事件与相关动作的概念有所了解。
事件可以通过多种方式发生,其中最重要的是:
- 在每个定义的时间间隔
- 在预设的一天中的某个时间
- 根据命令
此外,事件可以是瞬时性的或持续性的,例如,瞬时事件可以是读取传感器数据,持续事件可以是泵的启动,该泵必须运行一段时间,或者传感器需要一些时间才能准备就绪。
其他可能性,例如在开始或一段时间后发生的事件,都可以轻松地作为上述事件的变体来实现。
handleEvents 类
该软件已改进并测试用于 Arduino 和 ESP32 板;特别是:
- 事件结构已简化
- 支持异常延迟
- 该软件可与节能技术配合使用
Using the Code
该程序基于一组由 C++ `结构体` 编码的 `事件` 和一个包含调度器的类(名为 `handleEvents`)。
下面是使用调度器的模板:
...
cronStructure cron[] = { // the events set
{...}, // some events
{0,0,0,0,NULL,NULL} // terminator
};
handleEvents events(cron); // instantiate the object events
...
events.setTime(day/2); // possibly set time (at twelve o'clock)
// here the functions for handle events
...
void setup() {
...
// events.begin(cron, pw_Clock); // needed if ESP32 can go in deep sleep
...
}
void loop() {
delay(events.croner(cron)); // handleCronEvents
}
类 `handleEvents` 包含函数 `croner`,该函数分析事件数组,并对于已达到激活时间的事件,执行其管理函数。
该类还包含一些函数:
clocker(false) | 以秒为单位给出已逝的时间和日期,由于是无符号 `long` 类型,它可以达到 136 年。 天数 = `clocker(false) / 86400` 从午夜开始的秒数 = `clocker(false) % 86400` |
setTime(time_of_day_seconds) | 设置时间,将值添加到同一天的 00:00:00 |
listEvents(cronStructure) | 显示事件(即当前状态) |
Time() | 以 HH:MM:SS+DD 格式显示时间 |
toSeconds("HH:MM:SS") | 将 HH:MM:SS 格式的时间转换为秒,秒和分钟可能缺失 |
每个 `event` 包含其处理信息(`cronStructure` 包含在类的 *.h 文件中)。
typedef void (* fnzPointer) (struct cronStructure *x);
struct cronStructure {
uint32_t every; // every seconds
uint32_t next; // next event
uint32_t duration; // for start and stop events (in seconds)
int8_t status; // if 1 bit 1: enabled, bit 2: active,
// bit 3: active ending, bit 4: manual, bit 5: at time
fnzPointer fnzAction;
const char* name;
};
所有时间都以秒表示。
- `every`:这是事件两次连续发生之间的时间间隔,`86400` 是一个每日事件。
- `next`:事件下一次发生的时间:当内部时钟等于或大于 `next` 时,将调用关联的 `action`;此字段的初始值表示启动延迟或有效的一天中的时间,具体取决于 `status` 字段的值。
- `duration`:事件持续时间,如果大于 `0`,则处理此事件的函数在持续时间到期时也会被调用,这允许控制动作的开始和结束。
- `status`:当以下位的值为 1 时,事件的状态和类型:
- 位 1 事件已启用
- 位 2 事件处于活动状态
- 位 3 具有持续时间的活动事件
- 位 4 手动事件
- 位 5 定时事件
- `action`:处理事件的函数
- `name`:事件名称
事件必须以数组形式排列,并以 `NULL` 事件终止。
cronStructure cron[] = {{...},{0,0,0,0,NULL,NULL}};
或
RTC_DATA_ATTR cronStructure cron[] = {{...},{0,0,0,0,NULL,NULL}};
第二种形式**仅适用于 ESP32 板**,用于将事件存储在 ULP 协处理器的内存中(在深度睡眠期间保留),因此当处理器进入功耗降低模式时,某些数据得以保留;这些数据存储在所有板的 `NULL` 事件终止器中。
NULL 事件字段 | 存储变量 | 含义 |
duration | ag_Clock | `handleEvents` 定时器 |
next | ag_delay | 调度器的延迟 |
every | ag_millis | `millis` 函数返回的值 |
状态 | 它必须是 `0`,当 `handleEvents` 被实例化时,它被设置为 `1`。 |
事件处理类有一组用于创建事件实例的常量:
EVENT_ENABLED | |
EVENT_AT_TIME | 用于在固定时间发生的事件 |
EVENT_MANUAL | 用于通过程序激活的事件 |
day | 为 `86400`,即每天的秒数 |
有两个宏用于管理事件:
EVENT_ENABLE(event)
EVENT_DISABLE(event)
还有一些用于测试 `status` 的内联函数:
IS_EVENT_ENABLED
IS_EVENT_ACTIVE
IS_EVENT_ENDING
IS_EVENT_MANUAL
IS_EVENT_AT_TIME
下面是一组事件的示例:
cronStructure cron[] = {
{2,0,0,EVENT_ENABLED,(fnzPointer) handle_command,"Serial"},
// Pump event starts 10 seconds after program start
{180,10,60,EVENT_ENABLED,(fnzPointer) handle_command,"Pump"},
{day*1.5,handleEvents::toSeconds("23:59:30"),60,
EVENT_ENABLED|EVENT_AT_TIME,(fnzPointer) handle_command,"Timed B"},
{day/12,handleEvents::toSeconds("0:00:30"),60,
EVENT_ENABLED|EVENT_AT_TIME,(fnzPointer) handle_command,"Timed A"},
// OnCommand event start when it is activate and it is recalled after 90 seconds
{0,0,90,EVENT_MANUAL,(fnzPointer) handle_command,"OnCommand"},
{0,0,0,EVENT_MANUAL,(fnzPointer) handle_command,"pointCmd"}, // start on command
{0,0,0,0,NULL,NULL} // terminator
};
工作原理
对于每个已启用的事件,调度器 `croner` 会检查 `next` 字段是否等于或小于内部时钟;在这种情况下,将调用关联的函数,并传入 `cronStructure` 事件作为参数。
函数调用后,`next` 字段会根据事件和事件类型进行更新:
- 瞬时事件:`every` 字段添加到 `next` 字段
- 持续性事件:开始时,`duration` 字段添加到 `next` 字段;结束时,`every` 字段与 `duration` 字段的差值添加到 `next` 字段。
调度器返回下一个事件发生所需的时间,该时间可用于延迟处理器。
技巧
持续性事件
持续性事件与设备启动相关,该设备必须在预设的时间内工作(或直到关闭);下面是设备激活的示例,该设备需要 10 秒才能准备就绪。
...
{120,50,10,EVENT_ENABLED,(fnzPointer)
handle_Condutt,"Siemens"}, // at start active the sensor
...
void handle_Condutt(cronStructure &event) {
sprintf(workBuffer,"%s %s ",Time(),event.name);
Serial.print(workBuffer);
if (IS_EVENT_ACTIVE(event)) {
digitalWrite(10,HIGH); // enabled
Serial.println("enabled");
} else {
int conducibilita=(analogRead(4));
digitalWrite(10,LOW); // disabled
...
}
设置和修改计时器
时间应在 `loop` 循环激活之前通过 `setTime(seconds)` 设置,其中 `seconds` 是从午夜开始的秒数,例如 `setTime(day/2)` 将计时器设置为中午十二点;当然,如果没有设置,时间将从午夜开始。
可以更改时间,主要是由于 `millis()` 的不精确性;这可能会对事件的管理产生影响,但是,`setTime` 函数会考虑已过去的天数来更改时间,并重新安排非预定义计划的事件时间,即状态位 `EVENT_AT_TIME` 设置为 `0` 的事件。`addTime(seconds)` 函数修改 `Timer` 而不影响调度器,当板在睡眠期间不更新时钟时,这很有用。
有限间隔的重复事件
在预设时间内以有限间隔发生的重复事件,例如,从中午十二点开始,每三分钟启动泵一分钟,持续一小时,可以使用两个事件来处理,如下面代码片段所示:
...
cronStructure cron[] = {
{180,10,60,0,(fnzPointer) handle_pumps,"Pump"},
{day,43200,3600,EVENT_ENABLED|EVENT_AT_TIME,(fnzPointer) handle_StartPump,""},
...
{0,0,0,0,NULL,NULL} // terminator
};
handleEvents events(cron);
...
void handle_StartPump(cronStructure &event) {
if (!IS_EVENT_ENDING(event)) {
EVENT_ENABLE(cron[events.searchEvent("Pump")]);
} else {
EVENT_DISABLE(cron[events.searchEvent("Pump")]);
}
}
void handle_pumps(cronStructure &event) {
sprintf(workBuffer, "%s %s %s", events.Time(),event.name,
IS_EVENT_ENDING(event)?"ended":"started");
Serial.println(workBuffer);
delay(150);
}
启用和禁用手动事件
启用手动事件只需将事件状态设置为 `EVENT_ENABLED`;要禁用,我们必须通过将 `next` 字段设置为当前时间来结束。
...
cronStructure cron[] = {
{0,0,30,EVENT_MANUAL,(fnzPointer) handle_command,"OnCommand"},
...
{0,0,0,0,NULL,NULL} // terminator
};
handleEvents events(cron);
...
iEvent = events.searchEvent((char *) "OnCommand");
if (!IS_EVENT_ENABLED(cron[iEvent])) {
EVENT_ENABLE(cron[iEvent]);
Serial.println("OnCommand enabled");
} else {
cron[iEvent].next = events.clocker(false); // force close
Serial.println("OnCommand disabled");
}
...
事件顺序
为了避免重复事件的重叠,可以通过在 `next` 字段中操作来错开激活它们;例如,以下两个事件每分钟激活一次,但第二个事件延迟 10 秒启动。
...
cronStructure cron[] = {
{60,0,0,EVENT_ENABLED,(fnzPointer)
handle_timelyEvent,"timelyEvent"}, // active every minute
// active every minute, starts 10 seconds after start
{60,10,10,EVENT_ENABLED,(fnzPointer) handle_durationEvent,
"durationEvent"},
{0,0,0,0,NULL,NULL}
}
其他事件类型
在启动时必须发生的事件可以正常声明,并且必须在管理它的函数中禁用它,请参见示例:
...
{0,5,0,1,(fnzPointer) handle_command,"Scan"}, // starts 5 seconds after start
...
void handle_command(cronStructure &event) {
EVENT_DISABLE(event);
Serial.Println("Arduino started");
}
...
演示及其他
SimCron 是一个演示 `handleEvents` 与一组重要事件的 sketch:
- 延迟启动事件
- 周期性事件
- 具有不同频率的定时事件
- 手动命令
该 sketch 可以通过串行接口进行管理,主要用于调查状态、激活手动事件和更改计时器。
在下载的文件中,您可以找到:
- Simcron sketch(演示)
- 一个使 Arduino 进入 IDLE 模式的 sketch
- 一个使 Arduino 进入 `POWER_DOWN` 模式的 sketch
- 一个用于查看 Arduino 串行(不重启卡)的 Powershell 脚本
- Powershell 脚本的“可执行文件”
- 文档
历史
- 2015 年 3 月 29 日:初始版本
- 2020 年 9 月 16 日:文章更新