[임베디드 스케치 요약 및 정리] 4. Debug Analyse

Embedded June·2022년 12월 10일

임베디드스케치

목록 보기
5/5
post-thumbnail

'임베디드 스케치'의 4번째 요약 및 정리는 exception 및 디버깅과 연관된 내용을 담았으며 원본 교제의 다음 부분을 포함합니다.

  • 책의 Chapter 3 : 307 ‘소스 레벨 디버깅이 뭐죠?’
  • 책의 Chapter 5
    • 506 ‘모드 비트와 Exception 처리’
    • 507 ‘ARM 프로세서와 하드웨어 디버거?’
    • 508 ‘하드웨어 디버거와 Breakpoint’
    • 509 ‘하드웨어 디버거와 array, 그리고 pointer?’
  • 책의 Chapter 7
    • 700 ‘디버깅의 달인’
    • 701 ‘고급 디버깅의 기술’

[※] 표시와 함께 존댓말로 적힌 문장은 책에는 없으나 부가설명이 필요한 경우 구글링을 통해 제가 추가 학습한 내용입니다!


1. 소스 레벨 디버깅

  • 임베디드 시스템은 윈도우 개발 환경과 달리 디버깅이 특히 중요하다.
    • Host PC 환경에서는 printf()나 기타 IDE의 유용한 툴을 활용해 쉽게 디버깅 할 수 있지만, 임베디드 시스템에는 그런 게 없다.
    • printf()로 메시지를 출력하기 위해서는 serial controll register를 설정해야 하고 serial cable을 host PC와 연결한 뒤 serial port를 통해 타겟 보드와 통신해야 한다.
    • 만일 타겟 시스템에 serial port가 없거나, serial controll register 초기화 전 상황을 디버깅하려면 printf로는 힘들다.
  • 그러므로 임베디드 시스템의 소스 레벨 디버깅은 대부분 HW 디버깅이다.
  • 디버깅을 위한 준비물을 알아보자.
    1. HW 디버거 장비 및 SW 프로그램
    2. 타겟 시스템과 HW 디버거 사이를 연결하는 JTAG 장비 및 케이블
    3. 컴파일 후 만들어진 ELF 파일과 바이너리 파일
  • .elf 파일은 host-PC의 메모리에 다운로드를,
    .bin 파일은 JTAG을 사용해 타겟 플래시 메모리에 다운로드(write)한다.
  • HW디버거와 연동되는 SW 프로그램에 아래와 같이 명령어를 입력하면 위 과정이 수행되며 아래와 같은 화면을 볼 수 있다.
    // 타겟 플래시메모리에 바이너리 program 하기
    Flash.Erase 0x0--0x003FFFFF
    Flash.Program all
    data.load.binary project.bin 0x0
    Flash.Program off
    // Host-PC의 메모리에 ELF 올리기
    data.load.elf project.elf /nocode

  1. C source Debugging window
    • 개발자가 만든 C 레벨에서 한 라인씩 실행하면서 프로그램 수행 순서를 모두 추적하도록 돕는다.
    • IDE에서 하듯 특정 라인에 breakpoint를 걸어 halt 시킬 수 있다.
  2. Flash memory dump window
    • 현재 플래시 메모리에 들어 있는 바이너리를 보거나 peripheral register 값을 볼 수 있다.
  3. Function call history window
    • 프로그램 수행 순서가 어떻게 돼 왔는지를 한 눈에 알아 볼 수가 있다.
    • 특정 함수에 있는 지역변수의 값도 모니터링 할 수 있다.
  4. ARM Core register window
    • 현재 ARM Core 내부의 레지스터들의 값을 확인 할 수 있다.
    • ARM 파이프라인 과정을 확인할 수 있다.
  • 옛날에는 숙련된 개발자의 노하우와 감, 추측이 디버깅에 중요했지만, 이제는 HW 디버깅툴을 잘 사용하는 것 또한 중요하다.

2. HW 디버거의 사용

2.1. HW 디버거의 대표 기능, Breakpoint

  • HW 디버거의 최대 장점은 개발자가 core를 자유자재로 halt/ running 시키면서 실행흐름, 변수, 레지스터값, 메모리값을 확인할 수 있다는 점이다.
  • 이 기능의 핵심은 ‘Breakpoint’다. Breakpoint는 개발자가 원하는 조건에서 core를 멈춘다.
  • Core 제조회사는 breakpoint를 지원하기 위해 core 내부에 ICE(In-Circuit Emulator) Breakpoint Register를 만든다.
    • 예를 들어, ARM core의 이름에 ‘I’가 포함된다면, ICE Breakpoint 레지스터가 포함된 모델이다.
  • 이 레지스터를 활용해서 2가지 방법으로 breakpoint를 사용할 수 있다.

