Синхронизация времени с NTP сервером через GSM модем

Суть получения времени с NTP сервера сводится к посылки на него пустого(любого) UDP пакета. В ответ удаленный сервер вернет количество секунд, начиная с 1 января 1900года. Реализация отправки пакета на GSM модуле SIM сводится к нескольким этапам. Вначале необходимо зарегистрироваться в GPRS сети АТ командой, далее открыть соединение с удаленным сервером, затем послать сообщение, принять данные о времени и закрыть соединение. Полученные данные(четыре байта) необходимо преобразовать в формат хотя бы ЧЧ/ММ/СС, хотелось бы и дни и месяцы с годами определять, но мне достаточно знать часы, минуты и секунды. Время полученное с сервера пишем прямо в часы реального времени, которые есть в том же GSM модуле.

АТ команды работы GPRS в порядке их работы:

1)AT+CGATT=1 - команда регистрации в сети GPRS. Вернет ОК в случае успешной регистрации.
Реализующий её макрос:

  1. #define CGATT for(uint8_t n=0; n<sizeof(gprs_reg); n++) usart0_write(gprs_reg[n]);

Константы необходимые макросу:

  1. 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, каждой записи отведены фиксированные определенные длина, а текущая длина пользовательской записи вычисляется символом пробела по окончанию.

  1. // Читаем APN //
  2. uint8_t len=0; //Переменная показующая длину записи.
  3. for(uint8_t i=0; i<(ADDR_EEP_USER_NAME - ADDR_EEP_APN); i++)
  4. {
  5. //Если символ пробел, значит запись закончилась.
  6. //Выходим из цикла, при этом len больше не увеличивается.
  7. if(eeprom_read_byte(i+ADDR_EEP_APN)==' ') break;
  8. len++;
  9. }
  10.  
  11. char temp_apn[len];
  12. //Создаём переменную строку, где будет имя нашей точки доступа( у нас это internet.beeline.ru)
  13. for(uint8_t i=0; i<len; i++)
  14. { temp_apn[i]=(eeprom_read_byte(i+ADDR_EEP_APN)); }

В переменной len находится длина записи, а в переменной temp_apn сама запись АPN.

Аналогично вычисляются остальные настройки.

  1. // Читаем имя пользователя //
  2. len=0;
  3. for(uint8_t i=0; i<(ADDR_EEP_USER_PAS-ADDR_EEP_USER_NAME); i++)
  4. { if(eeprom_read_byte(i+ADDR_EEP_USER_NAME)==' ') break; len++; }
  5.  
  6. char temp_user_name[len];
  7. for(uint8_t i=0; i<len; i++)
  8. { temp_user_name[i]=(eeprom_read_byte(i+ADDR_EEP_USER_NAME)); }
  9. // Читаем пароль пользователя //
  10. len=0;
  11. for(uint8_t i=0; i<(ADDR_EEP_USER_PAS_END-ADDR_EEP_USER_PAS); i++)
  12. { if(eeprom_read_byte(i+ADDR_EEP_USER_PAS)==' ') break; len++; }
  13. char temp_user_pasvord[len];
  14. for(uint8_t i=0; i<len; i++)
  15. { temp_user_pasvord[i]=(eeprom_read_byte(i+ADDR_EEP_USER_PAS)); }
  16.  

3) Далее необходимо указать в каком виде будет вводится имя сервера:
AT+CDNSORIP=1-доменное имя сервера.
AT+CDNSORIP=0- имя сервера в виде IP адреса.
Будем использовать AT+CDNSORIP=1 так как IP адреса могут меняться.

Макрос:

  1. #define CDNSORIP
  2. for(uint8_t n=0; n<sizeof(dns_or_ip); n++)
  3. usart0_write(dns_or_ip[n]);

Константа для макроса:

  1. 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.

Команду открытия соединения реализуют два макроса:

  1. #define CIPSTART for(uint8_t n=0; n<sizeof(gprs_start); n++) usart0_write(gprs_start[n]);
  2. //и константа
  3. const char gprs_start[]={'A','T','+','C','I','P','S','T','A','R','T','=','"','U','D','P','"',',','"'};
  4.  

  1. #define CIPSTART_END for(uint8_t n=0; n<sizeof(gprs_start_end); n++)
  2. usart0_write(gprs_start_end[n]);
  3. //и константа
  4. const char gprs_start_end[]={'"',',','"','3','7','"', 0x0D, 0x0A};

В первом макросе Мы указуем что хотим использовать UDP протокол, а во втором, порт 37.
А между этими макросами Мы должны указать адрес NTP сервера(в виде доменного имени, а не IP адреса).

