通过适应PLC技术掌握非阻塞Arduino延迟

我欣赏Arduino Opta可编程逻辑控制器(PLC)的原因之一是它提供了探索不同编程语言之间关系的机会。例如,在这篇文章中,我们将探讨涉及类对象实例化的梯形逻辑与C++编程之间的关系。具体来说,我们将对比梯形逻辑基于定时器的简单性与在Arduino环境中实现非阻塞延时的复杂方法。

我们将从经典的Arduino阻塞延时开始,接着介绍类似PLC定时器的C++类实现,然后展示相比之下更易于使用的梯形逻辑实现。您将看到梯形逻辑实现隐藏了大量复杂性,从而实现了易于实现的逻辑,能够响应实时信号。

虽然梯形逻辑实现是Opta和其他符合IEC61131-3标准的PLC所独有的,但非阻塞代码可能对所有程序员和微控制器家族都感兴趣,而不仅仅是Arduino。

阻塞代码回顾

您还记得您的第一个Arduino程序吗?很可能它涉及使用类似以下代码结构来闪烁LED:

#define LED_PIN 13

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  delay(1000);
  digitalWrite(LED_PIN, LOW);
  delay(2000);
}

对于成千上万的Arduino程序员来说,这是一个很好的起点。然而,这段代码有一个重大缺陷。它完全不适合现实世界的控制接口。这个问题被称为阻塞代码

每次调用delay()函数时,微控制器在延时期间都是“盲”的。例如,如果我们想响应开关或传感器,我们需要等待delay()完成。在这个特定的例子中,我们可能需要等待长达三秒钟。这对于控制现实世界设备的机器来说是完全不可接受的。

非阻塞代码的重要性

为了解决这个问题并扩展您的Arduino知识,您可能使用了millis()函数。回想一下,millis()就像一个时钟。它提供了自Arduino启动以来经过时间的合理估计。对于非阻塞定时器,我们需要一些变量来保存特定的时间实例。有了这些变量,我们就可以监控时间的流逝。

这类似于闹钟,我们确定事件必须在06:45:30发生。当我们在循环中旋转时,我们只需询问当前时间是否大于07:45:30。如果是,我们就做一些事情。如果不是,我们快速退出并继续循环。整个过程非常快,允许微控制器快速迭代循环,提供一个对实时事件响应迅速的系统。典型的程序每秒会循环数千次,从而确保对现实世界的快速响应。

技术提示 :这种不断循环的方法提供了合理的现实世界响应。这种轮询方法无法提供精确的计时。为了更高的准确性和减少延迟,请考虑使用基于中断的延迟。

以下代码执行相同的LED闪烁操作,但不会阻塞。注意使用状态变量来跟踪LED是打开还是关闭。还要注意代码保留了两个计时器操作。一个用于一秒的开启持续时间,另一个用于两秒的关闭持续时间。


#define LED_PIN 13
#define LED_ON HIGH
#define LED_OFF LOW
#define ON_INTERVAL 1000
#define OFF_INTERVAL 2000

bool ledState = LED_OFF;
unsigned long elapsedTime = 0;
unsigned long startTime = 0;          // Global variable: retains value across loop( ) iterations

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, ledState);
  startTime = millis();
}

void loop() {
  unsigned long loopStartTime = millis();
  elapsedTime = loopStartTime - startTime;

  // OFF Logic
  if ((ledState == LED_OFF) && (elapsedTime >= OFF_INTERVAL)) {
    ledState = LED_ON;
    digitalWrite(LED_PIN, ledState);
    startTime = loopStartTime;
  }
  
  // ON Logic
  if ((ledState == LED_ON) && (elapsedTime >= ON_INTERVAL)) {
    ledState = LED_OFF;
    digitalWrite(LED_PIN, ledState);
    startTime = loopStartTime;
  }
  
  /***********************************
   *
   * // Your real-time responsive code goes here.
   *
   **********************************/
}

技术提示: 你是否使用过AI来帮助改进你的代码?这无疑是一个有争议的话题,尤其是在学术界,人们担心作弊问题。然而,许多程序员从中受益。至少你可能想将你的代码展示给AI,并请求帮助重构代码以提高清晰度。你可能会感到惊讶。

开发非阻塞类

为了让这篇文章简短,让我们跳过非阻塞计时器演变的几个步骤,直接跳到基于类的实现。考虑本笔记末尾包含的代码清单,包括timers.h(头文件)和timers.cpp(实现文件)以及一个示例应用程序Opta.ino。这段代码抽象了非阻塞延迟,提供了一个简单易用的接口。

为了解释这个Arduino类结构,我们将过渡到梯形逻辑。图形化实现使代码更容易理解,这并不奇怪。

