STM Урок 83. LAN. ENC28J60. NTP. Узнаём точное время. Часть 1



 

Урок 83

 

Часть 1

 

LAN. ENC28J60. NTP. Узнаём точное время

 

Так как на прошлом уроке мы сумели уже пообщаться с удалённым узлом, а также у нас освоен практически протокол транспортного уровня — UDP, то мы уже вполне можем получить какие-то данные из внешней сети, в частном случае из глобальной мировой сети — Интернет. Поэтому целью сегодняшнего занятия мы поставим себе задачу — получить точное время с одного из серверов точного времени по протоколу NTP (Network Time Protocol), который по модели OSI является уже протоколом прикладного уровня и несёт в себе уже осознанную полезную нагрузку. С протоколами такого уровня мы ещё не встречались, ну это ничего страшного. Просто по иерархии протоклов заголовок и данные данного уровня будут являться данными какого-то протокла транспортного уровня, только и всего. В качестве транспортного уровня мы возьмем UDP, так как мы с ним уже знакомы и умеем передавать по нему данные.

Протокол NTP также является структрурированным, как и все протоколы, которые мы пока рассматривали, то есть у него существует осознанный заголовок с определёнными полями, который в основном также является и данными. Также наряду со структурированными протоколами существуют и неструктурированные, которые осознанного или осязаемого заголовка в себе не содержат, как например протокл HTTP, который является полностью текстовым и разграничение данных там определяется либо тегами, либо переводом строки. До этого протокола мы доберёмся также в скором времени.

Приведу заголовок NTP (нажмите на картинку для увеличения изображения)

 

image00_0500

 

Как мы можем заметить, данный протокол имеет немаленький заголовок. Поэтому можно было бы в принципе познакомиться плотно с его некоторыми полями в процессе написания кода, но я всё же решил дать некоторую информацию по данным полям сразу. Кому это хорошо известно, то вполне это объяснение может пропустить и перейти сразу к написанию кода и к его тонкостям.

Начнём.

ИК (идентификатор коррекции) — целое число, показывающее предупреждение о секунде координации (секунда координации или високосная секунда — дополнительная секунда, добавляемая ко всемирному координированному времени для согласованием его со средним солнечным временем UT1).

0 — Нет предупреждения

1 — Последняя минута дня содержит 61 секунду

2 — Последняя минута дня содержит 59 секунд

3 — Неизвестно (время не синхронизировано)

Версия — целое число, представляющее версию протокола.

Режим

0 — зарезервировано

1 — симметричный активный режим

2 — симметричный пассивный режим

3 — клиент

4 — сервер

5 — широковещательный режим

6 — контрольное сообщение NTP

7 — зарезервировано для частного использования.

Часовой слой

0 — не определено или недопустим

1 — первичный сервер

2-15 — вторичный сервер, использующий NTP

16 — не синхронизировано

17-255 — зарезервировано.

Первичные серверы от вторичных отличаются большей точностью, и. соответственно, своей серверной мощностью. Но зато дождаться от них ответа значительно труднее.

Интервал опроса — целое число со знаком, представляющее максимальный интервал между последовательными сообщениями. Значение равно двоичному логарифму секунд. Предлагаемые по умолчанию пределы на минимальные и максимальные опросы — 6 и 10, соответственно.

Точность — (sys.precision, peer.precision, pkt.precision). Это целая переменная со знаком, обозначающая точность часов в секундах и выраженная как ближайшая степень числа 2. Значение должно быть округлено в большую сторону до ближайшего значения степени 2, например, сетевой частоте 50-Гц (20 мс) или 60-Гц (16.67 мс) будет поставлено в соответствие величина -5 (31.25 мс), в то время как кварцевой частоте 1000-Гц (1 мс) будет поставлено в соответствие значение -9 (1.95 мс).

Задержка — общее время распространения сигнала в обе стороны в коротком формате NTP. Короткий формат NTP — это такой формат, когда секунды представлены в 16 старших битах — а доли в 16 младших, длинный формат — это то же самое, только вместо 16 бит под секунды и доли отводится по 32 бита.

