Skip to Content

Timers, interrupts and non-blocking code: why delay() breaks a PLC scan

delay() is the first thing every Arduino tutorial teaches and the first habit a PLC program needs to unlearn. In the pump station, you need to wait before declaring a fault and still read the emergency stop while you wait. This lesson shows how.

What you will learn

  • Explain why delay() makes a PLC deaf and mute.
  • Time actions with millis() without blocking.
  • Use an interrupt on an interrupt-capable input for a signal you cannot afford to miss, keeping the handler short.

Why delay() is a problem

delay() blocks everything. While it runs, your program cannot read a new input, react to a fault or answer a Modbus request. A one second delay() leaves the PLC deaf and mute for a full second on every pass, including the emergency stop.

Timing without blocking: millis()

millis() returns the milliseconds since the board started and blocks nothing. Comparing it with a stored timestamp lets you run code on a schedule while loop() keeps executing.

unsigned long lastToggle = 0;
const unsigned long interval = 1000;

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

void loop() {
  if (millis() - lastToggle >= interval) {
    lastToggle = millis();
    digitalWrite(Q0_1, !digitalRead(Q0_1));
  }

  // the rest of loop() runs on every pass, e.g. the state machine
}

Expected result: the alarm beacon on Q0_1 blinks every second while the rest of the program keeps running. Use this pattern for a fault beacon in the FAULT state of the previous lesson.

Subtract the stored time from the current time, as above, and not the other way round: this form keeps working correctly when the millis() counter rolls over after about 49 days.

A timeout in the pump station

The same pattern implements a timeout. If the pump is running and the level does not rise within a set time, the machine goes to FAULT. You store when the state started and compare it on each pass, with no delay().

unsigned long runningSince = 0;
const unsigned long maxRunTime = 30000;  // 30 s without result

// when entering RUNNING: runningSince = millis();
// inside case RUNNING:
if (millis() - runningSince >= maxRunTime) {
  currentState = FAULT;
}

Interrupts: reacting immediately to a signal

An interrupt pauses loop() to run a short function the moment a signal changes. It fits events that cannot wait for the next pass, such as a fast encoder pulse. Not every input can trigger one. On the M-Duino family, two inputs per zone are interrupt-capable:

ZoneAnalog/digital shieldRelay shield
BI0_5, I0_6I0_0, I0_1
CI1_5, I1_6I1_0, I1_1
DI2_5, I2_6I2_0, I2_1

Which zones exist depends on your model, so check the interrupt table in your M-Duino user guide (section 5.4). On the Ardbox family, only I0_0 is interrupt-capable. Using an input that is not interrupt-capable will not raise an interrupt.

volatile bool stopRequested = false;

void onStopSignal() {
  stopRequested = true;
}

void setup() {
  // I0_6 is interrupt-capable on the analog/digital shield (zone B)
  pinMode(I0_6, INPUT);
  attachInterrupt(digitalPinToInterrupt(I0_6), onStopSignal, RISING);
}

Keep interrupt handlers very short and only set a flag, as above. Do the actual work in loop(), where it is safe to use the rest of the program state.

millis() or an interrupt?

millis() fits anything on a schedule: blinking, polling a sensor, timing out a state. An interrupt fits anything that must be caught the instant it happens. Most PLC programs use both on different signals.

Go deeper

Next lesson: exposing the pump station state over Modbus. Take the quiz below first.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.