Skip to content

概述

物联网设备常需准确的本地时间(数据上报时间戳、定时任务)。SNTP(简单网络时间协议)通过访问 NTP 服务器自动同步时间,误差可达毫秒级。本教程演示:连接 Wi-Fi 后从 ntp.aliyun.com 获取时间,并换算为**北京时间(UTC+8)**打印。

用大白话讲:SNTP 就是开发板联网对表——像手表时间不准了,打电话问「时间服务中心」现在是几点,再按它说的把表校准。开发板每次开机联网后都会自动向 NTP 服务器(本教程用 ntp.aliyun.com)要一次标准时间,拿到后按你的时区换算成北京时间打印出来。这样设备上报数据的时间戳(记录「某年某月某日几点几分」的数字)才是可信的。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/protocols/sntp_demo 编写,代码可在本地 SDK 中直接找到。

🎯本页目标通过 SNTP 从公网 NTP 服务器同步时间并换算北京时间,掌握 sntp 初始化、时间获取与本地时区换算。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器(能访问公网)③ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect)。
🔗相关章节联网基础见 [连接 Wi-Fi](./wifi_connect);时间戳常用于 [MQTT 连接](./mqtt) 等上报场景。

进入示例工程

打开终端,进入官方 sntp_demo 示例工程目录:

cd ~/Ai-Thinker-WB2/applications/protocols/sntp_demo

说明:cd 是「进入目录」的命令,进入官方示例工程;后面所有 make 命令都要在这个目录下执行。

修改路由器参数与时区

打开 sntp_demo/main.c,修改开头的 SSID/密码(同 连接 Wi-Fi),并按需调整时区:

#define ROUTER_SSID "ssid"
#define ROUTER_PWD "password"
#define sntp_server "ntp.aliyun.com"
#define UTC 8       //your Timezone, for example,beijing Timezone is GMT+8
默认值 说明
sntp_server ntp.aliyun.com NTP 服务器地址(可选 ntp.ntsc.ac.cn 等);连不上它,时间就同步不了
UTC 8 时区偏移(北京时间 = UTC+8;东京 = 9,伦敦 = 0);不加它,打印的是世界标准时间,会差 8 小时
编写代码

打开 sntp_demo/main.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/protocols/sntp_demo/sntp_demo/main.c)完全一致。

代码要点:

代码 作用
sntp_setoperatingmode(SNTP_OPMODE_POLL) 设置 SNTP 轮询模式(定时主动问服务器,不用自己管)
sntp_setservername(0, "ntp.aliyun.com") 指定 NTP 服务器(告诉它去问谁要时间)
sntp_init() 启动 SNTP 同步(不调用它就不干活)
tcpip_callback(_startup_sntp, NULL) lwIP 上下文中启动 SNTP(线程安全);在事件回调里直接调会死机
sntp_get_time(&seconds, &frags) 获取同步时间:自 1970 年起的秒数 + 亚秒;没同步成功时拿到的是旧值
utils_time_date_from_epoch(seconds+UTC*60*60, &date) 秒数 + 时区偏移后转为日期结构体(秒数看不懂,转成年月日时分秒)

💡 sntp_get_time 返回的 secondsUnix 时间戳(1970-01-01 00:00:00 UTC 起);加 UTC*60*60(8 小时)即北京时间。SNTP 需在 GOT_IP 之后启动。

编译工程

在工程目录执行编译:

make -j8

说明:make 是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8 表示用 8 个 CPU 核并行编译,更快。

编译成功后生成固件 build_out/sntp_demo.bin

烧录固件

开发板保持 USB 连接,确认串口设备号后执行烧录:

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件下载进开发板;p= 后面是串口设备号(Linux 下常为 /dev/ttyUSB0,Windows 下是 COM3 之类,以你电脑实际为准),b=921600 是烧录波特率(串口传数据的速度),保持默认即可。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。

运行验证

烧录完成后开发板自动重启运行,打开串口助手(波特率 921600,串口传数据的「语速」,两边必须一致),先看到 Wi-Fi 连接事件,然后打印:

[APP] [EVT] GOT IP 5
--------------------------------------- Start NTP now
--------------------------------------- Start NTP Done
[NTP] time is 4123456789:0
Date & time is: 2026-08-11 14:53:30 (Day 2 of week, Day 223 of Year)

出现 Date & time is: 且日期不是 1970 年即成功——时间与电脑/手机当前时间一致(时区为北京时间)。

💡 踩坑:如果一直不打印日期、或时间停在 1970 年,说明 SNTP 同步失败——先确认开发板已联网(先看到 GOT IP)、路由器能访问公网、NTP 服务器(ntp.aliyun.com)可达;可尝试更换服务器(如 ntp.ntsc.ac.cn),见文末 FAQ。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

sntp_setoperatingmode(mode)

设置 SNTP 工作模式。

参数

  • mode:工作模式,SNTP_OPMODE_POLL(轮询模式)

返回值:无

sntp_setservername(idx, server)

设置第 idx 个 NTP 服务器地址。

参数

  • idx:服务器序号(从 0 开始,官方示例用 0
  • server:服务器域名/地址,如 "ntp.aliyun.com"

返回值:无

sntp_init

启动 SNTP 时间同步。

返回值:无

sntp_get_time(&sec, &frag)

获取同步时间(Unix 秒 + 亚秒)。

参数

  • sec:输出参数,Unix 秒(uint32_t
  • frag:输出参数,亚秒部分

返回值:成功返回 0;失败返回负值错误码

utils_time_date_from_epoch(epoch, &date)

Unix 秒转日期结构体(年月日时分秒/星期/年日)。

参数

  • epoch:Unix 秒
  • date:输出参数,utils_time_date_t 结构体

返回值:成功返回 0;失败返回负值错误码

tcpip_callback(fn, arg)

在 lwIP 上下文中执行函数(线程安全,跨任务调用 lwIP API 推荐)。

参数

  • fn:要执行的回调函数
  • arg:回调参数

返回值:成功返回 0;失败返回负值错误码

wifi_mgmr_sta_connect(if, ssid, pwd, ...)

连接路由器(同 连接 Wi-Fi)。

参数

返回值:成功返回 0;失败返回负值错误码

📌 日期结构体 utils_time_date_t 字段:ntp_year(年)、ntp_month(月)、ntp_date(日)、ntp_hour(时)、ntp_minute(分)、ntp_second(秒)、ntp_week_day(星期 1~7)、day_of_year(年内第几天)。


完整代码

以下为 sntp_demo/main.c 完整源码,与官方示例(applications/protocols/sntp_demo/sntp_demo/main.c)完全一致:

📜 点击展开 sntp_demo/main.c 完整代码
c

#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <string.h>
#include "blog.h"
#include <aos/yloop.h>
#include <aos/kernel.h>
#include <lwip/tcpip.h>
#include <wifi_mgmr_ext.h>
#include <hal_wifi.h>
#include <lwip/tcpip.h>
#include <sntp.h>
#include <utils_time.h>

#define ROUTER_SSID "ssid"
#define ROUTER_PWD "password"
#define sntp_server "ntp.aliyun.com"
#define UTC 8       //your Timezone, for example,beijing Timezone is GMT+8

static wifi_conf_t conf =
{
    .country_code = "CN",
};

static void wifi_sta_connect(char* ssid, char* password)
{
    wifi_interface_t wifi_interface;

    wifi_interface = wifi_mgmr_sta_enable();
    wifi_mgmr_sta_connect(wifi_interface, ssid, password, NULL, NULL, 0, 0);
}

void _startup_sntp(void *arg)
{
    blog_info("--------------------------------------- Start NTP now\r\n");
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, sntp_server);
    sntp_init();
    blog_info("--------------------------------------- Start NTP Done\r\n");
}

void sntp_task()
{
    tcpip_callback(_startup_sntp, NULL); 
    vTaskDelay(5000 / portTICK_PERIOD_MS);
    while(1)
    {
        uint32_t seconds = 0, frags = 0;
        utils_time_date_t date;
        sntp_get_time(&seconds, &frags);
        blog_info("[NTP] time is %lu:%lu\r\n", seconds, frags);
        utils_time_date_from_epoch(seconds+UTC*60*60, &date);
        blog_info("Date & time is: %u-%02u-%02u %02u:%02u:%02u (Day %u of week, Day %u of Year)\r\n",
            date.ntp_year,
            date.ntp_month,
            date.ntp_date,
            date.ntp_hour,
            date.ntp_minute,
            date.ntp_second,
            date.ntp_week_day,
            date.day_of_year
        );        
        vTaskDelay(1000 / portTICK_RATE_MS); 
    }
}

static void event_cb_wifi_event(input_event_t* event, void* private_data)
{
    static char* ssid;
    static char* password;

    switch (event->code)
    {
        case CODE_WIFI_ON_INIT_DONE:
        {
            blog_info("[APP] [EVT] INIT DONE %lld", aos_now_ms());
            wifi_mgmr_start_background(&conf);
        }
        break;
        case CODE_WIFI_ON_MGMR_DONE:
        {
            blog_info("[APP] [EVT] MGMR DONE %lld", aos_now_ms());
            //_connect_wifi();

            wifi_sta_connect(ROUTER_SSID, ROUTER_PWD);
        }
        break;
        case CODE_WIFI_ON_SCAN_DONE:
        {
            blog_info("[APP] [EVT] SCAN Done %lld", aos_now_ms());
            // wifi_mgmr_cli_scanlist();
        }
        break;
        case CODE_WIFI_ON_DISCONNECT:
        {
            blog_info("[APP] [EVT] disconnect %lld", aos_now_ms());
        }
        break;
        case CODE_WIFI_ON_CONNECTING:
        {
            blog_info("[APP] [EVT] Connecting %lld", aos_now_ms());
        }
        break;
        case CODE_WIFI_CMD_RECONNECT:
        {
            blog_info("[APP] [EVT] Reconnect %lld", aos_now_ms());
        }
        break;
        case CODE_WIFI_ON_CONNECTED:
        {
            blog_info("[APP] [EVT] connected %lld", aos_now_ms());
        }
        break;
        case CODE_WIFI_ON_PRE_GOT_IP:
        {
            blog_info("[APP] [EVT] connected %lld", aos_now_ms());
        }
        break;
        case CODE_WIFI_ON_GOT_IP:
        {
            blog_info("[APP] [EVT] GOT IP %lld", aos_now_ms());
            blog_info("[SYS] Memory left is %d Bytes", xPortGetFreeHeapSize());
            xTaskCreate(sntp_task, (char*)"sntp_task", 1024*4, NULL, 15, NULL);
        }
        break;
        case CODE_WIFI_ON_PROV_SSID:
        {
            blog_info("[APP] [EVT] [PROV] [SSID] %lld: %s",
                   aos_now_ms(),
                   event->value ? (const char*)event->value : "UNKNOWN");
            if (ssid)
            {
                vPortFree(ssid);
                ssid = NULL;
            }
            ssid = (char*)event->value;
        }
        break;
        case CODE_WIFI_ON_PROV_BSSID:
        {
            blog_info("[APP] [EVT] [PROV] [BSSID] %lld: %s",
                   aos_now_ms(),
                   event->value ? (const char*)event->value : "UNKNOWN");
            if (event->value)
            {
                vPortFree((void*)event->value);
            }
        }
        break;
        case CODE_WIFI_ON_PROV_PASSWD:
        {
            blog_info("[APP] [EVT] [PROV] [PASSWD] %lld: %s", aos_now_ms(),
                   event->value ? (const char*)event->value : "UNKNOWN");
            if (password)
            {
                vPortFree(password);
                password = NULL;
            }
            password = (char*)event->value;
        }
        break;
        case CODE_WIFI_ON_PROV_CONNECT:
        {
            blog_info("[APP] [EVT] [PROV] [CONNECT] %lld", aos_now_ms());
            blog_info("connecting to %s:%s...", ssid, password);
            wifi_sta_connect(ssid, password);
        }
        break;
        case CODE_WIFI_ON_PROV_DISCONNECT:
        {
            blog_info("[APP] [EVT] [PROV] [DISCONNECT] %lld", aos_now_ms());
        }
        break;
        default:
        {
            blog_info("[APP] [EVT] Unknown code %u, %lld", event->code, aos_now_ms());
            /*nothing*/
        }
    }
}

static void proc_main_entry(void* pvParameters)
{
    aos_register_event_filter(EV_WIFI, event_cb_wifi_event, NULL);
    hal_wifi_start_firmware_task();
    aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0);
    vTaskDelete(NULL);
}


