这篇文章来源于DevicePlus.com英语网站的翻译稿。
本文最初发布在deviceplus.jp网站上,而后被翻译成英语。

目录

  • 前言
  • 关于ESP32闹钟
  • 创建日期和时间的“RTC”
  • 在ESP32 LCD上显示日期和时间
  • 使用MP3模块发出闹铃响声
  • 给闹钟主机接线
  • 给ESP32闹钟分机接线
  • 安装库和字体文件
  • Arduino IDE闹钟程序
  • 分机程序
  • 确认运行情况
  • 结论
  • 相关文章

前言
您每天早上被什么闹钟吵醒?早上人们的一个常见问题是关掉闹钟并直接回去睡觉。
这一次,我们决定使用ESP32制作一个“唤醒闹钟”,应该有助于解决这个问题。
设计步骤预计完成时间:120分钟
名称卖方价格
ESP32-DevKitC(2 件)贸泽电子约10.00美元
DS3231 模块亚马逊约3.00美元
LIR2032(纽扣充电电池)亚马逊约3.60美元
扬声器亚马逊约1.00~3.00美元
2.8英寸SPI连接器 320×240像素LCD屏幕亚马逊约15.00美元
*除上述物品外,还需要一个轻触开关、一个LED和一个约100Ω的电阻器。
wake-up-alarm-ESP32-1.png

关于ESP32闹钟
我们来使用ESP32完成一个目标吧:像普通闹钟一样在屏幕上显示日期和时间,并在指定的时间响起。在设计制作这款“唤醒闹钟”时,我们需要将分机按钮(用于关闭闹钟)与闹钟主机分开放置。之所以这样设计,是因为想要您必须起床并走到分机按钮处才能将闹钟关掉。
如下面的视频所示,使用两个ESP,一个用于主机,另一个用于分机。在视频中,主机和分机是挨着的,但如果通过Wi-Fi连接,它们是可以在Wi-Fi范围内分开放置的。

将主机和分机分开放置时,请使用ESP32的Wi-Fi功能。两个ESP32作为Web服务器和客户端运行,并相互通信,如图1所示。
wake-up-alarm-ESP32-2.jpg
图 1  主机和分机之间的通信


创建日期和时间的“RTC”
对于Arduino等微控制器来说,通常能够获取在启动程序后经过的时间。但是,您获得的时间通常不是很准确,因为断电时会重置经过时间。
而如果使用ESP32,则可以通过Wi-Fi连接到互联网,以定期从互联网上的NTP服务器获取日期和时间,并将其设置到ESP32上。但Arduino Uno等部分微控制器没有互联网连接功能,因此,拥有一种可以更轻松地处理日期和时间的机制会很方便。
这就是为什么经常使用一种被称作“RTC”(实时时钟)的IC。 RTC是基于周期性发出信号的元件来计时的IC。此外,通过将其连接到电池等外部电源,即使在微控制器断电时也可以继续计时。
许多产品都采用RTC,此次,我们将使用一种名为“DS3231”的RTC模块。
在电子设计所用的RTC模块当中,DS3231模块很受欢迎,且很容易获得。由于接口是I2C,因此只需要4根线。除了RTC功能,还具有温度传感器功能(不过本文不会用到温度传感器)。
此外,在照片1所示的DS3231上,安装一个名为“LIR2032”的纽扣电池,这样即使在微控制器关闭的情况下也能继续记录日期和时间。LIR2032的电池尺寸与CR2032的相同,但不同的是它可充电。
wake-up-alarm-ESP32-3.jpg
照片1  DS3231模块


在ESP32 LCD上显示日期和时间
由于闹钟用于查看当前日期和时间,因此需要以易于理解的方式显示日期和时间。以下设备用于显示日期和时间。

  • 7段LED
  • LED 矩阵
  • OLED 显示器
  • 字符液晶显示器
  • 图形液晶显示器
