임베디드 학습 노트 목차

디버거 — SWD 로 칩 안을 들여다보기

PC 에서는 디버거가 OS 의 도움을 받는다. MCU 에는 OS 가 없다. 그런데도 브레이크포인트가 걸리고 변수가 보인다 — 칩 안에 디버그 하드웨어가 따로 들어 있기 때문이다.

그 하드웨어가 무엇을 할 수 있고 무엇을 못 하는지를 알면, 디버거가 안 붙을 때 무엇을 봐야 하는지도 알게 된다.


JTAG 과 SWD

JTAG   TCK · TMS · TDI · TDO (+TRST)   선 4~5개
SWD    SWCLK · SWDIO                   선 2개  ← ARM 이 만든 축약판

SWDJTAG 의 기능을 거의 다 하면서 선을 둘로 줄였다. 핀이 귀한 MCU 에서 4개를 아끼는 것은 큰 이득이라, 요즘 Cortex-M 개발은 대부분 SWD 로 한다.

디버거(ST-LINK 등)                MCU
  SWCLK ────────────────────────> SWCLK
  SWDIO <───────────────────────> SWDIO   (양방향 — 반이중)
  GND   ────────────────────────  GND     ★ 반드시 연결
  VDD   ────────────────────────  VDD     (전압 감지용, 전원 공급은 아닐 수도)
  NRST  ────────────────────────> NRST    (선택 — 연결 리셋용)

GND 를 안 잇는 것이 "디버거가 안 붙는다" 의 흔한 원인이다. 기준 전압이 없으면 신호를 읽을 수 없다.

디버그 유닛이 하는 일

CPU 코어
  ├ FPB  (Flash Patch and Breakpoint)   ─ 명령 주소를 감시 → 브레이크포인트
  ├ DWT  (Data Watchpoint and Trace)    ─ 데이터 주소를 감시 → 워치포인트
  ├ ITM  (Instrumentation Trace)        ─ 소프트웨어가 로그를 내보낸다
  └ ETM  (Embedded Trace Macrocell)     ─ 실행 흐름 전체를 기록 (고급 칩만)

브레이크포인트가 하드웨어 자원이라는 점이 PC 와 크게 다르다.

하드웨어 브레이크포인트는 개수가 정해져 있다

PC 에서는 브레이크포인트를 원하는 만큼 걸 수 있다. 코드가 RAM 에 있어 디버거가 그 자리에 트랩 명령을 써넣기 때문이다.

MCU 는 코드가 Flash 에 있어 실행 중에 고쳐 쓸 수 없다. 그래서 다른 방식이 필요하다 — FPB 가 명령 주소를 비교기로 감시하다가 일치하면 정지시킨다.

비교기 하나 = 브레이크포인트 하나
자원개수
FPB 명령 비교기Cortex-M3/M4 에서 최대 6개 (구현에 따라 더 적을 수 있다)
DWT 워치포인트 비교기4개

구현마다 다르므로 벤더 문서를 확인해야 한다. 상한을 넘으면 디버거가 "브레이크포인트를 더 걸 수 없다" 고 거부한다 — 이유를 모르면 도구 버그로 오해한다.

증상: 브레이크포인트를 7번째로 걸었더니 무시된다
원인: 하드웨어 비교기가 다 찼다
대응: 안 쓰는 것을 지운다. 조건부 브레이크포인트는 특히 비싸다

코드가 RAM 에서 실행된다면 소프트웨어 브레이크포인트(BKPT 명령 삽입)를 쓸 수 있어 개수 제한이 없다. 부트로더 개발 등에서 이 차이가 드러난다.

워치포인트 — 변수가 바뀌는 순간을 잡는다

"이 변수가 왜 바뀌었나" 를 찾는 가장 강력한 도구다. 브레이크포인트는 코드 위치를 알아야 하지만, 워치포인트누가 건드리든 잡는다.

# GDB
watch  g_config          # 값이 바뀌면 멈춘다
rwatch g_config          # 읽으면 멈춘다
awatch g_config          # 읽거나 쓰면 멈춘다

# 주소로 직접
watch *(uint32_t*)0x20000100
쓸 곳
  스택 오버플로가 전역 변수를 덮어쓴다  → 그 변수에 워치포인트
  포인터가 널이 되는 시점을 모른다      → 그 포인터에 워치포인트
  DMA 가 예상 밖 영역을 건드린다        → 그 주소에 워치포인트

