Máquinas de Estados Finitos em Sistemas Embarcados

Introdução

Quando comecei a trabalhar com sistemas embarcados, meus primeiros firmwares eram simples, e eu fazia como quase todo iniciante: usava variáveis e flags por todo lado para controlar como o sistema deveria se comportar. Para cada situação em que o equipamento podia estar, surgia uma flag nova. Era uma abordagem simples, e funcionava muito bem, até o sistema crescer.

A cada nova funcionalidade vinham mais flags e mais condições, e os if começavam a se acumular uns dentro dos outros. Corrigir um bug criava outro! Eu passava a ter receio de mexer em trechos que funcionavam e levava cada vez mais tempo para entender o meu próprio código. Na seção 1.1, veremos um exemplo de como esse tipo de código costuma ficar.

A virada veio quando entrei em uma empresa em que, logo nos primeiros dias, recebi a documentação do firmware. Cada módulo era descrito por diagramas completos, com círculos ligados por setas, que mostravam o seu comportamento e os seus protocolos de comunicação. Acho que era a primeira vez que eu via algo assim. Em pouco tempo, eu já estava entendendo como aquele equipamento complexo funcionava. Aqueles desenhos eram máquinas de estados finitos. Desde então, elas fazem parte da forma como eu penso um firmware.

Este artigo, o primeiro de uma série, é sobre essa ferramenta. Uma máquina de estados finitos, conhecida pela sigla FSM (Finite State Machine), é uma forma organizada de descrever como um sistema se comporta em diferentes situações e como reage aos acontecimentos ao seu redor; o conceito é usado há décadas em sistemas digitais, protocolos de comunicação e controladores.

Vamos entender a ideia sem formalismos matemáticos desnecessários: identificar os elementos fundamentais de uma FSM e ver por que essa abordagem é tão útil em sistemas embarcados.

1.1. O problema: quando flags viram um labirinto

Imagine que estamos desenvolvendo o firmware de uma máquina de lavar roupa. Inicialmente, precisamos controlar apenas algumas operações: aguardar o início do ciclo, lavar, enxaguar, centrifugar e indicar o término.

Uma primeira abordagem poderia utilizar variáveis booleanas para representar cada situação:

// Máquina aguardando para iniciar o ciclo
bool is_waiting;

// Máquina em processo de lavagem
bool is_washing;

// Máquina em processo de enxágue
bool is_rinsing;

// Máquina em processo de centrifugação
bool is_spinning;

// Ciclo de lavagem concluído
bool is_finished;

Representação das situações por flags booleanas.

Cada variável pode assumir apenas dois valores: verdadeiro ou falso. O problema é que nada impede que mais de uma flag esteja ativa ao mesmo tempo. Considerando apenas essas cinco variáveis, existem 25 = 32 combinações possíveis.

Entretanto, para a máquina funcionar corretamente, apenas um estado pode estar ativo por vez. Assim, somente cinco combinações são válidas: aquelas em que exatamente uma flag está ativa.

Por exemplo, a máquina pode estar aguardando, lavando, enxaguando, centrifugando ou finalizada. Não faz sentido ela estar em mais de um estado simultaneamente. Portanto as outras 27 combinações são inválidas.

Nem todas essas combinações ocorrerão durante a execução. O ponto é que a representação permite estados que o projeto não considera válidos, e cabe ao código impedir que eles aconteçam.

O problema se torna ainda mais evidente quando adicionamos flags para indicar falhas, comandos pendentes, condições de segurança ou outras informações.

Com o tempo, a lógica pode assumir uma forma semelhante a esta, em que as flags de modo se misturam a outras informações, como has_error e lid_is_closed:

if (is_washing && !is_finished && !has_error)
{
    if (lid_is_closed)
    {
        /* Continua a lavagem */
    }
    else
    {
        /* Interrompe a operação */
    }
}

Condicionais que misturam as flags de modo com outras condições do sistema.

Imagine que outras funções do firmware também possam alterar essas variáveis. Como garantir que is_washing e is_spinning nunca sejam verdadeiras simultaneamente? O que acontece se o sistema estiver marcado como finalizado e, ao mesmo tempo, indicar uma falha? Em qual ponto do código devemos procurar quando a máquina executa uma operação inesperada?

O problema não está no uso de variáveis ou de if. A dificuldade surge quando um conjunto de variáveis passa a representar implicitamente as situações do sistema, enquanto as regras de comportamento ficam distribuídas pelo código.

O artigo do Barr Group sobre implementação de máquinas de estados em C e C++ discute como o acúmulo de flags e expressões condicionais pode tornar programas reativos difíceis de compreender e manter [1].

