이번장에서는 디버거 사용법에 대해 설명하겠다.
코딩을 하고 실행을 해보면 우리가 의도한 대로 실행되지 않는 경우가 많다. 이럴때는 소스를 한줄씩 실행해가면서 그 상황에서의 변수값이나 레지스터 값을 볼 수 있으면 버그를 빠르게 찾아 낼 수 있을 것이다.
따라서 디버거 사용의 숙련도에 따라 개발 기간을 엄청나게 단축할 수 있으므로 디버거를 사용해 본 적이 없다면 본 장에서 확실히 익히기 바란다.
|
1. 준비물
따로 더 준비해야 할건 없다. 이전 장에서 작성한 HelloWorld 프로젝트를 그대로 사용할 것이다.
2.디버그 시작하기
먼저 디버깅을 하기 위해 HelloWorld 프로젝트를 열고 NUCLEO보드를 PC에 연결하자.
그 다음 툴바의 벌레 버튼 또는 F11키를 누르면 디버깅이 시작된다.
모양이 약간씩 다를 수는 있지만 아래와 같이 화면이 표시된다.
Project Explorer가 있는 화면왼쪽 부분에 Debug창이 추가되고 오른쪽에는 Variables, Breakpoints, Expressios등의 창이 표시된다.
그리고 아래쪽에는 Debug Console, Memory 창등이 추가된다.
소스 부분을 보면 HAL_Init() 함수 호출부분 라인번호 앞에 작은 화살표가 표시되어 있다.
이 화살표가 현재 실행될 부분이다. 다시말해 지금 상태는 HAL_Init()함수를 실행하기 직전에 멈춰 있는 상태인 것이다.
여기서 F8키를 누르면 실행을 재개한다.
설명을 계속하기전 디버깅에 관련된 기능부터 보겠다.
주로 사용하는 기능은 툴바에 표시된 빨간 네모 안의 버튼들과 Run메뉴의 Toggle Breakpoint 정도이다.(툴바의 버튼들은 Run메뉴에도 있다.)
이 장에서 사용할 기능만 추려서 단축키를 보면
- F11 : 디버그 모드로 실행하기.
- F5 : Step Into
- F6 : Step Over
- F8 : 실행 재개(Resume)
- Ctrl+Shift+B : 브레이크 포인트 토글
- Ctrl+F2 : 디버깅 중지
이 정도이고 사실 이것 이외의 항목들은 잘 사용하지 않는다.
이제 하나씩 보자.
먼저 디버깅을 진행하려면 브레이크 포인트라는 개념을 알아야 한다.
브레이크 포인트라는건 개발자가 지정한 소스 위치에서 프로그램 실행을 일시정지 하겠다는 의미이다. 즉 프로그램이 실행되다 이 브레이크 포인트 지점에 도착하면 MCU는 그때부터 작동을 중지한다.
그래서 개발자들은 이 지점까지의 상태로 자신이 정의한 변수값을 확인하거나 현재 레지스터 내용을 확인할 수 있다.
F11버튼을 눌러 최초 디버깅 모드로 진입하면 HAL_Init()함수에 화살표가 표시되어 있는데 현재 이 부분이 브레이크 포인트로 지정되어 이 부분에서 프로그램이 일시정지한 상태라는 뜻이다.
단, HAL_Init()함수에 브레이크 포인트가 지정되어 있는건 우리가 직접 걸어놓은건 아니고 아마도 CubeIDE자체에서 최초 main()함수 진입시 자동으로 걸리게끔 되어 있는듯 하다.
당연히 우리가 필요한 위치에 브레이크 포인트를 걸 수 도 있다.
HelloWorld프로젝트에서 main.c파일에 HAL_GPIO_EXTI_Callback()함수를 작성해 놓았다. User Button이 눌릴때 인터럽트 핸들러로 이 함수가 호출된다고 설명했다.
이 함수를 찾아서 if문에 커서를 위치시키고 Ctrl+Shift+B키를 눌러보자. 해당위치의 라인넘버 앞에 조그마한 원이 표시될 것이다.(작아서 잘 안보인다…) 그리고 키를 한번 더 누르면 사라진다.
키를 눌러도 되고 라인넘버에 마우스커서를 대고 더블클릭 해도 된다.
현재 main()의 HAL_Init()에서 일시 중지 되어 있으므로 프로그램을 재개시키자. F8키를 누르거나 툴바의 Resume버튼을 누르면 프로그램이 다시 재개된다. HAL_Init()앞에 있던 화살표가 사라졌을 것이다.
현재는 정상적으로 MCU가 프로그램을 실행하고 있는 상태이다.이 상태에서 우리가 브레이크 포인트를 지정한 루틴이 실행되도록 하기 위해 보드의 User Button을 눌러보자.
우리가 지정한 브레이크 포인트에 화살표가 표시되어 있을 것이다. 이 상태는지정한 위치에서 프로그램이 일시정지 상태로 들어가 있고 해당 위치를 실행할 준비가 되었다는 의미다.
이 상태에서 소스의 if(GPIO_Pin == GPIO_PIN_13) { 부분의 GPIO_Pin 변수에 마우스 커서를 살짝 올려보자.
아래 그림과 같이 해당 변수의 현재 값이 화면에 표시된다.
GPIO_Pin 파라미터로 넘어온 값은 8192이다. #define문으로 정의된 GPIO_PIN_13에도 마우서 커서를 갖다대면 해당 값이 나온다.
그래서 우리는 이 인터럽트가 발생했을 때 MCU가 GPIO_Pin파라미터에 8192를 넘겨줬고 이게 PB0의 값과 비교해서 맞는지를 확인해 볼 수 있다.
다시 if문 위에 char buf[256] 변수선언 부분에서 buf에 커서를 갖다대 보자.
buf변수에 현재 저장된 정보를 볼 수 있다. 변수 선언시 = {0,}과 같이 256byte전체를 0으로 초기화 했으므로 그림과 같이 모든 값이 0으로 설정되어 있는걸 볼 수 있다.
이 Local변수들을 한번에 확인하려면 CubeIDE화면 오른쪽의 Variables 창을 보면 된다.
이 화면에 보면 좀전에 봤던 이 함수내의 Local변수 리스트가 모두 표시되는데 현재 GPIO_Pin과 buf를 볼 수 있다.
만일 Variables창이 보이지 않는다면 메뉴의 “Window-Show View-Variables”를 찾아 클릭하면 창이 열린다.
계속 진행해보자.
현재 if문을 실행하기 위해 대기중이고 다음 문장을 실행하기 위해서는 세가지 방법이 있다.
첫번째는 F8키를 눌러 프로그램을 재개하는 것이고 두번째는 Step Into실행, 마지막 세번째는 Step Over실행이다.
F8키는 일단 현재 상태에서는 더 이상 볼게 없으므로 프로그램을 다시 정상적으로 실행시킨다는 의미다.
그리고 Step Into(F5키)와 Step Over(F6)키는 현재 브레이크 포인트 지점부터 소스 한줄씩 진행하겠다는 것이다.
이 둘의 차이점은 다음 실행할 구문이 함수인 경우 Step Into이면 해당 함수 구현부분 안으로 들어가 다시 한줄씩 실행하겠다는 것이고 Step Over는 함수 내부로는 들어가지는 않고 실행만 하겠다는 의미다.
현재 상태에서 F5키를 눌러 Step Into로 실행해보자 if문은 함수구현부분이 없으므로 Step Into를 하든 Step Over를 하든 동일하다.
현재 HAL_GPIO_TogglePin()함수에 화살표가 있을 것이다. 여기서 다시 F5키를 눌러보자.
그러면 이 함수가 정의되어 있는 소스가 자동으로 열리면서 해당 함수 구현부분으로 화살표가 옮겨져 있다.
함수 내부로 들어와서 한단계씩 실행해야 할 경우 이와 같이 함수 호출부분에서 F5키를 눌러 이와 같이 함수내부로 진입할 수 있다.
다시 F5키를 몇번누르면 HAL_GPIO_TogglePin()함수를 모두 실행하고 다시 HAL_GPIO_EXTI_Callback()함수로 돌아온다.
이제 sprintf()함수에 화살표가 와 있을 것이다.
여기서 F5키를 눌러보자. sprintf()도 함수인데 아까와는 달리 함수 내부로 진입하지 않는다.
왜냐하면 sprintf함수는 C에서 제공하는 표준함수로 소스없이 Object파일형태인 Static Library로 제공되기 때문이다. 그래서 소스를 열 수 없으므로 바로 다음으로 넘어가 버린다.
다음으로 진행하기전 다시 HAL_GPIO_EXTI_Callback()안에 정의한 char buf[256]에 다시 한번 마우스 커서를 대보자.
아까와는 달리 sprintf로 buf를 채웠으므로 buf변수에 문자가 들어가 있는걸 볼 수 있다.
이제는 HAL_UART_Transmit()함수에 화살표가 와 있을 것이다. 이번에는 F6키를 눌러 함수 내부로는 들어가지 않고 실행만 하도록 하자.
곧바로 HAL_GPIO_EXTI_Callback()함수 마지막으로 이동한다. 물론 이 부분에서 F5키를 눌러 HAL_UART_Transmit()함수 내부로 들어갈 수도 있다.
아직 디버깅을 끝내지 말고 이번에는 CubeIDE의 왼쪽부분을 보자. 원래 Project Explorer가 있는 위치에 Debug창이 하나더 표시되어 있다. 만일 안보인다면 메뉴의 “Window-Show View-Debug”를 클릭해서 열면 된다.
Visual Studio나 다른 툴에서는 보통 Call Stack(호출스택)이라고 부르는 창이다.
보면 현재 우리가 브레이크 포인트로 잡고 있는 HAL_GPIO_EXTI_Callback()함수가 제일 위에 있고 하이라이트 되어 있다.
그 밑으로 HAL_GPIO_EXTI_IRQHandler(), EXTI15_10_IRQHandler() 함수가 나오는데 이게 뭘 의미하냐면 HAL_GPIO_EXTI_Callback()함수가 호출되기전 어떤함수에서 이 함수를 호출했는지를 표시하는 것이다.
함수 호출시 함수의 실행순서대로 스택영역에 쌓이므로 이 스택에서 각 함수 리스트만 가져와서 표시해 놓은 것이다.
이 순서는 앞장에서 HelloWorld 프로젝트를 진행할 때 이미 봤던 내용이다.
CubeMX에서 NVIC 항목의 EXTI line[15:10]부분에 체크를 하면 stm32f4xx_it.c에 EXTI15_10_IRQHandler() 추가된다고 하였다.
최초 인터럽트가 발생하면 인터럽트 벡터테이블의 주소값에 저장되어 있던 EXTI15_10_IRQHandler()함수가 호출되고 이 함수 안에서는 다시 HAL_GPIO_EXTI_IRQHandler가 호출된다. 다시 이 함수 안에서 우리가 선언한 HAL_GPIO_EXTI_Callback()함수가 호출된 것이다.
Debug창에서 각 함수를 더블클릭하면 소스가 열리므로 다시 한번 확인해보자.
이와 같이 Call Stack은 현재 함수가 어떤 경로로 호출됐는지 확인할 때 용이하다.
모두 확인했다면 F8키를 눌러 프로그램을 정상상태로 재개시키자.
이번에는 디버깅시 보게되는 다른 창들에 대해 설명하겠다. 잘 쓰지 않는 창은 생략하고 중요한 창만 보자.
오른쪽에 있는 Variables는 이미 설명했으므로 넘어가고, 그 다음 Breakpoints창이 있다.
Breakpoints
이름에서 보듯 우리가 표시한 브레이크 포인트 리스트를 가지고 있다.
소스가 얼마안되면 상관없지만 소스량이 많아지고 디버깅을 오래 진행하다 보면 자신도 모르게 소스 이곳저곳 브레이크 포인트를 걸어놓은 부분이 많다.
이 창은 이 브레이크 포인트를 한눈에 보고 Disable하거나 삭제하거나 할 수 있다. 그리고 더블클릭하면 해당 위치로 이동할 수도 있다.
Register
MCU 내부(코어) 레지스터 리스트와 상태를 볼 수 있는 창이다. 이 레지스터는 Peripheral관련된 레지스터와는 구분되며(Peripheral관련 레지스터는 SFRs 창에서 볼 수 있다.) MCU의 핵심 동작을 처리하는 레지스터 들이다.
r로 시작하는 범용 레지스터 여러 개와 현재 스택포인터 위치를 저장하는 sp, 현재 프로그램 실행위치를 저장하는 pc 등을 볼 수 있다.
User Button을 눌러 아까 걸어놓은 브레이크 포인트에서 브레이크를 걸고 Debug창에서 HAL_GPIO_EXTI_Callback()함수 옆의 주소값을 보자. 0x80009a8등과 같이 나온다.(경우에 따라 숫자값은 달라질 수 있다.)
그리고 Register창에서 pc레지스터 값을 보면 동일한 값이 표시될 것이다.
SFRs
Peripheral관련등의 레지스터 리스트를 볼 수 있는 창이다.
먼저 SFRs창에서 GPIOB레지스터를 확장하고 그 밑에 ODR을 선택한 뒤 소스에서 아까 걸어놨던 브레이크 포인트는 끄고 그 밑에 sprintf부분에 다시 브레이크 포인트를 잡자.
그리고 User Button을 누르면 sprintf에서 브레이크가 걸린다. 이 상태에서는 위의 HAL_GPIO_TogglePin이 실행된 상태인데 이때 ODR을 확인해보면 값이 바뀌어 있을 것이다.
F8을 누르고 다시한번 User Button을 눌러 보면 ODR값이 계속 바뀐다.
(아니면 Break Point를 모두 끄고 SFRs에서 ODR 레지스터에서 마우스 오른쪽 버튼을 누른 뒤 “Toggle live update”를 선택하면 실시간으로 변경사항을 확인할 수 있다.)
Disassembly
실행코드를 어셈블리어로 표시하는 창이다. 이 창은 아마 디폴트로 표시가 되지 않을 것이므로 “Window-Show View-Disassembly”를 선택해서 표시하자.
이 창은 브레이크 포인트가 걸린 지점의 어셈블리어를 표시한다. 친철하게도 중간중간 C 코드로도 표시를 해주므로 비교해 보기가 수월하다.
어셈블리어를 모두 볼 필요는 없지만 간혹 볼 일이 있다.
Entry Point를 설명하는 장에서 간략하게나마 다시 볼 것이므로 창은 추가를 해놓자.
Memory
등록한 변수의 메모리 값을 확인하는 창이다.메모리 창을 열어보면 Monitors라는 항목이 있고 옆에 "+"버튼이 보인다. 이 버튼을 눌러서 보고자 하는 변수명을 적어주면 되는데 해당 변수의 주소를 찾아야 하므로 C형태대로 "&변수명" 과 같이 앞에 &를 붙여줘야 한다.이렇게 직접 등록해줘도 되고 Variables창에서 해당 지역변수를 선택하고 마우스 우클릭으로 팝업메뉴를 띄운다음 "View Memory"항목을 선택해줘도 된다. 단 Variables창에는 지역변수만 표시되므로 전역변수들은 위의 방법대로 수동 등록해야 한다.
이렇게 해당 변수를 등록하면 위 그림과 같이 그 변수가 할당된 메모리 주소가 표시되고(여기서는 0x2002FEB6) 밑에 해당 주소의 현재 값을 볼 수 있다.(1B2F0020중 0020 : Little Endian방식이므로 실제는 0x2000=8192)
그리고 우리 예제에서 HAL_GPIO_EXTI_Callback()에 정의된 char buf[256] 변수는 변수 자체가 포인터이다. 따라서 Variables창에서 이 변수를 선택해보면 Default값에 이 포인터가 가리키고 있는 주소값이 들어있다. 그래서 변수를 View Memory로 추가하면 GPIO_Pin과는 달리 앞에 &가 붙지 않는다.
Memory Browser
메모리 전체를 볼 수 있는 창이다. 이 창도 디폴트로 표시가 안되므로 “Window-Show View-Memory Browser”를 선택해 수동으로 추가해 주자.
이 창은 아마 CubeIDE의 오른쪽이 아니라 아래쪽에 추가될 것이다.
위의 Memory창에 추가된 주소를 이 창 위의 입력란에 입력하고 엔터키를 치면 그 주소를 시작으로 메모리 값을 볼 수 있다. 단 주소를 적을때 16진수이면 앞에 0x를 붙여야 한다.
아까 걸어놓은 sprintf 브레이크 포인트를 그대로 두고 다시 User Button을 눌러 브레이크를 잡아보자.
F5나 F6을 한번 눌러 sprintf를 실행한다. 그리고 Variables창 또는 소스상에서 buf변수에 마우스 커서를 위치시키자.
이 변수는 포인터이므로 입력되어 있는 값자체가 주소값이다. 따라서 이 변수의 내용중 Default부분의 16진수값인 0x2002feb8 (여러분이 보는 값은 이 값과 다를 수 있음)를 긁어서 복사하자.
그리고 Memory Browser창의 제일 위에 있는 부분에 붙여넣기 한 후 엔터키를 누르면 입력한 위치의 현재 메모리 데이터가 쭈욱 표시된다.
오른쪽에는 아스키 문자로 Hello World!문자가 보이고 왼쪽에는 해당 위치의 16진수값들이 보인다.
아마 6C6C6548… 이렇게 보일텐데 현재 16진수를 아스키코드표에서 찾아보면 아스키 문자열과 달리 순서가 반전되어 있는 걸 볼 수 있다.
이건 Memory Browser가 값을 표시할 때 Little Endian으로 보게끔 설정이 되어 있어서다.
이와 같이 문자열로된 변수의 메모리를 볼 때는 Memory Browser창에서 마우스 우클릭해서 팝업창을 연 뒤 Cell Size를 1Byte로 설정하면 순서대로 볼 수 있다.
3. 마무리
디버거가 없었다면 우리는 코드가 실행되는 도중 변수 값을 확인하기 위해 UART로 해당 변수값을 전송해서 확인해야 했을 것이다. 이렇게 되면 디버그 코드를 일일이 작성해서 소스에 넣어야 하므로 귀찮기도 하거니와 소스도 쓸데없이 지저분해 진다.
디버거를 처음 쓰면 어색해서 적응이 안 될 수도 있지만 적응하면 굉장히 편리하므로 적극 사용해 보길 권한다.
변수 상태 뿐 아니라 Step실행을 통해 조건문이 어느쪽으로 분기하는지 등 여러가지를 관찰할 수 있다.
다른건 몰라도
F11 : 디버거 실행
F5 : Step Into
F6 : Step Over
F8 : Resume
Ctrl+Shift+B : Break point Toggle
5개 단축키는 외워두고 사용하자.
'임베디드 > STM32' 카테고리의 다른 글
6. [STM32] GPIO 기초 (0) | 2024.07.25 |
---|---|
5. [STM32] NUCLEO F429/439 보드 Pinmap (0) | 2024.07.22 |
4. [STM32] Entry Point (0) | 2024.07.12 |
2. [STM32] Hello World! (0) | 2024.07.09 |
1. [STM32] STM32 MCU (0) | 2024.07.08 |