Рейтинг@Mail.ru

Как сделать сервер времени (NTP) на Arduino

автор:
Be the first to comment! Arduino
Print Friendly, PDF & Email
Рассмотрим создание сервера времени, который берёт значение времени по спутниковому сигналу GPS или ГЛОНАСС, а затем при запросе клиентом возвращает его значение по протоколу NTP.

Для проекта нам понадобятся:

1Схема NTP сервера на Arduino

Протокол NTP – это протокол, который используется для синхронизации часов компьютера или иного устройства с сервером времени по сети. Любое сетевое устройство может послать сетевой пакет определённого вида серверу времени, а тот в ответ пришлёт точное значение времени. Запросившее устройство установит это значение на своих системных часах. Таким образом осуществляется синхронизация времени. Существует несколько популярных серверов точного времени. Так, например, в операционных системах Windows для синхронизации времени используются time.windows.com или time.nist.gov. Но можно использовать и свой собственный сервер. Но сначала мы его реализуем с помощью Arduino.

Откуда же мы будем брать значение точного времени? Для этого воспользуемся приёмником ГНСС сигналов. Такие приёмники с определённой периодичностью (обычно 1 раз в секунду или около того) возвращают данные в формате NMEA о своём местоположении, а также дате и времени. Описание формата NMEA можно скачать в конце статьи. Мы подключимся к приёмнику и по протоколу UART получим с него данные о точном времени.

GPS/GLONASS приёмник с UART
GPS/GLONASS приёмник с UART

Также нам понадобится модуль с часами реального времени (RTC), в который мы сохраним значение времени. И по запросу клиента будем возвращать время, считанное с этого модуля. Такие модули обычно имеют собственный элемент питания, и сохраняют время даже при отсутствии внешнего источника питания.

Модуль с часами реального времени DS1307
Модуль с часами реального времени DS1307

И, конечно же, нам понадобится модуль с сетевым интерфейсом или т.н. Ethernet-шилд. Этот модуль позволит подключить Arduino к локальной сети или к компьютеру по Ethernet.

Ethernet-шилд с микросхемой Wiznet W5100
Ethernet-шилд с микросхемой Wiznet W5100

В качестве альтернативы можно использовать Wi-Fi модуль, разумеется. Лишь бы ваши устройства, время на которых необходимо синхронизировать с сервером времени, находились в одной локальной сети с сервером времени на Arduino.

Теперь соединим все наши части воедино. Для этого сначала соберём «бутерброд» из Arduino и сетевого шилда, который выполнен в виде мезонинной платы. Далее подключим модуль часов DS1307 к выводам A4 и A5, а это шина I2C, как мы помним. Следовательно, пин A4 – это SDA, пин A5 – SCL. Приёмник сигналов ГНСС необходимо подключить к UART. Для этого можно подключить его к стандартным выводам RX и TX Arduino (пины 0 и 1, соответственно). Но тогда мы не сможем одновременно работать с приёмником и отлаживаться с выводом отладочных сообщений в последовательный порт. Поэтому рекомендую реализовать программный UART с помощью штатной библиотеки SoftwareSerial. Для этого подключим GPS приёмник к любым цифровым выводам (кроме 0 и 1), например, к 10 и 11.

Общий вид NTP сервера на Arduino
Общий вид NTP сервера на Arduino

Не забудем питание и землю, разумеется. И модуль часов, и приёмник питаются одним напряжением, равным 5 вольтам.

2Скетч NTP сервера для Arduino

Напишем скетч для Arduino, в котором реализуем функциональность сервера времени с поддержкой протокола NTP и с минимальным использованием сторонних библиотек.

Общий алгоритм следующий. Сначала будем опрашивать приёмник спутникового сигнала, пока не получим от него NMEA пакет с корректным значением времени. Нужный нам пакет с временем начинается с заголовка "$GPRMC". В этих пакетах время и дата хранятся на 2 и на 10 позициях соответственно, разделённых запятыми. Координаты достоверны, когда статус (позиция 3) будет "A".

Формат пакета NMEA с данными о времени
Формат пакета NMEA с данными о времени

Когда получим значение времени, запишем его в модуль RTC. Подробно работу с RTC мы разбирали здесь.

Далее запустим сервер и в цикле будем постоянно слушать входящие запросы по протоколу UDP на порту 123 (это стандартный порт протокола NTP). Как только сервер получит NTP запрос, прочитаем время из модуля часов реального времени, «упакуем» в ответный NTP пакет и отправим клиенту, который запросил время.

В конце статьи приложена программа для тестирования связи с NTP сервером. Вводите в единственное поле IP-адрес сервера, нажимаете кнопку Test и смотрите, какие данные возвращает ваш сервер.

Скетч сервера времени NTP и Arduino (разворачивается)
#define debug true // для вывода отладочных сообщений

#include <SoftwareSerial.h>
#include <Wire.h>
#include <Ethernet.h>
#include <EthernetUdp.h>

SoftwareSerial Serial1(10, 11);
EthernetUDP Udp;

// MAC, IP-адрес и порт NTP сервера:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // задайте свой MAC
IPAddress ip(192, 168, 0, 147); // задайте свой IP
#define NTP_PORT 123 // стандартный порт, не менять

#define RTC_ADDR 0x68 // i2c адрес RTC

static const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];

int year;
byte month, day, hour, minute, second, hundredths;
unsigned long date, time, age;
uint32_t timestamp, tempval;

void setup() {
  Wire.begin(); // стартуем I2C
  
#if debug
  Serial.begin(115200);
#endif
  Serial1.begin(4800); // старт UART для GPS модуля

  getGpsTime(); // получаем время GPS
  writeRtc(); // записываем время в RTC

  // запускаем Ethernet шилд в режиме UDP:
  Ethernet.begin(mac, ip);
  Udp.begin(NTP_PORT);
#if debug
  Serial.println("NTP started");
#endif  
}

void loop() {
  processNTP(); // обрабатываем приходящие NTP запросы
}

String serStr; // строка для хранения пакетов от GPS приёмника

// Читает пакеты GPS приёмника из COM-порта и пытается найти в них время
// Если время найдено, возвращает True, иначе - False
void getGpsTime() {
  bool timeFound = false;
  while (!timeFound) {
    while (Serial1.available()>0) {
    char c = Serial1.read();
      if (c != '\n') {
        serStr.concat(c);
      } else {
        timeFound = decodeTime(serStr);
        serStr = "";
      }
    }
  }
}

// Декодирует вермя по NMEA пакету 
// и возвращает True в случае успеха и False в обратном случае
bool decodeTime(String s) {
#if debug
    Serial.println("NMEA Packet = " + s);
#endif
  if (s.substring(0,6)=="$GPRMC") {
    String validFlag = s.substring(18,20);
    // Ждём валидные данные (флаг "V" - данные не валидны, "A" - данные валидны):
    if (validFlag == "A") {
      String timeStr = s.substring(7,17); // строка времени в формате ччммсс.сс
      hour = timeStr.substring(0,2).toInt();
      minute = timeStr.substring(2,4).toInt();
      second = timeStr.substring(4,6).toInt();
      hundredths = timeStr.substring(7,10).toInt();

      // ищем индекс 4-ой запятой с конца, после которой идёт дата
      int commaIndex = 1;
      for (int i=0;i<5;i++) {
        commaIndex = s.lastIndexOf(",", commaIndex-1);
      }
      String date = s.substring(commaIndex+1, commaIndex+7); // строка даты в формате ддммгг
      day = date.substring(0,2).toInt();
      month = date.substring(2,4).toInt();
      year = date.substring(4,6).toInt(); // передаются только десятки и единицы года
#if debug
    printDate();
#endif
      return true;
    }
  }
  return false;
}

// Запоминает время в RTC
void writeRtc() {
  byte arr[] = {0x00, dec2hex(second), dec2hex(minute), dec2hex(hour), 0x01, dec2hex(day), dec2hex(month), dec2hex(year)};
  Wire.beginTransmission(RTC_ADDR);
  Wire.write(arr, 8);
  Wire.endTransmission();
#if debug
  Serial.print("Set date: ");
  printDate();
#endif
}

