인터럽트는 편리합니다. 코드를 단순화할 수 있을 뿐 아니라 정밀한 타이밍 제어나 아두이노를 슬립 모드에서 깨우는 데에도 활용할 수 있습니다.
예를 들어 배터리로 작동하는 리모컨 같은 사용자 인터페이스가 있다고 가정해 봅시다. 전력 소모를 줄이기 위해 아두이노(또는 독립형 ATmega)를 파워다운 모드로 전환하고 싶을 것입니다. 아두이노가 파워다운 모드에 진입하면 외부 인터럽트로만 깨어날 수 있습니다. 아두이노 우노에 사용된 ATmega328P 칩에는 외부 핀 인터럽트가 단 두 개(INT0, INT1 - 핀 2, 3)뿐입니다. 그런데 사용자 인터페이스에는 보통 두 개 이상의 버튼이 필요하므로 문제가 됩니다.
일반적인 해결 방법은 모든 버튼을 평소대로 연결하면서 다이오드를 통해 인터럽트 핀에도 연결하는 것입니다. 하지만 이 방식은 회로를 상당히 복잡하게 만듭니다.
표준 외부 인터럽트 외에도 ATmega328P에는 핀 변경 인터럽트가 있습니다. 이를 다루는 라이브러리들이 있으며 이 문제의 좋은 해결책이 됩니다.
그러나 프로그래밍 대회 중에 외부 부품 없이 표준 외부 인터럽트만으로 이 문제를 해결하는 방법을 고안했습니다.
회로 구성
몇 개의 버튼을 사용합니다. 저는 10개를 사용했는데 브레드보드에 딱 맞았습니다(더 이상 없었습니다). 핀당 버튼 하나씩 연결하면 우노에서 최대 20개, 메가에서는 최대 70개까지 가능합니다! (실제로 70개 버튼이 필요하다면 멀티플렉싱을 권장합니다. 그만한 용도로 메가를 쓸 필요는 없습니다.)
각 버튼의 한 쪽은 임의의 핀에 연결합니다(제 경우 4~13번). 모든 버튼의 다른 쪽은 하나의 인터럽트 가능 핀에 함께 연결합니다(제 경우 2번).
코드
아래 코드를 보드에 업로드하고 시리얼 모니터를 여세요. 버튼을 누르면 해당 번호가 출력됩니다. loop() 함수는 전혀 사용되지 않습니다.
작동 원리
당연히 인터럽트가 사용됩니다. 제 경우 2번 핀에 연결하고 FALLING 모드로 설정했습니다.
다이오드를 쓰지 않으려고 아두이노가 실시간으로 회로를 재구성합니다. 두 가지 모드가 있습니다: 공통 모드(Common Mode)와 개별 모드(Distinct Mode).
공통 모드
대부분의 시간 동안 회로는 공통 모드에 있습니다. 인터럽트 핀은 INPUT_PULLUP으로, 나머지 핀들은 OUTPUT 및 LOW로 설정됩니다.
void configureCommon() {
pinMode(commonPin, INPUT_PULLUP);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], OUTPUT);
digitalWrite(buttonPins[i], LOW);
}
}
공통 모드에서 아무 버튼이나 누르면 인터럽트 핀이 LOW로 당겨지며 인터럽트가 발생합니다. 인터럽트 서비스 루틴(ISR)이 실행되면 핀들을 개별 모드로 재구성합니다.
개별 모드
인터럽트가 트리거되면 빠르게 개별 모드로 전환합니다.
개별 모드는 공통 모드의 반대입니다. 인터럽트 핀이 OUTPUT 및 LOW가 되고, 나머지 핀들은 INPUT_PULLUP이 됩니다.
void configureDistinct() {
pinMode(commonPin, OUTPUT);
digitalWrite(commonPin, LOW);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
}
}
개별 모드에서는 실제로 눌린 버튼에 해당하는 핀만 LOW로 당겨집니다. 모든 핀을 검사해 어떤 버튼이 인터럽트를 발생시켰는지 쉽게 알 수 있습니다.
확인이 끝나면 아두이노는 다시 공통 모드로 돌아가 다음 인터럽트를 기다리거나, 애플리케이션에 따라 개별 모드에 머물며 일반적인 사용자 입력을 처리하다가 슬립 모드 진입 전 공통 모드로 전환할 수도 있습니다.
더 복잡한 예제: 서보 제어 및 슬립 모드
서보 모터를 연결하고 각 버튼을 서로 다른 각도(1=0°, 2=20°… 10=120°)에 매핑해 보겠습니다. 아두이노는 배터리로 구동합니다.
이 예제에서는 5초간 입력이 없으면 아두이노를 파워다운 모드로 전환해 전력을 아낍니다. 슬립 모드 관련 튜토리얼은 온라인에서 찾아볼 수 있습니다. 또한 서보에 트랜지스터를 통해 전원을 공급하여 사용하지 않을 때 차단합니다.
이 예제의 코드도 아래에 첨부되어 있습니다.
코드
시리얼 로거 (기본 예제)
const int commonPin = 2;
const int buttonPins[] = {4,5,6,7,8,9,10,11,12,13};
unsigned long lastFire = 0;
void setup() {
configureCommon();
attachInterrupt(digitalPinToInterrupt(commonPin), pressInterrupt, FALLING);
Serial.begin(9600);
}
void loop() { /* 비워둠 */ }
void pressInterrupt() {
if (millis() - lastFire < 200) return; // 디바운스
lastFire = millis();
configureDistinct();
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
if (!digitalRead(buttonPins[i])) {
press(i);
}
}
configureCommon();
}
void configureCommon() {
pinMode(commonPin, INPUT_PULLUP);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], OUTPUT);
digitalWrite(buttonPins[i], LOW);
}
}
void configureDistinct() {
pinMode(commonPin, OUTPUT);
digitalWrite(commonPin, LOW);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
}
}
void press(int button) {
Serial.println(button + 1);
}
서보 + 슬립 모드 예제
#include <Servo.h>
#include <avr/sleep.h>
#include <avr/power.h>
const int commonPin = 2;
const int buttonPins[] = {4,5,6,7,8,9,10,11,12,13};
const int servoEnablePin = A1;
const int servoPin = A0;
Servo servo;
unsigned long lastFire = 0;
int status = 0;
void setup() {
pinMode(commonPin, INPUT_PULLUP);
configureCommon();
attachInterrupt(digitalPinToInterrupt(commonPin), pressInterrupt, FALLING);
servo.attach(servoPin);
pinMode(servoEnablePin, OUTPUT);
}
void loop() {
if (millis() - lastFire > 5000) {
digitalWrite(servoEnablePin, LOW);
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();
}
delay(10);
}
void pressInterrupt() {
sleep_disable();
power_all_enable();
if (millis() - lastFire < 200) return;
lastFire = millis();
configureDistinct();
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
if (!digitalRead(buttonPins[i])) {
press(i);
}
}
configureCommon();
}
void configureCommon() {
pinMode(commonPin, INPUT_PULLUP);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], OUTPUT);
digitalWrite(buttonPins[i], LOW);
}
}
void configureDistinct() {
pinMode(commonPin, OUTPUT);
digitalWrite(commonPin, LOW);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
}
}
void press(int button) {
digitalWrite(servoEnablePin, HIGH);
servo.write(map(button, 0, 9, 0, 180));
}
회로도
첨부된 프리츠िंग 파일 참조: multiinterrupt_hoF76Oc4T5.fzz, servo_with_sleep_u9ZqxF0jhY.fzz