Software timers

Software timers

(Read this article in english here.)

Muchas de nuestras aplicaciones están comandadas por el tiempo: cada cierto tiempo se debe actualizar el display; cada cierto tiempo se debe leer el teclado, cada cierto tiempo se debe realizar una conversión en el ADC, etc.

En la programación convencional (sin sistema operativo) nos hemos visto obligados a crear máquinas de estado dentro de una interrupción periódica para procesar todas esas actividades. Y aunque que esta solución efectivamente funciona, es difícil que escale.

Por otro lado, cuando usamos un sistema operativo para el manejo de esas tareas periódicas debemos crear una tarea por cada una de ellas. Cada tarea, por simple que sea (tal como leer un teclado o actualizar un display), consume recursos. Y tarea a tarea, los recursos se terminan.

La idea de los software timers (temporizadores por software) es tener una tarea a nivel del sistema operativo que centralice y administre a diferentes temporizadores, reduciendo el costo en recursos y simplificando la programación. Tú como programador solamente te encargas de decirle a cada temporizador lo que debe hacer cuando el tiempo establecido expire.

En esta lección veremos cómo puedes utilizarlos en tus proyectos de tiempo real.

Tabla de contenidos

¿Qué es y qué necesito?

Los temporizadores por software (de aquí en adelante les diré simplemente temporizadores o timers) son un mecanismo de FreeRTOS que nos permite temporizar actividades de manera muy fácil y con un mínimo de recursos. Luego de activar esta funcionalidad, dentro de FreeRTOS existirá una tarea (oculta a nosotros) que administrará cada uno de los temporizadores que hayamos creado, ya sin nuestra intervención.

Cada temporizador puede ser iniciado, detenido o reiniciado. Cada uno de estas acciones (FreeRTOS les dice comandos) puede ser lanzado por la propia callback, por otra callback, o por otra tarea.

Un dato interesante es que los temporizadores no dependen de otras tareas en el sistema (las tuyas), lo cual permite que ¡tengas una aplicación completa basada en temporizadores sin tareas! Podrás imaginarte cómo lograrlo después de ver los ejemplos más adelante.

Otra característica de los temporizadores es que los puedes crear como de una sola vez (one-shot) o repetitivos (auto-reload), y después, si es necesario, cambiar este comportamiento (a esta característica le decimos el modo, o mode en inglés). Así mismo, es posible cambiar el periodo (es decir, el tiempo entre llamadas) cuando el programa se está ejecutando.

Un elemento central de los temporizadres son las funciones callback, por lo que es lo primero que debemos revisar.

Funciones callback

Una función callback es una función, Fa, que tú le pasas a otra función, Fb, como parámetro para que ésta la ejecute; es decir, la función Fb recibe y ejecuta el código de Fa.

Cada vez que creas un temporizador deberás pasar una callback para que cuando el tiempo expire dicho código sea ejecutado, en cualquiera de los dos modos de operación de los temporizadores (one-shot o auto-reload).

La firma de las callbacks que los temporizadores utilizan es así:

Esto es, la función que escribas para que se ejecute cada vez que el tiempo expire no devuelve ningún resultado (void) y tiene un parámetro (TimerHandle_t). Por ejemplo, una función update_display() que actualiza un display de 7 segmentos cada cierto tiempo se vería así (omitiendo el código real):

void update_display( TimerHandle_t xTimer )
{
    // aquí el código que actualiza al display
}

El parámetro xTimer (al cual puedes nombrar como tú quieras) sirve para saber cuál temporizador mandó llamar a la callback. FreeRTOS permite que diferentes temporizadores llamen a la misma callback, por lo tanto, ésta debe de saber quién la llamó para ejecutar el código correcto. Pero si tu callback es llamada por un único temporizador, entonces podrías ignorarlo, o darle otros usos, como lo explicaré más adelante.

Demonios y daemons

A la tarea administradora de los timers le llamamos daemon (demonio en español).

El término daemon ha sido utilizado desde el principio de las computadoras para indicar procesos (o tareas) del sistema operativo que no son interactivas y que se ejecutan de manera oculta al usuario.

Cuando habilitas los temporizadores en FreeRTOS, éste crea una tarea que estará corriendo sin que te des cuenta (de ahí el término daemon) verificando el tiempo de finalización de cada temporizador.

