아두이노의 millis() 함수를 이용해 아두이노시계를 만들어 보자. 아두이노시계를 구현하는 방법으로는 millis() 함수를 이용하지 않고 시계용 모듈 DS1302 RTC을 이용하는 방법도 있다. DS1302 RTC 자체가 아두이노의 전원과 상관없이 소형 배터리(동전 배터리)를 이용하여 구동되고 있어서 아두이노의 전원이 꺼져있어도 DS1302 RTC는 내부의 시계값은 계속해서 계산하고 있게 된다.  따라서 아두이노 전원을 다시 켰을 때 사용자가 다시 시계값을 맞춰주는 과정 없이 DS1302 RTC의 시간값을 가져와 현재 시간을 표시해 줄 수 있게 된다. 하지만 모든 전자시계들이 그렇지만 DS1302 RTC 역시 오차가 있어서 시간이 지날수록 그 오차값이 초단위에서 분단위로 점점 커지게 되어 시간을 보정해주어야 하는 것은 같다. 요즘은 스마트폰이 일반화되고 스마트폰 시계를 사용하므로 시계의 시간을 맞춰줘야 한다는 개념이 없을 수도 있다. 이는 스마트폰의 시계는 무선 기지국을 통해 받은 시간 값과 동기화를 하고 있어서 시간을 맞춰줄 필요가 없기 때문이다. 정확한 시간 값과 동기화를 할 수 없는 독립된 시계에서는 오차가 필연적으로 발생하게 되며 그에 따라 시간을 보정해 주어야 한다.

 

아두이노시계 예제에서는 DS1302 RTC 모듈을 사용하는 대신 기본적인 시간값을 millis() 함수를 통해 구하고 아두이노의 전원을 껐다 켰을 때의 시간 초기화 문제는 아두이노 초기화 시 WiFi 모듈 ESP01를 통해 NTP 서버에 접속하여 UTC 시간 값을 받아 이를 현재의 시간 값으로 적용하도록 코드를 작성하여 해결하겠다. 이렇게 WiFi만 연결된다면 사용자가 현재 시간을 설정하지 않아도 되고, 일정 시간마다 동기화 과정을 거치도록 하여 오차값을 초단위로 유지할 수 있게 된다. 

 

이전 글 디지털 도어락 예제에서 설명했었던 delay() 함수 대체 방법의 milli() 함수 형식을 사용하여 1초씩 증가하도록 하는 함수가 아두이노시계의 핵심 코드이다.

if (millis() - start_time >= 1000) { // 1초가 되면 
  start_time = millis(); // 상기 조건을 만족할때의 밀리초를 다시 start_time에 저장하여 조건 초기화 
  s++; // 초 증가
  if (s == 60) { // 60초가 되면
    s = 0;       // 초는 0으로 초기화 
    m++;         // 분 증가
  } 
  if(m == 60) { m = 0; h++; }    // 60분이 되면 분은 0으로 초기화, 시 증가
  else if (h == 24) { h = 0; dd++; cal_dd(); } // 24시가 되면 시는 0으로 초기화 날짜 1일 증가, 날짜사용자 함수 시작
}

24시간 표시 또는 오전/오후 표시를 제어하기 위해  bool meridian = true; 플래그를 선언해주고 현재 시간이 오전인지 오후인지를 구분해주는 bool now_am = true;  플래그도 선언해준다. 

 

선언된 플래그를 통해 시리얼 모니터에 현재 시간값을 표시해주는 아래 코드를 작성해 주었다. 이 코드를 상기의 millis() 함수 내에 위치하게 하여 초값이 변할 때마다 시리얼 모니터에 출력하도록 한다. 

uint8_t hm = h; // 오전/오후 시간값 저장을 위한 로컬 변수 선언
if(meridian == true && hm == 12) { now_am = false; } // 시가 12시 이면 오후
else if (meridian == true && hm > 12) { hm = hm - 12; } // 12시 보다 크면 -12해서 오후 시간값 계산
else now_am = true;
Serial.print(yy); Serial.print("."); // 년도 시리얼 모니터 출력 
if(mm < 10) { Serial.print("0"); Serial.print(mm); Serial.print("."); } // 월이 한자리 숫자면 0추가 2자리 숫자 변환
else { Serial.print(mm); Serial.print("."); } 
if(dd < 10) { Serial.print("0"); Serial.print(dd); Serial.print(". "); } // 일이 한자리 숫자면 0추가 2자리 숫자 변환
else { Serial.print(dd); Serial.print(". "); } 
if (meridian == true) { // 오전/오후 표시 사용인 상태에서
  if (now_am == true) Serial.print("AM "); // 오전이면 AM 표시
  else Serial.print("PM ");                // 아니면 PM 표시
  if(hm < 10) { Serial.print("0"); Serial.print(hm); Serial.print(":"); } // 로컬 변수값이 한자리 숫자이면 0추가 
  else { Serial.print(hm); Serial.print(":"); } 
} 
else {  // 오전/오후 표시를 사용하지 않으면 시간값 그대로 표시
  if(h < 10) { Serial.print("0"); Serial.print(h); Serial.print(":"); } // 0추가
  else { Serial.print(h); Serial.print(":"); } 
}
if(m < 10) { Serial.print("0"); Serial.print(m); Serial.print(":"); } // 0추가
else { Serial.print(m); Serial.print(":"); } 
if(s < 10) { Serial.print("0"); Serial.println(s); } // 0추가
else Serial.println(s);

 

 

날짜의 증가에 따라 월의 변화값을 계산하기 위해 월별 일수를 swich()함수를 이용하여 가져온 뒤 그 값을 기준으로 월의 증가를 결정하고 날짜 조정 시 표시일 범위를 설정해주기 위해 아래 사용자 함수를 작성해 주었다. 

void cal_dd() { // 윤년은 고려되지 않음 
  uint8_t m_dds; // 월별 일수값 저장 로컬 변수
  switch (mm) { // 현재 월이 mm이면
    case 1:   m_dds = 31; break; // 월별 일수 값 테이블
    case 2:   m_dds = 28; break; 
    case 3:   m_dds = 31; break; 
    case 4:   m_dds = 30; break; 
    case 5:   m_dds = 31; break; 
    case 6:   m_dds = 30; break; 
    case 7:   m_dds = 31; break; 
    case 8:   m_dds = 31; break; 
    case 9:   m_dds = 30; break; 
    case 10:  m_dds = 31; break; 
    case 11:  m_dds = 30; break; 
    case 12:  m_dds = 31; break; 
  } 
  if (m_dds > dd) { dd = 1; mm++; } // 월별 일수 보다 날짜가크면 날짜는 1, 월 증가
  if (mm > 12) { mm = 1; yy++; } // 증가한 월이 12보다 크면 월은 1, 년 증가
}

아래 스케치를 아두이노에 업로드 해주면 시리얼 모니터에 설정된 초기 시간 값부터 1초씩 증가하는 것을 볼 수 있다.

arduino_clock_basic.ino
0.00MB

더보기
더보기
uint8_t h = 12; // initial Time display is 12:59:45 PM 
uint8_t m = 56; 
uint8_t s = 45; 
bool meridian = true; // 오전 오후 사용 유무 
bool now_am = true;   // AM? 
uint16_t yy = 2019; 
uint8_t mm = 1; 
uint8_t dd = 1; 

void setup() { 
  Serial.begin(9600); 
} 

void loop() { 
  cal_time(); 
} 

unsigned long int start_time = 0; 

void cal_time() { 
  if (millis() - start_time >= 1000) { // 시간 간격: 밀리초 
    start_time = millis(); // 상기 조건을 만족할때의 밀리초를 다시 start_time에 저장하여 조건 초기화 
    s++; 
    if (s == 60) { 
      s = 0; 
      m++; 
    } 
    if(m == 60) { m = 0; h++; } 
    if(h == 12) { now_am = false;} 
    else if (h == 24) { h = 0; now_am = true; dd++; cal_dd(); }  
    uint8_t hm = h; 
    if(meridian == true && hm == 12) { now_am = false; } 
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    else now_am = true;
    Serial.print(yy); Serial.print("."); // 시리얼 모니터 출력 
    if(mm < 10) { Serial.print("0"); Serial.print(mm); Serial.print("."); } 
    else { Serial.print(mm); Serial.print("."); } 
    if(dd < 10) { Serial.print("0"); Serial.print(dd); Serial.print(". "); } 
    else { Serial.print(dd); Serial.print(". "); } 
    if (meridian == true) { 
      if (now_am == true) Serial.print("AM "); 
      else Serial.print("PM "); 
      if(hm < 10) { Serial.print("0"); Serial.print(hm); Serial.print(":"); } 
      else { Serial.print(hm); Serial.print(":"); } 
    } 
    else { 
      if(h < 10) { Serial.print("0"); Serial.print(h); Serial.print(":"); } 
      else { Serial.print(h); Serial.print(":"); } 
    } 
    if(m < 10) { Serial.print("0"); Serial.print(m); Serial.print(":"); } 
    else { Serial.print(m); Serial.print(":"); } 
    if(s < 10) { Serial.print("0"); Serial.println(s); } 
    else Serial.println(s); 
  } 
} 

void cal_dd() { // 윤년은 고려되지 않음 
  uint8_t m_dds; 
  switch (mm) { 
    case 1:   m_dds = 31; break; 
    case 2:   m_dds = 28; break; 
    case 3:   m_dds = 31; break; 
    case 4:   m_dds = 30; break; 
    case 5:   m_dds = 31; break; 
    case 6:   m_dds = 30; break; 
    case 7:   m_dds = 31; break; 
    case 8:   m_dds = 31; break; 
    case 9:   m_dds = 30; break; 
    case 10:  m_dds = 31; break; 
    case 11:  m_dds = 30; break; 
    case 12:  m_dds = 31; break; 
  } 
  if (m_dds > dd) { dd = 1; mm++; } 
  if (mm > 12) { mm = 1; yy++; } 
}

이제 시리얼 모니터를 통해 현재 시간값으로 조정할 수 있도록 코드를 작성해 보겠다. 시리얼 모니터에서 입력할 텍스트 명령어를 아래와 같이 정했다. 

 

1. dtime : 시리얼 모니터에 시간을 표시해라. (시리얼 모니터에 시간이 출력되지 않도록 했을 경우 사용)

2. s : 초를 0으로 변경

3. mm : 분 증가

4. m : 분 감소

5. hh : 시 증가

6. h : 시 감소

7. 24 : 24시간 표시 또는 오전/오후 표시 설정

8. yy : 년 증가

9. y : 년 감소

10. mon : 월 증가

11. mon- : 월 감소

12. dd : 일 증가

13. d : 일 감소

14. tset : 시간값 일괄 변경 "tset010101" - 1시 1분 1초, tset시분초, 시분초는 2자리 숫자 

15. dset : 날짜 일괄 변경 "dset20190825" - 2019년 8월 25일, dset연월일, 년은 4자리, 월일은 2자리 숫자

 

명령어를 수신할 수신부 코드는 아래와 같다. 아울러 앞선 코드중 loop() 함수 내의 시리얼 모니터 출력 코드를 필요할 때마다 불러올 수 있도록 사용자 함수 display_time() 로 빼내어 주었다. display_t = false; 플래그를 이용하여 display_time() 함수가 실행되는 시점을 제어하게 된다. adjust = false; 플래그는 날짜 계산 cal_dd() 함수에서 사용자 날짜 조정 시 월이나 년이 영향을 받지 않도록 하기 위함이다. 사용자 조정으로 1월에서 날짜를 증가시킨다면 31일 이후에 1일로 변하지만 월은 2월로 변경되지 않게 된다. 

if (Serial.available() > 0) { // 시리얼 버퍼에 값이 있으면 
    String temp = Serial.readStringUntil('\n'); // '\n'(새줄)까지 스트링을 읽어라
    if (temp == "dtime") display_time(); // 읽은 값이 dtime 이면 display_time() 함수 실행
    else if (temp == "s") { s = 0; start_time = millis(); display_t = true; } // s 이면 s는 0 millis()값 초기화
    else if (temp == "mm") { m++; display_t = true; } 
    else if (temp == "m") { m--; display_t = true; } 
    else if (temp == "hh") { h++; display_t = true; } 
    else if (temp == "h") { h--; display_t = true; } 
    else if (temp == "24") { meridian = !meridian; display_t = true; } // 24이면 meridian 값이 참이면 거짓, 거짓이면 참
    else if (temp == "yy") { yy++; display_t = true; } 
    else if (temp == "y") { yy--; display_t = true; } 
    else if (temp == "mon") { mm++; adjust = true; display_t = true; } 
    else if (temp == "mon-") { mm--; adjust = true; display_t = true; } 
    else if (temp == "dd") { dd++; adjust = true; display_t = true; } 
    else if (temp == "d") { dd--; adjust = true; display_t = true; } 
    else if (temp.startsWith("tset")) { // 들어온 스트링이 "tset" 문자열로 시작하면 
      String set = temp.substring(4, 6);  // 스트링 인덱스 4, 5번 저장
      h = set.toInt(); // 스트링을 숫자값으로 변환
      set = temp.substring(6, 8); 
      m = set.toInt(); 
      set = temp.substring(8, 10); 
      s = set.toInt(); 
      start_time = millis(); // 시간값 변경됐으므로 millis()값 초기화
      adjust = true; // 시간 계산함수에서 사용할 플래그
      display_t = true; // 시간값이 변경됐으므로 시리얼 모니터에 출력하라는 플래그
    } 
    else if (temp.startsWith("dset")) { 
      String set = temp.substring(4, 8); 
      yy = set.toInt(); 
      set = temp.substring(8, 10); 
      mm = set.toInt(); 
      set = temp.substring(10, 12); 
      dd = set.toInt(); 
      adjust = true; 
      display_t = true; 
    } 
    if (m >= 60) m = 0; // 1씩 증가시 60이면 0으로 변경(분은 0 ~ 59분까지)
    else if (m < 0) m = 59; // 1씩 감소시 0보다 작으면 59로 변경 
    if (h >= 24) h = 0;     // -> 이 코드가 적용되기 위해서는 변수를 정의할때 
    else if (h < 0) h = 23; // 음수를 포함한 타입으로 정의해야함. uin8_t 사용불가
    if (h < 12) now_am = true; // 시가 12이하면 오전
    else now_am = false;       // 아니면 오후
    hm = h;  // 오전/오후 표시 시간 변환위해 시값 저장
    if(meridian == true && hm == 12) { now_am = false; } // 12는 오후
    else if (meridian == true && hm > 12) { hm = hm - 12; } // 오후 일때 1시부터의 시간값
    cal_dd(); // 날짜 계산 함수 콜
    if (display_t == true) { display_time(); display_t = false; } // 시리얼 모니터 출력
  }

 

다음 단계에서는 현재의 시리얼 수신코드도 loop() 함수에서 빼낼 것이다. 

 

아래 스케치를 아두이노에 업로드하고 시리얼 모니터에 상기 텍스트 명령어를 입력하여 시간 변경이 잘 되지는 확인해 보자. 시리얼 모니터의 전송 옵션을 새 줄로 변경하고 입력해야만 한다.

arduino_clock_time_adjust.ino
0.00MB

더보기
더보기
int h = 12; // initial Time display is 12:59:45 PM
int m = 59;
uint8_t s = 45;
bool meridian = true;
uint8_t hm;
bool now_am = true; //AM
uint16_t yy = 2019;
uint8_t mm = 1;
uint8_t dd = 1;
bool adjust = false;

bool display_t = false;

unsigned long int start_time = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  cal_time();
  if (Serial.available() > 0) {
    String temp = Serial.readStringUntil('\n');
    if (temp == "dtime") display_time();
    else if (temp == "s") { s = 0; start_time = millis(); display_t = true; }
    else if (temp == "mm") { m++; display_t = true; }
    else if (temp == "m") { m--; display_t = true; }
    else if (temp == "hh") { h++; display_t = true; }
    else if (temp == "h") { h--; display_t = true; }
    else if (temp == "24") { meridian = !meridian; display_t = true; }
    else if (temp == "yy") { yy++; display_t = true; }
    else if (temp == "y") { yy--; display_t = true; }
    else if (temp == "mon") { mm++; adjust = true; display_t = true; }
    else if (temp == "mon-") { mm--; adjust = true; display_t = true; }
    else if (temp == "dd") { dd++; adjust = true; display_t = true; }
    else if (temp == "d") { dd--; adjust = true; display_t = true; }
    else if (temp.startsWith("tset")) {
      String set = temp.substring(4, 6);
      h = set.toInt();
      set = temp.substring(6, 8);
      m = set.toInt();
      set = temp.substring(8, 10);
      s = set.toInt();
      start_time = millis();
      adjust = true;
      display_t = true;
    }
    else if (temp.startsWith("dset")) {
      String set = temp.substring(4, 8);
      yy = set.toInt();
      set = temp.substring(8, 10);
      mm = set.toInt();
      set = temp.substring(10, 12);
      dd = set.toInt();
      adjust = true;
      display_t = true;
    }
    if (m >= 60) m = 0;
    else if (m < 0) m = 59;
    if (h >= 24) h = 0; 
    else if (h < 0) h = 23; 
    if (h < 12) now_am = true;
    else now_am = false; 
    hm = h; 
    if(meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    cal_dd();
    if (display_t == true) { display_time(); display_t = false; }
  }
}

void cal_time() {
  if (millis() - start_time >= 1000) { // 시간 간격: 밀리초
    start_time = millis(); // 상기 조건을 만족할때의 밀리초를 다시 start_time에 저장하여 조건 초기화
    s++;
    if (s == 60) {
      s = 0;
      m++;
    }
    if(m == 60) { m = 0; h++; }
    if(h == 12) { now_am = false;}
    else if (h == 24) { h = 0; now_am = true; dd++; cal_dd(); } 
    hm = h;
    if(meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    else now_am = true;
    display_time();
  }
}

void cal_dd() {
  uint8_t m_dds;
  switch (mm) {
    case 1:   m_dds = 31; break;
    case 2:   m_dds = 28; break;
    case 3:   m_dds = 31; break;
    case 4:   m_dds = 30; break;
    case 5:   m_dds = 31; break;
    case 6:   m_dds = 30; break;
    case 7:   m_dds = 31; break;
    case 8:   m_dds = 31; break;
    case 9:   m_dds = 30; break;
    case 10:  m_dds = 31; break;
    case 11:  m_dds = 30; break;
    case 12:  m_dds = 31; break;
  }
  if (adjust == false) {
    if (dd > m_dds) { dd = 1; mm++; }
    if (mm > 12) { mm = 1; yy++; }
  }
  else {
    if (dd > m_dds) dd = 1;
    else if (dd < 1) dd = m_dds;
    if (mm > 12) mm = 1;
    else if (mm < 1) mm = 12;
    adjust = false;
  }
}

void display_time() {
  Serial.print(yy); Serial.print("."); 
  if(mm < 10) { Serial.print("0"); Serial.print(mm); Serial.print("."); }
  else { Serial.print(mm); Serial.print("."); }
  if(dd < 10) { Serial.print("0"); Serial.print(dd); Serial.print(". "); }
  else { Serial.print(dd); Serial.print(". "); }
  if (meridian == true) {
    if (now_am == true) Serial.print("AM ");
    else Serial.print("PM ");
    if(hm < 10) { Serial.print("0"); Serial.print(hm); Serial.print(":"); }
    else { Serial.print(hm); Serial.print(":"); }
  }
  else {
    if(h < 10) { Serial.print("0"); Serial.print(h); Serial.print(":"); }
    else { Serial.print(h); Serial.print(":"); }
  }
  if(m < 10) { Serial.print("0"); Serial.print(m); Serial.print(":"); }
  else { Serial.print(m); Serial.print(":"); }
  if(s < 10) { Serial.print("0"); Serial.println(s); }
  else Serial.println(s);
}

아두이노시계 코드는 완성되었다. 이제 ESP01을 이용해 NTP 서버에 접속하는 코드를 살펴 보겠다.

ESP01의 시리얼 통신 보드 레이트가 9600으로 설정 되어 있어야한다. 보드레이트 변경 방법은 이전 글 "아두이노 - ESP01wifi 모듈 무선 원격제어 그리고 시리얼 통신 - 6편"의 설명을 참조하기 바란다. 

우선 아래 사이트에서 ESP01 WIFI 라이브러리를 다운로드하자.

https://github.com/bportaluri/WiFiEsp

다운로드 후 압축을 풀면 아래와 같이 WiFiEsp-master 폴더 안에 똑같은 WiFiEsp-master 있는데 안의 폴더명에서 "-master"삭제하여 WiFiEsp로 변경한 뒤 폴더를 복사한다. 

 

 

내 컴퓨터의 아두이노 저장 폴더 -> 라이브러리 폴더로 이동하여 붙여 넣기를 해준다.

C:\Program Files (x86)\Arduino\libraries

WiFiEsp 라이브러리에는 WiFi를 통해 NTP 서버에 접속하여 유닉스 UTC 시간 값을 받아오는 코드가 포함되어 있고 그 시간 값으로 날짜 및 시간값으로 변경해주는 코드가 포함된 라이브러리를 다운로드하여 같은 방법으로 하나 더 추가해 준다. 

https://github.com/PaulStoffregen/Time

 

라이브러리를 추가한 뒤에는 반드시 아두이노를 종료하고 다시 시작해야만 붙여 넣은 라이브러리가 아두이노 IDE 프로그램에 반영된다. 

아두이노 IDE의 파일 -> WiFiEsp -> UdpNTPClient를 클릭해서 기본 예제를 살펴보자.

이 예제에서 수정해줘야 할 부분은 아래 코드들이다. 

// Emulate Serial1 on pins 6/7 if not present 
#ifndef HAVE_HWSERIAL1 
#include "SoftwareSerial.h" 
SoftwareSerial Serial1(6, 7); // RX, TX 
#endif

char ssid[] = "Twim";            // your network SSID (name) 
char pass[] = "12345678";        // your network password

char timeServer[] = "time.nist.gov";  // NTP server

// initialize serial for debugging 
Serial.begin(115200); 
// initialize serial for ESP module 
Serial1.begin(9600); 
// initialize ESP module 
WiFi.init(&Serial1);

 

첨부된 스케치 파일은 기본 예제를 단순화시킨 것이다. 아래 스케치에서 아래 항목에 사용 중인 와이파이 공유기의 SSID와 비밀번호를 입력해준다. 

char ssid[] = "SSID"; // 공유기 "SSID"     
char pass[] = "password"; // 공유기 "password"  

 

기본 예제의 NTP 서버 주소 "time.nist.gov"로는 연결이 되지 않는다. 다른 국가에서는 어떤지 모르겠다. 

아래 서버를 사용해주자.

char timeServer[] = "time.google.com";  // NTP server
char timeServer[] = "kr.pool.ntp.org";  // NTP server

arduino_clock_esp01_NTP_basic.ino
0.00MB

더보기
더보기
#include <SoftwareSerial.h> 
#define esp_rxPin 3 // esp01 RX -> arduino 2
#define esp_txPin 2 // esp01 TX -> arduino 3 
SoftwareSerial esp01(esp_txPin, esp_rxPin); // (RX, TX)

#include "WiFiEsp.h"
#include "WiFiEspUdp.h"

char ssid[] = "SSID"; // 공유기 "SSID"     
char pass[] = "password"; // 공유기 "password"  
int status = WL_IDLE_STATUS;     // the Wifi radio's status

char timeServer[] = "kr.pool.ntp.org";  // NTP server
//char timeServer[] = "time.google.com";  // NTP server
unsigned int localPort = 2390;        // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;  // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT = 2000;    // timeout in miliseconds to wait for an UDP packet to arrive

byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

WiFiEspUDP Udp;

void setup() {
  Serial.begin(9600);
  esp01.begin(9600);
  WiFi.init(&esp01);
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
  }
  Serial.println("You're connected to the network");
  Udp.begin(localPort);
}

void loop() {
  sendNTPpacket(timeServer); // send an NTP packet to a time server
  unsigned long startMs = millis();
  while (!Udp.available() && (millis() - startMs) < UDP_TIMEOUT) {}

  Serial.println(Udp.parsePacket());
  if (Udp.parsePacket()) {
    Serial.println("packet received");
    Udp.read(packetBuffer, NTP_PACKET_SIZE);
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    unsigned long secsSince1900 = highWord << 16 | lowWord;
    Serial.print("Seconds since Jan 1 1900 = ");
    Serial.println(secsSince1900);
    Serial.print("Unix time = ");
    const unsigned long seventyYears = 2208988800UL;
    unsigned long epoch = secsSince1900 - seventyYears;
    Serial.println(epoch);
    Serial.print("The UTC time is ");       // UTC is the time at Greenwich Meridian (GMT)
    Serial.print((epoch  % 86400L) / 3600); // print the hour (86400 equals secs per day)
    Serial.print(':');
    if (((epoch % 3600) / 60) < 10) {
      Serial.print('0');
    }
    Serial.print((epoch  % 3600) / 60); // print the minute (3600 equals secs per minute)
    Serial.print(':');
    if ((epoch % 60) < 10) {
      Serial.print('0');
    }
    Serial.println(epoch % 60); // print the second
  }
  delay(10000);
}

void sendNTPpacket(char *ntpSrv) {
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

상기의 스케치를 업로드하면 아두이노 초기화 시 WiFi공유기에 연결을 시도하고 연결이 된 다음에는 NTP 서버에 접속하여 유닉스 UTC 값을 요청하게 된다. 그렇게 수신된 유닉스 UTC값을 시간 값으로 변환하여 현재 시간으로 적용하면 시간 동기화는 완료된다. 일련의 과정이 완료되면 시리얼 모니터에 아래와 같이 출력된다. 

10초마다 값을 받아오는 것을 확인할 수 있다.

상기 코드에서는 10초마다 받아오는 데에 딜레이 함수를 사용했다. 딜레이 함수를 제거하고 NTP 수신 값이 없을 경우 2초마다 sendNTPpacket(timeServer); 를 실행하도록 millis() 함수를 사용하여 아래처럼 수정해 주었다. 

unsigned long startMs = 0; 

void NTP_send() { 
  if (got_NTP == false && Udp.available()) { // NTP 수신값이 없으면 2초마다 요청 
    if (millis() - startMs >=  2000) { 
      startMs = millis(); 
      sendNTPpacket(timeServer); 
    } 
  } 
}

 

이 라이브러리를 통해 NTP 서버로부터 얻고자 하는 값은 epoch 변수에 저장된 값이다. 이 값을 이용해 현재 시간으로 변환이 가능하다. 라이브러리 상에서 이미 시간 값을 시리얼 모니터로 출력하는 코드가 포함되어 있지만 날짜를 계산하는 코드가 빠져있다. 날짜를 계산하는 게 간단한 코드가 아닌 것이, 달력에는 윤년이라는 개념이 포함되어 있기 때문이다. 인터넷에서 윤년을 계산하는 코드들을 찾아볼 수 있는데 그리 간단한 코드는 아니다. 따라서 날짜는 Time 라이브러리를 이용하여 얻도록 하자.

 

Time 라이브러리 예제를 살펴보면 이 라이브러리를 통해서도 NTP 서버에서 유닉스 UTC값을 받아오는 코드가 있다는 것을 알 수 있을 것이다. 하지만 이 라이브러리에서는 WiFi 연결에 ESP8266WiFi.h 라이브러리를 사용하고 있다. 이 라이브러리는 통신 모듈로서 아두이노에 연결된 ESP01에서 사용하는 게 아니라 ESP8266 칩을 사용한 개발 보드(NodeMcu) 또는 통신 모듈로 서가 아닌 ESP01 자체에서 WiFi에 연결할 때 사용할 수 있다.  따라서 라이브러리에 포함된 날짜 변환 코드만을 사용하도록 하겠다. 사용방법은 간단하다. #include  <TimeLib.h> 코드를 통해 라이브러리를 불러오고 아래처럼 라이브러리에서 제공하는 변환 함수에 epoch 값을 대입해 주기만 하면 된다. 

 

년 : year(epoch)

월: month(epoch)

일: day(epoch)

요일: weekday(epoch) // 일요일 = 1

시: hour(epoch)

분: minute(epoch)

초: second(epoch)

epoch값에 uint8_t timezone = 9; 를 보정해 주었다. 

unsigned long epoch = secsSince1900 - seventyYears + timeZone * 3600;

arduino_clock_esp01_NTP_date.ino
0.00MB

더보기
더보기
#include <SoftwareSerial.h> 
#define esp_rxPin 3 // esp01 RX -> arduino 2
#define esp_txPin 2 // esp01 TX -> arduino 3 
SoftwareSerial esp01(esp_txPin, esp_rxPin); // (RX, TX)

#include "WiFiEsp.h"
#include "WiFiEspUdp.h"
#include <TimeLib.h>

char ssid[] = "SSID"; // 공유기 "SSID"     
char pass[] = "password"; // 공유기 "password"  
int status = WL_IDLE_STATUS;     // the Wifi radio's status

//char timeServer[] = "time.google.com";  // NTP server
char timeServer[] = "kr.pool.ntp.org";  // NTP server
unsigned int localPort = 2390;        // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;  // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT = 2000;    // timeout in miliseconds to wait for an UDP packet to arrive

byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

WiFiEspUDP Udp;

bool got_NTP = false;
uint8_t timeZone = 9;

void setup() {
  Serial.begin(9600);
  esp01.begin(9600);
  WiFi.init(&esp01);
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
  }
  Serial.println("You're connected to the network");
  Udp.begin(localPort);
}

void loop() {
  NTP_send();
  getNtpTime();
}

unsigned long startMs = 0;

void NTP_send() {
  if (got_NTP == false) { // NTP 수신값이 없으면 2초마다 요청
    if (millis() - startMs >=  2000) {
      startMs = millis();
      sendNTPpacket(timeServer);
    }
  }
}

void getNtpTime() {
  if (got_NTP == false) {
    if (Udp.available()) {
      if (Udp.parsePacket()) { // 수신된 값이 있으면
        Udp.read(packetBuffer, NTP_PACKET_SIZE);
        unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
        unsigned long secsSince1900 = highWord << 16 | lowWord;
        const unsigned long seventyYears = 2208988800UL;
        unsigned long epoch = secsSince1900 - seventyYears + timeZone * 3600;
        Serial.print(year(epoch));
        Serial.print(".");
        Serial.print(month(epoch));
        Serial.print(".");
        Serial.print(day(epoch)); 
        Serial.print(' ');
        Serial.print(hour(epoch)); 
        Serial.print(':');
        if (minute(epoch) < 10) Serial.print('0'); 
        Serial.print(minute(epoch)); 
        Serial.print(':');
        if (second(epoch) < 10) Serial.print('0'); 
        Serial.println(second(epoch)); // print the second
        got_NTP = true;
      }
    }
  }
}

