Hooks útiles de FreeRTOS y un watchdog por software

Mucho, si no es que casi todo el trabajo que hacemos, lo llevamos a cabo dentro de tareas e interrupciones, en lo que le llamaríamos la capa de usuario. Sin embargo, FreeRTOS nos ofrece la posiblidad de hacer cosas simples como si nuestro código fuese parte de él, ejecutándose en la capa del sistema operativo.

Imagina que debes implementar un LED que parpadée a intervalos regulares para indicarte que el sistema está «vivo» (a esta actividad le llamamos heartbeat, o en español, latido del corazón). Si el LED no parpadea, o dejó de hacerlo después de un tiempo, significa que algo malo pasó dentro del sistema. Para implementarlo puedes hacerlo de dos formas con lo que conocemos hasta el momento:

  1. Una tarea dedicada al heartbeat, o
  2. Un software timer con una callback que lleve a cabo el latido del corazón.

Ambas soluciones tienen el problema del alto consumo de recursos para una tarea tan simple. ¿Qué podríamos hacer? Qué tal que le decimos al sistema operativo que él mismo mande llamar a nuestra función heartbeat. ¡Excelente!

FreeRTOS nos provee dos maneras de hacerlo:

  • Que FreeRTOS llame a nuestra función heartbeat() cada vez que el sistema esté desocupado, o
  • Que FreeRTOS llame a nuestra función heartbeat() en cada tick del sistema.

Cuál escoger depende totalmente de la aplicación que estés escribiendo, y por supuesto que puedes implementar muchas cosas diferentes a un heartbeat.

Con este par de mecanismos no tendrás que crear tareas extra o usar a los software timers para algo tan sencillo. Eso sí, el código de heartbeat(), o cualquier otro que inyectes a FreeRTOS, tiene que ser simple y no debería llamar a ninguna función bloqueante.

Una aplicación muy útil que podemos inyectar al sistema para que se ejecute cada vez que el sistema está desocupado es la de un watchdog por software. Un watchdog se encarga de supervisar al sistema; si una tarea crítica deja de responder, entonces el sistema completo es reiniciado.

En la lección de hoy vamos a recordar qué son las callbacks, cómo usarlas en cada tick del sistema o cada vez que éste tenga tiempo libre, y cómo implementar un watchdog por software básico.

Tabla de contenidos

¿Qué es y qué necesito?

Callbacks

Una función callback es una función tuya que se la pasas a una función de un sistema independiente (o aparte del tuyo) como parte de una lista de argumentos. Esta función del sistema entonces ejecuta tu función:

int fn( int v );
void funcion_del_sistema( int a, int (*pf)(int) )
{
	pf( a );
}


int callback( int v )
{
	return v+1;
}

int main()
{
	funcion_del_sistema( 5, callback );
}

La función funcion_del_sistema() no es tuya, es parte de un sistema más grande, por ejemplo, FreeRTOS o un administrador de ventanas en un sistema gráfico. Pero tú le pasas código tuyo inyectándole la función callback() la cual sí es tuya (le puse ese nombre para que la identificaras). La función funcion_del_sistema() la recibe como parte de sus argumentos y le da el nombre de pf(). Cuando funcion_del_sistema() se encuentra pf( a ) manda llamar a callback( 5 ). Esta es una forma de extender la funcionalidad de un sistema sin modificar su código fuente.

Para que las callbacks funcionen sus firmas deben de corresponder. Por ejemplo, la función funcion_del_sistema() puede recibir cualquier función que devuelva un entero y reciba un sólo argumento y éste sea entero, y sería un error de compilación si le quieres pasar una función que devuelve un float o que recibe dos argumentos, etc.

Hooks

Una función hook (en español, ancla) sirve para lo mismo que una callback, extender la funcionalidad del sistema, pero se implementa diferente. Una función del sistema espera que exista una cierta función y la manda llamar; esto es, no se inyecta:

int hook( int );
void funcion_del_sistema( int a )
{
	hook( a );
}


int hook( int v )
{
	return v+1;
}

int main()
{
	funcion_del_sistema( 5 );
}

Para que este mecanismo funcione debes nombrar a la hook exactamente como el sistema lo espera, y además debe estar visible a la hora de compilar. Del código anterior podrás ver que la función del sistema otra_fun ya no tiene el parámetro para recibir una función, y que dentro manda llamar a una función con un nombre particular, en este ejemplo, hook(). Por lo tanto tú debes escribir una función con la misma firma y el mismo nombre, y además, como mencioné, tu función deberá estar visible al compilador.

Lo que vamos a ver hoy requiere de funciones hook.

Idle task

La idle task (se pronuncia más o menos, [áidel task]) es una función que crea FreeRTOS de manera automática con la prioridad más baja (¿recuerdas la constante tskIDLE_PRIORITY que usamos en la creación de tareas?) y que se ejecuta únicamente cuando no hay tareas de mayor prioridad activas. Esto es, en FreeRTOS siempre habrá una tarea ejecutándose cuando todas las demás estén bloqueadas.

