← Índice del curso

Sistemas Embebidos · Tema 9

Inicialización del hardware

Cuando termina el arranque y empieza main(), el chip está vivo pero casi todo lo que lo rodea está apagado: los periféricos sin reloj, los pines como entradas flotantes, las interrupciones deshabilitadas. Inicializar el hardware es encender cada bloque en el orden correcto, sin que se pierda ninguna escritura y sin que ninguna salida haga algo peligroso en el camino.

01 · Punto de partida

Del chip vivo al sistema listo

Al final del tema anterior, main() empezó con las variables en su lugar y el reloj principal configurado. Pero el microcontrolador no sabe todavía qué hay conectado a sus pines ni qué querés que haga cada periférico. Por defecto, tras un reinicio, los registros valen lo que el fabricante estableció como estado inicial: los periféricos tienen su reloj cortado (tema 3), los pines son entradas flotantes y las interrupciones están deshabilitadas.

La inicialización del hardware es la fase en la que el firmware declara el estado deseado de cada bloque: qué pines son salidas, cuáles son entradas con resistencia, qué velocidad tiene la UART, a qué frecuencia cuenta el temporizador. Es una fase corta (milisegundos) pero crítica: si falla, todo lo que viene después se apoya en un hardware mal configurado y el fallo aparece lejos de su causa.

Seguimos con los registros del tema 3 y el ejemplo de la UART del curso anterior. En el tema 10 veremos cómo una capa de abstracción (HAL) encapsula estos mismos pasos, pero para usarla bien hay que saber qué hace por dentro.

02 · El orden

La receta: seis pasos en orden

Casi cualquier periférico se inicializa con la misma secuencia. El orden no es una costumbre: cada paso depende del anterior, y saltarse la dependencia produce los fallos silenciosos que ya viste en el tema 3.

Seis pasos de la inicialización de un periférico Seis cajas en secuencia: dar reloj, configurar los pines, configurar el periférico, configurar interrupciones o DMA, habilitar el periférico y usarlo. Una nota indica que las escrituras anteriores al reloj se pierden y que habilitar debe ser el último paso de configuración. 1. Relojdel bloque 2. Pinesmodo y función 3. Parámetrosvelocidad, modo 4. Eventosinterrupción o DMA 5. Habilitarencender el bloque 6. Usarleer, escribir sin reloj, todo losiguiente se pierde siempre al final dela configuración
Cada paso, por qué va en ese lugar y qué pasa si se invierte.
PasoPor qué en ese lugarSi se invierte
1. Reloj del bloqueSin reloj, el bloque ignora las escrituras a sus registrosLas escrituras previas se pierden sin error
2. PinesEl pin debe estar asignado a la función antes de que el periférico trabajeEl periférico funciona, pero su señal no sale al exterior
3. ParámetrosSe fija cómo trabajará (velocidad, formato, modo) mientras está apagadoEl bloque arranca con parámetros incompletos y emite o recibe basura
4. Interrupciones o DMASe prepara cómo se avisará de los eventos y quién atenderáUna interrupción puede dispararse antes de que el programa esté listo para atenderla
5. HabilitarRecién ahora el bloque empieza a operar, ya con todo configuradoHay una ventana con el bloque activo pero a medio configurar
6. UsarYa se puede transmitir, medir, contarLa operación se pierde o queda a medias

El principio de fondo es el mismo de cualquier inicialización en programación: un objeto no se usa hasta que está completamente construido. En hardware el costo de olvidarlo es mayor porque el objeto es físico: puede encender un motor, emitir una señal o consumir corriente mientras está a medio configurar.

03 · Paso 1

Relojes de los periféricos

Cada periférico tiene una compuerta de reloj (clock gate) controlada por un bit del bloque de reloj y reinicio (RCC en la familia de ejemplo). Los bits se agrupan por bus: los periféricos rápidos en un registro de habilitación (RCC_APB2ENR) y los lentos en otro (RCC_APB1ENR). Activar uno es poner su bit en 1:

