Zurück zur Startseite

Bare-Metal-Firmware STM32F103 ohne CubeMX

Anleitung zum Erstellen von Bare-Metal-Firmware für STM32F103C8T6 ohne CubeMX und HAL. Beschreibt Linker-Skript, Startup-Code mit Reset_Handler, GPIO PC13 und TIM2-Initialisierung für 1-Sekunden-Verzögerung. Blinkende LED demonstriert vollständigen Zyklus von Reset bis main().

STM32 ohne CubeMX: von Reset_Handler bis LED
Advertisement 728x90

Minimales Bare-Metal-Firmware für STM32F103: Vom Linker-Skript zur blinkenden LED

Nach dem Reset lädt der Cortex-M3 den Stackzeiger von 0x08000000, springt zum Reset_Handler aus der Vektortabelle und führt den Startcode aus. In einem Bare-Metal-Projekt ohne HAL ist dieser Prozess vollständig transparent: manuelle Speichereinrichtung, Konfiguration der GPIO- und TIM2-Register. Dieses Projekt zeigt die Arbeit mit dem STM32F103C8T6 – Blinken einer LED an PC13 mit 1-Sekunden-Verzögerung über einen Hardware-Timer.

Die Projektstruktur ist geschichtet: inc/ für Schnittstellen, src/ für Implementierungen, startup/ für Initialisierung, myLinker.ld für Speicherkartierung.

Linker-Skript: Platzierung der Sektionen in FLASH und RAM

Das Linker-Skript definiert die Speicherbereiche des STM32F103C8T6 und verteilt die Sektionen:

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

Wichtige Symbole für den Start:

_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
}

.data wird aus FLASH in RAM kopiert mit den Adressen _sidata_sdata.._edata. .bss wird von _sbss bis _ebss mit Nullen gefüllt. _estack legt den initialen Stackzeiger fest.

Startcode: Reset_Handler und Vektortabelle

Der Startcode läuft vor main(): Kopiert .data, löscht .bss, ruft main() auf. Vektortabelle in .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) {}
}

weak alias leitet nicht implementierte ISRs an Default_Handler weiter.

Registerzugriff: volatile und direkte Adressierung

Bare-Metal bedeutet Schreiben in Register per Adresse:

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

volatile verhindert Optimierungen: Der Compiler erzeugt jeden Zugriff. Für TIM2 eine Struktur verwenden:

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)

Zugriff: TIM2->PSC = 7999;.

Peripherie-Initialisierung: GPIO und TIM2

GPIO

GPIOC-Takt aktivieren: RCC->APB2ENR |= (1 << 4);. PC13 als Ausgang konfigurieren:

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

LED: GPIOC->ODR |= (1 << 13); einschalten, &= ~(1 << 13); ausschalten.

TIM2

Für 1 Hz einrichten (72 MHz / 8000 / 9000 = 1 Hz):

  • RCC->APB1ENR |= (1 << 0); — TIM2-Takt aktivieren
  • TIM2->PSC = 7999; (Vorteiler)
  • TIM2->ARR = 8999; (Auto-Reload)
  • TIM2->CR1 |= (1 << 0); — Timer starten

delayOneSecond(): Warten bis TIM2->CNT >= 9000, CNT zurücksetzen.

Ablauf in main()

  • GPIO_init()
  • TIMERS_init()
  • Endlosschleife: turnOnLED()delayOneSecond()turnOffLED()delayOneSecond()

Wichtige Erkenntnisse

  • Linker-Symbole (_sdata, _edata usw.) sind essenziell für korrekte Speicherinitialisierung.
  • volatile ist entscheidend für Register: Ohne es cached der Compiler Werte.
  • Vektortabelle in .isr_vector muss zuerst in FLASH stehen.
  • Register-Strukturen vereinfachen den Zugriff gegenüber Raw-Pointern.
  • Bare-Metal enthüllt den echten Ablauf: Startup → main → Peripherie.

— Editorial Team

Advertisement 728x90

Weiterlesen