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 seumain()no mundo embarcado.- RAM é escassa: evite alocação dinâmica repetida dentro de
loop(); prefira alocar nosetup()e reaproveitar. - Exceções/RTTI costumam vir desabilitadas — use códigos de retorno.
virtualtem custo real, mas irrelevante frente adelay()/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.