全部例程

SNTP

W5500 其他标签

2025/02/17 更新

本篇文章我们将详细介绍如何在W5500芯片上面实现SNTP授时功能,并通过实战例程,为大家讲解如何让W5500从SNTP服务器获取准确的实际时间。

该例程用到的其他网络协议,例如DHCPDNS, 请参考相关章节。有关 W5500 的初始化过程,请参考Network install,这里将不再赘述。

SNTP协议简介

SNTP(Simple Network Time Protocol)是一种基于UDP协议的网络时间协议,主要用于在计算机网络中同步设备的时间。SNTP旨在提供简单的时间校准服务,比较NTP(Network Time Protocol)而言,SNTP功能更为简单,精度相对较低。

SNTP与NTP的区别

SNTP与NTP的区别如下表所示:

Blog Image

SNTP协议特点

  • 使用UDP通信:SNTP使用UDP协议在端口123进行通信。
  • 支持请求-响应模式:SNTP使用单次请求-响应模式完成时间同步:客户端向时间服务器发送请求,服务器返回当前时间戳。
  • 实现和部署成本低:SNTP的实现非常简单,通常只需要少量的代码,便于在嵌入式系统中集成。由于其资源占用少,适合大规模部署。
  • 与NTP兼容:SNTP 是 NTP 的子集,客户端可以与NTP服务器通信以获取时间。NTP服务器可以无缝提供时间同步服务,无需额外配置。
  • 支持单向时间同步:在特定场景中(如设备只需同步本地时间,而无需计算网络延迟),SNTP 可以仅基于服务器提供的时间戳完成时间同步。

SNTP应用场景

SNTP协议通常用于需要时间同步的场景。通过将时间同步到RTC,能够实现以下功能:

  • 协同工作:定期同步时间,使各模块能够按照预定的时间顺序执行任务。
  • 日志与事件管理:确保W5500的日志时间和事件记录准确,从而便于后续分析与故障排查。
  • 定时任务:通过时间同步,支持定时任务的准确执行。

时区介绍

通过 SNTP 获取世界标准时间 (UTC) 后,需要根据所在时区进行加减运算以计算当地时间。例如,中国位于 UTC+8 时区,在示例代码中定义为 39。因此,获取 UTC 后需加 8 小时才能转换为中国时间。


