Una workqueue es un hilo que ejecuta funciones enviadas a una cola de trabajo desde otros hilos o desde interrupciones.
¿Qué ocurre si el evento detectado por la interrupción requiere después una operación lenta? Al pulsar un botón, por ejemplo, quizá quieras actualizar una pantalla I2C y guardar el evento en una memoria flash SPI.
Escribir en I2C y SPI lleva tiempo. A veces requiere esperar a que el bus esté libre (Mutex) o esperar a que el byte se envíe. No puedes hacer eso dentro de la ISR. Si lo intentas, puedes bloquear el sistema o provocar un “Kernel Panic”.
Para solucionar este dilema, Zephyr nos ofrece la herramienta perfecta: la System Workqueue (Cola de Trabajo del Sistema).
¿Qué es una workqueue?
Una Workqueue no es un concepto raro. En realidad, es simplemente un Hilo (Thread) dedicado que el Kernel crea automáticamente al arrancar.
Este hilo tiene un trabajo muy aburrido:
- Mira si hay funciones (“trabajos” o “work items”) en su cola.
- Si hay, saca la función y la ejecuta.
- Si no hay, se duerme.
Como la workqueue ejecuta el handler en un hilo, puede utilizar APIs de hilo. Aun así, bloquear la system workqueue retrasa los demás trabajos que compartan esa cola.
Por lo tanto, la estrategia es:
- ISR (Rápido): Detecta el evento (botón) y “envía el trabajo” a la cola. Sale inmediatamente.
- Workqueue (Lento): Se despierta, coge el trabajo y ejecuta la tarea pesada (escribir I2C) con tranquilidad.
A esto se le llama Bottom-Half processing o procesamiento diferido.
Usar la system workqueue
Zephyr ya trae una Workqueue configurada por defecto llamada System Workqueue. Para la mayoría de tareas sencillas, es la que usaremos.
Necesitamos dos pasos:
Definir el trabajo: Crear una estructura k_work y decirle qué función debe ejecutar.
Enviar el trabajo: Llamar a k_work_submit desde nuestra interrupción.
Ejemplo práctico: del botón a la consola
Vamos a simular que la impresión por consola (printk) es una tarea pesada que no queremos hacer en la ISR.
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
#include <errno.h>
LOG_MODULE_REGISTER(demo_workqueue, LOG_LEVEL_INF);
/* Definición del botón (asumiendo alias sw0 en devicetree) */
#define SW0_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);
static struct gpio_callback button_cb_data;
/* 1. Definimos la estructura de trabajo */
struct k_work my_work;
/* Esta es la función que hará el trabajo pesado (corre en HILO) */
void my_work_handler(struct k_work *item)
{
/* Ya estamos en contexto de hilo. Mantenemos este trabajo breve
porque comparte la system workqueue con otros elementos. */
LOG_INF("Procesando evento de botón");
}
/* Esta es la ISR (corre en INTERRUPCIÓN) */
void button_pressed(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
/* AQUÍ NO PODEMOS DORMIR. */
/* Solo enviamos el trabajo a la cola. Es instantáneo. */
k_work_submit(&my_work);
}
int main(void)
{
if (!gpio_is_ready_dt(&button)) {
return -ENODEV;
}
/* Inicializamos el trabajo antes de habilitar la interrupción. */
k_work_init(&my_work, my_work_handler);
if (gpio_pin_configure_dt(&button, GPIO_INPUT) < 0) {
return -EIO;
}
gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
if (gpio_add_callback(button.port, &button_cb_data) < 0) {
return -EIO;
}
if (gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE) < 0) {
return -EIO;
}
LOG_INF("Sistema listo. Pulsa el botón.");
return 0;
}Si pulsas el botón muchas veces mientras el mismo objeto work está pendiente, las solicitudes se fusionan: no se guardan cincuenta copias del mismo trabajo.
Trabajos retardados (k_work_delayable)
A veces no queremos delegar el trabajo para “ahora mismo”, sino para “dentro de 500ms”.
“Espera, ¿para eso no estaban los Timers?”
Sí, pero recuerda: los callbacks de los timers corren en contexto de interrupción. Si quieres ejecutar algo lento periódicamente o con retardo, los timers no sirven. Necesitas un delayable work.
Es la combinación perfecta: La precisión de un Timer + El contexto seguro de un Hilo.
struct k_work_delayable my_delayed_work;
void my_delayed_handler(struct k_work *item)
{
LOG_INF("Han pasado 2 segundos y puedo usar Mutex aquí.");
}
int main(void) {
k_work_init_delayable(&my_delayed_work, my_delayed_handler);
/* Programamos para dentro de 2 segundos */
k_work_schedule(&my_delayed_work, K_MSEC(2000));
return 0;
}Esto es utilísimo para Debounce (antirrebote) de botones por software:
- Interrupción botón ->
k_work_reschedule(..., K_MSEC(50)), para reiniciar el plazo con cada rebote. - A los 50ms se ejecuta el handler (en hilo), comprueba si el botón sigue pulsado y actúa.
¿System workqueue o workqueue propia?
La system workqueue es un recurso compartido por la aplicación y por los componentes que decidan utilizarla.
- Si la tarea es breve, puedes usar la system workqueue.
- Si va a esperar mucho tiempo, crea una cola propia para no retrasar otros elementos.
En ese caso, crea tu propia Workqueue:
K_THREAD_STACK_DEFINE(my_wq_stack, 1024);
struct k_work_q my_wq;
/* En main: */
k_work_queue_start(&my_wq, my_wq_stack, K_THREAD_STACK_SIZEOF(my_wq_stack),
5, NULL);Los elementos destinados a esta cola se envían con k_work_submit_to_queue(&my_wq, &work_item).