← Índice del curso

Sistemas Embebidos · Tema 7

Qué es el firmware

El firmware no es solo tu código: es una imagen binaria completa que vive en la memoria no volátil del chip, con su tabla de vectores, sus constantes y los valores iniciales de las variables. Entender sus secciones y el archivo de mapa te dice, sin adivinar, cuánto ocupa cada cosa y dónde está.

01 · Punto de partida

Un programa que vive en el chip

La palabra firmware se usa tanto que a veces se vuelve vaga. En este curso la tomamos con precisión: es el programa que está grabado en la memoria no volátil de un dispositivo (normalmente la flash) y que el procesador ejecuta desde el momento en que se enciende, sin que nadie lo cargue. A diferencia de una aplicación común, no hay un sistema operativo que lo lea del disco, lo ubique en memoria y lo lance: el chip arranca directamente en él.

Eso tiene una consecuencia concreta que atraviesa todo el bloque: el firmware debe venir listo para ejecutarse tal cual está. Cada instrucción, cada constante y cada tabla tiene que estar en la dirección exacta que el hardware espera, porque nadie las va a reubicar. La herramienta que logra eso es el enlazador, y el documento que le dice cómo hacerlo es el linker script.

En el tema 5 viste la línea de montaje que produce el firmware. Acá abrimos el resultado y miramos qué hay adentro; en el tema 8 veremos qué hace el procesador con él en el instante del arranque.

02 · Anatomía

Qué contiene la imagen

Una imagen de firmware es una secuencia de bytes que se graba a partir de una dirección fija de la flash (por ejemplo 0x08000000). No es solo «las instrucciones»: contiene varias piezas, cada una con un propósito.

Componentes de una imagen de firmware, en el orden en que suelen aparecer.
PiezaQué esQuién la usa
Tabla de vectoresLista de direcciones: el valor inicial de la pila y la dirección de la rutina de reset, seguidas de las direcciones de las rutinas de interrupciónEl hardware, al reiniciar y al ocurrir una interrupción
CódigoLas instrucciones de tu programa, de las bibliotecas y del código de arranqueEl procesador
ConstantesTablas const, textos y valores que nunca cambianEl programa, solo en lectura
Valores iniciales de datosCopia de lo que deben valer las variables globales inicializadas al arrancarEl código de arranque, que la copia a la RAM
Opcionalmente: bootloader, configuración, encabezado de versión y firmaZonas reservadas para actualización y seguridadEl cargador de arranque y el sistema de actualización (temas 42 y 43)

La primera pieza es la más importante y la más ignorada. Cuando el procesador sale de un reinicio, no empieza en main(): lee dos palabras de la tabla de vectores. La primera se carga en el puntero de pila; la segunda es la dirección del reset handler y a ella salta. Todo lo demás (limpiar la RAM, copiar los datos iniciales, configurar el reloj y, por fin, llamar a main()) lo hace ese código de arranque. Si la tabla de vectores no está donde corresponde, el chip no arranca.

Por eso el firmware completo no se llama «programa» sino imagen: representa exactamente lo que va a quedar en la memoria del chip, byte por byte. Con un cargador de arranque (tema 5) o con una actualización por aire, lo que se transmite es esa imagen.

03 · El camino

De main() al binario

Recordemos la cadena del tema 5 desde otra perspectiva: qué se va decidiendo en cada etapa sobre la memoria.

  • El compilador traduce cada archivo .c a un objeto y clasifica cada cosa que contiene en una sección: las instrucciones en una, las constantes en otra, las variables con valor inicial en otra, las variables sin valor inicial en otra. Todavía no sabe en qué dirección quedará cada sección.
  • El enlazador reúne las secciones de todos los objetos (las de igual nombre, juntas), las ordena según el linker script y les asigna direcciones definitivas. Resuelve así las referencias entre archivos.
  • El resultado es el ELF, del que se extrae la imagen que se graba.

La clasificación en secciones es la idea central del tema. No es una curiosidad del compilador: es lo que permite que una misma variable ocupe lugares distintos en dos memorias distintas según su naturaleza (algo constante va a la flash, que sobra; algo que cambia va a la RAM, que escasea). Y es lo que hace posible el trabajo del tema 2: saber exactamente cuánto de cada memoria consume tu programa.

