目录
1 前言
NTP(网络时间协议)是计算机网络中用于时间同步的标准化协议,通过客户端-服务器模式,使本地设备从指定时间服务器获取精准时间,并结合网络延迟、时钟漂移等因素校准本地时钟,最终实现与UTC标准时间的高精度同步(精度达毫秒级甚至微秒级,受网络环境和服务器层级影响),核心是保障分布式设备在时间维度上的一致性。
W55MH32L-EVB是基于W55MH32L芯片开发的一款开发板,主频为216MHz,1MB的闪存以及96KB的SRAM,同时还具有一个完整的硬件TCP/IP卸载引擎,只需要简单的socket编程即可实现以太网应用。
具有以下特点:
- 增强型、真随机数、硬件加密算法单元
- 32位Arm® Cortex®-M3核心的片上
- 1024K字节闪存的微控制器
- 10/100M以太网MAC和PHY、集成完整的全硬件TCP/IP 协议栈引擎
- USB、CAN、17个定时器
- 3个ADC、2个DAC、12个通信接口
2 项目环境
2.1 硬件环境
- W55MH32L-EVB
- 网络连接线
- 交换机或路由器
2.2 软件环境
- 开发环境:keil uvision 5
- 飞思创串口助手
- NTP服务地址
3 硬件连接和方案
3.1 W5500硬件连接
W55MH32L-EVB网口通过网线连接交换机或者路由器
W55MH32L-EVB WIZ-Link USB口通过数据线连接电脑
3.2 方案图示
4 时间类型及时间戳简介
4.1 Unix 时间戳以及日期表示方法
Unix 时间戳表示的是从世界标准时间(UTC,Coordinated Universal Time)的 1970 年 1 月 1 日 0 时 0 分 0 秒开始的偏移量。全球共有 24 个时区,分为东西各 12 时区。所有地区在使用同一个时间戳的基础上,根据当地时区调整时间的表示。现在比较常见的日期和时间的表示标准是 ISO8601,或者在其基础上更加标准化的 RFC3339。
举个例子,北京时间 2021 年 1 月 28 日 0 时 0 分 0 秒用 RFC3339 表示为:2021-01-28T00:00:00+08:00。
+08:00 表示东 8 区(中国全境不含港澳台地区,但其实际也采用此时间),2021-01-28T00:00:00 表示这个时区的人所看到的时间。加号如果改为减号,则表示西时区。比较特殊的是 UTC 时区,可以表示为 2006-01-02T15:04:05+00:00,但通常简化为 2006-01-02T15:04:05Z。
4.2 日期和时间的解析
不同的数据来源很可能使用不同的时间表示方法。根据是否可读分成两类:
- 用数字表示的时间戳
- 用字符串表示的年月日时分秒
数字类型就不详细说明。
字符串又根据是否有时区分为两类:
- 2021-01-28 00:00:00 没有包含时区信息
- 2021-01-28T08:00:00+08:00 包含了时区信息
在解析没有包含时区信息的字符串时,通常要由程序员指定时区,否则默认为 UTC 时区。如果附带时区,那就可以不用另外指定。
在使用的时候,应当根据时区调整时间的展示。例如 1611792000 可以表示为 2021-01-28T00:00:00Z 或者 2021-01-28T08:00:00+08:00。
5 主要程序解析
5.1 main.c分析
通过初始化硬件(时钟、串口等)、网络(WIZnet 芯片配置、DHCP)和时间模块(RTC、NTP),在主循环中实现每 10 分钟从 NTP 服务器同步时间到本地 RTC、每秒打印当前时间、响应外部 NTP 时间请求及处理 UDP 数据,集 NTP 客户端、服务器与 RTC 同步功能于一体。
/******************************************************************************
* @file main.c
* @brief NTP 客户端 + 服务器 + RTC 同步
******************************************************************************/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "wizchip_conf.h"
#include "wiz_interface.h"
#include "bsp_tim.h"
#include "bsp_uart.h"
#include "bsp_rcc.h"
#include "delay.h"
#include "loopback.h"
#include "socket.h"
#include "ntp.h"
#include "w55mh32_rtc.h"
#define SOCKET_ID 0
#define ETHERNET_BUF_MAX_SIZE (1024 * 2)
/* 默认网络信息(DHCP) */
wiz_NetInfo default_net_info = {
.mac = {0x00, 0x08, 0xdc, 0x12, 0x22, 0x05},
.ip = {192, 168, 1, 30},
.gw = {192, 168, 1, 1},
.sn = {255, 255, 255, 0},
.dns = {8, 8, 8, 8},
.dhcp = NETINFO_DHCP
};
uint16_t local_port = 8080;
uint8_t ethernet_buf[ETHERNET_BUF_MAX_SIZE] = {0};
int main(void)
{
rcc_clk_config();
delay_init();
console_usart_init(115200);
tim3_init();
printf("%s NTP example\r\n", _WIZCHIP_ID_);
RTC_Init(); /* 初始化 RTC,预分频已补偿 4 tick */
wiz_toe_init();
wiz_phy_link_check();
network_init(ethernet_buf, &default_net_info);
ntp_init(); /* 初始化NTP模块 */
/* ---------- 首次 NTP 同步 ---------- */
ntp_first_sync();
uint32_t last_sync_tick = ntp_get_tick_count();
uint32_t last_print_tick = 0;
/* ---------- 主循环 ---------- */
while (1)
{
/* 每 10 分钟再次同步 */
if (ntp_get_tick_count() - last_sync_tick > 600000)
{
ntp_do_sync();
last_sync_tick = ntp_get_tick_count();
}
/* 每秒打印一次当前时间 */
if (ntp_get_tick_count() - last_print_tick >= 1000)
{
ntp_print_current_time();
last_print_tick = ntp_get_tick_count();
}
ntp_server_task();
loopback_udps(SOCKET_ID, ethernet_buf, local_port);
}
}
5.2 ntp.c分析
通过ntp_init初始化 UDP 套接字并解析阿里云 NTP 服务器 IP,客户端通过ntp_get_time向服务器发送请求获取 Unix 时间戳并同步到 RTC,ntp_first_sync和ntp_do_sync负责首次及周期性同步并打印 UTC 与 CST 时间;服务器通过ntp_server_task监听 123 端口,接收请求后基于本地 RTC 时间生成响应;同时提供时间戳与日期时间转换、格式化打印等工具函数,依赖定时器和 RTC 硬件实现时间管理,通过 UDP 协议完成网络通信。
/******************************************************************************
* @file ntp.c
* @brief NTP client & server with encapsulated time functions
******************************************************************************/
#include "ntp.h"
#include "wizchip_conf.h"
#include "socket.h"
#include "w55mh32_rtc.h"
#include "bsp_tim.h"
#include "delay.h"
#include <string.h>
#include <stdio.h>
#define NTP_CLIENT_SOCKET 1
#define NTP_SERVER_SOCKET 2
#define NTP_PACKET_SIZE 48
#define NTP_PORT 123
#define NTP_TIMEOUT 3000 /* ms */
#define NTP_TIMESTAMP_DELTA 2208988800UL /* 1900→1970 秒差 */
static uint8_t ntp_server_ip[4];
static uint8_t ntp_buffer[NTP_PACKET_SIZE];
/* ---------- 工具函数实现 ---------- */
uint32_t ntp_get_tick_count(void) {
return TIM3->CNT;
}
DateTime ntp_unix_to_datetime(uint32_t unix_time)
{
DateTime dt;
uint32_t days = unix_time / 86400;
uint32_t sec = unix_time % 86400;
uint8_t mdays[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
dt.year = 1970;
while (1) {
uint16_t ydays = 365 + (((dt.year % 4 == 0 && dt.year % 100 != 0) || (dt.year % 400 == 0)) ? 1 : 0);
if (days >= ydays) { days -= ydays; dt.year++; }
else break;
}
mdays[1] = (((dt.year % 4 == 0 && dt.year % 100 != 0) || (dt.year % 400 == 0)) ? 29 : 28);
dt.month = 1;
for (int m = 0; m < 12; ++m) {
if (days < mdays[m]) break;
days -= mdays[m];
dt.month++;
}
dt.day = days + 1;
dt.hour = sec / 3600;
dt.minute= (sec % 3600) / 60;
dt.second= sec % 60;
return dt;
}
void ntp_print_datetime(DateTime dt)
{
printf("%04d-%02d-%02d %02d:%02d:%02d\r\n",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
}
static uint8_t getDNSbyName(const char *domain, uint8_t *ip)
{
(void)domain;
ip[0] = 203; ip[1] = 107; ip[2] = 6; ip[3] = 88;
return 0;
}
/* ---------- NTP初始化 ---------- */
void ntp_init(void)
{
if (getDNSbyName("ntp.aliyun.com", ntp_server_ip) == 0)
printf("Resolved ntp.aliyun.com to %d.%d.%d.%d\r\n",
ntp_server_ip[0], ntp_server_ip[1],
ntp_server_ip[2], ntp_server_ip[3]);
socket(NTP_CLIENT_SOCKET, Sn_MR_UDP, 0, 0);
socket(NTP_SERVER_SOCKET, Sn_MR_UDP, NTP_PORT, 0);
}
/* ---------- 获取UNIX时间戳 ---------- */
uint32_t ntp_get_time(void)
{
uint8_t src_ip[4];
uint16_t src_port;
int16_t len;
memset(ntp_buffer, 0, NTP_PACKET_SIZE);
ntp_buffer[0] = 0x1B; /* NTP客户端请求标识 */
sendto(NTP_CLIENT_SOCKET, ntp_buffer, NTP_PACKET_SIZE, ntp_server_ip, NTP_PORT);
uint32_t start = ntp_get_tick_count();
while (ntp_get_tick_count() - start < NTP_TIMEOUT)
{
len = recvfrom(NTP_CLIENT_SOCKET, ntp_buffer, NTP_PACKET_SIZE, src_ip, &src_port);
if (len >= NTP_PACKET_SIZE &&
memcmp(src_ip, ntp_server_ip, 4) == 0 && src_port == NTP_PORT)
{
/* 提取服务器返回的时间戳并转换为UNIX时间 */
uint32_t ntp_sec = ((uint32_t)ntp_buffer[40] << 24) |
((uint32_t)ntp_buffer[41] << 16) |
((uint32_t)ntp_buffer[42] << 8) |
((uint32_t)ntp_buffer[43]);
return ntp_sec - NTP_TIMESTAMP_DELTA;
}
delay_ms(10);
}
printf("NTP timeout\r\n");
return 0;
}
/* ---------- NTP服务器任务 ---------- */
void ntp_server_task(void)
{
uint8_t src_ip[4];
uint16_t src_port;
int16_t len = recvfrom(NTP_SERVER_SOCKET, ntp_buffer, NTP_PACKET_SIZE, src_ip, &src_port);
if (len >= NTP_PACKET_SIZE && (ntp_buffer[0] & 0x07) == 0x03)
{
uint32_t rtc = RTC_GetCounter();
if (!rtc) { printf("Invalid RTC, skip response\r\n"); return; }
uint32_t tx = rtc + NTP_TIMESTAMP_DELTA;
memset(ntp_buffer, 0, NTP_PACKET_SIZE);
ntp_buffer[0] = 0x24; /* NTP服务器响应标识 */
ntp_buffer[40] = (tx >> 24) & 0xFF;
ntp_buffer[41] = (tx >> 16) & 0xFF;
ntp_buffer[42] = (tx >> 8) & 0xFF;
ntp_buffer[43] = tx & 0xFF;
sendto(NTP_SERVER_SOCKET, ntp_buffer, NTP_PACKET_SIZE, src_ip, src_port);
printf("Sent NTP response to %d.%d.%d.%d - Time: %lu\r\n",
src_ip[0], src_ip[1], src_ip[2], src_ip[3], tx);
}
}
/* ---------- 封装的同步和打印函数实现 ---------- */
uint8_t ntp_first_sync(void)
{
uint32_t unix_time = ntp_get_time();
if (unix_time > 0)
{
RTC_SetCounter(unix_time); /* 同步RTC */
printf("RTC synced to UNIX: %lu\r\n", unix_time);
// 打印UTC和CST时间
DateTime dt = ntp_unix_to_datetime(unix_time);
printf("Current time (UTC): ");
ntp_print_datetime(dt);
dt = ntp_unix_to_datetime(unix_time + 8*3600);
printf("Current time (CST): ");
ntp_print_datetime(dt);
return 1;
}
else
{
printf("Failed to get NTP time\r\n");
return 0;
}
}
uint8_t ntp_do_sync(void)
{
uint32_t unix_time = ntp_get_time();
if (unix_time > 0)
{
RTC_SetCounter(unix_time); /* 同步RTC */
printf("RTC re-synced: %lu\r\n", unix_time);
// 打印UTC和CST时间
DateTime dt = ntp_unix_to_datetime(unix_time);
printf("Current time (UTC): ");
ntp_print_datetime(dt);
dt = ntp_unix_to_datetime(unix_time + 8*3600);
printf("Current time (CST): ");
ntp_print_datetime(dt);
return 1;
}
return 0;
}
void ntp_print_current_time(void)
{
uint32_t rtc_time = RTC_GetCounter();
if (rtc_time > 0)
{
DateTime dt_utc = ntp_unix_to_datetime(rtc_time);
printf("UTC: ");
ntp_print_datetime(dt_utc);
DateTime dt_cst = ntp_unix_to_datetime(rtc_time + 8*3600);
printf("CST: ");
ntp_print_datetime(dt_cst);
}
else
{
printf("RTC time not set\r\n");
}
}
5.3 rtc.c分析
- RTC 初始化(RTC_Init())
- 时钟源选择:启用 LSE(32.768KHz 低频晶振),确保计时精度(每秒跳动 1 次)。
- 预分频配置:RTC_SetPrescaler(32767),将 32768Hz 晶振分频为 1Hz,与 UNIX 时间戳单位(秒)匹配。
- 核心接口
- RTC_SetCounter():将 NTP 获取的 UNIX 时间戳写入 RTC 计数器,实现时间同步。
- RTC_GetCounter():读取当前 RTC 计数器值(UNIX 时间戳),作为 NTP 服务器授时的基准。
/******************************************************************************
* @file w55mh32_rtc.c
******************************************************************************/
#include "w55mh32_rtc.h"
#include "w55mh32_pwr.h"
#include "w55mh32_bkp.h"
#include "bsp_uart.h"
#include <stdint.h>
#define RTC_LSB_MASK ((uint32_t)0x0000FFFF)
#define PRLH_MSB_MASK ((uint32_t)0x000F0000)
/* -------------- 函数实现 -------------- */
void RTC_Init(void)
{
/* 1. 打开 PWR & BKP 时钟 */
RCC->APB1ENR |= RCC_APB1Periph_PWR | RCC_APB1Periph_BKP;
PWR->CR |= PWR_CR_DBP;
/* 2. 备份域软复位,确保干净 */
RCC->BDCR |= RCC_BDCR_BDRST;
RCC->BDCR &= ~RCC_BDCR_BDRST;
/* 3. 启动 LSE */
RCC->BDCR |= RCC_BDCR_LSEON;
while (!(RCC->BDCR & RCC_BDCR_LSERDY))
;
/* 4. 选择 LSE 作为 RTC 时钟并启用 RTC */
RCC->BDCR &= ~RCC_BDCR_RTCSEL;
RCC->BDCR |= RCC_BDCR_RTCSEL_LSE | RCC_BDCR_RTCEN;
/* 5. 等待同步 & 设置预分频器(32763 = 32768-4) */
RTC_WaitForSynchro();
RTC_SetPrescaler(32763);
/* 6. 计数器先清零,由 NTP 设定正确 UNIX 时间 */
RTC_SetCounter(0);
printf("RTC initialized with LSE (prescaler 32763)\r\n");
}
/* -------------- 其余库函数保持不变 -------------- */
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState)
{
assert_param(IS_RTC_IT(RTC_IT));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
RTC->CRH |= RTC_IT;
else
RTC->CRH &= (uint16_t)~RTC_IT;
}
void RTC_WaitForSetComplete(void)
{
while ((RTC->CRL & RTC_CRL_RTOFF) == 0)
;
}
void RTC_EnterConfigMode(void)
{
RTC->CRL |= RTC_CRL_CNF;
}
void RTC_ExitConfigMode(void)
{
RTC->CRL &= (uint16_t)~RTC_CRL_CNF;
}
uint32_t RTC_GetCounter(void)
{
uint16_t tmp = RTC->CNTL;
return (((uint32_t)RTC->CNTH << 16) | tmp);
}
void RTC_SetCounter(uint32_t CounterValue)
{
RTC_EnterConfigMode();
RTC->CNTH = CounterValue >> 16;
RTC->CNTL = (CounterValue & RTC_LSB_MASK);
RTC_ExitConfigMode();
}
void RTC_SetPrescaler(uint32_t PrescalerValue)
{
assert_param(IS_RTC_PRESCALER(PrescalerValue));
RTC_EnterConfigMode();
RTC->PRLH = (PrescalerValue & PRLH_MSB_MASK) >> 16;
RTC->PRLL = (PrescalerValue & RTC_LSB_MASK);
RTC_ExitConfigMode();
}
void RTC_SetAlarm(uint32_t AlarmValue)
{
RTC_EnterConfigMode();
RTC->ALRH = AlarmValue >> 16;
RTC->ALRL = (AlarmValue & RTC_LSB_MASK);
RTC_ExitConfigMode();
}
uint32_t RTC_GetDivider(void)
{
return (((uint32_t)RTC->DIVH & 0x000F) << 16) | RTC->DIVL;
}
void RTC_WaitForLastTask(void)
{
while ((RTC->CRL & RTC_FLAG_RTOFF) == RESET)
;
}
void RTC_WaitForSynchro(void)
{
RTC->CRL &= (uint16_t)~RTC_FLAG_RSF;
while ((RTC->CRL & RTC_FLAG_RSF) == RESET)
;
}
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG)
{
assert_param(IS_RTC_GET_FLAG(RTC_FLAG));
return (RTC->CRL & RTC_FLAG) ? SET : RESET;
}
void RTC_ClearFlag(uint16_t RTC_FLAG)
{
assert_param(IS_RTC_CLEAR_FLAG(RTC_FLAG));
RTC->CRL &= (uint16_t)~RTC_FLAG;
}
ITStatus RTC_GetITStatus(uint16_t RTC_IT)
{
assert_param(IS_RTC_GET_IT(RTC_IT));
return ((RTC->CRH & RTC_IT) && (RTC->CRL & RTC_IT)) ? SET : RESET;
}
void RTC_ClearITPendingBit(uint16_t RTC_IT)
{
assert_param(IS_RTC_IT(RTC_IT));
RTC->CRL &= (uint16_t)~RTC_IT;
}
6 功能验证
6.1 获取时间验证
6.2 授时验证
w32tm /stripchart /computer:192.168.2.21 是 Windows 系统下用于检查本地计算机与指定 NTP(网络时间协议)服务器(这里是 192.168.1.6)之间时间同步状态的命令。我们通过命令行发送w32tm /stripchart /computer:192.168.1.6向W55MH32L-EVB发送NTP请求。
7 总结
本文介绍W55MH32L-EVB通过NTP实现取时与授时,解析软硬件配置、程序逻辑,含时间同步与校准,及功能验证方法。感谢大家的耐心阅读!如果您在阅读过程中有任何疑问,或者希望进一步了解这款产品及其应用,欢迎随时通过私信或评论区留言。我们会尽快回复您的消息,为您提供更详细的解答和帮助!