Pular para conteúdo

14. C++ no Wokwi/ESP32

setup()/loop() são o seu main()

Um programa Arduino/ESP32 não tem main() visível — o framework fornece um main() escondido que chama setup() uma vez, no início, e depois loop() repetidamente, para sempre, enquanto o dispositivo estiver ligado:

void setup() {
    // roda uma vez: configurar pinos, iniciar Serial, criar objetos globais
}

void loop() {
    // roda de novo, e de novo, e de novo -- este e o "programa" de verdade
}

Tudo que você já aprendeu sobre C++ — classes, std::vector, polimorfismo, RAII — continua valendo. O que muda é o ambiente de execução: sem sistema operacional por baixo, com muito menos memória, e rodando indefinidamente em vez de terminar.

O que muda com pouca RAM

Um ESP32 simulado no Wokwi tem algumas centenas de KB de RAM — muito, comparado a um Arduino Uno (2 KB!), mas ainda uma fração minúscula do que um PC tem. Duas regras práticas:

Evite alocar memória dinamicamente dentro de loop(). Cada new, cada std::vector::push_back que precisa crescer, cada std::string temporária concatenada, aloca memória. Se isso acontece a cada iteração de um laço que roda para sempre, e a memória não for devolvida de forma previsível, a fragmentação de memória cresce ao longo de horas/dias até o programa falhar — um bug que não aparece em 5 minutos de teste, só depois de rodar por muito tempo. Prefira alocar tudo que puder no setup() (uma vez só) e reaproveitar essas estruturas dentro do loop().

// Evite: aloca de novo a cada iteracao do loop
void loop() {
    std::vector<int> leituras;   // criado e destruido TODA vez que loop() roda
    leituras.push_back(analogRead(34));
    // ...
}

// Prefira: aloca uma vez, reaproveita
std::vector<int> leituras;   // global ou membro de uma classe, criado no setup()

void loop() {
    leituras.push_back(analogRead(34));
    if (leituras.size() > 100) {
        leituras.erase(leituras.begin());   // nao aloca nada, mas desloca os 100 elementos
    }
}

Exceções e RTTI costumam vir desabilitados em builds Arduino/ESP32 por padrão (para economizar memória e tempo de execução — o mecanismo de exceções tem custo mesmo quando não é usado). Isso significa: nada de throw/try/catch no código que roda no hardware, e dynamic_cast pode não funcionar. Prefira o que você já viu na Unidade 1: códigos de retorno — bool com o resultado saindo por um parâmetro de referência, um enum class de status, ou um valor impossível como -1 — que é o mesmo raciocínio do capítulo 3, aplicado agora por uma razão diferente (limitação de plataforma, não só estilo).

O custo de virtual (e por que geralmente não importa)

virtual tem um custo real: cada objeto com método virtual carrega um ponteiro extra (a vtable), e cada chamada virtual passa por uma indireção a mais que uma chamada direta. Em um sistema embarcado com recursos limitados, isso às vezes é motivo de preocupação.

Na prática, para o nível deste curso: use polimorfismo sem medo. O custo de uma chamada virtual (alguns nanosegundos) é irrelevante comparado ao tempo de um delay(), uma leitura de sensor, ou uma transmissão de rede — que são as operações que realmente dominam o tempo de um programa embarcado típico. Otimizar prematuramente removendo virtual "por precaução" troca clareza e a arquitetura testável do capítulo 15 por um ganho que, nesse contexto, não se paga. Isso viraria uma preocupação real em código de tempo real crítico (controle de motor a alta frequência, por exemplo) — fora do escopo deste curso.

Uma classe-driver: LED simples

O padrão mais básico: uma classe que embrulha um pino, escondendo os detalhes de pinMode/digitalWrite atrás de uma interface com nome de domínio:

class Led {
private:
    int pino_;
    bool ligado_;

public:
    explicit Led(int pino) : pino_(pino), ligado_(false) {
        pinMode(pino_, OUTPUT);
    }

    void liga() {
        ligado_ = true;
        digitalWrite(pino_, HIGH);
    }

    void desliga() {
        ligado_ = false;
        digitalWrite(pino_, LOW);
    }

    void alterna() {
        ligado_ ? desliga() : liga();
    }

    bool esta_ligado() const {
        return ligado_;
    }
};

Uso no sketch.ino:

Led led_status(2);

void setup() {
    // nada extra -- o construtor de Led ja chamou pinMode
}

void loop() {
    led_status.alterna();
    delay(500);
}

Uma classe-driver: botão com debounce

Um botão físico, ao ser pressionado, gera múltiplas transições elétricas rápidas ("ruído de contato") antes de estabilizar — sem tratamento, um único clique pode ser lido como vários. Debounce é a técnica de ignorar mudanças de estado que acontecem rápido demais para serem um clique de verdade:

class Botao {
private:
    int pino_;
    bool estado_anterior_;
    unsigned long ultima_mudanca_ms_;
    static constexpr unsigned long kDebounceMs = 50;

public:
    explicit Botao(int pino)
        : pino_(pino), estado_anterior_(false), ultima_mudanca_ms_(0) {
        pinMode(pino_, INPUT_PULLUP);   // botao conectado entre o pino e GND
    }

    // Retorna true so no instante em que o botao PASSA a estar pressionado
    // (nao enquanto ele continua pressionado) -- e ja filtra ruido de contato.
    bool foi_pressionado() {
        bool pressionado_agora = (digitalRead(pino_) == LOW);   // INPUT_PULLUP: LOW = pressionado
        unsigned long agora = millis();

        bool mudou = (pressionado_agora != estado_anterior_);
        bool tempo_suficiente = (agora - ultima_mudanca_ms_) > kDebounceMs;

        if (mudou && tempo_suficiente) {
            ultima_mudanca_ms_ = agora;
            estado_anterior_ = pressionado_agora;
            return pressionado_agora;   // true so na transicao para "pressionado"
        }
        return false;
    }
};

Uso:

Botao botao_modo(4);
Led led_status(2);

void loop() {
    if (botao_modo.foi_pressionado()) {
        led_status.alterna();
    }
}

Repare que Botao e Led, como classes, escondem completamente os detalhes de pinMode/digitalRead/millis() do resto do programa — quem usa Botao só vê foi_pressionado(). Essa é exatamente a mesma ideia de encapsulamento da Unidade 5, aplicada a hardware em vez de dados.

Erros comuns

Programa trava/reinicia sozinho depois de rodar um tempo — sintoma clássico de fragmentação de memória por alocação dinâmica repetida dentro de loop(). Revise se algo está alocando (new, push_back que cresce, concatenação de string) a cada iteração.

Botão "clica duas vezes" com um único aperto — falta debounce, ou o tempo de debounce está curto demais. 50ms é um valor de partida razoável; ajuste se necessário.

throw/try não compila, ou compila mas o programa reinicia ao lançar uma exceção — exceções desabilitadas no build Arduino/ESP32 (comum, dependendo da configuração). Troque por código de retorno: bool mais parâmetro de referência, ou um enum class de status.

Resumo

  • setup() roda uma vez; loop() roda para sempre — é o seu main() no mundo embarcado.
  • RAM é escassa: evite alocação dinâmica repetida dentro de loop(); prefira alocar no setup() e reaproveitar.
  • Exceções/RTTI costumam vir desabilitadas — use códigos de retorno.
  • virtual tem custo real, mas irrelevante frente a delay()/E-S — use polimorfismo sem medo neste nível.
  • Classes-driver (Led, Botao) encapsulam detalhes de hardware atrás de uma interface de domínio — mesma ideia de encapsulamento da Unidade 5, aplicada a pinos em vez de dados.