Administración de la memoria

Administración de la memoria

No podíamos terminar este curso sin mencionar brevemente la forma en que FreeRTOS administra la memoria. De ésta tenemos dos tipos, la memoria dinámica y la memoria estática, siendo la primera la que más problemas da. La memoria dinámica es memoria RAM que le pedimos al sistema operativo cuando el programa ya está ejecutándose. En programas de computadora esta es una actividad que realizamos todo el tiempo, sin apenas darnos cuenta. Aunque lenguajes como C/C++ nos obligan a devolver manualmente la memoria que hayamos pedido, hay otros más “pesados” (en términos de recursos y tiempo de procesamiento) que devuelven la memoria por nosotros, y además pareciera, en cualquier caso, que la memoria RAM nunca se termina. Y somos felices.

Sin embargo, los sistemas embebidos son arena de otro costal. La memoria RAM está severamente limitada y raras veces podemos darnos el lujo de pedir y devolver memoria en tiempo de ejecución. Y aunque tuviéramos la posibilidad de hacerlo debemos preguntarnos si eso es realmente lo que queremos.

La memoria dinámica tiene varios problemas que se amplifican en nuestros sistemas con recursos limitados:

  • Las llamadas a la función malloc() no son deterministas; esto es, mientras que una llamada puede tomar 300 microsegundos, otra podría tomar 5 milisegundos, y luego otra 1 milisegundo.
  • La función malloc() no es re-entrante. Esto es, la función no está diseñada para ser accesada por dos o más tareas al mismo tiempo en un programa concurrente, con lo cual el sistema podría corromperse. Este problema se podría resolver desactivando las interrupciones o al sistema operativo mientras malloc() se está ejecutando. El problema es que en algunos casos la búsqueda de un bloque de memoria contiguo puede tardar mucho.
  • La memoria se fragmenta. Cuando tu programa inicia toda la memoria es contigua, pero cada vez que la pides y la devuelves se van creando huecos, y podría llegar a ser que tengas, digamos, 100 bloques no contiguos (es decir, con huecos entre uno y otro) de 16 bytes (por dar un número) cada uno. En total tienes 1600 bytes NO contiguos. ¿Qué sucedería si haces una petición de 20 bytes? La llamada a malloc() va a fallar porque en el escenario propuesto no hay ningún bloque contiguo de al menos 20 bytes. “Oye, pero tengo 1600 bytes, ¿cómo es eso posible?”. Bueno, la memoria se fragmentó.
  • Fugas de memoria. Las funciones que piden memoria, deben devolverla (en los casos más clásicos. Cuando veamos los diferentes esquemas de FreeRTOS retomaré este punto), ¿y qué pasa si la devuelves? Ese bloque de memoria queda marcado como “en uso”, por lo que aunque ya no la estés utilizando, a los ojos del sistema operativo sí lo estás haciendo. La consecuencia es que nadie más, durante la vida del programa, podrá usar esa memoria.
  • FreeRTOS no devuelve la memoria inmediatamente. Uno pensaría que cuando llamas a la función free() la memoria se devuelve al momento y queda lista para volver a ser utilizada. Bueno, esto no es del todo cierto. FreeRTOS devuelve la memoria cuando tiene un poco de tiempo para hacerlo. Esto significa que si la carga de trabajo de tu aplicación es alta y FreeRTOS no se hace de tiempo, entonces esa memoria que devolviste… realmente no ha sido devuelta. Y lo peor de todo es que el escenario no es determinista (otra vez) por lo que repetir el incidente para investigarlo será casi imposible.
  • ¿Cuánta memoria es suficiente?. La memoria dinámica se toma de una región llamada heap, y tú estableces el tamaño (en bytes) de esa región. En los ejemplos que hemos estado viendo el heap está establecido como 1500 bytes, de los 2048 que tiene el ATmega328. ¿1500 es mucho o poco? ¿Porqué no los 2048? ¿Porqué no 1024? ¿Qué sucede con los bytes que no usemos del heap? FreeRTOS proporciona herramientas para determinar si 1500 bytes es mucho o poco, lo cual es tema de esta lección.

Durante mucho tiempo la creación dinámica de objetos fue la única disponible en FreeRTOS, y no fue sino hasta la versión 9.0.0 que se introdujo la creación estática de objetos. Y antes de ella siempre me había preguntado porqué no estaba “de fábrica”, ya que yo conocía de los problemas de la memoria dinámica. De hecho, los gurús de los sistemas embebidos advierten sobre su utilización y aconsejan no usarla. Afortunadamente me leyeron la mente e introdujeron el concepto a FreeRTOS.

Ahora que si a pesar de los problemas descritos, quieres seguir utilizando la creación dinámica de objetos, o estás trabajando sobre un proyecto que ya la usa (ya me pasó), o tu aplicación a mano los necesita, sí o sí, entonces lo mejor es que conozcas cómo se comporta y se administra la memoria para que no te lleves (desagradables) sorpresas.

En esta lección vamos a platicar sobre algunos conceptos importantes relacionados con la memoria, y de herramientas de FreeRTOS que nos permitirán determinar los tamaños óptimos para el heap y las pilas de las tareas, para intentar disminuir los problemas que se nos presenten.

Tabla de contenidos

¿Qué es y qué necesito?

Vamos a platicar sobre dos problemas que se nos presentan en el desarrollo de programas con FreeRTOS (y en general, con cualquier sistema operativo):

  • Desboardamiento del heap: un heap demasiado grande equivale a desperdicio de memoria, y uno demasiado pequeño equivale a problemas.
  • Desbordamiento de la pila. Igual que el anterior, pero con la pila de cada tarea.

Y también sobre algunas herramientas que FreeRTOS ofrece para afinar sus respectivos tamaños.

Heap

¿De dónde sale la memoria para la creación dinámica de objetos? La memoria es tomada de un área conocida como el heap (se pronuncia más o menos [jép]. En español es montón). En FreeRTOS es un arreglo cuyo tamaño tú defines en el archivo FreeRTOSConfig.h:

#define configTOTAL_HEAP_SIZE (( UBaseType_t )( 1500 ))

(Para el ATmega328 el tipo UBaseType_t está difinido como unsigned char. Para más detalles revisa el archivo portmacro.h.)

Aunque el ATmega328 tiene 2048 bytes de RAM, no es recomendable usar toda para el heap, ya que cuando declaras variables globales, o usas funciones antes de iniciar al sistema operativo, o usas interrupciones, necesitarás memoria que no es parte del heap.

Como mencioné en la introducción, ¿1500 bytes es mucho o poco? ¿Y si tengo dos tareas nada más, cada una con una pila de 256 bytes, qué sucede con la memoria no utilizada del heap? Hacer a ésta área de memoria muy grande o muy chica tiene sus consecuencias. Si la haces muy grande y no la utilizas toda, entonces estarás desperdiciando valiosa memoria (la RAM no se da en los árboles); pero si la haces muy chica, entonces se terminará antes de lo esperado y no podrás crear más objetos dinámicos mientras el programa se está ejecutando (tiempo de ejecución, le decimos). Los objetos estáticos no tienen esta disyuntiva: si eventualmente se termina la memoria lo sabrás cuando estés compilando el programa (tiempo de compilación) y sabrás qué hacer antes de instalar tu aplicación en la copa de un árbol; en el caso de que se agote la memoria en tiempo de ejecución, entonces tendrás que subir a la copa del árbol a presionar el botón de reset.

En FreeRTOS es posible que una tarea cree a otra tarea (u objetos dinámicos), pero lo más recomendable, en la medida de los posible, es crear TODOS los objetos dinámicos al inicio del programa, y probar que realmente se hayan creado, y no sólo suponerlo. Además, tampoco deberías destruirlos (es decir, devolver su memoria). Este escenario nos hace pensar en utilizar, mejor, objetos estáticos.

En algunas aplicaciones podría ser prohibitivo o inconveniente, desde muchos ángulos, probar el valor valor devuelto por la función xTaskCreate() de todas las tareas que hayas creado. Para estos casos puedes indicarle a FreeRTOS que te avise cuando una llamada a malloc() falle, es decir, cuando malloc() no haya encontrado la memoria contigua requerida.

