2016年6月14日 星期二

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

週日搞定 NTP 對時問題後, 接下來我想結合 NTP 對時訊息, 重做一下液晶顯示器 1602 模組的實驗, 把從 NTP 伺服器取得的時間顯示在 1602 模組上, 這樣就不需要用序列埠監控視窗來觀察時間了. 關於 1602 液晶模組測試參考 :

# Arduino 液晶顯示器測試

而此次所使用的 Arduino + ESP8266 物聯網模組是上周焊接製作完成的, 將 Nano 與 ESP8266 焊在一個 15*16 洞的小型洞洞板上, 內建一個蜂鳴器與切換開關, 組成一個小型開發板, 專門用在物聯網的應用上. 使用 Nano 而非更小巧的 Pro Mini 的原因是 Nano 具有 Mini USB 插座, 方便應用開發時期程式上傳與偵錯之用, 參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組


也可以按下列電路圖用杜邦線自行接線 :


至於 NTP 對時實驗, 則參考前文 :

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

由於此 Arduino+ESP8266 物聯網開發模組中 Nano 的 D7,D8 被拿來用作 Arduino 軟體序列埠的 Rx 與 Tx; D4 接腳被拿來做為模式選擇輸入; D9 被拿來接蜂鳴器, 這四個腳被保留不能使用, 所以我改用 D10~D13 接腳當作資料輸出, 與 1602 模組的 D4~D7 分別相接.

另外要控制 1602 可被寫入資料, 其第 5 腳 (RW) 須接 GND, 第 4 腳 (RS) 我選擇接到 Nano 的 D2, 第 6 腳 (E) 接到 Nano 的 D3, 詳細接線對照如下表 :

1602 接腳功能Arduino Nano 接腳
1 (VSS)電源負極GND
2 (VCC)電源正極5V
3 (Vo)調整對比可變電阻中腳
4 (RS)D0~D7放入資料暫存器 (1) 或指令資料暫存器 (0)D2
5 (RW)讀取 (1) 或寫入 (0) LCDGND (寫入)
6 (E)可寫入 (1) 或不可寫入 (0) LCDD3
7 (D0)資料位元 0不接
8 (D1)資料位元 1不接
9 (D2)資料位元 2不接
10 (D3)資料位元 3不接
11 (D4)資料位元 4D10
12 (D5)資料位元 5D11
13 (D6)資料位元 6D12
14 (D7)資料位元 7D13
15 (A+)背光電源正極5V
16 (-K)背光電源負極GND

這裡面還有一個重點是 1602 的第三腳必須接一個可變電阻的中間接腳來調整亮度對比, 否則液晶會太亮, 就算輸出資料正確也甚麼都看不到, 就只是一片亮亮的藍屏, 我使用的是如下的可變電阻, 剛好可以插在模組板上的 2*4 連通排母上方便接線, 其左腳接地, 右腳接 +5V, 中間腳則接 1602 的第 3 腳 (Vo) :


它上面有一個轉鈕, 可用小的一字起子調整電阻值, 使要顯示的字能在背景光的對比中明確顯示. 我用 Fritzing 繪製了 Nano 與 1602 的接線圖如下 :


其中紅色線為 +5V, 黑色線為 GND, 其餘為信號線. 線路接好後就可以修改程式了, 我把前面實驗二程式中的 setup() 函式的系統部分稍作修改, 多定義了一個 cifsr 字串來接收 AT+CIFSR 指令傳回的回應,  然後在後續的應用部分利用字串處理技巧取出 ESP8266 在區網裡所取得的 IP, 將其顯示在 1602 LCD 的第一列.

其次將原先的 GetTime() 函式改為 getCST(), 它會在取得 NTP 伺服器回應的時戳後, 換算成 UTC 時間, 但是把其中的 hour 部分加上 8, 因為台北時間為中原標準時間 CST 時區 (UTC+8), 最後把傳回的 HH:MM:SS 格式之時間字串顯示在 1602 LCD 的第二列, 完整程式如下 :

