검색결과 리스트
글
디버깅(Debugging)이란 프로그램에서 버그를 찾아 수정하거나 에러를 처리해나가는 과정을 말한다. 일반적으로 소프트웨어 디버깅의 경우 비주얼 스튜디오 등의 툴을 활용해 터미널 환경에서 쉽게 디버깅 메세지를 출력할 수 있는 반면 임베디드 환경에서는 외부에 출력하기도 힘들고 하드웨어적인 부분까지 같이 고려해야 하므로 다소 어렵다. 때문에 하드웨어를 디버깅 할때는 JTAG 등의 전용 디버깅 장비를 활용하여 프로세서를 특정 시점에서 멈추게 하거나 각각의 명령어 단위로 하나씩 실행시키는 방법으로 진행하는데 이러한 방식을 하드웨어 디버깅, 혹은 소스 레벨 디버깅(Source Level Debugging)이라고 한다.
이번 포스팅에서는 프로테우스 툴을 활용하여 전에 사용했던 외부 인터럽트 코드와 회로를 가지고 소스 레벨 디버깅하는 방법에 대해 다뤄보겠다.
1. 필요 파일
앞서 소스 레벨 디버깅을 위해서는 JTAG 같은 전용 장비가 필요하다고 언급했었다. 하지만 프로테우스에서는 cof 파일이나 elf 파일을 보드에 넣어주면 실제 컴파일러에서 디버깅 하는 것과 유사하게 소스 레벨 디버깅을 할 수 있는 기능을 지원한다. 그렇다면 여기서 cof와 elf 파일이 무엇이길래 위와 같은 일들을 가능하게 하는 것일까?
cof 파일은 COFF(Common Object File Format) 형식의 파일을 말하는 것으로 어셈블러를 거쳐서 만들어진 오브젝트 파일에서 공통된 성질의 것들을 모아 관리하는 포맷을 의미한다. 잘 이해가되지 않는다면 프로그램의 컴파일 과정에 대해 떠올려보자. 개발자가 작성한 .c 파일은 어셈블러에 의해 기계어로 변환된 .o 오브젝트 파일로 생성되고 이렇게 생성된 각각의 오브젝트 파일을 링커가 합쳐서 실행 파일로 만들어진다. 그런데 이렇게 생성된 실행 파일은 수많은 프로그램들이 합쳐져 있기 때문에 프로그램 메모리에 일일이 분산시켜 옮기는 과정이 매우 번거롭고 많은 시간이 필요하는 문제점이 있다. 때문에 이러한 문제를 해결한 것이 바로 COFF 형식이다.
COFF는 합쳐진 모든 프로그램과 데이터를 각각의 세션별로 구분하는 역할을 한다. 이 세션에는 컴파일된 코드가 들어가는 프로그램 코드 세션, 데이터가 저장되는 상수 데이터 세션 및 사용자 정의 데이터 세션 등 공통된 성질을 가지는 것들이 분류별로 나뉘어지며, 이로 인해 세션 별로 관리하기 용이하고 각 세션이 배치된 메모리의 영역과 크기도 확인할 수 있다. 또한 단순하게 기계어로 되어있는 HEX 파일에 비해 여러가지 프로그램에 대한 정보를 가지고 있기 때문에 디버깅에 유용하게 사용할 수 있다.
ELF (Executable and Linking Format) 의 개념도 마찬가지로 COFF와 비슷하며 단지 기존 형식의 단점을 개선하여 좀 더 복잡하고 그만큼 유연하게 동작할 수 있는 형식이다. 사실 임베디드 환경에서 COFF가 강력하기는 하지만 지금 사용하기에는 너무 오랜된 형식이며 컴파일러에 따라 해석이 조금씩 달라질 수 있는 문제점이 있기 때문에 가능하면 ELF 형식의 파일을 사용할 것을 권한다. 아래는 ELF 형식의 구성도이며 2가지 형태로 나눌 수 있는데, 먼저 왼쪽의 Linkable File은 Link 하기 전의 오브젝트 파일을 칭하고 오른쪽의 Executable File은 Link가 끝나고 실행 가능한 형태의 elf 파일을 말한다.
보다 세부적인 내용들은 그 개념이 방대하기 때문에 따로 검색해볼 것을 추천하며 여기서는 우리가 시뮬레이션할 때 사용했던 HEX 파일과 어떤 차이점이 있는지만 명확하게 알고 넘어가도록 하자.
2. 디버깅 기능
코드비전에서는 기본적으로 cof 파일이, avrstudio에서는 elf 파일이 프로젝트 빌드시 자동으로 생성되며 필자는 코드비전 컴파일러를 사용하기 때문에 cof 파일로 디버깅을 진행하였다. 이전 포스팅에서 사용했던 회로를 가져오고 HEX 파일 대신 cof 파일을 보드에 넣어보자. 그리고 시뮬레이션을 시작한뒤 Debug 메뉴를 살펴보면 다음과 같은 항목들이 나타날 것이다.
이 기능들은 소스 레벨 디버깅에 있어서 많은 도움이된다. 먼저 소스 코드 항목을 눌러보면 신기하게도 우리가 컴파일한 코드가 그대로 프로테우스에서 확인할 수 있다. 왼쪽 숫자는 각 명령어의 주소를 가리키며 순차적으로 PC 레지스터에 저장된다. 여기서 명령어를 하나씩 실행시키면서 프로그램이 수행되는 순서를 모두 추적해볼 수 있고 명령어 라인 위에다 마우스를 더블 클릭하면 BreakPoint가 지정되어 시뮬레이션 수행 도중 해당하는 포인트에서 멈추게 할 수도 있다. 또한 Ctrl + D 단축키를 누르면 해당 소스 코드를 어셈블리어 레벨에서 해석도 가능하다.
다음은 2번째 항목인 Variables 이다. 여기서는 소스 코드에서 전역으로 설정한 변수들의 이름과 주소, 그리고 값을 표시해주며, 마우스 오른쪽 버튼을 눌러서 Add to Watch Window를 클릭하면 시뮬레이션 도중에 Watch Window 창을 통해 해당 변수들의 값이 바뀌는 것을 즉시에 확인해 볼 수 있다. 중간에 .bss 변수의 경우에는 COFF 세션의 한 종류이며 초기화되지 않은 전역 변수들이 들어가는 부분이니 고려하지 않아도 된다.
세번째는 CPU Register 항목으로 ATmega128에서 사용하는 레지스터들의 값들을 확인할 수 있다. 먼저 PC 레지스터에서는 현재 연산중인 명령어의 주소가 나오며, INSTRUCTION 에는 연산중인 명령어가 어셈블리어로 어떤 레지스터에 어떻게 연산되는지 보여준다. 그리고 SREG 레지스터는 현재 상태에 대한 것들을 보여주며, 그 옆에 ITHSVNZC 는 SREG 레지스터 각 비트의 값들을 표시한다. 마지막으로 CYCLE COUNT 는 각 명령어를 얼마나 실행했는지 알려주는 역할을 하게 된다.
간단하게나마 이렇게 주요 디버깅 기능들에 대해 설명해 보았으며 나머지 기능들도 상황에 따라 유용하게 쓰일 수 있으므로 따로 확인해볼 것을 권한다.
3. 디버깅 하기
그러면 이제 위 기능들을 활용하여 프로테우스 툴에서 디버깅을 진행해보도록 하자. 코드와 회로는 모두 전 포스팅을 참고하기 바라며 ATmega128에 cof 파일을 넣고 다음과 같이 세 부분에 Break Point를 설정하여 외부 인터럽트 0번의 동작 과정을 살펴보는 것이 목표이다.
시뮬레이션을 시작해서 외부 인터럽트에 연결된 버튼을 누르고 각 값들이 어떻게 바뀌는지 확인해보자.
가장 먼저 SREG 가 15로 바뀐 것이 눈에 보인다. 이는 외부 인터럽트가 동작할 때 다른 외부럽트가 접근하지 못하도록 설정되어 있기 때문에 자동으로 바뀐 것이며, 만약 중복 인터럽트를 허용하고 싶다면 코드 안에 SREG 레지스터의 I 비트를 다시 1로 설정해주면 가능하다. 또한 INSTRUCTION에 LDR R30, $01 이라는 명령어는 R30 레지스터에 1의 상수를 전송하는 역할을 한다. 이제 다음 Break Point로 넘어가보자.
이번에는 인터럽트 코드 안을 벗어나면서 SREG 레지스터가 다시 허용되었음을 확인할 수 있으며 status 변수에도 1의 값이 쓰여졌다. 이 값들이 어디에 있는지 확인하기 위해 변수 주소를 따라가면 위와 같이 Data Memory 기능에서 각각 500번지와 502번지에 1이 쓰여져 있음을 알 수 있다. 여기서는 501번지가 왜 0인지가 중요한데, 그 이유는 status가 2바이트인 int 자료형이기 때문이며 또한 ATmega128이 Little-endian 방식이기 때문에 하위 비트가 먼저 쓰여져 500번지에는 01이, 501번지에는 00이 쓰여졌다고 알아두도록 하자.
다음으로 넘어가면 temp 에는 0x02의 값이 쓰여지고 LED가 Shift 되게 된다. 그 다음에 status 가 0이 되면서 앞서 설정했던 Break Point가 모두 끝나게 되며 여기서 한가지 재밌는 부분이 status 가 0으로 초기화 될 때의 명령어 주소가 없다는 점이다. 실제로 명령어를 하나씩 실행해 나가면 status 변수가 2일 때의 코드 맨 밑에 있는 명령어로 동작하게 되는데, 필자의 생각으로는 같은 명령어가 중복되기 때문에 컴파일러가 코드를 최적화 하면서 2개의 명령어를 하나로 합치지 않았나 하는 생각이 든다. 정확히는 모르겠으나 재밌는 상황인건 분명하다.
이렇게 ATmega128의 소스 레벨 디버깅 방법에 대해서 프로테우스 툴을 사용하여 알아보았다. 사실 어렵게 보이긴 해도 가장 기초적인 부분만 소개했기 때문에 막상 해보면 별거 아니다라는 생각이 들 것이다. 펌웨어 단에서는 소프트웨어 뿐만 아니라 하드웨어도 매우 중요하기 때문에 이러한 디버깅 방법을 잘 익혀둬서 유용하게 써먹을 수 있기를 바란다.
'Electronic > AVR' 카테고리의 다른 글
외부 인터럽트 (0) | 2017.06.08 |
---|---|
인터럽트 이해하기 (0) | 2017.06.06 |
프로테우스 사용하기 (1) | 2017.06.02 |
설정
트랙백
댓글
글
외부 인터럽트(External Interrupt)는 앞서 확인했던 인터럽트 벡터 테이블에서 RESET을 제외한 가장 우선순위가 높은 인터럽트로, 하드웨어에 가해지는 전압의 변화에 따라 발생하게 된다. 외부 인터럽트가 있으니 내부 인터럽트도 있지 않을까 하고 생각할 수 있지만 사실 내부 인터럽트라는 개념은 없으며 단지 MCU의 I/O 포트를 통해 외부에서 인터럽트를 수신하기 때문에 외부 인터럽트라고 한다.
ATmega128에서는 총 8개의 정해진 핀에서만 사용할 수 있으며 간단한 설정을 통해 바로 사용해볼 수 있으므로 프로테우스 툴을 사용하여 내부적으로 어떻게 동작하는지 확인해보자.
1. 레지스터 설정
외부 인터럽트를 사용하기 위해서는 먼저 각 핀에서의 인터럽트 발생을 허용해야 한다. 이러한 역할을 하는 레지스터가 바로 EIMSK (External Interrupt Mask Register) 이며 각 비트별로 단순하게 어떤 핀을 외부 인터럽트 핀으로 사용할 것인가를 결정한다. 사용하고자 하는 인터럽트가 있으면 해당 비트에 1을 세트시키면 된다.
다음으로 EICRA (External Interrupt Control Register A) 는 이름에서 알수 있듯이 실제 인터럽트가 발생하는 시점을 결정하는 레지스터로 INT0 ~ INT3 까지의 외부 인터럽트 발생 시점을 결정한다. 각 비트들을 살펴보면 인터럽트 별로 두 비트씩 할당되어 있음을 알 수 있으며 나머지 인터럽트는 EICRB 레지스터에 같은 규칙으로 적용되어 있다.
비트 설정에 따라 인터럽트가 발생하는 시점은 아래 표와 같으며, 쓰지 않는 Reserved를 제외하면 총 3가지의 신호를 통해 인터럽트를 제어할 수 있다. 예를 들어 ISC01=1, ISC00=1 로 설정하면 포트 0번을 0V에서 5V로 변할때, 즉 상승 엣지(Rising Edge)에서 인터럽트가 발생하게 된다.
다음은 인터럽트 요청이 발생했을때 해당 비트가 1로 세트되고 인터럽터 종료시 0으로 클리어되는 EIFR (External Interrupt Flag Register) 레지스터이다. 사실 잘 쓰이진 않는 레지스터이며 특이하게도 1이 세트되어 있는 상태에서 다시 1을 써주면 0으로 클리어되는 방식으로 동작한다. 마치 내부적으로 ~(input) 과 같이 동작하는 것 처럼 보인다. 그리고 클리어 되었다고 해도 단순히 해당 플래그가 0으로 바뀔뿐 인터럽트는 이 레지스터와 별개로 계속해서 발생할 수 있다.
활용 예를 간단하게 보여주고 싶은데 찾아봐도 마땅한 것이 안보인다. 정말 특수한 목적이 있지 않은 이상 위 레지스터를 건드릴 이유는 딱히 없을듯 싶다.
마지막으로 모든 인터럽트를 허용하는 레지스터인 SREG (Status Register) 이다. SREG 레지스터에서 전역적으로 인터럽트 발생을 허용하는 비트는 7번 비트이며 나머지 비트들은 산술 연산이나 논리 연산에 사용되는 비트이므로 이 레지스터에 접근할 때 다른 비트들을 건들지 않도록 주의해야한다. 예를 들어 단순히 대입 연산을 해버리면 다른 비트의 값들을 잃어버릴 수 있으므로 SREG |= 0x80 같은 방식으로 7번 비트만 세트시키거나 컴파일러에서 지원하는 함수인 sei 함수를 사용하면 된다.
정리하면 각각의 핀에 따라 개별적으로 인터럽트를 허용하고, 인터럽트의 발생 시점을 설정한 후 모든 인터럽트의 발생을 허용하면 그 시점 부터 인터럽트를 발생시킬 수 있게 된다.
2. 예제 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | #include <io.h> volatile int status = 0; volatile char temp = 0x01; interrupt [EXT_INT0] void ext_int0_isr(void) { status = 1; } interrupt [EXT_INT1] void ext_int1_isr(void) { status = 2; } void main(void) { EIMSK = 0x03; EICRA = 0x0A; DDRB = 0xFF; PORTB = 0x01; #asm("sei") while (1) { if(status == 1) { temp = temp << 1; if( temp == 0x00) temp = 0x01; PORTB = temp; status = 0; } else if(status == 2) { temp = temp >> 1; if( temp == 0x00) temp = 0x80; PORTB = temp; status = 0; } } } | cs |
위 코드는 버튼을 누를 때 마다 불이 켜진 LED 위치를 좌우로 바뀌는 간단한 코드이다. INT0과 INT1을 하강 엣지 신호로 처리하고 있으며, 컴파일러가 자동으로 최적화 시키는 것을 방지하기 위해 2개의 변수를 volatile로 선언하였다.또한 인터럽트 수행시 걸리는 시간을 최소화하기 위해 내부적으로 동작하는 코드를 main에 두고 플래그(flag) 변수만 사용하여 인터럽트의 발생 유무를 확인할 수 있게 했다. 이러한 작업은 단순해 보여도 코드 최적화 부분에서 매우 중요하기 때문에 되도록이면 인터럽트 루틴에 걸리는 시간을 최소화 시키는 것이 좋다.
3. 시뮬레이션
간략히 외부 인터럽트의 개념에 대해 설명해 보았다. 다음 포스팅에서는 프로테우스의 디버깅(Debugging) 기능을 활용하여 외부 인터럽트가 내부적으로 어떻게 동작하는지 확인해보자.
'Electronic > AVR' 카테고리의 다른 글
소스 레벨 디버깅 (0) | 2017.06.14 |
---|---|
인터럽트 이해하기 (0) | 2017.06.06 |
프로테우스 사용하기 (1) | 2017.06.02 |
설정
트랙백
댓글
글
인터럽트(Interrupt)란 프로그램이 동작하는 도중에 발생하는 비정상적인 사건을 말한다. 사실 여기서 비정상적인 사건이란 표현은 너무 딱딱한 어투의 말처럼 들리기 때문에 ‘기존의 흐름과는 다른 일’ 혹은 ‘의도하지 않은 일’, ‘생각하지 못한 문제’ 정도로 순화해서 생각하면 이해하기 편할듯 싶다. 그러면 이러한 인터럽트가 사용되게 된 이유는 무엇일까? 이 물음을 대답하려면 운영체제에 대한 개념을 빼놓을 수 없다.
컴퓨터의 운영체제는 외부 장치의 동작과 자신의 시스템 동작을 보다 수월하게 조정하기 위해 인터럽트를 사용한다. 예를 들어 어떤 프로그램이 동작하고 있는 상황에서 키보드와 마우스 같은 입력이 들어오게 되면 현재 프로그램의 상태를 저장하고 각각의 입력을 구분해서 처리해야 한다. 만약 여러 입력장치가 동시에 동작한다고 해도 데이터를 손실하지 않고 수행해야 하므로 언제 어떤 장치에서 입력이 들어왔는지 일일이 확인할 수 있어야 한다. 이때 사용되는 개념이 바로 인터럽트이며 컴퓨터의 프로세서는 외부 장치의 인터럽트 요청 회선 (IRQ, Interrupt Request Line)을 통해 각 장치의 상태를 확인한다. IRQ는 데이터 제어선의 일종으로 해당 입력이 발생했을 때만 전송되기 때문에 프로세서가 각 장치의 상태를 직접 점검할 필요가 없게 된다.
IRQ가 들어오게 되면 운영체제에서는 해당 인터럽트에 대해 적절한 처리를 하게 되는데 이때 발생 원인에 따라 적절한 처리 루틴을 수행하는 것이 바로 인터럽트 서비스 루틴(ISR, Interrupt Service Routine)이다. 쉽게 말해 운영체제에서 프로그램을 수행하는 도중 IRQ가 발생하면 현재 상태를 저장하고 ISR로 분기하여 해당 인터럽트를 처리하고 다시 돌아오는 과정을 거치게된다. 이를 그림으로 표현하면 다음과 같다.
처리 과정을 간략히 설명하자면 메인 프로그램이 동작하는 도중에 IRQ가 발생했을 때 운영체제에서는 현재 명령어를 종료하고 모든 레지스터의 값을 스택 영역에 저장한다. 이때 PC(Program Count) 레지스터에는 ISR이 처리할 프로그램의 시작 위치를 저장하고 인터럽트 프로그램에 제어권을 넘겨 실행한다. ISR에서 프로그램 수행이 완료되면 스택 영역에 저장해 두었던 레지스터를 복원하고 메인 프로그램의 위치에서 다시 시작하는 것으로 인터럽트 처리가 완료된다.
위 일련의 과정은 마치 서브 루틴의 호출과도 유사해 보인다. 하지만 큰 차이점 하나가 있는데 바로 현 상태 저장 유무이다. 서브 루틴의 경우 호출된 프로그램과 연관된 기능을 수행하고 그대로 종료되지만, 인터럽트는 호출된 프로그램과 관련이 없을 수 있기 때문에 다시 돌아갈 수 있도록 관련 레지스터들을 모두 저장해놓고 분기해야 된다. 이 때 중요한 역할을 하는 것이 바로 PC 레지스터이며, 이 레지스터가 다음 수행할 명령어들의 주소값을 모두 저장해놓기 때문에 인터럽트가 끝나면 안전하게 다시 원래 수행 위치로 돌아갈 수 있다. 따라서 위와 같은 과정을 통해 메인 프로그램은 인터럽트의 영향을 받지 않고 수행을 재개하게 된다.
간략하게 인터럽트의 개념에 대해서 알아보았는데 그러면 MCU를 제어하기 위해 사용하는 인터럽트도 마찬가지로 운영체제의 인터럽트와 같은 개념일까? 사실 같으면서도 약간 다르다고 볼 수 있다. 앞서 운영체제의 인터럽트는 ‘의도하지 않은 일’을 처리하는 것으로, 예를 들어 사용자의 의도치 않은 입력이 발생하거나 잘못된 명령어를 수행하려고 할 때 운영체제가 알아서 처리해주는 소프트웨어적인 개념으로 생각하면 된다. 반면에 MCU를 제어하기 위해 사용하는 인터럽트는 ‘언제든 발생할 수 있는 일’을 ‘미리 설정된 대응 방법’ 대로 처리하는 것을 의미한다. 여기서 ‘발생할 수 있는 일’이란 미리 설정해둔 입력 정도로 이해하면 되고 ‘대응 방법’은 해당 입력에 대해 어떻게 처리할 것인가에 대한 구체적인 해결 방법 정도로 이해하면 좋을듯 싶다.
그런데 두 개의 개념이 별개가 되는 것으로 이해하면 곤란하다. 보다 정확히 말하자면 운영체제는 인터럽트를 모두 포괄하는 개념으로, 예를 들어 컴퓨터의 리셋 버튼 같은 경우에는 MCU의 인터럽트 개념을 내포하고 있다. 위에서는 하드웨어 인터럽트를 좀 더 강조하기 위해 강제로 구분했을 뿐 실상 인터럽트는 운영체제의 탄생과 같이 시작된 개념이기 때문에 여기서는 하드웨어적인 인터럽트의 기본적인 정의만 숙지하고 넘어가도록 하자.
다음으로는 우리가 사용할 MCU인 AVR을 제어하기 위해 사용하는 인터럽트에 대해 살펴보자. ATmega128의 인터럽트 벡터 테이블은 다음과 같다.
위 표는 ATmege128에서 사용할 수 있는 모든 인터럽트의 종류를 나타내며, 보드 종류에 따라 지원하는 인터럽트의 종류와 벡터 주소값이 다르기 때문에 프로그램이 호환되지 않음을 알고있어야 한다.(해당 보드에 맞춰 다시 컴파일을 시켜주면 된다) 여기서 주의 깊게 봐야할 부분들이 몇 가지 있는데 먼저 ATmega128이 8bit 프로세서 임에도 불구하고각 벡터의 주소 크기가 2바이트로 설정되어 있음을 확인할 수 있다. 사실 일반적으로 메모리의 기본 단위는 1바이트인데 ATmega128 에서는 프로그램 메모리의 기본 단위가 1워드(2바이트)이며 주소 또한 1워드 단위로 부여된다. 이는 ATmega128의 모든 명령어 단위가 1워드를 기반으로 하기 때문에 보다 프로그램 처리 효율성을 높이기 위한 측면에서 그러한 것으로 이해하자.
다음으로 인터럽트의 주소는 0x0000 ~ 0x0044 까지 할당되어 있음을 확인할 수 있으며 이 주소의 값은 모두 프로그램 메모리의 주소에 해당 된다. 그리고 각각의 벡터 번호는 우선 순위를 나타내는 것으로 만약 인터럽트가 동시에 걸릴 경우 위 테이블의 번호에 따라 가장 우선 순위가 높은 인터럽트 부터 먼저 처리하게 된다. 테이블을 보면 0x0000에 해당하는 리셋 인터럽트가 가장 최우선 순위임을 확인할 수 있다.
그렇다면 인터럽트가 수행되는 도중에 또다른 인터럽트가 걸리게 되면 어떻게 될까? 8051 같은 MCU의 경우에는 중복 인터럽트가 허용되지만 ATmega128에서는 인터럽트 수행중에는 다른 인터럽트의 접근이 금지되어있다. 따라서 수행중인 인터럽트가 완료된 후 우선 순위에 따라 다음 인터럽트를 처리하게 된다. 물론 이를 인터럽트 처리 코드 안에서 허용할 수는 있지만 이렇게 되면 구현하려는 시스템이 복잡할수록 불안정하고 제어하기 힘들어질 수 있으므로 그다지 권장하지 않는 부분이다.
'Electronic > AVR' 카테고리의 다른 글
소스 레벨 디버깅 (0) | 2017.06.14 |
---|---|
외부 인터럽트 (0) | 2017.06.08 |
프로테우스 사용하기 (1) | 2017.06.02 |
RECENT COMMENT