Un uso que FreeRTOS le da a esta tarea, que ya había mencionado en otra lección, es devolver la memoria indicada por una llamada a la función free() en algunos de los esquemas de memoria dinámica (heap_2.c, heap_4.c, y heap_5.c). De manera conveniente FreeRTOS abrió una puerta para que nosotros los programadores tengamos la posibilidad de ejecutar código cada vez que la idle task se ejecuta.

RECUERDA: Tu código debe ser simple y no debe bloquear a la idle task. Así mismo toma en cuenta que esta tarea podría no ejecutarse contínuamente, ya que si la carga de trabajo es alta, entonces ésta, y en consecuencia tu código, se estará ejecutando esporádicamente.

Idle hook

La idle task siempre está en estado Ready (y cuando hay tiempo en el sistema pasa al estado Running), ya sea que se ejecute o no periódicamente. Para que tú inyectes tu código debes realizar cuatro cosas:

1. En el archivo FreeRTOSConfig.h asegúrate que la constante configUSE_IDLE_HOOK está puesta a 1:

#define configUSE_IDLE_HOOK 1

2. (Opcional) La idle task es a final de cuentas una tarea, por lo cual requiere de una pila. El tamaño mínimo de ésta ya viene especificado, pero si tu código necesitase más memoria, entonces debes aumentar su tamaño. En el mismo archivo FreeRTOSConfig.h puedes modificar su valor:

#define configIDLE_STACK_SIZE ( ( UBaseType_t ) 192 )
#define configIDLE_TASK_NAME  "IDLE" // por si quisieras cambiarle el nombre

3. La hook de la idle task se llama vApplicationIdleHook(). Ésta la puedes colocar donde quieras; en el proyecto Molcajete decidí poner todas las hooks en un mismo lugar (aunque puede ser diferente, por supuesto). Abre el archivo hooks.cpp (revisa la lección anterior para más detalles) y ubica a la función:

#if configUSE_IDLE_HOOK == 1
void vApplicationIdleHook()
{

}
#endif

Puedes escribir el código que quieres inyectar a la idle task directamente en esta función, o puedes hacerlo modular (indirecto, por si después necesitas agregar más funcionalidades sin ensuciar al código fuente). Para el ejemplo que veremos más adelante, un heartbeat(), utilizaremos la forma indirecta, es más limpia y el compilador se encarga de la optimización. De esta forma nuestra función vApplicationIdleHook() nos queda como:

#if configUSE_IDLE_HOOK == 1
void vApplicationIdleHook( void )
{
   void heartbeat_idle_hook();
   heartbeat_idle_hook();
}
#endif

4. Cuando activas la inyección de código en la idle task FreeRTOS espera una función vApplicationIdleHook(), la cual acabamos de escribir, pero así como está en este momento no hace nada, falta la función heartbeat_idle_hook() (o como gustes llamarle). Ésta es una función nuestra y la escribiremos en nuestro sketch (o donde nosotros querramos si estamos compilando desde la consola). La implementación completa está más adelante en el ejemplo, pero por el momento veamos su esqueleto para platicar un par de cosas interesantes:

// en un sketch o archivo fuente nuestro:

#ifdef __cplusplus
extern "C"{
#endif
void heartbeat_idle_hook()
{
   taskENTER_CRITICAL();
   {
      // tu código aquí
   }
   taskEXIT_CRITICAL();
}
#ifdef __cplusplus
}
#endif

1. La constante __cplusplus le dice al compilador que las funciones marcadas dentro de extern "C"{ ... } deben ser tratadas como funciones de C, no de C++ (recuerda que los sketches se compilan con C++). El compilador de C++ le cambia el nombre a las funciones para poder implementar cosas muy padres, como la sobrecarga de funciones, pero C no tiene idea de eso. Si la función heartbeat_idle_hook() no estuviera dentro de la sección extern "C"{ ... }, entonces su nombre podría ser algo tan raro como __vjgd74_heartbeat_idle_hook(). C++ sabe de este nombre porque él se lo puso, pero cuando mezclas C++ y C, éste último espera (de hecho es FreeRTOS quien espera) a la función heartbeat_idle_hook(), y aunque pareciera que sí existe, para el compilador de C no es así.

2. Tu código debe estar encerrado en una sección crítica (aplica sólo para micros de 8 bits):

taskENTER_CRITICAL();
{
   // tu código aquí
}
taskEXIT_CRITICAL();

Normalmente esto no es necesario (por «normalmente» hay que entender microcontroladores de 16 o 32 bits), pero de no hacerlo así el programa compilará pero no funcionará correctamente (le pasó al primo de un amigo y casi se vuelve loco). El problema es que en el código del heartbeat() que está más adelante utilizaremos aritmética de 16 bits, pero el ATmega328 es de 8 bits, y la atomicidad de las operaciones no está garantizada en un ambiente multitarea. El problema que tuve es el siguiente: cuando hacía restas (substracciones) sin región crítica obtuve resultados erróneos e inverosímiles, tales como: 1-0=65535 ó 0-0=65535. En un micro de 8 bits las operaciones y asignaciones de 16 y 32 bits ocupan varias instrucciones que deben ser ejecutadas de manera atómica (sin interrupciones), así que para asegurar dicha atomicidad encerré todo el código que involucra aritmética de 16 bits (el tipo TickType_t es de 16 bits) dentro de una región crítica.

