Minimalna proga bare-metal dla STM32F103: od skryptu linkera po miganie LED
Po resecie Cortex-M3 wczytuje wskaźnik stosu z 0x08000000, przechodzi do Reset_Handler z tablicy wektorów przerwań i wykonuje kod startowy. W projekcie bare-metal bez HAL ten proces jest przejrzysty: ręczna konfiguracja pamięci, rejestrów GPIO i TIM2. Projekt pokazuje pracę ze STM32F103C8T6 — miganie LED na PC13 z opóźnieniem 1 sekundy za pomocą sprzętowego timera.
Struktura projektu podzielona na warstwy: inc/ dla interfejsów, src/ dla implementacji, startup/ dla inicjalizacji, myLinker.ld dla mapy pamięci.
Skrypt linkera: rozmieszczenie sekcji w FLASH i RAM
Skrypt linkera definiuje obszary pamięci STM32F103C8T6 i rozdziela sekcje:
- FLASH (0x08000000, 64 KB): .isr_vector, .text, .rodata
- RAM (0x20000000, 20 KB): .data, .bss, stos
Kluczowe symbole dla startu:
_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 jest kopiowana z FLASH do RAM po adresach _sidata → _sdata.._edata. .bss jest zerowana od _sbss do _ebss. _estack ustawia początkowy SP.
Kod startowy: Reset_Handler i tablica wektorów
Startup wykonuje się przed main(): kopiuje .data, czyści .bss, wywołuje main(). Tablica wektorów w .isr_vector:
#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 kieruje niezaimplementowane ISR do Default_Handler.
Praca z rejestrami: volatile i bezpośredni dostęp
Bare-metal — zapis do rejestrów po adresach:
*(volatile uint32_t*)0x40021018 |= (1 << 4); // RCC_APB2ENR, GPIOC
volatile zapobiega optymalizacjom: kompilator generuje każde odwołanie. Dla TIM2 używana jest struktura:
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)
Dostęp: TIM2->PSC = 7999;.
Inicjalizacja peryferiów: GPIO i TIM2
GPIO
Włączamy zegar GPIOC: RCC->APB2ENR |= (1 << 4);. Konfigurujemy PC13 jako wyjście:
GPIOC->CRH &= ~(0xF << 20);GPIOC->CRH |= (0x2 << 20);(2.0 MODE, 00 CNF)
LED: GPIOC->ODR |= (1 << 13); włącz, &= ~(1 << 13); wyłącz.
TIM2
Konfiguracja na 1 Hz (72 MHz / 8000 / 9000 = 1 Hz):
RCC->APB1ENR |= (1 << 0);— zegar TIM2TIM2->PSC = 7999;(przeddzielnik)TIM2->ARR = 8999;(autoprzeladowanie)TIM2->CR1 |= (1 << 0);— start
delayOneSecond(): czekamy TIM2->CNT >= 9000, reset CNT.
Sekwencja main()
- GPIO_init()
- TIMERS_init()
- Pętla nieskończona: turnOnLED() → delayOneSecond() → turnOffLED() → delayOneSecond()
Co ważne
- Symbole linkera (_sdata, _edata itp.) niezbędne do poprawnej inicjalizacji pamięci.
- volatile kluczowe dla rejestrów: bez niego kompilator buforuje wartości.
- Tablica wektorów w .isr_vector musi być pierwsza w FLASH.
- Struktura rejestrów ułatwia dostęp zamiast surowych wskaźników.
- Bare-metal pokazuje rzeczywistą sekwencję: startup → main → peryferia.
— Editorial Team
Brak komentarzy.