2.3 외부 인터럽트

c{mpy02p03}

 NodeMCU모듈의 16번 핀을 제외한 GPIO핀들에 외부 인터럽트 핸들러를 붙여줄 수 있다. 예를 들어서 GPIO4 핀에 택스위치를 GND와 직결한 다음 풀업레지스터를 연결하여 FALLING EDGE를 검출하는 예제는 다음과 같다.


from machine import Pin
p4 = Pin(4, Pin.IN, Pin.PULL_UP)

# 인터럽트핸들러
# 인터럽트가 발생한 Pin 객체가 넘어온다.
def callback(pin):
   print('falling ',pin)

p4.irq(trigger=Pin.IRQ_FALLING,handler=callback)

먼저 Pin 객체를 입력+풀업저항으로 설정한 후 그 객체의 irq 메서드를 이용하여 인터럽트의 종류와 핸들러를 설정할 수 있다. 그리고 irq()  함수의 trigger 인수는 다음과 같이 세 가지로 설정할 수 있다.


trigger 인수

동작

Pin.IRQ_FALLING

핀이 1에서 0올 떨어질때 handler 호출

Pin.IRQ_RISING

핀이 0에서 1로 올라갈때 handler 호출

Pin.IRQ_RISING | Pin.IRQ_FALLING

두 경우 모두 handler 호출


이렇게 지정하면 해당하는 경우에 handler 인수로 지정해둔 함수가 호출된다. hadler의 인수로는 Pin 객체가 넘어온다.


 주의할 점은 동일한 핀에 IRQ_RISING과 IRQ_FALLING에 각각 다른 handler를 붙여줄 수 없다는 점이다. 하지만 위 표에서와 같이 두 경우 중 하나가 발생했을 때 모두 동일한 handler를 붙여줄 수 있으므로 그 함수 안에서 pin.value() 함수값을 읽으면 어떤 인터럽트가 발생했는지 확인할 수 있으므로 각각을 다르게 처리할 수는 있을 것이다.


 만약 기계식 접점에서 발생하는 인터럽트라면 채터링(chattering)현상으로 인해서 누를 때마다 여러 번의 인터럽트가 발생할기 쉽다. 이러한 채터링 현상을 제거하는 것을 디바운싱(debouncing)이라고 하는데 다음과 같이 간단히 프로그램으로 해결할 수 있다.


from machine import Pin
import time
p4 = Pin(4, Pin.IN, Pin.PULL_UP)

def callback(pin):
   time.sleep_ms(150)
   if pin.value()==0:
       #아래에 실제 처리할 코드를 넣는다.
       print('falling ',pin)

p4.irq(trigger=Pin.IRQ_FALLING,handler=callback)

여기에서 사용하는 방법은 채터링이 사라질 때까지 기다린 다음 핀의 값을 읽어서 여전히 0이라면 FALLING 인터럽트가 실제로 일어났다고 판단하는 것이다. 만약 RISING 인터럽트의 경우는 기다린 후 pin.value()가 아직도 1인지를 검사하면 될 것이다. 채터링이 사라지는  시간은 대락 100~200ms 정도로 알려져 있으므로 기다리는 시간은 적당히 설정하면 된다.


 외부 인터럽트는 실행하고 있는 모든 일을 중단시키고 즉시 handler 함수를 호출하며 메모리 할당 등은 할 수 없다. 또한 handler 함수는 되도록 짧고 간결하게 작성하여야 한다.

(현 시점에서 RISING 인터럽트 동작은 문제가 있는 것 같다.)



Posted by 살레시오

 지시자(directive)인 volatile 은 변수 선언문의 변수형 앞에 두어서 컴파일러가 그 변수를 접근하는 방식을 지정한다.


 일반적으로 변수 데이터는 런타임에 RAM영역에 저장되지만 어떤 변수는 레지스터(register)에 임시로 저장되어 사용하기도 하는데 이 경우 속도면에서는 월씬 유리하지만 RAM에 저장된 실제 데이터와 레지스터에 저장된 (임시)데이터가 서로 다른 경우가 발생할 수도 있다.


 어떤 변수를 volatile로 지정하면 그 변수 데이터는 레지스터의 임시 저장소가 아니라 RAM에서 직접 읽어오도록 컴파일된다. 아두이노 프로그램의 경우 보통은 volatile로 정의할 필요는 없으나 인터럽트 서비스 루틴 (ISR) 내부에서 그 값이 변경되는 변수는 반드시 volatile로 선언해야 실시간으로 변경되는 데이터 값을 ISR 외부에서 정확하게 읽어올 수 있게 된다. 예를 들면 다음과 같다.


// toggles LED when interrupt pin changes state

int pin = 13;
volatile int state = LOW;

void setup()
{
 pinMode(pin, OUTPUT);
 attachInterrupt(0, blink, CHANGE);
}

