Este blog está dedicado a mis experiencias, proyectos, dificultades y demás en todo lo relacionado a la electrónica y la programación en general sobre GNU/Linux

ARM Cortex-M, Electrónica, Nivel: Básico, STM32

SysTick Driven Tasks para libOpenCM3

Nivel: Básico

¿Qué tengo que saber para este post?

—————————————

Esta es mi segunda lib (biblioteca) para libOpenCM3 integrada al sistema de gestión de dependencias de PlatformIO. Ahora con más experiencia en el proceso. No voy a detenerme en explicar en profundidad el cómo lo hace, porque para eso está el repo con el código fuente bajo licencia LGPLv3. Solo voy describir las funciones que provee, cómo agregarlo en su proyecto y el funcionamiento básico. Lo que sí tengo que aclarar y que quede bien claro es que esto NO ES UN RTOS.

Funciones de la lib

Las siguientes funciones están a disposición al instalar SDTasks:

uint32_t get_millis(void);
void tasks_init(void);
void tasks_run(void);
uint16_t task_add(void (*function)(uint8_t), uint32_t period);
bool task_remove(uint8_t id);
bool task_change_period(uint8_t id, uint32_t new_period);
bool task_delay(uint8_t id);

La primera (get_millis()) es un bonus track, donde podemos obtener los mili-segundos transcurridos desde que se inicializó el SysTick, básicamente el mismo funcionamiento de millis() en Arduino. Luego tenemos las dos funciones más importantes las cuales no podemos olvidar, porque sino no funciona este mecanismo de software.

Debemos llamar o invocar a tasks_init() luego de inicializar el SYSCLK (obviamente) para configurar el Systick para que la interrupción funcione y actualice el valor de millis cada 1ms. Una vez hecho esto debemos llamar a tasks_run() dentro del loop principal para que verifique qué tarea debe ejecutarse. Luego están las funciones que gestionan las tareas.

Añadir una tarea

Las tareas son funciones que deben tener el siguiente prototipo:

void nombre_tarea(uint8_t id);

Cuando se ejecuta la tarea recibe por parametro el ID con la cual fue registrada al momento de ejecutar task_add(). Esta función devulve el ID de la tarea al momento de registrarla, si no se pudo registrar devolverá -1.

uint8_t id_tarea;
if( (id_tarea = task_add(nombre_tarea, 500) == -1) {
// Error no se pudo registrar la tarea...
} else {
// La tarea se registró con éxito se ejecuta cada 500ms 
// y se guarda su id en id_tarea
}

Como se observa en este ejemplo, la tarea se registra para repetirse cada 500ms. Es decir que tasks_run() la ejecutará después del momento de registrada cada 500ms a partir de ese instante. El ID guardado podemos utilizarlo con las siguientes funciones enumeradas al principio.

Quitar o modificar una tarea

Las funciones task_remove() y task_change_period() se explican solas. La primera es para quitar una tarea dado su ID de la cola de tareas. Y la otra es para modificar la periodicidad con la que se ejecuta. En ambas situaciones debemos utilizar el ID como argumento.

task_remove(id_tarea1); // se deja de ejecutar la tarea1
task_change_period(id_tarea2, 400); // el periodo de la tarea2 pasa a ser 400ms

Retrasar una tarea

Este es un caso especial, pero que era necesario en una implementación en la que estaba trabajando. Supongamos que tenemos una tarea que se ejecuta cada 400ms al menos que otro evento/interrupción llegue y tengamos que posponer la tarea, es decir reiniciar la cuenta de la misma. Para eso existe task_dealy(). Suponganse que la tarea3 está registrada para ejecutarse cada 400ms y está a 87ms de ejecutarse cuando llega una interrupción en la cual debemos retardar/retrasar la ejecución de la tarea3, entonces invocamos a la función task_delay(id_tarea3); entonces ahora la tarea3 deberá esperar otros 400ms antes de poder ser ejecutada por tasks_run(). Quizás no le vean el potencial, pero cuando la necesiten me lo van a agradecer.

¿Cómo añadirlo a mi proyecto?

Como cualquier otra lib de PlatformIO sólo hace falta agregarla a la variable lib_deps de su platformio.ini con su nombre corto SDTasks-libOpenCM3:

lib_deps = SDTasks-libOpenCM3

¿Cuantas tareas puedo añadir?

Bueno, en realidad, las que tu programa requiera dentro de la medida de lo lógico. Pero en sdtask.h está la etiqueta MAX_TASKS que de no ser definida al momento de la compilación tendrá el valor por omisión de 10 tareas. Se puede modificar en código, pero lo más prolijo sería agregar el define como parámetro de compilación. Esto en PlatformIO se puede hacer agregando al platformio.ini:

build_flags = -DMAX_TASKS="7" ; 7 tareas como máximio

Sería buena práctica poner tu número máximo de tareas para que no sobre ni falte 😉

Ejemplos de código

A continuación voy a dejar 1 o 2 programas para que entiendan cómo utilizar la lib.

El blinky

El que no puede faltar… qué sería de mi vida sin el blinky…

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <sdtasks.h>

void blink(uint8_t id);

int main(void)
{
  // SYSCLK
  rcc_clock_setup_in_hse_8mhz_out_72mhz();
  // PC13 (Blue Pill LED)
  rcc_periph_clock_enable(RCC_GPIOC);
  gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
  // no olvidar:
  tasks_init();
  task_add(blink, 500); // togglear el led cada 500ms
  while(true)
  {
    tasks_run(); // se ejecutan las tareas.
  }
}
// el __unused evita warnings de compilación
void blink(__unused uint8_t id)
{
  gpio_toggle(GPIOC, GPIO13);
}

Como se observa en el ejemplo anterior, nuestra tarea no hace uso del ID del argumento, y por ello podemos usar una máscara __unused que en realidad es una directiva para el compilador (GCC) que se expande a __attribute__((unused)). El ID lo podemos llegar a utilizar por ejemplo si queremos una tarea que se ejecuta una sola vez (one-time task).

Pulso fijo

En este ejemplo voy a mostrar como sería una tarea que se ejecuta una única vez:

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <sdtasks.h>

void pulse_down(uint8_t id);

int main(void)
{
  // SYSCLK
  rcc_clock_setup_in_hse_8mhz_out_72mhz();
  // PC13 (Blue Pill LED)
  rcc_periph_clock_enable(RCC_GPIOC);
  gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
  // no olvidar:
  tasks_init();
  gpio_set(GPIOC,GPIO13);     // Se pone en HIGH el PC13
  task_add(pulse_down, 1500); // en 1.5s se bajará el PC13
  while(true)
  {
    tasks_run(); // se ejecutan las tareas.
  }
}
// one-time task:
void pulse_down(uint8_t id)
{
  task_remove(id); // se saca de la cola de ejecución
  gpio_clear(GPIOC, GPIO13);
}

Como al ejecutar por primera vez la tarea pulse_down inmediatamente se auto-remueve de la cola de ejecución, esto hace que sea una tarea que se ejecuta una sola vez en este código. Si tuviéramos un evento o interrupción que la volviera a agregar a la cola esta sería una tarea que se ejecuta 1 vez cada vez que es registrada.

Bueno, eso es todo. De acá en más, su imaginación es el límite. Espero que les sea una solución agradable y sencilla al momento de manejar varios procesos a la vez sin caer en la complejidad de un RTOS.

Dejar una respuesta