RECUERDA: Las regiones críticas se deben ejecutar lo más rápido posible porque has desactivado a las interrupciones.

Tick del sistema

FreeRTOS utiliza un timer del microcontrolador para realizar cambios de contexto (cambiar entre tareas) periódicos. Los detalles de cuál timer y cuál configuración no son importantes en este momento; lo que sí es digno de anotarse es la frecuencia:

// en el archivo FreeRTOSConfig.h:
#define configTICK_RATE_HZ ( ( portTickType ) 1000 )

Esto significa que el planificador de tareas de FreeRTOS es llamado 1000 veces por segundo (la frecuencia), o lo que es lo mismo, una vez cada 1ms (el periodo). Aunque hemos estado utilizando este valor a lo largo del curso, no siempre tiene porqué ser tan rápido; quizás alguna aplicación requiera algo más lento, como 100 ó 10 veces por segundo.

Lo que verdaderamente nos interesa en este instante es que, así como le hicimos con la idle task, también podemos inyectar código en la función de FreeRTOS que es ejecutada en cada tick (con estos valores, cada 1ms).

RECUERDA: Tu código debe ser simple y no debe bloquear al tick. El tick NO es una tarea, es una función del sistema operativo que es llamada cada vez que el tiempo del timer correspondiente expiró, y desde el punto de vista del nivel sistema operativo utiliza la pila verdadera del chip.

Tick hook

Para que inyectes tu código debes realizar tres cosas (aplica la misma teoría de la idle hook, por lo cual no la repetiré a menos que se presenta alguna particularidad):

1. En el archivo FreeRTOSConfig.h asegúrate que la constante configUSE_TICK_HOOK está puesta a 1:

#define configUSE_TICK_HOOK 1

2. La hook del tick hook se llama vApplicationTickHook(). Ésta la puedes colocar donde quieras; en el proyecto Molcajete decidí poner todas las hooks en un mismo lugar (aunque puede ser diferente, por supuesto). Abre el archivo hooks.cpp (revisa la lección anterior para más detalles) y ubica a la función:

#if configUSE_TICK_HOOK == 1
void vApplicationTickHook( void )
{

}
#endif

Puedes escribir el código que quieres inyectar al tick directamente en esta función, o puedes hacerlo modular (indirecto, por si después necesitas agregar más funcionalidades sin ensuciar al código fuente). Para el ejemplo que veremos más adelante, un heartbeat(), utilizaremos la forma indirecta, es más limpia y el compilador se encarga de la optimización. De esta forma nuestra función vApplicationTickHook() nos queda como:

#if configUSE_TICK_HOOK == 1
void vApplicationTickHook( void )
{
   void heartbeat_tick_hook();
   heartbeat_tick_hook();
}
#endif

3. Cuando activas la inyección de código en el tick hook, FreeRTOS espera una función vApplicationTickHook(), la cual acabamos de escribir, pero así como está en este momento no hace nada, falta la función heartbeat_tick_hook() (o como gustes llamarle). Ésta es una función nuestra y la escribiremos en nuestro sketch (o donde nosotros querramos si estamos compilando desde la consola). La implementación completa está más adelante en el ejemplo, pero por el momento veamos su esqueleto:

// en un sketch o archivo fuente nuestro:

#ifdef __cplusplus
extern "C"{
#endif
void heartbeat_tick_hook()
{
   taskENTER_CRITICAL();
   {
      // tu código aquí
   }
   taskEXIT_CRITICAL();
}
#ifdef __cplusplus
}
#endif

Watchdog

En los sistemas embebidos un watchdogsupervisor» en español) es un contador hacia abajo independiente, que cuando llega a cero envía una señal de reset al procesador. Para evitarlo el programa debe estar constantemente alimentándolo (o como la mayoría de textos dice, «pateándolo«, del inglés «kicking the dog«). «Alimentar» significa escribir en unos registros especiales unos valores especiales, y también viene del inglés, feed the dog, otra expresón común con los supervisores. Mientras el programa se ejecuta correctamente, el supervisor es alimentado antes de que la cuenta llegue a cero; pero si el programa se pierde, y puede perderse por muchas razones, entonces ya no lo alimentará dejando que la cuenta llegue a cero, y por lo tanto, reiniciará al sistema.

Aunque cada procesador implementa sus propias formas de inicializar y alimentar al supervisor (sin olvidar que existen supervisores externos), el centro de la discusión no es saber cuándo alimentarlo (obviamente antes de que la cuenta llegue a cero), sino bajo cuáles condiciones debe ser alimentado.

Es decir, no se trata nada más de alimentarlo por alimentarlo, sino hacerlo cuando estemos seguros de que el sistema funciona correctamente. Si detectamos una anomalía, entonces no lo alimentamos, ponemos al sistema en modo seguro, y dejamos que el supervisor reinicie al sistema. Estos pasos son fundamentales en sistemas multitarea, como los que estamos desarrollando: si una tarea crítica falla, de las múltiples que podemos llegar a tener, entonces el sistema debe ser reiniciado.

