DÍA 267 DE 365

Fase 5: Diseño Industrial y Proyecto Final

📖 Teoría a Estudiar

Anatomía de un Archivo .ELF (Executable and Linkable Format)

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).

La Ecuación Real de Memoria

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:

El Concepto de "Quality Gate" en DevOps

Un 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.

DISECCIÓN ELF Y CONTROL DE PRESUPUESTO EN CI/CD firmware.elf .isr_vector (1 KB) .text (142 KB) Instrucciones ARM C .rodata (22 KB) Tablas API 54, Constantes .data (4 KB) Variables Inicializadas .bss (32 KB) Buffers RTOS / DMA SLOT FLASH FOTA (192 KB) USADO: 169 KB (.isr + .text + .ro + .data) 88.0% Consumo LIBRE: 23 KB (12%) LÍMITE ESTRICTO: 192 KB SRAM TOTAL (96 KB) ESTÁTICO: 36 KB (.data + .bss) 37.5% de RAM DINÁMICO: 60 KB Stacks de Tareas RTOS Colas de Mensajes Headroom Seguro

⚙️ Implementación del Guardián: Script Python de Análisis

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()
        

Integración en el Pipeline YAML (GitHub Actions)

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
        

🚀 Avance del Proyecto Tesis: Despachador de Gas LP

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).

📝 Resumen del Día