Primero deberás decirle a FreeRTOS que quieres el aviso. Para ello debes poner a 1 la constante configUSE_MALLOC_FAILED_HOOK en el archivo FreeRTOSConfig.h:

#define configUSE_MALLOC_FAILED_HOOK 1

Luego debes escribir una función callback (también conocidas como hooks) que será llamada por FreeRTOS. Una función callback o hook es una función que tú escribes, pero que el programa (en este caso FreeRTOS) utilizará como si fuera parte de él, sin ser parte de él. También le llamamos inyección de dependencias. Anteriormente en el curso ya hemos usado varias callbacks, así que el término no te debería ser desconocido. La función callback que deberás escribir es la siguiente:

Dentro de ella tú estableces qué hacer en caso de que la memoria del heap se haya agotado, y todo dependerá de la aplicación que estés escribiendo. Las acciones podrían ir desde activar un LED de error, o escribir un mensaje en la consola, o algo más drástico como resetear al sistema. Ten en cuenta que esta función la utilizarías mientras el programa está en desarrollo o pruebas y es para que te des cuenta si el tamaño de heap debe ser incrementado.

En el Ejemplo 1 que veremos más adelante está una posible implementación. Para que este mecanismo funcione asegúrate de crear todos los objetos dinámicos, directa o indirectamente, que tu programa va a necesitar.

Desbordamiento de la pila

Cuando creas tareas debes especificar la cantidad de memoria para su pila. La pila es una estructura de datos dinámica (en el sentido de que su contenido está cambiando continuamente) que almacena las variables declaradas en la tarea, así como los argumentos que envías a funciones y los valores que éstas pudieran devolver. Si tu tarea, o las funciones a las que llama, declaran muchas variables (o unas pocas pero del tipo arreglo), o las llamadas a las funciones son muy profundas, corres el riesgo de que la pila se desborde por tener un tamaño insuficiente. El desbordamiento de la pila es cuando se rebasa el tamaño máximo de la misma, por las razones que mencioné. ¿Cuál es el tamaño ideal para una pila? Es difícil decirlo; esta es una tarea que involucra la experiencia y el sentido común. FreeRTOS propone un tamaño mínimo para la pila para cada procesador. En el caso del ATmega328 es:

#define configMINIMAL_STACK_SIZE ( ( UBaseType_t ) 85 )

Y de ahí para arriba. Pero ¿cuánto es suficiente? Afortunadamente FreeRTOS incluye herramientas que nos permitirán verificar si la pila se ha desbordado, y en caso afirmativo, afinar su tamaño. Debes activar esta herramienta antes de usarla, estableciendo un valor 1 ó 2 en el archivo FreeRTOSConfig.h:

/*
 * 0: Deshabilitada
 * 1: Precisa
 * 2: Más precisa
 */
#define configCHECK_FOR_STACK_OVERFLOW 1

Cuando este mecanismo detecte un desbordamiento de la pila mandará llamar una función hook llamada vApplicationStackOverflowHook():

Los argumentos xTask y pcTaskName contienen cada uno, de manera respectiva, el handler y el nombre de la tarea que hizo que llegaras ahí.

(Nota: Por algún desfase entre el código y la documentación, el tipo del parámetro pcTaskName se muesra como signed char*, sin embargo el compilador lo rechazará; en su lugar usa char* únicamente, como lo he hecho en los ejemplos más adelante.)

En caso de un problema con la pila FreeRTOS buscará la función vApplicationStackOverflowHook() en tu proyecto y llevará a cabo lo que ahí indiques.

¿Qué hay de los valores 1 y 2 de la constante configCHECK_FOR_STACK_OVERFLOW? En realidad son dos métodos para la detección de los desbordamientos.

Método 1

configCHECK_FOR_STACK_OVERFLOW es puesta a 1. FreeRTOS observará, en cada cambio de contexto, si la pila no ha sido desbordada. En caso de desbordamiento llamará a la función hook vApplicationStackOverflowHook().

Método 2