#include <SoftwareSerial.h>
#define DEBUG true
//-----application codes listed HERE-----
#include <LiquidCrystal.h>
#define RS 2
#define E 3
#define D4 10
#define D5 11
#define D6 12
#define D7 13

//system use please do not edit
SoftwareSerial esp8266(7,8); //(RX,TX)
const int SW_PIN=4; //Pin to switch configuration or working mode
const int MAX_PAGE_NAME_LEN=48;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name/ssid/pwd
int mode; //store current mode(LOW=configuration, HIGH=working)

//-----application codes listed HERE-----
//Request for NTP time stamp (in the first 48 bytes)
byte packetBuffer[128]; //buffer : send & recv data to/from NTP
LiquidCrystal lcd(RS,E,D4,D5,D6,D7);  //create LCD object

void setup() {
  //system use please do not edit
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  pinMode(SW_PIN, INPUT);
  mode=digitalRead(SW_PIN);
  sendData("AT+GMR\r\n",1000,DEBUG);
  if (mode==LOW) { //setup mode : for wifi configuration
    sendData("AT+CWMODE=3\r\n",1000,DEBUG); //configure as access point
    sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
    sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port      
    }
  else {  //working mode : for running application
    sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as a station
    delay(3000); //wait for wifi connection to get local ip
    }
  String cifsr=sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  //-----application codes listed HERE-----
  String ip="0.0.0.0";
  if (cifsr.indexOf("OK") != -1) {
    cifsr.replace("AT+CIFSR","");
    cifsr.replace("\r\n","");
    Serial.println(cifsr);
    ip=cifsr.substring(1,cifsr.indexOf("OK"));
    }
  lcd.begin(16,2); //define 2*16 LCD
  lcd.clear(); //clear screen
  lcd.setCursor(0,0); //move to left top corner
  lcd.print(ip); //print Hello World!
  }

void loop() {
  if (mode==LOW) {setupWifi();}
  else { //-----application codes listed bellow-----
    Serial.println("Sending request to NTP server ...");
    String cst=getCST();
    Serial.println(cst);
    lcd.setCursor(0,1); //move to (x,y)
    lcd.print(cst); //print CST time
    }
  }

String getCST() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"82.209.243.241\",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
  unsigned long epoch=secsSince1900 - 2208988800UL;
  Serial.print("Unix time stamp (seconds since 1970-01-01)=");
  Serial.println(epoch); //print Unix time

  //Convert epoch to UTC/GMT (Greenwich Meridian) hour:minute:second
  String cst="";
  byte hour=(epoch % 86400L) / 3600 + 8; //hour (86400 secs per day)
  if (hour > 24) {hour -= 24;}
  if (hour < 10) {cst += "0";} //prefix with "0" if single digit
  cst.concat(hour);
  cst.concat(":");
  byte  min=(epoch % 3600) / 60;   //minute (3600 secs per minute)
  if (min < 10) {cst.concat("0");} //prefix with "0" if single digit
  cst.concat(min);
  cst.concat(":");
  byte sec=epoch % 60; //second
  if (sec < 10) {cst.concat("0");} //prefix with "0" if single digit
  cst.concat(sec);
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  return cst;
  }

