Embassy vs RTIC: Qual Framework Rust Embedded? | Rust Brasil

Compare Embassy e RTIC para Rust embarcado: async, interrupções, tempo real, memória, energia, STM32 e critérios práticos para escolher seu framework.

Escolha Embassy para firmware dominado por I/O concorrente — sensores, timers, rádio, rede, USB e baixo consumo — e escolha RTIC quando prioridades de interrupção, deadlines e recursos compartilhados precisam ficar explícitos na arquitetura. Embassy oferece async/await cooperativo e um ecossistema integrado; RTIC modela tarefas sobre interrupções e verifica parte importante do acesso concorrente em compilação. Não existe vencedor universal: a melhor opção é a que torna o requisito temporal do dispositivo mais fácil de explicar, medir e testar.

Se você quer apenas começar com o ecossistema async, leia primeiro o guia de Embassy em Rust. Este comparativo responde à pergunta seguinte, comum em equipes de firmware: quando continuar com Embassy e quando RTIC é uma arquitetura melhor?

Resposta rápida: Embassy ou RTIC?

Situação do projetoEscolha inicialPor quê
Sensor IoT com Wi-Fi, timers e várias operações de I/OEmbassyasync/await compõe espera sem bloquear e integra bem com rede
Dispositivo alimentado por bateria que dorme entre eventosEmbassyo executor pode ficar ocioso enquanto aguarda interrupções
Controle com tarefas de prioridades diferentes e deadlines clarosRTICprioridades e preempção fazem parte do modelo
Firmware centrado em interrupções e periféricos compartilhadosRTICrecursos e locking ficam explícitos na aplicação
USB, TCP/IP ou BLE com muitas máquinas de estadoEmbassyfutures reduzem callbacks e estados manuais
Equipe experiente em Tokio e async RustEmbassymodelo mental mais familiar, apesar das diferenças de no_std
Equipe experiente em bare metal, ISR e análise temporalRTICaproxima o código do modelo tradicional de interrupções
Protótipo que precisa ganhar forma rapidamenteEmbassyecossistema integrado e fluxo linear de código assíncrono
Sistema crítico com WCET e prioridades documentadasRTICarquitetura favorece raciocínio sobre interferência e contenção

A tabela é ponto de partida, não substitui medição. Um firmware Embassy disciplinado pode cumprir deadlines apertados; um projeto RTIC mal dimensionado também pode perdê-los.

O problema que os dois frameworks resolvem

Em um microcontrolador, várias coisas parecem acontecer ao mesmo tempo:

  • um timer dispara a cada 1 ms;
  • a UART recebe bytes em momentos imprevisíveis;
  • o ADC termina uma conversão;
  • a aplicação precisa atualizar uma tela;
  • a pilha de rede aguarda pacotes;
  • o dispositivo deve dormir quando não há trabalho.

Um superloop simples consegue atender projetos pequenos, mas cresce mal quando cada driver tem sua própria máquina de estados. Um RTOS resolve isso com threads, scheduler e primitivas de sincronização, ao custo de stacks por tarefa, configuração e mais estados de runtime.

Embassy e RTIC oferecem caminhos mais alinhados com Rust no_std:

  • Embassy transforma esperas por hardware em Futures e agenda tarefas cooperativas;
  • RTIC organiza tarefas, interrupções, prioridades e recursos compartilhados com análise estática.

Ambos aproveitam o sistema de tipos de Rust, interoperam com traits de embedded-hal e evitam boa parte dos data races que seriam possíveis em C.

Como o Embassy pensa concorrência

No Embassy, uma tarefa executa até encontrar um .await que ainda não pode continuar. Nesse ponto, ela devolve o controle ao executor. Quando uma interrupção indica que o timer venceu ou que o periférico terminou uma operação, a future é acordada e volta à fila.

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::task]
async fn amostrar_sensor() {
    loop {
        // Em um projeto real, aguarde uma leitura assíncrona do periférico.
        defmt::info!("amostrando sensor");
        Timer::after(Duration::from_millis(100)).await;
    }
}