考虑图1。这个梯形逻辑图的第一级包括一个开关触点、一个开启计时器(TON),输出连接到Opta的一个输出继电器和一个状态LED。TON块包含两个输入;IN作为逻辑电平控制信号,程序时间(PT)指定编程延迟。

这就像一个数字门卫。当输入保持时间达到PT中指定的时间时,输出(Q)变为高电平。它将保持高电平,直到IN变为低电平。经过时间(ET)允许我们监控块的时间。与Q类似,当达到PT时,ET时间值将停止增加。当IN为低电平时,它也会重置。

在此示例中,TON块由gxI1控制。这是Opta的数字输入之一。PT时间条件设置为2000。这遵循了Arduino以毫秒为单位列出时间的传统。操作很容易描述。在这里,Opta的继电器输出(gxO1)和状态LED(gxLED1)将在开关(gxI1)闭合2秒后激活。当开关打开时,两个输出将立即停用。

图1 :定时器的梯形逻辑表示。

传统的Arduino实现类在此帖子末尾的Opta.ino列表摘录中显示。

...
// Timer Instantiation
   TON TON_1, TON_2;
...
PB_1 = digitalRead(I1_PIN);  // Get all Inputs at start of loop
...
// Activate an output two seconds after an input transitions from low to high
  TON_1.update(PB_1, 2000);
  int test_1 = TON_1.Q;
...

第一步是创建定时器的实例。在这里,我们看到从TON类实例化的TON_1和TON_2。这相当于图1中的图形表示。在块内部,我们看到TON块,TON_1和TON_2实例直接位于块上方。

digitalRead()函数检索Opta输入的状态,并将值传递给pushbutton变量(PB_1)。TON_1.update()方法接受PB_1参数和2000 ms的PT值。最后,通过访问TON_1的公共Q变量(TON_1.Q)来检索Q值。

继续到图1中的第2梯级,我们遇到了另一个TON实例以及一个TOF实例。时间关闭定时器(TOF)与TON类似,两者都用于非阻塞延迟。它们的操作方式有所不同。顾名思义,TON在开启时提供延迟,而TOF在关闭时提供延迟。属性总结如下表:

TON 与TOF 的比较

时间开启定时器 时间关闭定时器
目的: 在指定的连续IN激活时间后激活输出。 在IN停用后指定的时间后停用输出。
用途: 用于延迟启动;检测持续输入。 通常用于延迟停止;指定最小操作时间。
示例 当开关激活2秒后,电机启动。 系统至少冷却3分钟。

在我们继续讨论时,请记住代码通常以读取、修改、写入的操作序列运行。还要记住,所有代码都在循环中按顺序执行。在PLC中,我们称之为程序扫描。在C++微控制器实现中,我们称之为超级循环。

图1的第2梯级包含一些微妙的条件,有助于理解TON和TOF的操作。让我们从TON开始。请注意,它正在自我反馈;TON_2的Q输出通过一个常闭触点作为TON_2的输入。在初始启动时,Q输出为低电平,允许逻辑TRUE传递到TON_2的IN控制输入。定时器开始计数。当达到PT时,Q输出被激活。记住读取、修改、写入的约束,Q输出将在一个程序扫描周期内保持高电平。因此,在下一个循环迭代中。TON_2 被重置,重新开始整个过程。

换句话说,TON_2 每 5 秒产生一个脉冲。此脉冲的宽度为一个程序扫描周期。等效的基于类的脉冲生成器包含在这行简单的代码中:

TON_2.update(!TON_2.Q, 5000); 

如图2所示,此脉冲被发送到 TOF_1。回想一下,当 IN 为真时,TOF 定时器的 Q 输出将立即激活。然后,TOF 定时器将保持 Q,直到 PT 时间持续时间结束。在这个特定的例子中,TOF_1 每5秒接收一次脉冲。每次接收到脉冲时,它将 Q 保持高电平300毫秒。结果在 Opta 的状态 LED 上显示为一个短(300毫秒)脉冲,每5秒发生一次。基于类的表示如下所示。请注意,TOF 实例接受 TON_2.Q 脉冲和5000毫秒的 PT。最后一行将 TOF_1.Q 值传输到一个变量,供 Arduino 的 digitalWrite() 函数使用,以设置所需的输出。

  // Produce a pulse that is one program scan long that occurs once every 5 seconds
  // Use that pulse to blink an LED with a 300 ms on period

  TON_2.update(!TON_2.Q, 5000);

  TOF_1.update(TON_2.Q, 300);
  int test_2 = TOF_1.Q;

结论