void setupWifi() {
  if (esp8266.available()) { // check if the esp is sending a message
    if (esp8266.find("+IPD,")) {
      delay(1000);
      //esp8266 link response : +IPD,0,498:GET / HTTP/1.1
      //retrieve connection ID from response (0~4, after "+IPD,")
      int connectionId=esp8266.read()-48;  //from ASCII to number
      //subtract 48 because read() returns ASCII decimal value
      //and in ASCII, "0" (the first decimal number) starts at 48
      if (esp8266.find("GET /")) { //retrieve page name (router)
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
          if (strcmp(buffer, "update") == 0) { //update wifi
            //"?ssid=aaa&pwd=bbb HTTP/1.1"          
            esp8266.find("?ssid="); //skip ssid token
            memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
            esp8266.readBytesUntil('&', buffer, sizeof(buffer)); //retrieve ssid
            String ssid=buffer;
            esp8266.find("pwd="); //skip pwd token
            memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
            esp8266.readBytesUntil(' ', buffer, sizeof(buffer)); //retrieve pwd
            String pwd=buffer;
            //configure as a station
            String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",6000,DEBUG);
                   
            //show setup result
            String webpage="<html>Wifi setup ";
            if (res.indexOf("OK") != -1) {webpage += "OK!</html>";}
            else {webpage += "Failed!</html>";}
            String cipSend="AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);
         
            String closeCommand = "AT+CIPCLOSE=";
            closeCommand+=connectionId; // append connection id
            closeCommand+="\r\n";
            sendData(closeCommand,3000,DEBUG);              
            }
          else { //show setup page
            String webpage="<html><form method=get action='/update/'>SSID ";
            webpage += "<input name=ssid type=text><br>";
            String cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            webpage="PWD <input name=pwd type=text> ";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            webpage="<input type=submit value=Connect></form></html>";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            String closeCommand = "AT+CIPCLOSE=";
            closeCommand+=connectionId; // append connection id
            closeCommand+="\r\n";
            sendData(closeCommand,3000,DEBUG);
            }
          }
        }
      }
    }
  }

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;
  }

編譯後程式記憶體 (Flash) 耗掉逾四成, 變數記憶體 (SRAM) 耗掉近六成 :

草稿碼使用了 13,210 bytes (43%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 1,198 bytes (58%) 的動態記憶體,剩餘 850 bytes 供局部變數。最大值為 2,048 bytes 。

注意, 在擷取 IP 時發現, ESP8266 回應的 AT+CIFSR 後面應該是跳行 \r\n 才對, 但卻沒辦法用 replace() 去除, 只好暫時在 substring() 中從索引 1 開始取子字串來解決. 也許可用其他可顯示 ASCII 碼的通訊程式來檢查看看. 程式中關於 C 語言字串處理之函數參考 :

Arduino 基本語法筆記

程式上傳後執行結果如下所示 :



上面影片顯示, 1602 LCD 顯示器上的秒數每次大約跳 8 秒. 可知, 在沒有額外添加 delay() 情況下, 每次向 NTP 伺服器查詢, 大約 8 秒後會收到回應的時戳. 此實驗後續可以做的是搭配 RTC 模組來做時鐘, 這樣秒數就會自然跳動, 然後使用 NTP 伺服器來每隔一段時間對 RTC 進行時間校正. 另外電流表顯示整體耗電平均約 20mA, 最大約 40mA, 還蠻省電的喔.

OK, 我想這次重做 NTP 的實驗到這裡應該可以收工囉, 上面的程式架構也將是未來做實驗的範本. 這兩個星期來的摸索不僅製作了新板子, 也解決了老問題, 可說收穫頗豐. 有了這個連網模組, 以後做實驗就方便多了.

參考資料 :

# EasyIOT
# Sparkfun Thing Hookup Guide
# HTTP slide show on ESP8266 w/ ILI9341 TFT LCD using Arduino IDE
# Building a Clock with an Arduino


5 則留言 :

cyut 提到...

你好我想詢問有關ESP8266的部分請問可以加你FB嗎??

小狐狸事務所 提到...

OK, 但我很久才會看 FB 一次, 您可以 PO 在這裡喔!

cyut 提到...

因為我的專題是想要,把資料透過ESP8266這塊模組,使用WIFI直接上傳到GOOGLE SPREADSHEET上,但是我一直有問題所以想問你有什麼方式可以教我嗎??

cyut 提到...

你好我的專題目的,是要使用ESP8266上傳資料到google spreadsheet上,想請問這方面你有碰觸過嗎?
並我想問你,碰觸Arduino多久了?

小狐狸事務所 提到...

Sorry, 我沒試過耶! 我都將資料丟到物聯網伺服器, 不過丟到 google 雲端硬碟是個好主意, 您可參考這篇 :

http://www.instructables.com/id/Post-to-Google-Docs-with-Arduino/

有空我也來試試.