아두이노를 처음 시작하면 누구나 한번은 보게 되는 가장 쉬운 프로그램(아두이노의 ‘hello world’)는 아마도 아래 코드일 것이다.
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH); // LED ON
delay(1000); // 1초 대기
digitalWrite(13, LOW); // LED OFF
delay(1000); // 1초 대기
}
예제 -> 01. Basics -> Blink 코드로 아두이노 보드에 붙어있는 LED를 깜빡이게 하는 코드이다.
위의 사진에 ‘L’자 옆의 LED가 digital I/O 13번 핀에 연결되어 있는 것으로 2초 간격으로 깜빡이게 된다.
코드는 매우 직관적이어서 곧바로 이해할 수 있고 아두이노에서 프로그램을 만들면 delay() 함수를 많이 사용하게 된다.
하지만 위와 같이 단순하게 한가지 작업만 하는 경우는 상관없지만 일반적인 프로그램의 경우 동시에 여러가지 작업을 수행해야만 한다. 그 경우 delay() 함수가 사용되면 delay()에 지정된 시간만큼 대기하는 동안 프로그램 실행이 중단되어 다른 작업을 할 수가 없게 된다.
그러므로 처음 코드를 이해할 때는 delay()함수가 편해도 실제 프로그램을 만들 때는 delay() 함수 사용을 최소화 해야만 한다. 예를 들어 2개의 스위치를 연결해 LED가 깜빡이는 속도를 조절하는 프로그램을 만든다고 생각해 보자.
단순하게 생각한다면 다음과 같은 식으로 코드를 만들 수 있을 것이다.
int gDelay = 1000;
void setup() {
pinMode(13, OUTPUT);
pinMode(7, INPUT_PULLUP);
pinMode(8, INPUT_PULLUP);
}
void loop() {
if (LOW == digitalRead(7)) {
gDelay += 100;
}
if (LOW == digitalRead(8)) {
gDelay -= 100;
}
gDelay = constrain(gDelay, 100, 2000);
digitalWrite(13, HIGH); // LED ON
delay(gDelay);
digitalWrite(13, LOW); // LED OFF
delay(gDelay);
}
위의 경우 스위치를 검사해 스위치가 눌리면 각각 delay 시간을 늘리거나 줄여주고, LED를 깜빡이는 코드가 다 포함되어 있다. 하지만 실행시켜 보면 스위치를 눌러도 깜빡이는 속도가 원하는대로 바뀌지 않을 것이다.
위의 그림에서 볼 수 있는 것 처럼 (1)과 (2)에서 스위치가 놀렸는지 검사하는데 만일 gDelay 변수 값이 1000이었다고 하면 2초에 한번씩만 스위치가 눌렸는지 검사하게 된다. 스위치를 검사하고 다음번에 다시 검사하는 2초 사이에 스위치를 눌러다 떼면 프로그램은 스위치가 눌린걸 알수가 없게 된다. 즉 delay()로 프로그램 실행이 중단되어 있는 동안에는 다른 작업을 하지 못하게 된다.
이 문제에 대한 해결책으로 millis() 함수를 사용하면 된다. 아두이노에는 전원이 들어와 스케치가 시작되면 0에서 시작되어 1/1000초 단위로 1씩 증가하는 카운터가 있다. millis() 함수를 호출하면 호출된 시점의 카운트 값(unsigned long 타입)을 알려준다.
위의 그림에서 b에서 a 값을 빼면 그 값이 a에서 b까지의 시간(ms 단위)이 된다.
그러므로 위의 코드를 delay()를 사용하지 않도록 다음과 같이 바꿀 수 있다.
int gDelay = 1000;
void setup() {
pinMode(13, OUTPUT);
pinMode(7, INPUT_PULLUP);
pinMode(8, INPUT_PULLUP);
}
void loop() {
static unsigned long last = 0;
static boolean ledStat = LOW;
if (LOW == digitalRead(7)) {
gDelay += 100;
}
if (LOW == digitalRead(8)) {
gDelay -= 100;
}
gDelay = constrain(gDelay, 100, 2000);
if ((millis()-last) >= gDelay) {
ledStat = !ledStat;
digitalWrite(13, ledStat);
last = millis();
}
}
delay() 함수를 사용한 스케치에서는 loop() 함수가 한번 실행되는데 2초(gDelay가 1000인 경우)가 걸리지만, delay()함수를 사용하지 않은 스케치는 loop() 함수가 한번 실행되는데는 매우 짧은 시간이 걸려 2초동안 loop()함수가 수만~수십만번 실행되므로 스위치 상태도 그 횟수만큼 검사가 되기 때문에 사람이 아무리 빠르게 스위치를 눌렀다 떼더라도 감지하지 못하는 경우는 없다. 하지만 LED의 상태를 토글하는 코드(위에서 빨간색 부분)는 그 중 두번만 실행되게 된다.
(물론 위의 코드는 스위치의 debouncing 처리가 안되어 있고 한번 눌린 동안 너무 여러번 스위치 상태를 감지하게 되기 때문에 스위치를 한번 눌렀을 때 gDelay 값이 크게 변하게 된다. 이런 문제들은 이 글의 주제와 다른 부분이라 다른 포스트에서 설명할 것이다.)
참고로 이런 문제들을 모두 해결한 코드는 아래와 같다.
#define SW1 8
#define SW2 9
#define LED 13
boolean gLedStat = HIGH;
int gDelay = 500;
void setup() {
pinMode(SW1, INPUT_PULLUP);
pinMode(SW2, INPUT_PULLUP);
pinMode(LED, OUTPUT);
Serial.begin(115200);
}
void loop() {
static unsigned long last = 0;
static unsigned long lastSw = 0;
unsigned long now = millis();
static boolean prev1 = HIGH;
static boolean prev2 = HIGH;
boolean cur;
if ((now - lastSw) >= 10) { // Check switch for every 10ms
cur = digitalRead(SW1);
if ((HIGH == prev1) && (LOW == cur)) {
gDelay -= 100;
gDelay = constrain(gDelay, 100, 2000);
Serial.print("Delay up to ");
Serial.println(gDelay);
prev1 = LOW;
} else if ((LOW == prev1) && (HIGH == cur)) {
prev1 = HIGH;
}
cur = digitalRead(SW2);
if ((HIGH == prev2) && (LOW == cur)) {
gDelay += 100;
gDelay = constrain(gDelay, 100, 2000);
Serial.print("Delay down to ");
Serial.println(gDelay);
prev2 = LOW;
} else if ((LOW == prev2) && (HIGH == cur)) {
prev2 = HIGH;
}
lastSw = now;
}
if ((now - last) >= gDelay) {
gLedStat = !gLedStat;
digitalWrite(LED, gLedStat);
last = now;
}
}