millis() vs delay(): delay(ms) detiene el programa durante el tiempo indicado, así que el bucle no puede leer entradas ni actualizar salidas mientras tanto. millis() devuelve los milisegundos desde que arrancó la placa y no detiene nada. Para ejecutar una acción cada N milisegundos sin bloquear el PLC, compara millis() - previousMillis >= interval en cada vuelta del bucle. Usa delay() solo para pausas cortas en programas sencillos.
Las dos funciones forman parte del núcleo de Arduino, así que funcionan igual en un PLC industrial basado en Arduino como el M-Duino o el Ardbox, y en el ESP32 PLC. La diferencia pesa más en un PLC que en una placa de aficionado: un programa que se detiene unos segundos también deja de leer sus entradas, sus alarmas y sus comunicaciones.
millis() vs delay(): tabla comparativa
| delay(ms) | millis() | |
|---|---|---|
| Qué hace | Espera el número de milisegundos indicado | Devuelve los milisegundos desde que empezó el programa |
| Parámetro | Tiempo de espera, en ms (unsigned long) | Ninguno |
| Valor de retorno | Ninguno | unsigned long |
| Bloquea el bucle | Sí. Las interrupciones siguen funcionando | No |
| Varios temporizadores a la vez | No | Sí, una variable por temporizador |
| Desbordamiento | No le afecta | Vuelve a 0 tras unos 49,7 días. Lo resuelve el patrón de la resta |
| Versión en microsegundos | delayMicroseconds(us) | micros(), se desborda tras unos 71,6 minutos |
| Uso típico | Pausas cortas al arrancar, pruebas sencillas | Tareas cíclicas, salidas intermitentes, timeouts, antirrebote, alarmas |
Cómo funciona millis()
millis() devuelve el número de milisegundos desde que la placa empezó a ejecutar el programa actual, como un unsigned long (32 bits). El valor máximo es 4.294.967.295 ms. Después de ese tiempo, unos 49,7 días, el contador vuelve a cero. Un PLC que funciona durante meses llega a ese punto, así que importa cómo comparas los tiempos (consulta la sección del desbordamiento más abajo).
Para intervalos más cortos, micros() devuelve microsegundos. Se desborda tras unos 71,6 minutos, y en las placas AVR de 16 MHz como el M-Duino y el Ardbox su resolución es de 4 µs.
Cómo funciona delay()
delay(ms) pausa el programa durante los milisegundos de su parámetro. Durante la pausa, loop() no se ejecuta: tu código no lee ninguna entrada, no cambia ninguna salida y no procesa ningún mensaje. Las interrupciones no se desactivan, así que el puerto serie sigue recibiendo datos en su buffer, las salidas PWM mantienen su valor y las rutinas de interrupción se ejecutan.
Para pausas de pocos microsegundos, usa delayMicroseconds(us). También bloquea el programa.
Ejemplo: imprimir un mensaje cada 5 segundos
Cuando el programa solo hace una cosa, delay() es la forma más corta de hacerlo:
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println("Hello World");
delay(5000); // the program stops here for 5 s
}
La misma tarea con millis() deja el bucle libre para otro trabajo. Este es el patrón que debes usar en un programa de PLC:
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...
}
La primera edición de este post usaba time = millis(); while (millis() < time + 5000);. Ese bucle bloquea tanto como delay() y falla con el desbordamiento: si time + 5000 supera el valor máximo, vuelve a un número pequeño y la espera termina enseguida.
Ejemplo: dos salidas con periodos distintos y una entrada que responde al momento
Con delay(), dos salidas con periodos distintos no pueden funcionar a la vez: cada pausa detiene la otra salida y las entradas. Con millis(), cada salida tiene su propio temporizador. Este sketch para un M-Duino 21+ conmuta Q0.0 cada segundo y Q0.1 cada dos segundos, y lee I0.0 en cada vuelta del bucle:
// 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
}
}
Hemos compilado los tres sketches para el M-Duino 21+ con avr-gcc, el paquete de placas AVR de Industrial Shields 1.2.1 y -Wall -Wextra: sin errores y sin avisos en los sketches. No los hemos ejecutado en hardware para esta edición. En otro modelo, cambia los nombres de los pines (por ejemplo Q0_0, I0_0) por los de tu PLC.
Desbordamiento de millis() a los 49,7 días: por qué funciona la resta
Escribe la comparación como now - previousMillis >= interval, con todas las variables unsigned long. La resta de dos valores sin signo da el tiempo transcurrido correcto también cuando el contador ha vuelto a cero. Evita millis() >= previousMillis + interval: la suma se puede desbordar y la condición se cumple antes o después de tiempo.
Lo hemos comprobado con un programa en C que simula el contador de 32 bits alrededor del desbordamiento: con un intervalo de 5000 ms, la resta se dispara cada 5000 ms antes y después de que el contador vuelva a cero, mientras que el antiguo while (millis() < time + 5000) termina sin esperar.
Regla para programas de PLC. Mantén loop() corto y sin pausas largas. Cada tarea comprueba su propio temporizador con millis() y vuelve enseguida. Así el tiempo de ciclo se mantiene bajo, como en un PLC con ejecución cíclica.
¿Qué función debería usar?
- delay(): pausas cortas en las que no debe pasar nada más, como una espera tras el arranque antes de leer un sensor, o un sketch de prueba rápido.
- millis(): todo lo que se repite o tiene un timeout en un programa real: salidas intermitentes, lectura periódica de sensores, envío de datos, antirrebote de entradas, retardos de alarma, timeouts de comunicación.
- Temporizadores al estilo PLC: para contar hacia arriba y hacia abajo, o para crear temporizadores a la conexión y a la desconexión, consulta el temporizador creciente y decreciente.
Preguntas frecuentes sobre millis() y delay()
¿Es millis() mejor que delay()?
Para programas que hacen más de una cosa, sí. millis() no bloquea el bucle, así que las entradas, las salidas y las comunicaciones siguen funcionando. delay() es más sencillo para una sola pausa en un programa simple.
¿Qué pasa cuando millis() se desborda?
Tras unos 49,7 días el contador vuelve a 0. Si comparas los tiempos con millis() - previousMillis >= interval y variables unsigned long, la temporización sigue funcionando después del desbordamiento.
¿Puedo usar delay() dentro de una rutina de interrupción?
No. Dentro de una rutina de interrupción, delay() no funciona y el valor de millis() no aumenta. Mantén las rutinas de interrupción cortas y activa una variable que loop() procese.
¿delay() detiene el puerto serie?
No detiene la recepción: los bytes que llegan van al buffer serie por interrupción. Pero tu programa no los lee hasta que termina la pausa, y el buffer se puede llenar si la pausa es larga.
¿El ESP32 PLC se comporta igual?
Las funciones son las mismas. En el ESP32, delay() deja que otras tareas de FreeRTOS se ejecuten mientras tu bucle espera, pero tu propio bucle se detiene igualmente. millis() también es un valor de 32 bits y se desborda tras unos 49,7 días.
Llévalo a la práctica
Código sin bloqueos en máquinas reales: una línea de etiquetado que sigue una velocidad cambiante, un clasificador que sigue los artículos por encoder y una reconexión WiFi que nunca bloquea el bucle.
Caso de estudio
Etiqueta botellas en movimiento, aunque cambie la velocidad de la línea.
Un encoder, una fotocélula y dos motores paso a paso en un solo PLC basado en Arduino.
Lee el caso de estudio
Simulador
Clasifica una oleada de picking en una cinta
Un M-Duino 42+ lee un escáner de códigos de barras por RS-232, sigue cada artículo con un encoder y activa cuatro desviadores hacia los carriles de los pedidos.
Abre el simulador
PLC Solution Library
Reconexión WiFi que nunca detiene el monitor de máquina
El ESP32 PLC reintenta la conexión como máximo cada 10 segundos sin bloquear nunca el bucle.
Ver el ejemplo