RTC 实时时钟

罗大富 BigRich大约 7 分钟ESP32Arduino

本节课来学习使用 ESP32 中的 RTC 模块。

实验原理

RTC 全称为实时时钟(Real-time Clock),是一种与 CPU 互不干扰,独立于 CPU 运行的计时设备。

RTC 主要用于在计算机系统关机时,保存计算机系统时钟,以便在下次计算机系统开机时能够从 RTC 中恢复出正确的时间。

RTC 的应用场景非常广泛,例如实现时间戳功能、自动唤醒、计时器等。在一些需要记录时间的项目中,RTC 可以作为重要的时间标记。

硬件电路设计

物料清单(BOM 表):

材料名称数量
带有 IIC 模块的 LCD1602 液晶屏1
杜邦线(跳线)若干

将材料按照下图相连:

注意

本实验需要用到 I2C 驱动代码,代码在 第 12 节课 中。

软件程序设计

学习 Arduino 中的 RTC 之前,我们还需要学习一下 C 语言中的 结构体

结构体(Struct) 也是一种数据类型,但与整数(int)、小数(float double)、字符(char)这些基本的数据类型不同的是,结构体是一种用于组织和存储数据的自定义数据类型,它允许你将不同数据类型的变量组合在一起,形成一个更复杂的数据结构。

在实际生活中,事物的属性远比基本的数据类型复杂多变,比如一个人的身份信息就包括名字、身份证号、年龄、身高、体重、联系方式等属性。在 C 和 C++ 中,结构体通常用于表示具有相关属性的数据集合。

结构体的定义如下:

struct Person {
    char name[50];
    int age;
    float height;
};

值得一提的是,结构体的定义只是定义了一种数据类型,也就是模板。我们可以根据这个模板创建出很多具体的结构变量,代码如下:

struct Person me = {
    "罗大富",
    29,
    180.5
}

这样,变量 me 就可以表示一个名字叫做罗大富、年龄 29、身高 180.5 的人了。

我们也可以使用 点运算符 . 加属性名的方式按照自己喜欢的顺序赋值,代码如下:

struct Person another = {
    .height = 149,
    .name = "如花",
    .age = 15
}

在实例化之后,我们可以使用 结构体变量名 + . + 属性名 的方式修改或者访问其中的值,例如:

// 访问结构体变量的成员
int age = me.age;
float height = another.height;
char name = me.name;

// 修改结构体变量的成员
me.age = 18;
me.height = 190.5;
me.name = "张三";

在 Arduino ESP32 核心支持库当中已经包含相关的获取时间的库,获取网络时间后,就可以不依赖网络,重复去获取时间,如果长时间运行,可以设置间隔时间同步 NTP 时间,只要访问本地时间的相关函数能正常调用,就没有问题。

使用读取本地时间,好处就是不需要频繁去获取 NTP 时间,占用网络资源,最大节省资源,适合低功耗下运行,保证时间运行准确。只要开机运行获取一次网络时间后,就可以关闭网络,后面读取本地时间,可以最大限度的不依赖网络来获取时间。

首先,我们需要使用 configTime 函数从网络时间服务器上获取并设置时间:

configTime(long gmtOffset_sec, 
        int daylightOffset_sec, 
        const char* server1, 
        const char* server2 = nullptr, 
        const char* server3 = nullptr)

其中参数的含义:

  • gmtOffset_sec 参数就是用来修正时区的,比如对于我们东八区(UTC/GMT+08:00)来说该参数就需要填写 8 * 3600
  • daylightOffset_sec 使用夏令时 daylightOffset_sec 就填写 3600,否则就填写 0;
  • server1, server2, server3: NTP 服务器的地址。可以提供多个服务器地址,以备备用,在这里我们使用阿里云 NTP 服务器open in new window就可以获取本地时间了

设置完成后就可以使用 getLocalTime 函数读取当前时间了:

bool getLocalTime(struct tm * info, uint32_t ms = 5000)

其中参数的含义:

  • info:是一个 struct tm 结构体对象,用于接收当前时间;
  • ms:操作超时时间,超时则返回 false;

