← Índice del curso

Sistemas Embebidos · Tema 8

El arranque del sistema

Entre el instante en que el chip recibe energía y la primera línea de main() pasan cosas que nadie escribió en tu programa: se lee una tabla, se reponen los valores de las variables, se configura el reloj. Si una de esas etapas falla, tu código correcto corre sobre un entorno roto.

01 · Punto de partida

Lo que pasa antes de main()

En el tema anterior viste que el firmware es una imagen con secciones: una parte en la flash, otra que debe vivir en la RAM, otra que debe empezar en cero. Pero un chip recién energizado no sabe nada de eso. Su RAM tiene contenido indefinido, su reloj es el más lento y simple que tiene, y su procesador solo sabe hacer una cosa: leer de una dirección fija y empezar a ejecutar.

El arranque (boot o startup) es la secuencia que convierte ese estado en el entorno que tu código da por hecho al entrar a main(): variables globales con sus valores, pila válida, reloj configurado. Casi nunca se escribe a mano (viene en un archivo del fabricante), pero hay que entenderla, porque cuando algo falla antes de main() el depurador no tiene nada de tu código que mostrar.

Usamos de nuevo un Cortex-M como ejemplo, con las direcciones del tema 7: tabla de vectores en 0x08000000, Reset_Handler en 0x08000130, copia inicial de .data en 0x08000EF4 y tope de pila en 0x20005000.

02 · El primer instante

El encendido y las fuentes de reset

Al aplicar la alimentación, la tensión no sube de golpe: tarda microsegundos o milisegundos en estabilizarse. Un circuito de reinicio por encendido (power-on reset) mantiene al procesador detenido hasta que la tensión supera un umbral seguro; recién entonces libera el reset y el procesador empieza a trabajar. Lo mismo ocurre cuando la tensión cae demasiado (brown-out): el chip se reinicia antes de ejecutar instrucciones con una tensión insuficiente, que podrían dar resultados erróneos.

El encendido es solo una de las causas posibles de un reinicio. El resultado es siempre el mismo (el procesador vuelve a empezar desde el vector de reset), pero la causa importa para el diagnóstico:

Fuentes de reinicio habituales de un microcontrolador.
FuenteCuándo ocurreQué indica al diagnosticar
Encendido (POR)Al aplicar la alimentaciónArranque normal
Caída de tensión (brown-out)La alimentación baja de un umbralFuente o batería débil, picos de corriente
Pin de resetPulsador o circuito externo lo lleva a nivel bajoReset manual o ruido en el pin
WatchdogEl programa dejó de reiniciar el temporizador a tiempoEl programa se colgó o tardó demasiado (tema 38)
Reinicio por softwareEl propio firmware lo pide (por ejemplo, tras una actualización)Reinicio deliberado

Casi todos los chips guardan la causa del último reinicio en un registro de estado (en la familia STM32F1, el registro RCC_CSR con banderas como IWDGRSTF, PORRSTF o SFTRSTF). Leerlo al comienzo de main() y registrarlo es de las prácticas que más tiempo ahorran en campo: un equipo que «se reinicia solo» deja de ser un misterio si sabés que cada reinicio fue por watchdog o por caída de tensión (tema 37).

uint32_t causa = RCC->CSR;                 // registro de estado de reset (STM32F1)

if (causa & RCC_CSR_IWDGRSTF)       registrar("reinicio por watchdog");
else if (causa & RCC_CSR_SFTRSTF)   registrar("reinicio por software");
else if (causa & RCC_CSR_PINRSTF)   registrar("reinicio por el pin de reset");
else if (causa & RCC_CSR_PORRSTF)   registrar("encendido o caída de tensión");

RCC->CSR |= RCC_CSR_RMVF;                 // borrar las banderas para el próximo reinicio

También hay que saber dónde empieza el procesador. En los Cortex-M siempre lee la tabla de vectores desde el inicio del espacio de direcciones, pero ese inicio puede ser un alias de otra memoria. En la familia de ejemplo, dos pines (BOOT0 y BOOT1) eligen si en la dirección 0x00000000 aparece la flash principal (el caso normal), la memoria del sistema (el cargador de fábrica del tema 3) o la SRAM. Un pin BOOT0 mal conectado es una causa clásica de «el programa no arranca»: el chip está arrancando otra cosa.

