你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】基于 STM32F7 的网络时间同步客户端实现

[复制链接]
STMCU小助手 发布时间:2022-2-14 22:11
前言
本文将介绍一个基于 STM32F7 的网络时间同步客户端例程。在开始介绍程序之前,我们先来了解一下什么是网络时间同步,和 NTP 协议等相关概念。
通常在计算机工作前,我们会预先设定好系统的时间。但如果网络中多台计算机(或者处于多个网络中的计算机)需要协同工作的时候,这些计算机的时间就需要保持同步。如果两个计算机(或者两个网络)的时间不同步,有可能会出现,比如你
会收到 5 分钟之后才会被发出的邮件。所以在这种情况下需要通过某种办法将网络上各设备的时间进行同步。
说到这里,我们也知道了网络时间同步,是指将计算机或者设备的时间与网络上的时间源保持一致。时间源是由网络上的时间服务器提供。本文介绍的是 NTP 客户端,NTP 协议是 TCP/IP 协议中的应用层协议。NTP 的全称是 Network Time Protocol, 它是用来同步网络中个计算机的时间的协议。它的目的是在国际互联网上传递统一、标准的时间。具体的实现方案是在网络上指定若干时钟源网站,为用户提供授时服务,并且这些网站间应该能够相互比对,提高准确度。
NTP 简介
NTP 工作原理

这里借用网上的一个图来简单的说下 NTP 的工作原理:假设设备 A 和设备 B 通过网络连接,它们都有各自的时间系统,需要通过网络进行同步。假设同步前设备 A 的时间是 10:00:00 AM, 设备 B 的时间是 11:00:00AM,相差 1 个小时。
设备 B 作为 NTP 时间服务器,设备 A 需要将自己的时间与设备 B 同步。

1. 首先设备 A 向设备 B(服务器)发送一个 NTP 报文,该报文带有它离开设备 A 时的时间戳 10:00:00AM(T1)
2. 该 NTP 报文到达设备 B,设备 B 加上当前自己的时间,也就 11:00:01AM(T2)
3. 设备 B 回复设备 A,并加上报文离开时的时间戳 11:00:02(T3)
4. 设备 A 收到设备 B 的回复,此时,设备 A 的当地时间是 10:00:03(T4)

