Aula 44 — Semáforo: enum de estados e switch

Robótica com C — Bloco 2

O que é uma máquina de estados?

Todo sistema que muda de comportamento ao longo do tempo — um semáforo, uma fechadura, um robô — pode ser modelado como uma máquina de estados: um conjunto de estados possíveis e as regras que determinam quando e como transitar entre eles.

Um semáforo é o exemplo mais simples e concreto que existe:

🟡
AMARELO
3 s → VERMELHO
→
🔴
VERMELHO
5 s → VERDE
→
🟢
VERDE
4 s → AMARELO
→
🟡
AMARELO
3 s → VERMELHO

Três estados, três transições, ciclo infinito. Parece trivial — mas esse padrão é idêntico ao que você vai usar no robô para alternar entre "avançar", "desviar" e "parar". Aprender bem aqui significa chegar nos projetos com o padrão já internalizado.

Conexão com C: você usou switch e enum desde as aulas iniciais. No Arduino eles ganham um papel central — enum nomeia os estados de forma legível e switch decide o comportamento de cada um. A variável de estado é simplesmente um int ou enum que guarda em qual estado o sistema está agora.

enum para nomear estados

Você poderia representar os estados do semáforo com inteiros: 0 = verde, 1 = amarelo, 2 = vermelho. Funciona, mas o código fica opaco. enum resolve isso sem custo nenhum — por baixo dos panos ainda é um inteiro, mas o nome torna o código autoexplicativo:

// Sem enum — funciona, mas quem lê não sabe o que é 0, 1 ou 2
int estado = 0;
if (estado == 0) { /* verde */ }

// Com enum — estado é um nome, não um número mágico
typedef enum {
    VERDE,     // vale 0 automaticamente
    AMARELO,   // vale 1
    VERMELHO   // vale 2
} EstadoSemaforo;

EstadoSemaforo estado = VERDE;

if (estado == VERDE) { /* imediatamente legível */ }

typedef enum no Arduino: funciona exatamente igual ao C que você aprendeu. A partir da Aula 24, você já usava enum com typedef. Aqui a novidade é o uso — o enum representa um estado de hardware que muda ao longo do tempo, não apenas uma constante.

Com o enum definido, o switch fica naturalmente legível:

switch (estado) {
    case VERDE:
        // acende verde, apaga os outros
        break;
    case AMARELO:
        // acende amarelo, apaga os outros
        break;
    case VERMELHO:
        // acende vermelho, apaga os outros
        break;
}

O circuito no Wokwi

O semáforo usa três LEDs (vermelho, amarelo/laranja e verde) cada um com resistor de 220Ω. No Wokwi basta arrastar e conectar:

Pino 11 → Resistor 220Ω → LED VERMELHO → GND
Pino 10 → Resistor 220Ω → LED AMARELO  → GND
Pino 9  → Resistor 220Ω → LED VERDE    → GND
Pino Cor Constante no código
11 🔴 Vermelho PINO_VERMELHO
10 🟡 Amarelo PINO_AMARELO
9 🟢 Verde PINO_VERDE

Versão 1 — semáforo com delay()

A versão mais direta. Boa para entender a estrutura antes de evoluir para millis():

/*
 * Semáforo v1 — com delay()
 * Verde: 4s | Amarelo: 3s | Vermelho: 5s
 */

const int PINO_VERDE    = 9;
const int PINO_AMARELO  = 10;
const int PINO_VERMELHO = 11;

const int T_VERDE    = 4000;
const int T_AMARELO  = 3000;
const int T_VERMELHO = 5000;

typedef enum { VERDE, AMARELO, VERMELHO } EstadoSemaforo;

EstadoSemaforo estado = VERDE;   // começa no verde

// Apaga todos os LEDs
void todosApagados() {
    digitalWrite(PINO_VERDE,    LOW);
    digitalWrite(PINO_AMARELO,  LOW);
    digitalWrite(PINO_VERMELHO, LOW);
}

