Volver al inicio

Firmware bare-metal STM32F103 sin CubeMX

Guía para crear firmware bare-metal para STM32F103C8T6 sin CubeMX y HAL. Describe script de enlazador, código de arranque con Reset_Handler, GPIO PC13 e inicialización de TIM2 para retardo de 1 segundo. LED parpadeante demuestra ciclo completo desde reset hasta main().

STM32 sin CubeMX: desde Reset_Handler hasta LED
Advertisement 728x90

# Firmware Mínimo Bare-Metal para STM32F103: Del Script de Enlace al LED Parpadeante

Tras el reset, el Cortex-M3 carga el puntero de pila desde 0x08000000, salta al Reset_Handler de la tabla de vectores e inicia el código de arranque. En un proyecto bare-metal sin HAL, este proceso es completamente transparente: configuración manual de memoria, registros GPIO y TIM2. Este proyecto muestra cómo trabajar con el STM32F103C8T6 —haciendo parpadear un LED en PC13 con un retardo de 1 segundo usando un temporizador de hardware.

La estructura del proyecto está organizada en capas: inc/ para interfaces, src/ para implementaciones, startup/ para inicialización, myLinker.ld para mapeo de memoria.

Script de Enlace: Colocando Secciones en FLASH y RAM

El script de enlace define las regiones de memoria del STM32F103C8T6 y asigna las secciones:

Google AdInline article slot
  • FLASH (0x08000000, 64 KB): .isr_vector, .text, .rodata
  • RAM (0x20000000, 20 KB): .data, .bss, pila

Símbolos clave para el arranque:

_estack = ORIGIN(RAM) + LENGTH(RAM);

MEMORY {
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x00010000
    RAM   (rwx): ORIGIN = 0x20000000, LENGTH = 0x00005000
}

SECTIONS {
    .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
    .text : { *(.text*) } > FLASH
    .rodata : { *(.rodata*) } > FLASH
    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT > FLASH
    _sidata = LOADADDR(.data);
    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM
}

La sección .data se copia de FLASH a RAM usando las direcciones _sidata → _sdata.._edata. La sección .bss se inicializa a cero desde _sbss hasta _ebss. _estack establece el puntero inicial de pila.

Código de Arranque: Reset_Handler y Tabla de Vectores

El código de arranque se ejecuta antes de main(): copia .data, limpia .bss y llama a main(). La tabla de vectores está en .isr_vector:

Google AdInline article slot
#include <stdint.h>

extern uint32_t _estack;
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;

int main(void);

void Reset_Handler(void);
void Default_Handler(void) {
    while (1);
}

void NMI_Handler(void)       __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((used, section(".isr_vector")))
const void* vector_table[] = {
    &_estack,
    Reset_Handler,
    NMI_Handler,
    HardFault_Handler
};

void Reset_Handler(void) {
    uint32_t* src = &_sidata;
    uint32_t* dst = &_sdata;
    while (dst < &_edata) {
        *dst++ = *src++;
    }
    dst = &_sbss;
    while (dst < &_ebss) {
        *dst++ = 0;
    }
    main();
    while (1) {}
}

El weak alias redirige las ISR no implementadas a Default_Handler.

Acceso a Registros: volatile y Direccionamiento Directo

En bare-metal, se escriben los registros por dirección:

*(volatile uint32_t*)0x40021018 |= (1 << 4);  // RCC_APB2ENR, GPIOC

volatile evita optimizaciones: el compilador genera cada acceso. Para TIM2, usa una estructura:

Google AdInline article slot
typedef struct {
    volatile uint32_t CR1, CR2, SMCR, DIER, SR, EGR;
    volatile uint32_t CCMR1, CCMR2, CCER, CNT;
    volatile uint32_t PSC, ARR, RCR;
    volatile uint32_t CCR1, CCR2, CCR3, CCR4;
    volatile uint32_t BDTR, DCR, DMAR;
} TIM_TypeDef;

#define TIM2 ((TIM_TypeDef*)0x40000000)

Acceso: TIM2->PSC = 7999;.

Inicialización de Periféricos: GPIO y TIM2

GPIO

Habilita reloj de GPIOC: RCC->APB2ENR |= (1 << 4);. Configura PC13 como salida:

  • GPIOC->CRH &= ~(0xF << 20);
  • GPIOC->CRH |= (0x2 << 20); (2.0 MODE, 00 CNF)

LED: GPIOC->ODR |= (1 << 13); para encender, &= ~(1 << 13); para apagar.

TIM2

Configura para 1 Hz (72 MHz / 8000 / 9000 = 1 Hz):

  • RCC->APB1ENR |= (1 << 0); — habilita reloj TIM2
  • TIM2->PSC = 7999; (preescalador)
  • TIM2->ARR = 8999; (recarga automática)
  • TIM2->CR1 |= (1 << 0); — inicia temporizador

delayOneSecond(): espera a que TIM2->CNT >= 9000, reinicia CNT.

Secuencia en Main()

  • GPIO_init()
  • TIMERS_init()
  • Bucle infinito: turnOnLED() → delayOneSecond() → turnOffLED() → delayOneSecond()

Lecciones Clave

  • Los símbolos del linker (_sdata, _edata, etc.) son esenciales para la inicialización correcta de memoria.
  • volatile es crucial para registros: sin él, el compilador almacena en caché los valores.
  • La tabla de vectores en .isr_vector debe ir primero en FLASH.
  • Las estructuras de registros simplifican el acceso frente a punteros crudos.
  • Bare-metal revela el flujo real: arranque → main → periféricos.

— Editorial Team

Advertisement 728x90

Leer después