19- Arduino, Funciones de Tiempo


Los timers de Arduino.

Comencemos con cómo el microcontrolador generalmente sabe cuánto tiempo pasa. ¡No tiene reloj! Para el funcionamiento del microcontrolador, es vital un llamado generador de reloj, o un oscilador de cristal, o un cristal de cuarzo. Es un oscilador, también es un reloj. Clock en inglés es reloj. Sí, pero no todo es tan simple. El cristal de cuarzo se encuentra junto al MC en la placa (también muchos MC tienen un generador de reloj incorporado), las placas Arduino suelen tener un oscilador de 16 MHz, también hay modelos de 8 MHz. El generador de reloj hace algo muy simple: acciona el microcontrolador a su propia frecuencia de reloj, es decir, un cristal de 16 MHz acciona el MC 16 millones de veces por segundo. El microcontrolador, a su vez, conociendo la frecuencia del cuarzo, puede estimar el tiempo entre ciclos (16 MHz = 0.0625 microsegundos), y así navegar en el tiempo. Todos los microcontroladores poseen temporizadores internos. Estos son dispositivos ubicados físicamente dentro del MC que se dedican a contar los pulsos del generador de reloj. 

Y ya podemos usar esto, para esto Arduino tiene funciones de tiempo listas para usar. Hay tres contadores en el ATmega328 de Arduino, y el temporizador número 0 es responsable de contar el tiempo. Cualquier otro contador puede hacer esto, pero trabajando en el IDE de Arduino obtienes inmediatamente esta configuración, porque al crear un boceto en el IDE de Arduino, trabajamos automáticamente con la biblioteca Arduino.h, donde se implementan todas las funciones de tiempo necesarias


.

Retrasos o delays.

La función de tiempo más simple desde el punto de vista de uso es la demora, tenemos dos de ellas:

  • delay (time) – «suspende» la ejecución del código por milisegundos. Durante delay() el código no se ejecuta,  excepto las interrupciones. Se recomienda usarlo solo en los casos más extremos o en aquellos casos en los que la demora no afecta la velocidad del dispositivo. El parametro time toma el tipo de datos unsigned long y puede pausar la ejecución desde 1 ms a ~ 50 días (4,294,967,295 milisegundos) con una resolución de 1 milisegundo. Se ejecuta en el temporizador del sistema Timer 0, por lo que no funciona dentro de una interrupción y cuando las interrupciones están deshabilitadas.
  • delayMicroseconds (time) – Análogo a delay(), pausa la ejecución del código durante time en microsegundos. time toma el tipo de datos unsigned int y puede pausar la ejecución de 4 a 16383 μs con una resolución de 4 μs. Importante: delayMicroseconds no funciona en un temporizador, como otras funciones de tiempo en Arduino, sino en la cuenta del reloj del procesador. De esto se deduce que delayMicroseconds puede funcionar en interrupción y con interrupciones desactivadas.

Los retrasos son muy fáciles de usar:

void setup() {}
void loop() {
  // codigo
  delay(500);  // espera medio segundo
}

De esta manera podemos hacer algo de código dos veces por segundo por ejemplo. La función delayMicroseconds () a veces no funciona correctamente con variables, debe intentar usar constantes (const o simplemente un número). Para crear retrasos de microsegundos con un período variable y trabajar correctamente en ciclos, es mejor usar la siguiente construcción:

// función de retardo μs casera
void myDelayMicroseconds(uint32_t us) {
  uint32_t tmr = micros();
  while (micros() - tmr < us);
}

Pero, ¿qué pasa si necesitamos realizar una acción dos veces por segundo y las otras tres? Y el tercero, 10 veces por segundo, por ejemplo. Inmediatamente nos acostumbramos a la idea de que es mejor no usar retrasos en el código real. Excepto esto delayMicroseconds (), a veces es necesario generar algún tipo de protocolos de comunicación. Una herramienta normal para la gestión del tiempo de su código son las funciones que cuentan el tiempo desde el inicio del MC.


Funciones de conteo de tiempo.

Estas funciones devuelven el tiempo transcurrido desde el inicio del microcontrolador, llamado uptime (Engl. Uptime). Tenemos dos de esas funciones:

  • millis ()– Devuelve el número de milisegundos desde el inicio. Es unsigned long, de 1 a 4 294 967 295 milisegundos (~ 50 días), tiene una resolución de 1 milisegundo, después del desbordamiento se restablece a 0.  Se ejecuta en el temporizador del sistema Temporizador 0.
  • micros ()– Devuelve el número de microsegundos desde el inicio. Es un unsigned long, de 4 a 4,294,967,295 microsegundos (~ 70 minutos), tiene una resolución de 4 microsegundos, se restablece a 0 después del desbordamiento.  Se ejecuta en el temporizador del sistema Temporizador 0.

Puede preguntar, ¿cómo nos ayudará el tiempo desde el inicio del MC a organizar acciones en el tiempo? Es muy simple, el esquema es así:

  • Tomó medidas
  • Recordamos la hora actual desde el inicio del MC (en una variable separada)
  • Buscamos la diferencia entre la hora actual y la memorizada
  • Tan pronto como la diferencia sea mayor que el tiempo requerido del «Temporizador», realizamos la acción
  • «Restablecemos» el temporizador
    • Aquí hay dos opciones, igualar la variable del temporizador con el milis () actual o aumentarla por el tamaño del período

La implementación de dicho temporizador en millis () se ve así:

// variable de almacenamiento de tiempo (unsigned long)
uint32_t myTimer1;
void setup() {}
void loop() {
  if (millis() - myTimer1 >= 500) {  // buscando la diferencia (500 ms)    
    myTimer1 = millis();              // reiniciar el temporizador
    // realizar una acción
  }
}

La segunda opción para restablecer el temporizador se escribirá así:

// variable de almacenamiento de tiempo (unsigned long)
uint32_t myTimer1;
int period = 500;
void setup() {}
void loop() {
  if (millis() - myTimer1 >= period) {   // buscando la diferencia (500 ms)  
    myTimer1 += period;              // reiniciar el temporizador
    // realizar una acción
  }
}

¿Cuáles son las ventajas y desventajas? La primera forma (temporizador = milis () 😉 «Sale» si aparecen retrasos y otras causas de bloqueo en el código, durante la ejecución de las cuales millis () logra aumentar durante más de un período de tiempo y, a largo plazo, ¡el período «desaparecerá»! Pero al mismo tiempo, si bloquea la ejecución del código por un tiempo superior a un período, el temporizador corregirá esta diferencia, ya que la reseteamos con los milis actuales.

La segunda forma ( temporizador + = período;) cumple rígidamente el período, es decir, no “desaparece” con el tiempo si hay un pequeño retraso en el código. La desventaja aquí es que si el temporizador salta un período, se “disparará” varias veces durante la siguiente verificación.

Déjame recordarte que uint32_t este es el segundo nombre del tipo de datos entero largo sin signo, unsigned long, es más corto de escribir. ¿Por qué una variable tiene que ser de este tipo? Porque la función millis () devuelve exactamente este tipo de datos, es decir si hacemos nuestra variable como int se desbordará en 32,7 segundos. Pero el milis también está limitado a 4.294.967.295, y en caso de desbordamiento también se restablecerá a 0. Lo hará en 4 294 967 295/1000/60/60/24 = 49,7 días. ¿Significa esto que nuestro temporizador se «romperá» después de 50 días? No, esta construcción sobrevive tranquilamente al desbordamiento poniéndose a 0 y sigue.

Verificación de desbordamiento (overflow)

Veamos un ejemplo de código para verificar si ha habido desbordamiento en el contador:

// millis se almacena en la variable timer0_millis en los archivos del kernel
// extern para modificación directa
extern volatile unsigned long timer0_millis;
void setup() {
  Serial.begin(9600);
  // 5 segundos antes del desbordamiento de milis
  timer0_millis = UINT32_MAX - 5000;
}
uint32_t timer;
void loop() {
 // nuestro temporizador predeterminado con un segundo período
  if (millis() - timer >= 1000) {
    timer = millis();
    // muestra milisegundos
    Serial.println(millis() / 1000L);
     // mira como superó el desbordamiento
    // y sigue funcionando
  }
}

¿Por qué este diseño funciona y no se rompe? Porque estamos usando un tipo de datos sin signo que, cuando se desborda, comienza a contar desde cero. Por lo tanto, cuando los milis se vuelven cero y crecen, y le restamos un número enorme, obtenemos no un valor negativo, sino un valor completamente correcto, que es el tiempo desde el reinicio anterior del temporizador. Por lo tanto, la estructura no solo continúa funcionando después de ~ 50 días, sino que también pasa el momento de «desbordamiento» sin perder el período.

Volvamos al tema de la multitarea: queremos realizar una acción dos veces por segundo, la segunda – tres y la tercera – 10. Necesitamos 3 variables de temporizador y 3 construcciones con la condición:

// variable de almacenamiento de tiempo (unsigned long)
uint32_t myTimer1, myTimer2, myTimer3;
void setup() {}
void loop() {
  if (millis() - myTimer1 >= 500) {   // temporizador de 500 ms (2 veces por segundo)
    myTimer1 = millis();              //reinicia
    // realizar la acción 1
    // 2 veces por segundo
  }
  if (millis() - myTimer2 >= 333) {   // temporizador de 333 ms (3 veces por segundo)
    myTimer2 = millis();              // reinicia
    // realizar la acción 2
    // 3 veces por segundo
  }
  if (millis() - myTimer3 >= 100) {   // temporizador de 100 ms (10 veces por segundo)  
    myTimer3 = millis();              //reinicia
   // realizar la acción 3
    // 10 veces por segundo
  }
}

Y así es como podemos, por ejemplo, sondear el sensor 10 veces por segundo, filtrar los valores y mostrar las lecturas en la pantalla dos veces por segundo. Y parpadea una luz tres veces por segundo. Por qué no?

Código optimizado para temporizaciones con millis()

Recientemente, me hice una pregunta: ¿es posible hacer un temporizador con milis(), que omita correctamente el desbordamiento sin eliminar el período?

#define PERIOD 500
uint32_t timer = 0;
void loop() {
  if (millis() - timer >= PERIOD) {
    do {
      timer += PERIOD;
      if (timer < PERIOD) break;  // desbordamiento de uint32_t  
    } while (timer < millis() - PERIOD); // protección contra saltarse un paso
  }
}

Este temporizador tiene la mecánica de un temporizador clásico con el almacenamiento en una variable de temporizador, y su período es siempre un múltiplo de PERIOD y no se pierde. Esta construcción se puede simplificar para aun más:

#define PERIOD 500
uint32_t timer = 0;
void loop() {
  if (millis() - timer >= PERIOD) {
    timer += PERIOD;
  }
}

En este caso, el algoritmo resulta ser más corto, se conserva la multiplicidad de los períodos (el período no desaparece si hay retrasos en el código), pero se pierde la protección contra perdidas.


Deja un comentario