根据设备的不同,有不同的库和不同的编程方法。这还取决于它是否适合您想要制作的作品。例如,7段LED仅适合以低成本显示数字,但不适合显示详情。其中,图形液晶显示器是最通用的,可以用于许多不同的项目,所以我这次决定使用它。
市面上有各种类型的液晶显示器模块,但对于今天的项目,我们将使用一个名为“ILI9341”的控制器,并使用SPI接口(照片2)。此外,液晶显示器通常以2.2英寸/2.4英寸/2.8英寸尺寸出售,因此,请根据您正在做的作品类型进行相应调整。
wake-up-alarm-ESP32-4.jpg
照片2  控制器用ILI9341 2.8英寸液晶显示器


使用MP3模块发出闹钟铃声
既然是闹钟,在指定时间发出铃声是必需的。您可以将蜂鸣器连接到ESP32以产生单一铃声,但如果您愿意,也可以使用自己喜欢的铃声。为此,我们将使用一个名为“DFPlayer Mini”的模块,它可以播放任何MP3数据(照片3)。
DFPlayer Mini是一个可以通过串口发送命令来播放microSD卡中MP3的模块。可以将一个小型扬声器连接到扬声器输出引脚以产生铃声。
wake-up-alarm-ESP32-5.jpg
照片3 DFPlayer Mini


给闹钟主机接线
让我们进入实际构建吧。首先,给闹钟接线。
使用两个面包板,一个配有ESP32和DS3231,另一个配有液晶显示器(LCD)和DFPlayer Mini。各部件接线如图2所示。
由于ESP32很宽,您只能在普通面包板的一面放一根跳线。因此,请改用电源线只在一侧、多一排插孔的面包板(例如Sanhayato的SAD-101)。
ESP32和液晶显示器通过SPI进行连接。ESP32可以使用两个 SPI(VSPI和HSPI),但使用的是VSPI(引脚18/19/23)(表1)。
ESP32和DS3231通过I2C进行连接。在ESP32中,I2C可以分配给任何引脚,但我们使用标准引脚(SDA=21和SCL=22)(表 2)。
DFPlayer Mini 进行串行连接。ESP32可以使用3个串口,但为此请同时使用引脚16和引脚17(表3)。此外,将扬声器连接到DFPlayer Mini“SPK1”和“SPK2”两个引脚。
wake-up-alarm-ESP32-6.jpg
图2 闹钟主机接线

ESP32 引脚液晶显示器引脚
5VVCC
GNDGND
5CS
4RESET
2DC
23MOSI
18SCK
19MISO
表 1:ESP32 与液晶显示器的连接

ESP32 引脚DS3231 引脚
5VVCC
GNDGND
21SDA
22SCL
表 2:ESP32与DS3231的连接

ESP32 引脚DFPlayer Mini 引脚
5VVCC
GNDGND
16TX
17RX
表 3:ESP32与DFPlayer Mini的连接

给ESP32闹钟分机接线
接下来,我们将给分机接线。分机接线应按图3所示进行。您所要做的就是将开关和 LED连接到ESP32。将开关的一侧连接到ESP32的3V3引脚,另一侧连接到引脚4。通过电阻器→LED再通过ESP32的引脚13连接到GND。
在读取开关状态的电路上插入一个上拉电阻或下拉电阻。但是,由于ESP32可以通过内部电阻进行上拉/下拉,因此省略了外部电阻。
wake-up-alarm-ESP32-7.jpg
图3 分机接线


安装库和字体文件
完成接线工作后,您可以创建程序。首先,从安装下面的各个库开始。

  • Adafruit GFX
  • Adafruit ILI9341
  • RTCLib
  • DFRobotDFPlayerMini
安装步骤如下:

  • 启动Arduino IDE。
  • 选择“Sketch”->“Include Library”->“Manage Library”菜单,以打开Library Manager。
  • 在“Filter search”字段中输入“Adafruit GFX”。
  • Adafruit GFX将在库列表中显示。单击“Install”按钮(图 4)。
  • 以相同的方式安装每个库。
