지난 포스팅에서 PWM을 적용해서 LED에 페이드 인 & 페이드 아웃 효과를 주는 방법에 대해 알아보았다.
이번에는 진동 모터(vibration motor) 기능을 추가하여 LED가 페이드인/아웃 하는 동시에 진동이 같이 울리는 프로그램을 구현해 보려고 한다.
우선 여기서 중요한 것이 LED와 진동모터 두 가지 작업을 어떻게 동시에 비동기적으로 처리하느냐가 될 것이다.
아두이노 프로그램은 기본적으로 loop() 함수 안의 코드를 매우 빠르게 반복 실행한다.
만약 특정 동작(예: LED 켜기/끄기, 센서 값 읽기)을 일정 시간 간격으로 실행하고 싶다면 어떻게 해야 할까?
가장 간단하게 생각할 수 있는 방법은 delay() 함수를 사용하는 것이다.
예를 들어, delay(1000); 를 사용하면 1초 동안 프로그램 실행이 멈춘다. 하지만 이 방법으로는 비동기 처리를 할 수 없다.
delay() 함수가 실행되는 동안에는 다른 어떤 코드도 실행될 수 없기 때문이다. 만약 LED를 깜빡이면서 동시에 진동 모터를 작동해야 한다면 delay()로는 불가능하다. LED가 깜빡이는 동안 아두이노는 delay()에 갇혀있어 진동 모터를 처리할 수 없기 때문이다.
이러한 블로킹 문제를 해결하고 여러 작업을 동시에 처리하는 것처럼 보이게 하는 것이 바로 millis()의 역할이다.
millis() 타이머 함수의 역할은?
millis()는 아두이노 보드가 시작된 이후 현재까지 경과된 시간을 밀리초(1/1000초) 단위로 반환하는 타이머 함수이다. 즉, 아두이노가 켜지는 순간부터 이 타이머는 자동으로 계속 작동하며, 필요할 때마다 millis() 함수를 호출하여 현재 시간을 확인할 수 있다.
이 millis() 함수의 가장 중요한 역할은 프로그램이 다른 작업을 멈추지 않고도 특정 시간 간격으로 작업을 수행할 수 있도록 해준다는 것입니다. delay() 함수를 사용하면 일정 시간 동안 프로그램을 멈춰놓고 다음 작업을 수행하므로 그 시간 동안 아두이노는 아무 다른 작업을 할 수 없지만, millis()를 사용하면 현재 시간을 계속 확인하면서 조건문을 통해 현재 시간이 지났는지를 체크하고, 그 시간이 지났을 때만 해당 작업을 수행하도록 만들 수 있다. 이렇게 하면 여러 작업을 거의 동시에 진행하는 것처럼 보이게 만들 수 있는 것이다.
millis() 함수 사용 방법
millis() 함수를 사용해서 여러 작업을 동시에, 그리고 특정 시간 간격으로 제어하려면 조건문(주로 if 문)을 반드시 사용해야 한다.
millis() 자체는 단순히 "지금 몇 밀리초가 경과되었는지" 시간을 알려주는 역할만 한다.
이 시간을 가지고 원하는 동작을 수행할지 말지 결정하는 것은 결국 프로그래머의 몫이고, 이 결정을 내릴 때 조건문이 사용되는 것이다.
예를 들어, 1초마다 LED 상태가 바뀌는(켜짐/꺼짐) 프로그램을 만든다고 생각해 보자.
millis()를 단독으로 써서는 1초가 지났는지 알 수 없다. 하지만 다음과 같은 논리를 조건문으로 표현할 수 있다.
- 현재 시간 (millis())에서 마지막으로 LED를 켜거나 끈 시간을 뺀 값이 1000 ms보다 크거나 같으면,
- LED의 상태를 바꾸고,
- '마지막으로 LED를 켜거나 끈 시간'을 현재 시간으로 업데이트할 것.
위의 모든 판단과 실행이 if문 안에서 이루어진다.
아래 예시 코드를 참고하자.
unsigned long previousMillis = 0; // 마지막 동작 시간 초기화
const long interval = 1000; // 인터벌 1초로 설정
void loop() {
unsigned long currentMillis = millis(); // 현재 시간 확인
if (currentMillis - previousMillis >= interval) { // 1초가 지났는지 확인
// 1초가 지났다면
previousMillis = currentMillis; // 마지막 동작 시간을 현재 시간으로 업데이트
// 여기에 LED 켜고 끄는 코드 등 1초마다 하고 싶은 동작을 넣음
// 예: digitalWrite(ledPin, !digitalRead(ledPin)); // LED 상태 토글
}
// 1초가 지나지 않았더라도 다른 코드는 계속 실행됨
}
실전 연습 : 버튼을 릴리즈하면 3초 동안 LED가 페이드인/아웃되고 동시에 진동 모터가 울리는 프로그램
회로 디자인