2.1.1. Soft Breakpoint

  • 동작 과정은 다음과 같다.
    1. 개발자가 멈추고 싶은 주소에 ICE breakpoint 레지스터를 통해 breakpoint를 설정한다.
    2. 해당 주소의 원본 코드를 백업해둔다.
    3. 해당 주소에 0xBEBEBEBE라는 값을 덮어씌운다.
    4. ARM core를 running 한다.
    5. 해당 주소에 PC값이 도달해 fetch를 할 때 data bus로 load되는 값이 0xBEBEBEBE라는 사실을 ICE Breakpoint 레지스터가 인식한다.
    6. ICE Breakpoint 레지스터는 core에게 멈추라는 명령을 보내 core가 멈춘다.
    7. 백업해둔 원본 코드를 해당 주소에 덮어씌워 복구한다.
    • ([※] 어디에 백업하는건지, 백업과 복원은 누가 담당하는지는 모르겠네요…)
  • Soft breakpoint는 개수 제한 없이 무한정으로 사용할 수 있다.
  • 하지만, soft breakpoint가 지정하는 주소는 반드시 메모리 영역만 가능하다. 왜냐하면, 특정 주소에 코드값을 쓰고 복원하는 과정이 포함되기 때문이다. 따라서 NOR 플래시를 사용하는 시스템이라면 soft breakpoint를 사용할 수 없다.
    • [※] 사실 이 부분이 조금 이해가 안 됐습니다. Breakpoint는 instruction이 수행되는 주소에 걸테니 메모리 영역의 주소이므로 NOR 플래시든 NAND 플래시든 상관없이 soft breakpoint를 사용할 수 있다고 생각하기 때문입니다. NOR/ NAND 플래시 모두 특정 주소만 핀포인트로 overwrite 할 수 없기 때문에 (NOR 플래시는 chip/sector erase 한 뒤 write 해야 하고, NAND도 block단위로 erase하고 다시 write 합니다.) soft breakpoint랑 플래시 메모리랑은 상관 없지 않나라는 의문이 들었습니다.
    • [※] 구글링 해보니 soft breakpoint는 SW Breakpoint라고 부르는 것 같습니다.

2.1.2. On-chip Breakpoint (HW Breakpoint)

  • On-chip breakpoint는 이름 그대로 ICE breakpoint 레지스터에 개발자가 직접 breakpoint를 걸고 싶은 주소를 등록한다.
  • ICE breakpoint 레지스터는 현재 PC값과 등록된 주소를 실시간으로 비교해서 일치할 때 ARM core를 멈춘다.
  • On-chip breakpoint는 3가지 기능을 제공한다.
    1. Instruction breakpoint: 개발자가 원하는 소스 라인에 breakpoint를 설정하는 기능.
    2. Memory Read/ Write breakpoint: Global 변수, 특정 메모리 주소, peripheral 주소에 write/ read 할 때 멈추는 기능. 주로 LDR/ STR 명령어를 수행할 때 멈춘다.
    3. Data value breakpoint: Global 변수, 특정 메모리 주소, peripheral 주소에 개발자가 원하는 값이 write 될 때 멈추는 기능.

2.1.3. ICE Breakpoint Register

  • ARM core마다 제공되는 ICE breakpoint 레지스터 개수가 다르다.
    • ARM7, ARM9는 2개 watchpoint를 제공한다. 따라서 on-chip breakpoint를 2개 밖에 사용 못 한다.
    • ARM11, Cortex-A8, Cortex-R4는 6개 watchpoint를 제공한다.
    • Cortex-M3는 4개 watchpoint를 제공한다.

2.1.4. Breakpoint 사용법

HW 디버거를 사용해서 breakpoint를 설정하는 방법은 다음과 같다.

  1. Soft breakpoint & On-chip breakpoint 설정하기
Break.Set 0x3003D7E8			// Instruction Soft breakpoint
Break.Set 0x3003D7E8 /onchip	// Instruction Onchip breakpoint
Break.Set 0x3003D7E8 /write		// Write Onchip breakpoint
Break.Set 0x3003D7E8 /read		// Read Onchip breakpoint
Break.Set 0x3003D7E8 /readwrite	// Read & write Onchip breakpoint
Break.Set 0x3003D7E8 /data 0x10	// Data value Onchip breakpoint
  1. Global 변수에 breakpoint 설정하기
Var.Break.Set 변수이름 /write
Var.Break.Set 변수이름 /read
  1. 특정 주소 구간에 breakpoint 설정하기
Break.Set 0x0--0x1000
Break.Set 0x0--0x1000 /write
Break.Set 0x0--0x1000 /read

2.2. HW 디버거의 기타 기능들

([※] 2.1.4절에서 HW 디버거의 사용법에 대해서 다뤄봤습니다. 이번 절에서는 HW 디버거가 할 수 있는 기능에 대해 조금 더 자세히 배워보도록 하겠습니다.)

2.2.1. ARM core 제어

  • 바이너리가 메모리에 없어도 HW 디버거를 사용하면 ARM core를 직접 제어할 수 있다.
  • 예를 들어, 두 정수 a, b를 덧셈하고 싶을 때 C프로그램으로 짠다면 아래와 같다.
#include <stdio.h>

int main() {
	int a = 5, b = 5, add= 0;
	add = a + b;
	return 0;
}
  • 원래는 위 C프로그램을 컴파일하고 바이너리로 만들어 SDRAM에 올려 CPU가 어셈블리 명령어를 수행하도록 해야하지만, HW 디버거를 사용한다면 직접 ARM core의 레지스터를 조작해서 덧셈을 할 수 있다.
; exam.cmm
Register.Set R2 0x5		; a = 5
Register.Set R3 0x5		; b = 5
Data.Set 0x3003D7EC	%long R(R2)			; 메모리 주소에 a값 저장
Data.Set 0x3003D7E8 %long R(R3)			; -0x4 주소에 b값 저장
Data.Set 0x3003D7E4 %long R(R2)+R(R3)	; -0x4 주소에 a+b값 저장

2.2.2. LED 제어 (Peripheral 제어)

  • 타겟 보드에 연결돼있는 LED를 C프로그램 작성 없이 제어해보자.
  • 현재 LED는 3.3V VCC에 연결돼있으므로 LED가 연결된 핀에 0을 set해서 GND로 만들면 LED가 켜질 것이다.

  • MCU의 데이터시트를 보자. LED는 포트 B의 2, 3, 4번 핀에 연결돼있다고 가정하자.
  • [※] 데이터시트에서 Port B control 레지스터 부분을 확인해봅시다.
    • [※] 포트 B의 입출력 방향을 제어하는 레지스터를 GPBCON이라 부르고 0x5600_0010번지에 있습니다.
    • [※] GPBCON의 [9:4] bits를 010101로 설정하면, 2, 3, 4번 핀의 입출력 방향이 ‘OUTPUT’이 됩니다.
    • [※] 포트 B의 입출력 값을 제어하는 레지스터는 GPBDAT이라 부르고 0x5600_0014번지에 있습니다.
    • [※] GPBDAT의 원하는 bit에 0을 설정하면 GND로 처리 돼 해당 핀과 연결된 LED가 점등합니다.
  • [※] 위 과정을 수행하는 C프로그램과 HW 디버거 명령어를 살펴보고 서로 비교해봅시다.
#define rGBPCON	(*(volatile unsigned *) 0x56000010)
#define rGBPDAT (*(volatile unsigned *) 0x56000014)

unsigned int ledData;
void Delay(unsigned short time);

void testLED() {
	rGBPCON = (rGBPCON & ~(0x3F<<4) | 0x15<<4);
	
	ledData = 0xFFFFFFFB;
	rGBPDAT = ledData;	// GBP2 pin
	Delay(100);
	
	ledData = 0xFFFFFFF7;
	rGBPDAT = ledData;	// GBP3 pin
	Delay(100);
	
	ledData = 0xFFFFFFEF;
	rGBPDAT = ledData;	// GBP4 pin
	Delay(100);
	
	ledData = 0xFFFFFFFF;
	rGBPDAT = ledData;	// Clear GBP pins
	Delay(100);
}
; LED_test.cmm

B::
PER.Set AD:0x56000010 %LONG 0x150	; GPBCON에 0x150 write
PER.Set AD:0x56000014 %L 0xFB		; GPBDAT의 GPB2에 GND
wait 100.ms
PER.Set AD:0x56000014 %L 0xF7		; GPBDAT의 GPB3에 GND
wait 100.ms
PER.Set AD:0x56000014 %L 0xEF		; GPBDAT의 GPB4에 GND
wait 100.ms
PER.Set AD:0x56000014 %L 0xFF		; GPBDAT clear
wait 100.ms
ENDDO
  • [※] C언어 프로그램에 대한 부가설명 하겠습니다.
    • [※] rGBPCON = (rGBPCON & ~(0x3F<<4) | 0x15<<4);은 GBPCON의 9, 7, 5번 bit를 0으로 만듭니다.
      • [※] ~(0x3F<<4)0011_1111_0000이고 0x15<<40001_0101_0000입니다.
      • [※] 두 수를 OR연산하면 1101_0101_1111이 되겠지요? 얘를 모든 bit가 1인 GBPCON과 & 연산 한다는 건 특정 bit를 0으로 만들겠다는 의미와 같습니다.
      • [※] 따라서 9, 7, 5번 자리 bit가 0이 돼 GBP 2, 3, 4번 pin이 OUTPUT으로 설정되겠네요.
      • [※] 근데 왜 0x3F0x15를 사용했는지는 잘 모르겠습니다. 바로 1101_0101_1111 = 0xD5F랑 & 연산 했으면 된다고 생각하거든요.
    • [※] GBPDAT에 순서대로 0xFFFF_FFFB, 0xFFFF_FFF7, 0xFFFF_FFEF를 쓰는걸 볼 수 있습니다.
      • [※] 0xFFFB = 1111_1111_1111_1011이니 2번 bit가 0이 되서 GBP2가 GND 되겠네요.
      • [※] 0xFFF7 = 1111_1111_1111_0111이니 3번 bit가 0이 되서 GBP3가 GND 되겠네요.
      • [※] 0xFFEF = 1111_1111_1110_1111이니 4번 bit가 0이 되서 GBP4가 GND 되겠네요.
  • [※] 위 과정을 HW 디버거로 하면 위 코드와 같습니다.
    • [※] 한 가지 의문은 PER.Set AD:0x56000010 %LONG 0x150에서 왜 0x150을 write 하는지 모르겠습니다. GBPCON은 모든 bit가 1로 초기화 된 상태라 0x150=0001_0101_0000을 write 하면 안되고 원하는 bit 이외에는 1로 set 된 1101_0101_1111 = 0xD5F를 write 해야 한다고 생각하기 때문입니다.

2.2.3. 특정 변수값 변경

  • 만일 특정 변수값을 변경하고 싶다면, 소스코드를 수정하고, 재컴파일 하고, 플래시메모리에 바이너리를 재다운로드 하는 귀찮은 과정을 거쳐야 한다.
  • 하지만, 반드시 소스코드와 어셈블리 명령어가 같아야만 시스템이 동작할까?
  • 아니다! 소스코드는 개발자를 위한 것이지 결코 ARM core를 위한 것이 아니다!
  • ARM core는 오직 어셈블리 명령어만 관심이 있기 때문에 소스코드를 고치지 않고 현재 플래시메모리 또는 메모리에 올라간 바이너리 및 어셈블리 명령어만 수정하면 된다.
DATA.ASSEMBLE 0x00000480 MOV R3, #0x7
DATA.ASSEMBLE 0x00000488 MOV R3, #0x9
  • HW 디버거로 위 명령어를 입력하면, 지정한 주소의 명령어가 위와 같이 바뀌게 된다.
  • 즉, 저 주소를 실행할 때 R3 레지스터에 들어있는 값이 바뀌게 되는 것이다.

2.2.4. 특정 라인 주석 처리

  • 소스코드의 특정 라인을 수행하고 싶지 않을 때가 있다면, 주석 처리를 한 후 재컴파일 해야 할 것이다.
  • 하지만, ARM 명령어 중 ‘NOP’을 사용한다면 주석 처리를 한 것과 같은 효과를 낼 수 있다.
  • DATA.ASSEMBLE 0x00000480 NOP명령어로 특정 주소의 명령어를 NOP으로 덮어씌워 해당 라인의 명령어를 수행하지 않도록 할 수 있다.

2.2.5. 메모리에 있는 데이터를 복사

  • 0x0~0xC번지 데이터를 0x2000으로 복사하고 싶다면 HW 디버거로 다음 명령어를 사용하면 된다.
  • DATA.COPY 0x0--0xC 0x2000
  • 또한, 덤프 관련 명령어도 제공된다. DATA.SAVE.BINARY 0x0--0x1000 C:\DUMP.bin라고 입력하면 C드라이브에 DUMP.bin이라는 바이너리 파일이 덤프될 것이다.

2.3. 변수는 어떻게 저장되는가

  • 개발자는 배열과 포인터 변수들이 메모리에 어떻게 할당되는지를 알고, 메모리 구조도 잘 파악하며 프로그램을 만들 수 있는 인재가 돼야 한다.
  • 평소에 알고도 얼떨결에 지나쳤던 배열과 포인터에 대해 HW 디버거로 분석해보자.

2.3.1. 배열

  • 배열에 저장할 값의 범위를 알고 있다면, 메모리를 단축할 수 있다.
  • char arr[2][2][2]int arr[2][2][2]는 둘 다 8개의 데이터를 저장할 수 있지만, HW 디버거로 검사하면, 전자는 0x3002_0000부터 16B만 할당되지만, 후자는 32B가 할당된다.
  • 따라서 만일 배열에 0x00~0xFF값이 들어간다면 char형으로 선언하는 게 메모리를 적게 쓸 수 있다.

2.3.2. 포인터

  • int* pint *p는 같은 표현이고 개발자 개인의 취향이다.
char str[] = "Embedded_C";
char* pChar = str;

char c1 = *(++pChar);    // (A)
char c2 = *(++pChar);    // (B)
char c3 = *(pChar+1);    // (C)
char c4 = *(pChar+2);    // (D)
  • 위 프로그램이 실행되면 각 변수에는 어떤 값들이 저장될지 HW 디버거로 알아보자.
  • pChar는 문자열 str의 시작주소를 가리키고 있다.
  • 변수 c1pChar이 char형의 크기만큼 주소가 증가한 곳의 문자(m)가 저장된다.
  • 변수 c2pChar이 char형의 크기만큼 주소가 증가한 곳의 문자(b)가 저장된다.
  • 변수 c3pChar이 가리키는 주소로부터 1×(char형 크기)만큼 떨어진 곳의 문자(e)가 저장된다.
  • 변수 c4pChar이 가리키는 주소로부터 2×(char형 크기)만큼 떨어진 곳의 문자(d)가 저장된다.
  • 이 예제를 통해 *(++pChar)*(pChar+1)의 차이를 알자.

2.3.3. 포인터의 포인터

unsigned char *pChar;
unsigned char **ppChar;
unsigned char ***pppChar;
const char cstr[] = "Embedded";

void foo() {
    pChar = (unsigned char *)&cstr[0];
	ppChar = (unsigned char **)&pChar;
	pppChar = (unsigned char ***)&ppChar;
}
  • 위 함수가 실행된 뒤 HW 디버거로 변수와 주소를 살펴보면 다음과 같다.
    • pChar0x3002_0124번지 변수이며 내부에 cstr의 시작주소인 0x0001_00D0를 갖고 있다.
    • ppChar0x3002_0120번지 변수이며 내부에 pChar의 주소인 0x3002_0124를 갖고 있다.
    • pppChar0x3002_011C번지 변수이며 내부에 ppChar의 주소인 0x3002_0120을 갖고 있다.
    • 결국 세 변수 모두 0x0001_00D0번지의 E를 가리키고 있다.
  • 변경하지 않을 문자열은 꼭 const 선언을 통해 불필요한 삽질을 줄이자.

3. 디버깅 기술

3.1. 디버깅을 잘하기 위한 팁

  1. 현재 개발 중인 MCU의 특징에 대해 잘 알고 있어야 한다.
    • 최근 MCU는 멀티프로세서 또는 SMP가 내장된 ARM 프로세서를 사용한다.
      • 멀티프로세서: 서로 다른 ARM core끼리 조합한 것 (i.e. ARM9 + ARM11)
      • SMP: 같은 ARM core끼리 조합해 듀얼코어/ 쿼드코어처럼 사용하는 것
    • RTOS의 경우 멀티프로세서 환경은 각 프로세서마다 따로 포팅하고, SMP는 하나만 포팅하면 된다.
      • 멀티프로세서는 각 RTOS에서 task를 처리하므로 속도가 빠르지만, 각 RTOS마다 어떤 한 task가 프로세서를 사용 중이면 다른 task는 waiting 해야 한다는 단점이 있다.
      • SMP는 한 task가 프로세서를 사용해도 다른 task는 놀고있는 다른 프로세서가 처리하면 되므로 waiting time이 상대적으로 짧다. 같은 ARM core를 사용하기 때문에 사용하는 어셈블리 명령어도 같기 때문에 다른 task도 실행시킬 수 있다. 이때 RTOS는 SMP2(SMP 스케줄링)을 지원해야 한다.
    • 이러한 MCU의 특징을 알고 있어야 개발 및 디버깅 시 큰 도움이 된다.
  2. 현재 개발하는 프로세서의 특징과 어셈블리 명령어를 잘 알고 있어야 한다.
    • 예를 들어, ARM11부터 도입된 ‘trustzone’이라는 기능의 특징을 알고 있어야만 개발 도중에 문제가 생기더라도 디버깅을 할 수 있다.
    • 버전의 발전에 따라 다양해지는 어셈블리 명령어도 많이 알고 있어야 모르는 어셈블리 코드가 나왔을 때 당황하지 않게 된다.
  3. 크로스컴파일러가 어떤 원리로 바이너리를 생성하는지 알아야 한다.
    • .text.data영역이 어느 주소에서 동작하는지 파악해두면 디버깅할 때 문제 접근이 쉬워진다.
    • R13은 .data영역의 주소를, R14, R15(PC)는 .text영역의 주소를 가리키고 있어야 함을 알고 있어야 한다.
  4. 개발 중인 RTOS와 기타 RTOS와의 차이점을 알아야 한다.
    • RTOS의 기본 지식인 task 단위 프로그래밍, 세마포어, 메시지큐 등을 잘 알고 있어야 한다.
    • 각 기능은 RTOS마다 조금씩 다르게 구현 돼 있으므로 차이점도 알고 있어야 한다.
  5. HW 디버거 및 디버깅툴을 잘 사용할 줄 알아야 한다.
    • 툴에서 제공하는 명령어를 많이 알고 응용력을 발휘할 수 있어야 한다.
    • 리셋 같은 문제가 발생했을 때 1~4를 기반으로 HW디버거를 사용해 해당 상황을 재현하고 당시 어셈블리 명령어와 레지스터 상태를 분석할 수 있어야 한다.
  6. 평소 다양한 부서 사람들과 친하게 지내야 한다.
    • 개발자 한 사람이 모든 것을 할 수는 없다.
    • 프로젝트는 모듈별로, 각 모듈은 여러 개발 부서로 나뉘기 때문에 어떤 문제가 발생했다면 우리 부서만 문제를 분석하는 것이 아니라 인근 부서와 함께 문제점을 찾아야 할 일이 종종 생긴다.
    • 워낙 전문가 그룹으로 나누어져 있기 때문에 어려운 문제일수록 접근하기도 힘들고, 또 쉽게 포기하게 된다.
    • 따라서 평소에 여러 부서의 개발자와 친하게 지내야 큰 도움을 받을 수 있다.

3.2. 대표적인 디버깅 상황

3.2.1. Sleep mode 진입 후 일정시간 지나서 wake-up 할 때 리셋 발생

  • Sleep mode에 진입할 때 peripheral의 인터럽트와 파워를 비활성화 한다.
    • 이때 PMIC(Power Management IC) 관련 레지스터를 설정해 사용하지 않는 peripheral에 전력을 공급하지 않는 방법으로 베터리 사용량을 최소화 하게 된다.
    • 하지만, 항상 동작해야 하는 peripheral(모뎀 등) 관련 인터럽트 & 파워는 놔둔다.
  • Wake-up 할 때는 sleep mode에 진입하기 전의 설정값을 그대로 복원해줘야 한다. 이 부분이 제대로 되지 않을 때 상기한 리셋 문제가 발생할 수 있다.