// Преобразует число из dec представления в hex представление
byte dec2hex(byte b) {
  String bs = (String)b;
  byte res;
  if (bs.length()==2) {
    res = String(bs.charAt(0)).toInt() * 16 + String(bs.charAt(1)).toInt();
  } else {
    res = String(bs.charAt(0)).toInt();
  }
#if debug
  Serial.println("dec " + (String)b + " = hex " + (String)res);
#endif  
  return res;
}

// Читает из RTC время и дату
void getRtcDate() {
  Wire.beginTransmission(RTC_ADDR);
  Wire.write(byte(0));
  Wire.endTransmission();
  
  Wire.beginTransmission(RTC_ADDR);
  Wire.requestFrom(RTC_ADDR, 7);
  byte t[7];
  int i = 0;
  while(Wire.available()) {
    t[i] = Wire.read();
    i++;
  }
  Wire.endTransmission();
  second = t[0];
  minute = t[1];
  hour = t[2];
  day = t[4];
  month = t[5];
  year = t[6];
#if debug
  Serial.print("Get date: ");
  printDate();
#endif
}

// Обрабатывает запросы к NTP серверу
void processNTP() {
  int packetSize = Udp.parsePacket();
  if (packetSize) {
    Udp.read(packetBuffer, NTP_PACKET_SIZE);
    IPAddress remote = Udp.remoteIP();
    int portNum = Udp.remotePort();

#if debug
    Serial.println();
    Serial.print("Received UDP packet size ");
    Serial.println(packetSize);
    Serial.print("From ");

    for (int i=0; i<4; i++) {
      Serial.print(remote[i], DEC);
      if (i<3) { Serial.print("."); }
    }
    Serial.print(", port ");
    Serial.print(portNum);

    byte LIVNMODE = packetBuffer[0];
    Serial.print("  LI, Vers, Mode :");
    Serial.print(packetBuffer[0], HEX);

    byte STRATUM = packetBuffer[1];
    Serial.print("  Stratum :");
    Serial.print(packetBuffer[1], HEX);

    byte POLLING = packetBuffer[2];
    Serial.print("  Polling :");
    Serial.print(packetBuffer[2], HEX);

    byte PRECISION = packetBuffer[3];
    Serial.print("  Precision :");
    Serial.println(packetBuffer[3], HEX);

    for (int z=0; z<NTP_PACKET_SIZE; z++) {
      Serial.print(packetBuffer[z], HEX);
      if (((z+1) % 4) == 0) {
        Serial.println();
      }
    }
    Serial.println();
#endif

    // Упаковываем данные в ответный пакет:
    packetBuffer[0] = 0b00100100;   // версия, режим
    packetBuffer[1] = 1;   // стратум
    packetBuffer[2] = 6;   // интервал опроса
    packetBuffer[3] = 0xFA; // точность

    packetBuffer[7] = 0; // задержка
    packetBuffer[8] = 0;
    packetBuffer[9] = 8;
    packetBuffer[10] = 0;

    packetBuffer[11] = 0; // дисперсия
    packetBuffer[12] = 0;
    packetBuffer[13] = 0xC;
    packetBuffer[14] = 0;
      
    getRtcDate();
    timestamp = numberOfSecondsSince1900Epoch(year,month,day,hour,minute,second);

#if debug
    Serial.println("Timestamp = " + (String)timestamp);
#endif

    tempval = timestamp;

    packetBuffer[12] = 71; //"G";
    packetBuffer[13] = 80; //"P";
    packetBuffer[14] = 83; //"S";
    packetBuffer[15] = 0; //"0";

    // Относительное время 
    packetBuffer[16] = (tempval >> 24) & 0xFF;
    tempval = timestamp;
    packetBuffer[17] = (tempval >> 16) & 0xFF;
    tempval = timestamp;
    packetBuffer[18] = (tempval >> 8) & 0xFF;
    tempval = timestamp;
    packetBuffer[19] = (tempval) & 0xFF;

    packetBuffer[20] = 0;
    packetBuffer[21] = 0;
    packetBuffer[22] = 0;
    packetBuffer[23] = 0;

    // Копируем метку времени клиента 
    packetBuffer[24] = packetBuffer[40];
    packetBuffer[25] = packetBuffer[41];
    packetBuffer[26] = packetBuffer[42];
    packetBuffer[27] = packetBuffer[43];
    packetBuffer[28] = packetBuffer[44];
    packetBuffer[29] = packetBuffer[45];
    packetBuffer[30] = packetBuffer[46];
    packetBuffer[31] = packetBuffer[47];

    // Метка времени 
    packetBuffer[32] = (tempval >> 24) & 0xFF;
    tempval = timestamp;
    packetBuffer[33] = (tempval >> 16) & 0xFF;
    tempval = timestamp;
    packetBuffer[34] = (tempval >> 8) & 0xFF;
    tempval = timestamp;
    packetBuffer[35] = (tempval) & 0xFF;

    packetBuffer[36] = 0;
    packetBuffer[37] = 0;
    packetBuffer[38] = 0;
    packetBuffer[39] = 0;

    // Записываем метку времени 
    packetBuffer[40] = (tempval >> 24) & 0xFF;
    tempval = timestamp;
    packetBuffer[41] = (tempval >> 16) & 0xFF;
    tempval = timestamp;
    packetBuffer[42] = (tempval >> 8) & 0xFF;
    tempval = timestamp;
    packetBuffer[43] = (tempval) & 0xFF;

    packetBuffer[44] = 0;
    packetBuffer[45] = 0;
    packetBuffer[46] = 0;
    packetBuffer[47] = 0;

    // Отправляем NTP ответ 
    Udp.beginPacket(remote, portNum);
    Udp.write(packetBuffer, NTP_PACKET_SIZE);
    Udp.endPacket();
  }
}

