ESP32-IDF03-2 系统-系统时间
Posted Ciaran-byte
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP32-IDF03-2 系统-系统时间相关的知识,希望对你有一定的参考价值。
系统时间
1. 概述
我们有时候需要获取实时时间的需求,esp32内部提供了RTC组件,能够完成系统时间的获取。同时结合SNTP进行网络时间校正,可以获取精准的时间。
该部分使用的时候,一般是按照这样的思路,首先esp32连接好wifi以后,通过sntp网络时间库进行系统时间校正,然后通过RTC模块,设置好时区,可以对SNTP获得到的时间进行变换。然后对于得到的时间,可以通过time.h自带的一些时间格式转换函数,进行时间转换。
2. 网络时间校正
2.1 SNTP概述
网络时间校正涉及到了SNTP协议。SNTP协议是一种NTP协议的子集,能够使得网络内所有具有时钟的系统进行同步。通过向sntp服务器发送报文,即可得到需要的时间。
国内可用的NTP 服务器地址
-
1.cn.pool.ntp.org
-
2.cn.pool.ntp.org
-
3.cn.pool.ntp.org
-
0.cn.pool.ntp.org
-
cn.pool.ntp.org
-
tw.pool.ntp.org
-
0.tw.pool.ntp.org
-
1.tw.pool.ntp.org
-
2.tw.pool.ntp.org
-
3.tw.pool.ntp.org
esp32提供了sntp时钟同步的库,sntp获得的网络时间可以更新到esp32的系统时钟里面去
2.2 NTP时间戳
ntp时间戳是该协议的重要内容,是一个64位无符号浮点数组成,前面32位表示整数,后面32位表示小数。单位是秒。时间是相对于1900年1月0点开始到现在所经过的秒数。它能表示的最大数字是4,294,967,295秒
2.3 通过SNTP进行系统时间校正
esp32的SNTP库会在内部调用settimeofday() and adjtime()来自动更新esp32的系统时间。这些两个函数的功能是,当收到来自NTP服务器的信息后,修改esp32的系统时间。
2.3.1 思路
SNTP 协议是用来同步本地的时间到 unix 时间戳.通常嵌入式设备上电, 连接 AP(access point), 获取 IP 地址后, 就需要使用 SNTP 协议获取全球时间.以便于下一步的应用交互和使用.
SNTP工作原理比较简单, 通俗来说, 就是设备向 SNTP server 发送一包 SNTP 请求,服务器收到请求后回复一包 SNTP reply. 其中 SNTP reply 中就含有 unix 时间戳.
2.3.2 库函数
sntp_set_sync_mode()
功能:用于设置sntp的工作模式
- SNTP_SYNC_MODE_IMMED(default):收到sntp服务器响应后立即更新系统时间
- SNTP_SYNC_MODE_SMOOTH:通过使用函数adjtime()逐步减少时间误差,平滑地更新时间。
sntp_setservername
功能:用于设置sntp服务器的地址,除了可以使用域名,也可以使用ip地址。
说明:如果有必要,可以多设置几个sntp server,防止某个server暂时关闭,导致sntp不能正常运行。默认是不能。默认情况下esp32和esp8266只能开启一个sntp server,如果需要修改,在menuconfig中进行修改
sntp_init()
功能:开始进行sntp时钟同步,修改esp32的rtc时间
2.3.3 举例
下面举例说明使用sntp修改系统时间的
static void esp_initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP");
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "ntp1.aliyun.com");
sntp_init();
}
3. 获取系统时间
3.1 时钟源
esp32内置了系统时钟,能够进行实时时间更新。esp32提供了两种系统时钟源。时钟源设置可以在menuconfig中更改
- RTC timer: RTC时钟只有上电复位会清空系统设置,在低功耗模式下功耗低,但是低功耗模式下可能出现系统时间漂移
- High-resolution timer:高分辨率系统时钟定时器,在任何复位的时候都会清空系统时钟设置,在低功耗模式下不可用,但是更加精准
3.2时间库
通过c语言的time.h库,能够实现对系统时间的调用,以及格式上的修改。
3.2.1 数据类型
(1) time_t
typedef long time_t;
这个数据类型实际上是长整型
(2) timeval
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
其中,tv_sec为Epoch(1970-1-1零点零分)到创建struct timeval时的秒数,tv_usec为微秒数,即秒后面的零头。
(3) tm
tm是一个记录时间的结构体
struct tm
{
int tm_sec; /*代表目前秒数,正常范围为0-59,但允许至61秒 */
int tm_min; /*代表目前分数,范围0-59*/
int tm_hour; /* 从午夜算起的时数,范围为0-23 */
int tm_mday; /* 目前月份的日数,范围01-31 */
int tm_mon; /*代表目前月份,从一月算起,范围从0-11 */
int tm_year; /*从1900 年算起至今的年数*/
int tm_wday; /* 一星期的日数,从星期一算起,范围为0-6。*/
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /*日光节约时间的旗标DST. [-1/0/1]*/
};
3.2.2 操作函数
(1) time
原 型:time_t time(time_t * timer)
功 能: 获取当前的系统时间,返回的结果是一个time_t类型,其实就是一个大整数,其值表示从CUT(Coordinated Universal Time)时间1970年1月1日00:00:00(称为UNIX系统的Epoch时间)到当前时刻的秒数。然后调用localtime将time_t所表示的CUT时间转换为本地时间(我们是+8区,比CUT多8个小时)并转成struct tm类型,该类型的各数据成员分别表示年月日时分秒。
例程1
time函数获得日历时间。日历时间,是用“从一个标准时间点到此时的时间经过的秒数”来表示的时间。这个标准时间点对不同的编译器来说会有所不同,但对一个编译系统来说,这个标准时间点是不变的,该编译系统中的时间对应的日历时间都通过该标准时间点来衡量,所以可以说日历时间是“相对时间”,但是无论你在哪一个时区,在同一时刻对同一个标准时间点来说,日历时间都是一样的。
#include <time.h>
#include <stdio.h>
#include <dos.h>
int main(void)
{
time_t t; t = time(NULL);
printf("The number of seconds since January 1, 1970 is %ld",t);
return 0;
}
例程2
time函数可以作为随机数的种子
include <stdio.h>
#include <time.h>
#include<stdlib.h>
int main(void)
{
int i;
srand((unsigned) time(NULL));
printf("ten random numbers from 0 to 99\\n\\n");
for(i=0;i<10;i++)
printf("%d\\n",rand()%100);
return 0;
}
(2) gmtime()
原 型:struct tm *gmtime(long *clock);
功 能:把日期和时间转换为格林威治(GMT)时间的函数。将参数timep 所指的time_t 结构中的信息转换成真实世界所使用的时间日期表示方法,然后将结果由结构tm返回。
说 明:此函数返回的时间日期未经时区转换,而是UTC时间。
返回值:返回结构tm代表目前UTC 时间
例程
#include "stdio.h"
#include "time.h"
#include "stdlib.h"
int main(void)
{
time_t t;
struct tm *gmt, *area;
tzset(); /* tzset()设置时区*/
t = time(NULL);
area = localtime(&t);
printf("Local time is: %s", asctime(area));
gmt = gmtime(&t);
printf("GMT is: %s", asctime(gmt));
return 0;
}
(3) localtime()
功 能: 把从1970-1-1零点零分到当前时间系统所偏移的秒数时间转换为日历时间 。
说 明:此函数获得的tm结构体的时间,是已经进行过时区转化为本地时间。
用 法: struct tm *localtime(const time_t *clock);
返回值:返回指向tm 结构体的指针.tm结构体是time.h中定义的用于分别存储时间的各个量(年月日等)
的结构体.
例程1
#include <stdio.h>
#include <stddef.h>
#include <time.h>
int main(void)
{
time_t timer;//time_t就是long int 类型
struct tm *tblock;
timer = time(NULL);
tblock = localtime(&timer);
printf("Local time is: %s\\n",asctime(tblock));
return 0;
}
执行结果:
Local time is: Mon Feb 16 11:29:26 2009
例程2
上面的例子用了asctime函数,下面这个例子不使用这个函数一样能获取系统当前时间。
需要注意的是年份加上1900,月份加上1。
#include<time.h>
#include<stdio.h>
int main()
{
struct tm *t;
time_t tt;
time(&tt);
t=localtime(&tt);
printf("%4d年%02d月%02d日 %02d:%02d:%02d\\n",
t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
return 0;
}
(4) localtime与gmtime的区别
gmtime()函数功能类似获取当前系统时间,只是获取的时间未经过时区转换。
localtime函数获得的tm结构体的时间,是已经进行过时区转化为本地时间。
(5) localtime_r()和gmtime_r()
struct tm *gmtime_r(const time_t *timep, struct tm *result);
struct tm *localtime_r(const time_t *timep, struct tm *result);
gmtime_r()函数功能与此相同,但是它可以将数据存储到用户提供的结构体中。
localtime_r()函数功能与此相同,但是它可以将数据存储到用户提供的结构体中。它不需要设置tzname。
使用gmtime和localtime后要立即处理结果,否则返回的指针指向的内容可能会被覆盖。
一个好的方法是使用gmtime_r和localtime_r,由于使用了用户分配的内存,这两个函数是不会出错的。
(6) asctime()
功 能: 转换日期和时间为相应的字符串(英文简写形式,形如:Mon Feb 16 11:29:26 2009)
用 法: char *asctime(const struct tm *tblock);
(7) ctime()
功 能: 把日期和时间转换为字符串。(英文简写形式,形如:Mon Feb 16 11:29:26 2009)
用 法: char *ctime(const time_t *time);
说 明:ctime同asctime的区别在于,ctime是通过日历时间来生成时间字符串,而asctime是通过tm结构来生成时间字符串。
(8)mktime()
功能:将tm时间结构数据转换成经过的秒数(日历时间)。
原 型:time_t mktime(strcut tm * timeptr);。
说 明:mktime()用来将参数timeptr所指的tm结构数据转换成从公元1970年1月1日0时0分0秒算起至今的UTC时间所经过的秒数。
返回值:返回经过的秒数。
(9)difftime()
功 能:计算时间间隔才长度,以秒为单位,且只能精确到秒。
原 型:double difftime(time_t time1, time_t time0);
说 明:虽然该函数返回值是double类型的,但这并不说明该时间间隔具有同double一样的精度,这是由它的参数决定的。
(10) strftime()
功 能:将时间格式化,或者说:格式化一个时间字符串。我们可以使用strftime()函数将时间格式化为我们想要的格式。
原 型:size_t strftime(char *strDest,size_t maxsize,const char *format,const struct tm *timeptr);
参 数:我们可以根据format指向字符串中格式命令把timeptr中保存的时间信息放在strDest指向的字符串中,
最多向strDest中存放maxsize个字符。
返回值:该函数返回向strDest指向的字符串中放置的字符数。
类似于sprintf():识别以百分号(%)开始的格式命令集合,格式化输出结果放在一个字符串中。格式化命令说明串strDest中各种日期和时间信息的确切表示方法。格式串中的其他字符原样放进串中。格式命令列在下面,它们是区分大小写的。
%a 星期几的简写
%A 星期几的全称
%b 月份的简写
%B 月份的全称
%c 标准的日期的时间串
%C 年份的后两位数字
%d 十进制表示的每月的第几天
%D 月/天/年
%e 在两字符域中,十进制表示的每月的第几天
%F 年-月-日
%g 年份的后两位数字,使用基于周的年
%G 年份,使用基于周的年
%h 简写的月份名
%H 24小时制的小时
%I 12小时制的小时
%j 十进制表示的每年的第几天
%m 十进制表示的月份
%M 十时制表示的分钟数
%n 新行符
%p 本地的AM或PM的等价显示
%r 12小时的时间
%R 显示小时和分钟:hh:mm
%S 十进制的秒数
%t 水平制表符
%T 显示时分秒:hh:mm:ss
%u 每周的第几天,星期一为第一天 (值从0到6,星期一为0)
%U 第年的第几周,把星期日作为第一天(值从0到53)
%V 每年的第几周,使用基于周的年
%w 十进制表示的星期几(值从0到6,星期天为0)
%W 每年的第几周,把星期一做为第一天(值从0到53)
%x 标准的日期串
%X 标准的时间串
%y 不带世纪的十进制年份(值从0到99)
%Y 带世纪部分的十制年份
%z,%Z 时区名称,如果不能得到时区名称则返回空字符。
%% 百分号
提示:与 gmstrftime() 的行为相同,不同的是返回时间是本地时间。
3.3 设置时区
- setenv()
功能:设置环节变量TZ,TZ是与时区有关的一个字符串,如果需要了解具体时区怎么修改,需要看GNU libc documentation
- tzset()
功能:把c库的运行数据更改位新时区时间
一旦设置了正确的时区,调用localtime()的时候,就会返回考虑时区偏移和夏令时的时间
4. 具体使用
#include <stdio.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/apps/sntp.h"
#include "esp_log.h"
static const char *TAG = "sntp";
static void esp_initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP");
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "ntp1.aliyun.com");
sntp_init();
}
void esp_wait_sntp_sync(void)
{
char strftime_buf[64];
esp_initialize_sntp();
// wait for time to be set
time_t now = 0;
struct tm timeinfo = { 0 };
int retry = 0;
while (timeinfo.tm_year < (2019 - 1900)) {
ESP_LOGD(TAG, "Waiting for system time to be set... (%d)", ++retry);
vTaskDelay(100 / portTICK_PERIOD_MS);
time(&now);
localtime_r(&now, &timeinfo);
}
// set timezone to China Standard Time
setenv("TZ", "CST-8", 1);
tzset();
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
}
// 应用层直接调用 esp_wait_sntp_sync() 即可.
5. 参考资料
【1】SNTP简介
【2】System Time
【4】lwIP-SNTP
以上是关于ESP32-IDF03-2 系统-系统时间的主要内容,如果未能解决你的问题,请参考以下文章
ESP32 SDK 开发——ESP32/ESP-IDF环境搭建-linux