03 · El primer paso del hardware

La tabla de vectores y el vector de reset

Al liberarse el reset, un Cortex-M hace por hardware dos lecturas y nada más, antes de ejecutar ninguna instrucción:

  1. Lee la palabra 0 de la tabla de vectores y la carga en el puntero de pila (SP). Así el procesador tiene una pila válida desde el primer momento y el código de arranque puede ya estar escrito en C.
  2. Lee la palabra 1, que es la dirección del Reset_Handler, y la carga en el contador de programa (PC). Esa es la primera instrucción que se ejecuta.

El bit menos significativo de la palabra 1 vale siempre 1: indica que el código usa el juego de instrucciones Thumb (el único en los Cortex-M). Por eso en la tabla vas a ver 0x08000131 aunque el handler esté en 0x08000130. El resto de la tabla contiene las direcciones de las rutinas de excepción y de interrupción:

Primeras entradas de la tabla de vectores de un Cortex-M3 (cada entrada ocupa 4 bytes).
EntradaDesplazamientoContenido
00x00Valor inicial del puntero de pila (tope de la RAM)
10x04Reset_Handler
20x08NMI (interrupción no enmascarable)
30x0CHardFault (error grave del procesador)
4 a 60x10 a 0x18Fallos de memoria, de bus y de uso
11, 14, 150x2C, 0x38, 0x3CLlamada al sistema (SVC), PendSV y SysTick
16 en adelante0x40 en adelanteLas interrupciones de los periféricos del fabricante

La misma tabla se usa después, durante la ejecución normal: cuando un periférico dispara una interrupción, el hardware busca en esta tabla la dirección de su rutina y salta a ella (tema 15). Por eso la tabla es también el contrato entre el hardware y tu código: cada rutina de interrupción debe estar registrada en la posición exacta. En C se define como un arreglo de punteros a función colocado, mediante un atributo, en la sección .isr_vector que el linker script pone primero:

extern uint32_t _estack;                       // símbolo del linker script: tope de la pila
void Reset_Handler(void);
void Default_Handler(void) { while (1) { } }   // rutina por defecto: se queda trabado

// Las rutinas no definidas por el programador quedan apuntando a Default_Handler
void NMI_Handler(void)       __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void)   __attribute__((weak, alias("Default_Handler")));

__attribute__((section(".isr_vector"), used))
void (* const vector_table[])(void) = {
    (void (*)(void))((uint32_t)&_estack),      //  0: valor inicial de SP
    Reset_Handler,                             //  1: reset
    NMI_Handler,                               //  2
    HardFault_Handler,                         //  3
    /* ...resto de las excepciones e interrupciones... */
    SysTick_Handler,                           // 15
};

La palabra weak permite que tu programa defina su propia SysTick_Handler y reemplace a la genérica sin tocar la tabla. Si una interrupción se dispara y nadie definió su rutina, el programa cae en Default_Handler y se queda en un bucle: una trampa conveniente para el depurador, que muestra enseguida dónde se detuvo.

04 · Preparar el terreno

El Reset_Handler: .data, .bss y main()

El Reset_Handler es la primera función que corre. Su trabajo es dejar el entorno tal como el estándar de C lo promete a un programa antes de main():

extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;   // direcciones del linker script
extern int main(void);
void SystemInit(void);
void __libc_init_array(void);

void Reset_Handler(void) {
    // 1) copiar los valores iniciales de .data desde la flash a la RAM
    uint32_t *origen = &_sidata;
    for (uint32_t *destino = &_sdata; destino < &_edata; ) {
        *destino++ = *origen++;
    }

    // 2) poner a cero todo .bss
    for (uint32_t *p = &_sbss; p < &_ebss; ) {
        *p++ = 0;
    }

    SystemInit();             // 3) configurar reloj y flash
    __libc_init_array();      // 4) inicializar la biblioteca y los constructores globales

    main();                   // 5) por fin, la aplicación

    while (1) { }             // main no debería retornar nunca
}