3.2.2. 부팅 시 LCD에 로그 화면이 나오고 무한 재부팅 발생

  • 부팅 시 device driver들이 초기화 될 때 순간적으로 전력소모량이 높아지는 현상이 있다.
  • 초기화 할 peripheral과 device driver의 개수가 늘수록 clock이 불안정해져 무한 리셋 현상이 발생한다.
  • 새로운 peripheral 또는 device를 연결할 때는 반드시 startup.s의 PLL 설정을 새롭게 해주자.
  • 특히 APB([※] peripheral들 연결된 bus를 말해요)관련 clock값들을 새로워진 환경에 맞게 설정해주자.

3.2.3. 특정 key/ button 눌렀을 때 50% 정도만 실행

  • 거의 전압 threshold 관련 문제다.
  • HW 설계가 잘못 돼 threshold 근방의 애매모호한 신호가 발생하는 경우 이런 문제가 생긴다.
  • 캐패시터를 연결하는 등 HW적으로 해결하거나 연속해서 신호를 여러 번 체크해서 SW적으로 해결할 수 있다.

3.2.4. 애플리케이션 동작 때 시스템이 lock-up 되는 경우

  • RTOS를 사용하는 경우, 각 task는 TCB와 stack을 할당받는데, 보통 연속된 주소로 할당된다.
  • 개발자의 코드 실수로 인해 stack 영역을 overflow해서 TCB 영역에 read/ write 하는 경우 TCB 구조체의 데이터가 손상돼 어떤 상황이 발생될지 예측할 수 없는 상황이 발생한다.

3.2.5. printf() 사용 여부에 따라 타겟이 리셋되는 경우

  • printf()는 보통 SW 디버깅을 위해 사용한다.
  • 사용하고, 확인하고, 지우면서 개발 중에 사용하는 건 괜찮다. 하지만, 개발 일정에 쫓기다보면 실수로 놔두고 제품 출시 직전에야 지우는 경우가 있다.
  • 이럴 경우, 이미 제품의 delay는 printf()가 있는 상황에 맞춰져 개발됐기 때문에 printf()가 사라짐으로써 각 task 사이에 균형이 깨져 reset이 언제 발생될지 모르는 예측 불허 상태에 놓인다.
    • Task A와 B가 번갈아가면서 10ms 동안 실행되는데 A에 printf()를 넣고 잊었다고 생각해보자.
    • Task A는 10ms보다 아주 약간 조금 더 긴 시간이 소요될 것이고,
      Task B는 그 시간만큼 A가 끝나기를 아주 조금씩 기다릴 것이다.
    • 시간이 한참 흐르게 되면 이 아주 약간의 시간이 점점 쌓여 커져 나중에는 task B가 task A가 끝나기를 한참을 기다려야 수행되게 된다.
    • Watchdog timer는 이를 타겟이 lock-up된 상황이라 판단해 자동으로 시스템을 리셋하게 된다.

3.3. 고급 디버깅 기술 : Exception Debugging