04 · Mapa de la memoria

Las secciones: .text, .rodata, .data y .bss

Distribución de las secciones en flash y en RAM En la flash, de abajo hacia arriba: tabla de vectores, código, constantes y la copia inicial de los datos. En la RAM, de abajo hacia arriba: datos inicializados, datos sin inicializar, el heap que crece hacia arriba, espacio libre y la pila que crece hacia abajo desde el tope. Una flecha indica que al arrancar la copia inicial de los datos se copia de la flash a la RAM. FLASH (no volátil) RAM (volátil) .data (copia inicial)valores de arranque .rodataconstantes y textos .textel código .isr_vectortabla de vectores pila (stack) ↓espacio libre heap ↑ .bss .data al arrancar se copia a la RAM direcciones bajas direcciones bajas
Secciones principales de un firmware.
SecciónContieneOcupa flashOcupa RAM
.isr_vectorTabla de vectoresSíNo
.textInstruccionesSíNo
.rodataConstantes (const) y literales de textoSíNo
.dataVariables globales y estáticas con valor inicial distinto de ceroSí (valor inicial)Sí (la variable)
.bssVariables globales y estáticas sin valor inicial o iniciadas en ceroNoSí
Heap y pilaMemoria dinámica y variables locales con llamadasNoSí (se reserva espacio)

Tres ideas de este cuadro merecen quedar claras:

  • .data cuesta el doble. Una variable inicializada necesita un lugar en la flash donde guardar su valor inicial y otro en la RAM donde vive. Al arrancar, el código de arranque copia uno al otro. En el diagrama, la flecha punteada. En los mapas esto se llama LMA (load address, donde se guarda en la flash) y VMA (virtual address, donde se usa en la RAM).
  • .bss es gratis en flash. Una variable sin valor inicial no necesita guardar nada: al arrancar el código de arranque pone a cero toda la zona. Por eso un arreglo grande sin inicializar es barato en flash, pero no en RAM.
  • La pila y el heap no figuran en el archivo. Se reservan en la RAM que sobra, uno desde el fondo hacia arriba (el heap) y otro desde el tope hacia abajo (la pila). Si se encuentran, el programa se corrompe (tema 29).

05 · Cada cosa en su lugar

Dónde vive cada variable

La sección donde termina una variable depende de cómo la declarás. Es una decisión que tomás, a veces sin darte cuenta, cada vez que escribís una línea de C:

#include <stdint.h>
#include <stdlib.h>

const uint16_t TABLA[4] = {10, 20, 30, 40};  // .rodata: solo flash (8 bytes)
uint32_t contador;                           // .bss:    RAM, arranca en 0 (4 bytes)
uint32_t umbral = 1500;                      // .data:   4 bytes en flash + 4 bytes en RAM
static uint8_t buffer_rx[128];               // .bss:    128 bytes de RAM, sin costo en flash

const char *saludo = "Hola";                 // el texto "Hola" en .rodata (5 B);
                                             // el puntero saludo en .data (4 B flash + 4 B RAM)
const char saludo2[] = "Hola";               // todo en .rodata (5 B): no usa RAM

void medir(void) {                           // el código de la función: .text
    int local = 5;                           // pila o registro: sin lugar fijo
    static int llamadas;                     // .bss: existe siempre, aunque la función no corra
    char tmp[64];                            // 64 bytes de pila mientras la función está activa
    char *dinamico = malloc(32);             // heap: se pide en tiempo de ejecución
    free(dinamico);
}

Las diferencias sutiles son las que más sorprenden. const char *saludo = "Hola" y const char saludo2[] = "Hola" parecen equivalentes, pero la primera crea además un puntero modificable, que ocupa RAM, y la segunda no. Y una variable static dentro de una función no vive en la pila: es una global disfrazada, con memoria reservada para siempre.

Otro punto importante: una variable local no ocupa un lugar fijo. Vive en la pila mientras la función está activa (o en un registro del procesador) y desaparece al salir. Por eso el archivo de mapa no la muestra, y por eso un buffer local grande es una trampa: el enlazador no avisa y el problema aparece en ejecución si la pila no alcanza.