void setup() {
    pinMode(PINO_VERDE,    OUTPUT);
    pinMode(PINO_AMARELO,  OUTPUT);
    pinMode(PINO_VERMELHO, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    switch (estado) {

        case VERDE:
            todosApagados();
            digitalWrite(PINO_VERDE, HIGH);
            Serial.println("VERDE — siga");
            delay(T_VERDE);
            estado = AMARELO;        // transição
            break;

        case AMARELO:
            todosApagados();
            digitalWrite(PINO_AMARELO, HIGH);
            Serial.println("AMARELO — atenção");
            delay(T_AMARELO);
            estado = VERMELHO;       // transição
            break;

        case VERMELHO:
            todosApagados();
            digitalWrite(PINO_VERMELHO, HIGH);
            Serial.println("VERMELHO — pare");
            delay(T_VERMELHO);
            estado = VERDE;          // transição — fecha o ciclo
            break;
    }
}

VERDE — siga

AMARELO — atenção

VERMELHO — pare

VERDE — siga

...

Limitação do delay(): enquanto o semáforo está esperando no delay(T_VERDE), o Arduino não faz absolutamente nada. Para este projeto isolado isso não é problema. Mas se você quiser adicionar um botão de pedestre ou um sensor, o delay() vai ignorá-los durante a espera. A versão 2 resolve isso.

Versão 2 — semáforo com millis()

A versão profissional: o loop() continua rodando livremente — o semáforo só troca de estado quando o tempo programado expirou. Isso deixa o Arduino disponível para outras tarefas simultaneamente.

A lógica é a mesma da Aula 41b: guarda o instante da última troca e verifica se já passou o tempo do estado atual:

/*
 * Semáforo v2 — com millis() (versão profissional)
 * O loop() fica livre para outras tarefas
 */

const int PINO_VERDE    = 9;
const int PINO_AMARELO  = 10;
const int PINO_VERMELHO = 11;

typedef enum { VERDE, AMARELO, VERMELHO } EstadoSemaforo;

EstadoSemaforo estado     = VERDE;
unsigned long  ultimaTroca = 0;    // momento em que o estado mudou

// Retorna o tempo (ms) que o estado atual deve durar
unsigned long tempoDuEstado(EstadoSemaforo e) {
    switch (e) {
        case VERDE:    return 4000;
        case AMARELO:  return 3000;
        case VERMELHO: return 5000;
    }
    return 1000;
}

// Retorna o próximo estado na sequência
EstadoSemaforo proximoEstado(EstadoSemaforo e) {
    switch (e) {
        case VERDE:    return AMARELO;
        case AMARELO:  return VERMELHO;
        case VERMELHO: return VERDE;
    }
    return VERDE;
}

// Acende apenas o LED do estado atual
void atualizarLEDs(EstadoSemaforo e) {
    digitalWrite(PINO_VERDE,    e == VERDE);
    digitalWrite(PINO_AMARELO,  e == AMARELO);
    digitalWrite(PINO_VERMELHO, e == VERMELHO);
}

void setup() {
    pinMode(PINO_VERDE,    OUTPUT);
    pinMode(PINO_AMARELO,  OUTPUT);
    pinMode(PINO_VERMELHO, OUTPUT);
    Serial.begin(9600);
    atualizarLEDs(estado);
    ultimaTroca = millis();
}

void loop() {
    unsigned long agora = millis();

    // Verifica se o tempo do estado atual expirou
    if (agora - ultimaTroca >= tempoDuEstado(estado)) {
        estado      = proximoEstado(estado);    // avança o estado
        ultimaTroca = agora;                    // reinicia o cronômetro
        atualizarLEDs(estado);

        Serial.print("→ Estado: ");
        Serial.println(estado == VERDE ? "VERDE" : estado == AMARELO ? "AMARELO" : "VERMELHO");
    }

    // Aqui há espaço para: leitura de botão de pedestre,
    // atualização de display, comunicação Serial, etc.
}

→ Estado: AMARELO

→ Estado: VERMELHO

→ Estado: VERDE

→ Estado: AMARELO

Três funções bem separadas: tempoDuEstado() sabe quanto tempo cada estado dura, proximoEstado() sabe qual estado vem a seguir, e atualizarLEDs() sabe qual LED acender. O loop() só coordena — não sabe nada sobre pinos ou tempos. Esse é o padrão que os projetos do Bloco 3 vão usar.

Extensão: botão de pedestre

A vantagem real do millis() aparece aqui. Adicione um botão no pino 7: quando pressionado durante o ciclo VERDE, força a transição imediata para AMARELO — sem travar nada.

// Acrescente ao código da v2 — só as partes novas:

const int PINO_BTN_PEDESTRE = 7;

void setup() {
    // ... (igual ao anterior) ...
    pinMode(PINO_BTN_PEDESTRE, INPUT_PULLUP);
}

void loop() {
    unsigned long agora = millis();

    // Botão de pedestre: força AMARELO se estiver no VERDE
    if (digitalRead(PINO_BTN_PEDESTRE) == LOW && estado == VERDE) {
        Serial.println("Pedestre solicitou travessia!");
        estado      = AMARELO;
        ultimaTroca = agora;
        atualizarLEDs(estado);
    }

    // Lógica de tempo (igual ao anterior)
    if (agora - ultimaTroca >= tempoDuEstado(estado)) {
        estado      = proximoEstado(estado);
        ultimaTroca = agora;
        atualizarLEDs(estado);
    }
}

Isso seria impossível com delay(). Com delay(T_VERDE), o Arduino ficaria congelado durante 4 segundos e não detectaria o botão. Com millis(), o loop() passa pelo teste do botão milhares de vezes por segundo — e reage imediatamente ao pressionamento.

Padrão máquina de estados — C vs Arduino

Aspecto C puro (terminal) Arduino
Representar estados enum + switch enum + switch — idêntico
Controlar tempo sleep() ou time(NULL) delay() (bloqueante) ou millis() (não bloqueante)
Saída do estado printf() no terminal digitalWrite() em pinos físicos + Serial.println()
Encapsular lógica Funções com parâmetro enum Funções com parâmetro enum — idêntico
Loop principal while(1) loop() — equivalente
Reatividade a eventos Verificar condição dentro do while Verificar condição dentro do loop() — idêntico

O padrão que você vai usar em todos os projetos do Bloco 3:

  1. Definir os estados com enum
  2. Guardar o estado atual numa variável global
  3. No loop(), verificar se é hora de transitar com millis()
  4. Ao transitar: atualizar o estado, atualizar os pinos, registrar o instante

O robô, a campainha, o portão e o carrinho do Bloco 3 vão todos usar exatamente essa estrutura.

Quiz — Aula 44

Teste o que você aprendeu sobre máquinas de estados e semáforo:

1. Por que usar enum para representar os estados em vez de inteiros 0, 1, 2?

2. Na versão v1 com delay(), o que acontece se um botão for pressionado durante o delay(T_VERDE)?

3. Para que serve a variável ultimaTroca na versão com millis()?

4. O que faz a expressão digitalWrite(PINO_VERDE, e == VERDE)?

5. Por que proximoEstado() e tempoDuEstado() são funções separadas em vez de um único bloco no loop()?

6. Na extensão com botão de pedestre, por que a condição é estado == VERDE além de verificar o botão?

7. Qual das sequências de transição abaixo está correta para o semáforo desta aula?

8. O padrão de máquina de estados desta aula (enum + switch + millis) será reutilizado nos projetos do Bloco 3. Qual elemento dos projetos seguintes mapeia diretamente para "estado" do semáforo?