Habilitación de los temporizadores

Necesitas 4 simples pasos para habilitar esta funcionalidad en tus proyectos:

1er paso. En el archivo de configuración FreeRTOSConfig.h asegúrate de tener las siguientes líneas (en cualquier lugar del archivo):

#define configUSE_TIMERS                    1
#define configTIMER_TASK_PRIORITY           ( ( UBaseType_t ) 1 )
#define configTIMER_QUEUE_LENGTH            ( ( UBaseType_t ) 3 )
#define configTIMER_TASK_STACK_DEPTH        configMINIMAL_STACK_SIZE * 2

configUSE_TIMERS: La pones a 1 para activar la funcionalidad de timers en FreeRTOS.

configTIMER_TASK_PRIORITY: El daemon de los temporizadores es a final de cuentas una tarea (interna a FreeRTOS) por lo cual requiere una prioridad. Una baja prioridad podría dar lugar a que los comandos no se ejecuten ya que tareas con mayor prioridad no le presten la CPU al daemon. Una alta prioridad (junto con un mal diseño) podría dar lugar a que el daemon consuma todo el tiempo de CPU.

configTIMER_QUEUE_LENGTH: Esta es una cola de comandos. Un comando es una llamada a las funciones (las cuales veremos más adelante) que inician, detienen, reinician o destruyen a los temporizadores. El hecho de hacer la llamada no significa que la instrucción le llegue de inmediato al daemon, y en consecuencia, al temporizador referido. Si hubiera tareas de mayor prioridad al daemon ejecutándose, entonces cada comando se inserta en una cola, y una vez que el daemon tiene tiempo de CPU los comandos se extraen de la cola y, efectivamente, se ejecutan sobre el o los timers indicados. El valor para esta constante simbólica dependerá de la carga de trabajo del sistema, el diseño, y la prioridad del daemon.

configTIMER_TASK_STACK_DEPTH: Esta constante tiene sentido cuando está habilitada la creación dinámica de objetos (configSUPPORT_DYNAMIC_ALLOCATION está puesta a 1). Aquí estableces cuánta memoria le quieres dar al daemon. En el caso de que la creación dinámica estuviera deshabilitada, entonces tú deberás proporcionar la memoria para el daemon (revisa esta lección si no recuerdas cómo hacerlo).

2do paso. Asegúrate que los archivos timers.h y timers.c están en la carpeta src donde están el resto de archivos de FreeRTOS. En el proyecto Molcajete ya están donde deben de estar, así que no deberías preocuparte.

3er paso. Si decides utilizar la forma estática para crear a los temporizadores (de manera exclusiva o junto con la forma dinámica) deberás declarar una función callback para obtener memoria para los temporizadores (nada que ver con las callbacks que hemos estado platicando). Se oye complicado, pero no tienes nada de que preocuparte: FreeRTOS te dice cómo debe ser esa función (aquí puedes verlo) y yo ya la puse donde tiene que ir; en otras palabras no tienes que hacer nada, solamente saberlo. (Si estás interesado busca en el directorio src el archivo static_mem_callbacks.c que yo agregué como parte del proyecto Molcajete.)

4to paso. En los archivos de tu proyecto donde utilices a los temporizadores deberás agregar el archivo de encabezado timers.h:

#include <timers.h>

Desarrollo

Creación de los temporizadores

Tanto el daemon que administra los temporizadores como los temporizadores, pueden ser creados con memoria dinámica o con memoria estática (revisa esta lección y esta para que recuerdes la diferencia entre estas dos formas). Aunque para la mayoría los ejemplos que veremos estaré usando la forma dinámica ya que es más fácil de implementar, te recuerdo que la creación estática de objetos es mejor y más segura.

Las funciones para la creación dinámica y estática de los temporizadores son casi idénticas, de no ser por un parámetro extra para la segunda, por eso voy a utilizar a esta última para explicar los diferentes parámetros.

Temporizadores con memoria estática

pcTimerName: Es una cadena de texto que representa un nombre que le quieras dar al temporizador. FreeRTOS no lo utiliza internamente, pero tú lo puedes utilizar cuando estés depurando tus aplicaciones. El nombre lo puedes obtener posteriormente con la función pcTimerGetName().