UTC-12:00 Baker Island, Howland Island (both uninhabited)
UTC-11:00 American Samoa, Samoa
UTC-10:00 (Summer)French Polynesia (most), United States (Aleutian Islands, Hawaii)
UTC-09:30 Marquesas Islands
UTC-09:00 Gambier Islands;(Summer)United States (most of Alaska)
UTC-08:00 (Summer)Canada (most of British Columbia), Mexico (Baja California)
UTC-08:00 United States (California, most of Nevada, most of Oregon, Washington (state))
UTC-07:00 Mexico (Sonora), United States (Arizona); (Summer)Canada (Alberta)
UTC-07:00 Mexico (Chihuahua), United States (Colorado)
UTC-06:00 Costa Rica, El Salvador, Ecuador (Galapagos Islands), Guatemala, Honduras
UTC-06:00 Mexico (most), Nicaragua;(Summer)Canada (Manitoba, Saskatchewan), United States (Illinois, most of Texas)
UTC-05:00 Colombia, Cuba, Ecuador (continental), Haiti, Jamaica, Panama, Peru
UTC-05:00 (Summer)Canada (most of Ontario, most of Quebec)
UTC-05:00 United States (most of Florida, Georgia, Massachusetts, most of Michigan, New York, North Carolina, Ohio, Washington D.C.)
UTC-04:30 Venezuela
UTC-04:00 Bolivia, Brazil (Amazonas), Chile (continental), Dominican Republic, Canada (Nova Scotia), Paraguay,
UTC-04:00 Puerto Rico, Trinidad and Tobago
UTC-03:30 Canada (Newfoundland)
UTC-03:00 Argentina; (Summer) Brazil (Brasilia, Rio de Janeiro, Sao Paulo), most of Greenland, Uruguay
UTC-02:00 Brazil (Fernando de Noronha), South Georgia and the South Sandwich Islands
UTC-01:00 Portugal (Azores), Cape Verde
UTC±00:00 Cote d'Ivoire, Faroe Islands, Ghana, Iceland, Senegal; (Summer) Ireland, Portugal (continental and Madeira)
UTC±00:00 Spain (Canary Islands), Morocco, United Kingdom
UTC+01:00 Angola, Cameroon, Nigeria, Tunisia; (Summer)Albania, Algeria, Austria, Belgium, Bosnia and Herzegovina,
UTC+01:00 Spain (continental), Croatia, Czech Republic, Denmark, Germany, Hungary, Italy, Kinshasa, Kosovo,
UTC+01:00 Macedonia, France (metropolitan), the Netherlands, Norway, Poland, Serbia, Slovakia, Slovenia, 	Sweden, Switzerland
UTC+02:00 Libya, Egypt, Malawi, Mozambique, South Africa, Zambia, Zimbabwe, (Summer)Bulgaria, Cyprus, Estonia,
UTC+02:00 Finland, Greece, Israel, Jordan, Latvia, Lebanon, Lithuania, Moldova, Palestine, Romania, Syria, 	Turkey, Ukraine
UTC+03:00 Belarus, Djibouti, Eritrea, Ethiopia, Iraq, Kenya, Madagascar, Russia (Kaliningrad Oblast), Saudi 	Arabia,
UTC+03:00 South Sudan, Sudan, Somalia, South Sudan, Tanzania, Uganda, Yemen
UTC+03:30 (Summer)Iran
UTC+04:00 Armenia, Azerbaijan, Georgia, Mauritius, Oman, Russia (European), Seychelles, United Arab Emirates
UTC+04:30 Afghanistan
UTC+05:00 Kazakhstan (West), Maldives, Pakistan, Uzbekistan
UTC+05:30 India, Sri Lanka
UTC+05:45 Nepal
UTC+06:00 Kazakhstan (most), Bangladesh, Russia (Ural: Sverdlovsk Oblast, Chelyabinsk Oblast)
UTC+06:30 Cocos Islands, Myanmar
UTC+07:00 Jakarta, Russia (Novosibirsk Oblast), Thailand, Vietnam
UTC+08:00 China, Hong Kong, Russia (Krasnoyarsk Krai), Malaysia, Philippines, Singapore, Taiwan, most of 	Mongolia, Western Australia
UTC+09:00 Korea, East Timor, Russia (Irkutsk Oblast), Japan
UTC+09:30 Australia (Northern Territory);(Summer)Australia (South Australia))
UTC+10:00 Russia (Zabaykalsky Krai); (Summer)Australia (New South Wales, Queensland, Tasmania, Victoria)
UTC+10:30 Lord Howe Island
UTC+11:00 New Caledonia, Russia (Primorsky Krai), Solomon Islands
UTC+11:30 Norfolk Island
UTC+12:00 Fiji, Russia (Kamchatka Krai);(Summer)New Zealand
UTC+12:45 (Summer)New Zealand
UTC+13:00 Tonga
UTC+14:00 Kiribati (Line Islands)

通过SNTP协议同步时间的基本流程

1. 客户端发送时间请求

  • 客户端向 SNTP 服务器发送一个请求数据包,并记录T1时间戳信息。
  • 请求数据包中通常包含客户端的时间戳(请求发送的时间),以便在计算延迟时使用。

2. 服务器接收请求并处理

  • SNTP 服务器接收到请求后,记录下接收请求的时间T2。
  • 服务器生成一个响应数据包,其中包括以下时间戳信息:
    T2:服务器接收到请求的时间。
    T3:服务器发送响应的时间。

3. 客户端接收响应并计算时间

  • 客户端从服务器返回的数据包中提取时间戳信息(T2、T3),并记录接收时间T4。
  • 客户端根据这些时间戳计算本地时间与服务器时间的差异,以及网络延迟:
    网络延迟公式: 网络延迟=(T4−T1)−(T3−T2)
    本地时间校准公式: 校准时间=T3+(T4−T1)−(T3−T2)/2

