전원이 들어온 뒤 main()에 도달하기까지 일어나는 일들.
전체 흐름
① 하드웨어가 하는 일
리셋 해제
→ CPU가 주소 0x00000000 에서 4바이트를 읽어 MSP 에 넣는다
→ 0x00000004 에서 4바이트를 읽어 PC 에 넣는다 (Reset_Handler)
→ 실행 시작
소프트웨어가 개입하기 전에 스택 포인터가 이미 설정된다. 이것이 Cortex-M의 특징으로, 다른 아키텍처는 어셈블리로 직접 설정해야 한다.
② Reset_Handler — startup 코드
void Reset_Handler(void) {
SystemInit(); // 클럭·FPU·VTOR 설정
// .data 복사: 플래시(LMA) → RAM(VMA)
uint32_t *src = &_sidata, *dst = &_sdata;
while (dst < &_edata) *dst++ = *src++;
// .bss 0으로
for (dst = &_sbss; dst < &_ebss; dst++) *dst = 0;
__libc_init_array(); // C++ 전역 생성자, atexit 등
main();
while (1) { } // main이 반환하면 여기 갇힌다
}
왜 .data를 복사하나 — 초기값이 있는 전역 변수는 그 값이 플래시에 저장되어 있어야 하지만, 실행 중에는 RAM에 있어야 쓸 수 있다. 그래서 부팅할 때 옮긴다. 이것이 VMA와 LMA가 다른 이유다.
왜 .bss를 0으로 채우나 — C 표준이 "초기화하지 않은 전역 변수는 0"이라고 규정하기 때문이다. 플래시에 0을 잔뜩 저장하는 것은 낭비이므로 런타임에 채운다.
③ 클럭 설정 — SystemInit
리셋 직후에는 내부 RC 오실레이터(HSI, 예: 16MHz)로 동작
→ 외부 크리스털(HSE) 활성화 및 안정화 대기
→ PLL 설정 (예: 8MHz → 168MHz)
→ 플래시 웨이트 스테이트 설정 ★
→ 버스 분주비 설정 (AHB, APB1, APB2)
→ 시스템 클럭 소스를 PLL로 전환
★ 순서가 매우 중요하다. 플래시는 CPU보다 느려서 고속에서는 대기 사이클이 필요한데, 웨이트 스테이트를 설정하기 전에 클럭을 올리면 명령어를 잘못 읽어 즉시 HardFault가 난다.
반대로 클럭을 내릴 때는 클럭을 먼저 내리고 웨이트를 줄인다. "느린 쪽에 맞추는 설정을 항상 먼저"가 규칙이다.
④ 부트로더가 있는 경우
[부트로더] 0x08000000 ~ 0x08007FFF [애플리케이션] 0x08008000 ~
- 부트로더가 앱으로 점프할 때
- ① SCB->VTOR = 0x08008000 ← 벡터 테이블 위치 변경
- ② __set_MSP(*(uint32_t *)0x08008000); ← 앱의 스택 포인터
- ③ 함수 포인터로 앱의 Reset_Handler 로 점프
점프 전에 반드시 정리해야 할 것들
- 사용한 주변장치 비활성화 (특히 인터럽트 소스)
- NVIC의 모든 인터럽트 비활성화·pending 클리어
- SysTick 정지
- 인터럽트 전역 활성화 상태로 복귀
정리를 빠뜨리면 앱이 시작된 뒤 부트로더 시절의 인터럽트가 발생해 앱의 벡터 테이블에 없는 핸들러로 가서 죽는다. OTA 구현에서 가장 흔한 사고다.
⑤ 리셋 원인 확인
if (RCC->CSR & RCC_CSR_IWDGRSTF) { /* 워치독 리셋 */ }
if (RCC->CSR & RCC_CSR_PORRSTF) { /* 파워온 리셋 */ }
RCC->CSR |= RCC_CSR_RMVF; // 플래그 클리어
현장에서 재부팅이 발생했을 때 원인을 아는 유일한 단서인 경우가 많다. 워치독 리셋 횟수를 백업 레지스터에 누적해 두면 문제 추적에 큰 도움이 된다.
디버깅
- main()에 도달하지 못한다면
- ① 벡터 테이블이 올바른 위치에 링크됐나 (map 파일 확인)
- ② 스택 포인터 초기값이 SRAM 범위인가
- ③ 클럭 설정에서 무한 대기에 걸렸나 (HSE 크리스털 불량)
- ④ 웨이트 스테이트 순서가 맞나
디버거로
Reset_Handler에 브레이크를 걸고 한 줄씩 따라가는 것이 정석이다.