xTimerPeriod: Es el periodo, en ticks, del temporizador. Puedes usar la macro pdMS_TO_TICKS() para utilizar milisegundos (ms). Por ejemplo, para un periodo de 500 ms, escribirías pdMS_TO_TICKS( 500 ).

uxAutoReload: Este parámetro es el modo. Cuando escribes pdFALSE estableces que el temporizador será de una sola vez (one-shot), mientras que cuando escribes pdTRUE estableces que será continuo (auto-reload). One-shot significa que cuando expire el periodo la callback se llamará y luego el temporizador entrará al estado dormant (durmiente) hasta que reciba un nuevo comando de inicio. Auto-reload significa que la callback se estará llamando de manera repetida cada vez que expire el periodo sin necesidad de enviar el comando de inicio. El comando de inicio lo envías con la función xTimerStart().

pvTimerID: Una misma callback puede ser llamada por varios temporizadores. En este parámetro puedes establecer un identificador que le ayudará a la callback a determinar cuál temporizador la llamó. Dado que este es un parámetro void* también puedes usarlo para pasarle datos a la callback, o para que ésta mantenga un estado (junto con las funciones TimerSetTimerID() y pvTimerGetTimerID()).

pxCallbackFunction: Es la función que se ejecutará cuando el periodo expire.

pxTimerBuffer: (Este parámetro sólo está definido para la función xTimerCreateStatic()). Es una variable de tipo StaticTimer_t (interna a y) que FreeRTOS utilizará para guardar el estado del temporizador. Deberás crearla global o estática (revisa aquí)

Valor devuelto: Esta función devuelve un handler de tipo TimerHandle_t. Este handler lo necesitan todas las funciones de los temporizadores para saber sobre cuál operarán. En caso de que el temporizador no se haya creado, entonces el valor devuelto es NULL.

Ejemplo 1

Vamos a ver un ejemplo simple utilizando temporizadores estáticos:

#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>

enum { TMR_ONE_SHOT = pdFALSE, TMR_AUTO_RELOAD = pdTRUE };

void callback( TimerHandle_t h )
{
   Serial.println( "hola mundo" );
}

void setup()
{
   static StaticTimer_t tmr1_state;

   TimerHandle_t tmr1_h = xTimerCreateStatic(
         "TMR1",               // nombre
         pdMS_TO_TICKS( 500 ), // periodo inicial
         TMR_AUTO_RELOAD,      // tipo (pdTRUE = auto-reload)
         NULL,                 // ID (se puede usar para pasar información)
         callback,             // función a ejecutar
         &tmr1_state );        // variable para guardar el estado

   configASSERT( tmr1_h != NULL );       

   xTimerStart( tmr1_h, 0 );
   // "arranca" al temporizador que acabamos de crear

   Serial.begin( 115200 );

   vTaskStartScheduler();
}

void loop() 
{
}

En este ejemplo podemos observar varias cosas:

No hay ninguna tarea creada por el usuario (tú). La única actividad del programa se lleva a cabo en la callback, la cual es llamada de manera continua por el daemon del temporizador. Que no haya tareas en este ejemplo no quiere decir que siempre será así; lo menciono porque es interesante que podamos crear aplicaciones completas basadas totalmente en temporizadores.

El handler que devuelve la función lo guardamos en una variable local a la función setup() porque es en esta misma función, y por única vez en este ejemplo, donde lo utilizamos. Lo más común es que sea otra función (una callback o una tarea) quien lo use. En esos casos deberás crear al handler (es decir, la variable tmr1_h) global.

Como el temporizador fue creado de manera estática debíamos crear su variable de estado de tal modo que fuera permanente durante toda la ejecución del programa. Teníamos dos opciones, que fuera global, o hacerla estática a la función. Escogí la segunda por ser más segura:

static StaticTimer_t tmr1_state;

Si utilizas a la función xTimerCreate() en su lugar, entonces este último paso no es necesario.

Habrás notado que en lugar de escribir directamente en el parámetro de modo el valor pdTRUE (modo continuo) agregué una enumeración con símbolos más claros; no es necesario, pero el código se entiende mejor:

enum { TMR_ONE_SHOT = pdFALSE, TMR_AUTO_RELOAD = pdTRUE };

La callback es muy sencilla, y para este ejemplo no guarda ningún estado ni utiliza el parámetro asociado al handler del temporizador que la llamó. Eso lo veremos en un siguiente ejemplo.