void loop()
{
 digitalWrite(pin, state);
}

void blink()
{
 state = !state;
}


위의 예에서 blink()함수가 ISR이며 이 안에서 변경되는 변수 state는 volatile로 지정하였음을 확인할 수 있다.

(출처 : https://www.arduino.cc/en/Reference/Volatile )



Posted by 살레시오

 이번에는 스위치와 7세그먼트를 이용하여 1에서 6사이의 숫자를 임의로 표시해 주는 전자주사위를 구현해 보겠다. 처음에는 7세그먼트의 테두리가 빙글빙글 돌고 있다. SW1을 누르면 임의로 1-6사이의 숫자가 빠르게 표시되다가 점점 느려진다. 이 때 숫자가 표시될 때마다 부저가 짧게 울린다. 마지막 숫자가 표시되면 2초 동안 멈추어 있다. 이후 다시 테두리가 돌아간다.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include "Am8USBasp.h"
void _delay_ms_var(uchar ucA);
#define NUMREPEAT 20
volatile uchar ucFlag = 0;
ISR(INT1_vect) {
   ucFlag = 1;
}
int main(void) {
   uchar ucaDice[6] = {0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d};
   uchar ucaCirc[6] = {0x03, 0x06, 0x0C, 0x18, 0x30, 0x21};
   uchar ucA=0, ucPrev, ucB, ucC;
   uint uiSeed = 0;
   InitAM8();
   GICR|=0b10000000; // External Interrupt(s) initialization
   MCUCR=0b00001000; // INT1: Falling Edge
   GIFR =0b10000000; // set interrupt flag as '0'
   sei();
   while(1) {
       if (ucFlag == 0) {
           ucA += (ucA==5) -5:1;
           SEG(ucaCirc[ucA]);
           _delay_ms(100);
           uiSeed++;
       } else { // if (ucFlag = 1)
           srand(uiSeed);
           ucPrev = rand()%6;
           for(ucB=1;ucB<=NUMREPEAT;ucB++) {
               do {
                   ucC = rand()%6;
               } while (ucC==ucPrev);
               SEG( ucaDice[ucC] );
               Beep;
               if (ucB == NUMREPEAT)
                   _delay_ms(2000);
               else
                   _delay_ms_var(ucB);
               ucPrev = ucC;
           }
           ucFlag = 0;
       } // else
   } // while(1)
}
void _delay_ms_var(uchar ucA) {
   do {
       _delay_ms(10);
   } while (--ucA>0);
}


이 예제에서 보면 while(1) 반복문이 크게 두 부분으로 나뉘어 있다는 것을 알 수 있는데 ucFlag==0 일 경우와 ucFlag==1 일 경우이다. 전자의 경우는 7세그먼트의 테두리가 돌아가도록 되어 있고 후자의 경우는 숫자들이 반복되어 표시되면서 주사위 숫자(1~6)중 하나를 표시하도록 되어 있다. 전역변수 ucFlag는 SW1이 눌려지면 ISR()함수 내부에서 1값으로 바뀌게 되어있으므로 volatile 키워드를 붙여서 정의하였음에 유의해야한다.


volatile uchar ucFlag = 0;


그 다음으로 설명할 점은 rand()함수의 사용법인데, 이 함수는 stdlib.h 함수에 정의되어 있으며 호출될 때마다 0 에서 RANDOM_MAX 상수 ( 0x7FFFFFFF 값으로 stdlib.h에 정의되어 있음) 사이의 정수를 임의로 발생시키는 함수이다. 그런데 이 함수는 srand()함수로 초기화를 시킨 이후에 사용을 해야지만 진정한 난수를 발생시킬 수 있다. srand()함수를 임의의 시드값으로 초기화 시키지 않으면 시스템이 재시작될 때마다 똑같은 난수가 발생하므로 진정한 난수라고 할 수 없는 것이다. 보통 PC환경에서는 이 시드값으로 시스템의 시간관련 값을 사용하는데 여기서는 그럴 수 없으므로 uiSeed변수를 두어 이 변수값은 계속 증가하게끔 하였다. 그리고 srand()함수를 초기화시킬 때 uiSeed 변수를 두어서 버튼이 눌려질 때 이 값으로 초기화를 시켜주면 그 시점에서의 uiSeed변수 값은 스위치를 누른 시점에 의존하므로 누를 때마다 달라질 것이다. 이렇게 하여 버튼이 눌려질 때마다 다른 시드값으로 초기화되어 진정한 난수가 발생되게 된다.


 그리고 _delay_ms()함수는 입력으로 상수 값만을 받으며 변수 값을 입력으로 줄 수 없다. 따라서 숫자가 표시되는 시간을 점점 늘려가도록 하는데 사용하기 위해서 _delay_ms_var()함수를 별도로 정의하여 (들어온 변수값 × 10ms) 시간 동안 지연할 수 있도록 작성하였다.


 이 프로그램을 PC에서 컴파일하면 958byte (전체 플래시롬의 약 11%)의 실행파일이 생성되었다. 따라서 ATmega8(A)에 꽉 차는 정도의 길이로 프로그램을 하려면 이 예제의 약 10배 정도 (대충 A4용지 10페이지 분량)는 작성해야 함을 알 수 있다.



Posted by 살레시오

 외부 인터럽트를 실험하기 위해서 INT1핀에 택스위치를 연결한 회로도를 [그림 1]에 도시하였다. 풀업 저항이 AVR내부에 내장되어 있기 때문에 외부에 별도로 풀업 저항을 달아주지 않아서 회로가 굉장히 간단해짐을 알 수 있다. 대신 내부에서 이 두 핀에 풀업 저항을 연결하려면 DDRD3 = '0', PORTD3 = '1'로 설정하여야 한다.


[그림 1] 외부 스위치 회로도


전기적인 접점을 갖는 스위치를 연결하여 사용할 때 꼭 고려해야 될 사항이 있는데 그것은 진동(bouncing) 현상이다. 이것은 전기적인 접점이 닫히거나 열릴 때 그 양단의 전기적인 신호가 깔끔하게 ‘1’에서 ‘0’으로 또는 ‘0’에서 ‘1’로 변하는 것이 아니라 아주 짧은 동안에 접점이 열렸다 닫히기를 반복하는 현상이다. [그림 2]는 접점이 떨어지는 순간의 진동 현상을 보여준다.


[그림 2] 기계적인 접점을 갖는 스위치의 바운싱 현상


이러한 바운싱을 적절히 처리하지 못하면 매우 빠른 속도로 처리되는 uC에서는 스위치가 여러 번 눌려진 것으로 인식하며 이로 인해서 ‘rising edge'나 ’falling edge'에서 인터럽트가 의도하지 않게 여러 번 발생하게 된다. 이러한 기계적인 접점의 진동을 제거하는 것을 디바운싱(debouncing)이라고 하며 크게 하드웨어적인 회로를 이용하는 법과 소프트웨어적인 방법 두 가지로 나뉜다. 많이 사용되는 하드웨어 디바운싱회로를 [그림 3]에 도시하였다.

[그림 3] 디바운싱 회로의 예들 (a) 좌측, (b) 우측


[그림 3]의 (a)회로도를 보면 스위치 양단에 커패시터를 병렬로 달아서 전압 리플을 억제해주는 가장 간단한 회로이다. (b)회로도는 여기에 슈미트 트리거를 추가하여 작은 리플도 제거해주는 회로이다.


 소프트웨어 디바운싱은 하드웨어의 추가 없이 프로그램으로 진동을 제거하는 방식으로 진동이 보통 10ms(길어야 20ms)정도 지속된다는 점에서 착안하여 처음 감지된 이후 일정 시간이 지난 후에도 그 값이 유지가 되는지를 확인하는 알고리즘을 프로그램으로 작성하는 것이다.


 여기에서는 바운싱을 제거하기 위해서 푸시 스위치와 병렬로 1uF의 커패시터를 각각 연결한 가장 간단한 회로를 택하였으며 이 커페시터가 양단의 전압이 급격하게 변하는 것을 어느 정도 억제해 준다.


 내부에 풀업 저항을 연결하였다면 스위치가 안 눌려졌을 때는 INT0/INT1핀으로 ‘1’신호가 입력이 될 것이다. 만약 스위치가 눌려졌다면 이 핀들은 GND에 연결이 되기 때문에 ‘0’신호가 읽려진다. 따라서 스위치를 누르는 순간 1→0으로 변하게 되고 이렇게 변하는 순간을 하강 엣지라고 한다. 반대로 스위치를 누르고 있다가 떼는 순간 0→1로 신호가 바뀌게 되며 이러한 순간을 상승 엣지라고 한다.


스위치 실험 1

 첫 번째 스위치 실험으로 SW1을 누를 때마다 짧게 부저가 울리면서 LED의 상위 니블과 하위니블의 위치가 바뀌는 프로그램을 작성해 보자. 인터럽트는 하강 엣지, 즉 스위치를 누르는 순간에 걸리도록 설정했다.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include "Am8USBasp.h"
volatile uchar ucLed = 0x0F;
ISR(INT1_vect) {
   BuzzOn;
   _delay_ms(10);
   BuzzOff;
   LED( ucLed=~ucLed );
}
ISR(BADISR_vect) {}
int main(void) {
   InitAM8();
   GICR|=0xC0; // External Interrupt(s) initialization
   MCUCR=0x0A; // INT0: Falling Edge, INT1: Falling Edge
   GIFR=0xC0;
   sei();
   LED(ucLed);
   while(1);
}

 이 예제를 보면 먼저 main()함수 내부의 while(1);에서 무한루프에 빠진다는 것을 알 수 있다. 즉, main()함수 내부의 동작은 여기에서 멈추지만 SW0을 누를 때마다 하드웨어적으로 인터럽트가 걸리게 되고 ISR(INT0_vect)함수가 호출이 된다. 이 함수 내부에서 부저를 짧게 울리고 LED출력(PORTB)를 반전시킨 후 다시 main()함수로 돌아와 무한루프를 계속 돌게 된다. 이것이 인터럽트가 운용되는 기본적인 구조이다. 여기에서는 단순히 무한루프에 빠져 있지만 중요한 점은 main()함수 내에서 스위치의 동작을 검출하려는 어떠한 코드도 없다는 것이다.

스위치 실험 2

 두 번째 스위치 실험으로 SW1을 누르면 LED가 위로 한 칸씩 움직이고 끝까지 가면 다시 처음 위치로 되돌아가는 프로그램을 작성해 보자. 이전 실험과 마찬가지로 스위치를 누를 때마다 부저가 짧게 울리게끔 해보자.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include "Am8USBasp.h"
volatile uchar ucLed = 0x01;
ISR(INT1_vect) {
   ucLed = (ucLed == 0x80) 0x01:(ucLed<<1);
   LED(ucLed);
   Beep;
}
ISR(BADISR_vect) {}
int main(void) {
   InitAM8();
   GICR|=0xC0; // External Interrupt(s) initialization
   MCUCR=0x0A; // INT1: Falling Edge
   GIFR=0xC0;
   sei();
   LED(ucLed);
   while(1);
}



Posted by 살레시오

 ATmega8A의 외부 인터럽트를 받아들이는 핀은 [그림 1]에 표시된 바와 같이 두 개가 있다. ‘외부 인터럽트를 받아들인다’는 것은 쉽게 설명하면 이 핀에 걸리는 전압의 변화를 감지하여 그 시점에서 특정한 일을 수행토록 할 수 있다는 의미이다. 예를 들어 INT0핀(4번핀)의 전압이 5V에서 0V로 떨어지는 순간 ISR함수를 호출한다든가 혹은 0V에서 5V로 올라가는 순간 ISR 함수를 실행토록 할 수 있다는 말이다. 이러한 외부 인터럽트 핀을 이용하여 버튼이 눌려지는 것을 감지한다든가 혹은 센서의 측정값이 변하는 것을 감지한다든가 하는 응용을 할 수 있다.


[그림 1] ATmega8(A)의 외부 인터럽트핀


일반적으로 PD2:3핀을 외부인터럽트로 사용하려면 외부 소자에서 신호를 읽어들여야 하기 때문에 방향을 입력(DDRD2='0', DDRD3='0')으로 설정한다. 하지만 방향을 출력으로 설정해 놓아도 인터럽트를 발생시킬 수 있는데 이 경우는 PORTD2 비트나 PORTD3 비트를 조작하여 (즉, 외부 요인이 아니라 내부 프로그램에 의해서) 그 신호 변화에 따라서 내부적으로 인터럽트를 발생시킬 수 있다. 이러한 방식을 ‘소프트웨어 인터럽트’라고 한다.


 관련된 레지스터들로서 일단 MCUCR(MCU Control Register)이 있다.

[그림 2] MCUCR

[그림 2]에서 외부 인터럽트와 관련된 비트는 0번부터 3번까지 4비트이다. ISC01:0 두 비트로 INT0의 동작을 설정하며 ISC11:0 두 비트로 INT1의 동작을 설정한다.


[표 1] 인터럽트 0/1의 발생 제어

ISCn1

ISCn0

동작

0

0

INTn의 low level에서 인터럽트 발생

0

1

INTn의 신호가 변하면 인터럽트 발생

1

0

INTn의 ‘falling edge’에서 인터럽트 발생

1

1

INTn의 ‘rising edge’에서 인터럽트 발생


ISCn1:0='00'이면 INTn핀이 '0'값이면 인터럽트가 무조건 발생한다. 즉, INTn핀이 ‘0’이 되면 ISR(INTn_vect)함수를 호출하고 이 함수가 끝나고 나서도 또 ‘0’이라면 다시 인터럽트가 발생해서 ISR(INTn_vect)함수를 다시 호출하게 된다. ISCn1:0='10'이면 하강 엣지(edge) 에서 인터럽트가 발생하게 되는데 ’1‘에서 ’0‘으로 변하는 순간을 하강 엣지라고 한다. 반대로 ISCn1:0='11'이면 상승 엣지에서 인터럽트가 발생하게 되는데 ’0‘에서 ’1‘로 변하는 순간을 상승 엣지라고 한다. ISCn1:0='01'로 설정하면 하강 엣지와 상승 엣지 두 경우 모두에서 인터럽트가 발생하게 된다. 다음 <그림 6.3.2>에 이들 각각의 경우를 도시하였다. 이 그림에서 하단의 위쪽으로 향하는 화살표는 인터럽트 요구를 표시한다.


[그림 3] 외부 인터럽트 동작 모드


GICR 레지스터의 구조는 다음과 같다.


[그림 4] GICR 레지스터


INT0비트가 세트(‘1’)되고 SREG의 I비트가 세트(‘1’)되었다면 외부 인터럽트 0번은 활성화된다. 마찬가지로 INT1비트가 세트(‘1’)되고 SREG의 I비트가 세트(‘1’)되었다면 외부 인터럽트 1번은 활성화된다.


 외부 인터럽트를 활성화시키기 위한 SFR의 설정을 다음과 같이 요약할 수 있다.

       ( 필요하다면 PD2(PD3)핀에 내부 풀업 저항을 연결한다.)

       ① 인터럽트 0번(1번)의 동작을 MCUCR을 설정하여 정한다.

       ② GICR레지스터의 INT0(INT1)비트를 세트시킨다.

       ③ SREG레지스터의 I비트를 세트시킨다.

이렇게 설정이 완료된 시점 이후부터는 INT0(INT1)핀에서 전기적인 신호가 변하면 인터럽트가 발생하게 된다.



Posted by 살레시오

 AVR툴체인으로 인터럽트를 처리하기 위해서 정해진 형식을 가지는 함수를 작성해야 한다. 어떤 인터럽트가 발생했을 때 이를 받아서 처리하는 함수를 ISR함수라고 하며 이 형식은 <interrupt.h>헤더파일에 정의되어 있다. 따라서 인터럽트를 사용하기 위해서는 이 헤더파일을 반드시 인클루드 시켜줘야 하며 ISR 정의 형식은 다음과 같다.


#include <avr/interrupt.h>
ISR(<vector>) {
   // 함수의 본체
}


여기서 <vector>는 발생한 인터럽트가 어느 것이냐에 따라 미리 정해진 식별자이며 ATmega8A의 경우 다음 표와 같이 정의된다.


[표 1[ ATmega8(A)의 인터럽트 벡터

번호

<vector>

인터럽트 종류

비고

1

INT0_vect

외부 인터럽트 0번

외부

발생

2

INT1_vect

외부 인터럽트 1번

3

TIMER2_COMP_vect

타이머/카운터2 비교매치

내부

발생

4

TIMER2_OVF_vect

타이머/카운터2 오버플로

5

TIMER1_CAPT_vect

타이머/카운터1 캡춰

6

TIMER1_COMPA_vect

타이머/카운터1 비교매치A

7

TIMER1_COMPB_vect

타이머/카운터1 비교매치B

8

TIMER1_OVF_vect

타이머/카운터1 오버플로

9

TIMER0_OVF_vect

타이머/카운터0 오버플로

10

SPI_STC_vect

직렬 전송 완료

11

USART_RXC_vect

USART 읽기(Rx) 완료

12

USART_UDRE_vect

USART 데이터레지스터 비워짐

13

USAER_TXC_vect

USART 쓰기(Tx) 완료

14

ADC_vect

ADC 완료

15

EE_RDY_vect

EEPROM 준비완료

16

ANA_COMP_vect

아날로그 비교

17

TWI_vect

TWI 인터페이스

18

SPM_RDY_vect

프로그램메모리 쓰기 준비완료


AVR-Toolchain에서는 ISR을 작성하지 않은 인터럽트가 발생하면(즉, 인터럽트 발생은 허용시켰는데 해당하는 ISR함수가 없다면) 리셋 벡터로 점프하도록 내부에서 처리하고

있다. 이것을 회피하려면 다음과 같이 ISR함수를 정의하면 ISR함수가 없는 모든 인터럽트를 처리하도록 할 수 있다.


#include <avr/interrupt.h>
ISR(BADISR_vect) {
   // 함수의 본체
}


또한 인터럽트와 관련된 매크로함수로서 sei(), cli() 함수가 있다. sei()함수는 인터럽트 발생을 전역적으로 허용하는 것이고, cli()함수는 반대로 인터럽트의 발생을 전역적으로 허용치 않도록 설정하는 함수이다.


 어떤 경우에는 ISR()함수와 다른 함수 (예를 들어서 main()함수) 들과 변수를 공유할 수도 있다. 이 경우에는 그 변수를 반드시 전역변수로 선언해야 하며, 한 가지 주의할 점은 만약 ISR()내부에서 이 변수가 갱신된다면 반드시 volatile 키워드를 붙여서 선언해야 한다는 점이다. 다음의 예를 보자.


#include <avr/interrupt.h>
int myValue; //<-
ISR(INT0_vect) {
   myValue++;
}
int main(void) {
   ⋮
   while (myValue == 0); // wait for interrupt
       TurnLEDOn;
   ⋮
}


위의 예에서 main()함수의 while()문 안에서는 myValue 변수 값이 변하지 않으므로 무한루프에 빠지게 된다. 이것의 내부적인 동작 방식을 보면 myValue==0 조건을 검사하기 위해서 맨 처음에는 myValue값을 읽어서 레지스터에 저장하고 그 다음 반복부터는 그 레지스터에 저장된 값(맨 처음 읽어들인 값)을 0과 비교하게 되므로 값이 ISR( )함수에서 갱신된 값이 반영이 안 되는 것이다. 따라서 무한루프에 빠지게 되고 여기서 프로그램이 멈추게 된다. 하지만 전역변수 myValue를 다음과 같이 volatile형으로 정의하면 매 반복마다 myValue값을 다시 읽어 들이게 되므로 ISR( )함수 내부에서 갱신된 값이 반영이 된다.


volatile int myValue;


 참고로 AVR은 하드웨어적으로 인터럽트 처리가 시작되면 SREG의 글로벌 인터럽트 프랙 I가 자동적으로 0이 되어 인터럽트처리가 끝날 때까지 추가적인 인터럽트는 발생하지 못하는 상태가 된다.



Posted by 살레시오

 마이크로컨트롤러(uC)가 어떤 작업을 수행하고 있는 도중에 특수한 이벤트가 발생하면 수행 중인 작업을 중단하고 발생한 이벤트를 처리한 후 이전에 수행하던 작업으로 되돌아가 나머지 작업을 계속 처리하게 된다. 이러한 ‘즉시 처리해야 하는 특수한 이벤트의 발생’을 인터럽트(interrupt)라고 한다. 인터럽트가 발생하면 uC는 그것을 처리하는 작업들이 수록된 함수로 명령 수행 과정을 옮기게 되는데 이 함수를 인터럽트 서비스 루틴 (interrupt service routine, 이하 ISR)이라고 한다

.

 인터럽트는 uC시스템에서 굉장히 중요한 비중을 차지한다. 비유를 들어서 설명하자면 어떤 전화기가 있는데 전화가 걸려왔는데도 벨소리도 안 나고 진동도 안 된다고 가정해보자. 이 전화기로 꼭 받아야만 하는 중요한 전화가 올 예정이라면 그 사람은 다른 일은 못하고 몇 초에 한 번씩 전화가 왔나 안 왔나 들어서 확인해 봐야 할 것이다. 하지만 전화가 걸려왔을 때 벨소리가 나거나 진동하는 전화라면 편하게 다른 일을 하고 있다가 전화가 오면 벨소리를 듣고(인터럽트 요구) 전화를 받아 용무를 처리한 후 하던 일을 계속할 것이다.


 이전 포스트에서 사용된 버튼을 가지고 더 설명하면 ‘버튼이 눌려졌다.’라는 특수한 이벤트를 감지하려면 uC가 main()함수 내에서 항상 주기적으로 이 버튼의 상태를 검사했어야 했다. 하지만 이것을 인터럽트로 처리하면 uC입장에서는 이 버튼의 상태를 항상 검사할 필요가 없이 다른 일을 할 수가 있는 것이다. 다른 작업을 하고 있다가 ‘버튼이 눌려지면’ 그 즉시 인터럽트를 발생하여 하던 일을 멈추고 이를 처리할 ISR을 호출한 뒤 ISR이 끝나면 하던 일을 다시 하면 되는 것이다.


 이와 같이 (외장 혹은 내장) 주변기기와 uC가 통신을 수행하는데 있어서 uC가 주변기기의 상태를 메인루틴에서 상시 검사하지 않고 인터럽트로 처리하면 전체적인 시스템의 운용 효율이 매우 높아지게 된다.


[그림 1] 인터럽트의 개념도


 AVR에서 처리하는 인터럽트는 크게 외부 인터럽트와 내부 인터럽트로 나눌 수 있다. 외부 인터럽트는 용어 그대로 외부에서 발생하는 신호이고 내부 인터럽트는 내장된 주변 장치(타이머/카운터, ADC 등등)에서 발생하는 것이다.



Posted by 살레시오

 여기에서는 이전 포스트에 이어서 다른 인터럽트 방식에 대한 디바운싱 방법에 대해서 알아보도록 하겠다.

rising edge 의 경우 디바운싱

 이전과 같이 INT0핀에 택스위치를 연결하고, 이번에는 라이징 에지에 인터럽트가 걸리도록 설정되었다고 하자.


attachInterrupt(INT0, toggleLed, RISING);


앞의 경우와 반대로 ISR의 함수 처음에 일정시간 지연시킨 후 스위치 신호가 LOW일 때 그냥 리턴시키는데 그 이유는 falling edge에서 발생한 것이 때문이다.


void isr() {
   _delay_ms(80);
   if (digitalRead(SW)==LOW) return;
   …
   <실제 인터럽트 처리 루틴>
}


changing edge 의 경우 디바운싱

 이번에는 changing edge, 즉 falling과 rising edge 모두에서  인터럽트가 걸리도록 설정되었다고 하자.


attachInterrupt(INT0, toggleLed, CHANGE);


이 경우는 하나의 에지에서 두 개 이상의 인터럽트가 걸리는 것을 막기 위해서 단순히  지연시키는 것으로 충분하다.


void isr() {
   _delay_ms(80);
   <실제 인터럽트 처리 루틴>
   ….
}


low 인터럽트 의 경우 디바운싱

 마지막으로  low 신호에서  인터럽트가 걸리도록 설정되었다고 하자. 이 인터럽트는 핀 신호가 low 이면 몇 번이고 반복해서 인터럽트가 걸리도록 하려는 의도로 설정되는 것이다. 즉 INT0핀이 low 상태이면 ISR을 수행한 후 그래도 low상태이면 ISR이 바로 다시 수행된다. 이런 식으로 low 상태일 경우에는 계속 ISR이 반복으로 수행된다.


attachInterrupt(INT0, toggleLed, LOW);


따라서 이 경우는 별도의 디바운싱이 필요치 않다.


아두이노 강좌 전체 목록 (TOP) >>>

C++ 언어 전체 강좌 목록 >>>

c{ard},n{ad018}

Posted by 살레시오

 택스위치에 대해서는 이전 포스트에서 기본적인 내용을 다루었다. 여기에서는 인터럽트를 이용해서 스위치를 누를 때마다 LED가 토글(toggle)되는 프로그램을 작성하도록 하겠다. 즉, 스위치 신호의 폴링 에지 (falling edge)에서 LED의 출력 신호가 반전되도록 하는 프로그램을 작성하도록 하겠다.


터치센서와 다르게 기계적인 접점을 갖는 스위치는 점점이 붙거나 떨어지는 순간에 바운싱(bouncing) 현상이 있다. 이것은 접점이 붙거나 떨어지는 그 짧은 순간에 접점이 고속으로 여러 번 on/off 되는 현상을 말하며 기계적인 스위치라면 반드시 발생하는 현상이다. 이것은  짧은 순간(약 100ms 이내임)에 여러 번 접점이 붙었다가 떨어지는 것을 반복하므로 의도하지 않게 인터럽트가 여러 번 발생할 수 있으므로 이러한 바운싱 현상을 제거해야 올바르게 동작하는데 이것을 디바운싱(debouncing)이라고 한다.


 다음 그림에서 하단의 그림이 바운싱을 도시한 것이고 이것을 제거한 깨끗한 신호가 상단에 도시되어 있다.


[그림 1] (a) 디바운싱 (b) 바운싱 현상


<- 디바운스된 신호




<- 기계적 접점의 바운싱 현상


 디바운싱하는데 여러 가지 방법들이 알려져 있는데 크게 두 가지로 분류할 수 있으며 하드웨어적인 방법과 소프트웨어적인 것이다. 하드웨어적인 방법은 RC 저역 필터를 슈미트트리거나 디지털 인버터에 직결한 후 이 신호를 포트 핀에 연결하는 방법인데 아래의 회로도가 대표적인 것이다.


[그림 2] 디바운싱 회로

대표적인 디바운싱 회로도


RC 필터와 슈미트트리거가 직결되어 있다. 이 회로도를 사용하면 아두이노의 내부 풀업 저항은 사용하지 못한다.


 하지만 아두이노와 같은 프로세서가 사용된다면 부가적인 디바운싱 회로 없이 소프트웨어적으로 이것을 구현하여 불필요한 인터럽트를 억제할 수 있다. 여기에서는 그 방법에 대해서 설명하도록 하겠다.  기본적인 아이디어는 ISR 함수 안에서 바운싱이 없어질 때 까지 기다렸다가 스위치 신호를 다시 읽어서 정상 호출된 것인가 아닌가를 판별하는 것이다.

falling edge 의 경우 디바운싱

 만약 INT0핀에 택스위치를 연결하고  폴링 에지에 인터럽트가 걸리도록 설정되었다고 가정하자.


attachInterrupt(INT0, toggleLed, FALLING);


 ISR 함수 처음에 바운싱이 안정될 때 까지 일정시간(80ms 정도가 적당하다.) 지연시킨 후 다음과 같이 스위치 신호를 읽은 다음 그것으로 올바른 인터럽트 호출인가를 판단한다.


void isr() {
   _delay_ms(80);
   if (digitalRead(SW)==HIGH) return;
   <실제 인터럽트 처리 루틴>
   ,,,
}


 이 코드를 보면 80ms 지연시킨 이후에 스위치 값을 읽어 HIGH라면 잘못 호출된 것으로 판단하고 아무 일도 않고 리턴한다. 즉, 이 경우는 rising edge의 바운싱 구간에서 호출된 것으로 판단하는 것이다. 반대로 80ms 지연시킨 이후에 스위치 값이 LOW라면 falling edge에서 정상 호출된 것으로 간주하고 이후에 있는 인터럽트 처리 루틴을 실행시키는 것이다.


 이 방법은  손으로 버튼을 조작하는 경우 정밀한 제어가 필요치 않으므로 필자의 경험상 너무 빠르게 버튼을 동작시키지 않는 한 대부분 정상 작동한다.


 이 설명에서 처음 소개하는 함수가 있는데 바로 _delay_ms() 함수이다. 이름을 보면 지연시키는 함수라는 것을 알 수 있을 텐데 왜 아두이노의 표준 함수인 delay() 함수 대신 이 함수를 사용했는지 의문이 들 것이다. ISR함수 안에서 아두이노의 시간관련 함수인 다음 함수들은 정상 동작하지 않는다.(주의!)


delay(), delayMicrosecond(), millis(), micros()


이 함수들은 내부적으로 타이머 인터럽트를 사용하므로 ISR안에서는 작동할 수 없다. 이유는 ISR이 호출되는 순간 인터럽트가 자동으로 막히고 ISR의 끝나야 인터럽트가 다시 가능하게 되기 때문이다. avr에 대한 지식이 있다면 이해가 쉽게 가겠지만 일단은 위 함수들은 ISR 안에서 정상 동작하지 않는다고만 알아도 된다. 이것은 필자가 구글링을 해 보고 직접 실험으로 확인한 사실이다.


 반면에 _delay_ms()함수는 단순히 소트프웨어적으로 지연을 시켜주는 함수라서 ISR안에서도 정상 작동한다. 단, 이 함수를 사용하기 위해서는 다음과 같이 헤더 파일을 인클루드 시켜야 한다. (avr-gcc에서 사용되는 유틸리티 라이브러리이다.)


#include<util/delay.h>


전체 프로그램은 다음과 같다.


#include<util/delay.h>
#define SW 2

void setup() {
         pinMode(LED_BUILTIN, OUTPUT);
         pinMode(SW, INPUT_PULLUP);
         attachInterrupt(INT0, toggleLed, FALLING);
}

void loop() {
}

void toggleLed() {
 _delay_ms(80);
 if (digitalRead(SW)==HIGH) return;
 digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}


 스위치의 디바운싱은 고전적이만 해결하기 꽤 까다로운 문제이다. 하지만 여기서 소개한 방법을 사용하면 대부분의 경우 잘 동작한다.


아두이노 강좌 전체 목록 (TOP) >>>

C++ 언어 전체 강좌 목록 >>>

c{ard},n{ad017}

Posted by 살레시오

 이번에는 터치 센서를 한 번 터치하면 LED가 켜지고 다시 터치하면 LED가 꺼지도록 동작하는프로그램을 작성하도록 하겠다. 여러 방법이 있겠지만 인터럽트(interrupt)를 이용하는 것이 가장 일반적이다.


 다음 프로그램은 우노의 11번 핀에 부저가 달렸고 3번 핀에 터치센서가 달렸다고 가정하고 작성한 예제이다.


#define TS 3
#define BUZ 11
void setup() {
   pinMode(LED_BUILTIN, OUTPUT);
   pinMode(BUZ, OUTPUT);
   pinMode(TS, INPUT);
   attachInterrupt(INT1, toggleLed, RISING);
}
void loop() {
   digitalWrite(BUZ, HIGH);
   delay(50);
   digitalWrite(BUZ, LOW);
   delay(450);
}
void toggleLed() {
   digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN) );
}


 setup()함수에서 attachInterrupt(INT1, toggleLed, RISING) 를 수행하여 INT1 핀(3번 핀)에 toggleLed()함수를 붙였다. 이 ISR 함수는 rising edge 에서 호출된다.


 그리고 loop() 함수에서는 부저를 0.5초마다 한 번씩 울리는 일 외에는 하지 않는다. 터치센서의 값을 읽는 코드는 어디에도 없다는 것에 주목해야 한다. 터치 센서의 출력값이 0에서 1로 변하는 순간 함수 toggleLed()가 (하드웨어적으로) 자동으로 호출된다. 그러면 main()함수에서 빠져나와서 toggleLed()함수를 수행한 후 다시 돌아와서 계속 수행하게 된다.


아두이노 강좌 전체 목록 (TOP) >>>

C++ 언어 전체 강좌 목록 >>>

c{ard},n{ad016}

Posted by 살레시오