zephyr-gestion-hilos-threads

Creación y gestión de hilos (threads) en Zephyr OS

  • 6 min

Un hilo es una secuencia de ejecución independiente que el planificador puede alternar con otras según su estado y prioridad.

Hasta ahora, hemos estado ejecutando todo nuestro código dentro de la función main(). Si venís del mundo de Arduino, esto os resultará familiar: un bucle infinito donde hacemos las cosas una detrás de otra.

Pero hemos venido a usar un RTOS (Real-Time Operating System), y la gracia de un RTOS es poder hacer varias cosas “a la vez”.

Imaginad que tenéis que leer un sensor cada 10ms, controlar un motor PID cada 1ms y enviar datos a la nube por Wi-Fi cuando sea posible. Si lo hacéis todo en un solo bucle (while(1)), en el momento en que el Wi-Fi se bloquee enviando un paquete, el motor se descontrolará.

Para solucionar esto, Zephyr utiliza Threads (Hilos). Hoy vamos a aprender a crearlos, asignarles prioridades y, lo más crítico, asignarles memoria.

¿Qué es un thread en Zephyr?

Un Thread es una secuencia de ejecución independiente. Podéis imaginarlo como si vuestro microcontrolador tuviera múltiples CPUs virtuales. Cada Thread cree que tiene el procesador para él solo.

El Scheduler (Planificador) del Kernel es el “árbitro” que decide qué Thread se ejecuta en cada instante físico, basándose en reglas de prioridad y tiempos.

Para funcionar, cada Thread necesita dos cosas fundamentales:

  1. Stack (Pila): Un trozo de memoria RAM reservado para sus variables locales e historial de funciones.
  2. Entry Point: La función C que ejecutará.

Prioridades: el juego de tronos del kernel

Zephyr no trata a todos los hilos por igual. Algunos son más importantes que otros. Para eso existe el sistema de prioridades, y conviene entenderlo bien porque funciona al revés de lo que dicta la intuición humana.

En Zephyr, cuanto MENOR es el número, MAYOR es la prioridad. El 1 es más importante que el 10.

Zephyr divide los hilos en dos categorías según su prioridad:

Hilos cooperativos (prioridad negativa)

  • Rango: -1, -2, …
  • Comportamiento: Un hilo cooperativo continúa hasta bloquearse, ceder o finalizar. Las interrupciones y los hilos especiales de tipo MetaIRQ son excepciones relevantes.
  • Uso: Tareas críticas que no deben ser interrumpidas por nada del software.

Hilos apropiativos (prioridad no negativa)

  • Rango: 0, 1, 2, … (configurables en prj.conf).
  • Comportamiento: Son “expropiables”. Si un hilo de prioridad 5 está ejecutándose y de repente se despierta un hilo de prioridad 2, el Kernel pausa inmediatamente al de prioridad 5 y pone al de prioridad 2.
  • Uso: La mayoría de las tareas de sensores, comunicaciones y lógica.

Creación de un hilo (K_THREAD_DEFINE)

También se pueden crear hilos en tiempo de ejecución con k_thread_create. La declaración estática facilita conocer de antemano qué pilas y objetos se reservan, algo útil en sistemas con memoria ajustada.

Para ello usamos la macro K_THREAD_DEFINE.

Sintaxis:

K_THREAD_DEFINE(nombre_hilo, tamano_stack, funcion_entrada, 
                arg1, arg2, arg3, prioridad, opciones, retardo);
Copied!

Ejemplo práctico: el baile de los LED

Vamos a crear un ejemplo con dos hilos extra además del main.

  • Hilo A (Prioridad 7): Imprime “Ping” cada segundo.
  • Hilo B (Prioridad 2): Imprime “PONG!” cada 5 segundos. Como tiene prioridad 2 (menor número), es más importante que el A.
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(demo_threads, LOG_LEVEL_INF);

/* 1. Definimos el tamaño del Stack */
/* 1024 bytes suele ser un buen punto de partida seguro para lógica simple */
#define MY_STACK_SIZE 1024

/* 2. Definimos las prioridades */
#define PRIORITY_A 7
#define PRIORITY_B 2

