Máquina de Estados em C

Em 06/02/2010, em Linguagem C, por Sergio Prado

Desen­volve­dor de soft­ware é um ser muito ansioso. Quer logo colo­car a mão na massa. E depois de 2.000 lin­has de código ele resolve começar os testes. O código nem com­pila. E quando ele se prepara para cor­ri­gir o prob­lema, percebe que o código está um caos total. Funções com mais de 200 lin­has e mais de 5 if’s anin­hados. O que pare­cia ser fácil no começo (e pode­ria ter sido) já não é mais. Tudo porque ele não tinha um plano. Nunca comece nada sem um plano, prin­ci­pal­mente quando se trata de desen­volvi­mento de soft­ware. E uma boa estratég­ica é o uso de design pat­terns.

Design pat­terns, ou padrões de pro­jeto, são soluções para prob­le­mas nor­mal­mente encon­tra­dos em pro­je­tos de soft­ware. São inde­pen­dentes de lin­guagem, e ofer­e­cem uma descrição ou tem­plate de como resolver deter­mi­nado prob­lema. Um design pat­tern muito con­hecido e uti­lizado no desen­volvi­mento de soft­ware para sis­temas embar­ca­dos é a Máquina De Esta­dos Finita, ou Finite State Machine (FSM), a qual chamare­mos aqui ape­nas de máquina de estados.

Mas qual o prob­lema que este design pat­tern pre­tende resolver?

Muitos dos dis­pos­i­tivos usa­dos em sis­temas embar­ca­dos, prin­ci­pal­mente os de automação e con­t­role, são dis­pos­i­tivos pas­sivos. Respon­dem a deter­mi­nadas ações do ambi­ente. E a saída ger­ada por estas ações são depen­dentes de seu estado atual. É exata­mente aí que entra a imple­men­tação deste design pat­tern: qual­quer dis­pos­i­tivo, físico ou con­ceitual, que pos­sue um con­junto finito de esta­dos e aceita um finito numero de entradas, pode pro­duzir um finito numero de difer­entes saidas. Este dis­pos­i­tivo deve ter uma memória que possa armazenar a sequen­cia de entradas rece­bidas, de forma que a saida seja depen­dente destas entradas.

OK, está um pouco con­fuso, não é? Vamos então ten­tar anal­isar um caso prático.

A entrada de um típico sis­tema de automação de esta­ciona­men­tos é com­posta pelos seguintes ele­men­tos: um emis­sor de tick­ets, uma can­cela e um ou mais loops de detecção de veícu­los. O loop é um sen­sor de chapa metálica local­izado no chão e uti­lizado para iden­ti­ficar o veiculo. Para o nosso exem­plo exis­tem dois loops, um em frente ao emis­sor de tick­ets (loop A) e outro abaixo da can­cela (loop B).

As seguintes ações são real­izadas na entrada de um veículo:

  1. O usuário posi­ciona o veículo em frente ao emis­sor de tick­ets, o loop A será acionado e uma men­sagem “Pres­sione o botão” é emitida.
  2. O usuário pres­siona o botão para emi­tir o ticket, o sis­tema real­iza a emis­são e lev­anta a cancela.
  3. O usuario passa pela can­cela (loop B) e entra no esta­ciona­mento. O sis­tema aguarda e abaixa a cancela.

Pelas ações lis­tadas acima, podemos iden­ti­ficar basi­ca­mente os esta­dos abaixo, na ordem em que são exe­cu­ta­dos:

  1. Aguardando veículo no loop A.
  2. Aguardando usuário pres­sionar o botão para emi­tir ticket.
  3. Aguardando veiculo sair do loop A.
  4. Aguardando veiculo pas­sar pelo loop B.

O impor­tante aqui é perce­ber que, para qual­quer um dos esta­dos acima, depen­dendo da entrada, a saida será difer­ente. Por exem­plo, se você esta no estado 2 e recebe como entrada o botão pres­sion­ado você vai para o estado 3. Porém, se ao invés do botão pres­sion­ado você rece­ber como entrada a saida de veiculo do loop A (usuário deu ré e desis­tiu de esta­cionar), você deve voltar para o estado 1.