#define RCC_APB2ENR (*(volatile uint32_t *)0x40021018)

RCC_APB2ENR |= (1u << 2);          // IOPAEN: reloj del puerto A
RCC_APB2ENR |= (1u << 14);         // USART1EN: reloj de USART1
(void)RCC_APB2ENR;                 // lectura de espera: asegura que el reloj ya está activo
// Otros relojes del mismo registro no se tocan: por eso se usa |= y no =

Tres detalles frecuentes:

  • Usar |=, no =. El registro de habilitación contiene los bits de muchos periféricos. Escribir con = apaga todos los demás sin avisar.
  • Esperar tras activar el reloj. Algunos chips tardan uno o dos ciclos en que el reloj llegue al periférico. La errata de varias familias recomienda leer el registro de vuelta (o ejecutar una instrucción de espera) antes de usar el bloque. La lectura (void)RCC_APB2ENR; hace eso y el compilador no la elimina porque el registro es volatile.
  • Energía. Un periférico con el reloj apagado casi no consume. Dar reloj solo a lo que se usa es la primera medida de ahorro de energía, y se puede apagar de nuevo cuando ya no hace falta (tema 35).

Lo normal es agrupar todas las habilitaciones de relojes al inicio de la inicialización, en una sola función. Así el orden queda a la vista y no hay dependencias ocultas entre módulos.

04 · Paso 2

Configurar los pines

Cada pin del chip es un recurso compartido entre varias funciones. El firmware debe decidir, para cada uno, qué es y cómo se comporta eléctricamente. Los modos más comunes son:

Modos habituales de un pin de propósito general.
ModoCómo se comportaUso típico
Entrada flotanteSin resistencia interna: toma el nivel que le llegue (es el estado tras el reset)Señales con nivel definido desde afuera
Entrada con pull-up o pull-downUna resistencia interna define el nivel cuando nada lo manejaPulsadores, interruptores, líneas abiertas
Entrada analógicaConectada al conversor; la entrada digital se desactivaSensores analógicos; también pines sin usar
Salida push-pullEl pin impone tanto el nivel alto como el bajoLED, transistores, señales digitales
Salida open-drainSolo baja el pin; el nivel alto lo pone una resistencia externaBuses compartidos como I²C, líneas «cableadas»
Función alternaEl pin pertenece a un periférico (TX de UART, SCK de SPI, PWM)Cualquier señal que genera o recibe un periférico

Además del modo, una salida tiene una velocidad de flanco (2, 10 o 50 MHz en la familia de ejemplo). Es el tiempo que tarda en cambiar de nivel; una más rápida permite señales de alta frecuencia pero genera más interferencia electromagnética. La regla es elegir la más lenta que cumpla: un LED o un relé con la de 2 MHz, un bus rápido con la de 50.

Y los pines que no se usan, ¿qué se hace con ellos? Dejarlos flotando es un error común: una entrada sin nivel definido capta ruido, puede oscilar y aumenta el consumo. Se los configura como entrada analógica (en muchos chips desactiva el circuito de entrada) o como entrada con resistencia, según lo que indique el fabricante.

Un ejemplo concreto: llevar el pin PA9 a su función de transmisión de la USART1 (pin TX), a 50 MHz. En esta familia, el modo de los pines 8 a 15 se configura en el registro CRH, con 4 bits por pin (2 de modo, 2 de configuración):

#define GPIOA_CRH (*(volatile uint32_t *)0x40010804)

// PA9 ocupa los bits 4 a 7 de CRH (pin 9 − 8 = 1; 1 × 4 = 4)
// 0xB = 0b1011: salida a 50 MHz (MODE=11) + función alterna push-pull (CNF=10)
GPIOA_CRH = (GPIOA_CRH & ~(0xFu << 4))   // 1) limpiar los 4 bits del pin 9...
                      | (0xBu << 4);        // 2) ...y escribir la nueva configuración