Длина строки с НТП именем сервера также хранится в EEPROM и вычисляется аналогично настройкам GPRS.

  1. // Читаем доменное имя сервера //
  2. len=0;
  3. if(domain_name==1)
  4. { for(uint8_t i=0; i<(ADDR_EEP_DOMAIN_NAME_END1-ADDR_EEP_DOMAIN_NAME1); i++)
  5.  
  6. if(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME1)==' ') break; len++; } }
  7.  
  8. if(domain_name==2)
  9. { for(uint8_t i=0; i<(ADDR_EEP_DOMAIN_NAME_END2-ADDR_EEP_DOMAIN_NAME2); i++)
  10. { if(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME2)==' ') break; len++; } }
  11. char temp_domain_name[len];
  12.  
  13. if(domain_name==1)
  14. { for(uint8_t i=0; i<len; i++)
  15. { temp_domain_name[i]=(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME1)); } }
  16. if(domain_name==2)
  17. { for(uint8_t i=0; i<len; i++)
  18. { temp_domain_name[i]=(eeprom_read_byte(i+ADDR_EEP_DOMAIN_NAME2)); } }

Следует сказать, что у Нас два НТП сервера и в случае не удачи с первым можно попробовать со вторым.

После всех настроек можно попробовать коннект.

5) Прежде всего регистрируемся в сети GPRS командой AT+CGATT.

  1. CGATT; //Макрос передачи АТ команды(AT+CGAAT, 0x0D, 0x0A).
  2. while(ok());
  3.  
  4.  
  5. CIPCSGP; //Макрос передачи AT команды(AT+CIPSGP=1,”).
  6.  
  7. //Передаём АПН
  8. for(uint8_t n=0; n<sizeof(temp_apn); n++) usart0_write(temp_apn[n]);
  9. usart0_write('"'); //Это разделения между записями, смотрите формат команды.
  10. usart0_write(',');
  11. usart0_write('"');
  12.  
  13. //Передаём имя пользователя.
  14. for(uint8_t n=0; n<sizeof(temp_user_name); n++) usart0_write(temp_user_name[n]);
  15. usart0_write('"');
  16. usart0_write(',');
  17. usart0_write('"');
  18.  
  19. //Передаём пароль пользователя.
  20. for(uint8_t n=0; n<sizeof(temp_user_pasvord); n++) usart0_write(temp_user_pasvord[n]);
  21. usart0_write('"');
  22. usart0_write(0x0D); //Это последняя настройка поэтому завершаем её вводом команды.
  23. usart0_write(0x0A);
  24.  
  25. while(ok()); //Ждем ОК.
  26.  
  27. CDNSORIP; //Мокрос АТ команды AT+CDNSORIP=1,0x0D, 0x0A.
  28. while(ok());
  29.  
  30. // Попробуем открыть соединение //
  31. CIPSTART; //Макрос открывающий соединение.
  32. for(uint8_t n=0; n<sizeof(temp_domain_name); n++) usart0_write(temp_domain_name[n]);
  33. CIPSTART_END;
  34. //Макрос завершающий AT команду.

В последних трех строках Мы пробуем открыть соединение с NTP сервером по протоколу UDP на 37 порт.

6)Теперь если соединение удастся Нам вернется «0», это ОК (так как мы отключили текстовый информационный ответ от модуля командой ATV=0).
Проверяем ответ от модуля.

  1. while(1)
  2. {
  3. while(!(usart0_rx_len()));
  4. uint8_t status=usart0_read();
  5.  
  6. if(status=='9') return status;
  7. if(status=='3') return status;
  8. if(status=='4') return status;
  9. if(status=='0') break;
  10.  
  11. }

Далее если сервер готов к обмену данными он возвращает «8», это CONNECT OK

Если соединение не получится вернется:
4 – ERROR обычно этот ответ говорит о неверном адресе, порте и протоколе.
3 или 9 возвращается, если сервер занят.

Проверяем ответ от сервера.

  1. while(1)
  2. {
  3. while(!(usart0_rx_len()));
  4. uint8_t status=usart0_read();
  5.  
  6. if(status=='9') return status;
  7. if(status=='3') return status;
  8. if(status=='4') return status;
  9. if(status=='8') break;
  10.  
  11. }

Если все нормально попробуем послать UDP пакет.
7) Для передачи сообщений удаленному серверу существует команда

AT+CIPSEND

Реализуем её макросом

  1. #define CIPSEND for(uint8_t n=0; n<sizeof(send); n++) usart0_write(send[n]);
  2. и константа для макроса
  3. const char send[]={'A','T','+','C','I','P','S','E','N','D', 0x0D, 0x0A};

После ввода команды нужно дождаться приглашения для ввода сообщения.
Это знак > .

  1. CIPSEND;
  2.  
  3. //Ждем символ >.
  4. while(1)
  5. {
  6. while(!(usart0_rx_len()));
  7. uint8_t status=usart0_read();
  8. if(status=='>') break;
  9. }

Чтобы получит овеет от NTP сервера необходимо отправить пустой UDP пакет.

usart0_write(0x1A); //Отправляем пустое сообщение.

В ответ от удаленного сервера могут вернуться следующие сообщения:

SEND OK -сообщение отправлено(но может не доставлено UDP же);
ERROR – соединение не установлено или отключено;
SEND FAIL – передача сообщения не прошла.

Причем эти сообщения будут в текстовом виде, в независимости от формата ответа модема (ATV=1 или ATV=0).

