【ESP32-IDF】03-2 系统-系统时间

本文介绍了ESP32如何通过SNTP协议进行网络时间校正,获取精准的UTC时间,并设置系统时区。ESP32连接WiFi后,使用sntp库与NTP服务器交互,更新系统时间。此外,还详细阐述了时间戳、时间库中的数据类型及操作函数,如time(), localtime(), gmtime()等,展示了如何获取和设置系统时间。最后,讨论了设置时区的方法,确保时间显示正确。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系统时间

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 每周的第几天,星期一为第一天 (值从06,星期一为0)

  %U 第年的第几周,把星期日作为第一天(值从053)

  %V 每年的第几周,使用基于周的年

  %w 十进制表示的星期几(值从06,星期天为0)

  %W 每年的第几周,把星期一做为第一天(值从053)

  %x 标准的日期串

  %X 标准的时间串

  %y 不带世纪的十进制年份(值从099)

  %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

【3】 C程序中对时间的处理——time库函数详解

【4】lwIP-SNTP

【5】 ESP8266/ESP32 基础篇: 时间同步 SNTP 介绍和使用

### 配置和搭建 ESP32-IDF 开发环境 #### 工具准备 为了成功配置和搭建 ESP32 的开发环境,需要先准备好必要的工具链以及 SDK 文件。这些文件可以通过官方推荐的方式获取,例如通过 GitHub 或者 Gitee 来下载所需的资源[^1]。 #### 软件安装 在 Ubuntu 下,首先需要确保系统已经更新至最新版本,并且安装了一些基础依赖项。运行以下命令来完成基本设置: ```bash sudo apt update && sudo apt upgrade -y sudo apt install git wget flex bison gperf python3 python3-pip python-is-python3 libncurses-dev libreadline-dev gcc-multilib g++-multilib ``` 上述命令会安装 Git、Python 及其相关包管理器以及其他一些编译过程中可能需要用到的库文件。 #### ESP-IDF 安装 接着按照如下方法克隆 ESP-IDF 到本地目录中: ```bash cd ~/esp git clone --recursive https://github.com/espressif/esp-idf.git ``` 如果网络条件不允许访问国外站点,则可以选择国内镜像源如 Gitee 进行替代操作[^2]: ```bash cd ~/esp git clone --recursive https://gitee.com/espressif/esp-idf.git ``` #### 编译工具链安装 ESP-IDF 使用特定版本的 xtensa-lx106-elf 工具链来进行交叉编译工作。对于 Linux 用户来说可以直接利用脚本来自动完成这一过程: 进入 esp-idf 所处路径之后执行 setup.sh 命令即可一键部署所需的一切组件。 ```bash cd ~/esp/esp-idf ./install.sh ``` 此步骤将会花费一定时间去下载并构建整个工具链条目结构。 #### 设置环境变量 为了让每次打开终端都能加载正确的环境参数,在用户的 shell profile 中加入下面这段话(假设使用 bash): ```bash echo 'export PATH="$HOME/.espressif/python_env/idf4.4_py3.7_env/bin:$PATH"' >> ~/.bashrc echo 'export IDF_PATH=~/esp/esp-idf' >> ~/.bashrc source ~/.bashrc ``` 最后验证是否正确设置了 idf.py 和其他辅助程序的位置信息: ```bash idf.py --version ``` 以上即完成了基于 Ubuntu 平台上的完整 ESP32-IDF 开发环境建立流程说明。 #### 示例代码片段测试 创建一个新的项目用于确认当前环境能否正常运作: ```python # 创建新工程模板 mkdir hello_world_project cd hello_world_project/ cp -r $IDF_PATH/examples/get-started/hello_world/* . idf.py build idf.py flash monitor ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值