Mastering Non-Blocking Arduino Delay By Adapting PLC Techniques

One of the reasons I appreciate the Arduino Opta Programmable Logic Controller (PLC) is the opportunity to explore the relationships between different programming languages. For example, in this post we will explore the relationship between Ladder Logic and C++ programming involving instantiation of class objects. Specifically, we will contrast and compare the simplicity of Ladder Logic based timers and the not-so-simple methods of implementing non-blocking time delays in the Arduino environment.

We will start with the classic Arduino blocking delay, move on to a C++ class implementation of PLC-like timers, and then present the, by comparison, simple to use ladder logic implementation. You will see that the ladder logic implementation hides a great deal of complexity resulting in easy to implement logic that responds to real-time signals.

While the ladder logic implementation is unique to the Opta and other IEC61131-3 compliant PLCs, the non-blocking code may be of interest to all programmers and microcontroller families, not just the Arduino.

Review of blocking code

Do you remember your first Arduino program? Most likely it involved blinking an LED with a code structure that looked something like this:

#define LED_PIN 13

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

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

This is a great starting place for the many thousands of Arduino programmers. Yet, this code has a major fault. In no way is it suitable for real-world control interfacing. The problem is described as blocking code.

For every call of the delay( ) function, the microcontroller is blind for the duration of the delay. For example, if we wanted to respond to a switch or sensor, we would need to wait for the delay( ) to complete. In this particular example, we would need to wait for up to three seconds. That’s completely unacceptable for a machine that controls equipment in the real world.

Importance of non-blocking code

To solve this problem and grow your knowledge of Arduino, you likely used the millis( ) function. Recall that millis( ) is like a clock. It provides a reasonable estimate of the amount of time that has passed since the Arduino started. For a non-blocking timer we need a few variables to hold specific time instances. With these variables in place, we can then monitor for the passing of time.

This is similar to an alarm clock where we determine that an event must happen at 06:45:30. As we spin through loop( ), we simply ask if the current time is greater than 07:45:30. If it is, we do something. If not, we quickly exit and spin through loop( ). The entire process is quick and allows the microcontroller to quickly iterate loop( ) providing a system that is responsive to real-time events. The typical program will spin through the superloop many thousands of times a second thereby ensuring a quick real-world response.

Tech Tip: This method of continually spinning through the loop provides a reasonable real-world response. In no way does this polling method provide precision timing. For more accuracy and reduced latency consider using interrupt-based delays.

This following code perform the same LED blinking operation without blocking. Note the use of a state variable to track if the LED is on or off. Also note that the code retains the two timer operations. One for a one-second on duration and one for a two-second off duration.


#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.
   *
   **********************************/
}

Tech Tip: Have you used an AI to help improve your code? This is certainly a controversial topic especially in academia where there is a concern about cheating. However, many programmers benefit from the practice. At the very least you may want to present your code to the AI and ask help refactoring the code for increased clarity. You may be surprised.

Development of a non-blocking class

To keep this post short, let’s skip a few steps in the evolution of non-blocking timers and jump to a class-based implementation. Consider the code listing contained at the end of this note including timers.h (header) and timers.cpp (implementation) files with an example application as Opta.ino. This code abstracts the non-blocking delay allowing a simple to use interface.

To explain this Arduino class structure, we will transition to ladder logic. It’s no surprise that the graphical implementation makes the code easier to understand.

Consider Figure 1. The first rung of this ladder logic diagram includes a switch contact, a Time-On-Timer (TON) with outputs connected to one of the Opta’s output relay and one of the status LEDs. The TON block contains two inputs; IN serves as a logic-level control signal and Program Time (PT) specifies the programmed delay.

This functions like a digital gatekeeper. The output (Q) goes high when the input has been held for the time specified in PT. It will stay high until IN transitions to low. The Elapsed Time (ET) allows us to monitor the block’s time. Similar to Q, the ET time value will stop increasing when PT is reached. It will also reset when IN is low.

The TON block in this example, is controlled by gxI1. This is one of the Opta’s digital inputs. The PT time condition is set to 2000. This follows the Arduino tradition of listing time in units of milliseconds. The operation is easy to describe. Here, the Opta’s relay output (gxO1) and a status LED (gxLED1) will activate 2 seconds after the switch (gxI1) is closed. Both outputs will deactivate immediately when the switch is open.

Ladder Loic example for TON and TOF.

Figure 1: Ladder logic representation of timers.

