本章介绍 ESP32-S3 实时时钟(RTC)的使用,实时时钟能为系统提供一个准确的时间,即时系统复位或主电源断电, RTC 依然能够运行,因此 RTC 也经常用于各种低功耗场景。通过本章的学习,将学习到 RTC 的使用。
本章分为如下几个小节:
23.1 RTC 时钟简介
23.2 硬件设计
23.3 程序设计
23.4 下载验证

23.1 RTC 时钟简介

        ESP32 的 RTC 并非传统独立的硬件外设,而是通过芯片内部的​​系统时间​​功能,结合特定的硬件时钟源来实现实时时钟功能。
(1)硬件时钟源​​:ESP32 主要使用两种硬件时钟源来维持系统时间,默认情况下,是使用这两种定时器。下面将逐一介绍。

  • RTC 定时器​​:在​​深度睡眠模式​​及​​任何复位后​​(除上电复位外)均可保持系统时间。其精度取决于时钟源,在睡眠模式下时间分辨率可达 6.667μs;
  • 高分辨率定时器​​:提供更高的时间精度(分辨率 1μs,时钟偏差通常小于 ±10ppm),但在睡眠模式下及复位后不可用。

(2)依赖软件与电源​​:ESP32 的 RTC ​​需要软件配合​​读取和维护时间。更重要的是,由于其 RTC ​​没有专用的后备电池引脚​​,因此​​不支持掉电保存​​。一旦完全断电,时间信息将会丢失。

        下表总结了 ESP32 RTC 的主要特点、时钟源及其影响:

特性维度

说明

​​实现方式​​

通过芯片内部​​系统时间​​功能实现,而非独立硬件 RTC 外设 

​​硬件时钟源​​

​RTC 定时器​​: 睡眠模式下及任何复位后(除上电复位)均可保持时间,睡眠模式下分辨率 6.667μs 

​高分辨率定时器​​: 睡眠模式及复位后不可用,但精度更高(分辨率1μs) 

​​供电与保持​​

​无专用后备电池引脚​​,​​不支持掉电保存​​,完全断电后时间信息丢失 

​​主要用途​​

为深层睡眠和定时唤醒提供计时;通过网络同步后可作为本地实时时钟 

​​精度挑战​​

可能存在误差(如秒级别误差累积 ),​​建议定期通过网络同步​​(如 SNTP)进行校准 

表23.1.1 ESP32 RTC 的主要特点、时钟源及其影响

        注意事项:

  • 断电时间丢失​​:ESP32 的 RTC ​​无法在完全断电后保持时间​​。若需断电保持,需考虑外置RTC 芯片(如 DS3231)或通过非易失存储(如 NVS)定期保存时间,上电后读取恢复;
  • 精度与定期同步​​:内部 RTC 的精度可能受温度、电压等因素影响,可能存在误差(如有资料提及约每7.75小时可能有秒级误差)。​​对于要求长时间准确计时的应用,务必定期通过网络同步​​(SNTP)来校准;
  • 时区处理​​:处理时间时要注意时区。SNTP 获取的是 UTC 时间,需要根据所在时区转换为本地时间。一些库(如 ESP32Time)允许在构造函数中设置时区偏移;
  • 睡眠模式下的时间保持​​:在​​深度睡眠模式​​下,ESP32 的 ​​RTC 定时器仍然可以工作​​,因此可用于实现定时唤醒。但唤醒后若需准确时间,仍需评估累积误差或同步。
23.2 硬件设计

23.2.1 例程功能
        1. 通过 LCD 实时显示 RTC 时间
        2. LED 闪烁,指示程序正在运行
23.2.2 硬件资源
        1. LED
                LED0 - IO1
        2. XL9555
                IIC_SDA-IO41
                IIC_SCL-IO42
        3. SPILCD
                CS-IO21
                SCK-IO12
                SDA-IO11
                DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
                PWR- IO1_3(XL9555)
                RST- IO1_2(XL9555)
        4. RTC

23.2.3 原理图

        本章实验使用的 RTC 为 ESP32-S3 的片上资源,因此没有相应的连接原理图。

23.3 程序设计

23.3.1 程序流程图
        本实验的程序流程图:

图 23.3.1.1 RTC 实验程序流程图

23.3.2 RTC 函数解析
        由于 ESP32 并未给出 RTC 相关的 API 函数,因而在设计例程时调用了 C 库中的一些函数来配置 RTC 时钟,这些函数的描述及其作用如下:
(1)获取当前时间
        该函数用于获取当前时间,其函数原型如下所示:

struct tm *localtime(const time_t *timer);

该函数的形参描述,如下表所示:

形参 描述
timer 这是指向表示日历时间的 time_t 值的指针

表 23.3.2.1 函数 localtime ()形参描述

        返回值:无。
(2)设置当前时间
        该函数用于设置当前时间,其函数原型如下所示:

int settimeofday(const struct timeval *tv, const struct timezone *tz);

        该函数的形参描述,如下表所示:

形参 描述
tv 设置当前时间结构体
tz 设置时区信息

表 23.3.2.2 函数 settimeofday ()形参描述

        返回值:无。