3.3.1. Mode bit와 Exception 처리

  • ARM core에는 주로 사용하는 6개 mode가 있으며 CPSR, SPSR 레지스터에서 모드 정보를 알 수 있다고 배웠다.
  • 6개 mode에는 임베디드 시스템이 오동작 할 경우, 친절하게 어디서 문제가 발생했는지를 알려 주는 Abort 모드Undefined 모드가 포함된다.

  • 만일, ARM core가 exception이 발생했다고 판다할 경우 위 표의 미리 정의된 주소로 무조건 점프하고 CPSR과 SPSR을 수정하도록 설계 돼있다.
  • Low address, high address 여부는 CP15의 CR 레지스터의 ‘V’ bit에 표현돼 있다.
  1. Data Abort Exception
    • 존재하지 않는 주소/ 접근할 수 없는 주소에 데이터를 read/ write 하려고 시도할 때 발생한다.
      • 예를 들어, 32MB SDRAM의 0x0400_0000번지 주소에 접근하는 경우 abort 된다.
    • Data abort가 발생했을 때 다음과 같은 동작을 수행하게 된다.
      1. SPSR_abt 레지스터에 CPSR 값을 저장(백업)한다.
      2. CPSR 레지스터 값을 변경한다.
        1. CPSR[5] = 0 : ARM 명령어 체제로 전환
          (∵THUMB는 CPSR 제어 불가해 exception 처리 불가능하므로)
        2. CPSR[7] = 1 : IRQ를 비활성화 한다.
        3. CPSR[4:0] = 10111 : Abort mode로 전환한다.
      3. R14(LR_abt) 레지스터에 PC값을 저장한다.
      4. PC를 지정된 주소(0x0000_0010)으로 바꿔 점프한다.
    • 여기서 링크 레지스터에 저장하는 PC값은 abort가 발생한 주소 + 0x8이라는 사실이 중요하다.
    • 왜냐하면, Fetch → Decode → Execute 파이프라인에서 Execute 단계에서 abort가 발생한 시점의 PC값은 2개 명령어 아래의 instruction을 fetch하는 단계에 있기 때문이다. 따라서 이때의 PC값은 abort를 유발한 execute 단계의 명령어보다 0x8위에 있다.
    • 따라서, HW 디버거를 사용할 때 data abort가 발생했다면,
      1. 우선, 링크 레지스터를 확인하고 해당 주소를 덤프해서 살펴본다.
      2. 해당 주소로부터 0x8을 뺀 주소의 명령어가 data abort를 발생시킨 범인이다.
  2. Prefetch Abort Exception
    • 존재하지 않는 주소/ 접근할 수 없는 주소에 대해 fetch 하는 경우 발생한다.
    • Data abort와 거의 비슷한 과정을 수행한다.
    • 이때, data abort 때 링크 레지스터에 저장되는 PC값은 abort 발생 지점 + 0x8번지지만, prefetch abort 때 저장되는 값은 +0x4번지다.
    • 왜냐하면, abort가 fetch와 decode 사이에서 발생하기 때문에 PC값은 1개 명령어 아래에 있기 때문이다.
  3. Undefined Exception
    • Decode 단계에서 instruction을 이해할 수 없어 기계어로 번역할 수 없을 때 발생한다.
    • 컴파일 에러 또는 개발자의 포인터 사용 미숙으로 일부 코드 영역이 깨지는 것이 원인이다.
    • Undefined exception이 발생하면 다음 과정을 수행한다.
      1. SPSR_undef 레지스터에 CPSR 값을 저장(백업)한다.
      2. CPSR 레지스터 값을 변경한다.
        1. CPSR[5] = 0 : ARM 명령어 체제로 전환
          (∵THUMB는 CPSR 제어 불가해 exception 처리 불가능하므로)
        2. CPSR[7] = 1 : IRQ를 비활성화 한다.
        3. CPSR[4:0] = 11011 : Undef mode로 전환한다.
      3. R14(LR_undef) 레지스터에 PC값을 저장한다.
      4. PC를 지정된 주소(0x0000_0004)으로 바꿔 점프한다.
    • Undefined exception 또한 decode 단계에서 발생하므로 PC값은 exception이 발생한 곳의 0x4번지 아래를 가리키고 있다.

3.3.2. Exception Debugging

