millis() vs delay(): delay(ms) stops the program for the time you give it, so the loop cannot read inputs or update outputs meanwhile. millis() returns the milliseconds since the board started and does not stop anything. To run an action every N milliseconds without blocking the PLC, compare millis() - previousMillis >= interval in every loop. Use delay() only for short pauses in simple programs.
Both functions are part of the Arduino core, so they work the same in an Arduino based industrial PLC such as the M-Duino or the Ardbox, and in the ESP32 PLC. The difference matters more in a PLC than on a hobby board: a program that stops for a few seconds also stops reading its inputs, its alarms and its communications.
millis() vs delay(): comparison table
| delay(ms) | millis() | |
|---|---|---|
| What it does | Waits the given number of milliseconds | Returns the milliseconds since the program started |
| Parameter | Time to wait, in ms (unsigned long) | None |
| Return value | None | unsigned long |
| Blocks the loop | Yes. Interrupts still run | No |
| Several timers at the same time | No | Yes, one variable per timer |
| Overflow | Not affected | Back to 0 after about 49.7 days. Handled by the subtraction pattern |
| Microsecond version | delayMicroseconds(us) | micros(), overflows after about 71.6 minutes |
| Typical use | Short pauses at start-up, simple tests | Cyclic tasks, blinking outputs, timeouts, debounce, alarms |
How millis() works
millis() returns the number of milliseconds since the board started running the current program, as an unsigned long (32 bits). The highest value is 4,294,967,295 ms. After that, about 49.7 days, the counter goes back to zero. A PLC that runs for months reaches this point, so the way you compare times matters (see the overflow section below).
For shorter intervals, micros() returns microseconds. It overflows after about 71.6 minutes, and on 16 MHz AVR boards such as the M-Duino and the Ardbox its resolution is 4 µs.
How delay() works
delay(ms) pauses the program for the number of milliseconds of its parameter. During the pause, loop() does not run: no input is read, no output changes from your code and no message is processed by your program. Interrupts are not disabled, so the serial port keeps receiving data into its buffer, PWM outputs keep their value and interrupt routines run.
For pauses of a few microseconds, use delayMicroseconds(us). It also blocks the program.
Example: print a message every 5 seconds
When the program does only one thing, delay() is the shortest way to do it:
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Hello World");
delay(5000); // the program stops here for 5 s
}
The same task with millis() leaves the loop free for other work. This is the pattern to use in a PLC program:
const unsigned long INTERVAL = 5000; // ms
unsigned long previousMillis = 0;
void setup() {
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - previousMillis >= INTERVAL) {
previousMillis = now;
Serial.println("Hello World");
}
// the loop keeps running: read inputs, check alarms, answer Modbus...
}
The first edition of this post used time = millis(); while (millis() < time + 5000);. That loop is as blocking as delay(), and it fails at the overflow: if time + 5000 passes the maximum value, it wraps to a small number and the wait ends at once.
Example: two outputs with different periods and an input that answers at once
With delay(), two outputs with different periods cannot run at the same time: each pause stops the other output and the inputs. With millis(), each output has its own timer. This sketch for an M-Duino 21+ toggles Q0.0 every second and Q0.1 every two seconds, and reads I0.0 in every loop:
// M-Duino 21+: Q0.0 toggles every 1 s, Q0.1 every 2 s, I0.0 is read in every loop
const unsigned long PERIOD1 = 1000; // ms
const unsigned long PERIOD2 = 2000; // ms
unsigned long time1 = 0;
unsigned long time2 = 0;
bool out1 = false;
bool out2 = false;
void setup() {
pinMode(Q0_0, OUTPUT);
pinMode(Q0_1, OUTPUT);
pinMode(I0_0, INPUT);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - time1 >= PERIOD1) {
time1 = now;
out1 = !out1;
digitalWrite(Q0_0, out1);
}
if (now - time2 >= PERIOD2) {
time2 = now;
out2 = !out2;
digitalWrite(Q0_1, out2);
}
if (digitalRead(I0_0) == HIGH) {
Serial.println("I0.0 active"); // answered at once, the outputs keep their timing
}
}
We compiled the three sketches for the M-Duino 21+ with avr-gcc, the Industrial Shields AVR board package 1.2.1 and -Wall -Wextra: no errors and no warnings in the sketches. We did not run them on hardware for this edition. On another model, change the pin names (for example Q0_0, I0_0) to the ones of your PLC.
millis() overflow after 49.7 days: why the subtraction works
Write the comparison as now - previousMillis >= interval, with all the variables as unsigned long. The subtraction of two unsigned values gives the right elapsed time also when the counter has gone back to zero. Avoid millis() >= previousMillis + interval: the addition can overflow and the condition becomes true too early or too late.
We checked it with a C program that simulates the 32 bit counter around the overflow: with an interval of 5000 ms, the subtraction fires every 5000 ms before and after the counter returns to zero, while the old while (millis() < time + 5000) ends without waiting.
Rule for PLC programs. Keep loop() short and without long pauses. Each task checks its own timer with millis() and returns at once. This keeps the scan time low, as in a PLC with cyclic execution.
Which function should I use?
- delay(): short pauses where nothing else must happen, such as a wait after power-up before you read a sensor, or a quick test sketch.
- millis(): everything that repeats or has a timeout in a real program: blinking outputs, polling sensors, sending data, debouncing inputs, alarm delays, communication timeouts.
- Timers in the PLC style: to count up and down, or to build on-delay and off-delay timers, see the increasing and decreasing timer.
FAQ about millis() and delay()
Is millis() better than delay()?
For programs that do more than one thing, yes. millis() does not block the loop, so inputs, outputs and communications keep working. delay() is simpler for a single pause in a simple program.
What happens when millis() overflows?
After about 49.7 days the counter goes back to 0. If you compare times with millis() - previousMillis >= interval and unsigned long variables, the timing keeps working across the overflow.
Can I use delay() inside an interrupt routine?
No. Inside an interrupt routine, delay() does not work, and the millis() value does not increase. Keep interrupt routines short and set a flag that loop() handles.
Does delay() stop the serial port?
It does not stop the reception: the incoming bytes go to the serial buffer by interrupt. But your program does not read them until the delay ends, and the buffer can fill up if the pause is long.
Does the ESP32 PLC behave the same?
The functions are the same. On the ESP32, delay() lets other FreeRTOS tasks run while your loop waits, but your own loop still stops. millis() is also a 32 bit value and overflows after about 49.7 days.
Take it further
Non-blocking code in real machines: a labelling line that follows a changing speed, a sorter that tracks items by encoder and a WiFi reconnection that never stalls the loop.
Case study
Label bottles in motion, while the line speed changes.
An encoder, a photocell and two stepper motors on one Arduino based PLC.
Read the case study
Simulator
Sort a picking wave on a conveyor
An M-Duino 42+ reads a barcode scanner on RS-232, follows each item with an encoder and fires four diverters into the lanes of the orders.
Open the simulator
PLC Solution Library
WiFi reconnection that never stops the machine monitor
The ESP32 PLC retries the connection at most once every 10 seconds without ever stalling the loop.
See the example