Cuando se habla de C++ en sistemas embebidos bare metal se habla de ejecutar código en un microcontrolador sin sistema operativo, sin librería estándar completa, con unos pocos kilobytes de RAM y flash, y sin la red de seguridad de un MMU que detecte accesos inválidos a memoria. Un Cortex-M0 con 8 KB de RAM, un ATmega328 del Arduino o un RP2040 son entornos donde cada byte cuenta.
C++ funciona bien en ese contexto, pero hay que saber qué partes del lenguaje usar y cuáles deshabilitar. La mayoría de las funcionalidades modernas de C++17, C++20 y C++23 que hemos cubierto en esta serie son perfectamente válidas en embebido, con algunas excepciones concretas.
Qué deshabilitar: los tres flags fundamentales
# Compilación bare metal con arm-none-eabi-g++
arm-none-eabi-g++
-std=c++20
-fno-exceptions
-fno-rtti
-fno-threadsafe-statics
-ffreestanding
-mcpu=cortex-m4
-mthumb
-Os
main.cpp -o firmware.elf
-fno-exceptions: deshabilita el mecanismo de excepciones de C++. El unwinding de la pila requiere tablas de excepción que añaden código y el runtime de soporte ocupa espacio que no hay en muchos micros. Sin excepciones, throw no compila, pero todo lo demás funciona.
-fno-rtti: deshabilita la información de tipo en tiempo de ejecución. Sin RTTI no puedes usar dynamic_cast ni typeid. Para la mayoría del código embebido esto no es una limitación real.
-fno-threadsafe-statics: las variables estáticas locales en C++11 se inicializan de forma thread-safe por defecto, lo que requiere locks del sistema operativo. Sin RTOS esto no tiene sentido y añade código inútil.
El heap: cuándo usarlo y cuándo no
El heap dinámico (new, delete, malloc) es complicado en bare metal. En sistemas sin MMU, la fragmentación del heap puede hacer que el firmware falle de formas difíciles de reproducir horas después del arranque. Muchos proyectos embebidos lo prohíben directamente.
La alternativa es placement new: construir objetos en memoria ya reservada (en stack o en arrays estáticos):
#include <new>
// Buffer de memoria de tamaño fijo en BSS (RAM no inicializada)
alignas(MiClase) uint8_t buffer_mi_clase[sizeof(MiClase)];
MiClase* objeto = nullptr;
void inicializar() {
// Construir el objeto en el buffer sin llamar a malloc
objeto = new (buffer_mi_clase) MiClase(arg1, arg2);
}
void destruir() {
if (objeto) {
objeto->~MiClase(); // Destructor explícito
objeto = nullptr;
}
}
También puedes interceptar operator new para redirigirlo a un allocator propio:
void* operator new(std::size_t size) noexcept {
return mi_pool_alloc(size); // allocator de pool estático
}
void operator delete(void* ptr) noexcept {
mi_pool_free(ptr);
}
constexpr: código de C++20 que funciona perfectamente en embebido
Las expresiones constexpr y consteval de C++20 son ideales en embebido porque calculan en tiempo de compilación y no generan código en runtime. Una tabla de lookup calculada en compilación ocupa flash pero no ciclos de CPU:
#include <array>
// Tabla de senos precalculada en compilación (256 puntos)
consteval auto generar_tabla_seno() {
std::array<float, 256> tabla{};
for (size_t i = 0; i < 256; ++i) {
tabla[i] = /* cálculo con approx. del seno */
static_cast<float>(i) * 0.0245436926f; // simplificado
}
return tabla;
}
constexpr auto TABLA_SENO = generar_tabla_seno();
// La tabla vive en flash (sección .rodata), cero ciclos en runtime
Templates y zero-cost abstractions
Los templates son especialmente valiosos en embebido porque toda la resolución ocurre en compilación: sin vtables, sin llamadas indirectas, sin overhead:
// GPIO tipado: el pin y el puerto son parámetros de template
// La función se resuelve en compilación a una instrucción de registro
template <uint32_t Puerto, uint8_t Pin>
struct GPIO {
static void set() { *reinterpret_cast<volatile uint32_t*>(Puerto) |= (1u << Pin); }
static void clear() { *reinterpret_cast<volatile uint32_t*>(Puerto) &= ~(1u << Pin); }
static bool read() { return (*reinterpret_cast<volatile uint32_t*>(Puerto) >> Pin) & 1u; }
};
using LED = GPIO<0x48000014, 5>; // GPIOA ODR en STM32F4, pin 5
int main() {
LED::set(); // Compila a una instrucción STR
LED::clear(); // Compila a una instrucción STR
}
La librería estándar: qué funciona y qué no
En entornos -ffreestanding la librería estándar completa no está disponible, pero muchas partes sí funcionan:
- Funciona bien:
std::array,std::span,std::optional,std::variant,std::string_view,std::bit_cast,std::to_integer,constexprmath,std::expected(C++23). - No disponible sin heap:
std::vector,std::string,std::map,std::function,std::shared_ptr. - No disponible sin OS:
std::thread,std::mutex,std::async, I/O de filesystem. - Alternativas:
etl(Embedded Template Library) ofrece versiones de vector, string, map con tamaño fijo en compilación.
El linker script
En bare metal el linker script define dónde va cada sección de memoria. Es un archivo .ld que especifica las regiones de flash y RAM y cómo mapear las secciones ELF (.text, .data, .bss, etc.):
/* linker.ld simplificado para Cortex-M */
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS {
.text : { *(.text*) *(.rodata*) } > FLASH
.data : { *(.data*) } > RAM AT> FLASH
.bss : { *(.bss*) } > RAM
}
Herramientas del ecosistema
El toolchain habitual para ARM Cortex-M es arm-none-eabi-g++ (GCC para ARM sin sistema operativo). Para AVR (Arduino) es avr-g++. Ambos soportan hasta C++20 en versiones recientes. Para depuración, openocd con GDB sobre JTAG/SWD.
Frameworks como Zephyr RTOS o ChibiOS permiten usar C++ con algo más de stdlib disponible (heap controlado, hilos, mutexes), aunque siguen requiriendo los flags de no-excepciones y no-rtti en la mayoría de configuraciones.
Si estás evaluando alternativas a C++ para embebido, Zig 0.14 y Rust tienen soporte bare metal serio, con ventajas distintas en seguridad de memoria en tiempo de compilación. Pero C++ sigue siendo el lenguaje con más ecosistema, más soporte de fabricantes y más código heredado en el mundo embebido.
Esta guía cierra la serie de C++ moderno en programacion.net. El resto de artículos de la serie cubren C++23 con GCC 14 y Clang 18, smart pointers, concepts, módulos, std::format, CMake moderno y sanitizers.
Imagen: Pexels / Tanha Tamanna Syed