4. 调整本地时间

  • 客户端根据校准时间调整本地时钟,以同步到服务器的时间。

SNTP协议的报文解析

SNTP的发送和接收报文为固定结构,通常为48字节,报文结构如下:

Blog Image

注意:

时间戳的定义是指从1900年1月1日00:00:00 UTC开始的秒数

SNTP发送请求实例:

Blog Image

SNTP服务器响应实例:

Blog Image

实现过程

接下来,我们在W5500上实现SNTP授时功能。

步骤1:通过DNS解析SNTP服务器的IP地址


if (do_dns(ethernet_buf, sntp_server_name, sntp_server_ip))
{
    printf("DNS request failed.\r\n");
    while (1)
    {
    }
}

步骤2:SNTP初始化


SNTP_init(SOCKET_ID, sntp_server_ip, timezone, ethernet_buf);

SNTP_init()函数需要传入四个参数,分别是使用的socket通道号,sntp服务器地址,时区,socket缓存,在这个函数中,我们会进行SNTP报文组包操作,并将传入的socket通道号,时区,socket缓存注册到库中,具体内容如下:


void SNTP_init(uint8_t s, uint8_t *ntp_server, uint8_t tz, uint8_t *buf)
{
  NTP_SOCKET = s;
  
  NTPformat.dstaddr[0] = ntp_server[0];
  NTPformat.dstaddr[1] = ntp_server[1];
  NTPformat.dstaddr[2] = ntp_server[2];
  NTPformat.dstaddr[3] = ntp_server[3];
  
  time_zone = tz;
  
  data_buf = buf;
  
  uint8_t Flag;
  NTPformat.leap = 0;           /* leap indicator */
  NTPformat.version = 4;        /* version number */
  NTPformat.mode = 3;           /* mode */
  NTPformat.stratum = 0;        /* stratum */
  NTPformat.poll = 0;           /* poll interval */
  NTPformat.precision = 0;      /* precision */
  NTPformat.rootdelay = 0;      /* root delay */
  NTPformat.rootdisp = 0;       /* root dispersion */
  NTPformat.refid = 0;          /* reference ID */
  NTPformat.reftime = 0;        /* reference time */
  NTPformat.org = 0;            /* origin timestamp */
  NTPformat.rec = 0;            /* receive timestamp */
  NTPformat.xmt = 1;            /* transmit timestamp */
  
  Flag = (NTPformat.leap<<6)+(NTPformat.version<<3)+NTPformat.mode; //one byte Flag
  memcpy(ntpmessage,(void const*)(&Flag),1);
}

步骤3:发送SNTP请求报文获取时间


while (1)
{
    if (SNTP_run(&date))
    {
        printf("Beijing time now: %04d-%02d-%02d  %02d:%02d:%02d\r\n", date.yy, date.mo, date.dd, date.hh, date.mm, date.ss);
        break;
    }
}

接着,我们需要运行SNTP_run()函数来执行发送报文以及解析报文的操作,当成功获取到时间后,我们打印出当前时间。

SNTP_run()函数需要传入一个时间结构体date,它的定义如下:


typedef struct _datetime
{
  uint16_t yy;
  uint8_t mo;
  uint8_t dd;
  uint8_t hh;
  uint8_t mm;
  uint8_t ss;
} datetime;

SNTP_run()函数定义如下:


int8_t SNTP_run(datetime *time)
{
uint16_t RSR_len;
uint32_t destip = 0;
uint16_t destport;
uint16_t startindex = 40; //last 8-byte of data_buf[size is 48 byte] is xmt, so the startindex should be 40

switch(getSn_SR(NTP_SOCKET))
{
case SOCK_UDP:
  if ((RSR_len = getSn_RX_RSR(NTP_SOCKET)) > 0)
  {
    if (RSR_len > MAX_SNTP_BUF_SIZE) RSR_len = MAX_SNTP_BUF_SIZE;	// if Rx data size is lager than TX_RX_MAX_BUF_SIZE
    recvfrom(NTP_SOCKET, data_buf, RSR_len, (uint8_t *)&destip, &destport);

    get_seconds_from_ntp_server(data_buf,startindex);
    time->yy = Nowdatetime.yy;
    time->mo = Nowdatetime.mo;
    time->dd = Nowdatetime.dd;
    time->hh = Nowdatetime.hh;
    time->mm = Nowdatetime.mm;
    time->ss = Nowdatetime.ss;

    ntp_retry_cnt=0;
    close(NTP_SOCKET);

    return 1;
  }

  if(ntp_retry_cnt<0xFFFF)
  {
    if(ntp_retry_cnt==0)//first send request, no need to wait
    {
      sendto(NTP_SOCKET,ntpmessage,sizeof(ntpmessage),NTPformat.dstaddr,ntp_port);
      ntp_retry_cnt++;
    }
    else // send request again? it should wait for a while
    {
      if((ntp_retry_cnt % 0xFFF) == 0) //wait time
      {
        sendto(NTP_SOCKET,ntpmessage,sizeof(ntpmessage),NTPformat.dstaddr,ntp_port);
#ifdef _SNTP_DEBUG_
        printf("ntp retry: %d\r\n", ntp_retry_cnt);
#endif
        ntp_retry_cnt++;
      }
    }
  }
  else //ntp retry fail
  {
    ntp_retry_cnt=0;
#ifdef _SNTP_DEBUG_
    printf("ntp retry failed!\r\n");
#endif
    close(NTP_SOCKET);
  }
  break;
case SOCK_CLOSED:
  socket(NTP_SOCKET,Sn_MR_UDP,ntp_port,0);
  break;
}
// Return value
// 0 - failed / 1 - success
return 0;
}

在这里会执行一个UDP的状态机,当socket处于SOCK_UDP状态时,首先会执行sendto发送初始化SNTP时组装的SNTP请求报文,然后是监听服务器响应。

当Sn_RX_RSR (Socket n 空闲接收缓存寄存器)的值大于0时,说明服务器给W5500返回了响应。首先会通过recvfrom()函数读取响应报文,然后执行get_seconds_from_ntp_server解析时间。

请注意:

get_seconds_from_ntp_server()函数目前只解析了服务器响应时间戳(即最后8个字节),没有减去网络延迟。

get_seconds_from_ntp_server()函数定义如下:


void get_seconds_from_ntp_server(uint8_t *buf, uint16_t idx)
{
  tstamp seconds = 0;
  uint8_t i=0;
  for (i = 0; i < 4; i++)
  {
    seconds = (seconds << 8) | buf[idx + i];
  }
  switch (time_zone)
  {
  case 0:
    seconds -=  12*3600;
    break;
  case 1:
    seconds -=  11*3600;
    break;
  case 2:
    seconds -=  10*3600;
    break;
  case 3:
    seconds -=  (9*3600+30*60);
    break;
  case 4:
    seconds -=  9*3600;
    break;
  case 5:
  case 6:
    seconds -=  8*3600;
    break;
  case 7:
  case 8:
    seconds -=  7*3600;
    break;
  case 9:
  case 10:
    seconds -=  6*3600;
    break;
  case 11:
  case 12:
  case 13:
    seconds -= 5*3600;
    break;
  case 14:
    seconds -=  (4*3600+30*60);
    break;
  case 15:
  case 16:
    seconds -=  4*3600;
    break;
  case 17:
    seconds -=  (3*3600+30*60);
    break;
  case 18:
    seconds -=  3*3600;
    break;
  case 19:
    seconds -=  2*3600;
    break;
  case 20:
    seconds -=  1*3600;
    break;
  case 21:                            
  case 22:
    break;
  case 23:
  case 24:
  case 25:
    seconds +=  1*3600;
    break;
  case 26:
  case 27:
    seconds +=  2*3600;
    break;
  case 28:
  case 29:
    seconds +=  3*3600;
    break;
  case 30:
    seconds +=  (3*3600+30*60);
    break;
  case 31:
    seconds +=  4*3600;
    break;
  case 32:
    seconds +=  (4*3600+30*60);
    break;
  case 33:
    seconds +=  5*3600;
    break;
  case 34:
    seconds +=  (5*3600+30*60);
    break;
  case 35:
    seconds +=  (5*3600+45*60);
    break;
  case 36:
    seconds +=  6*3600;
    break;
  case 37:
    seconds +=  (6*3600+30*60);
    break;
  case 38:
    seconds +=  7*3600;
    break;
  case 39:
    seconds +=  8*3600;
    break;
  case 40:
    seconds +=  9*3600;
    break;
  case 41:
    seconds +=  (9*3600+30*60);
    break;
  case 42:
    seconds +=  10*3600;
    break;
  case 43:
    seconds +=  (10*3600+30*60);
    break;
  case 44:
    seconds +=  11*3600;
    break;
  case 45:
    seconds +=  (11*3600+30*60);
    break;
  case 46:
    seconds +=  12*3600;
    break;
  case 47:
    seconds +=  (12*3600+45*60);
    break;
  case 48:
    seconds +=  13*3600;
    break;
  case 49:
    seconds +=  14*3600;
    break;
  
  }
  
  //calculation for date
  calcdatetime(seconds);
}