¿Qué tiene que ver un supervisor con las hooks de FreeRTOS? Te lo responderé de la siguiente manera:

Jamás alimentes al supervisor desde una tarea/interrupción periódica.

Los gurús de los sistemas embebidos

Lo más fácil es alimentar al supervisor desde una interrupción periódica. El problema es que si el programa falla, lo más seguro es que la interrupción continúe, por lo tanto, el sistema no se reiniciará (este tipo de cosas también le han pasado al primo de un amigo).

Nosotros vamos a usar el hook de la idle task para implementar a nuestro supervisor por software. Esta tarea no es ni de cerca periódica, lo cual la convierte en una candidata excelente para esta actividad (y aunque podríamos escribir una tarea de baja prioridad para nuestro supervisor, ya tenemos a la hook, ¡aprovechémosla!). En términos generales, si otorgamos a las tareas críticas una prioridad alta, y alguna de éstas se cuelga, entonces la idle task no se ejecutará, y por lo tanto, tampoco nuestro supervisor por software, y como la alimentación del supervisor por hardware es responsabilidad de ésta, entonces no lo alimentará y el sistema se reiniciará.

¿Porqué no usar la tick hook? Por lo que acabo de mencionar. Al final de cuentas la tick hook no solamente es periódica, sino que funciona por fuera del ámbito de nuestras tareas de usuario, por lo cual, si una tarea se cuelga, el tick siempre seguirá funcionando, dando al traste con la idea principal de los supervisores.

Deadline

Los sistemas embebidos de tiempo real deben realizar su trabajo a tiempo, antes de que sea demasiado tarde: el sistema de frenos de un auto debe activar las bolsas de aire antes de un cierto tiempo (tiempo contabilizado después de haber recibido la señal de impacto), ya que de no hacerlo, el conductor podrá sufrir graves daños. Al tiempo entre que la señal se genera y el sistema responde le llamamos el deadline (o tiempo límite). Existen hard deadlines, como el ejemplo de las bolsas de aire, y también existen soft deadlines, donde pasarse un poco no tiene consecuencias fatales.

Te comento esto porque el ejemplo que veremos no sólo verifica que las tareas no se cuelguen, sino que además reiniciará al sistema si uno o más deadlines no se cumplen.

Implementación del supervisor por software

Hay muchas formas de implementar al supervisor por software (en inglés, «software watchdog«), y la que te mostraré en los ejemplos es una adaptación de una que encontré en:

[LEWIS02] D.W. Lewis. Fundamentals of embedded software. Prentice Hall, USA: 2002.

La idea es la siguiente: tendrás una lista de tareas críticas (no todas las tareas son o deberían ser críticas). Cada tarea tendrá 3 atributos: busy: Bool; failed: Bool; deadline: UINT; last: UINT. busy indica si la tarea está ocupada o no; failed significa si la tarea perdió el deadline (se pasó) o no; deadline guarda el tiempo máximo en el que la tarea debe responder; last indica el tiempo en el que tarea inició su proceso. Al inicio las banderas se ponen a false y estableces el deadline. Cuando un evento se presenta y dispara la ejecución de una tarea, busy se pone a true y guardar la marca de tiempo en last. Cuando la tarea finaliza la bandera failed se pone a true si el deadline se perdió (la tarea se tardó más), e incondicionalmente pone busy a false. El siguiente pseudocódigo muestra la forma general de este método:

// Tarea productora del evento
last[ TASK ] = current_time()
busy[ TASK ] = True
SemaphoreGive(...)


// Tarea consumidora del evento
while( True )
{
	SemaphoreTake(...)

	/* hace lo que tiene que hacer ... */

	if current_time() - last[ TASK ]  > deadline[ TASK ] {
		failed[ TASK ] = True
	}

	busy = False
}

// Software watchdog
Idle_hook()
{
	now = current_time()

	for i = 0; i < MAX_TASKS; ++i {

		if  failed[ i ] == True { break }
		// la tarea perdió su deadline
		
		if busy[ i ] == True && now - last[ i ] > dealine[ i ] { break }
		// ¡ la tarea se perdió por completo !
	}

	if i == MAX_TASKS { feed_the_dog() }
	else { clean_and_reset() }
}

Implementación del watchdog por hardware

El micro ATmega328 incluye un watchdog, pero no lo vamos a usar; en su lugar utilizaremos uno externo. Pero si no tienes uno o no quieres construir uno, entonces puedes usar el interno (aquí está una explicación superficial (en inglés) de cómo usar a este watchdog interno). Ahora que si quieres construir uno, aquí (en mi blog alternativo) podrás encontrar información sobre cómo construir uno con el chip 555. De hecho, la explicación la tuve que sacar de aquí porque la lección se estaba haciendo demasiado larga. Ya sea que visites mi otro blog, o no, aquí te dejo el diagrama del watchdog basado en el 555:

Ejemplos

Ejemplo 1: Heartbeat en la idle hook con tiempo simétrico

