Completado
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:
| Zone | Analog/digital shield | Relay shield |
|---|---|---|
| B | I0_5, I0_6 | I0_0, I0_1 |
| C | I1_5, I1_6 | I1_0, I1_1 |
| D | I2_5, I2_6 | I2_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.
No hay comentarios por ahora.