The traditional Arduino implementation of the class is shown in this excerpt from the Opta.ino listing contained at the end of this post.

...
// 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;
...

The first step is to create an instance of the timer. Here we see the instantiation of TON_1 and TON_2 from the TON class. This is equivalent to the graphical representation in Figure 1. Inside the block we see the TON block with a TON_1 and TON_2 instance directly above the block.

The digitalRead( ) function retrieves the state of the Opta’s input and transfers the value to the pushbutton variable (PB_1). The TON_1.update( ) method accepts the PB_1 parameter and the PT value of 2000 ms. Finally, the Q value is retrieved by the accessing the TON_1 public Q variable (TON_1.Q).

Moving on to rung 2 in Figure 1 we encounter another instance of the TON as well as an instance of TOF. The Time-Off-Timer (TOF) timer is similar to the TON as both serve a non-blocking delays. There are different in how they operate. As implied by the name, the TON provides a delay on turn on while the TOF provides a delay in turning off. The attributes are summarized in this table:

Comparison between TON and TOF

Time On Timer Time Off Timer
Purpose: Activate output after specified period of continuous IN activation. Deactivates output a specified period after IN is deactivated.
Usage: Used for delay starts; detect sustained input. Commonly used for delay stops; specified minimal operation time.
Examples Motor start when switch is active for 2 seconds. System cools off for a minimum of 3 minutes.

As we continue this discussion keep in mind that code typically functions with a read, modify, write sequence of operation. Also remember that all code is performed sequentially within a loop. In the PLC we call this a program scan. In C++ microcontroller implementation we call this the superloop.

Rung 2 of Figure 1 contains a few subtle conditions that will help understand the operation of TON and TOF. Let’s start with a focus on the TON. Notice that it is feeding itself; the TON_2 Q output is an input to TON_2 via a normally closed contact. Upon initial starting the Q output is low allowing a logic TRUE to pass to the TON_2 IN control input. The timer starts counting. When PT is reached, the Q output is activated. Remembering thee read, modify, write constraint, the Q output will be high for one program scan. Consequently, on the next loop iteration. TON_2 is reset starting the process all over again.

Stated another way, TON_2 produces a pulse once every 5 seconds. This pule has a duration of one program scan in width. The equivalent class-based pulse generator is contained in this simple line of code:

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

As seen in Figure 2, this pulse is sent to the TOF_1. Recall that the Q output of a TOF timer will immediately be active when IN is true. The TOF timer will then hold Q until PT time duration has elapsed. In this particular example, TOF_1 receives a pulse once every 5 seconds. Every time it receives a pulse it holds Q high for 300 ms. The result is seen on the Opta’s status LED as a short (300ms) pulse that occurs once every 5 seconds. The class based representation is shown below. Observe that the TOF instance accepts the TON_2.Q pulse and a PT of 5000 ms. The final line transfers the TOF_1.Q value to a variable to be used by an Arduino digitalWrite( ) function to set the desired outputs.

  // 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;

Conclusion

It’s vitally important to understand that the ladder logic and the class-based implementation presented in this post are non-blocking. Which is to say, there is an infinitesimal time interaction between stimulus and response. From a human perspective of real-world events, TON_1, TON_2, and TON_3 operate independently; there is no interaction between rung 1 and rung 2. Many such lines can be added with all preserving the non-blocking constrain that is essential to computer interface and control in real-world applications.

The remainder of this post contains the Arduino class-based non-blocking code. While it is written for use in the Opta PLC it may be modified to work on most members in the Arduino Family. With a replacement of the Arduino millis( ) function it could be used on a large number of modern microcontrollers. This is one of many ways to construct non-blocking timers. There may be better methods; methods that are less memory intensive, faster, or easier to read. However, I trust it will be useful.

Best Wishes,

APDahlen

Be sure to see this link for related Arduino education content.

About the Author

Aaron Dahlen, LCDR USCG (Ret.), serves as an application engineer at DigiKey. He has a unique electronics and automation foundation built over a 27-year military career as a technician and engineer which was further enhanced by 12 years of teaching (interwoven). With an MSEE degree from Minnesota State University, Mankato, Dahlen has taught in an ABET accredited EE program, served as the program coordinator for an EET program, and taught component-level repair to military electronics technicians. Dahlen has returned to his Northern Minnesota home and thoroughly enjoys researching and writing articles such as this. LinkedIn | Aaron Dahlen - Application Engineer - DigiKey




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;
}

Return to the Industrial Control and Automation Index

1 Like