유한 상태 머신으로 아두이노 디지털 시계 만들기
부품 및 소모품
| 아두이노 우노 (Arduino UNO) |
×1 |
| LCD 키패드 실드 |
×1 |
앱 및 온라인 서비스
- YAKINDU Statechart Tools (비상업적 용도 무료)
- Eclipse C++ IDE for Arduino
프로젝트 소개
안녕하세요, YAKINDU Statechart Tools로 그래픽 상태 머신 모델을 만들고 C/C++ 코드를 생성하여 아두이노에서 실행하는 디지털 시계 제작 과정을 보여드리겠습니다.
원본 디지털 시계 모델은 David Harel이 발표한 "기존 상태 머신과 상태 다이어그램 형식주의의 광범위한 확장" 논문에서 예시로 사용되었습니다. 이를 영감 삼아 YAKINDU로 재구현하고 아두이노에서 구동시켰습니다.
디지털 시계 작동 원리
90년대 유행했던 '초쿨' 디지털 시계를 기억하시나요? 통합 스톱워치, 다중 알람, 매 정시마다 울리는 성가신 비프음. 만약 기억나지 않는다면 여기를 참조하세요.
기본적으로 현재 시간을 표시하는 설정 가능한 시계로, 다음과 같은 기능이 있습니다. 입력 버튼: On/Off, Mode, Set. 추가로 백라이트 On/Off 가능.
Mode 버튼으로 모드 전환 및 시계 기능 활성화/비활성화:
- 시간 표시 (Clock)
- 날짜 표시 (Date)
- 알람 설정 (Alarm 1, Alarm 2)
- 차임(정시 알림) 활성화/비활성화 (Set Chime)
- 스톱워치 (Stop Watch)
메뉴 내에서 On/Off 버튼으로 설정 변경, Set 버튼으로 시간 설정(시계, 알람 등). 스톱워치는 Light On/Off 버튼으로 시작/정지, 랩 카운터도 내장.
또한 매 정시마다 울리는 차임과 제어 가능한 백라이트 기능이 있습니다. 첫 단계에서는 아두이노에 연결하지 않았습니다.
상태 머신

상태 머신은 크게 두 부분으로 구성됩니다: 텍스트 부분(이벤트, 변수 등 정의)과 그래픽 부분(상태 다이어그램, 논리적 실행 명세).
상태 머신 생성을 위해 필요한 입력 이벤트: onoff, set, mode, light, light_r. 정의 섹션에서 100ms마다 시간을 증가시키는 내부 이벤트 사용:
every 100 ms / time += 1
100ms 단위를 기반으로 현재 시간을 HH:MM:SS 형식으로 계산:
display.first = (time / 36000) % 24; // 시
display.second = (time / 600) % 60; // 분
display.third = (time / 10) % 60; // 초
값은 updateLCD 연산으로 상태 머신 호출 시마다 LCD에 표시:
display.updateLCD(display.first, display.second, display.third, display.text)
기본 실행 흐름은 디지털 시계 작동 원리 섹션에 정의되어 있습니다. 모델링 요소로 CompositeState, History, Sub-Diagrams, Exit Nodes 등을 사용했습니다. 자세한 설명은 사용자 가이드 참조.
LCD 키패드 실드
화면 표시와 버튼 입력이 필요한 간단한 프로젝트에 적합한 전형적인 HMI(Human Machine Interface)입니다. 5개 사용자 버튼 + 리셋 버튼. 5개 버튼 모두 아두이노 A0 핀에 연결되며, 각 버튼은 전압 분배 회로로 구분됩니다.
analogRead(0)로 버튼별 값 확인 가능 (제조사별 차이 존재). 제 측정값:
- None: 1023
- Select: 640
- Left: 411
- Down: 257
- Up: 100
- Right: 0
이 임계값으로 버튼 판독:
#define NONE 0
#define SELECT 1
#define LEFT 2
#define DOWN 3
#define UP 4
#define RIGHT 5
static int readButton() {
int result = analogRead(0);
if (result < 50) return RIGHT;
if (result < 150) return UP;
if (result < 300) return DOWN;
if (result < 550) return LEFT;
if (result < 850) return SELECT;
return NONE;
}
상태 머신 인터페이싱
생성된 C++ 코드는 상태 머신 제어를 위한 인터페이스를 제공하며, 이를 구현해야 합니다. 첫 단계는 입력 이벤트를 키패드 실드 키와 연결하는 것입니다. 버튼 디바운싱이 필수입니다. 그렇지 않으면 이벤트가 중복 발생해 예측 불가능한 동작을 유발합니다.
제 구현에서는 폴링 에지(버튼 떼기) 감지 방식을 사용합니다. 버튼 값 읽기 → 80ms 대기 (50ms보다 80ms가 더 안정적) → 이전 값 저장 → 새 값 읽기. oldResult가 NONE이 아니고 새 결과가 NONE이면 버튼이 눌렸다 떼어진 것으로 판단, 해당 상태 머신 입력 이벤트 발생.
int oldState = NONE;
static void raiseEvents() {
int buttonPressed = readButton();
delay(80);
oldState = buttonPressed;
if (oldState != NONE && readButton() == NONE) {
switch (oldState) {
case SELECT: stateMachine->getSCI_Button()->raise_mode(); break;
case LEFT: stateMachine->getSCI_Button()->raise_set(); break;
case DOWN: stateMachine->getSCI_Button()->raise_light(); break;
case UP: stateMachine->getSCI_Button()->raise_light_r(); break;
case RIGHT: stateMachine->getSCI_Button()->raise_onoff(); break;
}
}
}
모든 것 연결하기
메인 프로그램은 세 부분으로 구성:
- 상태 머신
- 타이머
- 디스플레이 핸들러 (일반적인
lcd.print(...))
DigitalWatch* stateMachine = new DigitalWatch();
CPPTimerInterface* timer_sct = new CPPTimerInterface();
DisplayHandler* displayHandler = new DisplayHandler();
상태 머신은 디스플레이 핸들러와 타이머를 사용. 초기화 후 진입:
void setup() {
stateMachine->setSCI_Display_OCB(displayHandler);
stateMachine->setTimer(timer_sct);
stateMachine->init();
stateMachine->enter();
}
루프에서 세 가지 작업:
- 입력 이벤트 발생 (
raiseEvents())
- 경과 시간 계산 및 타이머 업데이트
- 상태 머신 사이클 실행 (
runCycle())
long current_time = 0;
long last_cycle_time = 0;
void loop() {
raiseEvents();
last_cycle_time = current_time;
current_time = millis();
timer_sct->updateActiveTimer(stateMachine, current_time - last_cycle_time);
stateMachine->runCycle();
}
예제 다운로드
구현 세부사항을 모두 다루진 않았지만, 예제를 보거나 댓글 남겨주세요.
실행 중인 IDE에 예제 추가:
File → New → Example → YAKINDU Statechart Examples → Next → Arduino - Digital Watch (C++)
> IDE 다운로드: YAKINDU Statechart Tools
30일 체험판으로 시작 가능. 이후 라이선스 필요하지만 비상업적 용도는 무료입니다!