Всё дело в том. что время на серверах NTP во всех полях его заголовка хранится неструктурированно, то есть оно никак не разбито на дни, месяцы, часы, минут и т.д, как это делается в микросхемах RTC. Время там хранится в количестве секунд и долей секунд.

Дисперсия — число с фиксированной запятой больше нуля, несущее в себе максимальное значение временной ошибки по отношению к первичному эталону в секундах.

Идентификатор источника — код источника синхронизации. Зависит от значения в поле «часовой слой». Хранится в формате 4-октетной ASCII-строки, выровненной по левому краю и дополненной при необходимости нулями.

Вот примеры таких идентификторов

 

image01

 

Время обновления — это время, когда система последний раз устанавливала или корректировала время. Хранится в полном формате NTP.

Начальное время — время клиента, когда запрос отправляется серверу. Хранится в полном формате NTP.

Время приёма — время сервера, когда запрос приходит от клиента. Хранится в полном формате NTP.

Время отправки — это время сервера, когда запрос отправляется клиенту. Хранится в полном формате NTP.

Вот приблизительно так с протоколом. Насчёт четырёх последних полей: нам более всего интересно последнее время, его мы и будем считывать. Значительная часть полей нам будет вообще не интересна, причём многие из них совсем не работают в случае клиента, поэтому мы их заполнять будем нулями. Если информация, которую я предоставил по протоколу NTP недостаточна или кому-то интересно будет узнать побольше по этой теме, то такой информации в сети весьма предостаточно. Знаю по себе. Когда начал изучать данный протокол, я даже сначала запутался. Но потом, посмотрев различные примеры других блогеров, я немного в своей голове всю путаницу устранил.

Для достижения основной цели нашего занятия — во что бы то ни стало узнать мировое время, пришлось прибегнуть к счётчику таймеру, к количеству попыток отправки запроса серверу, так как сервер почему-то отвечает не на все запросы. Причём серверов этих очень много, я выбрал тот сервер, который выбран по умолчанию в моей операционной системе Windows в качетстве основного сервера для коррекции точного времени. Как узнать его адрес IP (а нужен именно он, так как протокол DNS, который определяет IP по доменному имени, нам пока неизвестен), я покажу позже в процессе написания кода, который мы сейчас уже начнём и писать.

Проект создан был с именем ENC28J60_NTP из проекта прошлого урока ENC28J60_REMOTE.

Как всегда, запустим проект в генераторе кода Cube MX, а затем, ничего в нём не трогая, запустим его в Keil. Добавим все наши библиотечные файлы в проект, а также настроим программатор на авторезет.

Для работы с протоколом NTP я решил создать отдельную библиотеку в виде пары файлов ntp.c и ntp.h со следующим содержимым

 

ntp.c:

#include "ntp.h"

//--------------------------------------------------

extern UART_HandleTypeDef huart1;

//-----------------------------------------------

extern char str1[60];

extern uint8_t net_buf[ENC28J60_MAXFRAME];

//--------------------------------------------------

 

ntp.h:

#ifndef NTP_H_

#define NTP_H_

//--------------------------------------------------

#include "stm32f1xx_hal.h"

#include <string.h>

#include <stdlib.h>

#include <stdint.h>

#include "enc28j60.h"

#include "net.h"

//--------------------------------------------------

//--------------------------------------------------

#endif /* NTP_H_ */

 

В сегодняшнем уроке мы будем посылать запросы NTP также, как и многие запросы из командной строки терминальной программы, указывая IP адрес и порт сервера точного времени. Порт можно было бы не включать в командную строку, так как зарезервированный порт для передачи по протоколу NTP — 123, но всё же лучше указать, мало ли найдём какие-то серверы с другим номером порта, чем чёрт не шутит, да и не тяжело ввести комбинацию цифр 1 2 3. Ну и, как многие из вас, я надеюсь, догадались,. заканчиваться строка будет у нас на букву n.

 

 

Поэтому перейдём в файл net.c в обработчик приёма байт UART1_RxCpltCallback и добавим соответствующее условие в тело данного обработчика

 

  net_cmd();

}

else if (b=='n')

{

  usartprop.is_ip=6;//статус попытки отправить NTP-пакет

  net_cmd();

}

 

В функции net_cmd добавим соответствующие условия для попытки и самой отправки пакета

 

  usartprop.is_ip=0;

}

else if(usartprop.is_ip==6)//статус попытки отправить NTP-пакет

{

  ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt,ip);

  usartprop.is_ip=7;//статус отправки NTP-пакета

  usartprop.usart_cnt=0;

  arp_request(ip);//узнаем mac-адрес

}

else if(usartprop.is_ip==7)//статус отправки NTP-пакета

{

  port=port_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);

  usartprop.is_ip=0;

}

 

Буквально весь код условий аналогичен коду прошлого урока, поэтому в объяснении не нуждается.

Далее в местах возврата в функцию net_cmd добавим статус отправки пакета NTP

 

if((usartprop.is_ip==3)||(usartprop.is_ip==5)||(usartprop.is_ip==7))//статус отправки UDP-, ICMP- или NTP пакета

 

В файле net.h подключим нашу новую библиотеку, как обычно, внизу файла

 

#include "udp.h"

#include "ntp.h"

 

В файле ntp.c добавим функцию отправки пакета NTP

 

//--------------------------------------------------

uint8_t ntp_request(uint8_t *ip_addr, uint16_t port)

{

  uint8_t res=0;

  return res;

}

//--------------------------------------------------

 

Добавим на неё прототип и вызовем её в файле net.c в функции net_cmd в теле соответствующего условия

 

else if(usartprop.is_ip==7)//статус отправки NTP-пакета

{

  port=port_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);

  ntp_request(ip,port);

 

Вернёмся в файл ntp.h и сначала добавим структуру для разделения целой и дробной части секунд

 

#include "net.h"

//--------------------------------------------------

typedef struct ntp_ts {

  uint32_t sec;//целая часть

  uint32_t frac;//дробная часть

} ntp_ts_ptr;

//--------------------------------------------------

 

Добавим также структуру для заголовка NTP

 

//--------------------------------------------------

typedef struct ntp_pkt {

  uint8_t flags; //флаги

  uint8_t peer_clock_stratum;//страта

  uint8_t peer_pooling_interval;//Интервал опроса

  uint8_t peer_clock_precision;//Точность

  uint32_t root_delay;//Задержка

  uint32_t root_dispersion;//Дисперсия

  uint32_t ref_id;//Идентификатор источника

  ntp_ts_ptr ref_ts;//Время обновления

  ntp_ts_ptr orig_ts;//Начальное время

  ntp_ts_ptr rcv_ts;//Время приёма

  ntp_ts_ptr tx_ts;//Время отправки

} ntp_pkt_ptr;

//--------------------------------------------------

 

Как мы и можем наблюдать, отдельных данных у пакета не будет, только заголовок.

Также выше давайте напишем макрос для порта клиента, чтобы он был отдельный, так как типа 333 порта серверу может не очень понравиться вариант

 

#include "net.h"

//--------------------------------------------------

#define LOCAL_PORT_FOR_NTP 14444

 

 

Перейдём в файл ntp.c и подключим массивы MAC-адреса и адреса IP

 

extern uint8_t net_buf[ENC28J60_MAXFRAME];

extern uint8_t macaddr[6];

extern uint8_t ipaddr[4];

 

 

Продолжим писать код нашей функции отправки пакета NTP ntp_request.

Создадим переменную для длины и  создадим указатели для заголовков всех уровней нашего пакета

 

uint8_t res=0;

uint16_t len;

enc28j60_frame_ptr *frame=(void*) net_buf;

ip_pkt_ptr *ip_pkt = (void*)(frame->data);

udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);

ntp_pkt_ptr *ntp_pkt = (void*)(udp_pkt->data);

 

Заполним нулями весь заголовок NTP

 

ntp_pkt_ptr *ntp_pkt = (void*)(udp_pkt->data);

//заполним нулями всю структуру ntp

memset(ntp_pkt, 0, sizeof(ntp_pkt_ptr));

 