La salida es como la esperábamos:

Funciones para iniciar, detener y reiniciar

Antes de ver el siguiente ejemplo detengámonos un momento para ver a algunas de las funciones que controlan la operación de los temporizadores. Comencemos con la función de arranque, xTimerStart():

xTimer: Es el handler del temporizador que quieres iniciar. Éste lo obtuviste cuando lo creaste.

xBlockTime: Es el tiempo de espera, en ticks, para que el comando llegue a la cola de comandos antes de abortar la operación. Ya mencioné que emitir la instrucción no significa que el temporizador la recibirá de forma inmediata, y en caso de que el sistema esté ocupado, ésta se encolará (¿recuerdas la constante configTIMER_QUEUE_LENGTH?). Para convertir milisegundos a ticks puedes usar la macro pdMS_TO_TICKS(). Para un tiempo de espera infinito escribe portMAX_DELAY (asegúrate que la constante INCLUDE_vTaskSuspend, en el archivo FreeRTOSConfig.h, está puesta a 1); para regresar inmediatamente (es decir, sin esperar, puedes escribir 0. Este parámetro es ignorado cuando la función se llama antes de que el planificador inicie; por eso en el ejemplo anterior escribí el valor 0.

Valor devuelto: Si la instrucción no pudo ser encolada antes de que el tiempo establecido por xBlockTime expirara, entonces devolverá pdFAIL. En caso de que el comando sí alcance lugar en la cola, entonces devolverá pdTRUE. La instrucción será efectivamente ejecutada cuando el daemon obtenga tiempo de CPU.

Una función muy importante que quizás llegarás a utilizar con frecuencia es la que detiene al temporizador, xTimerStop():

Los parámetros y el valor devuelto tienen el mismo significado que la función xTimerStart(). Esta función detiene al temporizador indicado por xTimer que previamente hubiera sido iniciado con la función xTimerStart(). La función no espera a que el tiempo expire; en cuanto el daemon recibe el comando el temporizador es detenido. Por supuesto que podrás volver a arrancarlo.

Un temporizador detenido con xTimerStop() se va al estado Dormant. Esto significa que el temporizador sigue siendo parte de sistema, está inactivo (no usa ciclos de CPU), pero, a diferencia del estado Blocked, no está esperando por ningún evento. Como mencioné, para volverlo a la vida deberás reactivarlo con la función xTimerStart() o xTimerReset().

Ejemplo: Arrancando y deteniendo temporizadores

Ahora sí, otro ejemplo. En éste crearemos dos temporizadores. Uno imprimirá texto, T1, mientras que el otro hará parpadear a un LED, T2. T1 detendrá y arrancará a T2 de manera repetida. Por otro lado, los temporizadores serán creados con memoria dinámica:

#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>

enum { TMR_ONE_SHOT = pdFALSE, TMR_AUTO_RELOAD = pdTRUE };

#define BLINKS 3

TimerHandle_t led_h = NULL;
// el handler del timer asociado al LED debe ser global


void msg_callback( TimerHandle_t h )
{
   static uint8_t blinks = BLINKS;
   // usamos una variable estática para guardar el contador.
   // Este método no debe ser usado si la callback es compartida.

   static bool led_state = false;
   // idem


   Serial.print( blinks );
   Serial.println( ": hola mundo" );

   --blinks;
   if( blinks == 0 ) {
      blinks = BLINKS;

      led_state != false ?  
         xTimerStart( led_h, portMAX_DELAY ) :
         xTimerStop( led_h, portMAX_DELAY );

      led_state = !led_state;
   }
}

void led_callback( TimerHandle_t h )
{
   static bool state = false;
   // usamos una variable estática para guardar el estado de esta callback.
   // Este método no debe ser usado si la callback es compartida.

   digitalWrite( 13, state );

   state = !state;
}

void setup()
{
   TimerHandle_t msg_h = xTimerCreate(
         "MSG",                // nombre
         pdMS_TO_TICKS( 1000 ),// periodo inicial
         TMR_AUTO_RELOAD,      // tipo
         NULL,                 // ID
         msg_callback );       // función a ejecutar

   configASSERT( msg_h != NULL );       

   led_h = xTimerCreate(
         "LED",                // nombre
         pdMS_TO_TICKS( 125 ), // periodo inicial
         TMR_AUTO_RELOAD,      // tipo
         NULL,                 // ID
         led_callback );       // función a ejecutar

   configASSERT( msg_h != NULL );       

   xTimerStart( msg_h, 0 );
   // inicia al timer "maestro"
   
   pinMode( 13, OUTPUT );
   // lo usa el timer LED

   Serial.begin( 115200 );
   // lo usa el timer MSG

   vTaskStartScheduler();
}

void loop() 
{
}

La única información nueva que incluí en el ejemplo es la utilización de la constante simbólica portMAX_DELAY en las funciones xTimerStart() y xTimerStop(). Ésta se utiliza para indicarle a FreeRTOS que deberá esperar de manera indefinida por un evento; en otras palabras, que el tiempo de espera xBlockTime será infinito. Lo he hecho de esta manera para que supieras que puedes usarla en tus programas y por simplicidad. Sin embargo, recuerda que en un programa profesional deberás establecer un tiempo de espera para que el sistema responda correctamente ante posibles bloqueos.

Guardando el estado

En el ejemplo anterior guardamos estados y contadores marcando a las respectivas variables como static. Y funciona. Pero como mencioné en el código, éste método no es el adecuado cuando dos o más temporizadores comparten la misma callback. El siguiente ejemplo te muestra cómo utilizar el parámetro ID, junto con las funciones vTimerSetTimerID() y pvTimerGetTimerID(), para guardar un estado.

Antes del ejemplo revisemos muy rápidamente las firmas de dichas funciones:

El punto en común de estas funciones (junto con las de creación de los temporizadores) es que el parámetro para el identificador ID es del tipo void*. Este tipo es oro molido para nosotros los desarrolladores ya que se presta para realizar muchas cosas más allá de las que los diseñadores originales imaginaron. Es decir, la idea del parámetro (y sus funciones asociadas) es utilizarlo para asignar un ID del tipo que nosotros necesitemos (int o char, por ejemplo); sin embargo, void* es un lienzo en blanco que podemos utilizar, como lo muestra el siguiente ejemplo, para cosas completamente distintas:

#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>

enum { TMR_ONE_SHOT = pdFALSE, TMR_AUTO_RELOAD = pdTRUE };

void led_callback( TimerHandle_t h )
{
   int state = (int)pvTimerGetTimerID( h );

   digitalWrite( 13, state );

   vTimerSetTimerID( h, state == LOW ? (void*)HIGH : (void*)LOW );
}

void setup()
{
   static StaticTimer_t tmr1_state;

   auto tmr1_h = xTimerCreateStatic(
         "TMR1",               
         pdMS_TO_TICKS( 500 ), 
         TMR_AUTO_RELOAD,      
         NULL,                 // aquí podríamos pasar el estado inicial del LED
         led_callback,         
         &tmr1_state );        

   configASSERT( tmr1_h != NULL );       

   vTimerSetTimerID( tmr1_h, (void*)HIGH );
   // el estado inicial del LED es HIGH

   xTimerStart( tmr1_h, 0 );

   
   Serial.begin( 115200 );

   pinMode( 13, OUTPUT );

   vTaskStartScheduler();
}

void loop() 
{
}

Temporizador one-shot

Los ejemplos vistos hasta el momento han utilizado el modo auto-reload, el cual inicia de forma continua al temporizador. Sin embargo, en ocasiones vamos a querer iniciar un temporizador y que al finalizar su tiempo realice alguna acción, y ya; es decir, hará su tarea una sola vez. De ser necesario el programa volvería a arrancarlo.

Esta característica nos puede servir para establecer un tiempo para una acción y olvidarnos de ella (nosotros como programadores, no así el sistema operativo); esto es, nuestras tareas no ocuparán ciclos de CPU para estar revisando si el tiempo terminó.

Ejemplo

Imagina que estás escribiendo un sistema que necesite emitir un sonido intermitente mientras la temperatura de un proceso se mantenga por encima de un cierto nivel, solamente para llamar la atención (lo cual es diferente de una alarma donde la señal sería contínua hasta que alguien resuelva el problema). Por supuesto que podrías escribir el código que se encarga de esto en una de tus tareas, pero un temporizador en modo one-shot te simplificará la vida como lo muestra el siguiente código:

#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>

enum { TMR_ONE_SHOT = pdFALSE, TMR_AUTO_RELOAD = pdTRUE };


TimerHandle_t beep_h = NULL;


void read_task( void* pvParameters )
{
   TickType_t last_wake_time = xTaskGetTickCount();

   while( 1 )
   {
      vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( pdMS_TO_TICKS( 2000 ) ) );

      float temp = ( analogRead( A0 ) * 5.0 * 100.0 ) / 1024;
      // el sensor es un LM35

      if( temp > 25.0 ){

         // aquí apagaríamos al actuador

         digitalWrite( 13, HIGH );
         // simulamos que activamos la señal audible

         xTimerStart( beep_h, portMAX_DELAY );
         // arrancamos al timer
      }

      Serial.print( "Temp = " );
      Serial.println( temp );
   }
}

