65.9K
CodeProject 正在变化。 阅读更多。
Home

Arduino 调度器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (8投票s)

2015年3月29日

CPOL

7分钟阅读

viewsIcon

34946

downloadIcon

764

一个在 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++ 方言)以及事件与相关动作的概念有所了解。
事件可以通过多种方式发生,其中最重要的是:

  1. 在每个定义的时间间隔
  2. 在预设的一天中的某个时间
  3. 根据命令

此外,事件可以是瞬时性的或持续性的,例如,瞬时事件可以是读取传感器数据,持续事件可以是泵的启动,该泵必须运行一段时间,或者传感器需要一些时间才能准备就绪。

其他可能性,例如在开始或一段时间后发生的事件,都可以轻松地作为上述事件的变体来实现。

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:

  1. 延迟启动事件
  2. 周期性事件
  3. 具有不同频率的定时事件
  4. 手动命令

该 sketch 可以通过串行接口进行管理,主要用于调查状态、激活手动事件和更改计时器。

在下载的文件中,您可以找到:

  1. Simcron sketch(演示)
  2. 一个使 Arduino 进入 IDLE 模式的 sketch
  3. 一个使 Arduino 进入 `POWER_DOWN` 模式的 sketch
  4. 一个用于查看 Arduino 串行(不重启卡)的 Powershell 脚本
  5. Powershell 脚本的“可执行文件”
  6. 文档

历史

  • 2015 年 3 月 29 日:初始版本
  • 2020 年 9 月 16 日:文章更新
© . All rights reserved.