Uma alternativa é representar explicitamente as situações e definir as regras que determinam como o sistema passa de uma para outra. Em vez de examinar diversas flags para descobrir qual situação deveria estar ativa, podemos trabalhar com uma descrição clara do comportamento.

É justamente essa a proposta de uma Máquina de Estados Finitos (FSM).

1.2. Por que usar máquinas de estados em sistemas embarcados?

Sistemas embarcados frequentemente precisam reagir a acontecimentos externos ou internos: o acionamento de um botão, a leitura de um sensor, o recebimento de uma mensagem, o término de um temporizador ou a conclusão de uma operação.

Esses sistemas são chamados de reativos porque seu comportamento depende, em grande parte, dos estímulos que recebem e do contexto em que se encontram.

Uma máquina de estados é particularmente útil nesse cenário porque permite representar explicitamente as diferentes situações do sistema e as regras que as conectam.

Entre os principais benefícios estão:

  • Clareza do comportamento. Em vez de procurar todas as condições espalhadas pelo código, podemos identificar os estados possíveis e visualizar as transições entre eles.
  • Organização. Cada estado representa uma situação bem definida. Isso ajuda a concentrar a lógica relacionada a determinado comportamento.
  • Facilidade de manutenção. Quando um requisito muda, podemos identificar quais estados e transições precisam ser revisados, sem necessariamente alterar toda a estrutura do programa.
  • Testabilidade. Podemos preparar testes para verificar se determinados eventos disparam as transições esperadas e se o sistema responde corretamente a sequências específicas de acontecimentos.
  • Completude do modelo. Durante o projeto, precisamos identificar os eventos relevantes para cada estado e decidir como o sistema deve reagir a eles. Isso ajuda a descobrir comportamentos que ainda não foram especificados. Por exemplo, o que deve acontecer se a tampa da máquina de lavar abrir durante a centrifugação? E se chegar um comando de parada enquanto já existe uma falha ativa?
  • Documentação. O modelo pode funcionar como uma representação do comportamento esperado, útil durante o desenvolvimento, a revisão e a manutenção do firmware.

Parte desses benefícios é discutida tanto no guia introdutório The Complete Guide to State Machines [2] quanto no artigo do Barr Group [1].

Naturalmente, essas vantagens não aparecem automaticamente. Uma FSM mal projetada também pode ser difícil de entender. É necessário identificar estados adequados, definir transições coerentes e decidir o que acontece quando um evento não possui uma transição definida. Dependendo do requisito, esse evento pode ser ignorado, registrado ou tratado como uma condição de erro.

Outro ponto merece atenção: uma FSM não substitui toda a lógica do firmware. Ela é uma ferramenta para organizar comportamentos que podem ser descritos dessa maneira.

Cálculos numéricos, filtragem de sinais, processamento de dados e outras tarefas podem continuar sendo implementados por funções convencionais, chamadas quando necessário.

Nem tudo precisa virar uma máquina de estados.

1.3. O que é uma máquina de estados finitos?

Uma máquina de estados finitos é um modelo que descreve o comportamento de um sistema por meio de um conjunto limitado de estados e de regras que determinam como ele muda de um estado para outro.

Na definição do Dictionary of Algorithms and Data Structures, publicado pelo National Institute of Standards and Technology (NIST), uma máquina de estados finitos possui um conjunto de estados, um estado inicial, um conjunto de entradas e uma função de transição que determina o próximo estado a partir do estado atual e da entrada recebida [3].

Não precisamos nos preocupar agora com a formulação matemática. Podemos compreender a ideia pensando em um objeto bastante comum: um LED controlado por um botão.

O LED pode estar em duas situações relevantes para o nosso exemplo:

  • Desligado (OFF).
  • Ligado (ON).

O botão produz uma ocorrência que vamos representar pelo evento BUTTON_PRESSED. Se o LED estiver desligado, pressioná-lo faz com que ele acenda. Se estiver ligado, o mesmo evento faz com que ele apague. Apenas isso!

Isso já caracteriza a ideia central de uma máquina de estados: o comportamento é definido por um conjunto de estados e por regras que determinam as mudanças permitidas entre eles.

Observe que não precisamos conhecer os detalhes de implementação do botão, do circuito de acionamento ou do microcontrolador. Estamos descrevendo somente o comportamento esperado.

Essa separação permite discutir e validar o funcionamento desejado antes de decidir como implementá-lo.

Também vale destacar o significado da palavra finito. Ela se refere à quantidade de estados definidos no modelo, e não necessariamente à duração da operação. Uma máquina de estados pode permanecer ligada por horas ou aguardar um evento indefinidamente, sem deixar de ser finita.

1.4. Os elementos fundamentais: estado, evento e transição