Los símbolos _sidata, _sdata, _edata, _sbss y _ebss son los que viste en el linker script del tema 7: no son variables, son direcciones, por eso se toma su dirección con &. Cada vuelta del primer bucle copia una palabra de 4 bytes de la flash a la RAM; el segundo bucle escribe ceros en la zona de .bss.

Esas dos operaciones son el origen de dos garantías del lenguaje C que ahora ves en acción:

  • Las variables globales y estáticas con valor inicial lo tienen cuando empieza main(), porque alguien las copió de la flash.
  • Las que no tienen valor inicial valen cero, porque alguien limpió la zona. Las variables locales no tienen esa garantía, porque nacen en la pila sin que nadie las toque.

Si el código de arranque se omite, se corrompe o el linker script entrega límites erróneos, main() corre en un mundo donde esas garantías no existen: un umbral = 1500 puede valer cualquier cosa, y un contador global puede empezar en un valor aleatorio. Son errores de apariencia absurda («¿por qué la variable no vale lo que declaré?») cuya causa está en el entorno.

La copia y la limpieza cuestan tiempo, proporcional a la cantidad de RAM inicializada. Con 2 KB de .data son 512 palabras; a unos 5 ciclos por palabra, 2560 ciclos: unos 320 µs a 8 MHz o 36 µs a 72 MHz. Es poco, pero en un sistema con 256 KB de RAM puede llegar a milisegundos, y conviene saberlo si hay un requisito de tiempo de arranque. También es posible reservar variables que no se inicializan (una sección .noinit) para que conserven su valor a través de un reinicio por software, por ejemplo un contador de reinicios consecutivos.

05 · Velocidad de trabajo

SystemInit y el reloj del sistema

Tras un reinicio, el microcontrolador siempre arranca con la fuente de reloj más simple y segura: un oscilador RC interno de pocos megahercios (8 MHz en la familia de ejemplo). Funciona sin ningún componente externo y arranca enseguida, pero es lento y poco preciso. Para llegar a la velocidad que el producto necesita hay que configurar el reloj, y de eso se encarga una función que el fabricante llama habitualmente SystemInit:

  1. Configurar la flash. Antes de subir la frecuencia, se fijan los estados de espera que la flash necesita a esa velocidad (tema 3). Si se invierte el orden, el procesador pediría instrucciones a una memoria que todavía no puede entregarlas a ese ritmo y se colgaría.
  2. Arrancar el cristal externo (HSE) y esperar a que se estabilice. Es una espera por hardware: el programa consulta una bandera de «listo».
  3. Configurar el PLL, el multiplicador que convierte los 8 MHz del cristal en 72 MHz (multiplicador por 9), y esperar a que enganche.
  4. Ajustar los divisores de los buses de periféricos, porque algunos no soportan la frecuencia máxima (en esta familia, el bus APB1 se limita a 36 MHz).
  5. Conmutar el reloj del sistema al PLL y esperar la confirmación.

El reloj condiciona todo lo que viene después. Un temporizador, una UART o un I²C calculan sus divisores a partir de la frecuencia del reloj; si el programa supone 72 MHz y el chip corre a 8, todo funciona, pero nueve veces más lento. La UART lo muestra con claridad: configurada para 115200 baudios con la cuenta hecha a 72 MHz, transmite en realidad a 12.800 si el reloj sigue en 8 MHz. Por eso es una causa frecuente de comunicaciones que «casi» funcionan.

Un punto delicado son las esperas. Si el cristal no arranca (mal soldado, capacitores de carga equivocados, ruido), el bucle que espera la bandera «cristal listo» no termina nunca, y el sistema se queda colgado antes de main(). Los buenos arranques ponen un límite de tiempo a esa espera y, si se agota, vuelven a trabajar con el oscilador interno y registran la falla. Que la señal de vida (el LED, una salida por consola) aparezca justo después de configurar el reloj es una buena forma de saber hasta dónde llegó el arranque.

