Синхронизация времени с NTP сервером через GSM модем
Суть получения времени с NTP сервера сводится к посылки на него пустого(любого) UDP пакета. В ответ удаленный сервер вернет количество секунд, начиная с 1 января 1900года. Реализация отправки пакета на GSM модуле SIM сводится к нескольким этапам. Вначале необходимо зарегистрироваться в GPRS сети АТ командой, далее открыть соединение с удаленным сервером, затем послать сообщение, принять данные о времени и закрыть соединение. Полученные данные(четыре байта) необходимо преобразовать в формат хотя бы ЧЧ/ММ/СС, хотелось бы и дни и месяцы с годами определять, но мне достаточно знать часы, минуты и секунды. Время полученное с сервера пишем прямо в часы реального времени, которые есть в том же GSM модуле.
АТ команды работы GPRS в порядке их работы:
1)AT+CGATT=1 - команда регистрации в сети GPRS. Вернет ОК в случае успешной регистрации.
Реализующий её макрос:
#define CGATT for(uint8_t n=0; n<sizeof(gprs_reg); n++) usart0_write(gprs_reg[n]);
Константы необходимые макросу:
const char gprs_reg[]={'A','T','+','C','G','A','T','T','=','1', 0x0D, 0x0A};
2)AT+CIPCSGP=1,”APN”,”user name”,”password”- команда настроек GPRS. Здесь 1-режим GPRS, APN – имя точки входа, user name – имя пользователя password –пароль пользователя. Параметры APN, user name и password предоставляются оператором связи, их необходимо узнать у оператора перед работой и занести в EEPROM. У меня АPN – internet.beeline.ru, user name – beeline, password – beeline.
В программе эти все настройки записаны в EEPROM, каждой записи отведены фиксированные определенные длина, а текущая длина пользовательской записи вычисляется символом пробела по окончанию.
// Читаем APN // uint8_t len=0; //Переменная показующая длину записи. for(uint8_t i=0; i<(ADDR_EEP_USER_NAME - ADDR_EEP_APN); i++) { //Если символ пробел, значит запись закончилась. //Выходим из цикла, при этом len больше не увеличивается. if(eeprom_read_byte(i+ADDR_EEP_APN)==' ') break; len++; } char temp_apn[len]; //Создаём переменную строку, где будет имя нашей точки доступа( у нас это internet.beeline.ru) for(uint8_t i=0; i<len; i++) { temp_apn[i]=(eeprom_read_byte(i+ADDR_EEP_APN)); }
В переменной len находится длина записи, а в переменной temp_apn сама запись АPN.
Аналогично вычисляются остальные настройки.
// Читаем имя пользователя // len=0; for(uint8_t i=0; i<(ADDR_EEP_USER_PAS-ADDR_EEP_USER_NAME); i++) { if(eeprom_read_byte(i+ADDR_EEP_USER_NAME)==' ') break; len++; } char temp_user_name[len]; for(uint8_t i=0; i<len; i++) { temp_user_name[i]=(eeprom_read_byte(i+ADDR_EEP_USER_NAME)); } // Читаем пароль пользователя // len=0; for(uint8_t i=0; i<(ADDR_EEP_USER_PAS_END-ADDR_EEP_USER_PAS); i++) { if(eeprom_read_byte(i+ADDR_EEP_USER_PAS)==' ') break; len++; } char temp_user_pasvord[len]; for(uint8_t i=0; i<len; i++) { temp_user_pasvord[i]=(eeprom_read_byte(i+ADDR_EEP_USER_PAS)); }
3) Далее необходимо указать в каком виде будет вводится имя сервера:
AT+CDNSORIP=1-доменное имя сервера.
AT+CDNSORIP=0- имя сервера в виде IP адреса.
Будем использовать AT+CDNSORIP=1 так как IP адреса могут меняться.
Макрос:
#define CDNSORIP for(uint8_t n=0; n<sizeof(dns_or_ip); n++) usart0_write(dns_or_ip[n]);
Константа для макроса:
const char dns_or_ip[]={'A','T','+','C','D','N','S','O','R','I','P','=','1',0x0D, 0x0A};
После выполненных настроек наступает главное. Попробуем открыть соединение.
4) Открываем соединение.
AT+CIPSTART=”mode”,”domain name”,”port”
Здесь mode тип нашего протокола TCP либо UDP. NTP сервер поддержует UDP протокол.
domain name – адрес НТП сервера. Вот список известных мне НТП серверов:
ntp1.vniiftri.ru
ntp1.vniiftri.ru
port – порт по которому хотим послать запрос. Здесь я наткнулся на проблему. В описания везде указуют порт 123, но по нему ответ не приходит. Отвечает порт 37. Поэтому не удивляйтесь, что у меня не стандартный порт 123 а порт 37(!). Если Ты сможешь разобраться в этой заморочке, расскажи, я писал в несколько фирм, но некто мне не дал четкого ответа. Вообще NTP протокол разный, есть старые версии и есть более новые, вот по 37 порту идет старая версия протокола, вроде бы по 123 порту другой формат протокола, но при попытке послать пустой UDP пакет на 123 порт, в ответ нечего не приходит((. Поэтому я пользуюсь 37.
Команду открытия соединения реализуют два макроса:
#define CIPSTART for(uint8_t n=0; n<sizeof(gprs_start); n++) usart0_write(gprs_start[n]); //и константа const char gprs_start[]={'A','T','+','C','I','P','S','T','A','R','T','=','"','U','D','P','"',',','"'};
#define CIPSTART_END for(uint8_t n=0; n<sizeof(gprs_start_end); n++) usart0_write(gprs_start_end[n]); //и константа const char gprs_start_end[]={'"',',','"','3','7','"', 0x0D, 0x0A};
В первом макросе Мы указуем что хотим использовать UDP протокол, а во втором, порт 37.
А между этими макросами Мы должны указать адрес NTP сервера(в виде доменного имени, а не IP адреса).
Длина строки с НТП именем сервера также хранится в EEPROM и вычисляется аналогично настройкам GPRS.
// Читаем доменное имя сервера // len=0; if(domain_name==1) { for(uint8_t i=0; i<(ADDR_EEP_DOMAIN_NAME_END1-ADDR_EEP_DOMAIN_NAME1); i++) if(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME1)==' ') break; len++; } } if(domain_name==2) { for(uint8_t i=0; i<(ADDR_EEP_DOMAIN_NAME_END2-ADDR_EEP_DOMAIN_NAME2); i++) { if(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME2)==' ') break; len++; } } char temp_domain_name[len]; if(domain_name==1) { for(uint8_t i=0; i<len; i++) { temp_domain_name[i]=(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME1)); } } if(domain_name==2) { for(uint8_t i=0; i<len; i++) { temp_domain_name[i]=(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME2)); } }
Следует сказать, что у Нас два НТП сервера и в случае не удачи с первым можно попробовать со вторым.
После всех настроек можно попробовать коннект.
5) Прежде всего регистрируемся в сети GPRS командой AT+CGATT.
CGATT; //Макрос передачи АТ команды(AT+CGAAT, 0x0D, 0x0A). while(ok()); CIPCSGP; //Макрос передачи AT команды(AT+CIPSGP=1,”). //Передаём АПН for(uint8_t n=0; n<sizeof(temp_apn); n++) usart0_write(temp_apn[n]); usart0_write('"'); //Это разделения между записями, смотрите формат команды. usart0_write(','); usart0_write('"'); //Передаём имя пользователя. for(uint8_t n=0; n<sizeof(temp_user_name); n++) usart0_write(temp_user_name[n]); usart0_write('"'); usart0_write(','); usart0_write('"'); //Передаём пароль пользователя. for(uint8_t n=0; n<sizeof(temp_user_pasvord); n++) usart0_write(temp_user_pasvord[n]); usart0_write('"'); usart0_write(0x0D); //Это последняя настройка поэтому завершаем её вводом команды. usart0_write(0x0A); while(ok()); //Ждем ОК. CDNSORIP; //Мокрос АТ команды AT+CDNSORIP=1,0x0D, 0x0A. while(ok()); // Попробуем открыть соединение // CIPSTART; //Макрос открывающий соединение. for(uint8_t n=0; n<sizeof(temp_domain_name); n++) usart0_write(temp_domain_name[n]); CIPSTART_END; //Макрос завершающий AT команду.
В последних трех строках Мы пробуем открыть соединение с NTP сервером по протоколу UDP на 37 порт.
6)Теперь если соединение удастся Нам вернется «0», это ОК (так как мы отключили текстовый информационный ответ от модуля командой ATV=0).
Проверяем ответ от модуля.
while(1) { while(!(usart0_rx_len())); uint8_t status=usart0_read(); if(status=='9') return status; if(status=='3') return status; if(status=='4') return status; if(status=='0') break; }
Далее если сервер готов к обмену данными он возвращает «8», это CONNECT OK
Если соединение не получится вернется:
4 – ERROR обычно этот ответ говорит о неверном адресе, порте и протоколе.
3 или 9 возвращается, если сервер занят.
Проверяем ответ от сервера.
while(1) { while(!(usart0_rx_len())); uint8_t status=usart0_read(); if(status=='9') return status; if(status=='3') return status; if(status=='4') return status; if(status=='8') break; }
Если все нормально попробуем послать UDP пакет.
7) Для передачи сообщений удаленному серверу существует команда
AT+CIPSEND
Реализуем её макросом
#define CIPSEND for(uint8_t n=0; n<sizeof(send); n++) usart0_write(send[n]); и константа для макроса const char send[]={'A','T','+','C','I','P','S','E','N','D', 0x0D, 0x0A};
После ввода команды нужно дождаться приглашения для ввода сообщения.
Это знак > .
CIPSEND; //Ждем символ >. while(1) { while(!(usart0_rx_len())); uint8_t status=usart0_read(); if(status=='>') break; }
Чтобы получит овеет от NTP сервера необходимо отправить пустой UDP пакет.
usart0_write(0x1A); //Отправляем пустое сообщение.
В ответ от удаленного сервера могут вернуться следующие сообщения:
SEND OK -сообщение отправлено(но может не доставлено UDP же);
ERROR – соединение не установлено или отключено;
SEND FAIL – передача сообщения не прошла.
Причем эти сообщения будут в текстовом виде, в независимости от формата ответа модема (ATV=1 или ATV=0).
Проверяем ответ от сервера.
char status; while(1) { while(!(usart0_rx_len())); status=usart0_read(); if(status=='K') break; if(status=='F') return status; if(status=='R') return status; }
Проверка ответа реализована проверкой символа не встречающихся в этих сообщениях
SEND OK
SEND FAIL
ERROR
После получение подтверждения об отправке сообщения необходимо дождаться ответа от NTP сервера. Ответ прейдет четырьмя байтами, это количество секунд, начиная от 1 января 1900 года. Может случится что ответ будет приходит в течении секунды, может в течении часа, а может и вообще не прейти, так как используется UDP протокол, который не гарантирует доставку сообщения.
Поэтому в ожидании ответа используем таймаут, по истечению которого выходим из ожидания ответа и закрываем соединение.
8) Получаем время от сервера.
usart0_clear_rx_buffer(); uint8_t temp_time_ntp[4]; //Переменная где будет хранится время в символьном виде. _delay_ms(1000); //Мы ждем ответ 3 секунды. _delay_ms(1000); //Кто хочет больше или меньше, _delay_ms(1000); //просто добавь _delay_ms или удали. if(!(usart0_rx_len())) //Если нечего не пришло Мы закрываем соединение. { CIPSHUT; //Закрываем соединение по таймауту. _delay_ms(1000); return 0xFF; } //Если что то пришло тогда читаем время в переменную temp_time_ntp. for(uint8_t i=0; i<sizeof(temp_time_ntp); i++) temp_time_ntp[i]=usart0_read(); CIPSHUT; //Закрываем соединение. Ответ положительный. _delay_ms(1000);
Теперь как ответ получен в виде четырех байт, его необходимо расшифровать. Я не заморачивался с вычислением года, месяца и дня. Достаточно занести эти настройки в RTC и периодически обновлять час, минуту и секунду RTC.
9) Для преобразования времени к формату ЧЧ/ММ/СС нужно целочисленно разделить полученный код на 86400. Это количество секунд в сутках, далее целочислено умножить этот ответ на 86400 и вычисть из полученного кода полученный ответ. В результате проделанной операции мы получим остаток секунд от полуночи, а далее вычисляем минуты и часы.
uint32_t time_ntp=0; for(uint8_t i=0; i<sizeof(time_ntp); i++) { time_ntp|=temp_time_ntp[i]; if(i!=3) time_ntp=time_ntp<<8; } uint32_t time_day_ntp=time_ntp/86400; uint32_t temp_sec_ntp=time_ntp-(time_day_ntp*86400); uint16_t temp_min_ntp=temp_sec_ntp/60; isec=temp_sec_ntp-(temp_min_ntp*60); ihours_ntp=temp_min_ntp/60; imin=temp_min_ntp-(ihours_ntp*60); ihours=ihours_ntp+eeprom_read_byte(&time_zone); if(ihours>23) ihours=ihours-24;
В переменных isec, ihours и imin находится время прямом формате.
Для того чтобы занести время в RTC модуля необходимо преобразовать его в BCD формат. Для чего я написал функцию делающее это преобразование BCDFormat(uint8_t hex) и обратное преобразование HEXFormat(uint8_t bcd). Я также прилагаю файл BCD.h.
uint8_t temp_csec=BCDFormat(isec); uint8_t temp_cmin=BCDFormat(imin); uint8_t temp_chours=BCDFormat(ihours); csec[0]=((temp_csec & 0xF0)>>4)+0x30; csec[1]=(temp_csec & 0x0F)+0x30; cmin[0]=((temp_cmin & 0xF0)>>4)+0x30; cmin[1]=(temp_cmin & 0x0F)+0x30; chours[0]=((temp_chours & 0xF0)>>4)+0x30; chours[1]=(temp_chours & 0x0F)+0x30; return 0;
В конце программы встречается макрос закрывающий соединение.
CIPSHUT
Вот его описание
#define CIPSHUT for(uint8_t n=0; n<sizeof(shut); n++) usart0_write(shut[n]); и константа const char shut[]={'A','T','+','C','I','P','S','H','U','T', 0x0D, 0x0A};
Макрос завершает соединение и после его выполнения придет ответ
”0” - SHUT OK - соединение закрыто успешно
или
“4” – ERROR – соединение и не открывалось.
10) Вот и все. Теперь можно заносить полученное время в RTC модуля.