2016年7月16日 星期六

利用 NTP 伺服器來同步 Arduino 系統時鐘 (一)

上回在做 IOT 模組的 NTP 對時實驗時, 找到了 Arduino 新版 Time 函式庫的說明文件, 那時以為這個函式庫必須搭配 RTC 模組才能使用就沒仔細看. 週三晚上去河堤國小附近把姊姊忘記騎回來的 "小金" (我的小小摺疊腳踏車) 找回來後, 就大略地閱讀了這個 Time 函式庫的說明 :

# Arduino Time library

發現原來此函式庫不一定要用到 RTC, 它只是提供一堆跟時間有關的函式方便我們處理日期時間而已, 我們可以透過 setTime() 函式讓 Arduino 與外部時鐘同步, 而 RTC 只不過是同步來源的一種而已, 也可以透過網路與 NTP 伺服器同步 :

"The Time library adds timekeeping functionality to Arduino with or without external timekeeping hardware."

可從 Github 下載 Time 函式庫 :

https://github.com/PaulStoffregen/Time

把下載下來的 Time-master.zip 檔解壓縮後放到 Arduino IDE 的安裝目錄的 libraries 資料夾下面, 重開 IDE 就會抓到此函式庫了. 在 Arduino Time library 底下有一個範例程式, 我把範例程式整理並加註中文說明如下 :

測試 1 :

#include <Time.h>

#define TIME_MSG_LEN  11   //時間字串長度含標頭 T 為 11 字元 (T+10位數之 Unix 時戳)
#define TIME_HEADER  'T'   //時間同步訊息標頭
#define TIME_REQUEST  7    //ASCII bell character requests a time sync message

//時間字串格式 T1262347200  //noon Jan 1 2010

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

void loop() {  
  if (Serial.available()) { //串列埠收到訊息
    processSyncMessage(); //處理時間同步字串
    }
  if (timeStatus() == timeNotSet) { //時間尚未同步
    Serial.println("waiting for sync message");
    }
  else { //時間已同步, 顯示時間
    digitalClockDisplay();
    }
  delay(1000);
  }

void digitalClockDisplay() { //顯示時間
  // digital clock display of the time
  Serial.print(hour());   //時
  printDigits(minute()); //分
  printDigits(second()); //秒
  Serial.print(" ");
  Serial.print(day());    //日
  Serial.print(" ");
  Serial.print(month());  //月
  Serial.print(" ");
  Serial.print(year());   //年
  Serial.println();
  }

void printDigits(int digits) { //顯示數值 & 位數調整
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10) {Serial.print('0');}
  Serial.print(digits);
  }

void processSyncMessage() { //處理同步時間字串
  //若自串列埠收到同步時間訊息, 更新時間並回傳 true
  while (Serial.available() >=  TIME_MSG_LEN ) {  //收到至少 11 字元的同步訊息
    char c=Serial.read();
    Serial.print(c);
    if (c==TIME_HEADER ) { //收到同步訊息標頭 'T'    
      time_t pctime=0; //定義時間變數初值 (整數)
      for (int i=0; i<TIME_MSG_LEN-1; i++) { //讀取 Unix 時戳字串
        c=Serial.read();        
        if (c >= '0' && c <= '9') { //必須是 0~9 數字
          pctime=(10 * pctime) + (c - '0') ; //將數字字元轉成整數
          }
        }
      setTime(pctime); //將 Arduino 內部時鐘與自串列埠收到的時間訊息同步
      }
    }
  }

上傳執行後, 序列埠監控視窗會不斷出現 "waiting for sync message", 輸入 T1262347200 按傳送就會將 Arduino 內部時鐘與此時間訊息同步, 序列埠監控視窗顯示如下 :

waiting for sync message
waiting for sync message
waiting for sync message
T12:00:00 1 1 2010
12:00:01 1 1 2010
12:00:02 1 1 2010
12:00:03 1 1 2010
12:00:04 1 1 2010
12:00:05 1 1 2010
12:00:06 1 1 2010

注意第一個時間訊息前的 T 來自 if 判斷前的 Serial.print(c).

接下來我想修改上回 NTP 對時程式來使 Arduino 內部時鐘與其同步, 參考 :

以 Arduino + ESP8266 物聯網模組重作 NTP 實驗 (三)

我參考上面範例以及之前我做的 NTP 實驗, 希望從 NTP 取得 GMT 的 Unix 時間後, 加上 8 小時 (28800 秒) 得到台灣時間的 Unix 秒數, 再傳給 Time 函式庫的 setTime() 函數來設定 Arduino 的系統時鐘, 這樣就能透過此函式庫來取得完整的日期與時間訊息了, 如下面測試 2 所示 :

測試 2 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
int i=0; //sync cycle counter

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  setTime(getTime());
  }

void loop() {
  showDateTime();
  ++i;
  if (i >= 60) {  //1 分鐘同步週期到了
    setTime(getTime());  //設定 Arduino 系統時鐘
    i=0;
    }
  else {delay(1000);}  //一秒跳一次
  }

void showDateTime() { //輸出如 2016-07-15 19:56:12 格式的日期時間
  Serial.print(year());  
  Serial.print("-");
  byte M=month();
  if (M < 10) {Serial.print('0');}
  Serial.print(M);
  Serial.print("-");  
  byte d=day();
  if (d < 10) {Serial.print('0');}
  Serial.print(d);  
  Serial.print(" ");
  byte h=hour();  
  if (h < 10) {Serial.print('0');}
  Serial.print(h);
  Serial.print(":");
  byte m=minute();
  if (m < 10) {Serial.print('0');}
  Serial.print(m);
  Serial.print(":");  
  byte s=second();
  if (s < 10) {Serial.print('0');}
  Serial.println(s);
  }

long getTime() {  //從 NTP 伺服器取得 Unix 時間秒數
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;  //台灣時間為 GMT+8 小時, 即 28800 秒
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

在 setup() 函式中, 我們先呼叫自訂的 getTime() 函數從 NTP 伺服器取得台灣時區的 Unix 秒數, 再將其傳入 Time 函式庫的 setTime() 函數來設定 Arduino 內部時鐘. 而在 loop() 迴圈裡, 先呼叫自訂的 showDateTime() 函數來顯示目前的日期時間, 然後增量同步計數器 i, 若到達 60 次時就呼叫 getTime() 來與 NTP 伺服器對時, 並且將計數器歸零.

程式上傳後擷取串列埠監控視窗訊息如下 :

AT+RST


OK
bB�鑭b禔 ��"兀\�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+GMR

0018000902

OK
AT+CIFSR

192.168.2.108

OK
Sending request to NTP server ...
AT+CIPSTART="UDP","91.226.136.136",123


OK
AT+CIPSEND=48

>
NTP server answered :
E3 00 06 EC 00 00 00 00 00 00
00 00 31 4E 31 34 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0D 0A
53 45 4E 44 20 4F 4B 0D 0A !!!0D
0A 2B 49 50 44 2C 34 38 3A 24
02 06 ED 00 00 00 22 00 00 02
44 6B DC 0B 87 DB 33 43 02 94
20 CF 9D 00 00 00 00 00 00 00
00 DB 33 44 59 E4 D7 13 48 DB
33 44 59 E4 D7 9F D7 0D 0A 4F
4B 0D

123 bytes received
NTP time stamp packets (byte 101~104)=DB 33 44 59
NTP time stamp (seconds since 1900-01-01)=3677570137
Unix time stamp (seconds since 1970-01-01)=1468581337
AT+CIPCLOSE


OK
Unlink
2016-07-15 19:15:37
2016-07-15 19:15:38
2016-07-15 19:15:39
2016-07-15 19:15:40
2016-07-15 19:15:41
2016-07-15 19:15:42
2016-07-15 19:15:43
... (每秒跳一次)

2016-07-15 19:16:29
2016-07-15 19:16:30
2016-07-15 19:16:31
2016-07-15 19:16:32
2016-07-15 19:16:33
2016-07-15 19:16:34
2016-07-15 19:16:35
2016-07-15 19:16:36
AT+CIPSTART="UDP","91.226.136.136",123


OK
AT+CIPSEND=48

>
NTP server answered :
E3 00 06 EC 00 00 00 00 00 00
00 00 31 4E 31 34 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 0D 0A
53 45 4E 44 20 4F 4B 0D 0A !!0D
0A 2B 49 50 44 2C 34 38 3A 24
02 06 ED 00 00 00 22 00 00 02
86 6B DC 0B 87 DB 33 43 02 94
20 CF 9D 00 00 00 00 00 00 00
00 DB 33 44 9D 64 A8 39 88 DB
33 44 9D 64 B2 E4 33 0D 0A 4F
4B 0D 0A

124 bytes received
NTP time stamp packets (byte 101~104)=DB 33 44 9D
NTP time stamp (seconds since 1900-01-01)=3677570205
Unix time stamp (seconds since 1970-01-01)=1468581405
AT+CIPCLOSE


OK
Unlink
2016-07-15 19:16:45
2016-07-15 19:16:46
2016-07-15 19:16:47
2016-07-15 19:16:48
2016-07-15 19:16:49
2016-07-15 19:16:50
...

可見每次同步大約花掉 8 秒時間.

上面測試二使用一個同步計數器變數 i 來計算是否該與 NTP 伺服器同步了, 其實這可以用 TimeAlarms 函式庫來做, 同樣從 Github 的 Clone or download 下載  :

https://github.com/PaulStoffregen/TimeAlarms

解壓縮 TimeAlarms-master.zip 至 Arduino IDE 安裝目錄的 libraries 下即可. 程式如下 :

測試 3 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);  //設定每 60 秒觸發執行 sync_clock()
  }

void loop() {
  showDateTime();
  Alarm.delay(1000);   //每一秒檢查一下觸發條件
  }

void sync_clock() {
  setTime(getTime());
  }

void showDateTime() {
  Serial.print(year());  
  Serial.print("-");
  byte M=month();
  if (M < 10) {Serial.print('0');}
  Serial.print(M);
  Serial.print("-");  
  byte d=day();
  if (d < 10) {Serial.print('0');}
  Serial.print(d);  
  Serial.print(" ");
  byte h=hour();  
  if (h < 10) {Serial.print('0');}
  Serial.print(h);
  Serial.print(":");
  byte m=minute();
  if (m < 10) {Serial.print('0');}
  Serial.print(m);
  Serial.print(":");  
  byte s=second();
  if (s < 10) {Serial.print('0');}
  Serial.println(s);
  }

long getTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

這裡我們新增了一個無傳回值的函數 sync_clock() 用來使 Arduino 與 NTP 時間同步, 然後於 setup() 中先初始同步一次後, 再用 Alarm.timeRepeat() 函式設定每 60 秒觸發 sync_clock() 一次. 而在 loop() 中則改用 Alarm.delay(1000) 函數來延遲一秒, 而非 delay(1000), 因為這樣才會每一秒去檢查事件狀態, 也就是 Alarm.timerRepeat() 所設定的 60 秒觸發是否已滿足.

但是觀察序列埠監控視窗擷取訊息發現, 剛開始似乎一切都正常, 但是大約十次之後就不再觸發了, why?  不知何故, 今天重新執行又一切正常.

下面測試 4 是將測試 3 無傳回值的 showDateTime() 改寫為傳回日期時間字串的 getDateTime(), 功能完全一樣 :

測試 4 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);
  }

void loop() {
  Serial.println(getDateTime());
  Alarm.delay(1000);
  }

void sync_clock() {
  setTime(getTime());
  }

String getDateTime() {  //傳回日期時間
  String dt=(String)year() + "-";
  byte M=month();
  if (M < 10) {dt.concat('0');}
  dt.concat(M);
  dt.concat('-');
  byte d=day();
  if (d < 10) {dt.concat('0');}
  dt.concat(d);
  dt.concat(' ');
  byte h=hour();
  if (h < 10) {dt.concat('0');}
  dt.concat(h);
  dt.concat(':');
  byte m=minute();
  if (m < 10) {dt.concat('0');}
  dt.concat(m);
  dt.concat(':');
  byte s=second();
  if (s < 10) {dt.concat('0');}
  dt.concat(s);
  return dt;  //傳回格式如 2016-07-16 16:09:23 的日期時間字串
  }

long getTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

在測試 4 的 getDateTime() 函數中使用了 + 與字串的 concat() 兩種方式來串接日期字串. 此程式編譯後記憶體耗用結果 :

草稿碼使用了 10,838 bytes (35%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 866 bytes (42%) 的動態記憶體,剩餘 1,182 bytes 供局部變數。最大值為 2,048 bytes 。

最後一個測試我想把這個每分鐘與 NTP 同步的系統時鐘顯示在 1602 LCD 顯示器上, 參考之前的測試文章 :

# 以 Arduino + ESP8266 物聯網模組重作 NTP 實驗 (三)

在此測試中, LCD 上排顯示 IP, 下排顯示如 23:12:59 的時間, 因為 1602 顧名思義就是只能顯示 16*2 共 32 個字元, 而測試 4 中 getDateTime() 所傳回的是如 2016-07-16 16:57:02 格式的字串, 共 19 個字元, 無法於一排中全部顯示, 因此在下列測試 5 中, 我捨棄顯示 ip, 將傳回的日期時間字串以空格拆分後, 日期顯示在上排, 時間顯示在下排, 程式如下 :

測試 5 :

#include <SoftwareSerial.h>
#define DEBUG true
#include <Time.h>
#include <TimeAlarms.h>
#include <LiquidCrystal.h>
#define RS 2
#define E 3
#define D4 10
#define D5 11
#define D6 12
#define D7 13

SoftwareSerial esp8266(7,8); //(RX,TX)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
LiquidCrystal lcd(RS,E,D4,D5,D6,D7);  //create LCD object

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  Serial.println("Sending request to NTP server ...");
  sync_clock();
  Alarm.timerRepeat(60, sync_clock);
  lcd.begin(16,2); //define 2*16 LCD
  lcd.clear(); //clear screen
  }