06 · Un programa antes del programa

El bootloader

Un bootloader (cargador de arranque) es un programa pequeño que ocupa el comienzo de la flash y se ejecuta antes que la aplicación. Su razón de existir es poder actualizar el firmware sin un programador externo: arranca, decide si hay una actualización pendiente (un botón apretado, un comando por puerto serie, una imagen nueva descargada) y, si no, salta a la aplicación. En el tema 5 viste que algunos chips traen uno de fábrica en la memoria del sistema; también se escribe uno propio para productos con actualización en campo (temas 42 y 43).

Con un bootloader, la flash se reparte en zonas, y la aplicación ya no empieza en el comienzo:

Reparto de una flash de 64 KB con un bootloader de 16 KB.
ZonaDesdeTamañoContenido
Bootloader0x0800000016 KB (0x4000)Su propia tabla de vectores y su código; es lo que arranca el chip
Aplicación0x0800400048 KB (0xC000)El firmware del producto, con su tabla de vectores al comienzo

Esto exige tres cambios coordinados. Primero, el linker script de la aplicación debe declarar la flash con origen 0x08004000 y largo 48 KB, para que todas las direcciones se calculen desde ahí. Segundo, el procesador sigue buscando su tabla en el comienzo de la memoria, así que la aplicación debe avisarle dónde está la suya, escribiendo la dirección en el registro de control VTOR (vector table offset). Tercero, el bootloader debe saber saltar a la aplicación: carga el SP y el PC desde la tabla de ella, exactamente lo que haría el hardware en un reset:

#define APP_BASE 0x08004000u
typedef void (*funcion_t)(void);

void saltar_a_aplicacion(void) {
    uint32_t sp    = *(volatile uint32_t *)APP_BASE;           // palabra 0: pila de la aplicación
    uint32_t reset = *(volatile uint32_t *)(APP_BASE + 4u);    // palabra 1: su Reset_Handler

    if ((sp & 0x2FFE0000u) != 0x20000000u) {                   // ¿el SP apunta a la RAM?
        return;                                                // no: no hay aplicación válida
    }
    __disable_irq();                                           // sin interrupciones durante el traspaso
    SysTick->CTRL = 0;                                         // apagar el temporizador del sistema
    SCB->VTOR = APP_BASE;                                      // la aplicación usa su propia tabla
    __set_MSP(sp);                                             // cargar la pila de la aplicación
    ((funcion_t)reset)();                                      // saltar a su Reset_Handler
}

La comprobación del puntero de pila es una validación mínima de que la zona contiene una aplicación: una flash borrada lee 0xFFFFFFFF, que no apunta a la RAM. Un bootloader serio verifica además una suma de comprobación o una firma de toda la imagen antes de saltar (tema 43). Y cuidado con el estado que deja: periféricos encendidos o interrupciones activas del bootloader pueden sorprender a la aplicación, así que antes del salto se los apaga.

Dos reglas prácticas: el bootloader debe ser pequeño, simple y casi inmutable, porque un error en él deja el equipo inutilizable sin acceso físico; y la aplicación debe poder arrancar también sin él (con un programador) para poder depurarla. Eso se resuelve con dos configuraciones de construcción que cambian solo el origen de la flash.

07 · Diagnóstico

Cuando el sistema no arranca

Una falla de arranque es desconcertante porque ocurre antes de que tu código pueda informar nada: el LED no parpadea, la consola no imprime. Pero tiene un conjunto reducido de causas, y el depurador (tema 45) permite mirar el PC y los registros incluso antes de main():

