홈으로 돌아가기

CubeMX 없이 베어메탈 펌웨어 STM32F103

CubeMX와 HAL 없이 STM32F103C8T6용 베어메탈 펌웨어 생성 가이드. Reset_Handler가 포함된 startup 코드, GPIO PC13 및 TIM2 초기화(1초 지연용), 링커 스크립트를 설명. LED 깜빡임이 reset부터 main()까지 전체 사이클을 보여줌.

CubeMX 없이 STM32: Reset_Handler부터 LED까지
Advertisement 728x90

최소 베어메탈 STM32F103 펌웨어: 링커 스크립트부터 LED 깜빡임까지

리셋 후 Cortex-M3은 0x08000000에서 스택 포인터를 로드하고, 벡터 테이블에서 Reset_Handler로 점프해 시작 코드가 실행됩니다. HAL 없이 베어메탈 프로젝트에서는 이 과정이 완전히 투명합니다: 메모리 수동 설정, GPIO와 TIM2 레지스터 구성. 이 프로젝트는 STM32F103C8T6에서 LED를 PC13에 연결해 하드웨어 타이머로 1초 지연 깜빡임을 구현합니다.

프로젝트 구조는 계층화되어 있습니다: inc/는 인터페이스, src/는 구현체, startup/는 초기화, myLinker.ld는 메모리 매핑용입니다.

링커 스크립트: FLASH와 RAM에 섹션 배치하기

링커 스크립트는 STM32F103C8T6 메모리 영역을 정의하고 섹션을 할당합니다:

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

시작 코드에 필요한 주요 심볼:

_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는 _sidata → _sdata.._edata 주소로 FLASH에서 RAM으로 복사됩니다. .bss는 _sbss에서 _ebss까지 0으로 초기화됩니다. _estack은 초기 스택 포인터를 설정합니다.

시작 코드: Reset_Handler와 벡터 테이블

시작 코드는 main() 전에 실행: .data 복사, .bss 초기화, main() 호출. .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는 구현되지 않은 ISR을 Default_Handler로 라우팅합니다.

레지스터 접근: volatile과 직접 주소 지정

베어메탈은 주소를 직접 써 레지스터에 씁니다:

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

volatile은 최적화를 막아 컴파일러가 모든 접근을 생성합니다. TIM2는 구조체로:

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)

접근: TIM2->PSC = 7999;.

주변 장치 초기화: GPIO와 TIM2

GPIO

GPIOC 클럭 활성화: RCC->APB2ENR |= (1 << 4);. PC13을 출력으로:

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

LED: 켜기 GPIOC->ODR |= (1 << 13);, 끄기 &= ~(1 << 13);.

TIM2

1Hz 설정 (72 MHz / 8000 / 9000 = 1 Hz):

  • RCC->APB1ENR |= (1 << 0); — TIM2 클럭 활성화
  • TIM2->PSC = 7999; (프리스케일러)
  • TIM2->ARR = 8999; (오토 리로드)
  • TIM2->CR1 |= (1 << 0); — 타이머 시작

delayOneSecond(): TIM2->CNT >= 9000 대기 후 CNT 리셋.

main() 순서

  • GPIO_init()
  • TIMERS_init()
  • 무한 루프: turnOnLED() → delayOneSecond() → turnOffLED() → delayOneSecond()

주요 포인트

  • 링커 심볼 (_sdata, _edata 등)은 메모리 초기화에 필수입니다.
  • volatile은 레지스터에 핵심: 없으면 컴파일러가 값 캐싱합니다.
  • 벡터 테이블은 .isr_vector에 FLASH 첫 번째에 와야 합니다.
  • 레지스터 구조체는 raw 포인터보다 접근을 간소화합니다.
  • 베어메탈은 실제 흐름을 드러냅니다: 시작 → main → 주변 장치.

— Editorial Team

Advertisement 728x90

다음 읽기