Cortex-M에서 처리할 수 없는 오류가 발생했을 때 진입하는 예외. 임베디드 디버깅의 최전선이다.
언제 발생하나
- ① 잘못된 메모리 접근 — NULL 포인터, 매핑되지 않은 주소
- ② 정렬되지 않은 접근 — M0/M0+ 에서 (M3+ 는 설정에 따라)
- ③ 잘못된 명령어 — — 함수 포인터가 깨짐, Thumb 비트 누락
- ④ 0으로 나누기 — — SCB->CCR 의 DIV_0_TRP 활성 시
- ⑤ 스택 오버플로 — — 스택이 유효하지 않은 영역으로
- ⑥ 에스컬레이션 — — 다른 폴트 핸들러가 비활성이거나
- 폴트 처리 중 또 폴트
원인 파악 — 상태 레지스터부터
SCB->HFSR // HardFault Status
FORCED — 다른 폴트가 에스컬레이션됨 → CFSR 을 봐야 한다 ★
VECTTBL — 벡터 테이블 읽기 실패
SCB->CFSR // Configurable Fault Status (3개가 합쳐진 32비트)
├ MMFSR (하위 8비트) — MemManage: MPU 위반
│ MMARVALID → MMFAR 에 위반 주소
├ BFSR (중간 8비트) — BusFault: 존재하지 않는 메모리
│ BFARVALID → BFAR 에 위반 주소
│ PRECISERR — 정확한 주소를 안다
│ IMPRECISERR — 쓰기 버퍼 때문에 주소를 모른다 ★
└ UFSR (상위 16비트) — UsageFault
UNDEFINSTR — 잘못된 명령어
INVSTATE — Thumb 비트 문제
UNALIGNED — 정렬 위반
DIVBYZERO — 0 나누기
★ FORCED 비트가 켜져 있으면 원래는 MemManage/BusFault/UsageFault인데 그 핸들러가 비활성이라 HardFault로 승격된 것이다. CFSR을 봐야 진짜 원인을 안다.
개별 폴트 핸들러를 활성화하면 더 정확한 진단이 가능하다.
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk
| SCB_SHCSR_BUSFAULTENA_Msk
| SCB_SHCSR_USGFAULTENA_Msk;
스택 프레임에서 정보 꺼내기 — 필수 기법
void HardFault_Handler(void) {
__asm volatile (
"TST LR, #4 \n" // EXC_RETURN bit2: 0=MSP, 1=PSP
"ITE EQ \n"
"MRSEQ R0, MSP \n"
"MRSNE R0, PSP \n"
"B hard_fault_c \n"
);
}
void hard_fault_c(uint32_t *sp) {
volatile uint32_t r0 = sp[0];
volatile uint32_t r1 = sp[1];
volatile uint32_t r2 = sp[2];
volatile uint32_t r3 = sp[3];
volatile uint32_t r12 = sp[4];
volatile uint32_t lr = sp[5]; // 어디서 이 함수를 불렀나
volatile uint32_t pc = sp[6]; // ★ 폴트를 낸 명령의 주소
volatile uint32_t psr = sp[7];
volatile uint32_t cfsr = SCB->CFSR;
volatile uint32_t bfar = SCB->BFAR; // 접근하려던 주소
__asm volatile ("BKPT #0"); // 디버거 연결 시 여기서 멈춘다
while (1) { }
}
★ PC 값을 맵 파일이나 addr2line과 대조하면 어느 소스 줄에서
죽었는지 정확히 알 수 있다.
arm-none-eabi-addr2line -e firmware.elf 0x08001234
# → main.c:142
어느 스택인지 판단하는 것이 중요하다. LR(EXC_RETURN)의 비트 2가 0이면 MSP, 1이면 PSP다. 잘못된 스택을 뒤지면 엉뚱한 값이 나온다.
원인별 전형적 패턴
| 증상 | 의심 |
|---|---|
| BFAR ≈ 0x00000000 | NULL 포인터 역참조 |
| BFAR가 이상한 큰 값 | 초기화 안 된 포인터, 배열 범위 초과 |
| PC가 이상한 값 | 함수 포인터 손상, 스택 오버플로 |
| INVSTATE | Thumb 비트 누락 (직접 계산한 주소로 점프) |
| UNALIGNED | packed 구조체 멤버 접근 |
| IMPRECISERR | 쓰기 버퍼 때문에 주소 불명 ★ |
★ IMPRECISERR 대응
// 쓰기 버퍼를 비활성화하면 정확한 주소를 얻을 수 있다 (느려지지만 디버깅용)
SCB->ACTLR |= SCB_ACTLR_DISDEFWBUF_Msk;
또는 폴트 지점 근처에 __DSB()를 넣어 쓰기를 강제 완료시킨다.
운영 중 대응 — 로그를 남기고 리셋
void hard_fault_c(uint32_t *sp) {
// .noinit 영역에 저장 (소프트 리셋으로는 지워지지 않는다)
fault_log.magic = 0xFA017;
fault_log.pc = sp[6];
fault_log.lr = sp[5];
fault_log.cfsr = SCB->CFSR;
fault_log.bfar = SCB->BFAR;
NVIC_SystemReset(); // 리셋
}
// 부팅 시
if (fault_log.magic == 0xFA017) {
report_to_server(&fault_log);
fault_log.magic = 0;
}
현장에서 발생한 폴트를 나중에 분석할 수 있다. 디버거를 연결할 수 없는 양산 기기에서 유일한 단서가 된다.
예방
✅ MPU로 NULL 영역·스택 가드 설정 ✅ 포인터 사용 전 NULL 검사 ✅ 배열 인덱스 범위 검사 ✅ 스택 크기를 워터마크로 검증 ✅ 개별 폴트 핸들러 활성화 (더 정확한 정보) ✅ 폴트 핸들러에서 로그를 남기고 리셋