Síntomas de arranque frecuentes y sus causas probables.
SíntomaCausa probableCómo comprobarlo
No arranca; el depurador no se conectaAlimentación incorrecta, pin de reset flotante, pines de depuración reasignadosMedir la tensión; reiniciar con el reset presionado; ver si el programa desactivó SWD
Arranca el cargador de fábrica en lugar del programaPin BOOT0 mal conectado o en altoMedir BOOT0; conectarlo a masa con una resistencia
Falla inmediata: HardFault en las primeras instruccionesTabla de vectores mal ubicada; -mcpu equivocado; linker script de otro chipComparar el origen del linker script con el mapa de memoria; mirar el PC en el fallo
El PC queda en un bucle dentro de SystemInitEl cristal no arranca y se espera su bandera para siempreMedir el cristal con un osciloscopio; revisar capacitores de carga
Reinicios repetidos antes de main()Watchdog que vence durante una inicialización larga; caída de tensión en el arranqueLeer la causa del reset; mirar la alimentación durante el arranque
Arranca, pero las variables tienen valores imposiblesNo se copió .data ni se limpió .bss; símbolos del linker script erróneosMirar _sdata, _edata, _sbss y _ebss en el mapa
Arranca solo con el depurador conectadoEl depurador mantiene relojes o desactiva el watchdog; un periférico depende de su presenciaProbar con alimentación propia y sin el adaptador

Una táctica barata y eficaz es agregar señales de vida en el propio arranque: encender un LED o poner un pin en alto en cada etapa importante (después de limpiar la RAM, después del reloj, al entrar a main()). Con un osciloscopio o un analizador lógico, la secuencia de pulsos dice hasta dónde llegó el arranque sin necesidad de depurador, y sirve también en la línea de producción.

08 · Analogía con programación

El arranque, en Python

Esta simulación reproduce lo que hace el Reset_Handler con una RAM de seis palabras: tres de .data (umbral, modo y retardo_ms, con sus valores iniciales guardados en la «flash») y tres de .bss (contador, indice y estado). Al encender, la RAM se llena con contenido indefinido (un generador congruencial hace de «ruido» reproducible). Después se ejecuta el arranque completo y dos variantes en las que se omite un paso.

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

RAM_INICIO = 0x20000000
SDATA, EDATA = 0x20000000, 0x2000000C      # .data: 3 palabras
SBSS, EBSS = 0x2000000C, 0x20000018        # .bss: 3 palabras
DATA_EN_FLASH = [1500, 3, 90]              # valores iniciales guardados en la flash
NOMBRES = ["umbral", "modo", "retardo_ms", "contador", "indice", "estado"]
ESPERADO = [1500, 3, 90, 0, 0, 0]


def encender():
    """RAM tras el encendido: contenido indefinido (generador reproducible)."""
    x, ram = 2024, {}
    for i in range(6):
        x = (x * 1664525 + 1013904223) & 0xFFFFFFFF
        ram[RAM_INICIO + 4 * i] = x
    return ram


def reset_handler(ram, copiar_data=True, limpiar_bss=True):
    if copiar_data:                                    # 1) copiar .data de la flash
        for i, valor in enumerate(DATA_EN_FLASH):
            ram[SDATA + 4 * i] = valor
    if limpiar_bss:                                    # 2) poner a cero .bss
        for direccion in range(SBSS, EBSS, 4):
            ram[direccion] = 0


def main_ve(ram, titulo):
    print(titulo)
    for i, nombre in enumerate(NOMBRES):
        valor = ram[RAM_INICIO + 4 * i]
        marca = "" if valor == ESPERADO[i] else "   <-- basura"
        print(f"  {nombre:11s} = {valor:>10d}{marca}")


for titulo, copiar, limpiar in (
    ("Arranque correcto", True, True),
    ("Sin copiar .data", False, True),
    ("Sin limpiar .bss", True, False),
):
    ram = encender()
    reset_handler(ram, copiar, limpiar)
    main_ve(ram, titulo)
    print()

Resultado: con el arranque correcto, main() ve 1500, 3, 90 y tres ceros. Sin copiar .data, las tres primeras valen 87935527, 3706503514 y 2873663729. Sin limpiar .bss, las tres últimas valen 2529699228, 1339098187 y 4246015278. Las variables no reciben «valores al azar» por un error de main(): reciben el residuo de la RAM porque nadie las preparó. Probá cambiar la semilla 2024 por otra: la basura cambia, pero el efecto es el mismo. ¿Qué pasaría si la pila y el contenido del resto de la RAM tampoco se inicializaran?