configCHECK_FOR_STACK_OVERFLOW es puesta a 2. FreeRTOS llenará la pila con un valor conocido cuando la tarea sea creada. Posteriormente, en cada cambio de contexto observará los últimos 16 bytes de la pila para ver si cambiaron con respecto al valor inicial. Este método es más lento, pero te avisará antes de un posible desbordamiento. El aviso viene en forma de una llamada a la función hook vApplicationStackOverflowHook(). Esta funcionalidad se presta para ser utilizada con un depurador por hardware.

Ambos métodos introducen más carga de trabajo al sistema, por lo que deberías utilizarlas solamente mientras desarrollas la aplicación, o en la etapa de pruebas. Además, no tiene sentido continuar un programa si su pila se ha desbordado, so pena de muchos problemas.

FreeRTOS indica que no existe una garantía de capturar todos los desbordamientos, y es lógico. Pero tú puedes ayudarlo haciendo que tu programa recorra todos los caminos posibles en la lógica de tu aplicación. Imagina que uno de estos caminos manda llamar a funciones que ocupan mucha memoria, pero asumes que “en la vida real” la condición para tomar ese camino “nunca” se dará. Resulta que como la vida real es caprichosa (“Si puede suceder, entonces sucederá“) un día se le ocurrirá tomar ese camino y la pila se desbordará (ya cuando tu aplicación ha sido distribuída y el dispositivo ha sido instalado en la copa de un árbol en medio de la selva). Moraleja:

Si programas un camino crítico en tu programa, entonces haz pruebas que lo recorran.

Antes de terminar esta sección sobre conceptos veamos las diferentes opciones que FreeRTOS ofrece para las funciones malloc()/free().

Implementaciones de malloc

FreeRTOS incluye 5 implementaciones de malloc(), cada una con características y potencias diferentes, dentro de archivos con el nombre heap_x.c (donde la x va desde 1 hasta 5):

  1. heap_1.c. Es la implementación más simple y no permite devolver la memoria (la función free() no está definida). Su uso no es recomendado debido a que la creación estática de objetos la vino a substituir.
  2. heap_2.c. Igual que heap_1.c, pero permite devolver la memoria, sin juntar los fragmentos, cuyo resultado es memoria fragmentada.
  3. heap_3.c. Utiliza las funciones malloc() y free() de tu compilador/plataforma. Por ejemplo, en el caso del ATmega328 esta implementación simplemente manda llamar a dichas funciones que ya vienen incluídas en el compilador avr-gcc. NOTA: El projecto Molcajete utiliza este esquema; pero si tú quisieras usar algún otro, simplemente descárgalo y cópialo en el directorio /src de FreeRTOS. (Por favor lee la nota NOTA IMPORTANTE al final del primer ejemplo.)
  4. heap_4.c. Igual que heap_2.c, pero junta los fragmentos. Por supuesto es más lenta, pero la memoria no se fragmenta. FreeRTOS recomienda utilizar este esquema en lugar de heap_2.c para programas nuevos.
  5. heap_5.c. Igual que heap_4.c, pero permite distribuir el heap entre diferentes regiones de RAM. Algunos microcontroladores modernos y potentes incluyen no una región de RAM, sino varias no contiguas. Este esquema aprovecha todas las regiones, y obviamente es el más complicado de todos.

Recuerda: Solamente un esquema de los 5 mencionados debe estar activo a un mismo tiempo, de lo contrario el programa no compilará (por duplicidad de símbolos).

Función uxTaskGetStackHighWaterMark()

Si quieres saber cuánta memoria queda en la pila de una tarea debes utilizar la función uxTaskGetStackHighWaterMark():

Esta función devuelve el número de elementos aún no utilizados (words o bytes, depende de la definición de UBaseType_t) desde que la tarea indicada por el argumento xTask inició. Puedes usar el valor NULL para referirte a la propia tarea que hizo la llamada. En los ejemplos más adelante veremos su uso.

Configuración

Antes de ver los ejemplos de esta sección debes de saber que la forma de construir (compilar) sketches es un tanto especial cuando integras código de terceros, tal como FreeRTOS, y las cosas no siempre funcionan como uno esperaría. Y este es el caso de las funciones hook que te he platicado.

