Skip to content
isdnetworks
Go back

하드웨어에 디버거를 붙이며

임베디드 코드를 디버깅하는 방법이 몇 가지 있었다. 상황에 따라 골라 써야 했다.

Table of contents

Open Table of contents

멈춰서 보기

JTAG 으로 디버거를 붙여 breakpoint 를 걸고 멈춘 뒤 메모리와 레지스터를 본다.

JTAG 을 붙이면 볼 수 있는 것이 가장 많고 변수 값과 호출 스택과 메모리 덤프를 마음대로 본다.

대신 breakpoint 에 걸리는 순간 시간이 멈추고 이것이 문제가 되는 경우가 있다.

주변 장치가 계속 돌고 있으면 버퍼가 넘친다
통신 상대가 타임아웃으로 끊는다
감시 타이머가 재기동시킨다

멈추는 순간 다른 것들은 안 멈춘다. 그래서 멈췄다 재개하면 이미 상황이 달라져 있다.

흘려보내며 보기

멈추지 않고 값만 기록하는 방식으로 메모리에 쌓거나 GPIO 핀으로 내보낸다.

GPIO 토글은 타이밍을 거의 안 건드리므로 실시간 처리에서는 이것뿐이다.

대신 볼 수 있는 것이 제한적이다. 미리 정한 것만 보고 저 변수도 볼걸 하면 다시 심고 다시 돌려야 한다.

판단 기준 — 언제 무엇을 쓰나

기준이 생겼다.

초기화·설정 코드   멈춰서 본다 — 시간이 안 중요하다
주기 처리·인터럽트  흘려보내며 본다 — 멈추면 상황이 바뀐다
재현 안 되는 문제   흘려보내며 기록해 두고 나중에 본다
메모리 손상        특정 주소를 감시하는 기능을 쓴다

네 번째가 특히 유용했다. 어디서 값이 망가지는지 모를 때 watchpoint 를 걸어 두면 그 주소에 쓰는 순간 어느 코드가 썼든 거기서 멈춰 준다.

읽어서 찾는 것보다 훨씬 빨랐다.

최적화가 방해했다

릴리스 빌드에서 JTAG 으로 보려니 이상했다. 변수가 레지스터에만 있어 안 보이고 한 줄씩 진행하는데 순서가 튀고 있어야 할 코드가 없었다.

최적화 때문이다. 컴파일러가 변수를 없애고 순서를 바꾸고 코드를 합친다.

-O0 으로 바꾸면 잘 보이는데 그러면 타이밍이 달라져서 문제가 안 나타난다. 절충으로 일부만 최적화를 끄는 방법을 썼다.

#pragma GCC optimize ("O0")
void problem_function(void) { ... }

그 함수만 느려지고 나머지는 그대로라 타이밍 영향이 적다.

값이 이상한 경우 중 몇 개는 최적화가 읽기를 생략해서였다.

while (flag == 0) { }   // flag를 한 번만 읽고 캐시할 수 있다

인터럽트에서 flag 를 바꿔도 이 루프는 모른다. 컴파일러가 이 루프 안에서 안 바뀐다고 판단하고 최적화한다.

volatile uint8_t flag;

volatile 을 붙이면 매번 실제로 읽게 만든다. 인터럽트와 공유하는 변수와 하드웨어 레지스터를 가리키는 포인터는 전부 확인했고 빠뜨린 것이 있으면 최적화 수준에 따라 동작이 달라진다.

재현 조건을 먼저 만들었다

디버깅 방법보다 중요한 것이 하나 있었는데 재현이 되게 만드는 것이다.

가끔 나는 문제는 JTAG 을 붙여 놔도 안 나오고 그러면 아무것도 못 한다. 그래서 먼저 어떤 조건에서 잘 나는지 찾았다.

부하를 높이면 자주 난다
특정 데이터 패턴에서 난다
시작 직후에 난다

조건을 찾으면 그것을 반복하는 스크립트를 만들었고 몇 분에 한 번 나던 것이 몇 초에 한 번 나게 되면 디버깅이 훨씬 빨라진다.

정리


Share this post on:

Previous Post
배치가 표를 잡고 있었다
Next Post
재는 주기와 남기는 주기를 나눴다