La lección es de responsabilidades: el código de la aplicación no hace nada mal en ninguna de las tres corridas; la diferencia está por completo en lo que ocurrió antes de que empezara. Por eso, ante un comportamiento absurdo en el primer instante de la ejecución, vale la pena mirar el arranque antes de revisar la lógica.

09 · Exploración interactiva

Simulador de arranque

Este laboratorio recorre, paso a paso, el arranque de un Cortex-M con las direcciones de los últimos temas. Presioná «Siguiente paso» para avanzar de a una etapa o «Ejecutar hasta main()» para correr todo. Las casillas permiten sabotear el arranque, omitiendo la copia de .data, la limpieza de .bss o la configuración del reloj, para ver cómo es el entorno que recibe main(). Cambiar una casilla reinicia la simulación.

Del reset a main()

Etapa—
SP (puntero de pila)—
PC (contador de programa)—
Reloj del sistema—
Resultado—

Variables en la RAM

—

Registro del arranque

—

Avanzá paso a paso con «Siguiente paso» o corré todo el arranque con «Ejecutar hasta main()».

Recorrido guiado

  1. Sin marcar nada, presioná «Siguiente paso» una vez. El hardware carga SP = 0x20005000 y PC = 0x08000130 a partir de las dos primeras palabras de la tabla. Todavía no se ejecutó ninguna instrucción, y la RAM sigue con basura.
  2. Avanzá dos pasos más: primero .data recibe 1500, 3 y 90; luego .bss queda en ceros. Ahora las seis variables tienen el valor que el programa espera.
  3. Seguí hasta main(): el reloj pasó de 8 a 72 MHz y el resultado es «Arranque correcto».
  4. Marcá Omitir la copia de .data y ejecutá hasta main(). umbral, modo y retardo_ms valen basura aunque el código diga 1500, 3 y 90. Es la causa de variables «inexplicablemente raras».
  5. Desmarcala y marcá Omitir la limpieza de .bss. Ahora son las globales sin valor inicial las que arrancan con basura: un contador que «no empieza en cero».
  6. Probá Omitir SystemInit: las variables están bien, pero el reloj sigue en 8 MHz. El texto del resultado calcula la consecuencia para una UART configurada con el reloj de 72 MHz: transmite a 12.800 baudios en lugar de 115200.
Desafío: ¿por qué el SP se carga antes de ejecutar nada?

Porque el Reset_Handler está escrito en C y cualquier función de C necesita una pila: para guardar la dirección de retorno al llamar a otra función y para sus variables locales. Si el hardware no cargara el SP antes de la primera instrucción, el primer push o la primera llamada escribirían en un lugar cualquiera. Los Cortex-M resuelven esto con el primer valor de la tabla de vectores, y por eso su código de arranque puede escribirse enteramente en C. Otras arquitecturas necesitan unas pocas instrucciones de ensamblador para fijar el SP a mano antes de llamar a nada.

10 · Método de trabajo

Cómo diagnosticar un arranque

Cuando un equipo no arranca o arranca mal, avanzá de afuera hacia adentro y de lo más barato a lo más caro de comprobar:

  1. Alimentación. Medí la tensión en el pin del chip, no en el conector. Observá si cae durante el arranque (con un osciloscopio, no con un multímetro).
  2. Pines de arranque y de reset. BOOT0, BOOT1 y el pin de reset deben tener un nivel definido, no flotar.
  3. Conexión de depuración. ¿El depurador ve el chip? Si no, el problema es previo al firmware (o el firmware reconfiguró los pines de depuración).
  4. Causa del último reinicio. ¿Fue encendido, watchdog, software, pin? Ese dato descarta la mitad de las hipótesis.
  5. Dónde está el PC. Con el depurador, detené el procesador: ¿está en Reset_Handler, en SystemInit, en Default_Handler o en HardFault? Cada una apunta a una causa.
  6. Direcciones. Compará el origen de la flash del linker script y el contenido real de la tabla de vectores con el mapa de memoria del chip.
  7. El reloj. Si hay un cristal, comprobá que oscila. Si el arranque se cuelga en una espera, agregá un límite de tiempo.
  8. Señales de vida. Un pin que cambia en cada etapa permite medir cuánto tarda el arranque y en cuál se detiene.