진동모터의 VCC 핀은 전원 공급 장치의 양극(+) 단자를 의미하는데, 모터 작동에 필요한 양의 전압을 공급받는 핀이다.
이 핀에 3.3V 또는 5V의 전압을 공급하면 모터가 작동한다. 나는 5V로 하면 진동 세기가 너무 세서 3.3V로 맞춰놨다.
코드 작성
const int ledPin = 9;
const int vibePin = 10;
const int buttonPin = 2; // 푸시버튼 핀 (예: D2)
unsigned long prevLedMillis = 0;
const int ledInterval = 30;
int fadeValue = 0;
int fadeDirection = 5; // +5 or -5
// 버튼 상태 및 동작 제어 변수
bool actionActive = false; // 현재 동작이 진행 중인지 여부
unsigned long actionStartTime = 0;
const unsigned long actionDuration = 3000; // 3초 (3000 밀리초)
bool lastButtonState = HIGH; // 이전 버튼 상태 (INPUT_PULLUP 기준: 눌리지 않음)
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(vibePin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP); // 내부 풀업 저항 사용 (버튼 누르면 LOW)
}
void loop() {
unsigned long currentMillis = millis();
// 현재 버튼 상태 읽기
int currentButtonState = digitalRead(buttonPin);
// 버튼 릴리스 감지 (버튼이 LOW에서 HIGH로 변할 때)
if (lastButtonState == LOW && currentButtonState == HIGH) {
// 동작이 이미 진행 중이 아닐 때만 시작
if (!actionActive) {
actionActive = true;
actionStartTime = currentMillis; // 시작 시간 기록
digitalWrite(vibePin, HIGH); // 진동 모터 바로 ON
}
}
// 이전 버튼 상태 업데이트
lastButtonState = currentButtonState;
// actionDuration 동안 동작 실행
if (actionActive && (currentMillis - actionStartTime < actionDuration)) {
// 1. LED Fade 처리
if (currentMillis - prevLedMillis >= ledInterval) {
prevLedMillis = currentMillis;
analogWrite(ledPin, fadeValue);
fadeValue += fadeDirection;
// LED가 완전히 밝아지거나 어두워지면 방향 반전
if (fadeValue >= 255 || fadeValue <= 0) {
fadeDirection = -fadeDirection;
// 0이나 255를 넘어가지 않도록 보정
if (fadeValue >= 255) fadeValue = 255;
if (fadeValue <= 0) fadeValue = 0;
}
}
// 2. 진동 모터는 이미 HIGH로 설정되어 있으므로 별도 처리 필요 없음 (actionDuration 동안 유지)
} else if (actionActive && (currentMillis - actionStartTime >= actionDuration)) {
// actionDuration이 지나면 동작 중지
actionActive = false; // 동작 상태 초기화
digitalWrite(vibePin, LOW); // 진동 모터 OFF
analogWrite(ledPin, LOW); // LED OFF
fadeValue = 0; // LED 페이드 값 초기화
fadeDirection = 5; // LED 페이드 방향 초기화
}
}
코드 작동 원리
- 현재 시간을 계속 측정 :
- loop() 함수가 반복될 때마다 unsigned long currentMillis = millis(); 라인을 통해 항상 현재 시간을 currentMillis 변수에 저장함. 이것이 모든 시간 기반 로직의 기준점이 됨.
- 버튼 릴리즈 감지 및 동작 시작 :
- lastButtonState와 currentButtonState를 비교하여 버튼이 눌렸다가 떼어졌을 때(LOW에서 HIGH로 변할 때)를 감지.
- if (!actionActive) 조건으로 동작이 진행 중이 아닐 때만 새로운 동작을 시작함.
- 동작이 시작되면 actionActive를 true로 설정하고, actionStartTime = currentMillis;를 통해 동작이 시작된 시간을 기록.
(시간은 진동 모터가 켜지고 LED 페이드가 시작되는 시점) - digitalWrite(vibePin, HIGH);를 통해 진동 모터를 즉시 울림.
- 동작 지속 시간 관리(3초) :
- if (actionActive && (currentMillis - actionStartTime < actionDuration)) 이 조건이 핵심.
- actionActive가 true이고, currentMillis - actionStartTime (현재 시간에서 동작 시작 시간을 뺀 값, 즉 동작이 시작된 후 경과된 시간)이 actionDuration (3000ms, 즉 3초)보다 작으면 동작을 계속 수행.
- 이 조건을 만족하는 동안 LED 페이드와 진동 모터 작동 로직이 계속 실행.
- LED 페이드 인/아웃 처리 :
- if (currentMillis - prevLedMillis >= ledInterval) 이 조건은 LED 밝기를 일정 간격(30ms)으로 업데이트하기 위함.
- prevLedMillis는 직전에 LED 밝기 업데이트가 이루어진 시간을 저장.
- currentMillis - prevLedMillis는 마지막 LED 업데이트 이후 경과된 시간을 나타냄.
- 이 경과 시간이 ledInterval (30ms)보다 크거나 같으면, LED 밝기를 변경하고 prevLedMillis를 currentMillis로 업데이트하여 다음 업데이트 시간을 위한 기준점을 새로 설정.
- analogWrite(ledPin, fadeValue);를 통해 LED 밝기를 조절하고, fadeValue를 fadeDirection에 따라 증가 또는 감소시켜 페이드 효과를 만듬. fadeValue가 0 또는 255에 도달하면 fadeDirection을 반전시켜 LED가 어두워졌다가 다시 밝아지도록 함.
- 이 LED 페이드 로직은 진동 모터 로직과 독립적으로, 각자의 시간 기준(30ms 간격)에 따라 실행됨. 30ms가 지나지 않았다면 LED 페이드 코드는 실행되지 않고, 아두이노는 다른 작업(예: 버튼 상태 확인, 진동 모터 작동 여부 확인)을 계속 수행.
- 진동 모터는 3초 동안 켜진 상태 유지 :
- 버튼 릴리스 시 digitalWrite(vibePin, HIGH);로 진동 모터가 켜짐.
- 진동 모터는 별도의 시간 간격 체크 없이, actionActive가 true이고 actionDuration (3초)이 지나지 않는 한 계속 HIGH 상태를 유지함. 이는 LED 페이드와는 다른 방식으로 시간을 관리하며, 단순히 동작 지속 시간 조건에 의해 제어됨.
- 동작 종료 및 초기화 :
- else if (actionActive && (currentMillis - actionStartTime >= actionDuration)) 이 조건은 3초의 동작 시간이 모두 경과했을 때 실행됨.
- actionActive를 false로 설정하여 동작을 종료하고, digitalWrite(vibePin, LOW);로 진동 모터를 끄고, analogWrite(ledPin, LOW);로 LED를 끔.
- 관련 변수들(fadeValue, fadeDirection)도 초기화하여 다음 동작을 준비.
프로그램 실행 영상
'Journey to CS > 임베디드' 카테고리의 다른 글
| [메모리] 엔디언(Endianness)과 포인터 캐스팅(casting) (1) | 2025.09.14 |
|---|---|
| 아두이노에서 어떤 정수 타입을 써야 할까? 데이터 타입 비교표 한 눈에 보기 (2) | 2025.08.13 |
| 아두이노 스케치를 작성하다가 loop()함수에 대해 생긴 의문...그리고 답 (1) | 2025.07.27 |
| 아두이노에 PWM을 적용해서 LED에 페이드 인 & 페이드 아웃 효과 주기 (1) | 2025.07.17 |
| ESP32 시뮬레이터 - WOKWI 소개 (2) | 2025.06.24 |