非常重要的是要理解,本文中介绍的梯形逻辑和基于类的实现是非阻塞的。也就是说,刺激和响应之间存在无限小的时间交互。从人类对现实世界事件的角度来看,TON_1、TON_2 和 TON_3 独立运行;第1行和第2行之间没有交互。可以添加许多这样的行,所有行都保留了非阻塞约束,这对于计算机接口和现实世界应用中的控制至关重要。

本文的其余部分包含基于 Arduino 类的非阻塞代码。虽然它是为在 Opta PLC 中使用而编写的,但可以修改以适用于 Arduino 家族中的大多数成员。通过替换 Arduino 的 millis() 函数,它可以在许多现代微控制器上使用。这是构建非阻塞定时器的众多方法之一。可能有更好的方法;这些方法可能占用更少的内存、更快或更易于阅读。然而,我相信它会很有用。




timers.h

/************************************************************************************** 
 * Disclaimer:
 *     The content provided herein was generated with assistance from an artificial 
 *     intelligence model (ChatGPT V 4.0). 
 *     
 *     This code is subject to Term and Conditions associated with the DigiKey TechForum.
 *     Refer to https://www.digikey.com/en/terms-and-conditions?_ga=2.92992980.817147340.1696698685-139567995.1695066003
* 
 *     Should you find an error, please leave a comment in the forum space below. 
 *     If you are able, please provide a recommendation for improvement.
 *
 * Description:
 *     The code demonstrates the parallels between Arduino programming and PLC 
 *     constructs such as TON, TOF, and TONOFF.
 *
 *     The program is best demonstrated on the Arduino Opta PLC. However, it may be useful
 *     for any Arduino application where simple to implement non-blocking delays are desired. 
 *  
 * Acknowledgments:
 *     Thanks to OpenAI's ChatGPT for guidance and code assistance.
 * 
 ***************************************************************************************/



/************************************************************************************** 
 * CAUTION with the use of a global variable loopStartTime
 *
 * This allows a consistent time for all timer instances for a given loop iteration.
 *
 * The loop time is set once in the main loop as: loopStartTime = millis();
 *
 ***************************************************************************************/


#ifndef TIMERS_H
#define TIMERS_H

extern unsigned long loopStartTime;

class TON {
public:
  TON();                                   // Constructor declaration
  void update(bool IN, unsigned long PT);  // Method declaration

  bool Q;                                  // Output
  unsigned long ET;                        // Elapsed Time

private:
  bool prevIN;                             // Previous input state
  unsigned long startTime;                 // Time at which the timer was started
  unsigned long prevPT;                    // Previous PT value
};




// TOF - Timer Off Delay
class TOF {
public:
  TOF();
  void update(bool IN, unsigned long PT);
  bool Q;                                  // Output
  unsigned long ET;                        // Elapsed Time

private:
  unsigned long startTime;                 // Time when the input became FALSE
  bool prevIN;                             // Previous state of input
  unsigned long prevPT;                    // Previous delay time to check if it has changed
};




class TONOFF {
public:
  TONOFF();
  void update(bool IN, unsigned long OnDelay, unsigned long OffDelay);
  bool Q;                                  // Output
  unsigned long ET;                        // Elapsed Time

private:
  unsigned long startTime;                 // Time when the input changes state
  bool prevIN;                             // Previous state of input
  unsigned long prevOnDelay;               // Previous on-delay time to check if it has changed
  unsigned long prevOffDelay;              // Previous off-delay time to check if it has changed
  int mode;                                // 0: Idle, 1: OnDelay mode, 2: OffDelay mode
};




#endif  // TIMERS_H


Timers.cpp


#include "timers.h"

unsigned long loopStartTime;        // CAUTION: this is a global variable

TON::TON()
  : Q(false), ET(0), prevIN(false), prevPT(0) {} // Default values

void TON::update(bool IN, unsigned long PT) {

  // Check if PT has changed during the count
  if (IN && (PT != prevPT)) {
    startTime = loopStartTime;
    ET = 0;
    prevPT = PT;
  }

  // Rising edge detection
  if (IN && !prevIN) {
    startTime = loopStartTime;
    ET = 0;
  }

  // Timer logic
  if (IN) {
    ET = loopStartTime - startTime;
    Q = ET >= PT;
  } else {
    Q = false;
    ET = 0;
  }

  prevIN = IN;
}




TOF::TOF()
  : Q(true), ET(0), prevIN(true), prevPT(0) {}  // Default values

void TOF::update(bool IN, unsigned long PT) {

  // Check if PT has changed during the count
  if (!IN && (PT != prevPT)) {
    startTime = loopStartTime;
    ET = 0;
    prevPT = PT;
  }

  // Falling edge detection
  if (!IN && prevIN) {
    startTime = loopStartTime;
    ET = 0;
  }

  // Timer logic for TOF
  if (!IN) {
    ET = loopStartTime - startTime;
    Q = ET < PT;  // Q remains true until ET exceeds PT
  } else {
    Q = true;
    ET = 0;
  }

  prevIN = IN;
}