Y un hábito preventivo: registrar al inicio de cada ejecución la causa de reinicio y la versión del firmware. Un equipo en campo que «a veces se reinicia» deja de ser un misterio con ese dato guardado en memoria no volátil.

11 · Comprensión

Confusiones frecuentes

«main() es lo primero que se ejecuta»

Antes corre el Reset_Handler, que prepara la RAM y el reloj e inicializa la biblioteca. Algunos fabricantes y entornos ocultan esa parte, pero siempre existe. Por eso un depurador puede detenerse en código que no escribiste cuando algo falla muy temprano.

«Si reinicio por software, la RAM queda limpia»

Un reinicio no borra la RAM: conserva su contenido aunque el programa no pueda confiar en él. Lo que deja limpio es el código de arranque, que repone .data y pone a cero .bss. Solo las variables colocadas en una sección sin inicializar (.noinit) sobreviven con su valor a un reinicio, lo que se aprovecha a propósito para guardar, por ejemplo, el motivo del último fallo.

«El bootloader es parte de la aplicación»

Son dos programas independientes con tablas de vectores propias, en zonas distintas de la flash. Se compilan por separado, se actualizan por separado (el bootloader casi nunca) y la aplicación debe saber en qué dirección vive.

«Mover la aplicación solo requiere cambiar el linker script»

Hace falta además actualizar el registro VTOR para que las interrupciones usen la tabla de la aplicación, y que el bootloader salte con la pila correcta. Si falta alguno, el sistema arranca y después falla en la primera interrupción.

«Si funciona con el depurador conectado, funciona»

El depurador puede alterar el comportamiento: mantiene alimentados bloques que en producción se apagan, detiene el watchdog y a veces impide el sueño profundo. Hay que probar siempre el equipo con alimentación propia y sin el adaptador.

«El arranque lento no importa»

Importa cuando hay un requisito: un equipo médico que debe responder en 100 ms, un controlador de motor que debe quedar en estado seguro enseguida. Conocer lo que tarda cada etapa permite cumplirlo (por ejemplo, evitar copiar RAM innecesaria o esperar un cristal lento).

12 · Práctica guiada

Predecí antes de comprobar

1. Leer el vector de reset

En 0x08000004 el procesador lee 0x080001F1. ¿Dónde está el primer código que se ejecuta? ¿Por qué el valor es impar?

Ver solución

El código empieza en 0x080001F0. El bit menos significativo en 1 indica modo Thumb, el único juego de instrucciones de los Cortex-M; el hardware lo usa para elegir el modo y no forma parte de la dirección. Una tabla con un valor par en esa posición provoca un fallo inmediato.

2. El costo del arranque

Un programa tiene 2 KB de .data y el bucle de copia emplea unos 5 ciclos por palabra de 4 bytes. ¿Cuánto tarda la copia a 8 MHz y a 72 MHz?

Ver solución

2 KB = 2048 bytes = 512 palabras. 512 × 5 = 2560 ciclos. A 8 MHz: 2560 / 8.000.000 = 320 µs. A 72 MHz: 2560 / 72.000.000 ≈ 35,6 µs. Es una razón más para configurar el reloj rápido antes de copiar datos si el tiempo de arranque importa.

3. Repartir la flash con un bootloader

Un chip de 128 KB de flash empieza en 0x08000000. Se reservan 20 KB para el bootloader. ¿En qué dirección debe empezar la aplicación, de qué tamaño es su zona y qué hay que configurar para que sus interrupciones funcionen?

Ver solución

20 KB = 20 × 1024 = 20.480 bytes = 0x5000. La aplicación empieza en 0x08005000 y dispone de 128 − 20 = 108 KB (hasta 0x0801FFFF). El comienzo debe coincidir con el límite de una página de borrado (0x5000 son 20 páginas de 1 KB, así que cumple) y respetar la alineación que exige VTOR para la tabla de vectores (un múltiplo de 0x200 en este caso, que también cumple). Hay que cambiar el origen y el largo de la flash en el linker script de la aplicación (ORIGIN = 0x08005000, LENGTH = 108K) y escribir esa dirección en VTOR antes de habilitar interrupciones.

