sobota, 24 maja 2025

STM32H7 - Klient NTP

W tym poście chciałbym opisać implementację klienta NTP. 


Pakiet NTP składa się z 48 bajtów danych. 

  • 0 - 1 bajt - LI 2 bity, VN - 3 bity, Mode - 3 bity. 
  • 1 - 1 bajt - Stratum - Poziom Serwera 
  • 2 - 1 bajt - Poll Interval - Sugerowany interwał miedzy zapytaniami
  • 3 - 1 bajt - Precision - Precyzja między zapytaniami
  • 4-7 - 4 bajty - Root Delay - Całkowite opóźnienie względem źródła zegara
  • 8-11 - 4 bajty - Root Dispersion - Rozrzut zegara
  • 12-15 - 4 bajty - Reference ID - Identyfikator źródła czasu
  • 16-23 - 8 bajtów - Reference Timestamp - Czas ostatniej aktualizacji zegara serwera
  • 24-31 - 8 bajtów - Originate Timestamp - Czas wysłania żądania przez klienta
  • 32-39 - 8 bajtów - Receive Timestamp - Czas odebrania żądania przez serwer
  • 40-47 - 8 bajtów - Transmit Timestamp - Czas wysłania odpowiedzi przez serwer

Na początku tworzony jest nowy blok sterowania protokołu UDP (UDP PCB) za pomocą funkcji udp_new(). Następnie konfigurowana jest funkcja odbiorcza za pomocą udp_recv(), która zostanie wywołana po otrzymaniu odpowiedzi od serwera NTP. Adres IP serwera jest konwertowany z formatu tekstowego na binarny za pomocą funkcji ipaddr_aton().

Kolejnym krokiem jest przygotowanie pakietu NTP — zgodnie ze specyfikacją, pakiet ten ma 48 bajtów, z pierwszym bajtem ustawionym na wartość 0x1B, co oznacza wersję 3 i tryb klienta. Pakiet jest następnie umieszczany w buforze pbuf, który jest przesyłany do serwera NTP za pomocą funkcji udp_sendto() na porcie 123.

Tutaj wykorzystujemy bezpośrednie wywołanie pakietu przez UDP. Nie ma konieczności uruchamiania dodatkowych elementów w LWIP. 

  1. #define NTP_PORT 123
  2. #define NTP_SERVER_IP "194.146.251.100"
  3. #define NTP_EPOCH_DIFF 2208988800UL     // Between 1900 and 1970 in seconds
  4.  
  5. static struct udp_pcb *ntp_pcb;

  1. void ntp_request(void) {
  2.     if (ntp_pcb == NULL) {
  3.         ntp_pcb = udp_new();
  4.         if (ntp_pcb == NULL) {
  5.             printf("Failed to create UDP PCB\n");
  6.             return;
  7.         }
  8.         udp_recv(ntp_pcb, ntp_udp_recv, NULL);
  9.     }
  10.  
  11.     ip_addr_t ntp_ip;
  12.     ipaddr_aton(NTP_SERVER_IP, &ntp_ip);
  13.  
  14.     uint8_t ntp_packet[48] = {0};
  15.     ntp_packet[0] = 0x1B;
  16.  
  17.     struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, sizeof(ntp_packet), PBUF_RAM);
  18.  
  19.     if (p != NULL) {
  20.         memcpy(p->payload, ntp_packet, sizeof(ntp_packet));
  21.         udp_sendto(ntp_pcb, p, &ntp_ip, NTP_PORT);
  22.         pbuf_free(p);
  23.     }
  24. }

Przetwarzanie odebranych danych odbywa się w funkcji ntp_udp_recv(), która obsługuje dane otrzymane z serwera NTP. Na początku sprawdzana jest długość pakietu — czy wynosi co najmniej 48 bajtów, co jest standardowym rozmiarem pakietu NTP.

Następnie funkcja odczytuje 4 bajty z pozycji 40–43, które zawierają tzw. Transmit Timestamp — czyli znacznik czasu przesłany przez serwer NTP. Wartość ta wyrażona jest jako liczba sekund od 1 stycznia 1900 roku (epoka NTP).

Ponieważ typ time_t w systemie Unix liczy sekundy od 1 stycznia 1970 roku, konieczne jest odjęcie stałej NTP_EPOCH_DIFF, aby uzyskać zgodność między formatami czasu.

Po przekształceniu danych funkcja przekazuje wynik do print_polish_local_time(), która przelicza i wyświetla zarówno czas UTC, jak i lokalny czas w Polsce, uwzględniając obowiązywanie czasu letniego.

  1. int is_summer_time_pl(struct tm *utc) {
  2.     int year = utc->tm_year + 1900;
  3.  
  4.     // Ostatnia niedziela marca
  5.     struct tm last_sunday_march = {
  6.         .tm_year = year - 1900,
  7.         .tm_mon = 2,  // Marzec (0-based)
  8.         .tm_mday = 31,
  9.         .tm_hour = 2,
  10.         .tm_min = 0,
  11.         .tm_sec = 0,
  12.         .tm_isdst = 0
  13.     };
  14.     mktime(&last_sunday_march);
  15.     last_sunday_march.tm_mday -= last_sunday_march.tm_wday;
  16.     mktime(&last_sunday_march);
  17.  
  18.     // Ostatnia niedziela października
  19.     struct tm last_sunday_oct = {
  20.         .tm_year = year - 1900,
  21.         .tm_mon = 9,  // Październik
  22.         .tm_mday = 31,
  23.         .tm_hour = 3,
  24.         .tm_min = 0,
  25.         .tm_sec = 0,
  26.         .tm_isdst = 0
  27.     };
  28.     mktime(&last_sunday_oct);
  29.     last_sunday_oct.tm_mday -= last_sunday_oct.tm_wday;
  30.     mktime(&last_sunday_oct);
  31.  
  32.     time_t t = mktime(utc);
  33.     time_t t_start = mktime(&last_sunday_march);
  34.     time_t t_end   = mktime(&last_sunday_oct);
  35.  
  36.     return (t >= t_start && t < t_end);
  37. }
  38.  
  39. void print_pl_local_time(time_t timestamp) {
  40.     struct tm *utc = gmtime(&timestamp);
  41.     int offset_hours = is_summer_time_pl(utc) ? 2 : 1;
  42.     time_t local_ts = timestamp + offset_hours * 3600;
  43.     struct tm *local_tm = gmtime(&local_ts);
  44.  
  45.     printf("UTC time   : %s", asctime(utc));
  46.     printf("Polish time: %s", asctime(local_tm));
  47. }
  48.  
  49. void ntp_udp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p,
  50.                   const ip_addr_t *addr, u16_t port) {
  51.  
  52.     if (p->len == 48) {
  53.         uint8_t *data = (uint8_t *)p->payload;
  54.         uint32_t time_seconds = (data[40] << 24) | (data[41] << 16) |
  55.                                 (data[42] << 8) | data[43];
  56.         time_seconds -= NTP_EPOCH_DIFF;
  57.         time_t timestamp = (time_t)time_seconds;
  58.  
  59.         print_pl_local_time(timestamp);
  60.  
  61.     }
  62.     pbuf_free(p);
  63. }
  64.  

W celu wywołania funkcji wystarczy uruchomić funkcję ntp_request()

Wywołanie funkcji należy umieścić po uruchomieniu biblioteki LWIP. Jak ustawione DHCP to po ustawienia adresu.