El patrón «limpiar y luego escribir» (leer, modificar, escribir) se repite en toda la configuración: se limpia solo el campo del pin elegido para no alterar los demás. Un campo de 4 bits mal calculado configura otro pin sin avisar, otro motivo para tener las constantes con nombre y no números mágicos (tema 10).

05 · Pasos 3 a 5

Configurar un periférico: la USART

Con reloj y pines listos, falta configurar el periférico mismo. Un ejemplo completo con la USART1 a 115200 baudios y 8N1, que une todo lo visto hasta acá:

#define RCC_APB2ENR (*(volatile uint32_t *)0x40021018)
#define GPIOA_CRH   (*(volatile uint32_t *)0x40010804)
#define USART1_SR   (*(volatile uint32_t *)0x40013800)
#define USART1_DR   (*(volatile uint32_t *)0x40013804)
#define USART1_BRR  (*(volatile uint32_t *)0x40013808)
#define USART1_CR1  (*(volatile uint32_t *)0x4001380C)

void uart1_init(uint32_t fclk, uint32_t baudios) {
    // 1) relojes: puerto A (bit 2) y USART1 (bit 14)
    RCC_APB2ENR |= (1u << 2) | (1u << 14);
    (void)RCC_APB2ENR;

    // 2) pin PA9 como función alterna (transmisión)
    GPIOA_CRH = (GPIOA_CRH & ~(0xFu << 4)) | (0xBu << 4);

    // 3) parámetros: velocidad. Con 16 muestras por bit, BRR = fclk / baudios
    USART1_BRR = fclk / baudios;            // 72 000 000 / 115 200 = 625

    // 4) (aquí irían la configuración de interrupciones o de DMA, si hicieran falta)

    // 5) habilitar: primero el transmisor, y la USART al final
    USART1_CR1 = (1u << 3);                 // TE: transmisor activado
    USART1_CR1 |= (1u << 13);               // UE: USART habilitada
}

void uart1_enviar(uint8_t dato) {           // 6) usar
    while (!(USART1_SR & (1u << 7))) { }   // esperar a que el registro de transmisión esté vacío (TXE)
    USART1_DR = dato;
}

El valor 625 sale de la cuenta del reloj: la USART divide el reloj de su bus entre 16 por cada bit, y el divisor BRR guarda el cociente 72.000.000 / 115.200 = 625 (la parte entera y la fraccionaria viven juntas en el registro). Con el reloj del tema 8 sin configurar (8 MHz), 625 daría 12.800 baudios, y por eso la inicialización del reloj es un requisito previo, no un detalle.

Observá la última escritura: UE (habilitar la USART) se pone después de fijar la velocidad y el modo de transmisión. Así, el bloque nace ya configurado. Si se habilitara antes, durante un instante el periférico estaría activo con el divisor en cero. En una UART esa ventana es casi inofensiva; en un temporizador que maneja un motor o en un conversor que dispara una medida, no lo es.

La función uart1_enviar muestra otro patrón típico: antes de escribir hay que preguntarle al hardware si está listo (el bit TXE del registro de estado). Esa espera ocupada es aceptable en una inicialización o una consola de depuración; en la operación normal se reemplaza por interrupciones o DMA (temas 15 y 16).

06 · Cuidado con lo que se enciende

Estado seguro y dependencias

Los pasos de arriba suponen que equivocarse solo produce un periférico que no funciona. Pero en un producto real hay pines conectados a cosas con consecuencias: un relé, un motor, una válvula, un cargador de batería. Durante la inicialización, esas salidas no deben hacer nada inesperado, ni siquiera un instante.

