Introdução: Por que C++ em embedded não é masoquismo, mas um superpoder
\n\nQuando se ouve "desenvolvimento embedded", geralmente vem à mente um engenheiro severo com um ferro de solda, que escreve em C puro e reza para o datasheet. Ou, ao contrário, um entusiasta que carrega digitalWrite() no Arduino e fica feliz por piscar um LED. Mas entre esses dois mundos existe um meio-termo dourado — desenvolvimento embedded em C++.
Sim, aquele mesmo C++ que assusta com templates e funções virtuais. Na verdade, microcontroladores modernos (ARM Cortex-M, RISC-V) são perfeitamente capazes de processar código C++. Além disso, o uso inteligente de OOP, encapsulamento e até templates pode tornar seu código embedded mais limpo, seguro e rápido.
\n\nNeste artigo, vamos explorar como escrever código C++ eficiente para microcontroladores, evitar as armadilhas das tabelas virtuais e aprender a trabalhar com hardware no nível de registradores, mas no estilo C++. Vamos nessa?
\n\n1. Base: Como o embedded C++ difere do comum
\n\nA regra principal do desenvolvimento embedded: os recursos são limitados. Você não tem gigabytes de RAM nem um processador de gigahertz. Você tem 32 KB de flash e 8 KB de RAM. Cada byte conta. Portanto, C++ em embedded não é sobre "escrever uma classe com 10 níveis de herança", mas sobre controle sobre abstrações.
\n\nO que pode ser usado sem medo:
\n- \n
- Classes e estruturas — para agrupar dados e funções. Isso é gratuito (em memória) se não houver funções virtuais. \n
- Namespaces — para não proliferar prefixos como
hal_gpio_init(). \n - constexpr e templates — cálculos em tempo de compilação, economia de ciclos. \n
- Referências e passagem por referência constante — mais seguros que ponteiros. \n
O que é melhor não tocar sem necessidade urgente:
\n- \n
- Memória dinâmica (new/delete) — o heap fragmenta, o alocador pode falhar. Em projetos bare-metal — tabu. \n
- Exceções (try/catch) — exigem suporte de runtime, ocupam muito espaço. \n
- Funções virtuais — cada função virtual cria uma tabela (vtable) em RAM/Flash, e a chamada passa por dupla indireção. Mas às vezes são justificadas (drivers).
- RTTI (typeid) — não é necessário.
A essência é simples: o compilador C++ gera código tão eficiente quanto C, se você não usar recursos "pesados". Verifique o disassembler — e você será feliz.
\n\n2. Prática: Trabalhando com registradores no estilo C++
\n\nNo embedded C clássico, escrevemos algo como:
\n#define GPIOA_BASE 0x40020000\n#define GPIOA_MODER (*(volatile uint32_t*)(GPIOA_BASE + 0x00))\nGPIOA_MODER |= (1 << 10); // Configura o bit 10 (saída)\n\nIsso funciona, mas parece arcaico. Em C++, podemos criar um wrapper conveniente sem overhead.
\n\nExemplo: Classe para trabalhar com pino
\n#include <cstdint>\n\nclass GpioPin {\npublic:\n // Proíbe cópia\n GpioPin(const GpioPin&) = delete;\n GpioPin& operator=(const GpioPin&) = delete;\n\n // Enumeração para modos\n enum class Mode : uint32_t {\n Input = 0b00,\n Output = 0b01,\n AltFunc = 0b10,\n Analog = 0b11\n };\n\n // Construtor: recebe endereço da porta e número do pino (0-15)\n GpioPin(volatile uint32_t* port_base, uint8_t pin)\n : port_(port_base), pin_(pin) {\n // Pode configurar o pino imediatamente\n }\n\n // Configurar modo de operação\n void setMode(Mode mode) {\n volatile uint32_t* moder = port_ + 0x00; // Offset do registrador MODER\n *moder &= ~(0b11 << (pin_ * 2));\n *moder |= (static_cast<uint32_t>(mode) << (pin_ * 2));\n }\n\n // Configurar nível alto\n void high() {\n volatile uint32_t* bsrr