Idealmente deberías escribir el código de la hook en el mismo sketch de tu aplicación, pero no funciona. Por esta razón creé originalmente un archivo especial hooks.c en el directorio /src de FreeRTOS del proyecto Molcajete. Pero este tiene un problema, ¡es un archivo de lenguaje C, no de C++!. Esto significa que en dicho archivo no podrás usar objetos de C++, como por ejemplo Serial.println(), ya que en C no existen objetos. Pero si utilizaras funciones como digitalWrite() no tendrías inconvenientes. Sin embargo, para efectos de depuración de los programas es mejor contar con una salida de texto.

Por ello deberás realizar los siguientes 3 simples pasos antes de probar los ejemplos de la siguiene sección:

  1. Ubica (pero no lo abras, no es necesario) al archivo hooks.c, el cual está en /tu/ruta/de/instalación/de/molcajete/arduino-1.8.13/libraries/FreeRTOS/src.
  2. Cámbiale el nombre a hooks.c.txt. Este paso es para que no lo borres y para que el compilador no lo considere en el proceso de compilación. Por supuesto que también puedes borrarlo con toda seguridad.
  3. En ese mismo directorio crea un nuevo archivo hooks.cpp con el siguiente contenido (copia y pega para evitar errores de dedo):
#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()
{
}
#endif

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

#ifdef __cplusplus
}
#endif

En este archivo he concentrado todas las hooks que FreeRTOS pudiera llegar a necesitar; para la lección de hoy solamente utilizaremos las primeras dos.

Ejemplos

Los ejemplos a continuación los vas a estar trabajando con tres archivos abiertos: hooks.cpp (para que escribas el código de tus hooks), FreeRTOSConfig.h (para jugar con las constantes), y los archivos de tus sketches.

Ejemplo 1: Terminándonos el heap

(* Nota importante al final del ejemplo.)

En este ejemplo vamos a pedir más memoria de la que establecimos para el heap y veremos si la función vApplicationMallocFailedHook() se manda llamar. Recuerda que estas herramientas son para cuando estás desarrollando o probando tus aplicaciones, no son para código de producción (es decir, el código que ejecuta tu aplicación ya en campo).

Abre el archivo hooks.cpp y modifica la función vApplicationMallocFailedHook() con el siguiente contenido:

void vApplicationMallocFailedHook()
{
   Serial.println( "ERR: Se agotó el heap. El programa se detiene." );

   while( 1 )
      ;
}

El ciclo infinito while( 1 ); es para que el programa no continúe. Si esta función se llega a llamar es porque debes aumentar el heap o afinar el tamaño de la pila de cada tarea. Que el programa continue no tendría sentido.

Es importante que en este tipo de funciones no utilices funciones bloqueantes (xTaskDelay(), temporizadores, xQueueReceive(), etc) dado que tu programa está en una situación crítica; sin embargo sí que podrás utilizar algunas funciones de FreeRTOS que devuelven valores sin bloquearse (es decir, que regresan inmediatamente), o funciones de Arduino que no sean bloqueantes, como digitalWrite(), por ejemplo.

Luego escribe un sketch con el siguiente contenido:

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

// No olvides poner configUSE_MALLOC_FAILED_HOOK a 1 en el archivo FreeRTOSConfig.h

#define PROGRAM_NAME "mem_mang_1.ino"


void led_task( void* pvParameters )
{
    uint8_t pin = (uint8_t) pvParameters;

    pinMode( pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( pin, HIGH );
        vTaskDelay( pdMS_TO_TICKS( 497 ) );
        digitalWrite( pin, LOW );
        vTaskDelay( pdMS_TO_TICKS( 497 ) );
    }
}

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


   xTaskCreate( led_task, "LD1", 256, (void*) 13, tskIDLE_PRIORITY, NULL );
   Serial.println( "La tarea 1 se creó ..." );

   xTaskCreate( led_task, "LD2", 256, (void*) 12, tskIDLE_PRIORITY, NULL );
   Serial.println( "La tarea 2 se creó ..." );

   xTaskCreate( led_task, "LD3", 256, (void*) 11, tskIDLE_PRIORITY, NULL );
   Serial.println( "La tarea 3 se creó ..." );

   xTaskCreate( led_task, "LD4", 256, (void*) 10, tskIDLE_PRIORITY, NULL );
   Serial.println( "La tarea 4 se creó ..." );


   vTaskStartScheduler();
}