void beep_callback( TimerHandle_t h )
{
   // llegaremos aquí cuando el tiempo expire

   digitalWrite( 13, LOW );
   // simulamos que desactivamos la señal audible
}

void setup()
{
   xTaskCreate( read_task, "TEMP", 128, NULL, tskIDLE_PRIORITY, NULL );
   // esta tarea realiza las lecturas. Pudo haber sido un timer también,
   // pero quise cambiarle un poco

   beep_h = xTimerCreate(
         "BEEP",               // nombre
         pdMS_TO_TICKS( 500 ), // duración del beep
         TMR_ONE_SHOT,         // una sola vez
         NULL,                 // no lo usamos
         beep_callback );      // función a ejecutar

   configASSERT( beep_h != NULL );       

   pinMode( 13, OUTPUT );
   // emula a un búzer

   Serial.begin( 115200 );
   // lo usa la tarea principal

   vTaskStartScheduler();
}

void loop() 
{
}

Un diagrama de tiempo de este ejemplo es así:

Reiniciando al temporizador

El último tema que veremos en esta lección es sobre el reinicio (reset) de los temporizadores, a través de la función xTimerReset().

Esto es, puedes hacer que el tiempo se reinicie mientras el temporizador está activo. Una aplicación que se me ocurre es la luz de fondo (backlight) de una pantalla LCD de 16×2. Cuando el usuario presione una tecla la luz del LCD se encenderá por, digamos, 5 segundos. Si el usuario no volviera a presionar ninguna tecla dentro de esa ventana de tiempo, entonces la luz se apagaría; pero si el usuario presionara una tecla dentro de la ventana, entonces el temporizador se reiniciaría a 5 segundos otra vez. Y así sucesivamente mientras el usuario siga presionando teclas. Un ejemplo alternativo, pero similar, es de un sensor de presencia que mientras detecte que hay gente en un pasillo mantendrá las luces encendidas; una vez que deja de detectar personas y la ventana de tiempo se cierra, entonces las luces se apagan.