Tras un reinicio los pines son entradas flotantes: el circuito externo ve una línea sin nivel definido. Dos precauciones cubren los casos habituales:

  • Hardware: resistencias de pull-down o pull-up externas en las líneas de control de cargas, para que el estado de reposo sea el seguro (relé apagado) mientras el pin flota, antes y durante el arranque.
  • Software: primero el nivel, después el modo. Al convertir un pin en salida, el nivel de salida que tenía el registro pasa a manejarlo de inmediato; si ese nivel es el equivocado, se produce un pulso. Por eso se escribe el nivel de reposo antes de pasar el pin a salida.
#define GPIOB_CRH  (*(volatile uint32_t *)0x40010C04)
#define GPIOB_BSRR (*(volatile uint32_t *)0x40010C10)

// Relé conectado a PB12, apagado con nivel bajo
GPIOB_BSRR = (1u << (12 + 16));    // 1) dejar el nivel de salida en bajo (bit 28 de BSRR = reset del pin 12)
// 2) recién ahora, configurar PB12 como salida push-pull
GPIOB_CRH = (GPIOB_CRH & ~(0xFu << 16)) | (0x2u << 16);   // PB12: pin 12 − 8 = 4; 4 × 4 = 16

El otro concepto es el de armar al final: las partes que mueven energía (etapas de potencia, motores, calefactores) se habilitan en el último paso de la inicialización, cuando todo lo demás pasó sus comprobaciones. Si algo falla antes, el equipo queda en reposo seguro.

Dependencias entre módulos

La inicialización completa del producto es una secuencia de inicializaciones de módulos, y entre ellos hay dependencias: no se puede configurar una UART sin el reloj, ni arrancar un sensor I²C sin el bus, ni habilitar las interrupciones sin las rutinas y las estructuras de datos que usan. La práctica habitual es concentrarlas en una función con el orden a la vista y documentado:

typedef enum { INIT_OK = 0, INIT_ERR_RELOJ, INIT_ERR_SENSOR } init_t;

init_t hardware_init(void) {
    if (reloj_verificar() != 0)  return INIT_ERR_RELOJ;   // 1) el reloj es la base de todo
    gpio_init();                 // 2) pines: salidas en estado seguro, el resto definido
    uart_init(115200);           // 3) consola de depuración: sirve para informar el resto
    temporizador_init();         // 4) base de tiempo
    if (sensor_init() != 0)      return INIT_ERR_SENSOR;  // 5) dispositivos externos, con comprobación
    interrupciones_habilitar();  // 6) las interrupciones, al final: todo está listo para atenderlas
    actuadores_armar();          // 7) lo que mueve energía, lo último
    return INIT_OK;
}

Dos puntos de esta función merecen comentario. Primero, habilitar las interrupciones al final: una interrupción que se dispare antes de que sus estructuras estén listas ejecuta una rutina sobre datos a medias. Segundo, los dispositivos externos (un sensor, una pantalla, un módem) suelen necesitar un tiempo de estabilización tras la alimentación, de milisegundos a cientos de milisegundos; hay que respetarlo con un temporizador, no con un bucle de espera que dependa del compilador, y comprobar que respondan, con un límite de tiempo y una decisión clara de qué hacer si no lo hacen (tema 37).

Una última interacción a tener en cuenta es el watchdog (tema 38). Si se activa muy temprano y la inicialización es larga (esperar un cristal lento, una memoria externa), el perro guardián puede vencer a mitad de camino y reiniciar el equipo en bucle. O se alimenta durante la inicialización o se activa al terminarla.

07 · No suponer, comprobar

Verificar que quedó bien