Este primer ejemplo, y el más sencillo, muestra cómo implementar un heartbeat() utilizando la hook de la idle task. Como no debemos usar funciones bloqueantes en las hooks (vTaskDelay(), por ejemplo), estaremos calculando diferencias de tiempos.

El tiempo es simétrico porque es el mismo tanto para el LED encendido como apagado. La función xTaskGetTickCount() devuelve el número de ticks transcurridos desde el último reinicio del programa. Nota que el único lugar donde utilizamos la macro de conversión pdMS_TO_TICKS() fue en la constante que establece el intervalo de tiempo; fuera de eso, el resto del código trabaja en término de ticks y no de milisegundos.

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

#define PROGRAM_NAME "hooks_1.ino"

#define TIME_INTERVAL pdMS_TO_TICKS( 500 )
// el LED parpadea cada 500ms

#ifdef __cplusplus
extern "C"{
#endif
void heartbeat_idle_hook()
{
   static bool led_state = false;
   static TickType_t last_time = 0;

   TickType_t current;
   TickType_t elapsed_time;

   taskENTER_CRITICAL();
   {
      current = xTaskGetTickCount(); 
      elapsed_time = current - last_time;

      if( elapsed_time >= TIME_INTERVAL ){

         last_time = current;

         digitalWrite( 13, led_state );
         led_state = !led_state;
      }
   }
   taskEXIT_CRITICAL();
}
#ifdef __cplusplus
}
#endif

void a_task( void* pvParameters )
{
    while( 1 )
    {
        vTaskDelay( pdMS_TO_TICKS( 497 ) );
        Serial.println( "Ok" );
    }
}

void setup()
{
   Serial.begin( 115200 );
   Serial.println( PROGRAM_NAME );

   pinMode( 13, OUTPUT );
   // lo vamos a usar en la hook de la idle task

   xTaskCreate( a_task, "TSK1", 128, NULL, tskIDLE_PRIORITY + 1, NULL );
   // establecemos que esta tarea tenga mayor prioridad que la idle task

   vTaskStartScheduler();

}

void loop() {}

La función xTaskGetTickCount() devuelve un valor de 16 bits para el ATmega328; esto implica que el contador interno de ticks de FreeRTOS pasa de 65535 a 0 (le decimos rollover) cada 65.535 segundos (cada minuto, aprox). Haber utilizado diferencias de tiempo como en la expresión: elapsed_time = current - last_time nos permite obtener siempre resultados correctos que permiten que el programa funcione de manera continua a pesar de los constantes rollovers.

Las variables led_state y last_time son ser estáticas porque deben mantener su valor entre llamadas.

El archivo hooks.cpp, incluyendo nada más el código de la idle hook queda así:

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

#ifdef __cplusplus
extern "C"{
#endif

#if configUSE_MALLOC_FAILED_HOOK == 1
void vApplicationMallocFailedHook()
{
}
#endif

#if configCHECK_FOR_STACK_OVERFLOW > 0
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
}
#endif

#if configUSE_IDLE_HOOK == 1
void vApplicationIdleHook()
{
   void heartbeat_idle_hook();
   heartbeat_idle_hook();
}
#endif

#if configUSE_TICK_HOOK == 1
void vApplicationTickHook( void )
{
}
#endif

#if configUSE_DAEMON_TASK_STARTUP_HOOK == 1
void vApplicationDaemonTaskStartupHook( void )
{
}
#endif

#ifdef __cplusplus
}
#endif

Ejemplo 2: Heartbeat en la idle hook con tiempo asimétrico

Este ejemplo es un poquito más complejo que el anterior y lo puedes utilizar si no quieres un tiempo simétrico, o si tu aplicación necesita intervalos asimétricos. La diferencia principal está en que utiliza la variable led_state como parte de una pequeña máquina de estados. El resto del código es idéntico al ejemplo anterior.

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

#define PROGRAM_NAME "hooks_2.ino"

#define TIME_ON  pdMS_TO_TICKS( 250 )
#define TIME_OFF pdMS_TO_TICKS( 750 )

#ifdef __cplusplus
extern "C"{
#endif
void heartbeat_idle_hook()
{
   static bool led_state = false;
   static TickType_t last_time = 0;

   TickType_t current;
   TickType_t elapsed_time;

   taskENTER_CRITICAL();
   {
      current = xTaskGetTickCount(); 
      elapsed_time = current - last_time;

      if( led_state == false && elapsed_time >= TIME_OFF ){

         digitalWrite( 13, HIGH );
         led_state = true;
         last_time = current;

      } else if( led_state == true && elapsed_time >= TIME_ON ){

         digitalWrite( 13, LOW );
         led_state = false;
         last_time = current;

      } else{  // siempre debemos completar el if-else if-else:
         ; 
      }
   }
   taskEXIT_CRITICAL();
}
#ifdef __cplusplus
}
#endif

void a_task( void* pvParameters )
{
    while( 1 )
    {
        vTaskDelay( pdMS_TO_TICKS( 497 ) );
        Serial.println( "Ok" );
    }
}