Este é um exem­plo sim­ples de apli­cação onde cabe per­feita­mente a imple­men­tação de uma máquina de esta­dos. Além desta, exis­tem muitas out­ras apli­cações, como por exem­plo inter­pre­ta­dores de comando, proces­sadores de lin­guagem, trata­mento de pro­to­co­los de comu­ni­cação, etc.

Agora que con­hece­mos um pouco da teo­ria, vamos à prática.

Imple­men­tação

Um prob­lema que resolvo fre­quente­mente com máquina de esta­dos são roti­nas de trata­mento de pro­to­co­los de comu­ni­cação.

No nosso exem­plo, o pro­to­colo de comu­ni­cação tem o seguinte formato:

|STX|QTD_DADOS|DADOS|CHK|ETX|

STX       (1 Byte)  -> Ini­cio da trans­mis­são (0x02)
QTD_DADOS (1 Byte)  -> Quan­ti­dade de dados
DADOS     (N Bytes) -> Dados
CHK       (1 Byte)  -> Check­sum da trans­mis­são
ETX       (1 Byte)  -> Fim da trans­mis­são (0x03)

Vamos imple­men­tar aqui dois mod­e­los difer­entes, um com switch/case e outro com pon­teiro para função.

Imple­men­tação 1: Switch/case

Esta imple­men­tação é sim­ples e com­posta de ape­nas uma função, lis­tada abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* possiveis estados */
typedef enum {
    ST_STX = 0, ST_QTD, ST_DATA, ST_CHK, ST_ETX
} States;
 
/* maquina de estados */
void handleRx(unsigned char *data, int qtd)
{
    static States state = ST_STX;
    static unsigned char buffer[MAX_BUFFER];
    static int indBuffer = 0, qtdBuffer = 0;
    static unsigned char chkBuffer = 0;
    int i;
 
    for (i = 0; i < qtd; i++) {
 
        switch (state) {
 
            case ST_STX:
                if (data[i] == STX) {
                    indBuffer = qtdBuffer = 0;
                    chkBuffer = 0;
                    state = ST_QTD;
                }
                break;
 
            case ST_QTD:
                qtdBuffer = data[i];
                state = ST_DATA;
                break;
 
            case ST_DATA:
                buffer[indBuffer++] = data[i];
                chkBuffer ^= data[i];
                if (--qtdBuffer == 0) {
                    state = ST_CHK;
                }
                break;
 
            case ST_CHK:
                if (data[i] == chkBuffer) {
                    state = ST_ETX;
                }
                else {
                    state = ST_STX;
                }
                break;
 
            case ST_ETX:
                if (data[i] == ETX) {
                    handlePackage(buffer,
                        indBuffer);
                }
                state = ST_STX;
                break;
        }
    }
}

O uso da máquina de esta­dos é sim­ples. Basta chamar a função han­dleRx() para cada stream rece­bido do buffer de comu­ni­cação. Veja que ela pos­sui algu­mas var­iáveis sta­tic para que seja pos­sivel armazenar seu estado atual. O código-fonte com­pleto desta imple­men­tação pode ser baix­ado aqui.

A prin­ci­pal defi­ciên­cia desta solução é que a função pode ficar grande e com­plexa demais depen­dendo da quan­ti­dade de esta­dos. A mel­hor forma de resolver este prob­lema é imple­men­tar usando pon­teiros para função.

Imple­men­tação 2: Pon­teiros de função

Nesta forma de imple­men­tação da máquina de esta­dos, cada ação é imple­men­tada através de uma função. Veja um tre­cho da imple­men­tação abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/* possiveis estados */
typedef enum {
    ST_STX = 0, ST_QTD, ST_DATA, ST_CHK, ST_ETX
} States;
 
