El problema fundamental que aborda este artículo es la elección del paradigma de concurrencia y el entorno de ejecución en sistemas embebidos con restricciones de recursos y requisitos de tiempo real. Tradicionalmente, los sistemas operativos de tiempo real (RTOS) como FreeRTOS han dominado este espacio, ofreciendo multitarea preemptiva basada en hilos. Sin embargo, la evolución de los lenguajes de programación y los modelos de concurrencia, como async/await en Rust, presenta una alternativa que promete mayor eficiencia de recursos y una ergonomía de desarrollo mejorada.
La relevancia actual de esta comparación radica en la creciente complejidad de las aplicaciones embebidas y la necesidad de optimizar tanto el rendimiento como la productividad del desarrollador. Mientras que los RTOS se basan en la gestión de contextos de hilos completos, los ejecutores asíncronos como Embassy en Rust utilizan máquinas de estado generadas por el compilador, lo que puede resultar en un menor overhead y un uso más eficiente de la memoria estática. Este "Deep Dive" explora si estas promesas se traducen en ventajas tangibles en un escenario de aplicación real.
Arquitectura del Sistema
El sistema bajo prueba consiste en un microcontrolador STM32F446ZET6 ejecutando tres tareas concurrentes: un "blink_led" con retardo condicional, un "button_waiter" que detecta cambios de estado de un botón mediante interrupciones y actualiza un estado compartido, y un "uart_writer" que consume mensajes de una cola y los envía por serial. La comunicación entre tareas se realiza mediante un AtomicBool para el estado del botón y una MessageQueue para los mensajes UART.
En la implementación de FreeRTOS/C, cada tarea es un hilo independiente. Las interrupciones (EXTI) notifican a los hilos mediante osThreadFlagsSet, y los hilos esperan eventos con osThreadFlagsWait. La gestión de la cola de mensajes se realiza con osMessageQueuePut y osMessageQueueGet. El contexto completo del procesador se guarda y restaura en cada cambio de hilo, lo que permite la preemptividad. La configuración de los periféricos se realiza a través de la HAL de STM32Cube.
En la implementación de Embassy/Rust, las tareas son futures asíncronas anotadas con #[embassy::task]. Estas futures son máquinas de estado que son polleadas por el ejecutor de Embassy. Las operaciones de E/S, como button.wait_for_rising_edge().await, devuelven futures que registran un Waker en un array global. Cuando ocurre una interrupción EXTI, el Waker correspondiente es activado, señalando al ejecutor que la tarea debe ser polleada nuevamente. Embassy no implementa preemptividad a nivel de tarea dentro de un mismo ejecutor, pero permite múltiples ejecutores en diferentes contextos de interrupción para lograr prioridades. La comunicación se maneja con AtomicBool y embassy_sync::channel::Sender/Receiver para la cola de mensajes. La abstracción de hardware se logra mediante la HAL asíncrona de Embassy.
Flujo de Interrupción de Botón (Embassy/Rust)
- 1 GPIO EXTI IRQ Interrupción de hardware por cambio de estado del botón.
- 2 Embassy IRQ Handler El manejador de interrupción de Embassy se ejecuta.
- 3 Waker Activation El manejador activa el `Waker` asociado a la tarea `button_waiter`.
- 4 Executor Poll El ejecutor de Embassy `poll`ea la tarea `button_waiter`.
- 5 Future Ready `wait_for_rising_edge().await` retorna `Ready`.
- 6 Task Logic La tarea `button_waiter` actualiza el estado y envía mensaje.
- 7 Next Await La tarea `button_waiter` espera por `wait_for_falling_edge().await`.
Flujo de Interrupción de Botón (FreeRTOS/C)
- 1 GPIO EXTI IRQ Interrupción de hardware por cambio de estado del botón.
- 2 HAL_GPIO_EXTI_Callback El callback de la HAL de STM32Cube se ejecuta.
- 3 osThreadFlagsSet El callback notifica al hilo `buttonWaiterHandle`.
- 4 Scheduler Context Switch FreeRTOS conmuta al hilo `buttonWaiterHandle`.
- 5 osThreadFlagsWait Return La llamada `osThreadFlagsWait` en el hilo retorna.
- 6 Task Logic El hilo `buttonWaiter` actualiza el estado y envía mensaje.
- 7 Next Wait El hilo `buttonWaiter` llama a `osThreadFlagsWait` nuevamente.
| Capa | Tecnología | Justificación |
|---|---|---|
| orchestration | Embassy | Ejecutor asíncrono para tareas cooperativas en Rust embebido, gestionando `futures` y `wakers` para la concurrencia. vs RTIC, FreeRTOS Requiere compilador nightly de Rust y la feature `type_alias_impl_trait`. |
| orchestration | FreeRTOS | Sistema Operativo de Tiempo Real (RTOS) para multitarea preemptiva basada en hilos en C. vs Embassy, Zephyr, Mbed OS Configuración de prioridades de hilos y tamaño de stack por hilo. |
| storage | AtomicBool | Mecanismo de sincronización para compartir el estado del botón entre tareas de forma segura sin locks. vs Mutex, Spinlock Uso de `Ordering::SeqCst` para garantizar la coherencia de la memoria. |
| messaging | embassy_sync::channel::Channel | Cola de mensajes asíncrona para la comunicación entre la tarea del botón y la tarea de escritura UART en Embassy/Rust. vs `std::sync::mpsc` (no apto para no_std), implementación manual de cola Capacidad fija de 8 mensajes, sin asignación dinámica. |
| messaging | osMessageQueue | Cola de mensajes de FreeRTOS para la comunicación entre hilos en C. vs `osMailQueue`, implementación manual de cola Capacidad fija de 8 mensajes. |
Trade-offs
Ganancias
- ▲ Tiempo de interrupción (avg)
- ▲▲ Tiempo de interrupción (stddev)
- ▲ Tiempo de hilo (avg)
- ▲ Tiempo de hilo (stddev)
- ▲ Latencia de interrupción (avg)
- ▲ Latencia de interrupción (stddev)
- ▲ Tamaño de programa (.text)
- ▲▲ Uso de memoria estática (.data + .bss)
Costes
#[embassy::task]
async fn my_task(mut button: ExtiInput<'static, PC13>) {
loop {
button.wait_for_rising_edge().await;
info!("Pressed!");
button.wait_for_falling_edge().await;
info!("Released!");
}
}void StartBlinkLedTask(void *argument)
{
for (;;) {
osDelay(100);
if (atomic_load(&buttonPressed) == GPIO_PIN_RESET) {
HAL_GPIO_WritePin(LD1_GPIO_Port, LD1_Pin, GPIO_PIN_SET);
}
osDelay(100);
HAL_GPIO_WritePin(LD1_GPIO_Port, LD1_Pin, GPIO_PIN_RESET);
}
}void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == USER_Btn_Pin) {
osThreadFlagsSet(buttonWaiterHandle, 1);
}
}Fundamentos Teóricos
El concepto de futures y async/await tiene sus raíces en la programación concurrente y los modelos de actor, donde las operaciones no bloqueantes son fundamentales. La idea de una máquina de estados que representa una computación asíncrona se puede trazar a trabajos sobre continuaciones y corrutinas. Específicamente, la implementación de futures en Rust, donde son "lazy" y se ejecutan solo cuando son polleadas, se alinea con el patrón de "pull-based concurrency" o "cooperative multitasking" que ha sido estudiado en contextos de sistemas operativos ligeros y lenguajes funcionales.
La comparación con RTOS se relaciona con el debate clásico entre la multitarea preemptiva y la cooperativa. Mientras que la preemptividad (típicamente asociada con RTOS) ofrece garantías de tiempo real más fuertes al permitir que el kernel interrumpa cualquier tarea, la cooperatividad (como en Embassy) puede ofrecer menor overhead de contexto si las tareas ceden el control explícitamente en puntos de await. Este trade-off ha sido un tema recurrente en la investigación de sistemas operativos y runtimes, donde la eficiencia de la conmutación de contexto es crítica para el rendimiento en sistemas con recursos limitados.