void setup()
{
   Serial.begin( 115200 );
   Serial.println( PROGRAM_NAME );

   pinMode( 13, OUTPUT );
   // lo vamos a usar en la hook de la idle task

   xTaskCreate( a_task, "TSK1", 128, NULL, tskIDLE_PRIORITY + 1, NULL );
   // establecemos que esta tarea tenga mayor prioridad que la idle task

   vTaskStartScheduler();
}

void loop() {}

Ejemplo 3: Heartbeat en la tick hook con tiempo asimétrico

En este tercer ejemplo vamos a implementar el heartbeat() con tiempos asimétricos utilizando la tick hook. A diferencia de lo que hicimos en la idle hook, donde teníamos que utilizar diferencias de tiempos porque no sabíamos cada cuándo se activaría la tarea, en este caso solamente vamos a «contar» ticks, ya que esta hook se llama 1 vez en cada tick:

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

#define PROGRAM_NAME "hooks_3.ino"

#define TIME_ON  pdMS_TO_TICKS( 100 )
#define TIME_OFF pdMS_TO_TICKS( 900 )

#ifdef __cplusplus
extern "C"{
#endif
void heartbeat_tick_hook()
{
   static bool led_state = false;
   static uint16_t ticks = TIME_OFF;

   taskENTER_CRITICAL();
   {
      --ticks;
      if( ticks == 0 ){

         digitalWrite( 13, led_state );
         led_state = !led_state;

         ticks = led_state == false ? TIME_ON : TIME_OFF;
      }
   }
   taskEXIT_CRITICAL();
}
#ifdef __cplusplus
}
#endif

void a_task( void* pvParameters )
{
    while( 1 )
    {
        vTaskDelay( pdMS_TO_TICKS( 497 ) );
        Serial.println( "Ok" );
    }
}

void setup()
{
   Serial.begin( 115200 );
   Serial.println( PROGRAM_NAME );

   pinMode( 13, OUTPUT );
   // lo vamos a usar en la hook del tick system


   xTaskCreate( a_task, "TSK1", 128, NULL, tskIDLE_PRIORITY, NULL );
   // no importa la prioridad

   vTaskStartScheduler();
}

void loop() {}

Antes de ejecutar este programa asegúrate que en el archivo hooks.cpp tienes las siguientes líneas de código (no pongo el contenido completo de este archivo para no alargar la lección):

#if configUSE_TICK_HOOK == 1
void vApplicationTickHook( void )
{
   void heartbeat_tick_hook();
   heartbeat_tick_hook();
}
#endif

Ejemplo 4: Supervisor por software basado en la idle hook y el chip 555

Este ejemplo es una implementación del supervisor por software que mencioné anteriormente. Tiene 4 tareas, 3 de ellas críticas (la task_3() no es crítica).

Agrupé en una structura los parámetros necesarios para el supervisor, en lugar de mantenerlos sueltos, y declaré un arreglo para 3 tareas críticas:

struct Watchdog
{
   bool       busy{ false };
   bool       failed{ false };
   TickType_t deadline{ 0 };
   TickType_t last_time{ 0 };

   // podríamos agregar un ID que nos ayude a identificar la tarea que falló
};

// No todas las tareas deben ser supervisadas:
#define MAX_TASKS 3
Watchdog tasks[ MAX_TASKS ];

El código del supervisor es el siguiente. Un ciclo recorre la lista de tareas críticas y en cuanto una de ellas se cuelgue o el deadline no se haya cumplido, sale y ejecuta la función clean_and_reset(); en caso de no encontrar problemas, llama a la función feed_the_dog():

void watchdog_idle_hook()
{
   TickType_t now;
   uint8_t i;

   // las secciones críticas se necesitan cuando un micro de 8 bits debe hacer
   // operaciones o asignaciones de 16 o 32 bits en un ambiente multitarea

   taskENTER_CRITICAL();
   {
      now = xTaskGetTickCount();

      for( i = 0; i < MAX_TASKS; ++i ){

         if( tasks[ i ].failed == true ) break;
         // la tarea perdió el deadline (se tardó más de lo debido)

         TickType_t diff = now - tasks[ i ].last_time;
         if( tasks[ i ].busy == true && diff > tasks[ i ].deadline ) break;
         // la tarea se perdió
      }

      if( i == MAX_TASKS ) feed_the_dog();
      // ninguna tarea se colgó y todos los deadlines se cumplieron

      else clean_and_reset();
      // una o más tareas se colgaron, o uno o más deadlines no se cumplieron
   }
   taskEXIT_CRITICAL();
}

La función para alimentar al supervisor externo es feed_the_dog(), la cual envía un pulso negativo (ALTO-BAJO-ALTO) de 100 microsegundos antes de que el tiempo expire. Escogí el pin D4 de Arduino:

void feed_the_dog()
{
   // alimentamos al perro guardián:

   digitalWrite( 4, LOW );
   delayMicroseconds( 100 );
   digitalWrite( 4, HIGH );
}

Sin mayor problema podrías adaptarla para que alimentes al watchdog interno, si es que decidiste utilizarlo.

