MKR1000 - NTP klient

Zápisník experimentátora

Hierarchy: Arduino MKR1000

V tomto článku si zistíme presný čas z NTP servera pomocou Arduina MKR1000. Do Internetu sa pripojíme cez WiFi. Budeme vychádzať zo vzorového príkladu, ktorý je dodávaný ku knižnici WiFi101, program ale upravíme tak, aby sa výsledok zobrazoval na displeji Nokia 5110.

Nokia 5110

Tento displej rád používam v mojich projektoch. Klasické Arduino na 5 V potrebuje na jeho ovládanie level shifter. Toto všetko môže pri MKR1000 odpadnúť, pretože používa rovnako ako displej 3,3 V. Čiže celé zapojenie sa scvrkne do prepojenia niekoľkých vodičov.

Na ovládanie displeja budeme používať knižnicu Adafruit_PCD8544. Dá sa nainštalovať aj v správcovi knižníc, ale má to jeden háčik. Je potrebná minimálne verzia 1.0.1, ktorá vie spolupracovať s mikrokontrolérmi SAMD21. V čase písania článku bola už úprava na GitHub, ale nebola ešte k dispozícii v správcovi knižníc. Preto ju bolo potrebné z GitHub stiahnuť ručne a prepísať s ňou nainštalovanú knižnicu.

WiFi101

Ďalšou potrebnou knižnicou je WiFi101. Táto sa dá nainštalovať aj v správcovi knižníc. V čase písania článku tam bola verzia 0.11.0.

IDE

Celé zapojenie som testoval v IDE 1.6.12.

Program

Program sa prihlási do vašej WiFi siete a z NTP servera si stiahne aktuálny čas. Ten sa bude zobrazovať na displeji a pretože sa jedná o program, ktorý slúži na testovanie, bude vypisovať aj veľa ladiacich informácií cez sériový port. Informácie o čase sa budú aktualizovať približne každých 10 sekund.

Program sa skladá z dvoch súborov:

  • dump.h - V tomto súbore sú všetky šablóny a makrá, ktoré slúžia na ladenie programu. Problematike ladenia cez sériový port som sa venoval v samostatnom článku. Tu sú použité všetky makrá z článku a niekoľko makier nových. Nejedná sa o žiadne dramatické zmeny voči článku o ladení. Iba som pridal niekoľko makier na uľahčenie výpisov na sériový port.
  • nokia5110_ntp_client.ino - V tomto súbore sa nachádza hlavný program. Výchádza zo vzoru, ktorý je priložený ku knižnici WiFi101. Môj kód je ale prepracovaný tak, aby bol ľahšie pochopiteľný a ukázal aj všetky dôležité informácie o tom, čo sa v ňom práve deje.

Prejdime si dôležité časti programu.

Includovanie knižníc a nastavenie displeja.

#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include <WiFi101.h>
#include <WiFiUdp.h>
#include <time.h>
#include "dump.h"

// Software SPI (slower updates, more flexible pin options):
// pin 1 - Serial clock out (SCLK)
// pin 2 - Serial data out (DIN)
// pin 3 - Data/Command select (D/C)
// pin 4 - LCD chip select (CS)
// pin 5 - LCD reset (RST)
Adafruit_PCD8544 nokia = Adafruit_PCD8544(1, 2, 3, 4, 5);

Nastavenie hesiel do vašej WiFi siete.

int status = WL_IDLE_STATUS;
char ssid[] = "XXXX";  // your network SSID (name)
char pass[] = "XXXX";  // your network password

Nastavenie čísla portu, na ktorom budeme počúvať, IP adresa NTP servera a buffer, cez ktorý sa budú prenášať údaje o čase. Komunikačný protokol pre NTP server je UDP.

NTP používa na prenos údajov buffer s dĺžkou 48 bajtov. Na server sa odošle s niekoľkými nastavenými hodnotami a server odpovie rovnako, len má vyplnené aj údaje o čase.

unsigned int localPort = 2390;      // local port to listen for UDP packets
IPAddress timeServer(129, 6, 15, 28); // time.nist.gov NTP server
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP

V nastavení sa udeje niekoľko krokov:

  • Naštartuje sa komunikácia po sériovom porte. Výsledok takejto komunikácie nájdete na konci tohoto článku.
  • Nastaví sa displej Nokia 5110 a zobrazí sa na ňom malá úvodná animácia textov.
  • Skontroluje sa, či je prítomné WiFi. V prípade MKR1000 je tento krok trošku nadbytočný, pretože má WiFi pripojené napevno, tento istý kód by ale mohol fungovať aj pre Arduino Uno s shieldom s Wifi. Píšem že mohol, pretože si nie som úplne istý, či všetky použité funkcie existujú aj pre mikrokontroléry AVR. A je možné, že ak aj existujú, funkcie sprintf a funkcie na prácu s časom zaberú viac miesta, než má Arduino Uno k dispozícii.
  • Pripojí sa k WiFi.
  • Vytvorí sa objekt Udp a začne počúvať na konkrétnom porte.