有几个名称相似的RTCLib和DFPlayer库。RTCLib 安装“RTCLib by Adafruit”,而DFPlayer安装“DFRobotDFPlayerMini by DFRobot”。
wake-up-alarm-ESP32-8.jpg
图 4 Adafruit GFX库安装

此外,为通过大字符显示时间,需要安装字体文件。如果您下载并解压缩以下zip文件,将可以获得一个名为“FreeSans40pt7b.h”的文件。
打开Arduino IDE的标准草图目标文件夹,再打开“libraries”->“Adafruit_GFX_Library”->“Fonts”文件夹,将字体文件复制到那里。
https://www.h-fj.com/deviceplus/font.zip

Arduino IDE闹钟程序
接下来,在Arduino IDE中创建一个闹钟程序并将其写入ESP32。程序内容如清单1所示。
清单1:闹钟主机程序
程序内容(放在这里)
但是,第17行到第21行需要改写如下:
・第17/18行
根据您的Wi-Fi路由器的SSID/密码重写。
・第19行
指定要分配给ESS32的IP地址。根据您的Wi-Fi路由器的网络配置自行决定IP地址。
在普通IP地址中,四组数字用句点分隔,但在这一行中,它是函数参数的形式,所以四组数字用逗号分隔。
・第20行
指定网络默认网关的IP地址。通常,它是Wi-Fi路由器的IP地址。用逗号分隔IP地址中的四组数字。
・第21行
根据分配给分机ESP32的IP地址重写。

例如,如果您想按照表4所示进行设置,请重写第17行至第21行,如清单2所示。
项目设定值
Wi-Fi路由器SSIDmy_wifi
Wi-Fi路由器密码my_password
分配给主机ESP32的IP地址192.168.1.101
默认网关IP地址192.168.1.1
分配给分机ESP32的IP地址192.168.1.102

表4:主机网络设置示例

清单 2:重写第17-21行的示例