struct tm 是 C/C++ 标准库 <time.h> 中定义的一个结构体,通常用于处理时间和日期的相关操作,比如获取当前时间、格式化时间、计算时间差等。它包含以下成员:

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,0 为周日,1 为周一,依此类推;
    int tm_yday; // 年中的日期,取值 0~365,0 代表 1 月 1 日,1 代表 1 月 2 日,依此类推;
    int tm_isdst; // 夏令时标识符,实行夏令时的时候,tm_isdst 为正;
    // 不实行夏令时的进候,tm_isdst 为 0;不了解情况时,tm_isdst() 为负
};

获取成功后芯片会使用 RTC 时钟保持时间的更新,这时候,就可以不依赖网络了,可以关闭网络,运行时读取本地同步过的时间并且进行格式化输出,输出方法很简单,代码如下:

// 定义时间信息
struct tm timeinfo;
  
Serial.println(&timeinfo);
// & 符号用于获取结构体 timeinfo 的地址。

提示

在 Arduino 的 Serial.println() 函数中,第一个参数是指向要打印的数据的指针,第二个参数是一个格式化字符串。

在这里,&timeinfo 获取了 timeinfo 结构体的地址,然后 Serial.println() 函数使用这个地址来访问 timeinfo 结构体的内容,按照指定的格式进行打印。

我们也可以通过格式化字符串方法对输出进行格式化:

  1. %a 星期几的简写
  2. %A 星期几的全称
  3. %b 月分的简写
  4. %B 月份的全称
  5. %c 标准的日期的时间串
  6. %C 年份的后两位数字
  7. %d 十进制表示的每月的第几天
  8. %D 月/天/年
  9. %e 在两字符域中,十进制表示的每月的第几天
  10. %F 年-月-日
  11. %g 年份的后两位数字,使用基于周的年
  12. %G 年分,使用基于周的年
  13. %h 简写的月份名
  14. %H 24 小时制的小时
  15. %I 12 小时制的小时
  16. %j 十进制表示的每年的第几天
  17. %m 十进制表示的月份
  18. %M 十时制表示的分钟数
  19. %p 本地的 AM 或 PM 的等价显示
  20. %r 12 小时的时间
  21. %R 显示小时和分钟:hh:mm
  22. %S 十进制的秒数
  23. %t 水平制表符
  24. %T 显示时分秒:hh:mm:ss
  25. %u 每周的第几天,星期一为第一天 (值从 0 到 6,星期一为 0)
  26. %U 第年的第几周,把星期日做为第一天(值从 0 到 53)
  27. %V 每年的第几周,使用基于周的年
  28. %w 十进制表示的星期几(值从 0 到 6,星期天为 0)
  29. %W 每年的第几周,把星期一做为第一天(值从 0 到 53)
  30. %x 标准的日期串
  31. %X 标准的时间串
  32. %y 不带世纪的十进制年份(值从 0 到 99)
  33. %Y 带世纪部分的十进制年份

因此,如果想要在 LCD 液晶屏上显示当前时间的话,代码如下:

#include <WiFi.h>
#include <LiquidCrystal_I2C.h>
#define NTP "ntp.aliyun.com"


// 初始化 LCD1602
LiquidCrystal_I2C lcd(0x27, 16, 2);

// 定义时间信息
struct tm timeinfo;

// WiFi 账号密码
const char *ssid = "你的 WiFi 名称";
const char *password = "你的 WiFi 密码";

// 连接 WiFi
void wifi_init(){
  // 设置 ESP32 工作模式
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("正在连接 WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);  
  }
  Serial.println("WiFi 连接成功");
}

// 设置并获取本地时间
void time_init() {
  // 获取本地时间
  if (!getLocalTime(&timeinfo)) {
    Serial.println("获取本地时间失败");
    // 连接 WiFi
    wifi_init();
    // 从 NTP 服务器获取时间并设置
    configTime(8*3600, 0, NTP); 
    return;
  }

  // 输出时间
//  Serial.println(timeinfo.tm_year)
  Serial.println(&timeinfo, "%F %A %T");

  // 清屏
  lcd.clear();
  // 显示内容
  lcd.setCursor(0, 0);
  lcd.print(&timeinfo, "%F");
  lcd.setCursor(13, 0);
  lcd.print(&timeinfo, "%a");
  lcd.setCursor(8,1);
  lcd.print(&timeinfo, "%T");
  
}


void setup() {
  Serial.begin(115200);
  wifi_init();
  // 初始化 LCD 对象
  lcd.init();
  lcd.backlight();

}

void loop() {
  time_init();
  delay(1000);
}
上次编辑于:
贡献者: Luo