4. Diagnóstico

Un equipo nuevo no arranca. El depurador muestra el PC detenido en un bucle while (!(RCC->CR & RCC_CR_HSERDY)) dentro de SystemInit. ¿Cuál es la causa más probable y cómo la verificarías?

Ver solución

El programa espera la bandera «cristal listo» y esta nunca se activa: el cristal externo no oscila. Causas: cristal mal soldado o inadecuado, capacitores de carga equivocados o ausentes, pines del oscilador con ruido. Se verifica con un osciloscopio en los pines del cristal. Como mejora de software, agregar un tiempo máximo de espera y volver al oscilador interno registrando la falla.

5. Valores al llegar a main()

Dadas las declaraciones globales int a = 7;, int b;, static int c = 0; y la local int d; dentro de main(), ¿qué valor tiene cada una al empezar main()?

Ver solución

a = 7 (.data, copiada de la flash). b = 0 (.bss, limpiada). c = 0 (valor inicial cero: va a .bss). d: indeterminado: es una local, vive en la pila y nadie la inicializa; contiene el residuo de lo que hubo antes. Solo las globales y estáticas tienen garantía.

6. Reiniciar sin perder el motivo

Querés guardar el número de reinicios consecutivos por watchdog en una variable que sobreviva a un reinicio por software. ¿Cómo la declararías y por qué una variable común no sirve?

Ver solución

Con un atributo de sección sin inicializar, por ejemplo __attribute__((section(".noinit"))) uint32_t reinicios_wdt;, y una entrada .noinit en el linker script que no esté en .data ni en .bss. Una variable común vuelve a cero (si es .bss) o a su valor inicial (si es .data) en cada arranque, porque el Reset_Handler la repone. Además, tras un encendido la RAM tiene basura, por lo que conviene acompañarla de un valor de validación (un «número mágico») para saber si el contenido es confiable.

13 · Síntesis

Ideas para recordar

  • Al encenderse, el chip espera a que la alimentación se estabilice, y su RAM conserva contenido indefinido; el reloj arranca con un oscilador interno lento.
  • Un reinicio puede deberse a encendido, caída de tensión, el pin de reset, el watchdog o el software; la causa queda en un registro y conviene leerla y registrarla al inicio.
  • Al salir del reset, el hardware lee de la tabla de vectores el valor inicial del SP (palabra 0) y la dirección del Reset_Handler (palabra 1, con el bit 0 en 1 por el modo Thumb).
  • El Reset_Handler copia .data de la flash a la RAM, pone a cero .bss, llama a SystemInit, inicializa la biblioteca y recién entonces llama a main().
  • Por esos pasos, las globales y estáticas con valor inicial lo tienen y las demás valen cero; las variables locales no tienen esa garantía.
  • SystemInit configura la flash, el cristal, el PLL y los divisores: sin él, el sistema sigue en el reloj lento y todo lo que calcula frecuencias sale mal.
  • Un bootloader es un programa previo que decide si actualizar o arrancar la aplicación; exige un linker script con otro origen, un VTOR apuntando a la tabla de la aplicación y un salto correcto.
  • Las fallas de arranque se diagnostican de afuera hacia adentro: alimentación, pines de arranque, depurador, causa de reinicio, posición del PC, direcciones del linker script y reloj; las señales de vida ayudan.

¿Listo para el tema 9?

Antes de avanzar, explicá qué lee el hardware antes de ejecutar la primera instrucción, qué efecto tiene en main() que se omita la copia de .data, y qué cambios exige mover la aplicación detrás de un bootloader. Si podés justificar las tres respuestas, entendés el camino entre el encendido y tu código.

En el próximo tema entramos a main() con la inicialización del hardware: el orden correcto para configurar relojes, pines y periféricos, y cómo evitar los errores de orden que hacen que un periférico no responda.