TONOFF::TONOFF()
  : Q(false), ET(0), prevIN(false), prevOnDelay(0), prevOffDelay(0), mode(0) {}

void TONOFF::update(bool IN, unsigned long OnDelay, unsigned long OffDelay) {

  // Check if OnDelay or OffDelay has changed during the count
  if (mode == 1 && (OnDelay != prevOnDelay)) {
    startTime = loopStartTime;
    ET = 0;
    prevOnDelay = OnDelay;
  }

  if (mode == 2 && (OffDelay != prevOffDelay)) {
    startTime = loopStartTime;
    ET = 0;
    prevOffDelay = OffDelay;
  }

  // Rising edge detection for On Delay
  if (IN && !prevIN) {
    startTime = loopStartTime;
    ET = 0;
    mode = 1;  // OnDelay mode
  }

  // Falling edge detection for Off Delay
  if (!IN && prevIN) {
    startTime = loopStartTime;
    ET = 0;
    mode = 2;  // OffDelay mode
  }

  // Timer logic for OnDelay
  if (mode == 1) {
    ET = loopStartTime - startTime;
    Q = ET >= OnDelay;
    if (Q) {
      mode = 0;  // Reset mode when OnDelay time is reached
    }
  }

  // Timer logic for OffDelay
  if (mode == 2) {
    ET = loopStartTime - startTime;
    Q = ET < OffDelay;
    if (!Q) {
      mode = 0;  // Reset mode when OffDelay time is reached
    }
  }

  prevIN = IN;
}

pins.h

#ifndef PINS_H
#define PINS_H

//Names for the Opta I/O

#define I1_PIN A0
#define I2_PIN A1
#define I3_PIN A2
#define I4_PIN A3
#define I5_PIN A4
#define I6_PIN A5
#define I7_PIN A6
#define I8_PIN A7

#define O1_PIN D0
#define O2_PIN D1
#define O3_PIN D2
#define O4_PIN D3

#define LED_RED_PIN LEDR
#define LED_GRN_PIN LED_RESET
#define LED_BLU_PIN LED_USER

#define S1_PIN LED_D0
#define S2_PIN LED_D1
#define S3_PIN LED_D2
#define S4_PIN LED_D3

#define USER_PB_PIN BTN_USER

#endif // PINS.H

Opta.ino


#include "timers.h"
#include "pins.h"

// Global Variable

extern unsigned long loopStartTime;
bool gxFirstScan = true;  // High for one and only one program scan

// Program Constants

  // FIXME: This demo program contains magic numbers

// Timer Instantiation
TON TON_1, TON_2;
TOF TOF_1;
TONOFF TONOFF_1;

void setup() {
}

void loop() {

  bool PB_1;


  /************************************************************************************** 
   * PRELIMINARY LOOP TASKS:
   * 
   * This is where inputs are read and housekeeping tasks are conducted
   * such as the essential update to the global variable loopStartTime 
   *
   */

  PB_1 = digitalRead(I1_PIN);  // Get all Inputs at start of loop

  loopStartTime = millis();

  /************************************************************************************** 
   * MAIN PROGRAM BODY
   * 
   * DO NOT read from or write to I/O in this section of code. Stated another way, 
   * we will NOT use Ix_PIN and Ox_PIN in this section.
   *
   */


  // Activate an output two seconds after an input transitions from low to high
  TON_1.update(PB_1, 2000);
  int test_1 = TON_1.Q;

  // Produce a pulse that is one program scan long that occurs once every 5 seconds
  // Use that pulse to blink an LED with a 300 ms on period
  TON_2.update(!TON_2.Q, 5000);
  TOF_1.update(TON_2.Q, 300);
  int test_2 = TOF_1.Q;

  // Use the Time ON / Off Timer to blink a LED with one second on and 3 seconds off
  TONOFF_1.update(!TONOFF_1.Q, 3000, 1000);
  int test_3 = TONOFF_1.Q;


  /************************************************************************************** 
   * END OF LOOP TASKS
   * 
   * This is where physical outputs are updated. 
   * 
   */

  digitalWrite(O1_PIN, test_1);
  digitalWrite(O2_PIN, test_2);
  digitalWrite(O3_PIN, test_3);

  digitalWrite(S1_PIN, test_1);
  digitalWrite(S2_PIN, test_2);
  digitalWrite(S3_PIN, test_3);

  if (gxFirstScan == true)  // First scan is true for first loop iteration
    gxFirstScan = false;
}

返回工业控制与自动化索引