06 · Las reglas del reparto

El linker script

El linker script es el archivo que le dice al enlazador cuánta memoria hay, dónde empieza cada una y cómo debe distribuirse cada sección. Es específico de cada chip y suele entregarlo el fabricante. Un ejemplo mínimo para la familia del tema 3 (64 KB de flash y 20 KB de RAM):

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

_estack = ORIGIN(RAM) + LENGTH(RAM);          /* tope de la pila */

SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH       /* siempre primero */

  .text : {
    *(.text*)                                  /* el código */
    *(.rodata*)                                /* y las constantes */
    . = ALIGN(4);
  } > FLASH

  _sidata = LOADADDR(.data);                   /* dónde está la copia en flash */

  .data : {
    _sdata = .;                                /* inicio de .data en RAM */
    *(.data*)
    . = ALIGN(4);
    _edata = .;                                /* fin de .data */
  } > RAM AT> FLASH                            /* vive en RAM, se guarda en flash */

  .bss : {
    _sbss = .;
    *(.bss*) *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM
}

Se lee casi como una descripción de lo que vimos. MEMORY define las dos regiones y sus atributos (rx: lectura y ejecución; rwx: también escritura). SECTIONS dice qué va en cada una y en qué orden: primero la tabla de vectores, siguen el código y las constantes en la flash, y las variables en la RAM. La directiva AT> FLASH es la que crea la doble vida de .data: la variable se usa en la RAM pero su valor inicial se guarda en la flash.

Los símbolos que define el script (_sdata, _edata, _sidata, _sbss, _ebss, _estack) no son variables: son direcciones que el código de arranque usa para copiar .data y poner en cero .bss. En el tema siguiente los vas a ver en acción. Si el linker script no coincide con el chip (una dirección o un tamaño equivocados), el firmware compila perfecto y no arranca.

07 · Auditar

Leer el archivo .map

Con la opción -Wl,-Map=firmware.map el enlazador deja un informe completo de lo que hizo: cuánto ocupa cada sección, de qué objeto vino cada pieza y a qué dirección fue. Es la herramienta más honesta para saber dónde se fue la memoria. Un fragmento simplificado de un mapa real:

Memory Configuration

Name             Origin             Length             Attributes
FLASH            0x08000000         0x00010000         xr
RAM              0x20000000         0x00005000         xrw

.isr_vector      0x08000000      0x130
 .isr_vector     0x08000000      0x130 startup.o

.text            0x08000130      0xb4c
 .text           0x08000130       0xc0 startup.o
 .text           0x080001f0      0x4b0 main.o
 .text           0x080006a0      0x35c sensor.o
 .text           0x080009fc      0x280 uart.o

.rodata          0x08000c7c      0x278
 .rodata         0x08000c7c       0x78 main.o
 .rodata         0x08000cf4      0x200 sensor.o

.data            0x20000000       0x18 load address 0x08000ef4
 .data           0x20000000       0x10 main.o
 .data           0x20000010        0x8 uart.o

.bss             0x20000018      0x158
 .bss            0x20000018       0x40 main.o
 .bss            0x20000058       0x18 sensor.o
 .bss            0x20000070      0x100 uart.o

Cada línea tiene tres datos: la sección, la dirección donde quedó y el tamaño (en hexadecimal), y el objeto que lo aportó. Se leen así:

  • El .text total ocupa 0xb4c = 2892 bytes. De ellos, main.o aporta 0x4b0 = 1200.
  • sensor.o aporta 512 bytes de .rodata (0x200): es una tabla de constantes. Se nota enseguida quién consume las constantes.
  • La sección .data vive en 0x20000000 de la RAM, pero su valor inicial está en 0x08000ef4 de la flash (load address), justo después de .rodata.
  • El .bss termina en 0x20000018 + 0x158 = 0x20000170. Todo lo que queda de RAM hasta 0x20005000 es el espacio disponible para heap y pila.

Para obtener resúmenes sin leer el mapa completo, hay comandos de la misma cadena de herramientas:

$ arm-none-eabi-size firmware.elf              # resumen: text, data, bss
$ arm-none-eabi-size -A firmware.elf           # detalle por sección
$ arm-none-eabi-nm --size-sort -S firmware.elf # símbolos ordenados por tamaño
$ arm-none-eabi-objdump -h firmware.elf        # encabezados de sección y direcciones