23.3.3 RTC 驱动解析
        在 IDF 版的 13_rtc 例程中,作者在 13_rtc \components\BSP 路径下新增了一个 RTC 文件夹。分别用于存放 esp_rtc.c、 esp_rtc.h 两个文件。其中, esp_rtc.h 文件负责声明 RTC,而esp_rtc.c 文件则实现了 RTC 的驱动代码。下面将解析这两个文件的实现内容。
(1)esp_rtc.h 文件

/* 时间结构体, 包括年月日周时分秒等信息 */
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;

extern _calendar_obj calendar;      /* 时间结构体 */

/* 函数声明 */
void rtc_set_time(int year,int mon,int mday,int hour,int min,int sec);  /* 设置时间 */
void rtc_get_time(void);                                                /* 获取时间 */
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day);        /* 获取周几 */

(2)esp_rtc.c 文件

_calendar_obj calendar;         /* 时间结构体 */

/**
 * @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 datetime;
    /* 设置时间 */
    datetime.tm_year = year - 1900;
    datetime.tm_mon = mon - 1;
    datetime.tm_mday = mday;
    datetime.tm_hour = hour;
    datetime.tm_min = min;
    datetime.tm_sec = sec;
    datetime.tm_isdst = -1;
    /* 获取1970.1.1以来的总秒数 */
    time_t second = mktime(&datetime);
    struct timeval val = { .tv_sec = second, .tv_usec = 0 };
    /* 设置当前时间 */
    settimeofday(&val, NULL);
}

/**
 * @brief       获取当前的时间
 * @param       无
 * @retval      无
 */
void rtc_get_time(void)
{
    struct tm *datetime;
    time_t second;
    /* 返回自(1970.1.1 00:00:00 UTC)经过的时间(秒) */
    time(&second);
    datetime = localtime(&second);

    calendar.hour = datetime->tm_hour;          /* 时 */
    calendar.min = datetime->tm_min;            /* 分 */
    calendar.sec = datetime->tm_sec;            /* 秒 */
    /* 公历年月日周 */
    calendar.year = datetime->tm_year + 1900;   /* 年 */
    calendar.month = datetime->tm_mon + 1;      /* 月 */
    calendar.date = datetime->tm_mday;          /* 日 */
    /* 周 */
    calendar.week = rtc_get_week(calendar.year, calendar.month, calendar.date);
}

/**
 * @brief       将年月日时分秒转换成秒钟数
 *   @note      输入公历日期得到星期(起始时间为: 公元0年3月1日开始, 输入往后的任何日期, 都可以获取正确的星期)
 *              使用 基姆拉尔森计算公式 计算, 原理说明见此贴:
 *              https://www.cnblogs.com/fengbohello/p/3264300.html
 * @param       syear : 年份
 * @param       smon  : 月份
 * @param       sday  : 日期
 * @retval      0, 星期天; 1 ~ 6: 星期一 ~ 星期六
 */
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day)
{
    uint8_t week = 0;

    if (month < 3)
    {
        month += 12;
        --year;
    }

    week = (day + 1 + 2 * month + 3 * (month + 1) / 5 + year + (year >> 2) - year / 100 + year / 400) % 7;
    return week;
}

23.3.4 CMakeLists.txt 文件

        打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirs
            IIC
            LCD
            LED
            SPI
            RTC
            XL9555)

set(include_dirs
            IIC
            LCD
            LED
            SPI
            RTC
            XL9555)

set(requires
            driver
            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)

23.3.5 实验应用代码

        打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。

i2c_obj_t i2c0_master;

/* 定义字符数组用于显示周 */
char* weekdays[]={"Sunday","Monday","Tuesday","Wednesday",
                  "Thursday","Friday","Saterday"};

/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
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());
        ret = nvs_flash_init();
    }

    led_init();                         /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);  /* 初始化IIC0 */
    spi2_init();                        /* 初始化SPI2 */
    xl9555_init(i2c0_master);           /* IO扩展芯片初始化 */
    lcd_init();                         /* 初始化LCD */
    rtc_set_time(2023,8,26,00,00,00);   /* 设置RTC时间 */

    lcd_show_string(10, 40, 240, 32, 32, "ESP32",RED);
    lcd_show_string(10, 80, 240, 24, 24, "RTC Test",RED);
    lcd_show_string(10, 110, 240, 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);
            printf("Time:%02d:%02d:%02d\r\n", 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);
            printf("Date:%02d-%02d-%02d\r\n",  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 - 1]);
            lcd_show_string(10, 170, 210, 16, 16, (char *)tbuf,BLUE);
        }

        if ((t % 20) == 0)
        {
            LED_TOGGLE();               /* 每200ms,翻转一次LED */
        }

        vTaskDelay(10);
    }
}

        从上面的代码中可以看到,在初始化完 RTC 后便每间隔 100 毫秒获取一次 RTC 的时间和日期,并在 LCD 上进行显示。

23.4 下载验证

        在完成编译和烧录操作后,可以看到LCD上实时地显示着RTC的时间,并且可以看到LED在 RTC 周期性唤醒的驱动下以 0.5Hz 的频率闪烁着。

图 23.3.1 SPI LCD 显示效果图

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