Padrões de projeto embarcados aplicados: nomes, papéis e como encaixar no seu firmware
Agora vamos “carimbar” o que já fizemos com linguagem de arquitetura — porque isso é o que permite você escalar o design para vários periféricos, vários módulos e times diferentes mantendo consistência. Vou descrever os padrões e como eles aparecem no exemplo, e como evoluir para um firmware real.
Active Object (Objeto Ativo)
No exemplo, a StateMachineTask é um Active Object: ela tem seu próprio thread de execução, seu próprio estado interno (SmContext) e uma fila de entrada (smQueue). Isso garante uma propriedade muito importante: o estado não é compartilhado concorrentemente. A FSM não precisa de mutex para seu contexto, porque só ela toca nele. Em embarcados, isso é ouro: menos lock, menos deadlock, menos “bug que só aparece no cliente”.
Papel típico no seu projeto: qualquer coisa que tenha comportamento interno e reaja a eventos — “protocolo UART”, “estado de rede”, “controle de motor”, “supervisão de sensores” — vira um Active Object com fila própria. A disciplina é: ninguém chama função interna do objeto por fora; só manda mensagem.
Message Queue / Event-Driven Boundary (Fronteira por Mensagens)
A fila é mais do que transporte: é uma fronteira arquitetural. Ela define um contrato: “essas são as mensagens válidas”. Isso substitui acoplamento implícito (globais e callbacks soltos) por acoplamento explícito (tipos de mensagens). No nosso caso:
ioQueueé o contrato “FSM → I/O”.smQueueé o contrato “I/O → FSM”.
Em firmware real, isso permite:
- simular I/O (como fizemos) para testar a FSM sem hardware,
- inserir um logger ou trace no caminho,
- e até migrar parte do I/O para DMA/ISR sem mudar a FSM (desde que o evento seja o mesmo).
Gatekeeper (Dono do Periférico)
O Gatekeeper é um padrão muito prático: um único contexto (task) é o dono de um periférico, e todo mundo fala com ele por mensagens. A nossa IOTask é um Gatekeeper “mínimo”. Em produção, isso é onde você coloca:
- UART RX/TX, framing, CRC, timeouts de driver,
- SPI transactions serializadas,
- I2C com arbitragem e recovery (bus reset),
- e políticas de power management do periférico.
Por que isso importa? Porque periféricos raramente são “thread-safe”. Se duas tasks chamam HAL_UART_Transmit() sem governança, você ganha corrupção e travamento. Gatekeeper impede isso por construção.
State Pattern (variação em C) e disciplina de entry/actions
A separação sm_on_entry() + sm_on_event() é a forma embarcada de “State Pattern”. Em vez de objetos e herança, você usa:
enumde estados,- handlers por estado,
- e regras claras de transição.
O ganho real não é “elegância”: é evitar duplicação de ações de entrada e garantir “run-to-completion”. Isso faz o comportamento ficar rastreável.
Uma evolução natural (quando o switch crescer) é uma tabela de transições (State Transition Table). Você define:
(estado, evento) -> (ação, próximo estado)
e deixa o motor genérico. Isso reduz complexidade ciclomática e torna mais fácil revisão em equipe.
Supervisor (FSM acima de FSMs)
Quando seu firmware tem vários subsistemas (rede, sensores, atuadores, storage), um padrão comum é um Supervisor: uma FSM “acima” que:
- decide modos globais (BOOT, NORMAL, DEGRADED, SAFE),
- arbitra prioridades (ex.: desligar rádio para economizar energia),
- e coordena sequências (ex.: “para operar, rede precisa estar up e sensor calibrado”).
O nosso ST_SAFE já sugere isso. Em um sistema maior, ST_SAFE poderia:
- mandar comandos para cada Gatekeeper colocar saídas em estado seguro,
- e bloquear transições até receber
EVT_FAULT_CLEAR.
Watchdog lógico (mais útil que só “chutar o cão”)
Muita gente alimenta o watchdog no while(1) e acha que está protegido. Isso só prova que o loop roda, não que o sistema está saudável.
Um watchdog lógico é: “o sistema está avançando em estados esperados dentro de um orçamento de tempo?”. Exemplo prático:
- se
cycle_countnão aumenta por X segundos, algo travou, - se
retry_countestá alto por muito tempo, você está em falha persistente, - se o estado está preso em
ST_WAIT_IO, o driver não responde.
Você implementa isso com um contador/heartbeat que só é atualizado quando transições saudáveis acontecem. O Supervisor (ou uma task de monitoramento) alimenta o watchdog de hardware somente se os critérios lógicos forem atendidos.
Error Containment / Recovery Block
Separar ST_ERROR e ST_RECOVER é um padrão de contenção de falhas:
ST_ERROR: classifica e registra causa, decide estratégia.ST_RECOVER: executa recovery (reset, flush, backoff, reinit).- fallback:
ST_SAFE.
Isso evita o “espalhamento de tratamento de erro” pelo código todo (cada função tratando de um jeito).
Como isso vira uma arquitetura real (sem “explodir” em complexidade)
Uma forma comum de escalar:
- Cada periférico crítico vira um Gatekeeper (UART_GK, I2C_GK, NET_GK).
- Cada subsistema vira um Active Object com FSM (PROTO_SM, SENSOR_SM, MOTOR_SM).
- Um Supervisor coordena modos globais.
- Um Monitor implementa watchdog lógico e métricas.
E o FreeRTOS vira o “tecido conjuntivo”: filas e notificações, com prioridades bem definidas.