La firma de esta función es:

Los argumentos y el valor devuelto tienen el mismo significado que los de las funciones xTimerStart() y xTimerStop(), por lo cual no los voy a repetir aquí.

Un punto interesante a destacar, y que facilita la programación, es que si el temporizador está activo, entonces el tiempo se reinicia; pero si el temporizador está inactivo (es decir, en estado Dormant), entonces esta función lo arrancará, como si hubiese sido una llamada a xTimerStart().

El siguiente programa simula el encendido de la luz de fondo de un LCD:

#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>

enum { TMR_ONE_SHOT = pdFALSE, TMR_AUTO_RELOAD = pdTRUE };


TimerHandle_t button_h = NULL;
TimerHandle_t backl_h = NULL;


void button_callback( TimerHandle_t h )
{
   static uint8_t state = 0;
   
   static uint8_t ticks = 0;

   uint8_t pin = (uint8_t)pvTimerGetTimerID( button_h );
   // recuperamos el pin donde está conectado el push-button

   switch( state ) {

   case 0:
      if( digitalRead( pin ) == LOW ){
         state = 1;

         ticks = 5;
         // debouncing de 50 ms (10ms * 5)

      }
   break;

   case 1:
      --ticks;
      if( ticks == 0 ){

         if( digitalRead( pin ) == LOW ){

            digitalWrite( 13, HIGH );
            // encendemos la luz de fondo del LCD

            xTimerReset( backl_h, portMAX_DELAY );
            // si el timer está activo, lo reinicia;
            // en caso contrario lo arranca (como lo haría xTimerStart)
            
            state = 2;
            // esperamos a que deje de presionar el push-button

         } else{ // fue ruido:
            state = 0;
         }
      }
   break;

   case 2:
      if( digitalRead( pin ) != LOW ) state = 0;
   break;

   default:
      state = 0;
   break;
   }
}

