Gestión de memoria y secciones en sistemas embebidos

En un entorno de escritorio, el sistema operativo y la MMU (Memory Management Unit) te regalan una abstracción de memoria lineal y uniforme. En el mundo del bare-metal, esa ficción desaparece. La realidad física dicta que la memoria no es un bloque continuo, sino un mapa de regiones con capacidades y naturalezas distintas: la Flash (ROM) para el código y constantes, y la SRAM para las variables de trabajo.

Esta arquitectura exige que el programador entienda la disposición de los datos en el binario. Las variables globales no “viven” simplemente en la memoria; se clasifican en secciones que el linker script debe ubicar físicamente. Si declaras una variable global inicializada como int x = 10;, el compilador la coloca en la sección .data. Sin embargo, como la Flash es de solo lectura, el valor 10 reside físicamente en la Flash, y el startup code (un pequeño conjunto de instrucciones ejecutadas antes de main) debe copiar ese valor desde la Flash hacia una dirección de la RAM cada vez que el dispositivo se enciende. Las variables no inicializadas van a la sección .bss, que el startup simplemente rellena con ceros en la RAM.

Para trabajar con hardware, la palabra clave volatile es obligatoria. El compilador es un optimizador voraz; si ve un bucle while(!flag) y nota que flag no cambia dentro de ese bucle, asumirá que el valor es constante y optimizará la lectura moviéndola a un registro, ignorando cambios realizados por una interrupción (ISR) o por un periférico. volatile le prohíbe esa optimización, forzando una lectura directa a la dirección de memoria en cada iteración.

Debes usar estas técnicas de gestión de secciones cuando el hardware requiere que ciertos datos residan en regiones específicas (como una RAM de alta velocidad para buffers críticos) o cuando necesitas optimizar el uso de la limitada Flash. Si ignoras la naturaleza de estas secciones, el sistema colapsará: intentar escribir en una dirección de la sección .text (Flash) provocará un error de acceso de hardware, y un mal uso del stack por recursión profunda o variables locales excesivas resultará en un desbordamiento que corrompa la pila de llamadas sin previo aviso.

#include <stdint.h>
#include <stddef.h>
#include <stddef.h> // Para _Noreturn [C11]

/* Simulación de un periférico UART mediante un mapa de memoria */
typedef struct {
    volatile uint32_t CONTROL;
    volatile uint32_t STATUS;
    volatile uint32_t DATA;
} UART_Hardware_t;

/* El puntero es constante, pero el contenido es volatile para evitar
   que el compilador optimice lecturas/escrituras al hardware. */
UART_Hardware_t *const UART_REGS = (UART_Hardware_t *)0x40001000;

/* .text / .rodata: Las constantes se almacenan en la Flash (ROM). */
const uint8_t LOOKUP_TABLE[4] = {0x0A, 0x0B, 0x0C, 0x0D};

/* .data: El valor 42 se guarda en Flash y el startup lo copia a RAM. */
int global_initialized = 42;

/* .bss: No necesita espacio en el binario, el startup la inicializa a 0. */
int global_uninitialized;

/* Sección personalizada: Usamos un atributo de GCC para colocar datos
   en una sección de RAM rápida definida en el linker script. */
uint32_t fast_buffer[16] __attribute__((section(".fast_ram")));

/* Variable compartida con una ISR (Interrupt Service Routine).
   Sin volatile, el main podría no ver el cambio realizado por la ISR. */
volatile int data_ready = 0;

/* Función que no retorna [C11]. Informa al compilador que no debe
   preparar el retorno de la función, optimizando el epílogo. */
_Noreturn void system_halt(void) {
    /* En un MCU real, esto podría ser una instrucción de espera (WFI) */
    while (1) {
        __asm__ volatile ("nop");
    }
}

int main(void) {
    /* Acceso a hardware */
    UART_REGS->CONTROL = 0x01;

    /* Uso de memoria rápida */
    for (int i = 0; i < 16; i++) {
        fast_buffer[i] = i;
    }

    /* Lógica de control */
    if (global_initialized > 0) {
        global_uninitialized = 100;
        data_ready = 1;
    }

    /* Si algo falla, entramos en un estado terminal */
    if (UART_REGS->STATUS == 0xFFFFFFFF) {
        system_halt();
    }

    return 0;
}

Desglose del ejemplo

El puntero UART_REGS utiliza volatile dentro de la estructura. Esto es crucial porque, aunque la dirección del puntero es constante (*const), el contenido de la memoria que apunta cambia de forma externa al flujo principal del programa (el hardware modifica el STATUS). Si no fuera volatile, el compilador podría decidir que, como el código no escribe en STATUS, el valor nunca cambia, entrando en un bucle infinito si se hiciera un while(UART_REGS->STATUS == 0).

La variable fast_buffer utiliza __attribute__((section(".fast_ram"))). Esto no significa nada para el compilador por sí solo; es una instrucción para el linker. El programador debe haber definido en el script de enlazado que la sección .fast_ram mapea a una dirección física de SRAM de baja latencia. Sin esto, el linker la trataría como una sección .bss estándar.

global_initialized es una variable de la sección .data. En el binario final (el .elf o .bin), verás que se reserva espacio para el número 42 en la zona de Flash. En el arranque, el código de inicialización del microcontrolador leerá ese 42 de la Flash y lo escribirá en la dirección de la RAM asignada a global_initialized.

La función system_halt usa _Noreturn. Esto es vital en sistemas embebidos para evitar que el compilador genere código de retorno innecesario que intente volver a una dirección de memoria que, tras un desbordamiento o un error crítico, ya no es válida o es inestable.

El error frecuente

Uno de los errores más insidiosos en sistemas embebidos es olvidar el calificador volatile en variables compartidas con interrupciones.

// ERROR: Falta 'volatile'
int flag_error = 0; 

void ISR_Handler(void) {
    flag_error = 1; // Se ejecuta en un contexto distinto al main
}

int main(void) {
    while (flag_error == 0) {
        // El compilador optimiza esto como: if (flag_error == 0) while(true);
        // porque no ve cambios de 'flag_error' dentro del bucle.
    }
    // El código nunca llega aquí, aunque la ISR cambie la variable.
}

Este error es extremadamente difícil de depurar porque el código funciona perfectamente en modo Debug (donde las optimizaciones están desactivadas) pero falla estrepitosamente en modo Release con optimizaciones de alto nivel (-O2 o -O3). Herramientas como AddressSanitizer no suelen detectar esto, ya que no es un error de acceso a memoria, sino un error de visibilidad de la memoria ante el optimizador.

136

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio