산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

아두이노 나노와 16x2 LCD로 정밀 시계 만들기

부품 및 소모품

부품수량비고
아두이노 나노 R31다른 아두이노도 작동 가능
영숫자 LCD 16x21아무 디스플레이나 가능, 예: 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일 증가시킵니다. 변수 최대값 제한에 영향받지 않습니다.

if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
  noInterrupts();
  currentTime -= millisecondsInADay;
  interrupts();
  currentDate++;
}

인터럽트 중 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 단위로 조정 가능.

코드

시계 프로그램 (아두이노)

// Paul Brace - Feb 2021
// RTC 모듈 없이 아두이노만으로 날짜/시간 표시하는 정밀 시계
// 내부 클럭 속도 보정 기능 포함. 보정값 설정 후 5일간 오차 없음 검증.
// 16x2 LCD 표시, 버튼으로 시간/날짜/보정 설정
// 24시간제

#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 13, 6, 7, 8, 9); // RS, EN, D4, D5, D6, D7

#define MODE_BUTTON 2
#define HOUR_BUTTON 3
#define UP_BUTTON 3
#define DAY_BUTTON 3
#define MINUTE_BUTTON 4
#define DOWN_BUTTON 4
#define MONTH_BUTTON 4

#define SHOW_TIME 1
#define SET_TIME 2
#define SET_YEAR 3
#define SET_DATE 4
#define SET_SPEED_ADJ 5

int speedCorrection = 3545; // 내 나노는 시간당 3545ms 느림 (빠르면 음수)
volatile unsigned long currentTime = 0;
unsigned long lastTime = -1000;
volatile unsigned long elapsed = 0;
unsigned long millisecondsInADay = 24ul * 60 * 60 * 1000;
unsigned long millisecondsInHour = 60ul * 60 * 1000;
int currentMode = SHOW_TIME;
float currentDate = 0.0;
float lastDate = 0.0;
int currentDay, currentMonth, currentYear;
char *dayArray[] = { "Tue. ", "Wed. ", "Thur. ", "Fri. ", "Sat. ", "Sun. ", "Mon. " };

void setup() {
  // 타이머0 인터럽트 설정 (1ms)
  TCCR0A = (1 << WGM01);
  OCR0A = 0xF9;
  TIMSK0 |= (1 << OCIE0A);
  TCCR0B |= (1 << CS01) | (1 << CS00);
  TCNT0 = 0;
  sei();

  pinMode(MINUTE_BUTTON, INPUT_PULLUP);
  pinMode(HOUR_BUTTON, INPUT_PULLUP);
  pinMode(MODE_BUTTON, INPUT_PULLUP);

  currentTime = 0;
  currentDate = JulianDate(1, 1, 2021);
  elapsed = 0;
  lcd.begin(16, 2);
  lcd.noAutoscroll();
  lcd.display();
  lcd.clear();
  ShowTime(currentTime);
}

void loop() {
  // 자정 넘으면 날짜 증가
  if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
    noInterrupts();
    currentTime -= millisecondsInADay;
    interrupts();
    currentDate++;
  }
  // 매시간 속도 보정
  if (elapsed >= millisecondsInHour) {
    noInterrupts();
    currentTime += speedCorrection;
    elapsed = 0;
    interrupts();
  }
  CheckButtons();
  switch (currentMode) {
    case SHOW_TIME: ShowTime(currentTime); ShowDate(currentDate); break;
    case SET_TIME: ShowTimeSet(currentTime); break;
    case SET_YEAR: ShowYearSet(currentDate); break;
    case SET_DATE: ShowDDMMSet(currentDate); break;
    case SET_SPEED_ADJ: ShowSpeedSet(); break;
  }
  Wait(150);
}

ISR(TIMER0_COMPA_vect) {
  if (currentMode != SET_TIME) currentTime++;
  elapsed++;
}

// ... (JulianDate, GregorianDate, SplitTime, SetTime, Wait, CheckButtons,
//      FormatNumber, ShowTime, ShowDate, ShowDDMMSet, ShowYearSet,
//      ShowTimeSet, ShowSpeedSet 함수들 - 원문 참조) ...