Escribir un registro no garantiza que el hardware haya tomado la orden. Hay varias formas de comprobarlo, de la más inmediata a la más completa:

  • Lectura de vuelta. Después de escribir, leer el registro y comparar. Para registros de configuración, es una verificación barata; no sirve para los que cambian solos o son de solo escritura.
  • Vista de periféricos del depurador. Con el archivo SVD del chip, el entorno de depuración muestra todos los registros y sus campos con nombre. Es la forma más rápida de ver el estado real de un periférico y de detectar un reloj que no está activo.
  • Medir la señal. Un osciloscopio o un analizador lógico en el pin muestra si sale lo que debe: la velocidad real de una UART, la frecuencia de un PWM. Es la prueba definitiva, porque mide el resultado y no la intención.
  • Autoprueba de arranque. El firmware verifica por sí mismo lo que puede: que el cristal esté estable, que la memoria externa responda, que el sensor identifique su modelo, que la tensión esté en rango. Los resultados se informan o se guardan para el diagnóstico.
  • Prueba de cada driver. Un programa mínimo por periférico (encender un LED, enviar un byte conocido, leer un valor de referencia) antes de integrarlo con los demás.

El criterio es el mismo de todo el curso: probar en el hardware lo que solo el hardware puede responder (tema 5) y automatizar lo que se repite. Una inicialización que sale por la consola un resumen de sus pasos («reloj 72 MHz, UART1 115200, sensor OK») hace el diagnóstico en campo casi trivial.

08 · Analogía con programación

El orden importa, en Python

Este programa modela una USART diminuta con las reglas que ya conocés: un bloque sin reloj ignora las escrituras, un pin sin configurar no deja salir la señal y una USART no transmite hasta que está habilitada. Se ejecutan cuatro órdenes distintos de los mismos seis pasos y se informa cuántas escrituras se perdieron, cuántos avisos hubo y qué pasó con el byte.

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

BRR = 625      # 72 000 000 / 115 200


def simular(orden):
    e = {"clk_gpio": False, "clk_usart": False, "pin": False, "brr": 0, "ue": False}
    perdidas = avisos = 0
    resultado = "no se envió nada"
    for paso in orden:
        if paso == "clk_gpio":
            e["clk_gpio"] = True
        elif paso == "clk_usart":
            e["clk_usart"] = True
        elif paso == "pin":
            if e["clk_gpio"]:
                e["pin"] = True
            else:
                perdidas += 1                       # el puerto sin reloj ignora la escritura
        elif paso == "brr":
            if e["clk_usart"]:
                e["brr"] = BRR
            else:
                perdidas += 1
        elif paso == "enable":
            if e["clk_usart"]:
                e["ue"] = True
                if e["brr"] == 0:
                    avisos += 1                     # habilitada antes de configurarla
            else:
                perdidas += 1
        elif paso == "send":
            if not (e["clk_usart"] and e["ue"]):
                perdidas += 1
                resultado = "la USART no estaba habilitada"
            elif e["brr"] == 0:
                perdidas += 1
                resultado = "salió basura (sin velocidad)"
            elif not e["pin"]:
                perdidas += 1
                resultado = "el byte no llegó al pin PA9"
            else:
                resultado = "byte enviado a 115200"
    return perdidas, avisos, resultado


ESCENARIOS = {
    "Orden correcto": ["clk_gpio", "clk_usart", "pin", "brr", "enable", "send"],
    "Relojes al final": ["pin", "brr", "enable", "send", "clk_gpio", "clk_usart"],
    "Habilitar antes de la velocidad": ["clk_gpio", "clk_usart", "pin", "enable", "brr", "send"],
    "Olvidar configurar el pin": ["clk_gpio", "clk_usart", "brr", "enable", "send"],
}
for titulo, orden in ESCENARIOS.items():
    perdidas, avisos, resultado = simular(orden)
    print(f"{titulo:32s} perdidas={perdidas} avisos={avisos}  -> {resultado}")

Resultado: el orden correcto no pierde nada y envía el byte a 115200. Con los relojes al final se pierden cuatro escrituras (pin, velocidad, habilitación y envío) sin ningún error, y no sale nada. Con la habilitación antes de la velocidad el byte sale, pero con un aviso: hubo una ventana con la USART activa y sin configurar. Al olvidar el pin todo el resto funciona y el byte no llega al exterior: una falla muy desconcertante, porque cada registro del periférico está bien. Probá agregar un segundo "send" o quitar "clk_usart" y predecí el resultado antes de ejecutar.

