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 projeto | Escolha inicial | Por quê |
|---|---|---|
| Sensor IoT com Wi-Fi, timers e várias operações de I/O | Embassy | async/await compõe espera sem bloquear e integra bem com rede |
| Dispositivo alimentado por bateria que dorme entre eventos | Embassy | o executor pode ficar ocioso enquanto aguarda interrupções |
| Controle com tarefas de prioridades diferentes e deadlines claros | RTIC | prioridades e preempção fazem parte do modelo |
| Firmware centrado em interrupções e periféricos compartilhados | RTIC | recursos e locking ficam explícitos na aplicação |
| USB, TCP/IP ou BLE com muitas máquinas de estado | Embassy | futures reduzem callbacks e estados manuais |
| Equipe experiente em Tokio e async Rust | Embassy | modelo mental mais familiar, apesar das diferenças de no_std |
| Equipe experiente em bare metal, ISR e análise temporal | RTIC | aproxima o código do modelo tradicional de interrupções |
| Protótipo que precisa ganhar forma rapidamente | Embassy | ecossistema integrado e fluxo linear de código assíncrono |
| Sistema crítico com WCET e prioridades documentadas | RTIC | arquitetura 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ério | Embassy | RTIC |
|---|---|---|
| Unidade principal | tarefa async / future | tarefa ligada ou despachada por interrupção |
| Agendamento | predominantemente cooperativo | orientado a prioridades e preempção |
| Espera por I/O | .await | eventos, tasks e recursos do framework |
| Recursos compartilhados | mutexes/canais e tipos async | declaração de shared + locking gerado |
| Legibilidade de protocolos | fluxo sequencial muito forte | forte quando o fluxo acompanha eventos/ISR |
| Análise de prioridades | menos central no modelo | central no desenho da aplicação |
| Ecossistema integrado | executor, HALs, time, net, USB | foco 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
.awaitaumentam 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:
- coloque regras de negócio em crates
no_stdsem conhecer o framework; - exponha hardware por traits de
embedded-hal; - mantenha a composição Embassy ou RTIC no crate binário;
- teste a lógica no host sempre que possível;
- isole registradores e código
unsafeem 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:
- Quais são os deadlines? “Rápido” não é requisito; 50 µs, 2 ms e 100 ms são.
- Quais eventos podem preemptar outros? Liste prioridades e interrupções.
- O trabalho é I/O-bound ou CPU-bound? Async favorece espera, não computação longa.
- Quantos fluxos simultâneos existem? Rede, USB e múltiplos sensores favorecem composição async.
- Quais recursos são compartilhados? SPI, buffers, estado de controle e filas precisam de dono.
- Quanta RAM e flash existem? Faça um protótipo mínimo e meça ambos.
- Qual é a experiência da equipe? Async Rust e bare metal levam a modelos mentais diferentes.
- Como o sistema será observado? Use probe-rs e defmt desde o primeiro milestone.
- Como os deadlines serão testados? GPIO + analisador lógico costuma ser mais confiável do que timestamps de log.
- 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étrica | Embassy | RTIC |
|---|---|---|
| Flash em release | medir | medir |
| RAM estática | medir | medir |
| Pior latência observada | medir | medir |
| Jitter | medir | medir |
| Consumo em idle | medir | medir |
| Linhas de integração | medir | medir |
| Facilidade de explicar a arquitetura | nota da equipe | nota 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.
Comparar apenas o tamanho do exemplo blink
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.