아두이노 타이머 테스트 프로그램

// Paul Brace Feb 2021
// Processing 스크립트와 짝을 이뤄 아두이노 millis()와 컴퓨터 millis() 비교
int inByte = 0;
unsigned long firstReading = 100000;

void setup() {
  Serial.begin(9600);
  sayHello();
}
void loop() {
  if (Serial.available() > 0) {
    inByte = Serial.read();
    Serial.print(millis() - firstReading);
    Serial.print('E');
    delay(2000);
  }
}
void sayHello() {
  while (Serial.available() <= 0) {
    Serial.print('Z');
    delay(200);
  }
  firstReading = millis();
}

Processing 타이머 테스트 스크립트

// Paul Brace Feb 2021
// 아두이노에서 보낸 millis()를 읽어 내부 millis()와 비교해 아두이노 클럭 부정확도 측정
// 컴퓨터 클럭이 정확하다고 가정
// -ve = 아두이노가 느림 → 시계 프로그램에 +ve 보정값 입력
// +ve = 아두이노가 빠름 → -ve 보정값 입력

import processing.serial.*;
Serial theSerialPort;
int[] serialBytesArray = new int[15];
int bytesCount = 0;
boolean init = false;
int fillColor = 255;
long mills = 0, first = 0, now, firstReading = 100000, DiffPerHour = 0;
int inByte;

void setup() {
  size(500, 500);
  background(70);
  noStroke();
  printArray(Serial.list());
  String thePortName = Serial.list()[1];
  theSerialPort = new Serial(this, thePortName, 9600);
}
void draw() {
  background(70);
  fill(fillColor);
  textSize(25);
  text(hour() + ":" + minute() + ":" + second(), 50, 50);
  text("Incoming elapsed: " + mills, 50, 100);
  text("Local elapsed: " + (now - firstReading), 50, 150);
  text("Diff: " + (mills - (now - firstReading)), 50, 200);
  if (((now - firstReading) >= 3600000) && (DiffPerHour == 0)) {
    DiffPerHour = mills - (now - firstReading);
  }
  text("Diff after 1 hour: " + DiffPerHour, 50, 300);
}
void serialEvent(Serial myPort) {
  inByte = myPort.read();
  if (!init) {
    if (inByte == 'Z') {
      myPort.clear();
      init = true;
      myPort.write('Z');
      if (first == 0) first = millis();
    }
  } else {
    if (inByte != 69) { // 'E' 아님
      if (bytesCount < 14) {
        serialBytesArray[bytesCount] = inByte;
        bytesCount++;
      }
    }
    if (inByte == 69) { // 메시지 끝
      now = millis();
      mills = 0;
      for (int i = 1; i <= bytesCount; i++) {
        mills += (serialBytesArray[i-1] - 48) * pow(10, bytesCount - i);
      }
      if (firstReading == 100000) firstReading = now;
      myPort.write('Z');
      bytesCount = 0;
    }
  }
}

회로도

아두이노 나노와 16x2 LCD로 정밀 시계 만들기
아두이노 나노와 16x2 LCD로 정밀 시계 만들기

제조공정

  1. 아두이노 POV 클락: 퍼시스턴스 오브 비전 LED 디스플레이 제작
  2. 아두이노만으로 DTMF 디코더 구축하기 – 추가 IC 불필요
  3. 아두이노로 모니터 앰비라이트(Ambilight) 시스템 직접 만들기
  4. 아다프루트 60-LED 네오픽셀 링 4개로 스타일리시 벽시계 만들기
  5. DIY 아두이노 워드 클럭 – 세련된 실시간 텍스트 시계 만들기
  6. 아두이노 실시간 시계로 이슬람 기도 시간 알림 장치 만들기
  7. 아두이노로 만드는 대형 LED 마스터 시계 (알람 기능 포함, 12m 거리에서도 식별 가능)
  8. 아두이노와 스마트폰으로 만드는 스마트 전압계 – 초간단 DIY 프로젝트
  9. 아두이노와 MAX30100으로 만드는 실시간 IoT 심박수 모니터
  10. 아두이노 우노 와이파이 웹 서버: 브라우저로 LED 제어하기