El segundo y el tercero son los más útiles en el día a día: nm --size-sort te muestra, de mayor a menor, las funciones y variables que más pesan. Cuando el firmware no entra, ahí está la lista de sospechosos. Es frecuente descubrir que un solo printf con formato de decimales, o una tabla olvidada, ocupa más que toda la lógica del producto.

08 · Ahorrar

Reducir el tamaño sin perder funciones

Antes de recortar funciones, hay palancas que no cambian lo que hace el producto:

  • Marcar const todo lo que no cambia. Tablas, textos y configuraciones pasan de RAM a flash. Es el ahorro de RAM más barato que existe (salvo en AVR, como vimos).
  • Evitar inicializar arreglos grandes con valores. Un buffer que se llena en ejecución debe ir sin inicializar (.bss), no con una lista de ceros explícita que no se necesita (aunque el compilador suele tratar el cero como .bss).
  • Optimizar por tamaño con -Os y dejar que el enlazador elimine lo que nadie usa con -ffunction-sections -fdata-sections y --gc-sections (tema 5).
  • Usar una biblioteca estándar reducida (por ejemplo, newlib-nano con --specs=nano.specs) y no habilitar el formato de números decimales en printf si no hace falta; puede ahorrar varios KB.
  • Elegir tipos del tamaño justo: uint8_t o uint16_t en lugar de int o float en arreglos grandes (tema 2).
  • Activar la optimización en tiempo de enlace (-flto), que permite al compilador ver todo el programa a la vez; ayuda, aunque complica la depuración.

Siempre se mide antes y después: una optimización que no se mide es una creencia. El comando size ejecutado en cada compilación (tema 5) permite ver el efecto de cada cambio.

09 · Analogía con programación

Un enlazador en miniatura, en Python

Para entender qué hace el enlazador, nada mejor que hacer uno pequeño. El programa recibe cuatro «objetos» (con los bytes que cada uno aporta a las secciones), los coloca uno detrás de otro dentro de cada sección alineando a 4 bytes, asigna direcciones en la flash y en la RAM, y calcula lo que informaría size.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

FLASH_INICIO, FLASH_TAM = 0x08000000, 64 * 1024
RAM_INICIO, RAM_TAM = 0x20000000, 20 * 1024

OBJETOS = {   # bytes que aporta cada objeto a cada sección
    "startup.o": dict(vectores=304, text=192,  rodata=0,   data=0,  bss=0),
    "main.o":    dict(vectores=0,   text=1200, rodata=120, data=16, bss=64),
    "sensor.o":  dict(vectores=0,   text=860,  rodata=512, data=0,  bss=24),
    "uart.o":    dict(vectores=0,   text=640,  rodata=0,   data=8,  bss=256),
}


def alinear(n):                       # el enlazador alinea a 4 bytes
    return (n + 3) // 4 * 4


def colocar(secciones, cursor):
    totales = {}
    for seccion in secciones:
        inicio = cursor
        for nombre, obj in OBJETOS.items():
            tam = obj[seccion]
            if tam:
                print(f"  {seccion:8s} {nombre:10s} {cursor:#010x} {tam:5d} B")
                cursor += alinear(tam)
        totales[seccion] = cursor - inicio
    return cursor, totales


print("FLASH")
fin_flash, t_flash = colocar(("vectores", "text", "rodata"), FLASH_INICIO)
copia_data = fin_flash                # aquí se guarda la copia inicial de .data
print("RAM")
fin_ram, t_ram = colocar(("data", "bss"), RAM_INICIO)

data, bss = t_ram["data"], t_ram["bss"]
text = t_flash["vectores"] + t_flash["text"] + t_flash["rodata"]
flash_usada = text + data
print(f"\nsize -> text {text}  data {data}  bss {bss}  dec {text + data + bss}")
print(f"Copia inicial de .data en flash: {copia_data:#010x}")
print(f"Flash usada: {flash_usada} B ({100 * flash_usada / FLASH_TAM:.1f} % de {FLASH_TAM // 1024} KB)")
print(f"RAM estática: {data + bss} B ({100 * (data + bss) / RAM_TAM:.1f} % de {RAM_TAM // 1024} KB)")
print(f"Tope de la pila (_estack): {RAM_INICIO + RAM_TAM:#010x}")
print(f"Libre para heap y pila: {RAM_INICIO + RAM_TAM - fin_ram} B")