void setup()
{
  // Open serial communications and wait for port to open:
  Serial.begin(9600);

  nokia.begin();
  nokia.setContrast(60);
  drawIntro();
  nokia.clearDisplay();
  nokia.display();

  // check for the presence of the shield:
  if (WiFi.status() == WL_NO_SHIELD) {
    TRACETEXT("WiFi shield not present");
    // don't continue:
    while (true);
  }

  // attempt to connect to Wifi network:
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);

    // wait 10 seconds for connection:
    delay(10000);
  }

  TRACETEXT("Connected to wifi");
  printWifiStatus();

  TRACETEXT("Starting connection to server...");
  uint8_t res;
  Udp.begin(localPort);
  TRACE1("Udp.begin",res)
}

Vo funkcii loop sa periodicky odošle žiadosť o presný čas, prijme sa a spracuje tak, aby ju bolo možné zobraziť cez sériový port a displej Nokia 5110. Po odoslaní žiadosti sa čaká 1 sekundu a potom sa spracúva odpoveď. Takto to bolo naprogramované v originálnom deme a nie je to úplne najšťastnejšie riešenie. Ale nemal som toľko času na ladenie a povedal som si, že sa v budúcnosti tomuto aj tak budem venovať viac a neskôr prepíšem tieto napevno nastavené čakania na serióznejšiu formu.

Zastavím sa pri riadku DUMP(packetBuffer). Toto volanie makra vypíše celý obsah prijatého buffera a je to dobrá pomôcka na hľadanie prípadnej chyby. Vo výpise na konci programu sú takto zobrazené dva po sebe idúce buffre a ľahko tam zbadáte, na ktorých miestach v bufferi sa menia údaje o čase. Ale je to aj napísané v poznámke, že čas sa nachádza v bufferi od bajtu číslo 40 (ale indexujeme od 0, čiže je to 41. znak).

Nasleduje prevod údajov času do premennej secsSince1900. Už podľa názvu sa dá uhádnuť, že je to čas v sekundách od roku 1900. Historicky je dané, že v počítačoch sa čas meria od roku 1970. Ani mikrokontroléry firmy Atmel neporušujú túto tradíciu a preto si odčítaním získame aktuálnu hodnotu v sekundách, ktorú je možné použiť na ďalšie výpočty.

Nasledovnú časť programu som kompletne zmenil. Vzorový príklad (WifiUdpPNtpClient) tam robil všetky výpočty ručne. To ale nie je podľa mňa žiadúce, keď máme na prácu s časom štandartné funkcie. Preto môj kód pracuje iba s nimi. Používajú sa nasledovné funkcie a štruktúry:

  • time_t - V skutočnosti sa za týmto makrom skrýva celé číslo a obsahuje už spomínané sekundy od roku 1970.
  • tm - Štruktúra obsahuje všetky premenné pre konkrétny čas.
  • gmtime - Získanie času UTC. Pre nás to znamená, že je obvykle o hodinu späť a nezohľadňuje letný čas. Keby sa v mikrokontroléri podarilo nastaviť našu časovú zónu, dala by sa použiť aj funkcia localtime, ktorá vracia čas v rovnakej podobe, ako ho používame na území Slovenska. Ale pre krátkosť času a nedostatok informácií na Internete sa mi to zatiaľ nepodarilo zistiť.
  • sprintf - Funkcia na formátovanie do textového buffera. Omnoho flexibilnejšia než tá, ktorú bežne používame s Arduinom. S dostatkom pamäte v MKR1000 ju môžeme bez problémov používať.
  • asctime - Formátuje čas do štandartizovanej podoby reťazca Tue Nov 22 21:16:16 2016.
  • strftime - Formátuje čas podľa zdrojového reťazca.

Tieto funkcie som použil na to, aby som získal pekné výpisy času aj na sériovom porte, aj na displeji. Na fotografii je vidno, čo sa zobrazí na displeji a výpis sériového portu je na konci textu.