// Выводит отформатированноую дату
void printDate() {
  char sz[32];
  sprintf(sz, "%02d.%02d.%04d %02d:%02d:%02d.%03d", day, month, year+2000, hour, minute, second, hundredths);
  Serial.println(sz);
}

const uint8_t daysInMonth [] PROGMEM = { 31,28,31,30,31,30,31,31,30,31,30,31 }; // число дней в месяцах
const unsigned long seventyYears = 2208988800UL; // перевод времени unix в эпоху

// Формирует метку времени от момента 01.01.1900
static unsigned long int numberOfSecondsSince1900Epoch(uint16_t y, uint8_t m, uint8_t d, uint8_t h, uint8_t mm, uint8_t s) {
  if (y >= 1970) { y -= 1970; }
  uint16_t days = d;
  for (uint8_t i=1; i<m; ++i) {
    days += pgm_read_byte(daysInMonth + i - 1);
  }
  if (m>2 && y%4 == 0) { ++days; }
  days += 365 * y + (y + 3) / 4 - 1;
  return days*24L*3600L + h*3600L + mm*60L + s + seventyYears;
}

Функция getGpsTime() постоянно читает приходящие от ГНСС приёмника пакеты, и когда получает очередной пакет, проверяет, нет ли в нём валидных данных времени. Если время есть, то происходит его разбор, далее запоминание в модуле RTC

Проверка NMEA пакетов осуществляется в функции decodeTime().

Несколько слов о функции dec2hex(). В ней несколько извращённо число переводится из десятичного представления в 16-ное. Точнее, так. Модуль часов показывает время в виде, например, 16:52:08. Но здесь каждое из этих чисел не десятичное, а 16-ное. То есть, в действительности это время в RTC хранится так: 0x16:0x52:0x08. А с GPS-приёмника мы получаем время в десятичном формате. И чтобы записать те же 16 часов в модуль RTC, нужно преобразовать десятичное 16 в шестнадцатеричное 0x16, что является десятичным 22. А полное время 0x16:0x52:0x08 будет в десятичном представлении 22:82:08. Хм, 82 минуты, странно, да? :) Но такое уж надо сделать преобразование, чтобы модуль часов реального времени запомнил правильное время.

Last modified onВоскреснье, 06 Октябрь 2019 09:25 Read 439 times
Ключевые слова: :

Поделиться

Print Friendly, PDF & Email

Leave a comment