Проверяем ответ от сервера.

  1. char status;
  2. while(1)
  3. {
  4. while(!(usart0_rx_len()));
  5. status=usart0_read();
  6. if(status=='K') break;
  7. if(status=='F') return status;
  8. if(status=='R') return status;
  9. }

Проверка ответа реализована проверкой символа не встречающихся в этих сообщениях
SEND OK
SEND FAIL
ERROR

После получение подтверждения об отправке сообщения необходимо дождаться ответа от NTP сервера. Ответ прейдет четырьмя байтами, это количество секунд, начиная от 1 января 1900 года. Может случится что ответ будет приходит в течении секунды, может в течении часа, а может и вообще не прейти, так как используется UDP протокол, который не гарантирует доставку сообщения.

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

8) Получаем время от сервера.

  1. usart0_clear_rx_buffer();
  2.  
  3. uint8_t temp_time_ntp[4]; //Переменная где будет хранится время в символьном виде.
  4.  
  5. _delay_ms(1000); //Мы ждем ответ 3 секунды.
  6. _delay_ms(1000); //Кто хочет больше или меньше,
  7. _delay_ms(1000); //просто добавь _delay_ms или удали.
  8.  
  9. if(!(usart0_rx_len())) //Если нечего не пришло Мы закрываем соединение.
  10. {
  11. CIPSHUT; //Закрываем соединение по таймауту.
  12. _delay_ms(1000);
  13. return 0xFF;
  14.  
  15. }
  16.  
  17. //Если что то пришло тогда читаем время в переменную temp_time_ntp.
  18. for(uint8_t i=0; i<sizeof(temp_time_ntp); i++) temp_time_ntp[i]=usart0_read();
  19.  
  20.  
  21. CIPSHUT; //Закрываем соединение. Ответ положительный.
  22. _delay_ms(1000);

Теперь как ответ получен в виде четырех байт, его необходимо расшифровать. Я не заморачивался с вычислением года, месяца и дня. Достаточно занести эти настройки в RTC и периодически обновлять час, минуту и секунду RTC.

9) Для преобразования времени к формату ЧЧ/ММ/СС нужно целочисленно разделить полученный код на 86400. Это количество секунд в сутках, далее целочислено умножить этот ответ на 86400 и вычисть из полученного кода полученный ответ. В результате проделанной операции мы получим остаток секунд от полуночи, а далее вычисляем минуты и часы.

  1. uint32_t time_ntp=0;
  2.  
  3. for(uint8_t i=0; i<sizeof(time_ntp); i++)
  4. {
  5. time_ntp|=temp_time_ntp[i];
  6. if(i!=3) time_ntp=time_ntp<<8;
  7. }
  8.  
  9. uint32_t time_day_ntp=time_ntp/86400;
  10.  
  11. uint32_t temp_sec_ntp=time_ntp-(time_day_ntp*86400);
  12.  
  13. uint16_t temp_min_ntp=temp_sec_ntp/60;
  14.  
  15.  
  16. isec=temp_sec_ntp-(temp_min_ntp*60);
  17.  
  18. ihours_ntp=temp_min_ntp/60;
  19.  
  20. imin=temp_min_ntp-(ihours_ntp*60);
  21.  
  22. ihours=ihours_ntp+eeprom_read_byte(&time_zone);
  23.  
  24. if(ihours>23) ihours=ihours-24;

В переменных isec, ihours и imin находится время прямом формате.
Для того чтобы занести время в RTC модуля необходимо преобразовать его в BCD формат. Для чего я написал функцию делающее это преобразование BCDFormat(uint8_t hex) и обратное преобразование HEXFormat(uint8_t bcd). Я также прилагаю файл BCD.h.

  1. uint8_t temp_csec=BCDFormat(isec);
  2. uint8_t temp_cmin=BCDFormat(imin);
  3. uint8_t temp_chours=BCDFormat(ihours);
  4.  
  5. csec[0]=((temp_csec & 0xF0)>>4)+0x30;
  6. csec[1]=(temp_csec & 0x0F)+0x30;
  7.  
  8. cmin[0]=((temp_cmin & 0xF0)>>4)+0x30;
  9. cmin[1]=(temp_cmin & 0x0F)+0x30;
  10.  
  11. chours[0]=((temp_chours & 0xF0)>>4)+0x30;
  12. chours[1]=(temp_chours & 0x0F)+0x30;
  13. return 0;

В конце программы встречается макрос закрывающий соединение.
CIPSHUT
Вот его описание

  1. #define CIPSHUT for(uint8_t n=0; n<sizeof(shut); n++) usart0_write(shut[n]); и константа
  2. const char shut[]={'A','T','+','C','I','P','S','H','U','T', 0x0D, 0x0A};

Макрос завершает соединение и после его выполнения придет ответ
”0” - SHUT OK - соединение закрыто успешно
или
“4” – ERROR – соединение и не открывалось.

10) Вот и все. Теперь можно заносить полученное время в RTC модуля.