Урок 47
Часть 1
LAN. ENC28J60. NTP. Узнаём точное время
Так как на прошлом уроке мы сумели уже пообщаться с удалённым узлом, а также у нас освоен практически протокол транспортного уровня — UDP, то мы уже вполне можем получить какие-то данные из внешней сети, в частном случае из глобальной мировой сети — Интернет. Поэтому целью сегодняшнего занятия мы поставим себе задачу — получить точное время с одного из серверов точного времени по протоколу NTP (Network Time Protocol), который по модели OSI является уже протоколом прикладного уровня и несёт в себе уже осознанную полезную нагрузку. С протоколами такого уровня мы ещё не встречались, ну это ничего страшного. Просто по иерархии протоколов заголовок и данные данного уровня будут являться данными какого-то протокола транспортного уровня, только и всего. В качестве транспортного уровня мы возьмем UDP, так как мы с ним уже знакомы и умеем передавать по нему данные.
Протокол NTP также является структурированным, как и все протоколы, которые мы пока рассматривали, то есть у него существует осознанный заголовок с определёнными полями, который в основном также является и данными. Также наряду со структурированными протоколами существуют и неструктурированные, которые осознанного или осязаемого заголовка в себе не содержат, как например протокол HTTP, который является полностью текстовым и разграничение данных там определяется либо тегами, либо переводом строки. До этого протокола мы доберёмся также в скором времени.
Приведу заголовок NTP (нажмите на картинку для увеличения изображения)
Как мы можем заметить, данный протокол имеет немаленький заголовок. Поэтому можно было бы в принципе познакомиться плотно с его некоторыми полями в процессе написания кода, но я всё же решил дать некоторую информацию по данным полям сразу. Кому это хорошо известно, то вполне это объяснение может пропустить и перейти сразу к написанию кода и к его тонкостям.
Начнём.
ИК (идентификатор коррекции) — целое число, показывающее предупреждение о секунде координации (секунда координации или високосная секунда — дополнительная секунда, добавляемая ко всемирному координированному времени для согласованием его со средним солнечным временем 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-строки, выровненной по левому краю и дополненной при необходимости нулями.
Вот примеры таких идентификторов
Время обновления — это время, когда система последний раз устанавливала или корректировала время. Хранится в полном формате NTP.
Начальное время — время клиента, когда запрос отправляется серверу. Хранится в полном формате NTP.
Время приёма — время сервера, когда запрос приходит от клиента. Хранится в полном формате NTP.
Время отправки — это время сервера, когда запрос отправляется клиенту. Хранится в полном формате NTP.
Вот приблизительно так с протоколом. Насчёт четырёх последних полей: нам более всего интересно последнее время, его мы и будем считывать. Значительная часть полей нам будет вообще не интересна, причём многие из них совсем не работают в случае клиента, поэтому мы их заполнять будем нулями. Если информация, которую я предоставил по протоколу NTP недостаточна или кому-то интересно будет узнать побольше по этой теме, то такой информации в сети весьма предостаточно. Знаю по себе. Когда начал изучать данный протокол, я даже сначала запутался. Но потом, посмотрев различные примеры других блогеров, я немного в своей голове всю путаницу устранил.
Для достижения основной цели нашего занятия — во что бы то ни стало узнать мировое время, пришлось прибегнуть к счётчику таймеру, к количеству попыток отправки запроса серверу, так как сервер почему-то отвечает не на все запросы. Причём серверов этих очень много, я выбрал тот сервер, который выбран по умолчанию в моей операционной системе Windows в качетстве основного сервера для коррекции точного времени. Как узнать его адрес IP (а нужен именно он, так как протокол DNS, который определяет IP по доменному имени, нам пока неизвестен), я покажу позже в процессе написания кода, который мы сейчас уже начнём и писать.
Проект создан был с именем ENC28J60_NTP, файлы в нём использованы были из проекта прошлого урока ENC28J60_REMOTE. Процесс добавления файлов я давать уже не буду, думаю, все давно это уже поняли и запомнили.
Для работы с протоколом NTP я решил создать отдельную библиотеку в виде пары файлов ntp.c и ntp.h со следующим содержимым
ntp.c:
#include "ntp.h"
//--------------------------------------------------
ntp.h:
#ifndef NTP_H_
#define NTP_H_
//--------------------------------------------------
#include "enc28j60.h"
#include "net.h"
#include "usart.h"
//--------------------------------------------------
//--------------------------------------------------
#endif /* NTP_H_ */
В сегодняшнем уроке мы будем посылать запросы NTP также, как и многие запросы из командной строки терминальной программы, указывая IP адрес и порт сервера точного времени. Порт можно было бы не включать в командную строку, так как зарезервированный порт для передачи по протоколу NTP — 123, но всё же лучше указать, мало ли найдём какие-то серверы с другим номером порта, чем чёрт не шутит, да и не тяжело ввести комбинацию цифр 1 2 3. Ну и, как многие из вас, я надеюсь, догадались,. заканчиваться строка будет у нас на букву n.
Поэтому перейдём в файл usart.c в обработчик приёма байт и добавим соответствующее условие в тело данного обработчика
net_cmd();
}
else if (b=='n')
{
usartprop.is_ip=6;//статус попытки отправить NTP-пакет
net_cmd();
}
Далее перейдём в файл net.c и также в функции net_cmd добавим соответствующие условия для попытки и самой отправки пакета
usartprop.is_ip=0;
}
else if(usartprop.is_ip==6)//статус попытки отправить NTP-пакет
{
ip=ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);
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(uint32_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);
usartprop.is_ip=0;
}
Вернёмся в файл ntp.h и сначала добавим структуру для разделения целой и дробной части секунд
#include "usart.h"
//--------------------------------------------------
typedef struct ntp_ts {
uint32_t sec;//целая часть
uint32_t frac;//дробная часть
} ntp_ts_ptr;
//--------------------------------------------------
Добавим также структуру для заголовка NTP
} ntp_ts_ptr;
//--------------------------------------------------
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 "usart.h"
//--------------------------------------------------
#define LOCAL_PORT_FOR_NTP 14444
//--------------------------------------------------
Перейдём в файл ntp.c и подключим глобальный массив для нашего буфера, а также массивов строки и MAC-адреса
#include "ntp.h"
//--------------------------------------------------
extern char str1[60];
extern uint8_t net_buf[ENC28J60_MAXFRAME];
extern uint8_t macaddr[6];
//--------------------------------------------------
Продолжим писать код нашей функции отправки пакета NTP.
Создадим переменную для длины и создадим указатели для заголовков всех уровней нашего пакета
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;
ip_pkt->ipaddr_dst = ip_addr;
ip_pkt->ipaddr_src = IP_ADDR;
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 "usart.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,"%lurn", be32todword(ntp_pkt->tx_ts.sec));
USART_TX((uint8_t*)str1,strlen(str1));
Теперь давайте узнаем сетевой адрес сервера NTP. Для этого запустим WireShark и отфильтруем анализ пакетов по NTP
Кликнув по времени в трее, мы запустим изменение времени
В открывшемся диалоге перейдём на закладку «Время по интернету», а там запустим «Изменить параметры»
Оставим сервер по умолчанию и нажмём «Обновить сейчас», дождавшись затем синхронизации времени
Теперь мы можем все диалоги закрыть и перейти в Wireshark и из пакета забрать IP-адрес сервера NTP
Чтобы не писать вручную адрес, можно забрать его с помощью пунктов контекстного меню — Copy->Value.
Также можно найти список серверов точного времени в интернете и, мало того, ещё и посмотреть их загруженность на данный момент. Так что спосбобы узнавания адресов данных серверов различны, а какой удобен для вас — вам выбирать.
Наконец-то мы можем уже собрать наш код, прошить наш контроллер и посмотреть результат нашей работы в терминальной программе
Как мы видим, пакет нам с сервера пришёл, но только с 3 попытки. Но это ничего, нам не сложно сделать 3 попытки, хотелось бы, конечно, чтобы попытки происходили автоматически, без нашего участия, но об этом чуть позже.
Пока же перед нами стоит задача — время в секундах преобразовать в удобочитаемый формат.
В следующей части нашего занятия мы увидим время с сервера NTP в удобном формате, а также автоматизируем процесс повторного запроса времени.
Предыдущий урок Программирование МК AVR Следующая часть
Приобрести плату Arduino UNO R3 можно здесь.
Приобрести программатор USBASP USBISP с адаптером можно здесь USBASP USBISP 3.3 с адаптером
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добрый день! Почему-то не работает, сервер не отзывается. Пробовал другие серверы, те пингуются, но время всё равно не отдают…
Пробовал Ваш код, так же тишина…
Что-то не так настроили. Должно всё работать. Смотрите маску сети, шлюз и т.д.
Дык, прошлый проект работает, сайты отзываются… Не могу понять, почему здесь не работает…
У меня работало. Что ж, проверю на досуге ещё раз. Может, изменились адреса серверов. Вы сервер проверяли?
Спасибо, что отвечаете! Да, адрес сервера правильный, так же пробовал с серверами stratum… Игрался с портами, ничего не помогает…
Тогда, к сожалению, у меня нет ответа на данный вопрос.
Что-то с глобальным выходом в интернет из микросхемы. Возможно роутер, имея функцию файервола не пускает во внешнюю сеть. Надо проверять PING (работу по протоколу ICMP) именно из проекта, то есть из микросхемы.