NTP 時鐘 – RTC & NTP & master and slave 整合

繼上一篇文章文末分享到在 Colorduino 上面以 NodeMCU ESP8266 為 master 實作數字顯示、動畫以及顏色的變換,接下來要分享的東西是如何將這些我一一完成的 “積木" 成功的把他們組裝起來,並且順利運作。

bq32000 (RTC module)

在我可以正確的從 NodeMCU ESP8266 (master) 發送 data 給 Colorduino (slave),並且成功顯示在 Colorduino 上後 (真的是經過了一番折騰),我開始要將 NTP 時鐘主要的功能一個一個加上去。

首先要做的事情是讓 master 可以正確的一秒改變一次數字。畢竟我不可能無時無刻都在抓 NTP server 的資料 (如果 WiFi 掛了時鐘就掛了,不過這只是個小原因,後面會分享主要是為什麼),那假設我設定每隔 5 seconds 向 NTP server 抓時間資訊回來我本機端,master 只做這件事而且自己不數秒的話,在 Colorduino 上面就不會看到像電子鐘那樣一秒一秒地改變顯示,而是看到每隔 5 秒的狀況 (意思是秒數的個位數字只會顯示 5 或是 0)。

為了避免這麼蠢的事情發生 (我想也沒有時鐘長這樣吧),又不可能無時無刻都在向遠端抓 NTP server 的資料,那勢必需要一個 “東西" 讓 master 自己能數秒,並且正確顯示在 Colorduino 上,而這個東西就是接下來要分享的 RTC (real-time clock) module – bq32000。

先附上 bq32000 的 datasheet。這裡沒有要細說 bq32000 是如何操作 (如何操作其實看 datasheet 就會懂了,雖然說我有些也看得沒有很懂),我比較想要著重在『如何和 master 合作』這個部分。

由於 bq32000 也是採用 I2C protocol 和其他元件做溝通,我的 NodeMCU ESP8266 是為 master,因此 bq32000 會是這個 master 之下的其中一個 slave,而知道了主從關係後,要關心的重點就在於 write 和 read。

RTC 的應用就是要把 RTC module (slave) 跑的時間 write 給 master,master read data 後把東西再傳給另外一邊的 slave 也就是 Colorduino,不過事情當然沒有這麼簡單,如果我希望 user 可以自己設定時間的話,勢必是在 master 獲得 user 設定的時間,也就是說 master 必須也要 write data (user 設定的時間的 data) 給 RTC module (這個部分其實容易,因為就跟我先前做的 master (ESP8266) write data 給 slave (Colorduino) 一樣)。

想好怎麼做後,也大致瞭解 bq32000 datasheet 的內容後,便向 Bird 尋求 bq32000 的元件坐第一次的嘗試。不過當時 Bird 手邊只有這種形式的元件:

BQ32000.jpg

因此 Bird 先要我用軟體實現 RTC,也就是在不用外接 RTC module 的情況下,用 software 寫出等同於 RTC module 的 function,除此之外,Bird 直接要我嘗試將很早之前做好 (當兵前) NTP 的 function 和即將要寫的 software RTC 結合,讓他們可以合力運作。

Software RTC & NTP server function

Software RTC

由於無法使用 bq32000 RTC module,得先跟他暫別。原先想說用 software 寫 RTC 也未免太困難,不過冷靜想想後發現完全不是件難事。其實原理很簡單,要秒我給你秒,要分我給你分,要時我給你時,意思是 60 秒為 1 分鐘,60 分鐘為一小時,也就是只要我有一個不斷在計算時間的東西我就可以利用換算的方式達到同等於 real-time clock 功能的效果

而這個東西讓我馬上想到了在做之前 project 的時候用到了 Arduino 內建的一個 function – millis()。這個 function 簡單來說就是從 MCU 開始 run 的那刻,millis() 這個 function 便會開始計算 MCU run-time 的時間,詳細的說明我附上 Arduino 官方文件:millis

以下是部分程式碼:

unsigned long timeNow = 0;
unsigned long timeLast = 0;
int seconds = 0;
int minutes = 0;
int hours = 0;
int days = 0;
timeNow = millis()/1000;// millis is the function i have mentioned above
seconds = timeNow - timeLast;
if (seconds == 60)
{
timeLast = timeNow;
minutes += 1;
}
if (minutes == 60){
minutes = 0;
hours += 1;
}
if (hours == 24){
hours = 0;
days += 1;
}

以上簡單的程式碼就可以實現 software RTC 了 (意外的簡單),不過做到後來發現 software RTC 這樣寫的彈性不夠好,後續文章會說明為什麼 (其實接下來就會遇到所謂的彈性到底指的是什麼,我將一併解釋)。

NTP server function

做完 software RTC ,確定他可以成功顯示在 Colorduino 上,以『計時器』的方式在跑時間 (因為我設定時分秒的 initial value 都是 0),我還很無聊的用我手機碼錶的功能跟他較勁,目前 “看起來" 一切運作順利。

接著就是將 NTP server 這個原先寫好的 function 加進我 master 端的 code 裡面。再往下分享整合部分之前,由於我發現我講了這麼久的 『NTP 時鐘』,在之前的文章竟然都沒有分享到 NTP server 這件事情實在有點說不過去,所以我在這裡大致分享一些 material 和這個 function 的實作內容。

關於 NTP (network time protocol) server 的由來以及故事,我是看 鳥哥的Linux 私房菜– NTP 時間伺服器。各種有關於 GMT (Greenwich Mean Time),UTC (Coordinated Universal Time),Unix time 的說明以及之間的比較都寫的都很詳盡。