Resultado: la tabla de vectores ocupa 0x08000000, el código va desde 0x08000130 y las constantes desde 0x08000c7c; la copia inicial de .data queda en 0x08000ef4. En RAM, .data comienza en 0x20000000 y .bss en 0x20000018. El resumen es text 3828, data 24, bss 344, dec 4196; la flash usada es de 3852 bytes (5,9 % de 64 KB) y la RAM estática de 368 bytes (1,8 % de 20 KB). Quedan 20.112 bytes entre el fin de .bss y el tope de la pila. Probá cambiar el bss de uart.o a 20000: la RAM dejará de entrar y verás las direcciones pasarse del límite, aunque este programa no lo avisa. ¿Cómo lo avisarías?

Este enlazador de juguete omite casi todo lo real (símbolos, relocalizaciones, bibliotecas), pero muestra la idea esencial: es una operación de acomodar bloques y sumar tamaños. Y deja al descubierto algo importante: el enlazador real sí avisa cuando una región se desborda («region `RAM` overflowed by N bytes»), y ese mensaje es un cálculo tan simple como el que acabás de leer.

10 · Exploración interactiva

Secciones y memoria

Armá un programa pequeño eligiendo cómo se declaran sus datos: una tabla de 512 bytes, un buffer de recepción de 512 bytes, textos de mensajes de 400 bytes y 40 bytes de contadores globales. El laboratorio calcula cuántos bytes caen en cada sección, genera la salida de size y un mapa de direcciones, y compara con la memoria del chip elegido. La pila y el heap se eligen aparte.

Declaraciones, secciones y memoria

text (código + constantes)—
data—
bss—
Flash usada—
RAM (datos, heap y pila)—
Pila: necesaria de reservada—
Resultado—

Salida de size

—

Mapa de direcciones

—

Cambiá cualquier opción para ver cómo se reparten las secciones.

Recorrido guiado

  1. Con los valores iniciales (AVR, tabla const, buffer en .bss), el programa entra con margen: unos 7,2 KB de flash (22 %) y 1 KB de RAM (52 %). Mirá el size: data vale 0 y bss 552.
  2. Quitale el const a la tabla. El data sube a 512, el text baja en la misma cantidad y la RAM pasa al 77 %, ajustado. La misma tabla ahora se paga dos veces: una en flash y otra en RAM.
  3. Cambiá el buffer a «con valores iniciales». La RAM no cambia (sigue siendo 512 bytes), pero la flash crece 512 bytes porque ahora hay que guardar el contenido inicial. Inicializar un buffer que se va a llenar en ejecución es pagar sin necesidad.
  4. Pasá el buffer a «local de una función» con la pila de 512 B: la RAM estática baja, pero la pila necesita 712 B y solo hay 512. No cumple, sin ningún aviso del enlazador. Subí la pila a 1 KB y entra.
  5. Poné todo en RAM (tabla sin const, buffer con valores, mensajes copiados), pila de 1 KB y heap de 1 KB: pasa del 170 % de la RAM del AVR. Cambiá al Cortex-M3: la misma configuración usa el 17 %. Es el mismo código, con otro chip.
  6. Mirá el mapa de direcciones: .isr_vector primero, luego .text y .rodata, la copia de .data en la flash (LMA) y su ubicación definitiva en la RAM (VMA). La pila arranca en el tope de la RAM y crece hacia abajo.
Desafío: ¿por qué el buffer en .data no gasta más RAM pero sí más flash?

Una variable global ocupa la misma cantidad de RAM tanto en .bss como en .data: sus 512 bytes. La diferencia es que en .bss el valor inicial es siempre cero, y no hace falta guardarlo en ningún lado (el código de arranque pone a cero la zona), mientras que en .data el valor inicial puede ser cualquiera y hay que conservarlo en la flash para copiarlo al arrancar. Por eso el costo extra de inicializar está en la flash, no en la RAM.

