임베디드 면접 용어 사전
MCU·ARMexception stacking

자동 스태킹

예외 진입 시 하드웨어가 일부 레지스터를 자동으로 스택에 밀어 넣는 동작. ISR을 일반 C 함수로 쓸 수 있게 한다.

예외가 발생할 때 Cortex-M 하드웨어가 레지스터 일부를 자동으로 스택에 저장하는 기능(automatic stacking).

무엇을 저장하나

스택 (높은 주소 → 낮은 주소로 쌓임)
┌──────────┐  ← 예외 전 SP
│  xPSR    │
│  PC      │  ← 복귀 주소
│  LR      │
│  R12     │
│  R3      │
│  R2      │
│  R1      │
│  R0      │
└──────────┘  ← 예외 진입 후 SP  (총 32바이트)

FPU를 쓰면 S0~S15 + FPSCR이 추가되어 총 104바이트가 된다.

왜 하필 이 8개인가 — AAPCS 규약

ARM의 함수 호출 규약(AAPCS)은 레지스터를 두 종류로 나눈다.

  • caller-saved — (R0-R3, R12, LR) — 호출하는 쪽이 필요하면 저장
  • callee-saved — (R4-R11) — 호출받는 함수가 저장·복원할 책임

하드웨어가 caller-saved만 저장하면, 나머지 R4~R11은 C 함수가 자기 규약에 따라 알아서 저장한다.

하드웨어: R0-R3, R12, LR, PC, xPSR   저장
C 함수  : 필요하면 R4-R11 을 프롤로그에서 저장
────────────────────────────────────────
합치면 모든 레지스터가 보존된다

이 조합 덕분에 ISR을 평범한 C 함수로 쓸 수 있다.

void USART1_IRQHandler(void) {     // 특수 키워드가 전혀 필요 없다
    …
}

다른 아키텍처(AVR, PIC)는 __interrupt 같은 키워드로 컴파일러에게 "전체 레지스터를 저장하라"고 알려야 한다.

부수 효과 — 인터럽트 지연이 짧고 일정하다

Cortex-M3/M4 의 인터럽트 지연 = 12 사이클 (고정)

스태킹과 벡터 인출이 파이프라인에서 병렬로 진행되므로 오버헤드가 작다. 게다가 항상 같은 사이클 수라 실시간성 분석이 가능하다.

테일체이닝 — 자동 스태킹의 최적화

  • 인터럽트 A 처리 완료 → 인터럽트 B가 대기 중

  • 일반적이라면: 복원(pop) → 다시 저장(push) — = 낭비

  • 테일체이닝 — : 복원과 저장을 생략하고 바로 B의 핸들러로 ← 6 사이클

스택에 있는 프레임을 그대로 재사용하는 것이다.

늦은 도착 (late arrival)

인터럽트 A의 스태킹 진행 중
→ 더 높은 우선순위 B가 도착
→ 스태킹은 이미 시작됐으니 그대로 두고, 벡터만 B의 것으로 교체
→ B를 먼저 처리

스태킹을 버리지 않고 재활용해 지연을 줄인다.

스택 사용량 계산에 반영해야 한다

인터럽트 하나당 최소 32바이트 (FPU 사용 시 104바이트) 중첩 5단계면 → 160 ~ 520 바이트

  • 각 핸들러의 지역 변수

MSP 크기를 정할 때 이것을 빠뜨리면 인터럽트가 몰릴 때 스택 오버플로가 난다. 특히 FPU를 쓰는 ISR이 중첩되면 순식간에 커진다.

FPU 컨텍스트는 lazy stacking으로 최적화할 수 있다. FPU 레지스터 자리는 잡아 두되 실제 저장은 미룬다

  • ISR이 FPU를 안 쓰면 저장이 아예 일어나지 않는다 FPU->FPCCR |= FPU_FPCCR_LSPEN_Msk; (기본 활성) 자리는 잡으므로 스택 크기 계산에는 104바이트로 넣어야 한다.

HardFault 분석에 쓰인다

크래시가 났을 때 스택에 쌓인 프레임에서 PC를 꺼내면 어느 명령에서 죽었는지 알 수 있다.

void HardFault_Handler(void) {
    __asm volatile (
        "TST LR, #4        \n"     // EXC_RETURN 의 bit2 로
        "ITE EQ            \n"     // MSP인지 PSP인지 판단
        "MRSEQ R0, MSP     \n"
        "MRSNE R0, PSP     \n"
        "B hard_fault_c    \n"
    );
}

void hard_fault_c(uint32_t *sp) {
    uint32_t pc = sp[6];      // 스택 프레임의 7번째가 PC
    uint32_t lr = sp[5];
    // pc 값을 map 파일과 대조하면 어느 함수인지 나온다
}

이 코드를 쓸 수 있으면 HardFault 디버깅이 훨씬 수월해진다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — MCU·ARM Cortex-M — 레지스터·버스·동작모드·NVIC·부팅