La función clean_and_reset() es llamada cuando el sistema falló. Ésta forza un reinicio, pero no sin antes poner al sistema a un estado seguro, y avisar de alguna forma, que algo salió mal:

void clean_and_reset()
{
   // pone el sistema a un estado seguro


   Serial.println( "***" );
   // avisamos con un texto, un LED, un timestamp, etc.


   // obligamos a un watchdog reset:
   vTaskSuspendAll();
   taskDISABLE_INTERRUPTS();
   while( 1 )
      ;
}

El esqueleto de una tarea crítica en este programa es:

void critical_task( void* pvParameters )
{
   tasks[ 0 ].deadline = TIME_LIMIT;
   // el deadline es estático

   // inicializa lo relacionado con la tarea...

   while( 1 )
   {
      tasks[ TASK ].busy = true;
      tasks[ TASK ].last_time = xTaskGetTickCount();


      // --- Empieza el proceso:

      // aquí va el proceso crítico...

      // --- Finalizó el proceso


      TickType_t diff = xTaskGetTickCount() - tasks[ TASK ].last_time;
      if( diff > tasks[ TASK ].deadline ) tasks[ TASK ].failed = true;

      tasks[ TASK ].busy = false;
   }
}

Finalmente, actualiza el código del archivo hooks.cpp para que la idle hook mande llamar al supervisor:

#if configUSE_IDLE_HOOK == 1
void vApplicationIdleHook()
{
   void watchdog_idle_hook();
   watchdog_idle_hook();
}
#endif

(No olvides poner la constante configUSE_IDLE_HOOK a 1 en el archivo FreeRTOSConfig.h)

Ya tenemos todos los elementos para poder ver al ejemplo completo:

  • La task_1 hace parpadear a un LED a una razón que se va incrementendo en cada paso lo que hace que a larga el programa falle.
  • La task_2 imprime su nombre en la consola serial. No falla.
  • Las tareas task_3 y task_4 trabajan de manera coordinada; la primera hace las veces de generadora de eventos, mientras que la segunda los consume. Este par de tareas podrían hacer que el sistema falle.

Por supuesto las tareas las diseñé así para que el programa falle bajo condiciones controladas.

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

#define PROGRAM_NAME "hooks_4.ino"

#define TIME_LIMIT_1 pdMS_TO_TICKS( 500 )
#define TIME_LIMIT_2 pdMS_TO_TICKS( 500 )
#define TIME_LIMIT_3 pdMS_TO_TICKS( 500 )


struct Watchdog
{
   bool       busy{ false };
   bool       failed{ false };
   TickType_t deadline{ 0 };
   TickType_t last_time{ 0 };

   // podríamos agregar un ID que nos ayude a identificar la tarea que falló
};


// No todas las tareas deben ser supervisadas:
#define MAX_TASKS 3
Watchdog tasks[ MAX_TASKS ];


TaskHandle_t consumer_handler = NULL;
// lo necesita una de las tareas (la productora)


#ifdef __cplusplus
extern "C"{
#endif

void feed_the_dog()
{
   // alimentamos al perro guardián:

   digitalWrite( 4, LOW );
   delayMicroseconds( 100 );
   digitalWrite( 4, HIGH );
}

void clean_and_reset()
{
   // pone el sistema a un estado seguro


   Serial.println( "***" );
   // avisamos con un texto, un LED, un timestamp, etc.


   // obligamos a un watchdog reset:
   vTaskSuspendAll();
   taskDISABLE_INTERRUPTS();
   while( 1 )
      ;
}

//----------------------------------------------------------------------
//                     watchdog_idle_hook()
//----------------------------------------------------------------------
void watchdog_idle_hook()
{
   TickType_t now;
   uint8_t i;

   // las secciones críticas se necesitan cuando un micro de 8 bits debe hacer
   // operaciones o asignaciones de 16 o 32 bits en un ambiente multitarea

   taskENTER_CRITICAL();
   {
      now = xTaskGetTickCount();

      for( i = 0; i < MAX_TASKS; ++i ){

         if( tasks[ i ].failed == true ) break;
         // la tarea perdió el deadline (se tardó más de lo debido)


         TickType_t diff = xTaskGetTickCount() - tasks[ i ].last_time;
         if( tasks[ i ].busy == true && diff > tasks[ i ].deadline ) break;
         // la tarea se perdió

      }

      if( i == MAX_TASKS ) feed_the_dog();
      else clean_and_reset();
   }
   taskEXIT_CRITICAL();
}
#ifdef __cplusplus
}
#endif


//----------------------------------------------------------------------
//                          task_1()
//----------------------------------------------------------------------
void task_1( void* pvParameters )
{
   tasks[ 0 ].deadline = TIME_LIMIT_1;
   // el deadline es estático

   pinMode( 2, OUTPUT );
   // un led en D2

   bool led_state = false;

   uint16_t excess_time = pdMS_TO_TICKS( 50 );

   while( 1 )
   {

      tasks[ 0 ].busy = true;
      tasks[ 0 ].last_time = xTaskGetTickCount();


      // --- Empieza el proceso:

      vTaskDelay( excess_time );

      digitalWrite( 2, led_state );
      led_state = !led_state;

      Serial.println( pcTaskGetName( NULL ) );
      // hacemos tiempo calculando el nombre en lugar de usar texto estático

      // --- Finalizó el proceso


      TickType_t diff = xTaskGetTickCount() - tasks[ 0 ].last_time;
      if( diff > tasks[ 0 ].deadline ) tasks[ 0 ].failed = true;

      tasks[ 0 ].busy = false;


      excess_time += pdMS_TO_TICKS( 5 );
   }
}