void loop() {}

Este programa intentará crear 4 tareas (reutilizando el código de una misma, aunque eso es lo de menos en el ejemplo), cada una pidiendo 256 words, es decir, 512 bytes para cada pila. Este ejemplo lo voy a ejecutar utilizando la implementación heap_3.c, y para ésta el valor de la constante configTOTAL_HEAP_SIZE no es tomado en cuenta, por lo que su valor es irrelevante (el porqué usé el esquema heap_3.c lo explico en la nota al final del ejemplo).

#define configTOTAL_HEAP_SIZE (( UBaseType_t )( 1500 )) // Irrelevante para heap_3.c

La salida de este programa es:

En este ejemplo nosotros sabemos dónde y porqué se agotó el heap y porqué el programa se detuvo, dado que ese es el objetivo del ejemplo; sin embargo, en un escenario real donde crees objetos dinámicos de forma dispersa será difícil determinar, con el método Serial.println(), dónde fue el punto exacto donde el programa falló. Para determinarlo deberás utilizar un depurador por hardware, tal como el AVR Dragon (para el chip ATmega328). Microcontroladores más modernos usan depuradores más potentes, o las tarjetas donde vienen montados (las conocidas como tarjetas de desarrollo o de evaluación) integran el soporte para la depuración por hardware.

Parte de mi colección de tarjetas de desarrollo. Puedes ver que algunas integran al depurador por hardware en la misma tarjeta, mientras que en otras conecté depuradores externos (ambos del tipo JTAG). Desafortunadamente es muy complicado depurar por hardware al ATmega328 en Linux.

*NOTA IMPORTANTE: En esta lección hablé sobre creación dinámica de tareas, y en la del día de hoy hablé de las diferentes implementaciones para las funciones malloc() y free(), y durante todo el curso he estado utilizando la versión heap_3.c, la cual como ya dije, utiliza las implementaciones de dichas funciones que vienen con el compilador avr-gcc. ¿Y qué sucede con las otras? NO FUNCIONAN con el micro/compilador ATmega328/avr-gcc. Así de simple, y por lo tanto, si no tienes una razón de peso para no utilizar el esquema heap_3.c, entonces quédate con él, o mejor aún, descarta el uso de objetos dinámicos y usa nada más objetos estáticos.

Lo anterior te lo he comentado por dos cosas:

  1. Para que no te lleves sorpresas desagradables si utilizas un esquema diferente a heap_3.c.
  2. Porque FreeRTOS incluye una función, xPortGetFreeHeapSize(), que te devuelve el número de palabras (que no de bytes) que quedan libres en el heap. Sin embargo, el esquema heap_3.c no es parte de FreeRTOS y por eso la función no es implementada. No obstante, ahí queda la información para cuando uses un microcontrolador y compilador diferente. Dicha función la puedes utilizar para afinar el tamaño del heap en una aplicación dada.

Ejemplo 2: Desbordando la pila

Para este y el siguiente ejemplo fue complicado encontrar ejemplos simples que desbordaran a la pila; lo mejor que encontré fue utilizar funciones recursivas (una función recursiva es aquella que se llama a sí misma de manera controlada). Y aún así no me fue posible ver la llamada a la función hook vApplicationStackOverflowHook() (de alguna manera el código generado esta súperoptimizado). Sin embargo, ya tienes los elementos para supervisar los problemas con la pila.

La función hook, en el archivo hooks.cpp, quedó así:

#if configCHECK_FOR_STACK_OVERFLOW > 0
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
   Serial.print( "ERR: Desbordamiento de pila para la tarea: " );
   Serial.println( pcTaskName );

   vTaskSuspendAll();
   taskDISABLE_INTERRUPTS();

   while( 1 )
      ;
}
#endif