struct StateMachine {
    States state;
    unsigned char buffer[MAX_BUFFER];
    unsigned char chkBuffer;
    int indBuffer;
    int qtdBuffer;
    Action action[5];
} sm;
 
void stSTX(unsigned char data)
{
    if (data == STX) {
        sm.indBuffer = sm.qtdBuffer = 0;
        sm.chkBuffer = 0;
        sm.state = ST_QTD;
    }
}
 
void stQtd(unsigned char data)
{
    sm.qtdBuffer = data;
    sm.state = ST_DATA;
}
 
void stData(unsigned char data)
{
    sm.buffer[sm.indBuffer++] = data;
    sm.chkBuffer ^= data;
    if (--sm.qtdBuffer == 0) {
        sm.state = ST_CHK;
    }
}
 
void stChk(unsigned char data)
{
    if (data == sm.chkBuffer) {
        sm.state = ST_ETX;
    }
    else {
        sm.state = ST_STX;
    }
}
 
void stETX(unsigned char data)
{
    if (data == ETX) {
        handlePackage(sm.buffer, sm.indBuffer);
    }
    sm.state = ST_STX;
}
 
void handleRx(unsigned char *data, int qtd)
{
    int i;
 
    for (i = 0; i < qtd; i++)
        sm.action[sm.state](data[i]);
}

A forma de usar a máquina de esta­dos con­tinua a mesma. Basta chamar a função han­dleRx() para cada stream rece­bido do buffer de comu­ni­cação. Mas veja que a função diminuiu para ape­nas 2 lin­has! O estado atual da máquina de esta­dos fica armazenado na estru­tura StateMa­chine. E cada tre­cho de código den­tro de um case, no exem­plo ante­rior, se trans­for­mou em uma função neste exem­plo. O código ficou muito mais mod­u­lar e de fácil manutenção. Os fontes desta imple­men­tação tam­bém estão disponíveis aqui.

Espero que estes dois exem­p­los ten­ham aju­dado vocês a enten­der o mecan­ismo de imple­men­tação de uma máquina de esta­dos e como esse design pat­tern pode ser útil no nosso dia-a-dia. Se vocês tiverem alguma outra sug­estão de imple­men­tação de máquinas de estado, deixem seus comentários.

Um abraço a todos,

Ser­gio Prado

VN:F [1.9.17_1161]
Rat­ing: 10.0/10 (2 votes cast)
Máquina de Esta­dos em C, 10.0 out of 10 based on 2 ratings

