디버거 — SWD 로 칩 안을 들여다보기
PC 에서는 디버거가 OS 의 도움을 받는다. MCU 에는 OS 가 없다. 그런데도 브레이크포인트가 걸리고 변수가 보인다 — 칩 안에 디버그 하드웨어가 따로 들어 있기 때문이다.
그 하드웨어가 무엇을 할 수 있고 무엇을 못 하는지를 알면, 디버거가 안 붙을 때 무엇을 봐야 하는지도 알게 된다.
JTAG 과 SWD
JTAG TCK · TMS · TDI · TDO (+TRST) 선 4~5개
SWD SWCLK · SWDIO 선 2개 ← ARM 이 만든 축약판
SWD 가 JTAG 의 기능을 거의 다 하면서 선을 둘로 줄였다. 핀이 귀한 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 가 명령 주소를 비교기로 감시하다가 일치하면 정지시킨다.
비교기 하나 = 브레이크포인트 하나
구현마다 다르므로 벤더 문서를 확인해야 한다. 상한을 넘으면 디버거가 "브레이크포인트를 더 걸 수 없다" 고 거부한다 — 이유를 모르면 도구 버그로 오해한다.
증상: 브레이크포인트를 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 문서