#[embassy_executor::task]
async fn enviar_telemetria() {
    loop {
        defmt::info!("enviando lote de telemetria");
        Timer::after(Duration::from_secs(10)).await;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    spawner.spawn(amostrar_sensor()).unwrap();
    spawner.spawn(enviar_telemetria()).unwrap();
}

O código mantém a ordem lógica de cada tarefa sem criar uma stack convencional para cada uma. Estado local que precisa sobreviver ao .await fica armazenado na future gerada pelo compilador.

Onde Embassy brilha

  • drivers assíncronos para UART, SPI, I2C e USB;
  • dispositivos conectados usando embassy-net;
  • firmware com muitas esperas e pouco trabalho de CPU entre elas;
  • baixo consumo, porque o processador pode dormir até uma interrupção acordar trabalho;
  • fluxos que seriam difíceis de ler como callbacks ou máquinas de estado manuais.

O cuidado principal: cooperação exige disciplina

Uma tarefa Embassy não deve executar um loop longo de CPU sem chegar a um .await. Como o executor é cooperativo, isso pode atrasar outras tarefas:

// Ruim: ocupa o executor por tempo não limitado.
async fn processar_tudo(dados: &[u8]) {
    for bloco in dados.chunks(64) {
        operacao_cara(bloco);
    }
}

Divida o trabalho, use periféricos com DMA quando possível ou mova uma operação crítica para uma interrupção apropriada. async organiza espera; ele não transforma trabalho de CPU em preempção automaticamente.

Como o RTIC pensa concorrência

RTIC, sigla de Real-Time Interrupt-driven Concurrency, modela tarefas a partir de interrupções e prioridades do microcontrolador. Recursos são declarados como locais ou compartilhados, e o framework gera o mecanismo de acesso necessário.

A forma exata dos atributos pode variar entre releases e arquiteturas, mas a estrutura conceitual é esta:

#![no_std]
#![no_main]

#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [EXTI0])]
mod app {
    #[shared]
    struct Shared {
        contador: u32,
    }

    #[local]
    struct Local {
        buffer: [u8; 64],
    }

    #[init]
    fn init(_cx: init::Context) -> (Shared, Local) {
        (
            Shared { contador: 0 },
            Local { buffer: [0; 64] },
        )
    }

    #[task(binds = USART2, priority = 3, shared = [contador], local = [buffer])]
    fn receber_uart(mut cx: receber_uart::Context) {
        // Leia o periférico e use o buffer local sem contenção.
        cx.shared.contador.lock(|contador| {
            *contador += 1;
        });
    }

    #[task(priority = 1, shared = [contador])]
    async fn telemetria(mut cx: telemetria::Context) {
        cx.shared.contador.lock(|contador| {
            defmt::info!("eventos: {}", *contador);
        });
    }
}

O ponto não é decorar macros. O valor está em declarar quem acessa qual recurso e com qual prioridade. Isso torna mais visível a pergunta de engenharia: uma tarefa de baixa prioridade pode atrasar uma tarefa mais crítica ao segurar um recurso?

Onde RTIC brilha

  • controle e aquisição com prioridades bem definidas;
  • firmware dirigido por interrupções;
  • recursos compartilhados que precisam de locking previsível;
  • sistemas nos quais a documentação de timing importa tanto quanto a lógica funcional;
  • equipes que querem minimizar um scheduler genérico e usar os mecanismos do próprio hardware.

O cuidado principal: prioridade não substitui orçamento temporal

Marcar uma tarefa como prioridade alta não garante que o sistema cumpra o deadline. É necessário conhecer:

  • frequência máxima de ativação;
  • pior tempo de execução (WCET) aproximado ou medido;
  • tempo em regiões críticas;
  • interrupções de prioridade superior;
  • custo de drivers e acesso ao barramento;
  • jitter aceitável na saída.

RTIC torna essas relações mais explícitas, mas a análise continua sendo responsabilidade da equipe.

Async cooperativo vs interrupções com prioridade

A diferença central aparece quando duas tarefas estão prontas ao mesmo tempo.

No Embassy, a tarefa atual normalmente continua até ceder em um .await. Uma interrupção pode acordar outra future, mas o executor precisa voltar a agendá-la. Isso é excelente para I/O e fluxo legível, desde que trechos síncronos sejam curtos.

No RTIC, uma tarefa de prioridade superior pode preemptar uma inferior conforme o modelo de interrupções. Esse comportamento aproxima a implementação da análise clássica de sistemas de tempo real.

CritérioEmbassyRTIC
Unidade principaltarefa async / futuretarefa ligada ou despachada por interrupção
Agendamentopredominantemente cooperativoorientado a prioridades e preempção
Espera por I/O.awaiteventos, tasks e recursos do framework
Recursos compartilhadosmutexes/canais e tipos asyncdeclaração de shared + locking gerado
Legibilidade de protocolosfluxo sequencial muito forteforte quando o fluxo acompanha eventos/ISR
Análise de prioridadesmenos central no modelocentral no desenho da aplicação
Ecossistema integradoexecutor, HALs, time, net, USBfoco principal na concorrência da aplicação

Memória: qual usa menos RAM?

Não há resposta universal. Compare o firmware real com cargo size, mapa do linker e ferramentas como cargo-bloat.

No Embassy:

  • cada tarefa tem estado estático para sua future;
  • não existe necessariamente uma stack dedicada por tarefa;
  • buffers capturados através de .await aumentam o tamanho da future;
  • bibliotecas de rede e USB podem dominar o consumo, não o executor.

No RTIC:

  • tarefas e recursos também são alocados estaticamente;
  • o custo depende dos dados locais, filas, capacidade de spawn e handlers;
  • regiões críticas não exigem uma thread com stack independente;
  • prioridades adicionais podem consumir interrupções/dispatchers disponíveis.

Uma armadilha comum em async embedded é manter um array grande vivo através de um .await:

async fn exemplo() {
    let pacote = [0u8; 4096];
    enviar(&pacote).await; // a future precisa preservar o pacote durante a espera
}

Prefira buffers estáticos cuidadosamente gerenciados, pools ou processamento em blocos quando RAM for restrita. Meça o binário em --release; builds debug não representam o dispositivo final.

Energia e baixo consumo

Embassy oferece uma narrativa direta para aplicações battery-powered: quando todas as tarefas aguardam timers ou periféricos, o executor pode entrar em modo ocioso e deixar uma interrupção acordar o chip. Isso combina bem com sensores que passam a maior parte do tempo dormindo.

RTIC também permite baixo consumo, mas a política tende a ser mais explícita no idle e no desenho das interrupções. Em ambos os casos, consumo real depende de muito mais do que o framework:

  • clocks e periféricos desligados corretamente;
  • GPIOs sem estados flutuantes;
  • rádio e sensores colocados em sleep;
  • frequência de wake-up;
  • tempo acordado por ciclo;
  • configuração do debugger, que pode impedir modos profundos.

A decisão deve ser validada com medição de corrente, não apenas com logs.

Portabilidade entre STM32, nRF, RP2040 e ESP32

Embassy mantém HALs e integrações fortes para várias famílias, tornando-o atraente quando o objetivo é levar o mesmo modelo async entre placas. O suporte exato varia por periférico e chip; confirme no repositório e nos exemplos da família antes de fechar a arquitetura.

RTIC depende do suporte da arquitetura e do Peripheral Access Crate/HAL escolhido. Ele pode ser muito portátil no nível do modelo de tarefas, mas handlers e configuração de interrupções continuam ligados ao microcontrolador.

Para reduzir acoplamento nos dois casos:

  1. coloque regras de negócio em crates no_std sem conhecer o framework;
  2. exponha hardware por traits de embedded-hal;
  3. mantenha a composição Embassy ou RTIC no crate binário;
  4. teste a lógica no host sempre que possível;
  5. isole registradores e código unsafe em módulos pequenos.

Essa separação também melhora o portfólio para uma carreira em Rust embedded: recrutadores conseguem distinguir lógica, drivers e integração de hardware.

Posso combinar Embassy e RTIC?

Em teoria, componentes async e mecanismos orientados a interrupção podem coexistir. Na prática, usar dois frameworks de aplicação cria perguntas difíceis:

  • quem possui cada interrupção?
  • qual runtime inicializa timers?
  • como prioridades de hardware se relacionam com o executor?
  • qual mecanismo gerencia recursos compartilhados?
  • como testar starvation e deadlocks entre os modelos?

A regra mais segura é: escolha um framework como dono da aplicação. Reutilize crates independentes — drivers embedded-hal, codecs, protocolos e bibliotecas no_std — sem tentar manter dois schedulers conceituais.

Se há apenas uma rotina realmente crítica em um projeto Embassy, trate-a com uma interrupção curta e bem isolada, em vez de introduzir RTIC inteiro. Se há apenas um protocolo assíncrono em RTIC, avalie uma máquina de estados ou a integração async oficialmente suportada pela versão adotada.

Checklist de decisão para a equipe

Antes de escolher, responda por escrito:

  1. Quais são os deadlines? “Rápido” não é requisito; 50 µs, 2 ms e 100 ms são.
  2. Quais eventos podem preemptar outros? Liste prioridades e interrupções.
  3. O trabalho é I/O-bound ou CPU-bound? Async favorece espera, não computação longa.
  4. Quantos fluxos simultâneos existem? Rede, USB e múltiplos sensores favorecem composição async.
  5. Quais recursos são compartilhados? SPI, buffers, estado de controle e filas precisam de dono.
  6. Quanta RAM e flash existem? Faça um protótipo mínimo e meça ambos.
  7. Qual é a experiência da equipe? Async Rust e bare metal levam a modelos mentais diferentes.
  8. Como o sistema será observado? Use probe-rs e defmt desde o primeiro milestone.
  9. Como os deadlines serão testados? GPIO + analisador lógico costuma ser mais confiável do que timestamps de log.
  10. O framework suporta o chip e os periféricos críticos? Verifique exemplos reais, não apenas a lista de targets.

Um experimento de dois dias antes de decidir

Implemente a mesma fatia vertical nos dois frameworks:

  • timer de amostragem a cada 10 ms;
  • leitura de sensor por I2C ou SPI;
  • buffer de 32 amostras;
  • envio por UART;
  • GPIO pulsado no início e no fim para medir latência;
  • modo idle quando não há trabalho.

Registre:

MétricaEmbassyRTIC
Flash em releasemedirmedir
RAM estáticamedirmedir
Pior latência observadamedirmedir
Jittermedirmedir
Consumo em idlemedirmedir
Linhas de integraçãomedirmedir
Facilidade de explicar a arquiteturanota da equipenota da equipe

Esse spike é mais barato do que descobrir tarde que a arquitetura escolhida esconde starvation, dificulta a pilha de rede ou torna o timing impossível de justificar.

Erros comuns

Escolher Embassy só porque a sintaxe é elegante

async/await melhora legibilidade, mas não remove requisitos temporais. Identifique todo trecho sem .await e limite seu tempo de execução.

Escolher RTIC só porque o produto é chamado de “tempo real”

Muitos dispositivos “em tempo real” têm deadlines folgados e são dominados por I/O. Nesses casos, Embassy pode reduzir muito a complexidade sem comprometer o requisito.

O blink não testa contenção, DMA, rede, filas, energia nem recuperação de erro. Compare uma fatia que represente o produto.

Colocar lógica de negócio dentro dos handlers

Mantenha drivers e orchestration nas bordas. Regras puras devem continuar testáveis no host, independentemente de Embassy ou RTIC.

Ignorar observabilidade até o hardware falhar

Configure logging compacto, panic handler, símbolos de debug e o fluxo com probe-rs cedo. Bugs de timing raramente ficam mais fáceis depois.

Recomendação final

Use Embassy como padrão inicial para IoT, telemetria, USB, rede e dispositivos com muitos periféricos assíncronos. O fluxo linear de async/await, a integração com HALs e o executor sem stacks por tarefa entregam boa produtividade em firmware moderno.

Use RTIC quando a especificação começa com prioridades, deadlines, interrupções e contenção de recursos. Ele ajuda a transformar esses conceitos em estrutura de código, em vez de deixá-los apenas em documentação externa.

Se ainda houver dúvida, não decida por popularidade: faça o experimento com hardware, meça latência, memória e energia, e escolha o modelo que a equipe consegue explicar sem frases vagas. Depois, aprofunde o caminho escolhido no guia geral de Rust para embarcados, no tutorial de Embassy e no roadmap de carreira em firmware Rust. Para acompanhar oportunidades relacionadas, consulte também as vagas Rust e as empresas que usam Rust.