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.
| Pieza | Qué es | Quién la usa |
|---|---|---|
| Tabla de vectores | Lista 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ón | El hardware, al reiniciar y al ocurrir una interrupción |
| Código | Las instrucciones de tu programa, de las bibliotecas y del código de arranque | El procesador |
| Constantes | Tablas const, textos y valores que nunca cambian | El programa, solo en lectura |
| Valores iniciales de datos | Copia de lo que deben valer las variables globales inicializadas al arrancar | El código de arranque, que la copia a la RAM |
| Opcionalmente: bootloader, configuración, encabezado de versión y firma | Zonas reservadas para actualización y seguridad | El 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
.ca 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
| Sección | Contiene | Ocupa flash | Ocupa RAM |
|---|---|---|---|
.isr_vector | Tabla de vectores | Sí | No |
.text | Instrucciones | Sí | No |
.rodata | Constantes (const) y literales de texto | Sí | No |
.data | Variables globales y estáticas con valor inicial distinto de cero | Sí (valor inicial) | Sí (la variable) |
.bss | Variables globales y estáticas sin valor inicial o iniciadas en cero | No | Sí |
| Heap y pila | Memoria dinámica y variables locales con llamadas | No | Sí (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
.texttotal ocupa0xb4c= 2892 bytes. De ellos,main.oaporta0x4b0= 1200. sensor.oaporta 512 bytes de.rodata(0x200): es una tabla de constantes. Se nota enseguida quién consume las constantes.- La sección
.datavive en0x20000000de la RAM, pero su valor inicial está en0x08000ef4de la flash (load address), justo después de.rodata. - El
.bsstermina en0x20000018 + 0x158 = 0x20000170. Todo lo que queda de RAM hasta0x20005000es 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
consttodo 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
-Osy dejar que el enlazador elimine lo que nadie usa con-ffunction-sections -fdata-sectionsy--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 enprintfsi no hace falta; puede ahorrar varios KB. - Elegir tipos del tamaño justo:
uint8_touint16_ten lugar deintofloaten 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
Salida de size
—
Mapa de direcciones
—
Cambiá cualquier opción para ver cómo se reparten las secciones.
Recorrido guiado
- 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á elsize:datavale 0 ybss552. - Quitale el
consta la tabla. Eldatasube a 512, eltextbaja 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. - 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.
- 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.
- 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. - Mirá el mapa de direcciones:
.isr_vectorprimero, luego.texty.rodata, la copia de.dataen 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:
- Mirá el resumen.
sizedatext,dataybss. Flash usada =text + data; RAM estática =data + bss. - 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). - Buscá a los más pesados.
nm --size-sort -Slista los símbolos de mayor a menor. Los primeros cinco explican casi siempre la mayor parte del problema. - 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. - 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. - Cambiá una cosa por vez y volvé a medir.
- Dejá el control en el proceso. Que la salida de
sizeaparezca 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 amain(). - 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. .texty.rodatasolo ocupan flash;.bsssolo ocupa RAM;.dataocupa 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,statico dinámica; en AVR, además, hace faltaPROGMEM. - El linker script describe las regiones de memoria del chip y cómo repartir las secciones; el archivo
.mapy herramientas comosizeynm --size-sortmuestran 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.