Para compreender uma FSM, precisamos conhecer três elementos básicos. Na teoria clássica de autômatos, é comum falar em entrada (input). Já no desenvolvimento de sistemas reativos, utilizamos frequentemente o termo evento (event) para representar uma ocorrência discreta, como o acionamento de um botão ou o término de um temporizador.

Neste artigo, vou usar principalmente a palavra evento, por ser bastante natural no contexto de firmware, mantendo a relação com o conceito de entrada. Além desses três elementos, a definição do NIST citada na seção 1.3 inclui um estado inicial, aquele em que a máquina começa a operar. Voltaremos a ele na seção 1.6.

Estado (State)

O estado representa a situação em que o sistema se encontra.

No nosso LED, os estados são:

OFF
ON

Em outros sistemas, poderíamos encontrar estados como:

IDLE
RUNNING
STOPPING
ERROR

Esses nomes são apenas exemplos. O importante é que cada estado represente uma situação com significado claro para o comportamento do sistema.

Evento (Event)

O evento representa algo que aconteceu e que pode influenciar o comportamento da máquina.

No exemplo do LED, temos:

BUTTON_PRESSED

Em um firmware real, os eventos podem estar associados a botões, sensores, mensagens de comunicação, temporizadores ou ao resultado de uma operação anterior.

Não confunda evento com estado. BUTTON_PRESSED representa algo que aconteceu; ON representa uma situação em que o sistema pode permanecer.

Transição (Transition)

A transição define como o sistema passa de um estado para outro em resposta a uma entrada ou evento.

No exemplo, temos duas regras:

OFF + BUTTON_PRESSED → ON
ON  + BUTTON_PRESSED → OFF

O mesmo evento pode produzir resultados diferentes, dependendo do estado atual.

Essa é uma característica essencial das máquinas de estados: a resposta a um evento depende não apenas do que aconteceu, mas também da situação em que o sistema se encontrava.

Essa combinação entre estado atual, entrada e regra de transição é o fundamento da máquina de estados clássica [3].

Existem outros elementos importantes, especialmente quando precisamos descrever as saídas do sistema, condições de transição ou ações associadas ao comportamento. Vamos introduzi-los gradualmente nos próximos artigos, conforme forem necessários.

1.5. Estado não é apenas uma variável

Na implementação, é comum armazenar o estado atual em uma variável. Isso pode dar a impressão de que uma FSM é simplesmente uma variável chamada state.

Mas existe uma diferença importante entre armazenar o estado e definir o comportamento associado a ele.

Considere novamente o LED. Se a variável state indicar OFF, sabemos que o LED está desligado. Entretanto, o valor armazenado não explica, por si só, o que deve acontecer quando o botão for pressionado.

Essa regra faz parte do modelo:

Estado atualEventoPróximo estadoComportamento esperado
OFFBUTTON_PRESSEDONAcender o LED
ONBUTTON_PRESSEDOFFApagar o LED

A variável guarda a informação sobre o estado atual. O conjunto de regras define o comportamento da máquina.

Se estivéssemos implementando o controlador da máquina de lavar, poderíamos representar o estado atual por meio de uma única variável enumerada:

typedef enum
{
    washing_state_waiting,   /* aguardando o início do ciclo */
    washing_state_washing,   /* lavando */
    washing_state_rinsing,   /* enxaguando */
    washing_state_spinning,  /* centrifugando */
    washing_state_finished   /* ciclo concluído */
} washing_state_t;

Representação dos modos da máquina de lavar por um tipo enumerado.

Esse código não define, sozinho, como a máquina deve se comportar. Ainda precisamos estabelecer as regras que determinam quando e como o estado muda. A vantagem é que a representação do modo atual fica mais direta: temos uma única variável destinada a indicar o estado, em vez de diversas flags que precisam ser mantidas em combinação coerente.

Em sistemas maiores, essa distinção pesa ainda mais. Um controlador de motor, por exemplo, pode possuir estados como IDLE, STARTING, RUNNING e FAULT. Cada estado determina como o controlador deve responder aos eventos relevantes naquele momento.

Se o controlador estiver em RUNNING, um comando de parada pode iniciar o processo de desligamento. Se estiver em IDLE, o mesmo comando talvez não exija nenhuma ação. Se estiver em FAULT, o comportamento pode depender de uma regra específica de recuperação.

Além disso, repare que a tabela anterior descreve algo além do próximo estado: ela também informa o que o sistema deve fazer, como acender ou apagar o LED. Nos próximos artigos, vamos separar melhor essas ideias e estudar como representar as ações associadas ao comportamento.

1.6. Um primeiro exemplo completo: controlador de um LED