DWT 비교기가 4개뿐이므로 남용할 수 없다. 그리고 주소를 아는 것만 감시할 수 있어, 스택 지역 변수는 함수가 살아 있는 동안만 유효하다.

언제 디버거가 안 붙나

① GND 미연결                     기준이 없다
② 전원이 안 들어옴                VDD 를 재 본다
③ SWD 핀을 GPIO 로 재설정        코드가 PA13/PA14 를 다른 용도로 바꿨다
④ 저전력 모드에 들어가 클럭 정지  Stop/Standby 에서는 디버그 클럭도 멈춘다
⑤ 무한 리셋 루프                  부팅 직후 리셋되어 붙을 틈이 없다
⑥ 읽기 보호(RDP) 활성            보안 설정으로 접근이 막혔다

③이 특히 잘 걸린다.

// ✗ 모든 GPIO 를 초기화하면서 SWD 핀까지 바꿔 버린다
for (int p = 0; p < 16; p++) GPIOA->MODER |= (0b01 << (p*2));   // PA13/14 포함
// → 다음 부팅부터 디버거가 안 붙는다

한 번 굽고 나면 디버거로 못 들어가므로 복구가 어렵다. 해법은 리셋 직후의 짧은 창을 노리는 것이다.

연결 방법 (ST-LINK)
  "Connect under reset"  — NRST 를 잡고 있는 동안 붙는다
  또는 BOOT0 를 눌러 부트로더로 부팅한 뒤 지운다

④를 위한 설정도 있다.

// 저전력 모드에서도 디버그를 유지한다 (개발 중에만)
DBGMCU->CR |= DBGMCU_CR_DBG_SLEEP | DBGMCU_CR_DBG_STOP | DBGMCU_CR_DBG_STANDBY;

이 비트를 켜면 저전력 모드에서 전류가 늘어난다. 전력을 측정할 때는 반드시 꺼야 한다 — "데이터시트보다 대기 전류가 훨씬 크다" 의 흔한 원인이다.

디버거가 타이밍을 바꾼다

void TIM2_IRQHandler(void) {
    // 여기 브레이크포인트를 걸면
    // 멈춰 있는 동안 다음 인터럽트가 계속 대기·유실된다
}

정지시키는 순간 실시간성이 사라진다. 통신 타임아웃이 나고, 모터가 폭주하고, 워치독이 리셋한다.

// 디버그 중에는 워치독을 멈춘다
DBGMCU->APB1FZ |= DBGMCU_APB1_FZ_DBG_IWDG_STOP | DBGMCU_APB1_FZ_DBG_TIM2_STOP;

타이머와 워치독을 "디버그 중 정지" 로 설정하는 것이 실무 필수 설정이다. 안 하면 브레이크포인트에서 멈출 때마다 워치독이 리셋한다.

그래도 못 잡는 문제가 있다.

증상: 브레이크포인트를 걸면 문제가 사라진다 (하이젠버그)
원인: 타이밍 의존 버그 — 경쟁 조건, 인터럽트 순서
대응: 멈추지 말고 '기록' 한다 → 다음 편의 트레이스·로깅

멈추지 않고 보는 방법

라이브 워치 — 실행 중에 메모리를 읽는다

Cortex-M 의 디버그 포트는 CPU 를 멈추지 않고 메모리를 읽을 수 있다
→ IDE 의 "Live Watch" 로 변수 값이 실시간으로 갱신되는 것을 본다

아주 유용하지만 원자성이 없다. 64비트 변수나 구조체를 읽는 중에 값이 바뀌면 찢어진 값이 보인다. 그리고 버스를 조금 쓰므로 극한 타이밍에서는 영향이 있다.

반정지 — printf 를 디버거로 보낸다

// 세미호스팅 — UART 없이 IDE 콘솔로 printf 가 나간다
printf("value=%d\r\n", v);        // BKPT 명령으로 디버거에 요청한다

편하지만 극히 느리다. 호출 하나가 수 ms 걸리고, 디버거가 안 붙어 있으면 그 자리에서 멈춘다. 개발 초기에만 쓰고 제품에는 절대 남기지 않는다.

GDB 로 하는 실무

# OpenOCD 를 서버로 띄우고
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg

# 다른 터미널에서
arm-none-eabi-gdb firmware.elf
(gdb) target extended-remote :3333
(gdb) monitor reset halt
(gdb) load                        # 플래시에 굽는다
(gdb) continue
# 자주 쓰는 것들
(gdb) info registers              # 코어 레지스터 전부
(gdb) p/x $sp                      # 스택 포인터
(gdb) x/16xw 0x20000000            # 메모리를 워드 16개 덤프
(gdb) p *(GPIO_TypeDef*)0x40020000 # 주변장치 레지스터를 구조체로 본다
(gdb) bt                           # 콜 스택
(gdb) info frame                   # 현재 프레임 상세
# 최적화된 코드에서 변수가 안 보일 때
(gdb) p x
$1 = <optimized out>

-O2 에서는 변수가 레지스터에만 있거나 아예 없어져 값을 볼 수 없다. 디버깅 중에는 -Og(디버깅 가능한 최적화)로 빌드하는 것이 절충이다. -O0 로 내리면 타이밍이 달라져 문제가 재현되지 않을 수 있다.

# 주변장치 레지스터를 이름으로 보려면 SVD 파일이 필요하다
(gdb) monitor mmw 0x40020014 0x20 0     # 또는 직접 쓴다

실무 순서 — 안 붙을 때

① 배선          SWCLK·SWDIO·GND 를 확인한다
② 전원          VDD 를 재 본다 (3.3V?)
③ 리셋 연결     "connect under reset" 으로 시도
④ 지우기        전체 칩 소거(mass erase) 후 다시 시도
⑤ BOOT0        부트로더로 부팅해 접근
⑥ 클럭          외부 크리스털을 기다리다 멈췄는지 확인

④가 결정적이다. 코드가 SWD 핀을 망가뜨렸거나 무한 리셋에 빠졌다면, 칩을 비우면 그 코드가 사라져 다시 붙는다. 개발 중에는 잃을 것이 없으므로 주저할 이유가 없다.

# ST-LINK 로 전체 소거
st-flash erase
# 또는 OpenOCD 로
openocd -f ... -c "init; reset halt; stm32f4x mass_erase 0; exit"

한눈에 정리

  • SWD 는 선 두 개JTAG 을 대체한다. GND 를 안 이으면 안 붙는다
  • 브레이크포인트가 하드웨어 자원이다 — Flash 를 실행 중에 못 고치므로 비교기로 주소를 감시한다
  • FPB 명령 비교기는 Cortex-M3/M4 에서 최대 6개, DWT 워치포인트는 4개 (구현마다 다르니 확인)
  • 상한을 넘으면 조용히 무시되거나 거부된다 — 도구 버그로 오해하기 쉽다
  • 워치포인트는 "누가 이 변수를 건드렸나" 를 잡는 최강 도구스택 오버플로·DMA 오염 진단
  • SWD 핀을 GPIO 로 바꾸면 다음 부팅부터 못 붙는다 — connect under reset 또는 mass erase 로 복구
  • 저전력 디버그 비트를 켜면 대기 전류가 늘어난다 — 전력 측정 전에 꺼야 한다
  • 워치독·타이머를 "디버그 중 정지" 로 설정하지 않으면 브레이크포인트마다 리셋된다
  • 멈추면 사라지는 버그는 기록으로 잡는다(다음 편)
  • 세미호스팅은 느리고 디버거가 없으면 멈춘다 — 제품에 남기지 않는다
  • -O2 에서는 변수가 <optimized out>-Og 가 절충점
  • 안 붙으면 mass erase 가 가장 확실한 복구 수단이다

꼬리질문 대비

  • "MCU 에서 브레이크포인트 개수가 제한되는 이유는?" → 코드가 Flash 에 있어 트랩 명령을 못 써넣는다. FPB 비교기 개수(M3/M4 최대 6)가 곧 한계
  • "브레이크포인트에 멈출 때마다 리셋되는 이유는?" → 워치독이 계속 돈다. DBGMCU 로 '디버그 중 정지'를 설정해야 한다
  • "디버거를 붙이면 재현이 안 되는 버그는 어떻게 잡나?" → 멈추지 않는 관측 — RTT·ITM·링버퍼 로그·GPIO 토글로 흐름을 기록한다

출처 — Arm, Cortex-M4 Technical Reference Manual(DDI0439) — FPB·DWT 비교기 · Arm, Cortex-M4 Devices Generic User Guide(DUI 0553) §5 디버그 · Arm, Serial Wire Debug(ADIv5, IHI0031) · STMicroelectronics, RM0090 §38 DBGMCU · OpenOCD·GDB 문서

RTOS — 선점과 블로킹을 되찾는다폴트 해독 — HardFault 에서 원인을 찾아내기