//----------------------------------------------------------------------
//                          task_2()
//----------------------------------------------------------------------
void task_2( void* pvParameters )
{
   tasks[ 1 ].busy = false;
   tasks[ 1 ].failed = false;
   tasks[ 1 ].deadline = TIME_LIMIT_2;

   while( 1 )
   {
      tasks[ 1 ].busy = true;
      tasks[ 1 ].last_time = xTaskGetTickCount();


      // --- Empieza el proceso:

      vTaskDelay( pdMS_TO_TICKS( 93 ) );

      Serial.println( pcTaskGetName( NULL ) );

      // --- Finalizó el proceso


      TickType_t diff = xTaskGetTickCount() - tasks[ 1 ].last_time;
      if( diff > tasks[ 1 ].deadline ) tasks[ 1 ].failed = true;
      tasks[ 1 ].busy = false;
   }
}

//----------------------------------------------------------------------
//                          task_3()
//----------------------------------------------------------------------
void task_3( void* pvParameters )
{
   configASSERT( consumer_handler );

   // las inicializamos aquí porque la tarea productora tiene mayor prioridad que la consumidora:
   tasks[ 2 ].busy = false;
   tasks[ 2 ].failed = false;
   tasks[ 2 ].deadline = TIME_LIMIT_3;


   TickType_t last_wake_time = xTaskGetTickCount();

   uint16_t excess_time = 0;

   while( 1 )
   {
      vTaskDelayUntil( &last_wake_time, pdMS_TO_TICKS( 111 + excess_time  ) );

      // actualizamos los atributos ANTES de dar el aviso:
      tasks[ 2 ].busy = true;
      tasks[ 2 ].last_time = xTaskGetTickCount();

      digitalWrite( 13, HIGH );

      xTaskNotifyGive( consumer_handler );
      // avisamos

      Serial.println( pcTaskGetName( NULL ) );

      excess_time += 10;
   }
}

//----------------------------------------------------------------------
//                          task_4()
//----------------------------------------------------------------------
void task_4( void* pvParameters )
{
   while( 1 )
   {
      // --- Empieza el proceso:

      // los atributos fueron establecidos en el productor antes de dar el aviso

      ulTaskNotifyTake( pdTRUE, portMAX_DELAY );

      Serial.println( pcTaskGetName( NULL ) );

      digitalWrite( 13, LOW );

      // --- Finalizó el proceso


      TickType_t diff = xTaskGetTickCount() - tasks[ 2 ].last_time;
      if( diff > tasks[ 2 ].deadline ) tasks[ 2 ].failed = true;

      tasks[ 2 ].busy = false;
   }
}

//----------------------------------------------------------------------
//                         setup()
//----------------------------------------------------------------------
void setup()
{
   Serial.begin( 115200 );
   Serial.println( PROGRAM_NAME );

   pinMode( 13, OUTPUT );

   pinMode( 4, OUTPUT );
   digitalWrite( 4, HIGH );

   xTaskCreate( task_1, "TSK1", 128, NULL, tskIDLE_PRIORITY + 0, NULL );
   // requiere supervisión, podría fallar

   xTaskCreate( task_2, "TSK2", 128, NULL, tskIDLE_PRIORITY + 1, NULL );
   // requiere supervisión, pero nunca falla

   xTaskCreate( task_3, "TSK3", 128, NULL, tskIDLE_PRIORITY + 2, NULL );
   // productor (no requiere supervisión)

   xTaskCreate( task_4, "TSK4", 128, NULL, tskIDLE_PRIORITY + 1, &consumer_handler );
   // consumidor (requiere supervisión)


   feed_the_dog();

   vTaskStartScheduler();
}

void loop() {}

¿Qué sigue?

Hoy vimos 3 temas importantes: 2 sobre cómo inyectar código nuestro al sistema operativo, y otro sobre cómo implementar y utilizar un supervisor por software utilizando un watchdog externo.

Tú podrías utilizar ya sea un watchdog externo propiamente dicho, o el interno al ATmega328.

RECUERDA: No utilices funciones bloqueantes dentro de las hooks de FreeRTOS.

RECUERDA: Nunca alimentes al watchdog desde una tarea o interrupción periódica.

RECUERDA: Revisa que el sistema está en perfectas condiciones antes de alimentar al supervisor.

Índice del curso

Espero que esta lección 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.


Si encuentras este blog interesante, entonces podrías considerar suscribirte a él y recibir información relevante sobre tecnología y sistemas embebidos, y de vez en cuando, uno que otro regalo.

Fco. Javier Rodríguez

Deja una respuesta