La función vTaskSuspendAll() suspende al planificador, esto es, ya no habrá cambios de contexto; para restaurar al planificador usa xTaskResumeAll(). Ambas funciones devuelven void y no reciben argumentos. La función vTaskSuspenAll() no detiene las interrupciones, para ello es necesario que llames a taskDISABLE_INTERRUPTS(); para reactivarlas usa taskENABLE_INTERRUPTS(). La documentación oficial menciona que este par de funciones deberían ser substituídas por taskENTER_CRITICAL() y taskEXIT_CRITICAL() (ambas funciones también devuelven void y no reciben argumentos), pero el contexto en que estoy usando me da para usar las funciones que te mencioné (y además internamente son diferentes). Puedes usar vTaskSuspendAll() y taskDISABLE_INTERRUPTS() en el Ejemplo 1.

El programa de prueba es:

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

// No olvides poner configCHECK_FOR_STACK_OVERFLOW a 1 o 2 en el archivo FreeRTOSConfig.h

#define PROGRAM_NAME "mem_mang_2.ino"


void led_task( void* pvParameters )
{
    uint8_t pin = (uint8_t) pvParameters;
    pinMode( pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( pin, HIGH );
        vTaskDelay( pdMS_TO_TICKS( 200 ) );
        digitalWrite( pin, LOW );
        vTaskDelay( pdMS_TO_TICKS( 200 ) );
    }
}

void recursive( int val )
{
   if( val > 0 ){
      Serial.print( "Val = " );
      Serial.println( val );
      recursive( val - 1 );
   } else{
      return;
   }
}

void other_task( void* pvParameters )
{
   Serial.println( "Antes del desbordamiento ..." );

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 497 ) );
      recursive( 5000 );
   }
}

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

   xTaskCreate( led_task, "LED", 128, (void*) 13, tskIDLE_PRIORITY, NULL );
   xTaskCreate( other_task, "OTHR", 128, NULL, tskIDLE_PRIORITY, NULL );

   vTaskStartScheduler();
}

void loop() {}

Al igual que en el ejemplo anterior, no olvides que lo que estamos haciendo es para descubrir problemas con la pila ANTES de que instales tu aplicación en la copa de un árbol muy alto en una selva. Una vez que has determinado que no hay problemas con las pilas, entonces deberías desactivar tanto su verificación como la del heap.

En una situación crítica donde la pila se desbordó quizás sea mejor idea mantener el código de vApplicationStackOverflowHook() lo más simple posible. Serial.print() está lejos de ser simple; en su lugar puedes activar un LED (si tu hardware y aplicación lo permiten):

#if configCHECK_FOR_STACK_OVERFLOW > 0
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
   digitalWrite( 2, HIGH );

   vTaskSuspendAll();
   taskDISABLE_INTERRUPTS();

   while( 1 )
      ;
}
#endif

Para que funcione debes configurarlo en la función setup():

void setup()
{
   pinMode( 2, OUTPUT );
   digitalWrite( 2, LOW );

   // más código
}

Este ejemplo no tiene imagen de la salida de una ejecución porque fue difícil obtener algo

Ejemplo 3: Verificando la memoria disponible en la pila con uxTaskGetStackHighWaterMark()

Mencioné que la función uxTaskGetStackHighWaterMark() devuelve el número de elementos restantes de la pila que no han sido usados. En este ejemplo imprimiremos dichos valores, que para el ATmega328 el valor devuelto estará en bytes.

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

// No olvides poner configCHECK_FOR_STACK_OVERFLOW a 1 o 2 en el archivo FreeRTOSConfig.h

#define PROGRAM_NAME "mem_mang_3.ino"

void led_task( void* pvParameters )
{
    uint8_t pin = (uint8_t) pvParameters;

    pinMode( pin, OUTPUT );

    while( 1 )
    {
        digitalWrite( pin, HIGH );
        vTaskDelay( pdMS_TO_TICKS( 200 ) );
        digitalWrite( pin, LOW );
        vTaskDelay( pdMS_TO_TICKS( 200 ) );
    }
}

void recursive( int val )
{
   uint8_t arr[ val ];

   UBaseType_t high_watermark = uxTaskGetStackHighWaterMark( NULL );
   Serial.print( "Memoria restante: " );
   Serial.println( high_watermark );

   if( val > 0 ){
      Serial.print( "Val = " );
      Serial.println( val );

      vTaskDelay( pdMS_TO_TICKS( 50 ) );

      recursive( val - 1 );
   } else{
      return;
   }
}