Далее заполним необходимые поля заголовка NTP. Остальные нам не нужны

 

memset(ntp_pkt, 0, sizeof(ntp_pkt_ptr));

//Заполним заголовок NTP

ntp_pkt->flags = 0x1b;

 

Мы видим, что мы заполнили только флаги. Больше нам ничего не нужно.

Пойдём вниз по протокольной лестнице OSI. Следующий протокол — UDP

 

ntp_pkt->flags = 0x1b;

//Заполним заголовок UDP

udp_pkt->port_dst = be16toword(port);

udp_pkt->port_src = be16toword(LOCAL_PORT_FOR_NTP);

len = sizeof(ntp_pkt_ptr) + sizeof(udp_pkt_ptr);

udp_pkt->len = be16toword(len);

udp_pkt->cs=0;

udp_pkt->cs=checksum((uint8_t*)udp_pkt-8, len+8, 1);

 

Далее заполним заголовок более нижнего уровня — IP

 

udp_pkt->cs=checksum((uint8_t*)udp_pkt-8, len+8, 1);

//Заполним заголовок пакета IP

len+=sizeof(ip_pkt_ptr);

ip_pkt->len=be16toword(len);

ip_pkt->id = 0;

ip_pkt->ts = 0;

ip_pkt->verlen = 0x45;

ip_pkt->fl_frg_of=0;

ip_pkt->ttl=128;

ip_pkt->cs = 0;

ip_pkt->prt=IP_UDP;

memcpy(ip_pkt->ipaddr_dst,ip_addr,4);

memcpy(ip_pkt->ipaddr_src,ipaddr,4);

ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);

 

Я думаю, здесь также всё предельно ясно. Мы делали это неоднократно.

Следующий уровень — канальный. Поэтому заполним заголовок Ethernet и отправим наш пакет

 

  ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);

  //Заполним заголовок пакета Ethernet

  memcpy(frame->addr_src,macaddr,6);

  frame->type=ETH_IP;

  enc28j60_packetSend((void*)frame,len + sizeof(enc28j60_frame_ptr));

  return res;

}

 

Далее нам ничего не остаётся, как ждать ответа сервера NTP аналогично тому, как мы ждали ответ удалённых узлов на прошлом занятии, посылая пакеты ICMP.

Только сегодня будет это отягощено тем, что мы не можем посмотреть, пришёл ли на сервер наш пакет, так как на локальный компьютер мы такой запрос не пошлём. Вернее, послать-то мы его можем, только компьютер нам не ответит, так как там не организован NTP-сервер.

Поэтому будем ждать. Причём ждать не на сетевом уровне, а на транспортном.

Поэтому для начала давайте в нашем файле ntp.c добавим выше ещё одну функцию для считывания заголовка NTP

 

//--------------------------------------------------

uint8_t ntp_read(enc28j60_frame_ptr *frame, uint16_t len)

{

  uint8_t res=0;

  return res;

}

//--------------------------------------------------

 

Создадим для этой функции прототип и затем зайдём в файл udp.c в функцию udp_read и в соответствующем месте проверим порт, не является ли он стандартным портом NTP — 123, перед этим передвинув выше установку указателей на пакеты

 

uint8_t res=0;

ip_pkt_ptr *ip_pkt = (void*)(frame->data);

udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);

if(be16toword(udp_pkt->port_src)==123)

{

  ntp_read(frame,len);

  return 0;

}

 

Теперь если порт у нас совпадёт со значением 123, то мы вызовем функцию чтения пакет NTP и выйдем из функции, не выполняя дальнейший код.

Вернёмся в файл ntp.c и продолжим писать тело нашей функции приёма пакета NTP.

Как всегда, сначала установим указатели на заголовки различных уровней

 

uint8_t res=0;

ip_pkt_ptr *ip_pkt = (void*)(frame->data);

udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);

ntp_pkt_ptr *ntp_pkt = (void*)(udp_pkt->data);

 

Ну и, соответственно нам нужно будет из заголовка NTP только одно поле — это время отправки.

Но так как оно будет у нас в секундах, так как дробную часть мы вообще не будем использовать, микросекунды и прочие доли нам вообще не нужны, то нам придётся как-то из этих секунд выбрать год, месяц, и всё остальное. Я сначала хотел написать свой алгоритм, а затем понял, что это ни к чему,так как будет очень длинный и сложный алгоритм, особенно в расчёте даты, нужно учесть разные високосные мелочи, а в стандартной библиотеке у нас уже имеется готовые функции, достаточно, только подключить соответствующий заголовочный файл, что я и сделал в файле ntp.h

 

#include "net.h"

#include <time.h>

 

Из данной библиотеки мы будем использовать структуру типа tm, указатель на которую мы и добавим в файле ntp.c в функции ntp_read

 

uint8_t res=0;

struct tm *timestruct;

ip_pkt_ptr *ip_pkt = (void*)(frame->data);

 

Так как секунды у нас также хранятся в формате big endian (с переворотом старшинства байтов), а число у нас здесь уже 32-битное, то нужно добавтить ещё одну дефайновую замену в файл net.h

 

#define be16toword(a) ((((a)>>8)&0xff)|(((a)<<8)&0xff00))

#define be32todword(a) ((((a)>>24)&0xff)|(((a)>>8)&0xff00)|(((a)<<8)&0xff0000)|(((a)<<24)&0xff000000))

 

Вернёмся в нашу функцию ntp_read и покажем в терминальной программе пока сырое значение секунд без структурирования

 

ntp_pkt_ptr *ntp_pkt = (void*)(udp_pkt->data);

sprintf(str1,"%lu\r\n", be32todword((unsigned long)ntp_pkt->tx_ts.sec));

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

 

Теперь давайте узнаем сетевой адрес сервера NTP. Для этого запустим WireShark и отфильтруем анализ пакетов по NTP

 

image03

 

Кликнув по времени в трее, мы запустим изменение времени

 

image04

 

В открывшемся диалоге перейдём на закладку «Время по интернету», а там запустим «Изменить параметры»

 

image05

 

Оставим сервер по умолчанию и нажмём «Обновить сейчас», дождавшись затем синхронизации времени

 

image06

 

Теперь мы можем все диалоги закрыть и перейти в Wireshark и из пакета забрать IP-адрес сервера NTP

 

image07

 

Чтобы не писать вручную адрес, можно забрать его с помощью пунктов контекстного меню — Copy->Value.

Также можно найти список серверов точного времени в интернете и, мало того, ещё и посмотреть их загруженность на данный момент. Так что спосбобы узнавания адресов данных серверов различны, а какой удобен для вас — вам выбирать.

Наконец-то мы можем уже собрать наш код, прошить наш контроллер и посмотреть результат нашей работы в терминальной программе

 

image00 

 

Как мы видим, пакет нам с сервера пришёл, но только с 2 попытки. Но это ничего, нам не сложно сделать и 2, и 3 и более попыток, хотелось бы, конечно, чтобы попытки происходили автоматически, без нашего участия, но об этом чуть позже.

 

В следующей части нашего занятия мы увидим время с сервера NTP в удобном формате, а также автоматизируем процесс повторного запроса времени.

 

 

Предыдущий урок Программирование МК STM32 Следующая часть

 

 

Отладочную плату можно приобрести здесь STM32F103C8T6

Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN

Переходник USB to TTL можно приобрести здесь USB to TTL ftdi ft232rl

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM LAN. ENC28J60. NTP. Узнаём точное время

3 комментария на “STM Урок 83. LAN. ENC28J60. NTP. Узнаём точное время. Часть 1
  1. megger380:

    Простите за назойливость, но ведь в wireshark даже не видно запросов…

    • megger380:

      Ещё раз простите, вопросы сняты, заработало… А почему в wireshark не видно запросов и ответов? С пк видно, с модуля нет…?

      • Артём:

        Может потому, что Wireshark попросту не видит пакетов, которые не предназначены этому компьютеру. Свитч просто роутит запросы в инет. Можно на ПК запустить службу NTP и обращаться за «точным» временем именно к ПК.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*