외부 인터럽트(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