이번 절에서는 위에서 다룬 3가지 exception의 실제 사례를 통해 디버깅 하는 방법을 살펴보자.

  1. Data Abort exception
    1. 예제 1. Alignment fault
      • HW 디버거를 통해 레지스터 윈도우를 확인하니 abort mode로 진입했음을 알 수 있다.
      • 링크 레지스터(R14) 값 - 0x8을 해보니 STR R3, [R2]명령어를 확인할 수 있다.
      • R2가 가리키는 주소에 R3 값을 write 하는 명령어이고, R2값은 0x3000_0002이므로 xxxx_xxxxDEAD_DEAD가 아니니 존재하는 주소인데도 불구하고 왜 data abort가 발생한걸까?
      • 소스코드 라인을 보니 *one = 0x0304를 수행하다가 abort가 발생했다.
      • 변수 oneunsigned int *형이므로 ARM 컴파일러는 4-byte 단위(0x0, 0x4, 0x8, 0xC)로 할당할 것이다.
      • 그런데 소스코드를 보니 one의 선언 및 초기화 때 unsigned int* one = (unsigned int *) 0x30000002로 끝이 2로 끝나는 주소를 할당했음을 알 수 있다. 즉, 주소의 alignment가 맞지 않아서 ‘alignment fault’가 발생한 것이다.
      • 이런 경우 컴파일 할 때 에러는 발생하지 않지만 warning 메시지는 나온다.
      • ARM9 이후로 형 선언과 주소 할당이 맞지 않을 때 exception 처리를 하는 ‘Alignment Fault Checking Register’ 레지스터(CP15의 C가 있다. 개발자는 이 레지스터를 활성화/ 비활성화 해서 alignment fault를 무시할지 안 할지 설정 할 수 있다.
    2. 예제 2. Permission fault
      • HW 디버거를 통해 레지스터 윈도우를 확인하니 abort mode로 진입했음을 알 수 있다.
      • 링크 레지스터(R14) 값 - 0x8을 해보니 이번에도 STR R3, [R2]명령어 때문에 abort가 발생했음을 알 수 있다.
      • 그러나, 이번에는 변수 star0x3000_0A60으로 alignment도 잘 맞춰 선언했고 중간에 *star = 0x2008로 데이터값을 넣는것도 문제가 없는데 왜 이때 abort가 발생했을까?
      • SPSR을 확인해보자. SPSR이 0x6000_00D0으로 mode bit 부분인 SPSR[4:0]이 10000이므로 0x10인 USR(유저)모드임을 알 수 있다.
      • USR에서 abort가 난 경우 data abort를 의심해볼 수 있는데, 일반적으로 MMU에 의한 권한과 밀접관 관계가 있다.
      • 권한을 확인해보기 위해 MMU page table을 덤프해보자.
        C: 3000_0000 — 300F_FFFF | A: 3000_0000 — 300F_FFFF | 00 | 0010_0000 | P: readwrite, U: readonly문장을 확인해보니, 변수 star이 저장된 주소 영역이 USR모드에서는 읽기전용으로 권한 설정이 된 것을 알 수 있다.
      • ARM core가 write 할 수 없는 주소에 write하려고 시도했으니 data abort가 발생한 것이다.
      • MMU_SetMPT()함수를 사용해서 권한을 readwrite(RW_CNB)로 바꾸던지, 변수 star를 읽고쓰기가 모두 가능한 주소로 선언해서 해결할 수 있다.
  2. Undefined Exception
    1. 잘못된 포인터 사용으로 인한 일부 코드 영역 깨짐
      • ([※] 원본 책 기준 이 부분 코드에 오타가 많습니다. 주의하세요!)
      • HW 디버거를 통해 레지스터 윈도우를 확인하니 UND mode로 진입했음을 알 수 있다.
      • 링크 레지스터(R14) 값 - 0x4를 하니 STC P0, C0, [R0]명령어 때문에 exception이 발생했음을 알 수 있다.
      • STC 명령어는 Co-processor의 레지스터 값을 SDRAM에 저장하는 명령어이고, 단독으로 사용할 수 없는 명령어이므로 undefined exception이 발생한 것이다.
        • [※] STC(Store Co-processor Register to SDRAM) 명령어는 STC Pn, Cn, address꼴로 사용됩니다. n번 co-processor의 n번 레지스터 값을 SDRAM의 지정된 주소에 저장하는 명령어입니다.
        • [※] 하지만, ARM은 Co-processor 관련해서 MCR, MRC 명령어 2개만 표준 명령어로 지원합니다. 따라서 디버깅 용도를 제외하고 STC 명령어를 사용하면 undefined exception이 발생합니다.
      • 따라서, 어떤 이유로 0x3000_0168번지에 잘못된 값이 write 돼 코드가 깨져 STC 명령어로 바뀐 것이라고 유추할 수 있다.
      • HW 디버거를 이용해서 0x3000_0168번지에 write on-chip breakpoint를 걸어보자.
      • Write on-chip breakpoint로 시스템이 멈췄을 때 PC값 - 0x8을 한 주소로 가보니 STR R3, [R2]명령어를 확인할 수 있다. R3는 0xED00_0000이, R2는 0x3000_0168을 갖고 있으므로 R2가 가리키는 주소에 잘못된 값인 R3를 저장하면서 해당 주소의 명령어가 오염된 것이다.
      • 0x3000_0168번지로 가보니 원래 명령어는 STR R3, [R11, #-0x14]임을 확인할 수 있다.
    2. ([※] 예제2도 예제1과 거의 똑같은 시나리오라 스킵하겠습니다.)
  3. Reset exception
    1. 순간적으로 MCU에 충분한 전압이 인가되지 않는 경우의 리셋
      • 보통 PMIC 관련된 코드에서 오류가 있을 가능성이 높다.
      • 본인이 작성한 코드가 아니라면, BSP 제공 업체에 문의해서 해결한다.
      • 해결하기 까다로운 exception이다. 왜냐하면 이미 MCU가 리셋돼 core의 레지스터 값이 남은게 없기 때문이다. 따라서 HW 디버거도 무용지물이 되기 때문에 디버깅하기 어렵다.
    2. Watchdog Timer에 의한 리셋
      • Watchdog timer란, 일정시간마다 주기적으로 watchdog 레지스터에 일정한 값을 설정하지 않을 때 문제가 발생했다고 판단해서 타겟을 자동으로 리셋시키는 timer를 말한다.
      • 따라서 리셋되기 전에 일정시간마다 watchdog timer가 초기화되도록 미리 코드를 만들어야 한다.
      • Watchdog timer에 의한 리셋이 발생하면, 시스템은 무조건 0x0000_0000 또는 0xFFFF_0000으로 점프한다. ([※]High address, low address에 대해서는 위에 설명드렸습니다.)
        1. SPSR_rst에 CPSR 값을 저장한다.
        2. CPSR[5] = 0, CPSR[7:6] = 1 (IRQ/ FIQ 비활성화)
        3. R14에 PC 값 저장한다.
        4. PC를 0x0000_0000으로 설정 후 점프한다.
      • 사람 손이 잘 닿지 않는 곳에 있는 임베디드 시스템의 경우 watichdog timer가 큰 도움이 된다.

profile
임베디드 시스템 공학자를 지망하는 컴퓨터공학+전자공학 복수전공 학부생입니다. 타인의 피드백을 수용하고 숙고하고 대응하며 자극과 반응 사이의 간격을 늘리며 스스로 반응을 컨트롤 할 수 있는 주도적인 사람이 되는 것이 저의 20대의 목표입니다.

0개의 댓글