11 · Método de trabajo

Cómo auditar el uso de memoria

Cada vez que el firmware cambia de tamaño sin que lo hayas decidido, o que se acerca al límite de un chip, seguí estos pasos:

  1. Mirá el resumen. size da text, data y bss. Flash usada = text + data; RAM estática = data + bss.
  2. Sumá la pila y el heap. No aparecen en size. Sumalos a la RAM estática y compará con el total del chip (tema 2).
  3. Buscá a los más pesados. nm --size-sort -S lista los símbolos de mayor a menor. Los primeros cinco explican casi siempre la mayor parte del problema.
  4. Revisá el mapa por objeto. En el .map, ¿qué archivo aporta más a .text? ¿Es código propio o una biblioteca? Una función de formato o de matemática de punto flotante suele sorprender.
  5. Clasificá cada dato grande. ¿Es constante? Entonces const. ¿Se llena en ejecución? Entonces sin inicializar. ¿Es temporal? Entonces local, con la pila bien dimensionada.
  6. Cambiá una cosa por vez y volvé a medir.
  7. Dejá el control en el proceso. Que la salida de size aparezca en cada compilación, y fallá la construcción si se supera un límite acordado.

Con este método se llega a respuestas concretas a preguntas como «¿por qué creció 6 KB desde la versión anterior?» o «¿qué me cuesta incluir esta biblioteca?», en minutos y sin adivinar.

12 · Comprensión

Confusiones frecuentes

«El firmware es solo mi código»

Incluye además la tabla de vectores, el código de arranque, las bibliotecas que enlazaste, las constantes y los valores iniciales de los datos. En muchos proyectos, el código propio es menos de la mitad de la imagen.

«size muestra toda la RAM que usa el programa»

Solo muestra la RAM estática (data + bss). La pila y el heap se reservan y se llenan en ejecución, y no figuran. Un programa con 1 KB de bss puede necesitar 3 KB de RAM en total.

«Las variables globales empiezan con basura»

En C, las variables globales y estáticas empiezan en cero, porque el código de arranque pone a cero la sección .bss. Las que tienen basura son las locales, que viven en la pila y no se inicializan solas.

«const siempre ahorra RAM»

En un Cortex-M, sí: la constante se lee directamente de la flash. En un AVR, no: sin PROGMEM se copia igual a la RAM. También hay que tener cuidado con los punteros: const char *p = "texto" deja el puntero en RAM, aunque el texto esté en flash.

«Inicializar a cero no cuesta nada»

Una variable global inicializada explícitamente en cero se coloca en .bss (el compilador lo reconoce) y no cuesta flash. Pero inicializar con cualquier otro valor, incluso un solo byte distinto de cero en un arreglo grande, la manda a .data y se paga la tabla completa en flash.

«Si compila y no da errores de enlace, la memoria alcanza»

El enlazador solo avisa si la flash o la RAM estática se desbordan. No sabe nada de cuánta pila va a hacer falta, ni de cuánto heap vas a pedir. Esos desbordes se descubren en ejecución, a veces horas después.

13 · Práctica guiada

Predecí antes de comprobar

1. ¿En qué sección queda?

Indicá la sección (.text, .rodata, .data, .bss, pila o heap) de cada elemento: (a) const uint8_t FUENTE[256] = {...};; (b) uint16_t lecturas[100]; global; (c) int limite = 80; global; (d) una variable char linea[64] dentro de una función; (e) el bloque que devuelve malloc(100); (f) static int cuenta; dentro de una función.

Ver solución

(a) .rodata (solo flash). (b) .bss (200 bytes de RAM). (c) .data (flash y RAM). (d) pila (64 bytes mientras la función está activa). (e) heap. (f) .bss: una estática local es una global con alcance limitado.

2. Leer el resultado de size

Un programa da text 12000, data 300, bss 1500. ¿Cuánta flash y cuánta RAM estática usa? Si el chip tiene 32 KB de flash y 2 KB de RAM, ¿qué porcentaje de cada memoria ocupa? ¿Hay un problema?

Ver solución

