아무 디스플레이나 가능, 예: https://www.amazon.co.uk/gp/product/B00N8K2BYM
택타일 스위치 (상단 작동형)
3
트리머 포텐쇼미터 10kΩ
1
아무 10k 트리머나 가능
점퍼 와이어
1
프로젝트 소개
학습용 연습으로 시작했지만 5일간 실행 결과 1초도 빠르거나 느려지지 않는 매우 정확한 시계가 되었습니다.
아두이노 내부 클럭 속도가 100% 정확하지 않은 것이 주된 문제입니다. 그냥 millis()에 의존하면 경과 밀리초 카운트가 약간 틀려서 시계가 시간이 지날수록 늦거나 빨라집니다. 제가 사용한 아두이노의 정확도를 테스트해 시간당 몇 밀리초가 빠르거나 느린지 측정하고, 매시간 그 차이를 내부적으로 추적하는 밀리초에서 더하거나 빼는 속도 보정을 프로그래밍했습니다.
또 다른 우려는 아두이노 클럭이 일관되게 부정확한지였지만, 5일간 정확한 시간을 유지했으므로 일관되게 틀린 것 같습니다.
두 번째 문제는 내부 millis() 함수가 약 50일마다 리셋되며 밀리초 카운트를 조작할 수 없다는 점입니다. 해결책은 millis() 인터럽트를 제가 조작할 수 있는 카운터로 대체하고, 자정부터의 밀리초를 세어 매일 리셋되도록 하여 런타임 제한을 없애는 것이었습니다.
부정확도 평가
내 컴퓨터 시계(따라서 Processing의 millis())가 정확하다고 가정했습니다. 아두이노가 핸드셰이크 이후 경과 밀리초를 2초마다 Processing으로 전송하고, Processing 스크립트가 이를 읽어 자신의 경과 밀리초와 비교해 실시간 결과와 1시간 후 차이를 표시하는 프로그램을 만들었습니다. 이를 통해 시간당 손실/증가 밀리초를 구해 시계 프로그램의 속도 보정값으로 사용했습니다.
아두이노 프로그램과 Processing 스크립트 코드는 아래에 있습니다.
Processing이 설치되어 있지 않다면 https://processing.org 에서 다운로드하고 배울 수 있습니다.
시계 코드
시계 코드에서 주요 관심사는 인터럽트 설정 및 활용 방식, 날짜 저장 및 조작 방식입니다.
인터럽트 설정
다음 코드는 1밀리초마다 트리거되는 인터럽트를 설정합니다. 이는 millis()를 유지하는 인터럽트를 가로채므로 millis()와 delay()는 더 이상 작동하지 않습니다.
// 시간 인터럽트 설정 - millis()는 50일 후 롤오버하므로
// 직접 밀리초 카운터를 사용해 매일 자정에 리셋
TCCR0A = (1 << WGM01); // CTC 모드
OCR0A = 0xF9; // 비교값 249 = 1ms @ 16MHz/64
TIMSK0 |= (1 << OCIE0A); // 비교 매치 인터럽트 활성화
TCCR0B |= (1 << CS01) | (1 << CS00); // 프리스케일러 64
TCNT0 = 0; // 카운터 초기화
sei(); // 전역 인터럽트 활성화
1밀리초마다 호출되는 인터럽트 서비스 루틴:
volatile unsigned long currentTime = 0; // 자정부터 경과 밀리초
volatile unsigned long elapsed = 0; // 지연 및 시간 카운트용
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
currentTime과 elapsed는 volatile로 선언해 메인 코드에서도 최신 값을 읽도록 했습니다.
currentTime은 자정부터의 밀리초를 저장하며, 이를 시:분:초로 변환하고 시간 설정 시 리셋하는 루틴이 있습니다.
24시간(86,400,000ms)이 경과하면 하루 치 밀리초를 빼고 날짜를 1일 증가시킵니다. 변수 최대값 제한에 영향받지 않습니다.
인터럽트 중 currentTime 조작 시 오염 방지를 위해 인터럽트를 잠시 비활성화합니다.
매시간(3,600,000ms)마다 앞서 계산한 속도 보정값을 적용해 아두이노 내부 클럭의 빠름/느림을 보정합니다.
if (elapsed >= millisecondsInHour) {
noInterrupts();
currentTime += speedCorrection; // 양수면 느린 것, 음수면 빠른 것
elapsed = 0;
interrupts();
}
날짜 저장 및 계산 (율리우스 날짜)
날짜는 율리우스 날짜(Julian Date)로 저장합니다. 기원전 4713년 1월 1일 월요일부터 경과한 일수입니다. 율리우스 날짜 계산과 그레고리력 변환 루틴을 포함했습니다.
float JulianDate(int iday, int imonth, int iyear) {
unsigned long d = iday, m = imonth, y = iyear;
if (m < 3) { m += 12; y -= 1; }
unsigned long t1 = (153 * m - 457) / 5;
unsigned long t2 = 365 * y + (y / 4) - (y / 100) + (y / 400);
return 1721118.5 + d + t1 + t2;
}
void GregorianDate(float jd, int &iday, int &imonth, int &iyear) {
unsigned long f = jd + 68569.5;
unsigned long e = (4.0 * f) / 146097;
unsigned long g = f - (146097 * e + 3) / 4;
unsigned long h = 4000ul * (g + 1) / 1461001;
unsigned long t = g - (1461 * h / 4) + 31;
unsigned long u = (80ul * t) / 2447;
unsigned long v = u / 11;
iyear = 100 * (e - 49) + h + v;
imonth = u + 2 - 12 * v;
iday = t - 2447 * u / 80;
}
설정 버튼
모드 버튼(핀 2)으로 현재 모드를 순환: 시간 표시 → 시간 설정 → 연도 설정 → 일/월 설정 → 속도 보정 설정 → 시간 표시.
각 모드에서 위/아래 버튼(핀 3, 4)으로 값 조절. 시계 실행 중 시간이 틀어지면 속도 보정 모드에서 5ms 단위로 조정 가능.