void main()
{
    puts("[OS] Starting TCP/IP Stack...");
    tcpip_init(NULL, NULL);
    puts("[OS] proc_main_entry task...");
    xTaskCreate(proc_main_entry, (char*)"main_entry", 1024, NULL, 15, NULL);
}

常见问题与踩坑提示

⚠️ 时间一直停在 1970 年
原因:SNTP 未同步成功(服务器不可达、DNS 解析失败、路由器无法访问公网)
解决:确认能正常浏览网页;更换 NTP 服务器(ntp.ntsc.ac.cntime.windows.com);检查是否在 GOT_IP 之后才启动 SNTP

⚠️ 时间差 8 小时
原因UTC 时区宏未设置为 8(默认打印的是 UTC 时间)
解决#define UTC 8;其他国家/地区按当地时区修改

⚠️ 在事件回调中直接调用 sntp 相关 API 死机
原因:Wi-Fi 事件回调不在 lwIP 上下文,直接调用 lwIP API 不安全
解决:参照官方用 tcpip_callback(_startup_sntp, NULL) 包装启动逻辑

⚠️ 获取的时间有偏差(几十秒)
原因:网络延迟、服务器负载高
解决:SNTP 轮询会持续校正,等待数分钟后自然收敛;要求高精度请用 PTP 或更近的 NTP 服务器

⚠️ 一直打印 Connecting,始终没有 GOT IP(连不上路由器)
原因:SSID/密码填错、路由器是 5GHz、或信号太弱
解决:核对 sntp_demo/main.cROUTER_SSID/ROUTER_PWD 与路由器完全一致;确认路由器是 2.4GHz(开发板不支持 5GHz);把开发板靠近路由器再试

⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足可 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线

⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;换 USB 口或数据线重试

运行自检

串口每秒打印的北京时间与真实时间一致,即 SNTP 网络授时验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23