void other_task( void* pvParameters )
{

   UBaseType_t high_watermark = uxTaskGetStackHighWaterMark( NULL );
   Serial.println( "Memoria antes del desbordamiento ..." );
   Serial.println( high_watermark );

   while( 1 )
   {
      vTaskDelay( pdMS_TO_TICKS( 497 ) );

      recursive( 30 );

      Serial.print( "MEMORIA RESTANTE: " );
      high_watermark = uxTaskGetStackHighWaterMark( NULL );
      Serial.println( high_watermark );
   }
}

void setup()
{
   // para stackOverflow:
   pinMode( 2, OUTPUT );
   digitalWrite( 2, LOW );

   Serial.begin( 115200 );
   Serial.println( PROGRAM_NAME );

   xTaskCreate( led_task, "LED", 128, (void*) 13, tskIDLE_PRIORITY, NULL );
   xTaskCreate( other_task, "OTHR", 128, NULL, tskIDLE_PRIORITY, NULL );

   vTaskStartScheduler();
}

void loop() {}

La salida de este programa cuando configCHECK_FOR_STACK_OVERFLOW es 1 es la siguiente:

Observa que a pesar de que la cantidad de memoria restante es cero, al menos entró dos veces más a la función recursiva. Y aunque la impresión serial ya no continuó, el programa hizo cosas raras, y muy importante, no llamó a la función vApplicationStackOverflowHook(); el programa pareció haberse perdido. Es natural, ya que el desbordamiento de una pila tiene consecuencias desastrosas.

Veamos la salida de este programa cuando configCHECK_FOR_STACK_OVERFLOW es 2:

Aquí puedes ver que el programa se detuvo antes de que la memoria de la pila llegara 0, y aunque no se ve, sí entró a la función vApplicationStackOverflowHook(). Ya te decía que la función Serial.print() no es exactamente simplona, por lo cual no aparece el mensaje de error; sin embargo, en su lugar sí que el LED asociado a D2 se activó, y a diferencia de la ejecución anterior, el programa efectivamente se detuvo y dejó de hacer cosas raras. La hook que utilicé para este ejemplo es:

#if configCHECK_FOR_STACK_OVERFLOW > 0
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
   Serial.print( "ERR: Desbordamiento de pila para la tarea: " );
   Serial.println( pcTaskName );

   digitalWrite( 2, HIGH );

   vTaskSuspendAll();
   taskDISABLE_INTERRUPTS();

   while( 1 )
      ;
}
#endif

¿Qué sigue?

Hoy vimos un tema algo delicado, importante y muy técnico relacionado con los sistemas operativos: la administración de la memoria. Vimos conceptos y herramientas que nos permiten identificar mientras desarrollamos o probamos la aplicación posibles desbordamientos del heap o de las pilas. También vimos que de las 5 implementaciones de malloc() que FreeRTOS incluye, debemos usar heap_3.c para el ATmega328.

RECUERDA: Siempre que te sea posible evita el uso de memoria dinámica y prefiere en su lugar la memoria estática.

RECUERDA: Si te vez utilizando la implementación heap_1.c, entonces puedes considerar usar memoria estática.

RECUERDA: Si estás desarrollando para el ATmega328 utiliza la implementación heap_3.c.

RECUERDA: Prefiere la implementación heap_4.c en lugar de la heap_2.c (en plataformas que no tengan problemas con ellas.

RECUERDA: FreeRTOS no devuelve inmediatamente la memoria cuando llamas a free(). FreeRTOS espera a que haya tiempo libre en el sistema para devolverla (a través de la tarea interna Idle_task).

RECUERDA: Cuando utilices configCHECK_FOR_STACK_OVERFLOW para atrapar los desbordamientos de las pilas prefiere el Método 2 (valor 2); ya vimos que es más seguro.

RECUERDA: Las herramientas para atrapar los desbordamientos del heap y de las pilas deberías utilizarlos únicamente mientras estás desarrollando tu aplicación. Puedes auxiliarte con la compilación condicional para eliminarla en código de producción.

Í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

1 COMENTARIO