第二十五章 RTC实验
本章将学习如何使用ESP32-P4的系统时间,实现一个简单的实时时钟。
本章分为如下几个小节:
25.1 RTC介绍
25.2 硬件设计
25.3 程序设计
25.4 下载验证
25.1 RTC介绍
RTC,Real Time Clock,实时时钟,专门用来记录时间的。
在ESP32-P4中,并没有像STM32芯片一样,具有RTC外设,但是存在一个系统时间,利用系统时间,也可以实现实时时钟的功能效果。
ESP32-P4使用两种硬件时钟源建立和保持系统时间。根据应用目的及对系统时间的精度要求,既可以仅使用其中一种时钟源,也可以同时使用两种时钟源。这两种硬件时钟源为RTC定时器和高分辨率定时器。
RTC定时器:RTC定时器在任何睡眠模式下及在任何复位后均可保持系统时间(上电复位除外,因为上电复位会重置RTC定时器)。时钟频率偏差取决于RTC定时器时钟源,该偏差只会在水面模式下影响时间精度。睡眠模式下,时间分辨率为6.667us。
高分辨率定时器:高分辨率定时器在睡眠模式下及在复位后不可用,但其时间精度更高。该定时器使用APB_CLK时钟源(通常为80MHz),时钟频率偏差小于±10ppm,时间分辨率为1us。
默认情况下,是使用这两种定时器。
25.2 硬件设计
25.2.1 例程功能
通过LCD屏实时显示RTC时间,包括年、月、日、时、分、秒等信息。
25.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD / MIPILCD(引脚太多,不罗列出来)
25.2.3 原理图
RTC使用ESP32-P4内部定时器实现,而定时器属于片上资源,因此没有对应的连接原理图。
25.3 程序设计
25.3.1 RTC的驱动接口
RTC数据的获取,可通过使用POSIX函数或使用标准C库函数。使用时间相关功能函数,必须先导入以下头文件:
#include
接下来,作者将介绍一些常用的函数,这些函数的描述及其作用如下:
1,获取时间函数time
该函数用于从系统中获取时间,其函数原型如下:
time_t time(time_t *_Time);
函数形参:

表25.3.1.1 time函数形参描述
函数返回值:
成功时,返回time_t类型的当前日历时间。错误时,返回-1。
注意:time_t类型最终是long int类型,这个会跟运行硬件平台相关,库中也有很多平台相关的编译开关。
2,得到struct tm格式及本地时间表达的日历时间函数localtime
该函数用于获取struct tm格式的时间,其函数原型如下:
struct tm localtime(const time_t *_Time);
函数形参:

表25.3.1.2 tm_localtime函数形参描述
函数返回值:
成功时为指向内部静态struct tm对象的指针,否则为NULL。
struct tm结构体其定义如下:
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到11 */
int tm_year; /* 自1900年起的年数 */
int tm_wday; /* 一周中的第几天,范围从0到6 */
int tm_yday; /* 一年中的第几天,范围从0到365 */
int tm_isdst; /* 夏令时 */
};
3,得到总秒数函数mktime
该函数得到总秒数,其函数原型如下:
time_t mktime(struct tm *_Tm);
函数形参:

表25.3.1.3 mktime函数形参描述
函数返回值:
返回自1970年1月1日以来持续时间的秒数,发生错误时返回-1。
4,设置当前时间函数settimeofday
该函数会把时间设置成由tv所指向的信息,当地时区信息则设置成tz,其函数原型如下:
int settimeofday(const struct timeval *tv, const struct timezone *tz);
函数形参:

表25.3.1.4 settimeofday函数形参描述
函数返回值:
成功返回0,失败返回-1。
tv是指向struct timeval结构体指针。struct timeval结构体中各个成员,如下代码所示:
struct timeval {
time_t tv_sec; /* 秒 */
suseconds_t tv_usec; /* 微妙 */
};
注意:获取时区tz信息依赖于系统,有的系统无法获取,所以一般调用时该函数第二个参数为NULL。对于ESP32-P4而言,该函数就是更新内部定时器时间。
25.3.2 程序流程图

