我欣赏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;
}