Flash = text + data = 12.300 bytes (12.300 / 32.768 ≈ 37,5 %). RAM estática = data + bss = 1.800 bytes (1.800 / 2.048 ≈ 87,9 %). Hay un problema: sin contar la pila ni el heap, la RAM ya pasa del 75 % recomendado. Solo quedan unos 248 bytes para todo lo demás.

3. El costo de olvidar const

Una tabla de 1000 bytes se declara sin const en un Cortex-M con 4 KB de RAM. ¿Cuánto cuesta en flash y en RAM? ¿Y con const?

Ver solución

Sin const: 1000 bytes de flash (valor inicial) más 1000 de RAM, es decir, casi el 24 % de los 4096 bytes de RAM. Con const: 1000 bytes de flash y 0 de RAM. Una sola palabra recupera un cuarto de la RAM.

4. Puntero o arreglo

Compará el costo en un Cortex-M (punteros de 4 bytes) de const char *s = "Hola"; y de const char s[] = "Hola";, ambas globales.

Ver solución

La primera: el texto (5 bytes con el terminador) en .rodata, más el puntero s en .data: 4 bytes de RAM y 4 de flash. Total: 9 bytes de flash y 4 de RAM. La segunda: solo el arreglo en .rodata, 5 bytes de flash y 0 de RAM. Si además el puntero se declara const char *const s, tampoco usa RAM.

5. Leer el mapa

En un mapa, .bss empieza en 0x20000018 y mide 0x158 bytes. La RAM termina en 0x20005000. ¿Dónde termina .bss y cuánta memoria queda para el heap y la pila?

Ver solución

Termina en 0x20000018 + 0x158 = 0x20000170. Quedan 0x20005000 − 0x20000170 = 0x4E90 = 20.112 bytes para el heap (que crece hacia arriba) y la pila (que crece hacia abajo). Si el programa necesita más que eso entre los dos, se pisan.

6. Por qué se copia .data

Un colega pregunta: «si la variable umbral = 1500 ya tiene su valor en el código, ¿para qué se copia nada?». Explicalo.

Ver solución

Porque la variable debe poder cambiar y la flash no se puede escribir como una variable. Debe vivir en la RAM. Pero la RAM se pierde al apagar, así que en cada arranque hay que reponer su valor inicial (1500), y ese valor tiene que estar guardado en algún lado no volátil: la flash. Por eso hay dos copias: una en flash (el valor inicial, el LMA) y otra en RAM (la variable viva, el VMA), y el código de arranque copia la primera sobre la segunda.

14 · Síntesis

Ideas para recordar

  • El firmware es una imagen binaria que reside en la flash y se ejecuta desde el reinicio; contiene la tabla de vectores, el código, las constantes y los valores iniciales de los datos.
  • El procesador no empieza en main(): lee de la tabla de vectores el valor inicial de la pila y la dirección del reset handler, que prepara el entorno y recién después llama a main().
  • El compilador clasifica todo en secciones: .text (código), .rodata (constantes), .data (variables con valor inicial) y .bss (variables sin valor inicial); el enlazador les asigna direcciones.
  • .text y .rodata solo ocupan flash; .bss solo ocupa RAM; .data ocupa ambas, porque guarda su valor inicial en la flash y vive en la RAM.
  • La pila y el heap se reservan en la RAM sobrante y no figuran en size; el enlazador no avisa si no alcanzan.
  • Cómo se declara una variable decide dónde vive: const, global, inicializada o no, local, static o dinámica; en AVR, además, hace falta PROGMEM.
  • El linker script describe las regiones de memoria del chip y cómo repartir las secciones; el archivo .map y herramientas como size y nm --size-sort muestran el resultado y permiten auditar el consumo.
  • Flash usada = text + data; RAM estática = data + bss; la RAM total necesaria incluye además la pila y el heap.

¿Listo para el tema 8?

Antes de avanzar, decí en qué sección cae una variable global inicializada y por qué ocupa memoria en dos lugares; calculá la flash y la RAM estática a partir de una salida de size; y explicá por qué el procesador no empieza en main(). Si podés justificar las tres respuestas, entendés cómo está armada la imagen que se graba en el chip.

En el próximo tema seguimos esa imagen en acción: el arranque del sistema. Vamos a ver el vector de reset, el código de inicio que copia .data y pone a cero .bss, y la intervención del bootloader.