Introducción: Por qué C++ en embedded no es masoquismo, sino un superpoder
\n\nCuando escuchas «desarrollo embedded», normalmente te imaginas a un ingeniero severo con un soldador, que escribe en C puro y reza al datasheet. O, por el contrario, un entusiasta que carga digitalWrite() en Arduino y se alegra de que un LED parpadee. Pero entre estos dos mundos hay un punto medio dorado: el desarrollo embedded en C++.
Sí, ese mismo C++ que asusta con plantillas y funciones virtuales. En realidad, los microcontroladores modernos (ARM Cortex-M, RISC-V) son perfectamente capaces de procesar código C++. Es más, el uso inteligente de POO, encapsulación e incluso plantillas puede hacer que tu código embedded sea más limpio, seguro y rápido.
\n\nEn este artículo, analizaremos cómo escribir código C++ eficiente para microcontroladores, evitaremos los problemas con las tablas virtuales y aprenderemos a trabajar con el hardware a nivel de registros, pero al estilo C++. ¿Empezamos?
\n\n1. Base: En qué se diferencia el embedded C++ del normal
\n\nLa regla principal del desarrollo embedded: los recursos son limitados. No tienes gigabytes de RAM ni un procesador de gigahercios. Tienes 32 KB de flash y 8 KB de RAM. Cada byte cuenta. Por eso, C++ en embedded no se trata de «escribiré una clase con 10 niveles de herencia», sino de control sobre las abstracciones.
\n\nQué se puede usar sin miedo:
\n- \n
- Clases y estructuras — para agrupar datos y funciones. Esto es gratuito (en memoria) si no hay funciones virtuales. \n
- Espacios de nombres — para no crear prefijos como
hal_gpio_init(). \n - constexpr y plantillas — cálculos en tiempo de compilación, ahorro de ciclos. \n
- Referencias y paso por referencia constante — más seguros que los punteros. \n
Qué es mejor no tocar a menos que sea estrictamente necesario:
\n- \n
- Memoria dinámica (new/delete) — el heap se fragmenta, el asignador puede fallar. En proyectos bare-metal es tabú. \n
- Excepciones (try/catch) — requieren soporte de runtime, ocupan mucho espacio. \n
- Funciones virtuales — cada función virtual crea una tabla (vtable) en RAM/Flash, y la llamada pasa por doble indirección. Pero a veces están justificadas (drivers). \n
- RTTI (typeid) — no es necesario en absoluto. \n
La idea es simple: el compilador de C++ genera código tan eficiente como C, si no usas funciones «pesadas». Revisa el desensamblador y serás feliz.
\n\n2. Práctica: Trabajo con registros al estilo C++
\n\nEn el embedded C clásico escribimos algo como:
\n#define GPIOA_BASE 0x40020000\n#define GPIOA_MODER (*(volatile uint32_t*)(GPIOA_BASE + 0x00))\nGPIOA_MODER |= (1 << 10); // Establecemos el bit 10 (salida)\n\nEsto funciona, pero se ve arcaico. En C++ podemos hacer un envoltorio cómodo sin overhead.
\n\nEjemplo: Clase para trabajar con un pin
\n#include <cstdint>\n\nclass GpioPin {\npublic:\n // Prohibimos la copia\n GpioPin(const GpioPin&) = delete;\n GpioPin& operator=(const GpioPin&) = delete;\n\n // Enumeración para los modos\n enum class Mode : uint32_t {\n Input = 0b00,\n Output = 0b01,\n AltFunc = 0b10,\n Analog = 0b11\n };\n\n // Constructor: recibe la dirección del puerto y el número de pin (0-15)\n GpioPin(volatile uint32_t* port_base, uint8_t pin)\n : port_(port_base), pin_(pin) {\n // Se puede configurar el pin inmediatamente\n }\n\n // Establecer el modo de trabajo\n void setMode(Mode mode) {\n volatile uint32_t* moder = port_ + 0x00; // Desplazamiento del registro MODER\n *moder &= ~(0b11 << (pin_ * 2));\n *moder |= (static_cast<uint32_t>(mode) << (pin_ * 2));\n }\n\n // Establecer nivel alto\n void high() {\n volatile uint32_t* bsrr