Sem posts relacionados.

  • Fábio R. Coutinho

    Muito inter­es­sante o artigo! Só para complementar,uma vez lí num livro uma ter­ceira forma de imple­men­tar máquinas de estado. O algo­ritmo era mais ou menos assim:

    1)Ler entrada (l)
    2)Efetue tran­sição de estado ( q(l)<-qn )
    3)Emita saída ( s=f(qn) )

    Ou seja o código de imple­men­tação é sem­pre o mesmo, o que muda é a tabela de tran­sição de estado e a tabela de saí­das de cada estado. É um imple­men­tação mais geral, con­tudo mais com­pli­cada de ser enten­dida quando se lé o código fonte. Para superar essa desvan­tagem nor­mal­mente introduz-se um comen­tário para que seja visto a rep­re­sen­tação grá­fica da máquina de esta­dos ou se desenha a máquina de estado com car­ac­teres ascii ao lado do código(qdo a rep. grá­fica da mesma não é muito grande).

    Senão em engano o livro que lí essa forma foi o “Intro­duc­tion to Automata The­ory, Lan­guages, and Computation”…

    Abs,

    Fábio

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • Cris­tiano Cortezia

    Gostei do post. Acho muito inter­es­sante e enx­uta essa forma de imple­men­tar máquinas de estado. E tam­bém dis­cordo do “Rule 104″ do Misra-C (http://www.sergioprado.org/2010/05/27/misra-c-padrao-para-software-em-c/).
    Cos­tumo usar essa abor­dagem na imple­men­tação de cli’s e out­ras soluções que envolvam tabelas asso­cia­ti­vas. Os resul­ta­dos são exce­lentes no que diz respeito ao tempo (e con­forto rs) de manutenção do código para inclusão/remoção de novos coman­dos ao con­junto. Prezo muito a leg­i­bil­i­dade de código.
    []‘s
    Cristiano

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • @St0rm_W0lf

     
    Olá.
    Muito bom o artigo.
    Eu fiquei inter­es­sado em estu­dar mel­hor o exem­plo que você deu no artigo. É que acabei ficando com algu­mas dúvi­das e eu acho que vendo o código com­pleto eu con­siga enten­der mel­hor o que esta sendo feito ali.
    Ten­tei aces­sar o link  (http://www.embarcados.com.br/View-document-details/118-blog-sprado-state_machine.html) mas retorna 404: Not Found (Me cadas­trei no por­tal)
     
    Pode­ria disponi­bi­lizar acesso ao código nova­mente se pos­sível.
     
    Obri­gado
    Att
    Cyph3rk

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org ser­gio­prado

      Olá Cyph3rk,

      Atu­al­izei os links. Veja se agora con­segue baixar os fontes. Qual­quer prob­lema me avise.

      Abraços!

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)
  • Matheus Gus­tavo

    Tem gente que é fã do Ronaldo, gente fã do Elton John eu sou fã de Ser­gio Prado!!!
    Blog, arti­gos… tudo extrema­mente orga­ni­zado, com muita clareza e con­teúdo muito, muito bom.
    Con­tinue assim. Ajuda muita gente!

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org ser­gio­prado

      Obri­gado Matheus!

      Agora só falta ficar rico igual eles! :-)

      Con­tinue acompanhando!

      Um abraço.

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)
  • roge­rio

    Ja imple­mentei um algo­ritmo com maquinas de esta­dos um pouco mais elab­o­rado. Os esta­dos sao mod­i­fi­cado em um ambi­ente mul­ti­thread:
    1)Ler entrada (l)
    2)if (Tran­sição de estado efe­t­u­ada de forma atom­ica ( q(l)<-qn ).
        {
           // Tran­si­cao efe­t­u­ada
        }
         else
         {
           // Tran­si­cao fal­hou
         }
    3)Emita saída ( s=f(qn) )

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • david

    Boa Tarde, gostaria de saber se tem como imple­men­tar uma máquina de estado do jogo batalha naval?

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org Ser­gio Prado

      Olá David!

      Acred­ito que sim! Divida a imple­men­tação entre esta­dos, defina as tran­sições entre estes esta­dos e você terá seu jogo de batalha naval!

      Um abraço.

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)
  • Elton Pereira

    Olá Ser­gio!
    Em primeiro lugar parabéns pelos seus ótimos posts.
    Me des­culpe se a per­gunta que vou fazer for ignorân­cia de minha parte, mas estou com a seguinte dúvida:
     
    Essa é definição do pon­teiro para função: typedef void (*Action)(unsigned char data);
    Mas exis­tem funções que pos­suem dois parâmet­ros, como esta:
    void handleRx(unsigned char *data, int qtd) 
    E na declar­ação do pon­teiro ele só está “con­fig­u­rado” para rece­ber um parâmetro. Como o pon­teiro vai se com­por­tar neste caso?
    Desde já agradeço a atenção!

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org Ser­gio Prado

      Olá Elton,

      É que a han­dleRx não faz parte das funções da máquina de esta­dos. Veja que a estru­tura StateMa­chine é um vetor de 5 ele­men­tos, que deve apon­tar para as funções stSTX, stQtd, stData, stChk e stETX. Todas elas respeitam a definição “type­def void (*Action)(unsigned char data)”.

      Se ficou alguma dúvida me avise.

      Um abraço.

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)