[아두이노] [강좌] 26. 시간 관련 함수 (1) - delay() / millis()
*줄줄이사탕 주의 : 최대한 쉽게 설명하려고 했지만, 타이머/카운터 기능 자체가 쉽게 설명이 되는 기능이 아니라 강좌를 쓰다보니 주절주절하게 됐..ㅜㅜ 중요하지 않으므로, 함수를 사용하는 방법만 이해하고 넘어가도 됨. |
우리가 처음 살펴봤던 예제 “Blink”에서 LED를 1초 간격으로 켜고 끄기 위해 delay() 함수를 사용했었다. 그리고 다른 많은 예제에서도delay() 함수는 유용하게 사용되며, 이 전의 인터럽트 강좌에서는 delayMicroseconds() 함수까지 사용했었다.
그때 delay() 함수는 타이머 인터럽트를 사용하는 함수이므로 ISR(Interrupt Service Routine, 인터럽트 처리 함수)에서는 사용할 수 없으며, 대신 타이머 인터럽트를 사용하지 않는 delayMicroseconds() 함수는 사용할 수 있다고 잠시 설명했었는데, 이건 “참고”에 있던 내용이라 못 본 사람도 있겠군.
아두이노에서는 시간과 관련된 기능을 제공하기 위해서 하나의 타이머를 기본 타이머로 설정하여 사용하고 있으며, 이 타이머를 이용하는 기본 함수는 delay() 함수와 millis() 함수이다.
타이머에 관해서는 나중에 따로 자세히 포스팅하려고 계획 중이지만, 개념에 관한 설명이 될 것 같아서 넣을까 말까 아직 고민 중임. 간단하게는 지난 강좌에서 설명했고. 이 것도 "참고"에 있던 내용이라 못 봤을 수도..ㅎㅎㅎㅎ
아무튼 아두이노에 전원을 딱 넣으면 setup() 함수의 내용을 실행하기 전 기본 세팅을 하도록 되어있는데, 바로 여기에서 타이머가 설정된다. 설정되는 타이머는 0번 타이머.
아두이노 보드의 종류에 따라 각각 보유하고 있는 타이머의 개수가 다른데, 우노는 3개, 메가는 6개. 이 타이머들을 모두 항상 사용하는 건 아니지만, 0번 타이머 하나는 항상 사용된다고 보면 된다. 나머지는 analogWrite() 함수라던지, tone() 함수 등에 사용된다.
우선 함수부터 살펴보자.
millis()
반환 값 ms : 전원이 인가된 이후의 시간. 밀리초. (unsigned long 타입)
|
말했다시피 아두이노 보드에 전원이 인가된 후 얼마만큼의 시간이 지났는지를 밀리초 단위로 반환한다. 반환 값의 데이터 타입은 unsigned long 타입이고, unsigned long 타입(4바이트)이 가질 수 있는 최대 값은 4,294,967,295이며, 이 값으로 약 49.71일을 셀 수 있다. 즉 50일 정도가 지나면 다시 0부터 시작한다는 뜻.
반환 타입이 unsigned long이라는 점에 유의해야 한다. 만일 int 같은 다른 데이터 타입으로 값을 저장할 경우 사용자의 의도와는 다른 결과가 나타날 수 있다, 라고 아두이노 홈페이지에 써있어요.
delay() 함수를 사용해서 동작을 멈추게 되면 시리얼 통신이나 센서 체크 등의 기능을 사용할 수 없게 되는 경우가 많기 때문에 delay() 함수 대신 millis() 함수를 이용한 시간 처리 루틴을 만들어 사용하기도 한다.
micros()
반환 값 us : 전원이 인가된 이후의 시간. 마이크로초. (unsigned long 타입) |
millis()와 동일한 동작을 하지만 반환 값이 마이크로초 단위이다. 단, 16MHz의 보드에서 (메가와 우노 모두 16MHz의 클럭 속도를 가진다) micros() 함수로 반환되는 값은 항상 4로 나누어 떨어지는 값이며, 따라서 최대 3.9999 마이크로초의 오차가 발생할 수 있다.
millis() 함수와 마찬가지로 반환 타입이 unsigned long이므로 micros()의 반환 값은 약 71분에 한번씩 0으로 되돌아간다.
위에서 말했다시피, millis() 함수와 micros() 함수는 모두 타이머/카운터 기능을 사용해서 동작한다.
타이머/카운터는 칩이 구동하기 위한 클럭(clock)을 기준으로 증가하는 카운터를 활용하여 시간을 계산하는 기능이다. 단, 클럭과 동일하게 카운터가 증가하게 되면 너무 큰 수의 값이 사용되기 때문에 적당한 값으로 클럭을 나눠서 사용(이를 분주(Prescale)라 한다)하고, 그렇게 증가하는 값이 일정한 값이 되었을 때 인터럽트가 발생, 인터럽트 처리 함수에서 그 카운터 값을 활용하여 실제 시간 값을 계산해 저장하게 된다.
타이머 인터럽트를 사용하기 때문에, attachInterrupt() 함수로 설정한 인터럽트 처리 루틴에서 millis() 함수나 micros() 함수를 사용할 수 없다. (사용할 수는 있어도 값이 변하지 않기 때문에 소용이 없다. 하나의 인터럽트가 처리 중일 때 다른 인터럽트가 처리될 수 없으므로.)
delay(ms)
이미 많이 사용했던 함수지만 다시 한번 살펴보자.
매개 변수
ms : 밀리초 단위로 프로그램이 대기해야 할 시간을 지정한다. unsigned long 타입이므로, 최대 49.71일까지 설정할 수 있다. (근데 그만큼 설정할 일은 아마 없…) |
간단히 설명하면 이렇고, 좀 더 자세히 설명하자면 우선 delay() 함수의 내용을 보자. (몰라도 됨 주의)
void delay(unsigned long ms) { uint16_t start = (uint16_t)micros();
while (ms > 0) { if (((uint16_t)micros() - start) >= 1000) { ms--; start += 1000; } } }
|
micros() 함수는 전원이 인가된 후로 지난 시간을 마이크로초 단위로 반환하고, delay() 함수가 시작될 때 start 변수에 저장된다. 그리고while() 문을 반복하며, micros() 함수가 반환하는 시간(시간이 지날수록 micros() 함수가 반환하는 값이 점점 커질테니)이 start 변수에 저장된 시간과 비교해서 1000을 넘을 경우, 즉 1000 마이크로초(=1 밀리초)가 지나면 매개 변수로 받은 ms 값을 1 감소시킨다. 이 동작을ms 값이 0보다 같거나 작아질 때까지 반복한다.
그래서 설정한 ms 밀리초만큼 대기하게 된다,는 원리.
delay() 함수 내에서 micros() 함수가 사용되므로, delay() 함수 역시 인터럽트 처리 루틴 내에서 사용할 수 없다.
delayMicroseconds(us)
매개 변수 us는 마이크로초 단위의 시간을 나타내는 unsigned long 타입의 변수이다.
이 함수는 micros() 함수를 이용하지 않는다. 단지 us 값이 0이 될 때까지 us 값을 1씩 감소시키는 구문을 어셈블리어(기계어)로 처리한다. 이 어셈블리 구문이 총 4클럭을 소요하게 되므로 아두이노 보드의 클럭 속도에 맞춰 us 값을 적당히 곱해주는 작업이 이 전에 처리된다.
어렵게 들릴지 모르겠지만, 간단하게 말하면 설정한 마이크로초만큼의 시간동안 아무 의미 없는 명령어를 읽어오고 분석하는 것을 반복한다는 뜻. delay() 함수처럼 micros() 함수를 사용하는 것이 아니므로, 인터럽트와는 상관 없이 사용할 수 있다.
인터럽트 강좌에서는 millis() 함수나 micros() 함수에 대해 아직 다뤄보지 않았기 때문에 채터링 현상을 delayMicroseconds() 함수로 처리했지만, 사실은 인터럽트 루틴 내에서는 delay() 함수든 delayMicrosecodns() 함수든 사용하지 않는 것이 좋다. 인터럽트 루틴은 처리 시간이 최소화 되어야 다른 인터럽트에 영향이 없기 때문.
다음 강좌에서는 delay() 함수나 delayMicroseconds() 함수를 사용하지 않고 시간 지연을 줄 수 있는 방법에 대해서 알아보자.
그럼 이만. 뿅!