而 NTP server 這個 function 就是藉由實作 protocol 去抓取我需要的資料,比如說 NTP 這個 protocol 是以 port123 為連接的埠口 (想像自己問別人問題 <相當於送出 request>,被問的那個人要接收你的訊息,而他打算用右耳接收 <特定的連接埠口>),以 UDP 封包的形式傳送時間的資訊。

再者,NTP server 其實是一種階層的改念,就是一般的 server/client 主從架構,而最多可以到 15 個階層,不過 anyway 這不是重點,有興趣的人可以點選以上連結看更詳細的說明。

以下是部份程式碼:

IPAddress timeServerIP; // time.nist.gov NTP server address
const char *ntpServerName = "tick.stdtime.gov.tw";// this is one of the NTP server
const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];
WiFiUDP udp;
unsigned long sendNTPpacket(IPAddress &address)
{
Serial.println("sending NTP packet...");
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
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
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
udp.beginPacket(address, 123); //NTP requests are to port 123
udp.write(packetBuffer, NTP_PACKET_SIZE);
udp.endPacket();
}
void ntpServer(char *NTP)
{
//get a random server from the pool
WiFi.hostByName(NTP, timeServerIP);
sendNTPpacket(timeServerIP); // send an NTP packet to a time server
delay(1000);
int alreadyConnected = udp.parsePacket();//return true or false
if (!alreadyConnected) {
Serial.println("no packet yet");
}
else {
Serial.print("packet received, length= ");
Serial.println(alreadyConnected);
// We've received a packet, read the data from it
udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
//the timestamp starts at byte 40 of the received packet and is four bytes,
// or two words, long. First, esxtract the two words:
unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
Serial.print("Seconds since Jan 1 1900 = " );
Serial.println(secsSince1900);
// now convert NTP time into everyday time:
Serial.print("Unix time = ");
// Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
const unsigned long seventyYears = 2208988800UL;
// subtract seventy years:
epoch = secsSince1900 - seventyYears;
// print Unix time:
Serial.println(epoch);
// print the hour, minute and second:
Serial.print("The UTC time is ");
Serial.print(((epoch % 86400L) / 3600) + 8); //hour (GMT+8)
Serial.print(':');
if ( ((epoch % 3600) / 60) < 10 ) {
Serial.print('0');
}
Serial.print((epoch % 3600) / 60);//minute
Serial.print(':');
if ( (epoch % 60) < 10 ) {
Serial.print('0');
}
Serial.println(epoch % 60); //second
}
}
view raw NTP_server.cpp hosted with ❤ by GitHub

由上方的程式碼可以看得出來,想必在 console 裡面一定看得到現在的標準時間,比如說 11:35:27 (當然是要在成功抓到 NTP server 資料的情況下),而接下來我要做的就是把顯示在 console 裡的資訊從 master 透過 I2C 傳給 slave,即顯示在 Colorduino 上方。

在上上篇文章 NTP 時鐘 – I2C 應用 (master v.s slave) (二) & NodeMCU ESP8266 as master 有提到 master 是如何傳 data 給 slave (Colorduino) 使其能夠跨板子顯示各種 pattern,所以我的做法就是把 NTP server 抓來的時間資料,總共 6 個數字 (時 x 2,分 x 2,秒 x 2) 每個數字分割成兩個部份,每部份 16 bytes (一個數字 32 bytes),傳送到 slave 端顯示。

搞定後再加上原先動畫和改變顏色的部分 (改變顏色上次有提到就是我每個 byte 送的資料不同,比如說我要紅色我就送 1,黃色 2,橘色 3 以此類推,但做到後來也會發現這樣寫很沒有彈性,往後的文章會解釋)。

19749549_1431652130261165_1118607472_o.jpg

成功把上面我要達成的目標完成後,目前 NTP 時鐘長這個樣子。我記得當時開心了半小時就發現了新的問題……,我又很無聊的把它跟我在 google 上查到的 NTP 時間做比較,發現了驚人的現象 – 秒數的變動很不規律,意思是 1 到 2 秒可能是 a 這個速度,但 3 到 4 秒時卻是 b,然後 a != b,這絕對不正常。

不僅如此,我放在旁邊不管他,看他過了一兩個小時後會不會變快或變慢,結果很慘的他慢了 1 分多鐘 (我當時設定兩個小時重新抓一次 NTP server 的資料),這也很不合理,因為 ESP8266 本身在計算 MCU 運算時間方面,精度可以到 50 ppm (也就是說每一百萬秒才會有 50 秒的誤差,大概是 11 12 天的時間才會差 50 秒),但我現在卻是兩個小時就差了 70 幾秒。

還有一個問題是 Colorduino 顯示會出現奇怪的狀況,所謂奇怪就是顯示在不該顯示的地方,比如說 ab (16 bytes) 應該要顯示在 column 1 和 2 (一個 column 8 個 bytes),但過了一段時間 ab 卻顯示在 column 7 和 8,使得 Colorduino 顯示的東西不是我要的結果,而這個原因待查。

以上三個問題在我前去問 Bird 後,整個被大ㄉㄧㄤ 了一番,不過 Bird 交給我的這些東西經過我思考消化後發現真的是在這條路上一輩子都受用無窮的道理,有礙於篇幅,讓我們下篇文章再繼續分享!

1 thoughts on “NTP 時鐘 – RTC & NTP & master and slave 整合

  1. 引用通告: NTP 時鐘 – 程式碼彈性 & 產品思維 – 自造者萊恩

發表留言