$@@31R1~TSZ3_D[~7CT6J5G.png

通过这个过程,设备 A 得到了四个时间戳。假设 NTP 报文从设备 A 到设备 B,和从设备 B 回到设备 A 的时间是对等的,利用这四个时间戳,设备 A 就可以计算出和设备 B 的时间差,从而来更新自己的系统时间。
    NTP 报文的往返时延 Delay = (T4-T1)-(T3-T2)
   设备 A 相对设备 B 的时间差 Offset=((T2-T1)+(T3-T4))/2

NTP 的工作模式和工作模型
NTP 有三种工作模式:主/被动模式,客户端/服务器模式和广播模式。主/被动模式下,连接双方可以互相同步,客户端/服务器模式下,只能客户端被服务器同步。广播模式,是一对多的连接,服务器主动发出时间信息,客户由此调整自己的时间。

NTP 采用分层结构进行工作(如下图)。最顶层(0 层)是时间同步网络的基准时间参考源,它位于整个同步网络的顶层。
直接连接到 0 层时钟源的计算机属于第 1 层,从第 1 层接受时间的计算机属于第 2 层,以此类推。每一层的号码代表了这一层到顶层时钟源的距离。参考时钟源的层数从 0 到 15,16 表示未同步的设备。下层设备可以同时应用几个上层设备的时间作为参考,也可以引用同层设备的时间作为参考。

{XDS_ET1Y{@PAC5]O%AWE6U.png

NTP 时间戳
NTP 以 UTC 作为标准时间。UTC 是以原子时秒长为基础的时间计量系统。
NTP 时间戳,是从 1900 年 1 月 1 日 0 时 0 分 0 秒开始所经过的秒数。NTP 的时间戳通过一个 64 比特的无符号定点数来表示。
前 32 比特表示整数部分,后 32 比特表示小数部分,以秒为单位。

3ULZO31(N33OJO5@OY%(OSV.png

因为只有 32 位表示秒数, 50ZBX44FZH[(TZK)KOQ%X.png 。所以 136 年为一个周期,当到 2036 年时,数据会溢出。NTP 协议中定义了 Era Number 来解决这个问题。从 1900 年 1 月 1 日开始的第一个 136 年,Era Number 为 0,之后的每 136 年加 1。但 Era Number 的值本身并不能从 NTP 的数据中得出,需要从外部采用一些方法来解决。

NTP 报文格式
下图所示是 NTP 报文的格式:

9_29]560OAER8(N56[N74TL.png

   LI 闰秒标识器,占 2 个比特位。预警最近的分钟里将要被插入或者删除的闰秒秒数。
   版本号,占 3 个比特位。现在为版本 4.
   模式,占 3 个比特位。
   Stratum(层) ,占 8 个比特位。表示当前时钟的层。
   轮询(Poll)间隔,占 8 个比特位。表示连续信息之间的最大间隔。
   本地时钟精度,占 8 个比特位。
   原始时间戳 Originate Timestamp,64 比特。客户端发出 NTP 报文的时间。
   接收时间戳 Receive Timestamp,64 比特。服务器端接收到 NTP 报文的时间。
   发送时间戳 Transmit Timestamp,64 比特。服务器端发送应答的时间。

NTP,SNTP 和 IEEE1588(PTP)的区别
大多数环境中,NTP 可以提供 1~50ms 的可靠时钟源。但对于很多系统来说,并不需要这么高精度的同步,而且完全实现NTP 协议太复杂了,所以 SNTP(Simple Network Time Protocol)应运而生。SNTP 基于 NTP 协议,和 NTP 的数据帧格式
是一样的,计算时间偏差以及数据包往返时延的方法也一眼。区别就是 SNPT 没有 NTP 中复杂的同步算法。SNTP 提供的同步时间精度比 NTP 低。SNTP 与 NTP 协议具有互操作性,SNTP 客户端可以与 NTP 服务器协同工作,NTP 客户端也可以接
受 SNTP 服务器发出的授时信息。

IEEE1588 协议是专门针对工业应用提出的精确时钟同步协议。它能提供微秒级的时间同步。

与 IEEE1588 相比,NTP/SNTP 授时精度不高的原因在于打时间戳的位置。NTP/SNTP 是在应用层写入或者读出时间戳,客户端发起授时请求,先从应用层到物理层,经过网络传输,到达服务器端再从物理层到应用层被读出,这三个阶段都存在不
确定性。反之亦然,造成 NTP/SNTP 的精度不会很高。
而 IEEE1588 的时间戳的获取位置是在物理层,可以避免报文处理时间的不一致性。STM32 的以太网外设支持 IEEE1588 协议提供高精度的同步,在每个帧的发送或接收时给出 64 位时间戳,与 NTP 的时间戳格式相同。关于 IEEE1588 的实现可以
参考 ST 的另一篇 AN3411。

NTP 客户端代码实现
本代码基于 STM32F7Cube 库,通过 NTP 协议从远程 NTP 时间服务器上读取时间,并同步本地 RTC 的实时时钟。程序使用了 STM32F746 的以太网和 RTC 两个外设,应用 FreeRTOS 操作系统,TCP/IP 编程部分使用 netconn 接口函数。

实时时钟 RTC
STM32F7 的实时时钟是一个独立的 BCD 定时器/计数器。可以提供具有可编程闹钟中断功能的日历时钟/日历。两个 32 位寄存器包含二进码十进数格式 (BCD) 的秒、分钟、小时(12 或 24 小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。并且还可以进行夏令时补偿。此外,还可以使用数字校准功能对晶振精度的偏差进行补偿。
备份域复位后,所有 RTC 寄存器都会受到保护,以防止可能的非正常写访问。
无论器件状态如何(运行模式、低功耗模式或处于复位状态),只要电源电压保持在工作范围内,RTC 便不会停止工作。
在本代码中,首先将 RTC 初始化到默认的时间 2014 年 2 月 18 号,2 点 0 分 0 秒。当收到 NTP 服务器的响应后,利用收到的时间修改 RTC 的值,完成同步。
下面是 RTC 相关的函数:
RTC_Init 和 RTC_CalendarConfig,在 main 函数中调用
  1. if (HAL_RTC_Init(&RtcHandle) != HAL_OK)
  2. {
  3. /* Initialization Error */
  4. while(1) {};
  5. }
  6. }
  7. /**

  8. * @brief Configure the current time and date.
  9. * @param None
  10. * @retval None
  11.   */
  12.   static void RTC_CalendarConfig(void)
  13.   {
  14.   RTC_DateTypeDef sdatestructure;
  15.   RTC_TimeTypeDef stimestructure;
  16.   /*##-1- Configure the Date #################################################*/
  17.   /* Set Date: Tuesday February 18th 2014 */
  18.   sdatestructure.Year = 0x14;
  19.   sdatestructure.Month = RTC_MONTH_FEBRUARY;
  20.   sdatestructure.Date = 0x18;
  21.   sdatestructure.WeekDay = RTC_WEEKDAY_TUESDAY;

  22. if(HAL_RTC_SetDate(&RtcHandle,&sdatestructure,RTC_FORMAT_BCD) != HAL_OK)
  23. {
  24. /* Initialization Error */
  25. while(1) {};
  26. }
  27. /*##-2- Configure the Time #################################################*/
  28. /* Set Time: 02:00:00 */
  29. stimestructure.Hours = 0x02;
  30. stimestructure.Minutes = 0x00;
  31. stimestructure.Seconds = 0x00;
  32. stimestructure.TimeFormat = RTC_HOURFORMAT12_AM;
  33. stimestructure.DayLightSaving = RTC_DAYLIGHTSAVING_NONE ;
  34. stimestructure.StoreOperation = RTC_STOREOPERATION_RESET;
  35. if (HAL_RTC_SetTime(&RtcHandle, &stimestructure, RTC_FORMAT_BCD) != HAL_OK)
  36. {
  37. /* Initialization Error */
  38. while(1) {};
  39. }
  40. /*##-3- Writes a data in a RTC Backup data Register1 #######################*/
  41. HAL_RTCEx_BKUPWrite(&RtcHandle, RTC_BKP_DR1, 0x32F2);
  42. }
复制代码

RTC_CalendarUpdate 函数,更新 RTC 的值,在 NTP 进程中调用。
  1. /**
  2. * @brief Update the current time and date according to the time get from NTP server.
  3. * @param newtime. pointer to newtime
  4. * @retval None
  5.   */
  6.   void RTC_CalendarUpdate(struct tm * newtime)
  7.   {
  8.   RTC_DateTypeDef sdatestructure;
  9.   RTC_TimeTypeDef stimestructure;

  10. /*##-1- Configure the Date #################################################*/
  11. sdatestructure.Year = newtime->tm_year+1900-2000;
  12. sdatestructure.Month = newtime->tm_mon +1;
  13. sdatestructure.Date = newtime->tm_mday;
  14. sdatestructure.WeekDay = newtime->tm_wday;

  15. if(HAL_RTC_SetDate(&RtcHandle,&sdatestructure,RTC_FORMAT_BIN) != HAL_OK)
  16. {
  17. /* Initialization Error */
  18. while(1) {};
  19. }
  20. /*##-2- Configure the Time #################################################*/
  21. stimestructure.Hours = newtime->tm_hour;
  22. stimestructure.Minutes = newtime->tm_min;
  23. stimestructure.Seconds = newtime->tm_sec;
  24. stimestructure.TimeFormat = RTC_HOURFORMAT12_AM;
  25. stimestructure.DayLightSaving = RTC_DAYLIGHTSAVING_NONE ;
  26. stimestructure.StoreOperation = RTC_STOREOPERATION_RESET;
  27. if (HAL_RTC_SetTime(&RtcHandle, &stimestructure, RTC_FORMAT_BIN) != HAL_OK)
  28. {
  29. /* Initialization Error */
  30. while(1) {};
  31. }
  32. /*##-3- Writes a data in a RTC Backup data Register1 #######################*/
  33. HAL_RTCEx_BKUPWrite(&RtcHandle, RTC_BKP_DR1, 0x32F2);
  34. }
复制代码

NTP 部分
NTP 协议基于 UDP 进行传输,使用的 UDP 端口号为 123。使用 LwIP 实现 NTP 协议,要记得在 Lwipopts.h 里将LWIP_UDP 宏定义打开。
下面是一个简单的 NTP 通信流程:

X`CY20RO{L~5~ITL6H]LFGR.png

NTP 通信相关代码:
建立 NTP Client 的服务进程
  1. void ntp_client_init(void)
  2. {
  3. sys_thread_new("ntp_thread", ntp_thread, NULL, DEFAULT_THREAD_STACKSIZE,
  4. NTP_THREAD_PRIO);
  5. }
复制代码


ntp_thread,ntp_request 与 ntp_process,完成与服务器的通信以及 NTP 时间格式到可读的年月日格式的转换。
  1. static void ntp_thread(void *arg)
  2. {
  3. while(1)
  4. {
  5. /*#-send ntp request ##############################*/
  6. ntp_request();

  7. /*#-update local time #############################*/
  8. printf("RTC time before update\n");
  9. RTC_CalendarShow();

  10. RTC_CalendarUpdate(synchronized_local_time);

  11. /*display local time*/
  12. printf("RTC time after update\n");
  13. RTC_CalendarShow();

  14. /*#-delay 1min ######################################*/
  15. osDelay(60*1000);
  16. }

  17. }
  18. static void ntp_request(void)
  19. {
  20. struct netconn * conn= NULL;
  21. struct netbuf * buf = NULL;
  22. uint8_t ntp_request_buf[NTP_PKT_LEN];
  23. uint8_t* ntp_receive_buf_p;
  24. uint16_t buf_len;
  25. err_t err;
  26. //get ntp server address
  27. #if USE_DNS
  28. if(netconn_gethostbyname(ntp_server_list[0],&ntp_server_addr)!=ERR_OK)
  29. {
  30. IP4_ADDR(&ntp_server_addr, NTP_Server_ADDR0, NTP_Server_ADDR1, NTP_Server_ADDR2,
  31. NTP_Server_ADDR3);
  32. }
  33. #else
  34. IP4_ADDR(&ntp_server_addr, NTP_Server_ADDR0, NTP_Server_ADDR1, NTP_Server_ADDR2,
  35. NTP_Server_ADDR3);
  36. #endif

  37. //Create new netconn
  38. conn = netconn_new(NETCONN_UDP);

  39. if(conn!= NULL)
  40. {
  41. buf = netbuf_new();
  42. if(buf != NULL)
  43. {
  44. //initialize ntp packet to 0
  45. memset(ntp_request_buf, 0, NTP_PKT_LEN);
  46. //buid ntp packet
  47. ntp_request_buf[0] = NTP_LI_NO_WARNING|NTP_VERSION|NTP_MODE_CLIENT;

  48. err = netbuf_ref(buf,ntp_request_buf,NTP_PKT_LEN);
  49. if(err ==ERR_OK)
  50. {
  51. //connect to NTP server
  52. err = netconn_connect(conn,&ntp_server_addr,NTP_PORT);
  53. if(err == ERR_OK)
  54. {
  55. //Org_Timestamp, read current local time
  56. Org_Timestamp = ntp_get_currenttime();

  57. //send ntp request to ntp server
  58. if(netconn_send(conn,buf)!=ERR_OK)
  59. {

  60. }
  61. netbuf_delete( buf );
  62. //reveive ntp response
  63. netconn_recv(conn,&buf);

  64. //Destination_Timestamp,read current local time
  65. Destination_Timestamp = ntp_get_currenttime();

  66. netbuf_data(buf,(void**)&ntp_receive_buf_p,&buf_len);

  67. //check ntp packet
  68. if(buf_len==NTP_PKT_LEN)
  69. {
  70. if(((ntp_receive_buf_p[0]& NTP_MODE_MASK) == NTP_MODE_SERVER)||
  71. ((ntp_receive_buf_p[0] & NTP_MODE_MASK) == NTP_MODE_BROADCAST))
  72. {
  73. // extract time from packet
  74. Receive_Timestamp = ntp_receive_buf_p[RECEIVE_TS_OFFSET]<<24 |
  75. ntp_receive_buf_p[RECEIVE_TS_OFFSET+1]<<16|
  76. ntp_receive_buf_p[RECEIVE_TS_OFFSET+2]<<8
  77. |ntp_receive_buf_p[RECEIVE_TS_OFFSET+3];
  78. Transmit_Timestamp = ntp_receive_buf_p[TRANSMIT_TS_OFFSET]<<24 |
  79. ntp_receive_buf_p[TRANSMIT_TS_OFFSET+1]<<16|
  80. ntp_receive_buf_p[TRANSMIT_TS_OFFSET+2]<<8
  81. |ntp_receive_buf_p[TRANSMIT_TS_OFFSET+3];
  82. // start conver time format
  83. ntp_process(Transmit_Timestamp);
  84. }
  85. }

  86. netconn_close(conn);
  87. netconn_delete(conn);
  88. netbuf_delete( buf );
  89. }
  90. else
  91. {
  92. netconn_delete(conn);
  93. netbuf_delete( buf );
  94. }
  95. }
  96. else//
  97. {
  98. netconn_delete(conn);
  99. netbuf_delete( buf );

  100. }
  101. }
  102. else //buf ==NULL
  103. {
  104. netconn_delete(conn);
  105. }
  106. }
  107. //process
  108. }
  109. /**
  110. * @brief process the time obtained from NTP server.
  111. Change it to a human-readable format. And consider the timezone
  112. The NTP timestamp use 1900 as epoch, but the input param of gmtime() function
  113. consider 1970 as epoch.
  114. * @param None
  115. * @retval None
  116. */
  117. static void ntp_process(uint32_t timestamp)
  118. {
  119. uint32_t local_ntp_timestamp;
  120. //minus the difference value of 1900 epoch and 1970 epoch
  121. local_ntp_timestamp = timestamp - DIFF_SEC_1900_1970;

  122. //consider the time zone
  123. local_ntp_timestamp += SEC_TIME_ZONE;

  124. //conver to human-readable format
  125. synchronized_local_time = gmtime(&local_ntp_timestamp);
  126. }
复制代码

编程要点
前面已经介绍过 NTP 的时间格式是以 1900 年 1 月 1 日零时为元年,以秒为单位来表示某个时刻的时间的。所以从 NTP 报文中提取到当前的时间后(NTP 格式),还需要转换成可读的时间格式(年月日,时分秒)。C 标准函数库里已经提供了对应的转换函数 gmtime。但 gmtime 处理的时间是以 1970 年 1 月 1 日零时为元年,所以在调用 gmtime 之前先要减去这 70 年的时间差。另外 NTP 时间是不考虑时区的,所以还需要程序将本地的时区考虑进去。
在编写客户端程序的时候,另外一个需要注意的地方就是发起授时请求的间隔时间。这个间隔时间不宜太短,太频繁的请求会加重服务器的负担,导致不能及时获得响应。这个时间间隔应该根据具体系统的精度来计算最大的请求时间间隔,最小间
隔不能小于 15 秒。在 RFC4330-SNTP verion4 中更详细的描述了 NTP/SNTP 客户端实现时应该注意的事项。

测试结果
将该程序在 STM32F746 Nucleo 板上进行测试。
硬件连接方式:STM32F746Nucelo 板连接到 PC,PC 通过无线上网,将无线连接的属性的高级配置中,设置“允许其他网络用户通过此计算机的 Internet 连接来连接”。PC 的有线网口的 IP 地址设为:192.168.0.1。STM32 测试板通过 PC 连接到
Internet。STM32F746Nucelo 板的 IP 地址设为:192.168.0.8. 网关设为:192.168.0.1.
程序向远程网络时间服务器 1.cn.pool.ntp.org 发起请求,再根据服务器的反馈修改本地时间。
下面是程序执行的打印信息,和 wireshark 捕获的通信过程。
同步前的时间是:2014 年 2 月 18 日,2:00:00
同步后的时间是:2016 年 7 月 14 日,11:18:26

JO3{Y9APDVION0CP9@(]$WY.png

利用 RTC 的亚秒字段实现高精度同步
前面的例程里,只用到 NTP 时间中的前 32 位。只同步到了秒。STM32F7 的 RTC 还可以实现亚秒字段的高精度远程时钟同步。
在读取 RTC 亚秒字段后(RTC_SSR 或 RTC_TSSSR),即可计算远程时钟的时间与本地 RTC 之间的精准时间差。之后,可使用 RTC_SHIFTR 对 RTC 的时钟进行零点几秒的“平移”,经过调整后可消除此偏差。
RTC_SSR 包含同步预分频器计数器的值。这样,便可计算分辨率低至 1/(PREDIV_S + 1)秒的 RTC 的准确时间。因此,可通过增大同步预分频器的值 (PREDIV_S[14:0]) 来提高分辨率。将 PREDIV_S 设置为 0x7FFF 时,可得到允许的最大分辨率
(30.52 μs,时钟频率为 32768 Hz)。

提高本地时间的精度:RTC 频率校准
RTC 的时间精度与所用的时钟频率相关,想得到高精度的计时,就需要有精确的时钟频率。RTC 的精密数字校准寄存器可以帮助校准外部时钟频率。具体的校准方法可以参看 STM32F7 的参考手册 RTC 章节。





收藏 评论0 发布时间:2022-2-14 22:11

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版