SNTP的全称是Simple Network Time Protocol,意思是简单网络时间协议,用来从网络中获取当前的时间,也可以称为网络授时。项目中会使用LwIP SNTP模块从服务器(pool.ntp.org)获取时间
我们使用sntp例程,sntp例程路径为D:\Espressif\frameworks\esp-idf-v5.1.3\examples\protocols\sntp。复制到实验文件夹,路径为D:\esp32c3\sntp,不用修改名字,使用VSCode打开sntp文件夹,程序不需要修改,直接配置好串口、目标芯片,menuconfig就可以下载看结果,menuconfig中,除了把flash大小修改为8MB以外,还需要添加你要连接wifi的名称和密码。
添加好之后,保存关闭,编译下载到开发板,看终端结果
cs
I (10319) example: The current date/time in New York is: Wed Jan 31 06:54:54 2024
I (10319) example: The current date/time in Shanghai is: Wed Jan 31 19:54:54 2024
I (10329) example: Entering deep sleep for 10 seconds
只有一个c文件,点击打开main下面的sntp_example_main.c文件,可以先浏览一下这个c文件,一共有4个函数,分别是app_main()、obtain_time()、time_sync_notification_cb()、print_servers(),app_main()中调用了obtain_time()获取时间函数,obtain_time()函数中又调用了剩下的两个函数,后面两个函数只起到了终端打印信息提示作用,先看app_main函数,最开始的两行代码如下所示:
cs
++boot_count;
ESP_LOGI(TAG, "Boot count: %d", boot_count)
前面我们已经知道,程序每10秒钟会唤醒一次,每次唤醒后,boot_count值会加1,并使用ESP_LOGI把boot_count的值打印到终端。这里用来表示唤醒和重启的不同,如果是重启,boot_count的值永远都是1
cs
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
// Is time set? If not, tm_year will be (1970 - 1900).
if (timeinfo.tm_year < (2016 - 1900)) {
ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP.");
obtain_time();
// update 'now' variable with current time
time(&now);
}
1.time_t now定义一个64位变量now,使用time(&now)来获取系统当前时间,获取到的是一个64位的数字。
2.struct tm timeinfo定义一个结构体变量timeinfo,使用localtime_r(&now, &timeinfo)把64位的数字时间,各自提取到该结构体中的年月日时分秒等变量中。
cpp
struct tm {
int tm_sec; /* 秒 - 取值区间为[0,59] */
int tm_min; /* 分 - 取值区间为[0,59] */
int tm_hour; /* 时 - 取值区间为[0,23] */
int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
int tm_mon; /* 月份(从一月开始,0代表一月)- 取值区间为[0,11] */
int tm_year; /* 年份,其值等于实际年份减去1900 */
int tm_wday; /* 星期--取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 */
int tm_yday; /* 从每年的1月1日开始的天数 -- 取值区间为[0,365],其中0代表1月1日,以此类推 */
int tm_isdst; /*夏令时标识符,实行夏令时的时候,为1。不实行夏令时的进候,为0;不了解情况时,为-1。*/
};
使用上面提到的变量类型和函数,需要包含c语言的标准库函数<time.h>;
接下来,使用if判断结构体变量timeinfo中的成员tm_year的值。这条语句前面的注释,已经给到一个提示,如果tm_year的值没有设置过,将等于70。也就是说,如果开发板第一次执行这个程序,这个值将是70。如果现在是睡眠唤醒后执行到这里,今年是2024年,tm_year的值就是124,这个值需要加上1900才是当前的实际年份。obtain_time()函数用来从网络上获取当前时间。我们假设obtain_time函数已经执行完,继续看app_main函数。接下来的if条件编译没有在menuconfig中配置这个选项,所以不会执行。条件编译之后的语句如下所示:
cpp
char strftime_buf[64];
// Set timezone to Eastern Standard Time and print local time
setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in New York is: %s", strftime_buf);
// Set timezone to China Standard Time
setenv("TZ", "CST-8", 1);
tzset();
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
上面代码中,先定义一个strftime_buf字符数组,用来存放最后将要打印的字符。
接下来的两个片段,分别设置为纽约时间和上海时间。
setenv()和tzset()设置时区。
strftime()函数根据timeinfo中的成员值把日期时间组合成一串字符,存储到strftime_buf数组中。
最后通过ESP_LOGI打印出strftime_buf数组中的字符串。
接下来的if语句,也不会执行,因为我们没有在menuconfig中设置SNTP_SYNC_MODE_SMOOTH。
最后的3条语句,如下所示:
cpp
const int deep_sleep_sec = 10;
ESP_LOGI(TAG, "Entering deep sleep for %d seconds", deep_sleep_sec);
esp_deep_sleep(1000000LL * deep_sleep_sec);
esp_deep_sleep()函数把ESP32-C3设置为睡眠状态,10秒后自动唤醒。
以上就是app_main函数的执行流程。
接下来我们看obtain_time函数,看看时间是怎么获取到的。
obtain_time函数的前3条语句如下所示:
cpp
ESP_ERROR_CHECK( nvs_flash_init() );
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK( esp_event_loop_create_default() );
以上3条语句用来做连接wifi之前的准备工作。
之后有一个if条件编译,我们没有设置,也不会执行,这里直接跳过分析。
接下来的一条语句如下所示,用来连接wifi:
cpp
ESP_ERROR_CHECK(example_connect());
接下来的if条件编译,也不会执行,会执行它的else条件编译后的语句,用来打印信息。
cpp
ESP_LOGI(TAG, "Initializing and starting SNTP");
接下来的if条件编译,当服务器数量大于1才会执行,我们只设置了一个获取时间的服务器,所以也不会执行。会执行else条件编译后的语句:
cpp
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_SNTP_TIME_SERVER);
这条语句定义了一个esp_sntp_config_t类型变量config,并给该变量配置了默认值。
接下来定义了一个回调函数time_sync_notification_cb。
cpp
config.sync_cb = time_sync_notification_cb; // Note: This is only needed if we want
放到这个函数上面,单击右键选择"转到定义",函数原型如下所示:
cpp
void time_sync_notification_cb(struct timeval *tv)
{
ESP_LOGI(TAG, "Notification of a time synchronization event");
}
该函数的目的只是用来打印信息,提示发生时间同步事件。
点击软件最上面的向左的箭头←,回到obtain_time函数中刚才的位置。
接下来的if条件编译也不会执行。
再接下来的语句如下所示,初始化sntp。
cpp
esp_netif_sntp_init(&config);
再接下来执行打印服务器名称的函数,如下所示:
cpp
print_servers();
把鼠标放到这个函数上面单击右键,选择"转到定义",函数原型如下所示:
cpp
static void print_servers(void)
{
ESP_LOGI(TAG, "List of configured NTP servers:");
for (uint8_t i = 0; i < SNTP_MAX_SERVERS; ++i){
if (esp_sntp_getservername(i)){
ESP_LOGI(TAG, "server %d: %s", i, esp_sntp_getservername(i));
} else {
// we have either IPv4 or IPv6 address, let's print it
char buff[INET6_ADDRSTRLEN];
ip_addr_t const *ip = esp_sntp_getserver(i);
if (ipaddr_ntoa_r(ip, buff, INET6_ADDRSTRLEN) != NULL)
ESP_LOGI(TAG, "server %d: %s", i, buff);
}
}
}
在我们输出终端打印内容的下面两条就是该函数打印出的内容
cpp
I (6439) example: List of configured NTP servers:
I (6449) example: server 0: pool.ntp.org
点击软件最上面的向左的箭头←,回到obtain_time函数中刚才的位置。再接下来的几条语句如下所示:
cpp
// wait for time to be set
time_t now = 0;
struct tm timeinfo = { 0 };
int retry = 0;
const int retry_count = 15;
while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) {
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
}
time(&now);
localtime_r(&now, &timeinfo);
esp_netif_sntp_sync_wait()函数用来从服务器获取时间。间隔2秒获取一次,直到成功获取到时间,retry_count 定义了最多的获取次数。最后的两条语句如下所示:
cpp
ESP_ERROR_CHECK( example_disconnect() );
esp_netif_sntp_deinit();
example_disconnect()函数用来断开网络连接。
esp_netif_sntp_deinit()函数用来清除初始化sntp配置。