void loop()
{
  sendNTPpacket(timeServer); // send an NTP packet to a time server
  // wait to see if a reply is available
  delay(1000);
  if ( Udp.parsePacket() ) {
    TRACETEXT("Packet received");
    // We've received a packet, read the data from it
    Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
    DUMP(packetBuffer)

    //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:
    // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
    const unsigned long seventyYears = 2208988800UL;
    // subtract seventy years:
    unsigned long epoch = secsSince1900 - seventyYears;

    time_t unixtime = epoch;
    tm *timeinfo;
    timeinfo = gmtime( &unixtime );

    char bufout[256];
    sprintf(bufout, "Unix time = %u is year=%d, month=%d, day=%d, wday=%d, hour=%d, min=%d, sec=%d",
            (unsigned int)unixtime,
            timeinfo->tm_year + 1900,
            timeinfo->tm_mon,
            timeinfo->tm_mday,
            timeinfo->tm_wday,
            timeinfo->tm_hour,
            timeinfo->tm_min,
            timeinfo->tm_sec);
    Serial.println(bufout);
    DUMPVAL(*tzname);

    sprintf(bufout,"asctime = %s",asctime(timeinfo));
    Serial.println(bufout);
    
    sprintf(bufout,"%s",asctime(timeinfo));
    nokia.clearDisplay();
    nokia.setCursor(0, 0);
    nokia.print(bufout);
    nokia.setCursor(0, 24);
    strftime (bufout,80,"%T",timeinfo);
    nokia.print(bufout);
    nokia.setCursor(0, 32);
    strftime (bufout,80,"%F",timeinfo);
    nokia.print(bufout);
    nokia.setCursor(0, 40);
    strftime (bufout,80,"%A",timeinfo);
    nokia.print(bufout);
    nokia.display();
   
  }
  else
    TRACETEXT("Packet lost");
  // wait ten seconds before asking for the time again
  delay(10000);
}

Odoslanie žiadosti. Funkciou memset sa celý buffer naplní nulami a potom sa nastaví niekoľko konkrétnych údajov. Toto som nemenil, iba som doplnil ladiace výpisy. Podľa Internetových zdrojov sa ale zdá, že sa v bufferi nastavuje aj to, čo nie je nevyhnutne treba. Ak si nájdem trochu času, otestujem viac časových serverov a prípadne porovnám kód s nejakými C++ implementáciami tohoto istého riešenia, aby sme našli aj pre Arduino kód, ktorý bude zodpovedať zvyšku sveta.

unsigned long sendNTPpacket(IPAddress& address)
{
  TRACEFUNC()
  // 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:
  int res;
  res=Udp.beginPacket(address, 123); //NTP requests are to port 123
  TRACE1("Udp.beginPacket",res)
  res=Udp.write(packetBuffer, NTP_PACKET_SIZE);
  TRACE1("Udp.write",res)
  res=Udp.endPacket();
  TRACE1("Udp.endPacket",res)
}

Takto vyzerá výpis na sériovom porte. Z výpisu by ste sa mohli dovtípiť, na ktorom mieste zdrojového kódu sa nachádzajú príkazy, ktoré vypíšu konkrétne riadky na výpise.

Get current time from NTP server.
Attempting to connect to SSID: UPC3A7CE89
[016541] Connected to wifi
SSID: UPC3A7CE89
IP Address: 192.168.0.38
Signal strength (RSSI): -75 dBm
[016546] Starting connection to server...
Udp.begin: res=0
[016550] long unsigned int sendNTPpacket(IPAddress&)
Udp.beginPacket: res=1
Udp.write: res=48
Udp.endPacket: res=1
[017555] Packet received
Dump: packetBuffer
200003F4 - 24 01 06 E3 00 00 00 00 00 00 00 00 41 43 54 53 $...........ACTS
20000404 - DB DF 33 E5 D1 43 39 36 00 00 00 00 00 00 00 00 ..3..C96........
20000414 - DB DF 34 20 80 DD 3A BB DB DF 34 20 80 DE 54 00 ..4 ..:...4 ..T.
Seconds since Jan 1 1900 = 3688838176
Unix time = 1479849376 is year=2016, month=10, day=22, wday=2, hour=21, min=16, sec=16
Dump: *tzname=GMT
2000030C - F0 DC 00 00 ....
asctime = Tue Nov 22 21:16:16 2016

[027570] long unsigned int sendNTPpacket(IPAddress&)
Udp.beginPacket: res=1
Udp.write: res=48
Udp.endPacket: res=1
[028575] Packet received
Dump: packetBuffer
200003F4 - 24 01 06 E3 00 00 00 00 00 00 00 00 41 43 54 53 $...........ACTS
20000404 - DB DF 34 27 D1 4C F5 D1 00 00 00 00 00 00 00 00 ..4'.L..........
20000414 - DB DF 34 2B 86 64 DF 2C DB DF 34 2B 86 65 62 6F ..4+.d.,..4+.ebo
Seconds since Jan 1 1900 = 3688838187
Unix time = 1479849387 is year=2016, month=10, day=22, wday=2, hour=21, min=16, sec=27
Dump: *tzname=GMT
2000030C - F0 DC 00 00 ....
asctime = Tue Nov 22 21:16:27 2016

Zdrojový kód

Zdrojový kód sa nachádza na GitHub.

Video

Video sa nachádza na YouTube.

Možné problémy

Počas ladenia mi program niekoľkokrát kompletne zamrzol. Problém som vystopoval potiaľ, že UDP odoslanie sa uskutočnilo. Ťažko povedať, čo presne je za tým. Možno je problém v implementácii WiFi a podľa dostupných informácií viem, že Atmel pracuje na modifikácii firmvéru. Je možné, že s novou verziou pominú aj tieto problémy. Keď to bude k dispozícii, môžem otestovať tento kód znovu.


22.11.2016


Menu