Fijate en lo que no hace el modelo: en ningún momento avisa de las escrituras perdidas. Es la parte más realista. El hardware no protesta: ignora. Por eso conviene tener el hábito de comprobar el resultado en lugar de confiar en que no hubo errores.

09 · Exploración interactiva

Ordenar la inicialización

Armá tu propia secuencia: en cada posición elegí uno de los seis pasos de inicializar la USART1 y enviar un byte. El laboratorio ejecuta el orden elegido sobre el mismo modelo del apartado anterior, marca qué escrituras se perdieron y si el byte llegó al pin. Los botones cargan órdenes típicas para empezar.

Seis posiciones, seis pasos

Escrituras perdidas—
Avisos—
Resultado—

Qué pasó con cada paso

—

Elegí los pasos en el orden en que los ejecutarías.

Recorrido guiado

  1. Presioná «Orden correcto»: seis pasos, ninguna escritura perdida y el byte sale a 115200. Es la receta del apartado 2 aplicada a la USART.
  2. Probá «Relojes al final»: las cuatro primeras acciones se pierden sin ningún error. Los dos relojes se activan recién al final, cuando ya no hay nada por hacer.
  3. Probá «Habilitar antes de la velocidad»: funciona, pero con un aviso. Es el tipo de error que no se ve en el escritorio y aparece en campo, cuando el periférico gobierna algo con consecuencias.
  4. Probá «Olvidar el pin»: todo parece bien, pero el byte no sale. Si solo mirás los registros de la USART, no encontrarás el problema: está en otro bloque.
  5. Armá a mano un orden en el que «Dar reloj a USART1» venga después de «Fijar la velocidad» pero antes de «Habilitar». ¿Por qué se pierde la velocidad? Porque ese reloj aún no estaba activo cuando la escribiste, y la USART queda habilitada con la velocidad sin configurar.
  6. Elegí «(nada)» en alguna posición y repetí el orden correcto sin Dar reloj al puerto A: la configuración del pin se pierde y el byte no llega a PA9.
Desafío: ¿es válido cualquier orden de los relojes?

Sí, entre sí los dos relojes (puerto A y USART1) son independientes: Dar reloj al puerto A y Dar reloj a USART1 pueden intercambiarse sin consecuencias. Lo que importa es que cada reloj esté antes de la primera escritura en su bloque. Probalo: poné primero el reloj de USART1 y después el del puerto A, y el resultado sigue siendo correcto. Esa distinción entre dependencias reales y orden arbitrario es la que permite reordenar la inicialización para agruparla o acortarla sin romperla.

10 · Método de trabajo

Método para inicializar un periférico nuevo

  1. Leé el capítulo del periférico en el manual de referencia: diagrama de bloques, pines que usa y secuencia de configuración recomendada. Muchos manuales incluyen el orden exacto.
  2. Identificá el reloj del bus al que pertenece y la frecuencia que recibe: de ella dependen los divisores.
  3. Asigná los pines con la tabla de funciones alternas del chip y comprobá que no haya conflictos con otras funciones de la placa.
  4. Calculá los parámetros (divisores, tiempos) con la frecuencia real y verificá el error resultante.
  5. Escribí los pasos en orden: relojes, pines, parámetros, eventos, habilitación. Usá constantes con nombre para cada bit.
  6. Comprobá en el depurador que los registros contienen lo esperado y en el pin que la señal sale como debe.
  7. Definí el estado seguro de las salidas durante la inicialización y qué pasa si el periférico no responde.
  8. Documentá las dependencias («requiere reloj principal a 72 MHz») para quien reorganice el arranque.

