W55MH32方案 NTP协议实现取时授时一体的网络计时器

发布于:2025-08-20 ⋅ 阅读:(20) ⋅ 点赞:(0)

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 硬件环境

  1. W55MH32L-EVB
  2. 网络连接线
  3. 交换机或路由器

2.2 软件环境

  1. 开发环境:keil uvision 5
  2. 飞思创串口助手
  3. 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分析

  1. RTC 初始化(RTC_Init())
  • 时钟源选择:启用 LSE(32.768KHz 低频晶振),确保计时精度(每秒跳动 1 次)。
  • 预分频配置:RTC_SetPrescaler(32767),将 32768Hz 晶振分频为 1Hz,与 UNIX 时间戳单位(秒)匹配。
  1. 核心接口
  • 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实现取时与授时,解析软硬件配置、程序逻辑,含时间同步与校准,及功能验证方法。感谢大家的耐心阅读!如果您在阅读过程中有任何疑问,或者希望进一步了解这款产品及其应用,欢迎随时通过私信或评论区留言。我们会尽快回复您的消息,为您提供更详细的解答和帮助!


网站公告

今日签到

点亮在社区的每一天
去签到