void loop() {
  String dt=getDateTime();
  Serial.println(dt);
  String d=dt.substring(0,dt.indexOf(" "));  //以空格拆分日期與時間
  String t=dt.substring(dt.indexOf(" ") + 1);  //空格後為時間
  lcd.setCursor(0,0); //move to (x,y)
  lcd.print(d); //print date  
  lcd.setCursor(0,1); //move to (x,y)
  lcd.print(t); //print time    
  Alarm.delay(1000);
  }

void sync_clock() {
  setTime(getTime());
  }

String getDateTime() {
  String dt=(String)year() + "-";
  byte M=month();
  if (M < 10) {dt.concat('0');}
  dt.concat(M);
  dt.concat('-');
  byte d=day();
  if (d < 10) {dt.concat('0');}
  dt.concat(d);
  dt.concat(' ');
  byte h=hour();
  if (h < 10) {dt.concat('0');}
  dt.concat(h);
  dt.concat(':');
  byte m=minute();
  if (m < 10) {dt.concat('0');}
  dt.concat(m);
  dt.concat(':');
  byte s=second();
  if (s < 10) {dt.concat('0');}
  dt.concat(s);
  return dt;
  }

long getTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"91.226.136.136\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,128); //clear buffer
  packetBuffer[0]=0xE3;       //LI, Version, Mode
  packetBuffer[1]=0x00;       //Stratum, or type of clock
  packetBuffer[2]=0x06;       //Polling Interval
  packetBuffer[3]=0xEC;       //Peer Clock Precision
  packetBuffer[12]=0x31;      //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=0x31;
  packetBuffer[15]=0x34;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  for (byte i=0; i < 48; i++) {
    esp8266.write(packetBuffer[i]);
    delay(5);
    }

  //deal with NTP response
  memset(packetBuffer,0,128); //clear buffer to store NTP response
  Serial.println();
  Serial.println("NTP server answered : ");
  int i=0; //packet byte counter
  while (esp8266.available() > 0) { //if receive NTP response : fall in loop
    byte ch=esp8266.read(); //got NTP response, read one byte for each loop
    if (i < 128) {packetBuffer[i]=ch;} //store received byte to packet buffer
    //show receving bytes in hex
    if (ch < 0x10) {Serial.print('0');} //prefix with '0' if byte value 0~9
    Serial.print(ch, HEX);
    Serial.print(' ');
    if ((((i+1) % 10) == 0)) {Serial.println();} //newline if exceeds 10 bytes
    delay(5); //wait 5ms for next incoming byte
    i++; //increment packet byte counter
    if ((i < 104) && (esp8266.available() == 0)) { //wainting if lags
      //Response packets not enough but no response : wait 1.5 seconds
      byte wcount=0; //waiting counter
      while (esp8266.available() == 0) { //loop until timeout (1.5 seconds)
        Serial.print("!"); //show ! means waiting for response packet
        delay(100);
        wcount += 1; //increment waiting counter
        if (wcount >= 15) {break;} //waiting timeout : quit loop
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  //Show time stamp (locates from byte 101~104 of the response packet)
  Serial.print("NTP time stamp packets (byte 101~104)=");
  Serial.print(packetBuffer[101],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[102],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[103],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[104],HEX);
  Serial.println();

  //handling time packets (4 bytes long) : combine them into words
  unsigned long highWord=word(packetBuffer[101],packetBuffer[102]);
  unsigned long lowWord=word(packetBuffer[103],packetBuffer[104]);
  //shift high word 16 bits left & OR with low word to form a double word
  //the result is a NTP time stamp (seconds since Jan 1 1900):
  unsigned long secsSince1900=highWord << 16 | lowWord;
  Serial.print("NTP time stamp (seconds since 1900-01-01)=");
  Serial.println(secsSince1900);

  //convert NTP time stamp to Unix time stamp :
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  //subtract seventy years to get Unix time stamp, plus 28800 (UTC+8)
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return epoch + 28800;
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

程式上傳後運作正常, 秒數會每一秒跳一下, 只有在每分鐘要同步時會暫停跳動約 9~10 秒 :



請注意當時間跑到 16:44:03 時會停止約 9 秒, 然後直接跳到 16:44:12, 這 9 秒就是 NTP 同步所花的時間.


2 則留言 :

Unknown 提到...

您好
我目前使用的RTC是DS3232
給予時間的來源為
setTime(14, 56, 59, 13, 2, 2017);
RTC.set(now());
setSyncProvider(RTC.get);
請問是否有方法能夠在燒錄時抓去當前電腦時間
我的目的是如果有一個以上的RTC,在燒錄後
只要不斷電就能夠使其達到時間同步的效果

小狐狸事務所 提到...

您可以用 Time 函式庫在 setup() 中取得目前電腦時間填入 setTime() 中即可, 參考 :
http://playground.arduino.cc/Code/Time