Cuando algo no funciona, recorré la lista de afuera hacia adentro: reloj del bloque, pin (modo y función), parámetros, habilitación. Es la misma secuencia de la inicialización, al revés, y encuentra la gran mayoría de los problemas en minutos.

11 · Comprensión

Confusiones frecuentes

«Si compila, el periférico está configurado»

El compilador no sabe nada del estado del hardware. Una escritura a un registro es válida aunque el bloque no tenga reloj; simplemente no tiene efecto. Nada en el lenguaje avisa.

«Puedo habilitar el periférico primero y configurarlo después»

Funciona a veces, pero deja una ventana con el bloque activo y sin configurar. En un temporizador que maneja una salida de potencia o un conversor que arranca una medida, esa ventana produce comportamientos espurios. Habilitar siempre al final.

«Los pines sin usar no importan»

Una entrada flotante puede captar ruido, oscilar y consumir corriente de más. Se configuran explícitamente en el modo que recomienda el fabricante.

«Escribir un registro con = es lo mismo que con |=»

No: = reemplaza todo el registro y borra los bits de los demás periféricos o pines; |= solo agrega el bit. Para modificar un campo se limpia con &= ~máscara y se escribe con |=.

«La inicialización se hace una sola vez en la vida del programa»

Se repite después de cada reinicio, y en algunos casos también al despertar de un sueño profundo, porque ciertos periféricos pierden su configuración al apagarse (tema 34). Conviene que cada módulo tenga una función de inicialización que se pueda volver a llamar.

«Con una biblioteca HAL no necesito entender esto»

La HAL escribe estos mismos registros en este mismo orden. Si se invoca fuera de orden o con parámetros incorrectos, los errores son los mismos, pero más difíciles de rastrear porque el código real está escondido. Entender lo de abajo es lo que permite usarla bien (tema 10).

12 · Práctica guiada

Predecí antes de comprobar

1. Ordenar los pasos

Ordená correctamente la inicialización de un periférico SPI: (a) habilitar el SPI; (b) configurar los pines SCK y MOSI como función alterna; (c) fijar el divisor de velocidad y el modo; (d) dar reloj al SPI y al puerto de los pines; (e) habilitar la interrupción de recepción; (f) leer un dato.

Ver solución

El orden es (d), (b), (c), (e), (a), (f): primero los relojes, luego los pines, después los parámetros, luego los eventos, la habilitación casi al final y por último el uso. Las posiciones de (b) y (c) son intercambiables, pero ambas van después de (d) y antes de (a).

2. ¿En qué registro y bits está el pin?

En la familia de ejemplo, el modo de los pines 0 a 7 está en CRL y el de los pines 8 a 15 en CRH, con 4 bits por pin. ¿Dónde está la configuración de PB5? ¿Y la de PA12?

Ver solución

PB5: está en GPIOB_CRL, bits 20 a 23 (5 × 4 = 20). PA12: está en GPIOA_CRH, bits 16 a 19 ((12 − 8) × 4 = 16). La fórmula general: registro bajo o alto según el número de pin y desplazamiento de 4 × (pin mod 8).

3. Calcular el divisor

Con el reloj de la USART a 72 MHz y 16 muestras por bit, ¿cuánto vale BRR para 9600 baudios? Y si la USART recibe un reloj de 36 MHz y se quieren 115200 baudios, ¿cuál es el valor ideal y qué error de velocidad resulta al redondear?

Ver solución

Para 9600: 72.000.000 / 9600 = 7500 (0x1D4C), exacto. Para 115200 a 36 MHz: 36.000.000 / 115.200 = 312,5, que no es entero. Redondeando a 313 la velocidad real es 36.000.000 / 313 ≈ 115.016 baudios, un error de −0,16 % (con 312, +0,16 %). Un error por debajo de aproximadamente el 2 % es tolerable en una UART, así que el redondeo no es un problema.

4. Un pulso en el arranque