然后调用calcdatetime()函数将调整后的秒数转换为年、月、日、时、分、秒的具体时间格式:


void calcdatetime(tstamp seconds)
{
  uint8_t yf=0;
  tstamp n=0,d=0,total_d=0,rz=0;
  uint16_t y=0,r=0,yr=0;
  signed long long yd=0;
  
  n = seconds;
  total_d = seconds/(SECS_PERDAY);
  d=0;
  uint32_t p_year_total_sec=SECS_PERDAY*365;
  uint32_t r_year_total_sec=SECS_PERDAY*366;
  while(n>=p_year_total_sec)
  {
    if((EPOCH+r)%400==0 || ((EPOCH+r)%100!=0 && (EPOCH+r)%4==0))
    {
      if(n<r_year_total_sec)
        break;
      n = n -(r_year_total_sec);
      d = d + 366;
      
    }
    else
    {
      n = n - (p_year_total_sec);
      d = d + 365;
    }
    r+=1;
    y+=1;
  
  }
  
  y += EPOCH;
  
  Nowdatetime.yy = y;
  
  yd=0;
  yd = total_d - d;
  
  yf=1;
  while(yd>=28)
  {
  
    if(yf==1 || yf==3 || yf==5 || yf==7 || yf==8 || yf==10 || yf==12)
    {
      yd -= 31;
      if(yd<0)break;
      rz += 31;
    }
  
    if (yf==2)
    {
      if (y%400==0 || (y%100!=0 && y%4==0))
      {
        yd -= 29;
        if(yd<0)break;
        rz += 29;
      }
      else
      {
        yd -= 28;
        if(yd<0)break;
        rz += 28;
      }
    }
    if(yf==4 || yf==6 || yf==9 || yf==11 )
    {
      yd -= 30;
      if(yd<0)break;
      rz += 30;
    }
    yf += 1;
  
  }
  Nowdatetime.mo=yf;
  yr = total_d-d-rz;
  
  yr += 1;
  
  Nowdatetime.dd=yr;
  
  //calculation for time
  seconds = seconds%SECS_PERDAY;
  Nowdatetime.hh = seconds/3600;
  Nowdatetime.mm = (seconds%3600)/60;
  Nowdatetime.ss = (seconds%3600)%60;
  
}

运行结果

注意:

因为本示例需要访问互联网,请确保 W5500 的网络环境及配置能够正常访问互联网。

烧录例程运行后,首先进行了PHY链路检测,然后是通过DHCP获取网络地址并打印网络地址信息,接着通过DNS解析SNTP服务器地址。最后,通过SNTP获取到时间后打印出来。整体流程如下图:

Blog Image

总结

本文讲解了如何在W5500芯片上实现SNTP授时功能,通过实例详细展示了从SNTP服务器同步时间的实现流程,包括时间请求、响应解析和本地时间校准等核心步骤。文章还对SNTP的应用场景进行了分析,帮助读者理解其在时间同步中的实际应用价值。

下一篇文章我们将讲解SMTP协议的原理及在邮件通信中的应用,同时讲解如何在W5500芯片上实现SMTP功能,敬请期待!

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板