SNPT(Simple Network Time Protocal简单网络时间协议)用于跨广域网或局域网时间同步的协议,具有较高的精确度(几十毫秒)。SNTP是NTP协议的简化版
SNTP协议采用客户端/服务器的工作方式,可以采用单播(点对点)或者广播(一点对多点)模式操作。
单播模式下,客户端能够通过定期访问SNTP服务器来获取精确的时间信息,用于调整客户端自身的系统时间。
广播模式下。SNTP服务器周期性地发送消息给指定的IP广播地址或IP多播地址。SNTP客户端通过监听这些地址来获取时间信息。
网络中一般存在多台SNTP服务器,客户端会通过一定的算法选择最好的几台服务器使用。如果一台SNTP服务器在工作过程中发生异常,则会通知SNTP客户端,那么SNTP客户端就会丢弃发生故障的SNTP服务器发给它的时间信息,然后重新选择其他的SNTP服务器。
SNTP协议主要是通过记录客户端向服务器发送数据包时的时间戳 t1,服务器端接收到该数据包时的时间戳 t2,服务器向客户端回应的时间戳 t3和最后客户端接收到服务器回应时的时间戳 t4来计算客户端时间和服务端时间的偏差,从而进行校准时间操作,如下图所示:
则 t1 和 t2之间的时间差为((t2 - t1) + (t4 - t3)) / 2
数据包在网络上传输的时间为(t2 - t1) + (t4 - t3)
获取到时间差后,设备会校准RTC时间,来保证时间的正确性。
设置操作模式
函数原型:
void sntp_setoperatingmode(u8_t operating_mode);
参数:
operating_mode 操作模式
SNTP_OPMODE_POLL //单播模式
SNTP_OPMODE_LISTENONLY //组播模式
返回值:
无
实例:
sntp_setoperatingmode(SNTP_OPMODE_POLL); //设置为单播模式
设置要连接的服务器主机名字
函数原型:
void sntp_setservername(u8_t idx, const char *server)
{
LWIP_ASSERT_CORE_LOCKED();
if (idx < SNTP_MAX_SERVERS) {
sntp_servers[idx].name = server;
}
}
参数:
idx 服务器编号。
server 服务器名字。
可以看到,服务器可以设置多个,而数量的大小取决于SNTP_MAX_SERVERS参数。这个值可以根据需求自行设置。
返回值:
无
实例:
sntp_setservername(0, "1.cn.pool.ntp.org");
sntp_setservername(1, "1.hk.pool.ntp.org");
设置要连接的服务器主机IP
函数原型:
void sntp_setserver(u8_t idx, const ip_addr_t *server)
{
LWIP_ASSERT_CORE_LOCKED();
if (idx < SNTP_MAX_SERVERS) {
if (server != NULL) {
sntp_servers[idx].addr = (*server);
} else {
ip_addr_set_zero(&sntp_servers[idx].addr);
}
#if SNTP_SERVER_DNS
sntp_servers[idx].name = NULL;
#endif
}
}
参数:
idx 服务器编号。
server 服务器IP地址。
返回值:
无
实例:
struct ip4_addr test_addr;
IP4_ADDR(&test_addr, 213, 161, 194, 93);
sntp_setserver(0, (const ip_addr_t *)(&test_addr));
IP4_ADDR(&test_addr, 129, 6, 15, 29);
sntp_setserver(1, (const ip_addr_t *)(&test_addr));
初始化SNTP模块,并创建UDP连接。
函数原型:
void sntp_init(void)
{
#ifdef SNTP_SERVER_ADDRESS
#if SNTP_SERVER_DNS
sntp_setservername(0, SNTP_SERVER_ADDRESS);
#else
#error SNTP_SERVER_ADDRESS string not supported SNTP_SERVER_DNS==0
#endif
#endif /* SNTP_SERVER_ADDRESS */
if (sntp_pcb == NULL) {
sntp_pcb = udp_new_ip_type(IPADDR_TYPE_ANY); //创建一个UDP PCB(协议控制块),IPV4+IPV6
LWIP_ASSERT("Failed to allocate udp pcb for sntp client", sntp_pcb != NULL);
if (sntp_pcb != NULL) {
udp_recv(sntp_pcb, sntp_recv, NULL); //设置udp接收回调
if (sntp_opmode == SNTP_OPMODE_POLL) { //是单播模式
SNTP_RESET_RETRY_TIMEOUT();
#if SNTP_STARTUP_DELAY
sys_timeout((u32_t)SNTP_STARTUP_DELAY_FUNC, sntp_request, NULL);
#else
sntp_request(NULL); //请求
#endif
} else if (sntp_opmode == SNTP_OPMODE_LISTENONLY) { //广播模式
ip_set_option(sntp_pcb, SOF_BROADCAST);
udp_bind(sntp_pcb, IP_ANY_TYPE, SNTP_PORT); //绑定
}
}
}
}
参数:
无
返回值:
无
创建一个针对IP类型的PCB(Protocol Control Block协议控制块)
函数原型:
struct udp_pcb *udp_new_ip_type(u8_t type)
{
struct udp_pcb *pcb;
LWIP_ASSERT_CORE_LOCKED();
pcb = udp_new(); //创建一个UDP
#if LWIP_IPV4 && LWIP_IPV6
if (pcb != NULL) {
IP_SET_TYPE_VAL(pcb->local_ip, type);
IP_SET_TYPE_VAL(pcb->remote_ip, type);
}
#else
LWIP_UNUSED_ARG(type);
#endif /* LWIP_IPV4 && LWIP_IPV6 */
return pcb;
}
参数:
type IP地址类型,类型如下
enum lwip_ip_addr_type {
/** IPv4 */
IPADDR_TYPE_V4 = 0U,
/** IPv6 */
IPADDR_TYPE_V6 = 6U,
/** IPv4+IPv6 ("dual-stack") */
IPADDR_TYPE_ANY = 46U
};
返回值:
NULL失败
其他值 UDP的协议控制块
实例:
static struct udp_pcb* sntp_pcb;
sntp_pcb = udp_new_ip_type(IPADDR_TYPE_ANY);
if (sntp_pcb != NULL) {
//成功
}
设置一个接收回调,从UDP的协议控制块中
函数原型:
void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg)
{
LWIP_ASSERT_CORE_LOCKED();
LWIP_ERROR("udp_recv: invalid pcb", pcb != NULL, return);
/* remember recv() callback and user data */
pcb->recv = recv;
pcb->recv_arg = recv_arg;
}
参数:
pcb UDP协议控制块,由udp_new_ip_type返回所得。
recv UDP接收的回调函数、
recv_arg UDP接收的回调函数的参数
返回值:
无
实例:
udp_recv(sntp_pcb, sntp_recv, NULL);
发送SNTP请求
函数原型:
static void sntp_request(void *arg)
{
ip_addr_t sntp_server_address;
err_t err;
LWIP_UNUSED_ARG(arg);
/* initialize SNTP server address */
#if SNTP_SERVER_DNS
if (sntp_servers[sntp_current_server].name) {
/* always resolve the name and rely on dns-internal caching & timeout */
ip_addr_set_zero(&sntp_servers[sntp_current_server].addr);
err = dns_gethostbyname(sntp_servers[sntp_current_server].name, &sntp_server_address,
sntp_dns_found, NULL);
if (err == ERR_INPROGRESS) {
/* DNS request sent, wait for sntp_dns_found being called */
LWIP_DEBUGF(SNTP_DEBUG_STATE, ("sntp_request: Waiting for server address to be resolved.\n"));
return;
} else if (err == ERR_OK) {
sntp_servers[sntp_current_server].addr = sntp_server_address;
}
} else
#endif /* SNTP_SERVER_DNS */
{
sntp_server_address = sntp_servers[sntp_current_server].addr;
err = (ip_addr_isany_val(sntp_server_address)) ? ERR_ARG : ERR_OK;
}
if (err == ERR_OK) {
LWIP_DEBUGF(SNTP_DEBUG_TRACE, ("sntp_request: current server address is %s\n",
ipaddr_ntoa(&sntp_server_address)));
sntp_send_request(&sntp_server_address);
} else {
/* address conversion failed, try another server */
LWIP_DEBUGF(SNTP_DEBUG_WARN_STATE, ("sntp_request: Invalid server address, trying next server.\n"));
sys_timeout((u32_t)SNTP_RETRY_TIMEOUT, sntp_try_next_server, NULL);
}
}
参数:
可以忽略
返回值:
无
实例:
sntp_request(NULL); //请求
UDP从SNTP的PCB的接收回调
函数原型:
static void sntp_recv(void *arg, struct udp_pcb* pcb, struct pbuf *p, const ip_addr_t *addr,u16_t port)
参数:
arg:接收的数据传参
pcb:协议控制块
p:接收到的数据
addr:IP地址
Port:端口
返回值:
无
整个函数中,需要注意的是sntp_process函数,该函数相当于对接收到的数据进行数据处理。
对接收到的时间数据进行处理
函数原型:
static void sntp_process(const struct sntp_timestamps *timestamps)
{
s32_t sec;
u32_t frac;
sec = (s32_t)lwip_ntohl(timestamps->xmit.sec);
frac = lwip_ntohl(timestamps->xmit.frac);
#if SNTP_COMP_ROUNDTRIP
# if SNTP_CHECK_RESPONSE >= 2
if (timestamps->recv.sec != 0 || timestamps->recv.frac != 0)
# endif
{
LOGI("Roundtrip compare processing...\n");
s32_t dest_sec;
u32_t dest_frac;
u32_t step_sec;
SNTP_GET_SYSTEM_TIME_NTP(dest_sec, dest_frac);
step_sec =
(dest_sec < sec) ? ((u32_t)sec - (u32_t)dest_sec) : ((u32_t)dest_sec - (u32_t)sec);
/* In order to avoid overflows, skip the compensation if the clock step
* is larger than about 34 years. */
if ((step_sec >> 30) == 0) {
s64_t t1, t2, t3, t4;
/* t4 the time sntp client recv the reply, or the current time. */
t4 = SNTP_SEC_FRAC_TO_S64(dest_sec, dest_frac);
/* t3 the time sntp server transmitt the reply. */
t3 = SNTP_SEC_FRAC_TO_S64(sec, frac);
/* t1 the time sntp client send a request. */
t1 = SNTP_TIMESTAMP_TO_S64(timestamps->orig);
/* t2 the time sntp server recv the request. */
t2 = SNTP_TIMESTAMP_TO_S64(timestamps->recv);
/* Clock offset calculation according to RFC 4330 */
t4 += ((t2 - t1) + (t3 - t4)) / 2;
sec = (s32_t)((u64_t)t4 >> 32);
frac = (u32_t)((u64_t)t4);
}
}
#endif /* SNTP_COMP_ROUNDTRIP */
time_t tim = (u32_t)((sec) + DIFF_SEC_1970_2036);
/* change system time and/or the update the RTC clock */
SNTP_SET_SYSTEM_TIME(sec);
/* display local time from GMT time */
//LWIP_DEBUGF(SNTP_DEBUG_TRACE, ("sntp_process: %s", ctime(&tim)));
LOGI("sntp_process: %s", ctime(&tim));
LWIP_UNUSED_ARG(tim);
}
参数:
timestamps 时间戳,结构体类型如下:
struct sntp_timestamps
{
#if SNTP_COMP_ROUNDTRIP || SNTP_CHECK_RESPONSE >= 2
struct sntp_time orig; //发送前时间戳
struct sntp_time recv; //接收到时间戳
#endif
struct sntp_time xmit; //传输
};
sntp_time类型如下:
struct sntp_time
{
u32_t sec; //秒
u32_t frac; //毫秒
};
返回值:
无
从上述代码中可以看到,实际上正如在文章开头所讲的那样,SNTP是根据时间差来进行校准时间,计算后得到结果后,需要关注的函数是SNTP_SET_SYSTEM_TIME,后续的数据处理,都在该函数中进行。
#define SNTP_SET_SYSTEM_TIME(sec) sntp_set_system_time((u32_t)((sec)+DIFF_SEC_1970_2036),0)
sntp_set_system_time()
sntp根据计算得到的结果来设置系统时间。
函数原型:
static void sntp_set_system_time(time_t t, u32_t us)
{
struct tm *gt = NULL;
hal_rtc_time_t r_time;
hal_rtc_status_t st = HAL_RTC_STATUS_OK;
LOGI("sntp_set_system_time input: %"U32_F" s,%"U32_F" us\n",t,us);
gt = gmtime(&t);
if (gt == NULL) {
gt = localtime(&t);
}
r_time.rtc_year = (gt->tm_year % 100);
r_time.rtc_mon = gt->tm_mon + 1;
r_time.rtc_day = gt->tm_mday;
r_time.rtc_week = gt->tm_wday;
r_time.rtc_hour = gt->tm_hour;
r_time.rtc_min = gt->tm_min;
r_time.rtc_sec = gt->tm_sec;
st = hal_rtc_set_time(&r_time);
if (sntp_cb) {
LOGI("sntp callback\n");
sntp_cb(r_time);
}
LOGI("sntp(%d %d %d ",gt->tm_wday,gt->tm_mon,gt->tm_mday); LOGI("%d:%d:%d %d)\n",gt->tm_hour,gt->tm_min,gt->tm_sec,gt->tm_year); LOGI("sntp(atx):(%d:%d)\n",gt->tm_isdst,gt->tm_yday); LOGI("sntp st1(%u)\n",st);
st = hal_rtc_get_time(&r_time);
(void)st;
LOGI("sntp(%u %u %u ",r_time.rtc_week,r_time.rtc_mon,r_time.rtc_day); LOGI("%u:%u:%u %u)\n",r_time.rtc_hour,r_time.rtc_min,r_time.rtc_sec,r_time.rtc_year); LOGI("sntp st2(%u)\n",st);
}
参数:
t 时间戳
us 微秒
返回值:
无
该函数将数据进行划分计算,得到最终需要的数据结果,然后设置本地的RTC时间。设置完成后,调用sntp_cb函数来进行用户自己的操作。
设置用户自定义的回调结果
函数原型:
void sntp_set_callback(sntp_callback callback)
{
sntp_cb = callback;
}
参数:
callback 用户自定义回调结果
返回值:
无