Ir al contenido

millis() vs delay() en Arduino y PLC industriales

Cuándo usar cada función de tiempo de Arduino en un programa de PLC
28 de septiembre de 2020 por
millis() vs delay() en Arduino y PLC industriales
Boot & Work Corp. S.L, Marti Guillem Cura

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é haceEspera el número de milisegundos indicadoDevuelve los milisegundos desde que empezó el programa
ParámetroTiempo de espera, en ms (unsigned long)Ninguno
Valor de retornoNingunounsigned long
Bloquea el bucleSí. Las interrupciones siguen funcionandoNo
Varios temporizadores a la vezNoSí, una variable por temporizador
DesbordamientoNo le afectaVuelve a 0 tras unos 49,7 días. Lo resuelve el patrón de la resta
Versión en microsegundosdelayMicroseconds(us)micros(), se desborda tras unos 71,6 minutos
Uso típicoPausas cortas al arrancar, pruebas sencillasTareas 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.

Buscar en nuestro blog

millis() vs delay() en Arduino y PLC industriales
Boot & Work Corp. S.L, Marti Guillem Cura 28 de septiembre de 2020
Compartir esta publicación

¿Estás buscando tu Controlador Lógico Programable ideal?

Echa un vistazo a esta comparativa de producto de varios controladores industriales basados en Arduino.

Comparamos entradas, salidas, comunicaciones y otras especificaciones con las de los equipos de otras marcas destacadas.

Comparativa de PLCs