Un relé conectado a un pin se activa con nivel alto. Tras una actualización de firmware por bootloader, el relé «da un golpe» en cada arranque. El código inicializa primero el pin como salida y luego escribe 0. ¿Qué sucede y cómo se corrige?

Ver solución

Al pasar el pin a salida, toma el nivel que tenía el registro de salida. Si el bootloader o el firmware anterior lo había dejado en 1, el pin sale en alto durante el instante que tarda el programa en escribir 0. La solución es escribir primero el nivel de reposo y recién entonces configurar el pin como salida, y reforzarlo con una resistencia de pull-down externa para que el estado seguro valga también mientras el pin flota.

5. Encontrar el error de orden

Esta inicialización falla de forma intermitente. ¿Qué cambiarías? interrupciones_habilitar(); gpio_init(); uart_init(115200); cola_mensajes_init(); temporizador_init();

Ver solución

Las interrupciones se habilitan primero, cuando todavía no están listos los pines, la UART ni la cola que usarían sus rutinas. Una interrupción que llegue en esa ventana ejecuta código sobre datos sin inicializar. Hay que habilitarlas al final: gpio_init(); uart_init(115200); cola_mensajes_init(); temporizador_init(); interrupciones_habilitar();. La intermitencia se explica porque depende de que justo llegue una interrupción en esa ventana corta.

6. Diagnosticar por el síntoma

Una UART configurada para 115200 baudios transmite caracteres que el monitor serie del PC muestra como basura. Con el osciloscopio medís un bit de 78 µs de duración. ¿Qué velocidad real tiene y qué sospechás?

Ver solución

1 / 78 µs ≈ 12.800 baudios (115200 × 8 / 72). La velocidad real es la que resulta de usar un reloj de 8 MHz en lugar de los 72 MHz que supuso el cálculo: el reloj principal no se configuró (se omitió o falló SystemInit, tema 8). El osciloscopio permitió llegar a la causa sin tocar el código de la UART.

13 · Síntesis

Ideas para recordar

  • Tras el arranque, los periféricos no tienen reloj, los pines son entradas flotantes y las interrupciones están deshabilitadas: la inicialización declara el estado deseado de cada bloque.
  • El orden es: reloj del bloque, pines, parámetros, interrupciones o DMA, habilitación y uso. Las escrituras a un bloque sin reloj se pierden en silencio.
  • Se habilita el periférico al final de su configuración para que nazca completo; hacerlo antes deja una ventana con el bloque activo y a medio configurar.
  • Los relojes se activan con |=, agrupados al inicio, y solo para lo que se usa; conviene una lectura de espera antes de usar el bloque.
  • Cada pin se configura según su función (entrada, salida, analógico, función alterna) y con la velocidad de flanco más lenta que sirva; los pines sin usar no se dejan flotando.
  • Las salidas que mueven cargas se dejan en un estado seguro: primero el nivel de reposo, después el modo de salida; con resistencias externas que fijen el reposo durante el arranque, y los actuadores se arman al final.
  • Las interrupciones se habilitan al final de la inicialización; el watchdog debe tenerse en cuenta durante las inicializaciones largas, y los dispositivos externos necesitan tiempo de estabilización y una comprobación de que responden.
  • Se verifica con lectura de vuelta, la vista de periféricos del depurador, la medición de la señal en el pin y una autoprueba de arranque que informe lo que hizo.

¿Listo para el tema 10?

Antes de avanzar, escribí de memoria los seis pasos de inicialización de un periférico y por qué cada uno va donde va; explicá por qué un pin que arranca como entrada flotante exige cuidado con un relé; y calculá el divisor de una UART para una velocidad y un reloj dados. Si podés justificar las tres respuestas, sabés inicializar hardware sin perder escrituras.

En el próximo tema subimos un nivel: las capas de abstracción, HAL y drivers. Veremos cómo encapsular estas secuencias de registros en funciones con nombre para reutilizarlas y cambiar de chip sin reescribir la aplicación.