图25.3.2.1 RTC实验程序流程图
25.3.3 程序解析
在16_rtc例程中,作者在16_rtc \components\BSP路径下新建了1个文件夹RTC,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1. RTC驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。RTC驱动源码包括两个文件:esp_rtc.c和esp_rtc.h。
下面先解析esp_rtc.h的程序。定义了一个_calendar_obj结构体类型,其余都是函数声明,代码如下:
/* 时间结构体, 包括年月日周时分秒等信息 */
typedef struct
{
uint8_t hour; /* 时 */
uint8_t min; /* 分 */
uint8_t sec; /* 秒 */
/* 公历年月日周 */
uint16_t year; /* 年 */
uint8_t month; /* 月 */
uint8_t date; /* 日 */
uint8_t week; /* 周 */
} _calendar_obj;
该结构体类型将会被用来存放时钟的年月日时分秒等信息。在esp_rtc.c文件中定义了_calendar_obj结构体类型的变量calendar,方便其他函数调用。
接下来,解析一下esp_rtc.c的程序,首先来看一下设置时间函数rtc_set_time,代码如下:
/**
* @brief RTC设置时间
* @param year :年
* @param mon :月
* @param mday :日
* @param hour :时
* @param min :分
* @param sec :秒
* @retval 无
*/
void rtc_set_time(int year, int mon, int mday, int hour, int min, int sec)
{
struct tm time_set; /* 存放时间的各个量(年月日时分秒)结构体 */
time_t second; /* 存放时间的总秒数 */
time_set.tm_year = year - 1900;
time_set.tm_mon = mon - 1;
time_set.tm_mday = mday;
time_set.tm_hour = hour;
time_set.tm_min = min;
time_set.tm_sec = sec;
time_set.tm_isdst = -1;
second = mktime(&time_set); /* 将时间转换为自1970年1月1日以来持续时间秒数 */
struct timeval val = {
.tv_sec = second,
.tv_usec = 0
};
settimeofday(&val, NULL); /* 设置当前时间 */
}
在rtc_set_time函数中,首先定义struct tm结构体类型的变量time_set,根据要设置的时间对其成员进行赋值,这里需要注意成员的取值范围,然后调用mktime函数将time_set存放的时间信息转换为总秒数,最后通过settimeofday函数设置当前时间。
接下来看一下获取当前时间函数rtc_get_time,代码如下
/**
* @brief 获取当前的时间
* @param 无
* @retval 无
*/
void rtc_get_time(void)
{
struct tm *time_block; /* 存放时间的各个量(年月日时分秒)结构体 */
time_t second; /* 存放时间的总秒数 */
time(&second); /* 获取当前的系统时间总秒数 */
time_block = localtime(&second); /* 把总秒数格式化存放到struct tm变量 */
/* 公历年 月 日 星期 时 分 秒 */
calendar.year = time_block->tm_year + 1900; /* 年(从1900开始) */
calendar.month = time_block->tm_mon + 1; /* 月(从0(1月)到11(12月)) */
calendar.date = time_block->tm_mday; /* 日(从1到31) */
calendar.week = time_block->tm_wday; /* 周几(从0(周日)到6(周六)) */
calendar.hour = time_block->tm_hour; /* 时(从0到23) */
calendar.min = time_block->tm_min; /* 分(从0到59) */
calendar.sec = time_block->tm_sec; /* 秒(从0到59) */
}
在rtc_get_time函数中,首先通过time函数获取当前系统时间换算成的总秒数,然后通过localtime函数得到struct tm结构体类型的数据,同时更新到calendar结构体变量中,方便其他函数调用。
2. CMakeLists.txt文件
本例程的功能实现主要依靠RTC驱动。要在main函数中,成功调用RTC文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
set(src_dirs
LED
LCD
RTC)
set(include_dirs
LED
LCD
RTC)
set(requires
driver
esp_lcd
esp_common
newlib)
idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
3. main.c驱动代码
在main.c里面编写如下代码。
/* 定义字符数组用于显示星期 */
char *weekdays[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday"};
void app_main(void)
{
esp_err_t ret;
uint8_t tbuf[40];
uint8_t t = 0;
ret = nvs_flash_init(); /* 初始化NVS */
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
led_init(); /* LED初始化 */
lcd_init(); /* LCD屏初始化 */
rtc_set_time(2025, 1, 1, 8, 8, 00); /* 设置RTC时间 */
lcd_show_string(10, 50, 200, 16, 16, "ESP32-P4", RED);
lcd_show_string(10, 70, 200, 16, 16, "RTC TEST", RED);
lcd_show_string(10, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
while (1)
{
t++;
if ((t % 10) == 0) /* 每100ms更新一次显示数据 */
{
rtc_get_time();
sprintf((char *)tbuf, "Time:%02d:%02d:%02d", calendar.hour,
calendar.min, calendar.sec);
lcd_show_string(10, 130, 210, 16, 16, (char *)tbuf,BLUE);
sprintf((char *)tbuf, "Date:%04d-%02d-%02d", calendar.year,
calendar.month, calendar.date);
lcd_show_string(10, 150, 210, 16, 16, (char *)tbuf,BLUE);
sprintf((char *)tbuf, "Week:%s", weekdays[calendar.week]);
lcd_show_string(10, 170, 210, 16, 16, (char *)tbuf,BLUE);
}
if ((t % 20) == 0)
{
LED0_TOGGLE();
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
在app_main函数中,首先通过rtc_set_time函数设置时间,然后在循环中,每隔10毫秒调用rtc_get_time函数,然后调用lcd_show_string函数对时间信息进行显示。LED0闪烁,以提示程序正在运行。
25.4 下载验证
将程序下载到开发板后,可以看到LED0不停闪烁,提示程序已经在运行了,可看到LCD显示的内容如图25.4.1所示:

图25.4.1 RTC实验程序运行效果图