вторник, 26 марта 2013 г.

Урок 2: iBoard Pro - синхронизация времени (NTP)

На предыдущем уроке мы реализовали работу с модулем RTC и организовали отображение данных о дате/времени на TFT-дисплее.

Некоторые из вас могут остаться недовольны точностью хода часов, реализованных на DS1370 (заметно "убегают" или "отстают"), но это не является проблемой, ведь в нашей плате iBoard Pro "на борту" имеется полноценный сетевой интерфейс на базе Wiznet W5100 - сегодня мы реализуем синхронизацию времени с помощью NTP-сервера.
На дисплее отображается (сверху-вниз): время, дата, IP-адрес, время последней синхронизации 
Для этого необходимо следующее:
  • "Завести" сетевой интерфейс (чтобы устройство правильно зарегистрировалось в домашней сети и получило IP-адрес)
  • Отправить UDP-запрос к NTP-серверу
  • Получить UDP-ответ и преобразовать его в удобный формат
  • "Подвести" модуль часов на DS1307
Собственно, план работ понятен. Приступим.
Мы продолжим развивать тот скетч (iBoardPro) из предыдущего урока - добавим в наш проект дополнительный файл ntp.

Сетевой интерфейс

В основном файле нам необходимо добавить подключение библиотек, необходимых для работы с сетевым интерфейсом и протоколом UDP:
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
Поскольку все ntp-сервера возвращают время GMT - обязательно нужно ввести свой часовой пояс (если вы, конечно, не находитесь на Гринвичском меридиане). Дополнительно введем константу, значение которой (в миллисекундах) будет определять период синхронизации времени с сервером и определим переменную, в которой будем остлеживать, пора ли производить синхронизацию:
#define NTPtimeSync 600000   // 10 минут
#define TimeOffset 4  // часовой пояс - GMT +4 (Москва)
unsigned long nextNTPtime = 0;
Так же необходимо выбрать сервер, с которым будет производиться синхронизация. Обычно выбирают тот, что "поближе" и "постабильнее", чтобы часы показывали время, наиболее близкое к истинному. Но поскольку мы реализуем "бытовой" прибор - можно выбрать любой:
IPAddress timeServer(195, 13, 1, 153); // ntp2.belbone.be NTP server
В нашем уроке мы реализуем вариант, когда наше устройство автоматически должно получить IP-адрес от роутера (DHCP).
Для этого достаточно определить только mac-адрес нашего сетевого интерфейса:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEA };
Вы можете выбрать произвольный mac-адрес, главное, чтобы он не совпадал ни с каким другим mac-адресом в вашей домашней сети.

Для того, чтобы потом можно было где-то использовать полученный IP-адрес, заведем соответствующий массив char - currentIP.

Инициализируем (в функции setup) сетевой интерфейс следующим образом:

  if (Ethernet.begin(mac) == 0) {
    Serial.println("Failed to configure Ethernet using DHCP");
  }
В случае неудачи - в последовательный интерфейс выведем соответствующее сообщение.
Сразу после инициализации - зафиксируем полученный IP-адрес в массиве currentIP (выведем его на дисплей в соответствующей функции (myDisplay()).
Визуальным подтверждением того, что вы задействовали сетевой интерфейс будет задействованная световая индикация работы сетевого модуля рядом с LAN-разъемом на плате (6 зеленых светодиодов, которые в предыдущем уроке скромно "молчали").

Для фиксации времени последней синхронизации часов с сервером заведем другой массив: lastNTPsync[].

Дополнительно в наш проект введем файл common - в нем будем определять функции, которые могут понадобиться в дальнейшем. На сегодняшнем "уроке" такой функцией станет time2str, с помощью которой текущее время и дату преобразуем в соответствующий символьный массив.

Теперь перейдем непосредственно к корректировке времени.

NTP

Создадим функцию, которую будем вызывать в каждом цикле loop(), но сделаем ее таким образом, чтобы она отрабатывала только по истечении заданного периода синхронизации (NTPtimeSync):
void ntpRTCupdate() {
  if(millis() > nextNTPtime) {
    Udp.begin(localPort);
    sendNTPpacket(timeServer); // send an NTP packet to a time server
      // wait to see if a reply is available
    delay(200);
    if ( Udp.parsePacket() ) {
      // 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;
      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
      const unsigned long seventyYears = 2208988800UL;  
      // subtract seventy years:
      // calculate Unix time:
      unsigned long epoch = secsSince1900 - seventyYears;
      // print Unix time:
      // введем корректировку для часового пояса
      epoch = epoch + (TimeOffset * 3600L);  
      // скорректируем RTC
      RTC.adjust(DateTime(epoch));
      time2str(RTC.now(), lastNTPsync);
   
      nextNTPtime = millis()+NTPtimeSync;
    }
    Udp.stop();
  }
}

// send an NTP request to the time server at the given address
unsigned long sendNTPpacket(IPAddress& address)
{
  // 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();
}

В приведенном коде присутствует и другая функция (sendNTPpacket), с помощью которой отправляется UDP-пакет данных к серверу.
Выделенная жирным шрифтом строчка - собственно то, ради чего все и было сделано - "подводка" модуля часов, имеющегося на плате.

Результат 2 урока

Осталось поправить функцию myDisplay(), чтобы вывести важные для нас параметры на экран:
void myDisplay() {
  // время
  time2display(70, 0, false, 255, 255, 255);
  // дата
  date2display(0, 50, true, 255, 255, 0, BigFont);
  // выведем текущий IP-адрес
  myGLCD.setColor(255, 255, 255); // белый
  myGLCD.setFont(BigFont);
  myGLCD.print(currentIP, 50, 80);
  // и время последней синхронизации с NTP-сервером
  myGLCD.setColor(0, 255, 255);  // голубой
  myGLCD.print(lastNTPsync, 20, 100);
}  
Тут мы удалили лишние вызовы функций для вывода времени и даты и добавили вывод новых параметров.

Собственно, на этом план урока полностью выполнен. В результате вы должны на своей плате видеть что-то типа (IP-адрес будет отличаться от того, что виден на экране: 192.168.1.xxx, 192.168.0.xxx или другой, в зависимости от того, как настроена ваша сеть):

Полный скетч можно скачать по ссылке.

Продолжение следует... 

2 комментария:

  1. >введем файл common
    и где его взять?

    display_temperature_with_SNMP_and_pressure.ino: In function 'void ntpRTCupdate()':
    display_temperature_with_SNMP_and_pressure:630: error: 'time2str' was not declared in this scope

    ОтветитьУдалить
  2. При воспроизведении данного примера на плате Iteaduino IBoard Pro (ATmega 2560) с установленной батарейкой не работает RTC, вместо даты/времени на секунду вспыхивают рандомные цифры в строке времени и плата уходит в ребут. Без батарейки всё работает как надо.
    Аналогично на других примерах с RTC, с батарейкой выводится NO RTC, без батарейки часы работают.
    Глюк моего экземпляра платы или можно вылечить?

    ОтветитьУдалить