Cómo tener diferentes bases de tiempo en los sketches de Arduino

Cómo tener diferentes bases de tiempo en los sketches de Arduino

(Read this article in english here.)

En nuestros programas es común tener diversas tareas o periféricos ejecutándose en periodos diferentes: un LED que parpadea cada 500ms, un display que se refresca cada 5ms, un teclado que se debe escanear cada 100ms, etc.

Arduino, junto con su horrible súper-loop, nos impone algunas restricciones cuando de establecer diferentes periodos se trata:

  1. Es complicado acceder al temporizador por hardware que hace que las funciones `delay() y `millis() funcionen. En entornos de desarrollo normales nosotros los programadores basamos nuestras bases de tiempo en alguno de los diferentes temporizadores por hardware del microcontrolador. En Arduino es difícil lograrlo sin intervenir el código fuente del mismo (ya lo hice).
  2. La función `loop() se manda llamar de manera continua por lo que debemos insertar en ésta un mecanismo que nos permita realizar programas basados en tiempo (time-based programming).

En este artículo te quiero mostrar lo que hice para implementar bases de tiempo diferentes dentro del súper-loop (lo odio). (Con respecto a intervenir al temporizador por hardware, ya lo hice y esta solución evolucionó en el proyecto KleOS, pero esto será tema de otro artículo.)

Sigue leyendo para que veas cómo puedes implementar dos o más bases de tiempo diferentes en tus proyectos de Arduino. ¡Es súper fácil!

Tick (del sistema)

Lo primero que hay que entender es que la función loop() se manda llamar de manera continua por la función main() dentro de un ciclo infinito:

// Función main() de Arduino

int main(void)
{
  init();
  initVariant();

  #if defined(USBCON)
    USBDevice.attach();
  #endif
  
  setup();

  for (;;) {
    loop();
    
    if (serialEventRun) serialEventRun();
  }

  return 0;
}

Así que para empezar debemos colocar un mecanismo que haga las veces de base de tiempo principal dentro de loop(). A esta base, en los sistemas embebidos, le decimos “el tick del sistema” y es quien gobernará al resto del programa.

Este tick se puede implementar tanto con la función delay() o con la función millis(). La primera usa un tiempo relativo y por lo tanto introducirá desplazamientos en el tiempo, mientras que la segunda usa un tiempo absoluto y no introducirá desplazamientos. Por esta razón usaremos a millis():

#define SYSTEM_TICK 5    // in milliseconds

#define MS_TO_TICKS( x ) ( (x) / ( SYSTEM_TICK ) )

void loop()
{
   static unsigned long last_time = 0;
   unsigned long now = millis();
   if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return;
   last_time = now;

   // código que se ejecutará en cada tick...
}

El loop de Arduino se seguirá ejecutando de manera continua, como es de esperarse, pero mientras el siguiente tick no llegue, la función terminará (es decir, no hará nada, como en los sistemas normales que basan su tick en un temporizador por hardware).

De este pequeño pero importantísimo código podrás notar varias cosas:

  1. He definido una constante SYSTEM_TICK para simplificar las cosas. En este ejemplo el tick del sistema está establecido en 5 milisegundos.
  2. He definido una macro que se encargue de convertir tiempo en milisegundos a tiempo en ticks:
    1. El tick podría cambiar de periodo y lo último que queremos es buscar y modificar en todo el programa aquellos lugares donde hubiéramos utilizado directamente ticks.
    2. Es más fácil para nosotros entender milisegundos que ticks.
  3. La variable last_time es quien lleva el tiempo absoluto de nuestro tick y está marcada como static para que guarde su valor entre llamadas (en lo personal NO uso variables globales si no son estrictamente necesarias).

Para que las bases de tiempo funcionen tendrás que evitar usar funciones bloqueantes dentro de la función loop() (delay() es una función bloqueante: se queda con la CPU hasta que termina). Deberás modificar tu código para que utilice la Programación Orientada a Eventos y esto involucrará la utilización de máquinas de estado.

Antes de implementar las diferentes bases de tiempo debo mencionar que podemos utilizar a este tick directamente si tenemos una o más tareas cuyo periodo coincida con él. Escogí 5ms porque es el tiempo perfecto para refrescar una pantalla de 4 dígitos y 7 segmentos (aquí el proyecto), y con este valor es fácil obtener muchos otros. (En muchos sistemas embebidos es común establecer el periodo del tick en 1ms, yo lo hago muy seguido, pero en esta ocasión lo dejaré como 5ms.)

Por ejemplo, digamos que queremos utilizar al tick para refrescar la pantalla (omitiendo los detalles):

void loop()
{
   static unsigned long last_time = 0;
   unsigned long now = millis();
   if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return;
   last_time = now;

   display.update() // refresca la pantalla cada 5ms

  // más código ...
}

Escribiendo las diferentes bases de tiempo

Ahora sí, equipados con el tick principal ya podemos escribir nuestras otras bases de tiempo. Comencemos con una de 100ms.

Para cada base de tiempo vamos a necesitar una variable contadora de ticks. Fiel a mis principios la marcaré como static (repito: no uso variables globales si las estáticas pueden hacer el trabajo). Te recomiendo utilizar un nombre que evoque el periodo que estará contando, por ejemplo base_time_100ms (se lee mejor 100ms_base_time, pero no podemos tener variables que empiecen con números):

void loop()
{
   static unsigned long last_time = 0;
   unsigned long now = millis();
   if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return;
   last_time = now;

   static uint16_t base_time_100ms = MS_TO_TICKS( 100 );
   --base_time_100ms;
   if( base_time_100ms == 0 )
   {
      base_time_100ms = MS_TO_TICKS( 100 );

      // código que se debe ejecutar cada 100ms
   }

   // ...
}

¿Cómo funciona?

Es muy sencillo. La variable base_time_100ms se carga por primera vez con un valor inicial (20 para este ejemplo, el cual se obtiene de dividir 100 entre 5). En cada tick esta variable se va decrementando en uno, y cuando llega a 0 el código correspondiente se ejecuta. Cada vez que la variable llega a 0 debe recargarse para repetir el proceso. Y ya, eso es todo (casi, falta hablar sobre el código que se ejecutará).

Para otras bases de tiempo basta con replicar el procedimiento anterior. Agreguemos otra base de 500ms para completar el ejercicio:

   static unsigned long base_time_100ms = MS_TO_TICKS( 100 );
   --base_time_100ms;
   if( base_time_100ms == 0 )
   {
      base_time_100ms = MS_TO_TICKS( 100 );

      // código que se debe ejecutar cada 100ms
   }

   static uint16_t base_time_500ms = MS_TO_TICKS( 500 );
   --base_time_500ms;
   if( base_time_500ms == 0 )
   {
      base_time_500ms = MS_TO_TICKS( 500 );

      // código que se debe ejecutar cada 500ms
   }

NOTA: Cuando utilices tipos enteros de tamaño fijo, como el tipo uint16_t, no olvides incluir el archivo de cabecera <stdint.h>, como lo muestra el ejemplo completo más adelante.

Programación dirigida por eventos y máquinas de estados

¿Y ya, eso fue todo?

En cuanto a las bases de tiempo, sí. La siguiente parte, que tiene que ver con el código que se ejecutará, es más complicada. ¿Notaste que en un código anterior escribí la siguiente instrucción?:

display.update();

El código dentro del método .update() es una máquina de estados. Cuando usas bases de tiempo como las que hemos desarrollado tu código no puede bloquearse (esperar por un evento sin hacer nada, como por ejemplo llamar a la función delay() o esperar de manera indefinida por un carácter del puerto serial). El secreto está en ejecutar una acción (leer, preguntar, hacer, etc) e inmediatamente salirse.

Veamos un ejemplo sencillo. Un LED que cambiará de estado de manera indefinida cada 500ms, ¡sin usar a la función delay()!. Solamente te presento el código relevante:

   static uint16_t base_time_500ms = MS_TO_TICKS( 500 );
   --base_time_500ms;
   if( base_time_500ms == 0 )
   {
      base_time_500ms = MS_TO_TICKS( 500 );

      static bool led_state = false;
      if( led_state == false )
      {
         led_state = true;
         digitalWrite( 13, LOW );
      }
      else
      {
         led_state = false;
         digitalWrite( 13, HIGH );
      }
   }

Las líneas 7 a 17 forman una máquina de estados sencilla. La máquina podrá tener más estados y ser más compleja dependiendo de lo que estés programando.

En cualquier caso las instrucciones 10 y 15 son las que indican el siguiente estado al que entrará la máquina, y las instrucciones 11 y 16 son las acciones llevadas en cada estado.

NOTA: El código anterior para hacer parpadear al LED conectado en el pin 13 podría expresarse en una sola línea:

digitalWrite( 13, (led_state = !led_state) ? LOW : HIGH );

Sin embargo, no se aprecia de manera correcta la actualización del siguiente estado, ni de la acción a ejecutar. Además, este código sólo funciona para dos estados, y normalmente tendremos más de 2 de ellos en nuestras máquinas (en este proyecto Sistema Tinaco-Cisterna podrás ver una máquina de estados más compleja).

Quiero mostrarte ahora el mismo código pero utilizando un switch, ya que cuando tenemos muchos estados ya no nos funciona una variable boolena, y también es más conveniente que estar utilizando if’s y expresa mejor nuestras intenciones (además, es la forma más común de escribir una máquina de estados):

      static int led_state = 0;
      switch( led_state )
      {
         case 0:
            led_state = 1;
            digitalWrite( 13, LOW );
            break;

         case 1:
            led_state = 0;
            digitalWrite( 13, HIGH );
            break;
      }

Nota que la variable led_state, quien es la que controla a los estados, ahora es del tipo int.

Más ejemplos

Si quieres ver ejemplos más complejos y completos sobre diferentes temporizaciones y máquinas de estado en un código real te invito a que veas los artículos Módulo de 4 dígitos LED de 7 segmentos y Sistema Tinaco-Cisterna. Revísalos en cuanto puedas, mientras tanto te muestro la parte que tiene que ver con el tema que hoy nos ocupa (del proyecto tinaco-cisterna, y donde el tick del sistema está establecido como 100ms. El código completo lo puedes descargar desde aquí):

   static unsigned long last_time = 0;
   auto now = millis();
   if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return;
   last_time = now;

   // Este código se ejecuta cada 100ms:

   keypad.state_machine();
   lcd_backlight.state_machine();
   led13.state_machine();
   buzzer.state_machine();

   static bool error = false;
   static bool settings = false;
   if( not error and settings == false )
   {
      uint8_t minutes, seconds;

      if( state == eStates::WAITING )
      {
         minutes = downTimer_set.minutes;
         seconds = downTimer_set.seconds;
      }
      else // process is running:
      {
         timer.get( &minutes, &seconds );
      }
      print_time( 0, 1, minutes, seconds );
   }

   // Este código se ejecuta cada 1000ms:

   --seconds_tick_base;
   if( seconds_tick_base == 0 )
   {
      seconds_tick_base = MS_TO_TICKS( 1000 );

      timer.state_machine();
      timer_after.state_machine();
   }

Pon mucha atención a todas las máquinas de estado que implementé; cada evento o periférico que es accionado por tiempo requiere su propia máquina de estado. En el código fuente asociado a ese proyecto podrás ver cómo se implementan las máquinas para cada tipo de evento.

De hecho fue a partir de dicho proyecto que me surgió la necesidad de implementar diferentes bases de tiempo, y luego consideré escribir precisamente sobre este tema.

En lo personal hubiera preferido utilizar en ese proyecto un sistema operativo en tiempo real (más de esto a continuación), pero mi intención fue que estuviera al alcance de la mayor cantidad de entusiastas de la electrónica, por eso decidí hacerlo con sketches de Arduino.

Código completo

Te dejo aquí el código completo que utilicé a lo largo del artículo. Es muy simple como para tenerlo en un repositorio:

#include <stdint.h>

#define SYSTEM_TICK 5    // in milliseconds

#define MS_TO_TICKS( x ) ( (x) / ( SYSTEM_TICK ) )

void setup() 
{
   pinMode( 13, OUTPUT );
}

void loop()
{
   static unsigned long last_time = 0;
   unsigned long now = millis();
   if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return;
   last_time = now;

   static uint16_t base_time_100ms = MS_TO_TICKS( 100 );
   --base_time_100ms;
   if( base_time_100ms == 0 )
   {
      base_time_100ms = MS_TO_TICKS( 100 );

      // código que se debe ejecutar cada 100ms
   }


   static uint16_t base_time_500ms = MS_TO_TICKS( 500 );
   --base_time_500ms;
   if( base_time_500ms == 0 )
   {
      base_time_500ms = MS_TO_TICKS( 500 );

     
      static bool led_state = false;
      if( led_state == false )
      {
         led_state = true;
         digitalWrite( 13, LOW );
      }
      else
      {
         led_state = false;
         digitalWrite( 13, HIGH );
      }
   }
}

¿Qué sigue?

Esta solución funciona y escala bien hasta cierto grado; sin embargo, si tu aplicación necesita muchas bases de tiempo, o temporización compleja, o que dentro de cada base se ejecute mucho código, o código que deba esperar por otros eventos, entonces lo más conveniente es que migres a soluciones más elaboradas, como los sistemas operativos de tiempo real. Para este caso te presento a mi proyecto KleOS y mi curso gratuito de Arduino en tiempo real con FreeRTOS, capítulo 10. 

En el sistema operativo FreeRTOS puedes crear temporizaciones complejas utilizando directamente tareas; sin embargo, incluye una funcionalidad de temporizadores por software que permite crear las mismas temporizaciones complejas pero de forma más fácil.

En cualquier caso, el tema que hoy te presenté te puede servir como introducción a la Programación dirigida por eventos (Event-driven development).


Espero que este artículo te sea útil. Si quieres más artículos como éste, podrías considerar suscribirte al blog.


¿Necesitas alguien que te diseñe un circuito impreso de manera profesional en KiCAD? Búscame en Fiverr. ¡Diseños desde 30USD!


¿Ya conoces mi curso gratuito de Arduino en tiempo real, utilizando el Arduino UNO o el Arduino Due y FreeRTOS?


¿Te gustaría ser de los primeros en recibir las actualizaciones a mi blog? ¡Suscríbete, es gratis!


Fco. Javier Rodríguez
Escrito por:

Fco. Javier Rodríguez

Soy Ingeniero Electrónico con 20+ años de experiencia en el diseño y desarrollo de productos electrónicos de consumo y a medida, y 12+ años como profesor. Egresado de la UNAM, también tengo el grado de Maestro en Ingeniería por la misma universidad. Mi perfil completo lo puede encontrar en: https://www.linkedin.com/in/fjrg76-dot-com/

Ver todas las entradas

3 COMENTARIOS