Cuando el compilador termina, genera dos archivos principales: el .bin (que es código binario puro, ciego e incomentado) y el .elf. El archivo ELF contiene la imagen del programa combinada con metadatos cruciales: tablas de símbolos, información de depuración DWARF y, fundamentalmente, la Tabla de Cabeceras de Sección (Section Headers).
Muchos desarrolladores novatos cometen el error de mirar únicamente el tamaño del archivo .bin para saber cuánta Flash usan. Eso ignora cómo se distribuyen las variables en tiempo de ejecución. Las fórmulas matemáticas exactas para el silicio son:
Flash_Total = .isr_vector + .text + .rodata + .data.data cuenta en Flash? Porque los valores iniciales de tus variables globales (ej. uint32_t precio = 15;) deben guardarse en la memoria no volátil para que, al encender la máquina, el código de arranque los copie hacia la RAM.
RAM_Estatica = .data + .bss.bss contiene todas las variables globales no inicializadas o inicializadas en cero (ej. float buffer_adc[1024];).
RAM_Libre = RAM_Total_Hardware - RAM_Estatica - Heap_Minimo - Stacks_FreeRTOSUn Quality Gate (Puerta de Calidad) es un punto de control automatizado en el servidor de CI. Si el código compila con éxito pero supera un umbral estricto (Budget Threshold), el script devuelve un código de salida exit(1). Esto rompe la integración, marca el commit con una cruz roja en GitHub y previene que el firmware se publique para descarga FOTA.
No dependeremos de comandos básicos de bash. Crearemos un script profesional en Python llamado tools/check_memory_budget.py. El script invoca la herramienta binaria arm-none-eabi-size con el parámetro -A (System V format), analiza línea por línea las secciones del archivo ELF, calcula los totales exactos, imprime un reporte visual en consola y aborta con código de error 1 si se supera el presupuesto asignado para el Despachador de Gas LP.
Código del Script: tools/check_memory_budget.py
#!/usr/bin/env python3
"""
@file check_memory_budget.py
@brief Analizador de Presupuesto de Memoria ELF para CI/CD (Quality Gate)
@author Arquitecto de Sistemas Embebidos
"""
import sys
import subprocess
import os
# ==============================================================================
# PRESUPUESTOS ESTRICTOS DE HARDWARE (STM32F401RE - Partición FOTA)
# ==============================================================================
# Límite impuesto por el Linker Script para el Slot de Aplicación (Sectores 4 y 5)
FLASH_BUDGET_BYTES = 192 * 1024 # 196,608 Bytes (192 KB)
# Capacidad total de la memoria SRAM física del microcontrolador
RAM_TOTAL_BYTES = 96 * 1024 # 98,304 Bytes (96 KB)
# Umbral de advertencia para RAM estática (Dejar mínimo 40% libre para Stacks de FreeRTOS)
RAM_STATIC_WARNING_LIMIT = int(RAM_TOTAL_BYTES * 0.60) # 60% máximo (58,982 Bytes)
def obtener_secciones_elf(elf_path, size_tool="arm-none-eabi-size"):
"""Ejecuta arm-none-eabi-size en formato System V y extrae las secciones."""
if not os.path.exists(elf_path):
print(f"[-] ERROR CRÍTICO: No se encontró el archivo ELF en '{elf_path}'")
sys.exit(2)
try:
# Ejecutar arm-none-eabi-size -A
cmd = [size_tool, "-A", elf_path]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode("utf-8")
except FileNotFoundError:
print(f"[-] ERROR: La herramienta '{size_tool}' no está instalada o no está en el PATH.")
sys.exit(2)
except subprocess.CalledProcessError as e:
print(f"[-] ERROR al ejecutar toolchain size: {e.output.decode('utf-8')}")
sys.exit(2)
secciones = {}
lineas = output.splitlines()
# Parseo de salida formato System V
# Encabezados típicos: section, size, addr
for linea in lineas:
partes = linea.split()
if len(partes) >= 2:
nombre = partes[0]
try:
tamano = int(partes[1])
# Ignorar encabezados o metadatos de depuración
if not nombre.startswith(".debug") and not nombre.startswith(".comment"):
secciones[nombre] = tamano
except ValueError:
continue
return secciones
def generar_barra_progreso(porcentaje, longitud=30):
"""Genera una barra visual ASCII para el log de CI."""
llenado = int(longitud * (porcentaje / 100.0))
if llenado > longitud:
llenado = longitud
vacio = longitud - llenado
return f"[{'#' * llenado}{'.' * vacio}]"
def main():
if len(sys.argv) < 2:
print("Uso: python3 check_memory_budget.py [size_tool_path]")
sys.exit(1)
elf_file = sys.argv[1]
tool = sys.argv[2] if len(sys.argv) > 2 else "arm-none-eabi-size"
print("=" * 75)
print(" AUDITORÍA DE PRESUPUESTO DE MEMORIA EMBEBIDA - FASE 5 FOTA ")
print("=" * 75)
print(f"[*] Analizando binario: {elf_file}\n")
secciones = obtener_secciones_elf(elf_file, tool)
# Identificación y agregación de secciones
# FLASH = Código (.text) + Vectores (.isr_vector) + Constantes (.rodata) + Inicializadores (.data)
text_size = secciones.get(".text", 0)
isr_size = secciones.get(".isr_vector", 0)
rodata_size = secciones.get(".rodata", 0)
data_size = secciones.get(".data", 0)
bss_size = secciones.get(".bss", 0)
total_flash_used = text_size + isr_size + rodata_size + data_size
total_ram_used = data_size + bss_size
pct_flash = (total_flash_used / FLASH_BUDGET_BYTES) * 100.0
pct_ram = (total_ram_used / RAM_TOTAL_BYTES) * 100.0
# Desglose en consola
print(f"{'Sección ELF':<20} | {'Tamaño (Bytes)':<15} | {'Destino Hardware'}")
print("-" * 75)
print(f"{'.isr_vector':<20} | {isr_size:<15} | FLASH (Tabla de Vectores)")
print(f"{'.text':<20} | {text_size:<15} | FLASH (Instrucciones C)")
print(f"{'.rodata':<20} | {rodata_size:<15} | FLASH (Constantes / API54)")
print(f"{'.data':<20} | {data_size:<15} | FLASH (Valores init) & RAM")
print(f"{'.bss':<20} | {bss_size:<15} | RAM (Zero-init globals)")
print("-" * 75)
print("\n" + "=" * 75)
print(" RESUMEN DE ASIGNACIÓN FÍSICA ")
print("=" * 75)
# Evaluación de FLASH
print(f"[*] OCUPACIÓN FLASH (Límite FOTA Slot: {FLASH_BUDGET_BYTES / 1024:.1f} KB):")
print(f" Consumo: {total_flash_used:,} Bytes ({total_flash_used / 1024:.2f} KB) - {pct_flash:.2f}%")
print(f" Barra : {generar_barra_progreso(pct_flash)} {pct_flash:.1f}%")
bytes_libres_flash = FLASH_BUDGET_BYTES - total_flash_used
# Evaluación de RAM
print(f"\n[*] OCUPACIÓN ESTÁTICA RAM (Total Silicio: {RAM_TOTAL_BYTES / 1024:.1f} KB):")
print(f" Consumo: {total_ram_used:,} Bytes ({total_ram_used / 1024:.2f} KB) - {pct_ram:.2f}%")
print(f" Barra : {generar_barra_progreso(pct_ram)} {pct_ram:.1f}%")
bytes_libres_ram = RAM_TOTAL_BYTES - total_ram_used
print(f" Headroom para FreeRTOS (Heap + Stacks): {bytes_libres_ram:,} Bytes ({bytes_libres_ram / 1024:.2f} KB)")
print("-" * 75)
# Control de Salida (Quality Gatekeeper)
fallo_encontrado = False
if total_flash_used > FLASH_BUDGET_BYTES:
exceso = total_flash_used - FLASH_BUDGET_BYTES
print(f"\n[!] ERROR FATAL (BUILD BREAKER): El firmware excede el presupuesto FLASH por {exceso} bytes.")
print(f" ¡El binario invadirá el Slot OTA de descarga! Despliegue FOTA abortado.")
fallo_encontrado = True
else:
print(f"\n[+] FLASH VERIFICADA: Margen de seguridad de {bytes_libres_flash:,} bytes ({bytes_libres_flash / 1024:.2f} KB) restantes.")
if total_ram_used > RAM_STATIC_WARNING_LIMIT:
print(f"[!] ADVERTENCIA: La memoria estática supera el 60% de la RAM total.")
print(f" Riesgo elevado de Stack Overflow en tareas concurrentes de FreeRTOS.")
# Podrías decidir si esto rompe el build o solo advierte
if fallo_encontrado:
print("\n[-] QUALITY GATE: RECHAZADO (Exit Code 1)")
sys.exit(1)
else:
print("\n[+] QUALITY GATE: APROBADO (Exit Code 0)")
sys.exit(0)
if __name__ == "__main__":
main()
Ahora actualizamos el archivo de integración continua (.github/workflows/firmware_build.yml del Día 266) para insertar el paso del Guardián de Memoria inmediatamente después de la compilación. Si el script falla, el pipeline se detiene antes de generar el archivo `.bin` y publicarlo en AWS.
# ==========================================================================
# PASO AÑADIDO EN .github/workflows/firmware_build.yml
# ==========================================================================
- name: Compilar Firmware (Make)
run: |
make -j4
# 4.1 AUDITORÍA DE MEMORIA Y QUALITY GATE
- name: Verificar Presupuesto de Memoria FOTA (.elf Profiling)
run: |
# Dar permisos de ejecución e invocar el auditor en Python
chmod +x tools/check_memory_budget.py
python3 tools/check_memory_budget.py build/despachador_gas_lp.elf arm-none-eabi-size
# Este paso solo se ejecutará si el paso anterior terminó con Exit Code 0:
- name: Generar Hash del Binario y Publicar Artefactos
run: |
sha256sum build/despachador_gas_lp.bin > build/despachador_gas_lp.bin.sha256
Objetivo del día: Telemetría de Compilación, Presupuestos Estáticos y Prevención de Brickeo Remoto.
En el software convencional, si una aplicación de escritorio consume 5 MB adicionales de memoria, el sistema operativo utiliza la memoria virtual (Swap) en el disco duro y el usuario apenas lo nota. En un microcontrolador Cortex-M4 sin memoria virtual, la memoria es una frontera física inquebrantable. Un solo byte de desbordamiento en la Flash corrompe el sector contiguo; un desbordamiento en la RAM pisa el Stack Pointer de FreeRTOS, alterando el conteo de pulsos del dispensador de gas.
Al implementar el **CI Memory Profiler con Quality Gates**, has blindado el ciclo de vida del producto. Has transformado el archivo ejecutable `.elf` en una radiografía forense completa. Si mañana un ingeniero intenta integrar una nueva característica que sobrepase los 192 KB asignados para el Slot de Aplicación, el pipeline de GitHub Actions abortará la compilación al instante con un código de error explícito, impidiendo que ese código peligroso llegue jamás a los servidores de AWS ni a la memoria de un despachador físico en una gasolinera. Tu infraestructura de software ahora cuenta con las mismas barreras de contención que exige la industria automotriz y médica (normas IEC 62304 / ISO 26262).
.bin) es engañoso. La ocupación real en la memoria FLASH del microcontrolador se calcula sumando las secciones .isr_vector, .text, .rodata y .data (valores de inicialización persistentes)..data (copiada a RAM en el arranque) y .bss (variables en cero). El remanente es el Headroom obligatorio para alojar el Heap y los Stacks dinámicos de las tareas de FreeRTOS.arm-none-eabi-size -A permite parsear de forma automatizada los tamaños de cada bloque y compararlos con los límites de partición definidos en el Linker Script (Día 258).sys.exit(1) si el tamaño del ejecutable supera el presupuesto asignado (ej. 192 KB para el Slot FOTA). Esto detiene el pipeline de CI/CD de inmediato, evitando que firmwares incompatibles sean distribuidos a la flota.