void backl_callback( TimerHandle_t h )
{
   digitalWrite( 13, LOW );
   // apagamos la luz de fondo del LCD
}


#define PUSHBUTTON_PIN 2

void setup()
{
   button_h = xTimerCreate(
         "BTN",
         pdMS_TO_TICKS( 10 ),
         TMR_AUTO_RELOAD,
         
         (void*) PUSHBUTTON_PIN, 
         // usamos al campo ID para pasar el número de pin
         
         button_callback );

   backl_h = xTimerCreate(
         "BCKL",               
         pdMS_TO_TICKS( 5000 ),
         TMR_ONE_SHOT,         
         NULL,                 
         backl_callback );     

   pinMode( 13, OUTPUT );
   // emula a la luz de fondo del LCD

   pinMode( PUSHBUTTON_PIN, INPUT );
   // nuestro push-button

   Serial.begin( 115200 );
   // lo usa la tarea principal


   xTimerStart( button_h, 0 );
   // arrancamos al temporizador "maestro"

   vTaskStartScheduler();
}

void loop() 
{
}

La API de FreeRTOS y las interrupciones

En todos los ejemplos que hemos visto a lo largo de este curso hemos estado llamando a las diferentes funciones de FreeRTOS desde nuestras funciones y tareas; sin embargo, en la vida real vamos a querer obtener la misma funcionalidad desde las interrupciones.

Muchas de las funciones de FreeRTOS que hemos visto (y muchas otras que no hemos visto) no deben ser llamadas desde una interrupción, so pena de un mal-funcionamiento del programa. Para esos casos FreeRTOS provee funciones espejo que pueden ser llamadas con toda seguridad desde dentro de una interrupción. Estas funciones espejo tienen la terminación FromISR():

Funciones de la API de los software-timers de FreeRTOS.

En el caso que nos ocupa en esta lección, de la imagen anterior podrás observar diversas funciones con terminación FromISR(). Esas son las que debes llamar si lo necesitas hacer desde dentro de una interrupción (ISR son las siglas de Interrupt Service Routine).

RECUERDA
Cuando tengas que llamar a alguna función de FreeRTOS desde una interrupción, verifica que estás llamando a la versión correcta.

¿Qué sigue?

Los temporizadores (software-timers) son una adición reciente de FreeRTOS que, como pudiste observar, simplifican nuestras aplicaciones gracias a que la administración de los diferentes temporizadores se delega a una tarea central integrada en el sistema operativo (el daemon). Y seamos honestos, mucho de nuestro tiempo de diseño y desarrollo lo invertimos en actividades temporizadas, y lo queramos o no, terminamos escribiendo temporizadores. Cuando nos veamos haciendo esto deberíamos considerar utilizar los software timers tomando en cuenta que si dichas actividades son complejas, escogeríamos las tareas; pero si no son tan complejas, escogeríamos temporizadores.

En esta lección vimos las operaciones más importantes de los temporizadores, pero como viste en la imagen de la API de los mismos, hay muchas más funciones, las cuales puedes revisar por tu cuenta. Sin embargo, con lo que estudiamos es suficiente para que comiences a integrarlos a tus aplicaciones.

RECUERDA (otra vez): Cuando llames a funciones de FreeRTOS desde interrupciones, verifica que estás llamando a la versión correcta. (Debía insistir en este punto.)

En la siguiente lección veremos semáforos binarios y semáforos contadores utilizando las notificaciones directas a las tareas (direct to task notifications). Éstas ya las vimos, pero ahora lo haremos desde el punto de vista de la sincronización de tareas.

Índice del curso

Espero que esta entrada haya sido de tu interés. Si fue así podrías suscribirte a mi blog, o escríbeme a fjrg76 dot hotmail dot com, o comparte esta entrada con alguien que consideres que puede serle de ayuda.


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

2 COMENTARIOS