/* 3. Definimos las funciones de los hilos */
void thread_a_entry(void *p1, void *p2, void *p3)
{
    while (1) {
        LOG_INF("Hilo A (Prio %d): Ping", PRIORITY_A);
        k_msleep(1000); /* Cedemos el control 1s */
    }
}

void thread_b_entry(void *p1, void *p2, void *p3)
{
    while (1) {
        /* Fíjate que este hilo se ejecutará ENCIMA del A si coinciden */
        LOG_WRN("Hilo B (Prio %d): PONG! (Soy prioritario)", PRIORITY_B);
        k_msleep(5000); 
    }
}

/* 4. Creamos los hilos estáticamente */
K_THREAD_DEFINE(my_thread_a, MY_STACK_SIZE, thread_a_entry, NULL, NULL, NULL,
                PRIORITY_A, 0, 0);

K_THREAD_DEFINE(my_thread_b, MY_STACK_SIZE, thread_b_entry, NULL, NULL, NULL,
                PRIORITY_B, 0, 0);

/* main también es un hilo; su prioridad depende de CONFIG_MAIN_THREAD_PRIORITY */
int main(void)
{
    LOG_INF("Iniciando sistema...");
    /* El main termina, pero los otros hilos siguen vivos */
    return 0;
}
Copied!

Dimensionar la pila

El parámetro MY_STACK_SIZE es el más difícil de ajustar.

  • Si pones poco stack: Tu hilo intentará escribir variables en memoria que no es suya. Resultado: Stack Overflow y el sistema crashea (o peor, corrompe datos silenciosamente).
  • Si pones mucho stack: Estás desperdiciando RAM, que es el recurso más escaso en un microcontrolador.

¿Cómo calculo el tamaño?

A ojo de buen cubero… no, es broma.

  1. Estimación: Parte de los requisitos de la arquitectura, las llamadas realizadas y las recomendaciones del subsistema. 1024 bytes puede servir para este ejemplo, pero no es una cifra universal.
  2. Protección: Activa en prj.conf la protección de hardware. Si te pasas, el sistema lanzará un error controlado en lugar de volverse loco.
CONFIG_HW_STACK_PROTECTION=y
Copied!
  1. Análisis: Usa el Thread Analyzer. Zephyr tiene una herramienta que te dice exactamente cuánto stack está usando cada hilo.

Añade esto a prj.conf:

CONFIG_THREAD_ANALYZER=y
CONFIG_THREAD_ANALYZER_USE_PRINTK=y
CONFIG_THREAD_ANALYZER_AUTO=y
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=10
Copied!

Al ejecutar, verás en la consola un informe cada 10 segundos:

Thread analyze:
 Thread name      Stack usage  Stack size  % used
 my_thread_a      340          1024        33 %
 my_thread_b      340          1024        33 %
Copied!

Si ves que usas el 33%, puedes bajar el stack a 512 bytes y ahorrar memoria. Si ves 95%, ¡aumentalo inmediatamente!

Estados de un hilo

Es importante saber que un hilo no siempre está ejecutándose. Puede estar:

  1. Running (Ejecutándose): Tiene la CPU ahora mismo.
  2. Ready (Listo): Quiere ejecutarse, pero hay otro hilo de mayor prioridad ocupando la CPU. Está en la cola de espera.
  3. Waiting (En espera): Espera un tiempo, un semáforo u otro objeto del kernel.
  4. Suspended (Suspendido): Otro código lo ha suspendido explícitamente y no volverá a competir por la CPU hasta reanudarlo.

Control manual

Podemos controlar hilos desde otros hilos usando su ID (el nombre que pusimos en K_THREAD_DEFINE).

/* Pausar el hilo A */
k_thread_suspend(my_thread_a);

/* Reanudar el hilo A */
k_thread_resume(my_thread_a);

/* Matar el hilo A (Cuidado con esto) */
k_thread_abort(my_thread_a);
Copied!

Con varios hilos en marcha, el siguiente paso es coordinar cuándo se despiertan mediante retardos y temporizadores.