分机程序
分机程序如清单3所示。
以与闹钟主机相同的方式重写第5行到第9行。 但是,在第7行,指定分配给分机的IP地址。 此外,在第9行的“Main console IP address(主控台IP地址)”中指定闹钟的IP地址。
清单 3:分机程序
程序内容
  1. #include
  2. #include
  3. #include
  4. #include <Fonts/FreeSans12pt7b.h>
  5. #include <Fonts/FreeSans18pt7b.h>
  6. #include <Fonts/FreeSans40pt7b.h>
  7. #include
  8. #include
  9. #include "time.h"
  10. #include
  11. #include
  12. #include
  13. #include
  14. #include
  15. // Initial setup
  16. const char *ssid = "Wi-Fi SSID";
  17. const char *pass = "Wi-Fi password";
  18. IPAddress ip(IP address assigned to main unit);
  19. IPAddress gateway(IP address of default gateway);
  20. const char* notify_url = "http://IP address of extension unit/alarm";
  21. const char* adjust_time = "04:00:00";
  22. #define DF_VOLUME 30
  23. // Constants, etc.
  24. #define ALARM_SIG 25
  25. #define TFT_DC 2
  26. #define TFT_CS 5
  27. #define TFT_RST 4
  28. #define TFT_WIDTH 320
  29. Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
  30. RTC_DS3231 rtc;
  31. HardwareSerial hs(1);
  32. DFRobotDFPlayerMini myDFPlayer;
  33. WebServer server(80);
  34. char old_date[15];
  35. char old_time[9];
  36. char old_alarm[15];
  37. char alarm_time[9];
  38. char wdays[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
  39. bool alarm_checked = false;
  40. bool alarm_on = false;
  41. bool ntp_adjusted = false;
  42. int alarm_ctr;
  43. // Set date and time using NTP
  44. void setTimeByNTP() {
  45.   struct tm t;
  46.   configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
  47.   if (!getLocalTime(&t)) {
  48.     Serial.println("getLocalTime Error");
  49.     return;
  50.   }
  51.   rtc.adjust(DateTime(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec));
  52. }
  53. // Display string on LCD
  54. void showMessage(char* s_new, char* s_old, int y0, int height) {
  55.   int16_t x1, y1;
  56.   uint16_t w, w2, h;
  57.   int x, y;
  58.   if (strcmp(s_new, s_old) != 0) {
  59.     tft.getTextBounds(s_old, 0, 0, &x1, &y1, &w, &h);
  60.     w2 = w * 11 / 10;
  61.     tft.fillRect((TFT_WIDTH - w2) / 2 , y0 - (height / 2) + 1, w2, height, ILI9341_BLACK),
  62.     tft.getTextBounds(s_new, 0, 0, &x1, &y1, &w, &h);
  63.     tft.setCursor((TFT_WIDTH - w) / 2, y0 + (h / 2) - 1);
  64.     tft.print(s_new);
  65.     strcpy(s_old, s_new);
  66.   }
  67. }
  68. // Main settings page
  69. void handleRoot() {
  70.   int i;
  71.   
  72.   String html =
  73.     "<!DOCTYPE html>\n"
  74.     "<html>\n"
  75.     "<head>\n"
  76.     "<meta charset="utf-8">\n"
  77.     "<title>Alarm clock settings</title>\n"
  78.     "</head>\n"
  79.     "<body>\n"
  80.     "<form method="get" action="/set">\n"
  81.     "<p>\n"
  82.     "<select name="hour">\n";
  83.   for (i = 0; i < 24; i++) {
  84.     html += "<option value="";
  85.     html += String(i);
  86.     html += "">";
  87.     html += String(i);
  88.     html += "</option>\n";
  89.   }
  90.   html += "</select>(h)\n";
  91.   html += "<select name="min">\n";
  92.   for (i = 0; i < 60; i++) {
  93.     html += "<option value="";
  94.     html += String(i);
  95.     html += "">";
  96.     html += String(i);
  97.     html += "</option>\n";
  98.   }
  99.   html += "</select>(min) \n";
  100.   html += "<input type="submit" name="submit" value="Alarm setting" />\n";
  101.   html += "</p>\n";
  102.   html += "</form>\n";
  103.   html += "<form method="get" action="/set">\n";
  104.   html += "<input type="hidden" name="off" value="1">\n";
  105.   html += "<p><input type="submit" name="submit" value="Alarm off" /></p>\n";
  106.   html += "</form>\n";
  107.   html += "</body>\n";
  108.   html += "</html>\n";
  109.   server.send(200, "text/html", html);
  110. }
  111. // Set alarm
  112. void handleSetAlarm() {
  113.   int i, hour, min, sec;
  114.   bool is_off = false;
  115.   String s_hour = "", s_min = "", s_sec = "";
  116.   // Get "off/hour/min/sec" parameters from URL
  117.   for (i = 0; i < server.args(); i++) {
  118.     if (server.argName(i).compareTo("off") == 0) {
  119.       is_off = true;
  120.       break;
  121.     }
  122.     else if (server.argName(i).compareTo("hour") == 0) {
  123.       s_hour = server.arg(i);
  124.     }
  125.     else if (server.argName(i).compareTo("min") == 0) {
  126.       s_min = server.arg(i);
  127.     }
  128.     else if (server.argName(i).compareTo("sec") == 0) {
  129.       s_sec = server.arg(i);
  130.     }
  131.   }
  132.   // Turn off alarm if "off" parameter is set if (is_off) {
  133.     strcpy(alarm_time, "Off");
  134.     server.send(200, "text/plain; charset=utf-8", "Alarm turned off.");
  135.   }
  136.   // Set alarm time else if (s_hour.length() > 0 && s_min.length() > 0) {
  137.     hour = s_hour.toInt();
  138.     min = s_min.toInt();
  139.     if (s_sec.length() > 0) {
  140.       sec = s_sec.toInt();
  141.     }
  142.     else {
  143.       sec = 0;
  144.     }
  145.     if (hour >= 0 && hour <= 23 && min >= 0 && min <= 59 && sec >= 0 && sec <= 59) {
  146.       sprintf(alarm_time, "%02d:%02d:%02d", hour, min, sec);
  147.       String msg = "Alarm set to ";
  148.       msg.concat(alarm_time);
  149.       msg.concat(" .");
  150.       server.send(200, "text/plain; charset=utf-8", msg);
  151.     }
  152.     else {
  153.       server.send(200, "text/plain; charset=utf-8", "Incorrect date/time.");
  154.     }
  155.   }
  156.   else {
  157.     server.send(200, "text/plain; charset=utf-8", "Incorrect parameters.");
  158.   }
  159. }
  160. / Stop alarm
  161. void handleStopAlarm() {
  162.   myDFPlayer.pause();
  163.   alarm_on = false;
  164.   tft.drawRect(30, 180, 260, 40, ILI9341_BLACK);
  165.   server.send(200, "text/plain", "Alarm stop");
  166. }
  167. // If an invalid URL is specified
  168. void handleNotFound() {
  169.   String message = "Not Found : ";
  170.   message += server.uri();
  171.   server.send(404, "text/plain", message);
  172. }
  173. // Setup
  174. void setup() {
  175.   int16_t x1, y1;
  176.   uint16_t w, h;
  177.   Serial.begin(115200);
  178.   strcpy(old_date, "00000000000000");
  179.   strcpy(old_time, "00000000");
  180.   strcpy(old_alarm, "00000000000000");
  181.   strcpy(alarm_time, "Off");
  182.   // Initialize display
  183.   tft.begin();
  184.   tft.setRotation(3);
  185.   tft.fillScreen(ILI9341_BLACK);
  186.   tft.setTextColor(ILI9341_WHITE);
  187.   tft.setFont(&FreeSans12pt7b);
  188.   String s = "Initializing...";
  189.   tft.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  190.   tft.setCursor(0, h);
  191.   tft.println(s);
  192.   // Connect to WiFi
  193.   WiFi.begin(ssid, pass);
  194.   while (WiFi.status() != WL_CONNECTED) {
  195.     delay(500);
  196.     Serial.print(".");
  197.   }
  198.   WiFi.config(ip, gateway, WiFi.subnetMask(), IPAddress(8, 8, 8, 8), IPAddress(8, 8, 4, 4));
  199.   Serial.println("");
  200.   Serial.println("WiFi Connected.");
  201.   tft.println("WiFi Connected.");
  202.   // Initialize DFPlayer
  203.   hs.begin(9600, SERIAL_8N1, 16, 17);
  204.   int count = 0;
  205.   while (count < 10) {
  206.     if (!myDFPlayer.begin(hs)) {
  207.       count++;
  208.       Serial.print("DFPlayer initialize attempt ");
  209.       Serial.println(count);
  210.     }
  211.     else {
  212.       break;
  213.     }
  214.   }
  215.   if (count < 10) {
  216.     Serial.println("DFPlayer Initialized.");
  217.     tft.println("DFPlayer Initialized.");
  218.     myDFPlayer.pause();
  219.     myDFPlayer.volume(DF_VOLUME);
  220.   }
  221.   else {
  222.     Serial.println("DFPlayer Error.");
  223.     tft.println("DFPlayer Error.");
  224.     while(1);
  225.   }
  226.   
  227.   // Initialize RTC
  228.   if (!rtc.begin()) {
  229.     Serial.println("Couldn't find RTC");
  230.     while (1);
  231.   }
  232.   Serial.println("RTC Initialized");
  233.   tft.println("RTC Initialized.");
  234.   // Get current date/time via NTP and set to RTC
  235.   setTimeByNTP();
  236.   // Initialize web server
  237.   server.on("/", handleRoot);
  238.   server.on("/set", handleSetAlarm);
  239.   server.on("/stop", handleStopAlarm);
  240.   server.onNotFound(handleNotFound);
  241.   server.begin();
  242.   
  243.   // Fill display with black
  244.   tft.fillScreen(ILI9341_BLACK);
  245. }
  246. void loop() {
  247.   char new_time[9], new_date[15], new_alarm[15];
  248.   // Launch web server
  249.   server.handleClient();
  250.   // Display current date/time on LCD
  251.   DateTime now = rtc.now();
  252.   sprintf(new_date, "%04d/%02d/%02d ", now.year(), now.month(), now.day());
  253.   strcat(new_date, wdays[now.dayOfTheWeek()]);
  254.   sprintf(new_time, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
  255.   strcpy(new_alarm, "Alarm ");
  256.   strcat(new_alarm, alarm_time);
  257.   tft.setFont(&FreeSans18pt7b);
  258.   tft.setTextColor(ILI9341_WHITE);
  259.   showMessage(new_date, old_date, 40, 28);
  260.   showMessage(new_alarm, old_alarm, 200, 28);
  261.   tft.setFont(&FreeSans40pt7b);
  262.   showMessage(new_time, old_time, 120, 64);
  263.   // Check if current time is time set for alarm
  264.   if (strstr(new_time, alarm_time) != NULL) {
  265.     if (!alarm_checked) {
  266.       // If it's alarm time, ring out then send message to extension unit
  267.       myDFPlayer.loop(1);
  268.       alarm_checked = true;   
  269.       alarm_on = true;
  270.       alarm_ctr = 0;
  271.       HTTPClient http;
  272.       http.begin(notify_url);
  273.       int httpCode = http.GET();
  274.     }
  275.   }
  276.   else {
  277.     alarm_checked = false;
  278.   }
  279.   // While alarm is sounding, make red frame flash around alarm time on display
  280.   if (alarm_on) {
  281.     if (alarm_ctr == 0) {
  282.       tft.drawRect(30, 180, 260, 40, ILI9341_RED);
  283.     }
  284.     else if (alarm_ctr == ALARM_SIG) {
  285.       tft.drawRect(30, 180, 260, 40, ILI9341_BLACK);
  286.     }
  287.     alarm_ctr++;
  288.     alarm_ctr %= ALARM_SIG * 2;
  289.   }
  290.   // Update date/time using NTP at a specific time everyday
  291.   if (strstr(new_time, adjust_time) != NULL) {
  292.     if (!ntp_adjusted) {
  293.       setTimeByNTP();
  294.       ntp_adjusted = true;
  295.     }
  296.   }
  297.   else {
  298.     ntp_adjusted = false;
  299.   }
  300.   delay(20);
  301. }

确认运行情况
程序写入主机/分机后,我们来确认一下运行情况。步骤如下:


    • 准备一张32GB或以下的microSD卡,并创建一个名为“mp3”的文件夹。
    • 准备好要用作闹钟铃声的MP3文件,将其命名为“0001.mp3”,并将其保存在mp3文件夹中。
    • 将microSD卡插入DFPlayer Mini。
    • 启动主机和分机。
    • 初始化和Wi-Fi设置完成后,当前日期和时间将显示在主机上。
    • 在与主机/分机处于同一网络的电脑上启动Web浏览器,并连接到地址“http: // 主机IP地址/”。
    • 闹钟设置表单将显示在Web浏览器上,如照片4所示。指定时间并单击“Alarm Settings”按钮。


wake-up-alarm-ESP32-9.jpg
照片 4 闹钟设置


  • 闹钟响起的时间设置为“Alarm set to 10:00:00”。
  • 确认设置闹钟时主机会响起铃声。
  • 确认设置闹钟时分机的LED亮起。
  • 按分机上的开关检查闹钟是否关闭。
如要取消闹钟设置,请使用Web浏览器连接到地址“http: //主机IP地址/”,然后单击“Alarm Off”按钮。

结论
ESP32可以通过Wi-Fi连接到网络。不仅可以联网,还可以像今天的项目一样,与网络上的各种适用的电子产品连接使用。


来源:techclass.rohm