Retour à l'accueil

Firmware bare-metal STM32F103 sans CubeMX

Guide pour créer un firmware bare-metal pour STM32F103C8T6 sans CubeMX et HAL. Décrit le script linker, le code de démarrage avec Reset_Handler, GPIO PC13 et l'initialisation de TIM2 pour un délai d'une seconde. Le clignotement LED démontre le cycle complet du reset à main().

STM32 sans CubeMX : de Reset_Handler à LED
Advertisement 728x90

Firmware minimal STM32F103 sans HAL : du script linker à la LED clignotante

Après un reset, le Cortex-M3 charge le pointeur de pile depuis 0x08000000, saute vers Reset_Handler depuis la table des vecteurs, et exécute le code de démarrage. Dans un projet bare-metal sans HAL, ce processus est totalement transparent : configuration manuelle de la mémoire, des registres GPIO et TIM2. Ce projet illustre le travail avec le STM32F103C8T6 — faire clignoter une LED sur PC13 avec un délai d'une seconde via un timer matériel.

La structure du projet est stratifiée : inc/ pour les interfaces, src/ pour les implémentations, startup/ pour l'initialisation, myLinker.ld pour la cartographie mémoire.

Script linker : placement des sections en FLASH et RAM

Le script linker définit les régions mémoire du STM32F103C8T6 et alloue les sections :

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

Symboles clés pour le démarrage :

_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 est copié depuis FLASH vers RAM via les adresses _sidata → _sdata.._edata. .bss est mis à zéro de _sbss à _ebss. _estack définit le pointeur de pile initial.

Code de démarrage : Reset_Handler et table des vecteurs

Le démarrage s'exécute avant main() : copie .data, efface .bss, appelle main(). Table des vecteurs dans .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 redirige les ISR non implémentées vers Default_Handler.

Accès aux registres : volatile et adressage direct

Bare-metal signifie écrire directement aux registres par adresse :

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

volatile empêche les optimisations : le compilateur génère chaque accès. Pour TIM2, utilisez une structure :

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)

Accès : TIM2->PSC = 7999;

Initialisation des périphériques : GPIO et TIM2

GPIO

Activez l'horloge GPIOC : RCC->APB2ENR |= (1 << 4);. Configurez PC13 en sortie :

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

LED : GPIOC->ODR |= (1 << 13); pour allumer, &= ~(1 << 13); pour éteindre.

TIM2

Configuration pour 1 Hz (72 MHz / 8000 / 9000 = 1 Hz) :

  • RCC->APB1ENR |= (1 << 0); — activer horloge TIM2
  • TIM2->PSC = 7999; (prédiviseur)
  • TIM2->ARR = 8999; (recharge auto)
  • TIM2->CR1 |= (1 << 0); — démarrer timer

delayOneSecond() : attendre TIM2->CNT >= 9000, réinitialiser CNT.

Séquence dans Main()

  • GPIO_init()
  • TIMERS_init()
  • Boucle infinie : turnOnLED()delayOneSecond()turnOffLED()delayOneSecond()

Points clés

  • Symboles linker (_sdata, _edata, etc.) essentiels pour l'initialisation mémoire.
  • volatile crucial pour les registres : sans cela, le compilateur met en cache les valeurs.
  • Table des vecteurs en .isr_vector doit être en premier en FLASH.
  • Structures de registres simplifient l'accès par rapport aux pointeurs bruts.
  • Bare-metal révèle le flux réel : démarrage → main → périphériques.

— Editorial Team

Advertisement 728x90

Lire ensuite