Vamos reunir os conceitos em um modelo pequeno.

O requisito é simples:

O LED começa desligado. Cada vez que o botão é pressionado, ele alterna entre ligado e desligado.

Antes de pensar no código, podemos definir os elementos da máquina.

Estados

OFF
ON

Evento

BUTTON_PRESSED

Estado inicial

OFF

O estado inicial é aquele em que a máquina começa a operar. Nos diagramas de máquinas de estados, ele costuma ser representado por um círculo preenchido com uma seta apontando para o estado inicial.

A Figura 1 reúne as transições do LED, com OFF como estado inicial:

Figura 1 – Diagrama da FSM do LED. Fonte: elaborado pelo autor.

Podemos acompanhar algumas ocorrências para verificar se o modelo faz sentido:

Tabela 2 – Exemplo de execução da FSM do LED.

PassoOcorrênciaEstado após o processamento
1Início da máquinaOFF
2BUTTON_PRESSEDON
3BUTTON_PRESSEDOFF
4BUTTON_PRESSEDON
5BUTTON_PRESSEDOFF

O comportamento é simples, mas já podemos perceber uma vantagem: as regras estão explícitas. Não precisamos consultar várias flags para descobrir qual situação deveria estar ativa. E, se chegasse um evento sem regra para o estado atual, a máquina simplesmente permaneceria onde está: ignorar um evento também é uma decisão do modelo, como veremos nos próximos artigos.

Naturalmente, um firmware de verdade precisaria tratar também aspectos que não estão representados nesse modelo, como o acionamento físico da saída e o debounce do botão, isto é, a filtragem das oscilações do contato mecânico no instante em que ele é pressionado.

A separação entre o modelo e os detalhes da implementação vai nos acompanhar ao longo da série. Nos próximos artigos, vamos estudar como transformar requisitos em diagramas mais completos e, posteriormente, em código C.

Conclusão

Máquinas de estados finitos oferecem uma maneira clara de representar comportamentos que dependem da situação atual do sistema e dos eventos que ele recebe.

Começamos com um problema comum: à medida que um firmware ganha funcionalidades, o número de flags e condições pode crescer até dificultar a compreensão do comportamento. Em seguida, vimos como uma FSM organiza esse comportamento por meio de estados, eventos e transições.

A principal ideia deste primeiro artigo é simples: em vez de espalhar as regras de funcionamento pelo código, podemos primeiro definir explicitamente as situações possíveis e como o sistema deve reagir a cada evento.

Isso não elimina a necessidade de condicionais, variáveis ou funções. O que muda é a maneira como organizamos e compreendemos o comportamento do sistema.

No próximo artigo, vamos aprender a projetar uma máquina de estados a partir de um requisito. Vamos identificar estados, eventos, transições, variáveis e condições, além de introduzir a diferença entre as máquinas de Moore e Mealy.

A partir daí, começaremos a construir uma base que permitirá implementar máquinas de estados em C de maneira progressivamente mais organizada.

E você, já se deparou com um firmware em que as flags tomaram conta da lógica? Como resolveu? Compartilhe sua experiência nos comentários.

Referências

[1] Barr Group. How to Code a State Machine in C or C++.
Sem indicação de autor. Apresenta os fundamentos das máquinas de estados e discute como elas ajudam a reduzir a complexidade de programas reativos com muitas flags e condições aninhadas.
https://barrgroup.com/embedded-systems/how-to/coding-state-machines

[2] Mülder, Andreas. The Complete Guide to State Machines (statecharts.online), cap. 1: What is a State Machine?
Introdução prática aos conceitos de estado, entrada, transição e às vantagens de representar explicitamente o comportamento de um sistema.
https://statecharts.online/chapters/01-what-is-a-state-machine.html

[3] Black, Paul E. Finite State Machine.
Dictionary of Algorithms and Data Structures, National Institute of Standards and Technology (NIST). Definição de máquina de estados finitos, incluindo estados, estado inicial, alfabeto de entradas e função de transição.
https://xlinux.nist.gov/dads/HTML/finiteStateMachine.html

[4] Harel, David. Statecharts: A Visual Formalism for Complex Systems.
Science of Computer Programming, volume 8, número 3, 1987, páginas 231–274. Artigo clássico sobre a extensão das máquinas de estados convencionais com hierarquia, concorrência e comunicação. O tema será retomado em um artigo posterior da série.
https://doi.org/10.1016/0167-6423(87)90035-9

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Inscrever-se
Notificar de
0 Comentários
mais recentes
mais antigos Mais votado
Home » Software » Máquinas de Estados Finitos em Sistemas Embarcados

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste:
Nenhum resultado encontrado.