void sendNTPpacket(char *ntpSrv) {
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

상기의 스케치를 아두이노에 업로드하면 시리얼 모니터에 아래와 같이 표시된다. 

수신된 값과 인터넷의 UTC 시간(https://time.is/ko/UTC)과 비교해 본 결과 거의 차이가 없는 것을 확인했다.

이제 수신된 시간 값을 아두이노시계 코드에 적용시켜 보자.

arduino_clock_time_adjust.ino 코드와 arduino_clock_esp01_NTP_date.ino 코드를 합쳐주고 아래 코드를 추가하여 요일도 출력하도록 해 주었다. 또한 1시 1분 10초에 동기화를 실행하는 코드도 추가해 주었다.

// 변수에 시간값 대입
yy = year(epoch); 
mm = month(epoch); 
dd = day(epoch);  
h = hour(epoch);  
m = minute(epoch);  
s = second(epoch);  
week_num = weekday(epoch); // 일요일 1, 월요일 2, , , , , 토요일 7
week_day_converter();

// 시리얼 텍스트 명령어 수신부 요일 명령어 추가
else if (temp == "ww") { week_num++; adjust = true; display_t = true; }
else if (temp == "w") { week_num--; adjust = true; display_t = true; }

// 요일 조정 함수
if (week_num > 7) week_num = 1; 
else if (week_num < 1) week_num = 7;  
week_day_converter();

// 요일에 대한 숫자값을 스트링 값으로 변환하는 함수 추가
void week_day_converter() { 
  switch (week_num) { 
    case 1:   week_day = "SUN"; break; 
    case 2:   week_day = "MON"; break; 
    case 3:   week_day = "TUE"; break; 
    case 4:   week_day = "WED"; break; 
    case 5:   week_day = "THU"; break; 
    case 6:   week_day = "FRI"; break; 
    case 7:   week_day = "SAT"; break; 
  } 
}

// 1시 1분 10초 동기화 코드
if (h == 1 && m == 1 && s == 10) { // NTP 동기화 시간
      got_NTP = false; 
}

아래 스케치를 아두이노에 업로드하고 동기화가 잘되는지 확인해보고 tset 텍스트 명령어를 통해 시간을 변경하여 1시 1분 10초에 동기화가 잘되는지도 확인해 보자.

arduino_clock_time_adjust_NTP.ino
0.01MB

더보기
더보기
#include <SoftwareSerial.h> 
#define esp_rxPin 3 // esp01 RX -> arduino 2
#define esp_txPin 2 // esp01 TX -> arduino 3 
SoftwareSerial esp01(esp_txPin, esp_rxPin); // (RX, TX)

#include "WiFiEsp.h"
#include "WiFiEspUdp.h"
#include <TimeLib.h>

char ssid[] = "SSID"; // 공유기 "SSID"     
char pass[] = "password"; // 공유기 "password" 
int status = WL_IDLE_STATUS;     // the Wifi radio's status

//char timeServer[] = "time.google.com";  // NTP server
char timeServer[] = "kr.pool.ntp.org";  // NTP server
unsigned int localPort = 2390;        // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;  // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT = 2000;    // timeout in miliseconds to wait for an UDP packet to arrive

byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

WiFiEspUDP Udp;

bool got_NTP = false;
uint8_t timeZone = 9;

int h = 12; // initial Time display is 12:59:45 PM
int m = 59;
uint8_t s = 45;
bool meridian = true;
uint8_t hm;
bool now_am = true; //AM
uint16_t yy = 2019;
uint8_t mm = 1;
uint8_t dd = 1;
uint8_t week_num = 1;
String week_day = "SUN";
bool adjust = false;

bool display_t = false;

unsigned long int start_time = 0;

void setup() {
  Serial.begin(9600);
  esp01.begin(9600);
  WiFi.init(&esp01);
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
  }
  Serial.println("You're connected to the network");
  Udp.begin(localPort);
}

void loop() {
  cal_time();
  adjust_time();
  NTP_send();
  getNtpTime();
}

void adjust_time() {
  if (Serial.available() > 0) {
    String temp = Serial.readStringUntil('\n');
    if (temp == "dtime") display_time();
    else if (temp == "s") { s = 0; start_time = millis(); display_t = true; }
    else if (temp == "mm") { m++; display_t = true; }
    else if (temp == "m") { m--; display_t = true; }
    else if (temp == "hh") { h++; display_t = true; }
    else if (temp == "h") { h--; display_t = true; }
    else if (temp == "24") { meridian = !meridian; display_t = true; }
    else if (temp == "yy") { yy++; display_t = true; }
    else if (temp == "y") { yy--; display_t = true; }
    else if (temp == "mon") { mm++; adjust = true; display_t = true; }
    else if (temp == "mon-") { mm--; adjust = true; display_t = true; }
    else if (temp == "dd") { dd++; adjust = true; display_t = true; }
    else if (temp == "d") { dd--; adjust = true; display_t = true; }
    else if (temp == "ww") { week_num++; adjust = true; display_t = true; }
    else if (temp == "w") { week_num--; adjust = true; display_t = true; }
    else if (temp.startsWith("tset")) {
      String set = temp.substring(4, 6);
      h = set.toInt();
      set = temp.substring(6, 8);
      m = set.toInt();
      set = temp.substring(8, 10);
      s = set.toInt();
      start_time = millis();
      adjust = true;
      display_t = true;
    }
    else if (temp.startsWith("dset")) {
      String set = temp.substring(4, 8);
      yy = set.toInt();
      set = temp.substring(8, 10);
      mm = set.toInt();
      set = temp.substring(10, 12);
      dd = set.toInt();
      adjust = true;
      display_t = true;
    }
    if (m >= 60) m = 0;
    else if (m < 0) m = 59;
    if (h >= 24) h = 0; 
    else if (h < 0) h = 23; 
    if (h < 12) now_am = true;
    else now_am = false; 
    hm = h; 
    if (meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    cal_dd();
    if (display_t == true) { display_time(); display_t = false; }
  }
}

void cal_time() {
  if (millis() - start_time >= 1000) { // 시간 간격: 밀리초
    start_time = millis(); // 상기 조건을 만족할때의 밀리초를 다시 start_time에 저장하여 조건 초기화
    s++;
    if (s == 60) {
      s = 0;
      m++;
    }
    if(m == 60) { m = 0; h++; }
    if(h == 12) { now_am = false;}
    else if (h == 24) { h = 0; now_am = true; dd++; week_num++; cal_dd(); } 
    hm = h;
    if(meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    else now_am = true;
    display_time();
    if (h == 1 && m == 1 && s == 10) { // NTP 동기화 시간
      got_NTP = false; 
    }
  }
}

void cal_dd() {
  uint8_t m_dds;
  switch (mm) {
    case 1:   m_dds = 31; break;
    case 2:   m_dds = 28; break;
    case 3:   m_dds = 31; break;
    case 4:   m_dds = 30; break;
    case 5:   m_dds = 31; break;
    case 6:   m_dds = 30; break;
    case 7:   m_dds = 31; break;
    case 8:   m_dds = 31; break;
    case 9:   m_dds = 30; break;
    case 10:  m_dds = 31; break;
    case 11:  m_dds = 30; break;
    case 12:  m_dds = 31; break;
  }
  if (adjust == false) {
    if (dd > m_dds) { dd = 1; mm++; }
    if (mm > 12) { mm = 1; yy++; }
    if (week_num > 7) week_num = 1;
    week_day_converter();
  }
  else {
    if (dd > m_dds) dd = 1;
    else if (dd < 1) dd = m_dds;
    if (mm > 12) mm = 1;
    else if (mm < 1) mm = 12;
    if (week_num > 7) week_num = 1;
    else if (week_num < 1) week_num = 7; 
    week_day_converter();
    adjust = false;
  }
}

void week_day_converter() {
  switch (week_num) {
    case 1:   week_day = "SUN"; break;
    case 2:   week_day = "MON"; break;
    case 3:   week_day = "TUE"; break;
    case 4:   week_day = "WED"; break;
    case 5:   week_day = "THU"; break;
    case 6:   week_day = "FRI"; break;
    case 7:   week_day = "SAT"; break;
  }
}

void display_time() {
  Serial.print(yy); Serial.print("."); 
  if (mm < 10) { Serial.print("0"); Serial.print(mm); Serial.print("."); }
  else { Serial.print(mm); Serial.print("."); }
  if (dd < 10) { Serial.print("0"); Serial.print(dd); Serial.print(". "); }
  else { Serial.print(dd); Serial.print(". "); }
  Serial.print(week_day); Serial.print(" ");  
  if (meridian == true) {
    if (now_am == true) Serial.print("AM ");
    else Serial.print("PM ");
    if (hm < 10) { Serial.print("0"); Serial.print(hm); Serial.print(":"); }
    else { Serial.print(hm); Serial.print(":"); }
  }
  else {
    if (h < 10) { Serial.print("0"); Serial.print(h); Serial.print(":"); }
    else { Serial.print(h); Serial.print(":"); }
  }
  if (m < 10) { Serial.print("0"); Serial.print(m); Serial.print(":"); }
  else { Serial.print(m); Serial.print(":"); }
  if (s < 10) { Serial.print("0"); Serial.println(s); }
  else Serial.println(s);
}

unsigned long startMs = 0;

void NTP_send() {
  if (got_NTP == false) { // NTP 수신값이 없으면 2초마다 요청
    if (millis() - startMs >=  2000) {
      startMs = millis();
      sendNTPpacket(timeServer);
    }
  }
}

void getNtpTime() {
  if (got_NTP == false) {
    if (Udp.available()) {
      if (Udp.parsePacket()) { // 수신된 값이 있으면
        Udp.read(packetBuffer, NTP_PACKET_SIZE);
        unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
        unsigned long secsSince1900 = highWord << 16 | lowWord;
        const unsigned long seventyYears = 2208988800UL;
        unsigned long epoch = secsSince1900 - seventyYears + timeZone * 3600;
        yy = year(epoch);
        mm = month(epoch);
        dd = day(epoch); 
        h = hour(epoch); 
        m = minute(epoch); 
        s = second(epoch); 
        week_num = weekday(epoch);
        week_day_converter();
        got_NTP = true;
      }
    }
  }
}

void sendNTPpacket(char *ntpSrv) {
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

상기 코드로 아두이노시계를 작동시키고 수동으로 동기화를 시켜 인터넷 시간과의 오차를 측정해보니 1000 밀리초 기준에서 약 20분 동안 7초 정도 느려지는 것을 알게 되었다. 아두이노의 크리스털 발진기의 속도차에 의해 발생하는 것인지 코드에 영향을 받아 발생했는지는 알 수 없으나 현 상태에서 1초를 증가시키는 기준값인 1000을 994로 변경해주었다. 즉, 아두이노의 994 밀리 초가 1초와 가장 근접해진 것이다. 이는 아두이노마다 다를 수 있고 코드에 따라 다를 수도 있다. 테스트를 해보고 값을 정하면 될 것 같다. 

 

 

NTP 서버와 수동 동기화를 하기 위해 아래와 같이 텍스트 명령어 코드를 추가해 주어 시리얼 모니터에서 "ntp"를 입력하여 수동 동기화시킨 후 일정 시간 경과 뒤에 몇 초 정도 차이가 나는지 살펴보면서 조정해주면 된다.

 

else if (temp == "ntp") got_NTP = false;

 

기준값이 994일 때 1시간에 1.5초 정도 빨라졌다. 기준값이 995일 때 느려졌으므로 995 ~ 994 사이 값이 더 정확한 값이 될 거 같다. 하지만 millis() 값은 소수를 적용할 수 없어서 아래처럼 플래그를 하나 설정해주고 플래그에 따라 기준값을 995, 994로 값을 변경하여 중간값을 도출하도록 하였다. 

bool change_millis = false;  
uint16_t millis_sec = 994;

change_millis = ! change_millis; 
if (change_millis == true)  millis_sec = 995; 
else millis_sec = 994;

 

이제 거의 오차가 없어진 거 같다. 하루에 1 ~ 2초 정도의 오차가 발생될 것으로 예상하고 매일 한번 이상 동기화를 시켜준다면 차이를 못 느낄 것이다.  

arduino_clock_time_adjust_NTP_set_millis.ino
0.01MB

더보기
더보기
#include <SoftwareSerial.h> 
#define esp_rxPin 3 // esp01 RX -> arduino 2
#define esp_txPin 2 // esp01 TX -> arduino 3 
SoftwareSerial esp01(esp_txPin, esp_rxPin); // (RX, TX)

#include "WiFiEsp.h"
#include "WiFiEspUdp.h"
#include <TimeLib.h>

char ssid[] = "SSID"; // 공유기 "SSID"     
char pass[] = "password"; // 공유기 "password" 
int status = WL_IDLE_STATUS;     // the Wifi radio's status

//char timeServer[] = "time.google.com";  // NTP server
char timeServer[] = "kr.pool.ntp.org";  // NTP server
unsigned int localPort = 2390;        // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;  // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT = 2000;    // timeout in miliseconds to wait for an UDP packet to arrive

byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

WiFiEspUDP Udp;

bool got_NTP = false;
uint8_t timeZone = 9;

int h = 12; // initial Time display is 12:59:45 PM
int m = 59;
uint8_t s = 45;
bool meridian = true;
uint8_t hm;
bool now_am = true; //AM
uint16_t yy = 2019;
uint8_t mm = 1;
uint8_t dd = 1;
uint8_t week_num = 1;
String week_day = "SUN";
bool adjust = false;

bool display_t = false;

unsigned long int start_time = 0;

void setup() {
  Serial.begin(9600);
  esp01.begin(9600);
  WiFi.init(&esp01);
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
  }
  Serial.println("You're connected to the network");
  Udp.begin(localPort);
}

void loop() {
  cal_time();
  adjust_time();
  NTP_send();
  getNtpTime();
}

void adjust_time() {
  if (Serial.available() > 0) {
    String temp = Serial.readStringUntil('\n');
    if (temp == "dtime") display_time();
    else if (temp == "s") { s = 0; start_time = millis(); display_t = true; }
    else if (temp == "mm") { m++; display_t = true; }
    else if (temp == "m") { m--; display_t = true; }
    else if (temp == "hh") { h++; display_t = true; }
    else if (temp == "h") { h--; display_t = true; }
    else if (temp == "24") { meridian = !meridian; display_t = true; }
    else if (temp == "yy") { yy++; display_t = true; }
    else if (temp == "y") { yy--; display_t = true; }
    else if (temp == "mon") { mm++; adjust = true; display_t = true; }
    else if (temp == "mon-") { mm--; adjust = true; display_t = true; }
    else if (temp == "dd") { dd++; adjust = true; display_t = true; }
    else if (temp == "d") { dd--; adjust = true; display_t = true; }
    else if (temp == "ww") { week_num++; adjust = true; display_t = true; }
    else if (temp == "w") { week_num--; adjust = true; display_t = true; }
    else if (temp == "ntp") got_NTP = false;
    else if (temp.startsWith("tset")) {
      String set = temp.substring(4, 6);
      h = set.toInt();
      set = temp.substring(6, 8);
      m = set.toInt();
      set = temp.substring(8, 10);
      s = set.toInt();
      start_time = millis();
      adjust = true;
      display_t = true;
    }
    else if (temp.startsWith("dset")) {
      String set = temp.substring(4, 8);
      yy = set.toInt();
      set = temp.substring(8, 10);
      mm = set.toInt();
      set = temp.substring(10, 12);
      dd = set.toInt();
      adjust = true;
      display_t = true;
    }
    if (m >= 60) m = 0;
    else if (m < 0) m = 59;
    if (h >= 24) h = 0; 
    else if (h < 0) h = 23; 
    if (h < 12) now_am = true;
    else now_am = false; 
    hm = h; 
    if (meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    cal_dd();
    if (display_t == true) { display_time(); display_t = false; }
  }
}

bool change_millis = false; 
uint16_t millis_sec = 994;

void cal_time() {
  if (millis() - start_time >= millis_sec) { // 시간 간격: 밀리초
    start_time = millis(); // 상기 조건을 만족할때의 밀리초를 다시 start_time에 저장하여 조건 초기화
    change_millis = ! change_millis;
    if (change_millis == true)  millis_sec = 995;
    else millis_sec = 994;
    s++;
    if (s == 60) {
      s = 0;
      m++;
    }
    if(m == 60) { m = 0; h++; }
    if(h == 12) { now_am = false;}
    else if (h == 24) { h = 0; now_am = true; dd++; week_num++; cal_dd(); } 
    hm = h;
    if(meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    else now_am = true;
    display_time();
    if (h == 1 && m == 1 && s == 10) { // NTP 동기화 시간
      got_NTP = false; 
    }
  }
}

void cal_dd() {
  uint8_t m_dds;
  switch (mm) {
    case 1:   m_dds = 31; break;
    case 2:   m_dds = 28; break;
    case 3:   m_dds = 31; break;
    case 4:   m_dds = 30; break;
    case 5:   m_dds = 31; break;
    case 6:   m_dds = 30; break;
    case 7:   m_dds = 31; break;
    case 8:   m_dds = 31; break;
    case 9:   m_dds = 30; break;
    case 10:  m_dds = 31; break;
    case 11:  m_dds = 30; break;
    case 12:  m_dds = 31; break;
  }
  if (adjust == false) {
    if (dd > m_dds) { dd = 1; mm++; }
    if (mm > 12) { mm = 1; yy++; }
    if (week_num > 7) week_num = 1;
    week_day_converter();
  }
  else {
    if (dd > m_dds) dd = 1;
    else if (dd < 1) dd = m_dds;
    if (mm > 12) mm = 1;
    else if (mm < 1) mm = 12;
    if (week_num > 7) week_num = 1;
    else if (week_num < 1) week_num = 7; 
    week_day_converter();
    adjust = false;
  }
}

void week_day_converter() {
  switch (week_num) {
    case 1:   week_day = "SUN"; break;
    case 2:   week_day = "MON"; break;
    case 3:   week_day = "TUE"; break;
    case 4:   week_day = "WED"; break;
    case 5:   week_day = "THU"; break;
    case 6:   week_day = "FRI"; break;
    case 7:   week_day = "SAT"; break;
  }
}

void display_time() {
  Serial.print(yy); Serial.print("."); 
  if (mm < 10) { Serial.print("0"); Serial.print(mm); Serial.print("."); }
  else { Serial.print(mm); Serial.print("."); }
  if (dd < 10) { Serial.print("0"); Serial.print(dd); Serial.print(". "); }
  else { Serial.print(dd); Serial.print(". "); }
  Serial.print(week_day); Serial.print(" ");  
  if (meridian == true) {
    if (now_am == true) Serial.print("AM ");
    else Serial.print("PM ");
    if (hm < 10) { Serial.print("0"); Serial.print(hm); Serial.print(":"); }
    else { Serial.print(hm); Serial.print(":"); }
  }
  else {
    if (h < 10) { Serial.print("0"); Serial.print(h); Serial.print(":"); }
    else { Serial.print(h); Serial.print(":"); }
  }
  if (m < 10) { Serial.print("0"); Serial.print(m); Serial.print(":"); }
  else { Serial.print(m); Serial.print(":"); }
  if (s < 10) { Serial.print("0"); Serial.println(s); }
  else Serial.println(s);
}

unsigned long startMs = 0;

void NTP_send() {
  if (got_NTP == false) { // NTP 수신값이 없으면 2초마다 요청
    if (millis() - startMs >=  2000) {
      startMs = millis();
      sendNTPpacket(timeServer);
    }
  }
}

void getNtpTime() {
  if (got_NTP == false) {
    if (Udp.available()) {
      if (Udp.parsePacket()) { // 수신된 값이 있으면
        Udp.read(packetBuffer, NTP_PACKET_SIZE);
        unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
        unsigned long secsSince1900 = highWord << 16 | lowWord;
        const unsigned long seventyYears = 2208988800UL;
        unsigned long epoch = secsSince1900 - seventyYears + timeZone * 3600;
        yy = year(epoch);
        mm = month(epoch);
        dd = day(epoch); 
        h = hour(epoch); 
        m = minute(epoch); 
        s = second(epoch); 
        week_num = weekday(epoch);
        week_day_converter();
        got_NTP = true;
      }
    }
  }
}

void sendNTPpacket(char *ntpSrv) {
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

이제 크리스털 LCD를 연결하여 시간값을 표시해보자. 

필자는 리퀴드 크리스털 LCD I2C버전을 갖고있다. 일반버전과의 차이는 아두이노와의 연결에 I2C 통신을 사용하므로 연결선이 GND, VCC, SDA, SCL 4개만으로 제어가 가능하다는 장점이 있고 나머지 기능들은 같다. 이 글에서는 리퀴드 크리스탈 LCD I2C로 진행하겠다. 

 

아두이노에서 SDA핀은 A4핀, SCL핀은 A5번 핀이다. 아래 그림처럼 연결해주면 된다. 

아두이노에 기본 설치된 리퀴드 크리스탈 LCD라이브러리는 일반 버전용이다. 리퀴드 크리스탈 LCD I2C를 사용하기 위해서는 따로 I2C용 라이브러리를 설치해야 한다. 

 

라이브러리를 아래 사이트에서 다운로드하고 아두이노 라이브러리 폴더에 붙여 넣기 하여 라이브러리를 설치해준다.  

https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

 

아래 기본 예제를 살펴보면 초기화에 I2C address 0x27을 사용했다. 아래 코드를 아두이노에 업로드한 뒤에 크리스털 LCD에 Hello, world! 가 출력된다면 문제는 없다. 하지만 출력이 되지 않는다면 점퍼선의 연결이 잘못되었거나 I2C address가 0x27이 아닌 다른 값일 경우 발생할 수 있다.  

LiquidCrystal_I2C_basic.ino
0.00MB

#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2); // Set the LCD address to 0x27 for a 16 chars and 2 line display

void setup() {
  lcd.begin();
  lcd.backlight();
  lcd.setCursor(0,0);  // row, col의 좌표로 커서를 위치 col: 0 ~ 15 row: 0, 1
  lcd.print("Hello, world!");
  lcd.setCursor(0,1);
  lcd.print("Arduino clock");
}

void loop() {
}

LCD 출력이 되지 않는다면 아래 I2C scanner 스케치를 아두이노에 업로드해주고 시리얼 모니터에 출력되는 주소 값을 확인해 보자. 아래 그림처럼 address 값을 찾아 시리얼 모니터에 출력해준다.

I2C_scanner.ino
0.00MB

리퀴드 크리스털 LCD의 명령어 함수

lcd.begin();                  LCD를 사용을 시작
lcd.display();                LCD에 내용을 표시
lcd.noDisplay();            LCD에 내용을 숨김
lcd.setCursor(col, low);  row, col의 좌표로 커서를 위치 col: 0 ~ 15 row: 0, 1
lcd.cursor();                 LCD에 커서를 표시
lcd.noCursor();             LCD에 커서를 숨김
lcd.home();                  커서의 위치를 0,0으로 이동
lcd.blink();                   커서를 깜빡임
lcd.noBlink();                커서를 깜빡이지 않음
lcd.backlight();              LCD backlight을 킴
lcd.noBacklight();          LCD backlight를 끔
lcd.write(val);                LCD 화면에 val 출력(아스키코드 입력 시에는 아스키코드에 해당하는 문자 출력)
lcd.print(val);                LCD 화면에 val(char, byte, int, long, or string) 출력, 

lcd.print(val, BASE) 

lcd.print(i, HEX);            BIN, DEC, OCT
lcd.clear();                    LCD 화면의 모든 내용 지움
lcd.scrollDisplayRight();   내용을 우측으로 1칸 이동
lcd.scrollDisplayLeft();     내용을 좌측으로 1칸 이동
lcd.autoscroll();             내용을 자동으로 우에서 좌로 스크롤

 

아래 코드를 display_time() 사용자 함수 시리얼 모니터 출력 코드 아래에 추가해 주었다. 

lcd.setCursor(0,0); 
lcd.print(yy); lcd.print("."); 
if (mm < 10) { lcd.print("0"); lcd.print(mm); lcd.print("."); }
else { lcd.print(mm); lcd.print("."); }
if (dd < 10) { lcd.print("0"); lcd.print(dd); lcd.print(". "); }
else { lcd.print(dd); lcd.print(". "); }
lcd.print(week_day); lcd.print(" ");  
lcd.setCursor(0,1);
if (meridian == true) {
  if (now_am == true) lcd.print("AM ");
  else lcd.print("PM ");
  if (hm < 10) { lcd.print("0"); lcd.print(hm); lcd.print(":"); }
  else { lcd.print(hm); lcd.print(":"); }
}
else {
  if (h < 10) { lcd.print("0"); lcd.print(h); lcd.print(":"); }
  else { lcd.print(h); lcd.print(":"); }
}
if (m < 10) { lcd.print("0"); lcd.print(m); lcd.print(":"); }
else { lcd.print(m); lcd.print(":"); }
if (s < 10) { lcd.print("0"); lcd.print(s); lcd.print("     "); }
else { lcd.print(s); lcd.print("     "); }  // 크리스탈 LCD에는 pritln 명령어 없음

arduino_clock_time_adjust_NTP_set_millis_Lcd.ino
0.01MB

더보기
더보기
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2); // Set the LCD address to 0x27 for a 16 chars and 2 line display

#include <SoftwareSerial.h> 
#define esp_rxPin 3 // esp01 RX -> arduino 2
#define esp_txPin 2 // esp01 TX -> arduino 3 
SoftwareSerial esp01(esp_txPin, esp_rxPin); // (RX, TX)

#include "WiFiEsp.h"
#include "WiFiEspUdp.h"
#include <TimeLib.h>

char ssid[] = "SSID"; // 공유기 "SSID"     
char pass[] = "password"; // 공유기 "password" 
int status = WL_IDLE_STATUS;     // the Wifi radio's status

//char timeServer[] = "time.google.com";  // NTP server
char timeServer[] = "kr.pool.ntp.org";  // NTP server
unsigned int localPort = 2390;        // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48;  // NTP timestamp is in the first 48 bytes of the message
const int UDP_TIMEOUT = 2000;    // timeout in miliseconds to wait for an UDP packet to arrive

byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

WiFiEspUDP Udp;

bool got_NTP = false;
uint8_t timeZone = 9;

int h = 12; // initial Time display is 12:59:45 PM
int m = 59;
uint8_t s = 45;
bool meridian = true;
uint8_t hm;
bool now_am = true; //AM
uint16_t yy = 2019;
uint8_t mm = 1;
uint8_t dd = 1;
uint8_t week_num = 1;
String week_day = "SUN";
bool adjust = false;

bool display_t = false;

unsigned long int start_time = 0;

void setup() {
  Serial.begin(9600);
  esp01.begin(9600);
  WiFi.init(&esp01);
  lcd.begin();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(0,0);  // row, col의 좌표로 커서를 위치 col: 0 ~ 15 row: 0, 1
  lcd.print("Arduino clock");
  lcd.setCursor(0,1);
  lcd.print("Connecting...");
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
  }
  lcd.setCursor(0,1);
  lcd.print("                ");
  lcd.setCursor(0,1);
  lcd.print("Connected!!");
  Serial.println("You're connected to the network");
  Udp.begin(localPort);
}

void loop() {
  cal_time();
  adjust_time();
  NTP_send();
  getNtpTime();
}

void adjust_time() {
  if (Serial.available() > 0) {
    String temp = Serial.readStringUntil('\n');
    if (temp == "dtime") display_time();
    else if (temp == "s") { s = 0; start_time = millis(); display_t = true; }
    else if (temp == "mm") { m++; display_t = true; }
    else if (temp == "m") { m--; display_t = true; }
    else if (temp == "hh") { h++; display_t = true; }
    else if (temp == "h") { h--; display_t = true; }
    else if (temp == "24") { meridian = !meridian; display_t = true; }
    else if (temp == "yy") { yy++; display_t = true; }
    else if (temp == "y") { yy--; display_t = true; }
    else if (temp == "mon") { mm++; adjust = true; display_t = true; }
    else if (temp == "mon-") { mm--; adjust = true; display_t = true; }
    else if (temp == "dd") { dd++; adjust = true; display_t = true; }
    else if (temp == "d") { dd--; adjust = true; display_t = true; }
    else if (temp == "ww") { week_num++; adjust = true; display_t = true; }
    else if (temp == "w") { week_num--; adjust = true; display_t = true; }
    else if (temp == "ntp") got_NTP = false;
    else if (temp.startsWith("tset")) {
      String set = temp.substring(4, 6);
      h = set.toInt();
      set = temp.substring(6, 8);
      m = set.toInt();
      set = temp.substring(8, 10);
      s = set.toInt();
      start_time = millis();
      adjust = true;
      display_t = true;
    }
    else if (temp.startsWith("dset")) {
      String set = temp.substring(4, 8);
      yy = set.toInt();
      set = temp.substring(8, 10);
      mm = set.toInt();
      set = temp.substring(10, 12);
      dd = set.toInt();
      adjust = true;
      display_t = true;
    }
    if (m >= 60) m = 0;
    else if (m < 0) m = 59;
    if (h >= 24) h = 0; 
    else if (h < 0) h = 23; 
    if (h < 12) now_am = true;
    else now_am = false; 
    hm = h; 
    if (meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    cal_dd();
    if (display_t == true) { display_time(); display_t = false; }
  }
}

bool change_millis = false; 
uint16_t millis_sec = 994;

void cal_time() {
  if (millis() - start_time >= millis_sec) { // 시간 간격: 밀리초
    start_time = millis(); // 상기 조건을 만족할때의 밀리초를 다시 start_time에 저장하여 조건 초기화
    change_millis = ! change_millis;
    if (change_millis == true)  millis_sec = 995;
    else millis_sec = 994;
    s++;
    if (s == 60) {
      s = 0;
      m++;
    }
    if(m == 60) { m = 0; h++; }
    if(h == 12) { now_am = false;}
    else if (h == 24) { h = 0; now_am = true; dd++; week_num++; cal_dd(); } 
    hm = h;
    if(meridian == true && hm == 12) { now_am = false; }
    else if (meridian == true && hm > 12) { hm = hm - 12; }
    else now_am = true;
    display_time();
    if (h == 1 && m == 1 && s == 10) { // NTP 동기화 시간
      got_NTP = false; 
    }
  }
}

void cal_dd() {
  uint8_t m_dds;
  switch (mm) {
    case 1:   m_dds = 31; break;
    case 2:   m_dds = 28; break;
    case 3:   m_dds = 31; break;
    case 4:   m_dds = 30; break;
    case 5:   m_dds = 31; break;
    case 6:   m_dds = 30; break;
    case 7:   m_dds = 31; break;
    case 8:   m_dds = 31; break;
    case 9:   m_dds = 30; break;
    case 10:  m_dds = 31; break;
    case 11:  m_dds = 30; break;
    case 12:  m_dds = 31; break;
  }
  if (adjust == false) {
    if (dd > m_dds) { dd = 1; mm++; }
    if (mm > 12) { mm = 1; yy++; }
    if (week_num > 7) week_num = 1;
    week_day_converter();
  }
  else {
    if (dd > m_dds) dd = 1;
    else if (dd < 1) dd = m_dds;
    if (mm > 12) mm = 1;
    else if (mm < 1) mm = 12;
    if (week_num > 7) week_num = 1;
    else if (week_num < 1) week_num = 7; 
    week_day_converter();
    adjust = false;
  }
}

void week_day_converter() {
  switch (week_num) {
    case 1:   week_day = "SUN"; break;
    case 2:   week_day = "MON"; break;
    case 3:   week_day = "TUE"; break;
    case 4:   week_day = "WED"; break;
    case 5:   week_day = "THU"; break;
    case 6:   week_day = "FRI"; break;
    case 7:   week_day = "SAT"; break;
  }
}

void display_time() {
  Serial.print(yy); Serial.print("."); 
  if (mm < 10) { Serial.print("0"); Serial.print(mm); Serial.print("."); }
  else { Serial.print(mm); Serial.print("."); }
  if (dd < 10) { Serial.print("0"); Serial.print(dd); Serial.print(". "); }
  else { Serial.print(dd); Serial.print(". "); }
  Serial.print(week_day); Serial.print(" ");  
  if (meridian == true) {
    if (now_am == true) Serial.print("AM ");
    else Serial.print("PM ");
    if (hm < 10) { Serial.print("0"); Serial.print(hm); Serial.print(":"); }
    else { Serial.print(hm); Serial.print(":"); }
  }
  else {
    if (h < 10) { Serial.print("0"); Serial.print(h); Serial.print(":"); }
    else { Serial.print(h); Serial.print(":"); }
  }
  if (m < 10) { Serial.print("0"); Serial.print(m); Serial.print(":"); }
  else { Serial.print(m); Serial.print(":"); }
  if (s < 10) { Serial.print("0"); Serial.println(s); }
  else Serial.println(s);
  lcd.setCursor(0,0); 
  lcd.print(yy); lcd.print("."); 
  if (mm < 10) { lcd.print("0"); lcd.print(mm); lcd.print("."); }
  else { lcd.print(mm); lcd.print("."); }
  if (dd < 10) { lcd.print("0"); lcd.print(dd); lcd.print(". "); }
  else { lcd.print(dd); lcd.print(". "); }
  lcd.print(week_day); lcd.print(" ");  
  lcd.setCursor(0,1);
  if (meridian == true) {
    if (now_am == true) lcd.print("AM ");
    else lcd.print("PM ");
    if (hm < 10) { lcd.print("0"); lcd.print(hm); lcd.print(":"); }
    else { lcd.print(hm); lcd.print(":"); }
  }
  else {
    if (h < 10) { lcd.print("0"); lcd.print(h); lcd.print(":"); }
    else { lcd.print(h); lcd.print(":"); }
  }
  if (m < 10) { lcd.print("0"); lcd.print(m); lcd.print(":"); }
  else { lcd.print(m); lcd.print(":"); }
  if (s < 10) { lcd.print("0"); lcd.print(s); lcd.print("     "); }
  else { lcd.print(s); lcd.print("     "); }  // 크리스탈 LCD에는 pritln 명령어 없음
}

unsigned long startMs = 0;

void NTP_send() {
  if (got_NTP == false) { // NTP 수신값이 없으면 2초마다 요청
    if (millis() - startMs >=  2000) {
      startMs = millis();
      sendNTPpacket(timeServer);
    }
  }
}

void getNtpTime() {
  if (got_NTP == false) {
    if (Udp.available()) {
      if (Udp.parsePacket()) { // 수신된 값이 있으면
        Udp.read(packetBuffer, NTP_PACKET_SIZE);
        unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
        unsigned long secsSince1900 = highWord << 16 | lowWord;
        const unsigned long seventyYears = 2208988800UL;
        unsigned long epoch = secsSince1900 - seventyYears + timeZone * 3600;
        yy = year(epoch);
        mm = month(epoch);
        dd = day(epoch); 
        h = hour(epoch); 
        m = minute(epoch); 
        s = second(epoch); 
        week_num = weekday(epoch);
        week_day_converter();
        got_NTP = true;
      }
    }
  }
}

void sendNTPpacket(char *ntpSrv) {
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

다음 편에는 상기 아두이노시계 코드와 DFplayer 모듈을 이용하여 말하는 알람시계를 구현해 보겠다. 

아두이노 시계코드의 활용

아두이노 시계를 단순히 시간을 표시하는 장치로서 사용하게 되면 큰 의미가 없다. 이미 값싸고 작고 모양도 좋은 시계들이 많기 때문에 기성품을 구입하는게 현명한 방법이다. 시계코드를 이용하여 아두이노를 어떤 장치를 구동시키는 타이머나 스케쥴러로 구성을 한다면 필요에 따라 유연성있게 여러 장치들을 제어 할 수 있고 또한, millis() 함수를 이용하여 타이머나 스케줄러를 코딩하는 것보다 그 제어를 편리하게 할 수 있다. 

 

관련 글

[arduino] - DFplayer - 아두이노 사운드 모듈

[arduino] - NodeMcu(ESP8266)에서 DFplayer를 제어하는 코드

[arduino] - ESP32(DevKit)로 DFplayer 제어하기

[arduino] - 안드로이드 앱 DFcontroller를 이용하여 DFplayer 제어

[arduino] - 아두이노시계 예제, ESP01 WiFi 이용 시간 동기화 하기

[arduino] - 아두이노 말하는알람시계 예제 - DFPlayer

[arduino] - 말하는알람시계 - 블루투스 연결 및 시간 동기화, DFPlayer 제어

[arduino] - NodeMcu - 말하는 알람시계, wifi이용 시간 동기화 및 DFPlayer 원격제어

[arduino] - ESP8266 / ESP32 - time.h 라이브러리 이용 시간 출력 및 NTP 서버 시간 동기화 하기

 

 

반응형

+ Recent posts