Originally published by Dec 3, 2019
本文最初發佈在deviceplus.jp網站上,而後被翻譯成英語。
您每天早上被什麼鬧鐘吵醒?早上人們的一個常見問題是關掉鬧鐘並直接回去睡覺。
這一次,我們決定使用ESP32製作一個“鬧鐘”,應該有助於解決這個問題。
預計完成時間:120分鐘
名稱 | 賣方 | 價格 |
ESP32-DevKitC(2件) | 貿澤電子 | 約10.00美元 |
DS3231 模組 | Amazon | 約3.00美元 |
LIR2032(鈕扣充電電池) | Amazon | 約3.60美元 |
揚聲器 | Amazon | 約1.00~3.00美元 |
2.8英寸SPI連接器 320×240圖元LCD螢幕 | Amazon | 約15.00美元 |
*除上述物品外,還需要一個輕觸開關、一個LED和一個約100Ω的電阻。
我們來使用ESP32完成一個目標吧:像普通鬧鐘一樣在螢幕上顯示日期和時間,並在指定的時間響起。在設計製作這款“鬧鐘”時,我們需要將分機按鈕(用於關閉鬧鐘)與鬧鐘主機分開放置。之所以這樣設計,是因為想要您必須起床並走到分機按鈕處才能將鬧鐘關掉。
如下面的視頻所示,使用兩個ESP,一個用於主機,另一個用於分機。在視頻中,主機和分機是挨著的,但如果透過Wi-Fi連接,它們是可以在Wi-Fi範圍內分開放置的。
對於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的相同,但不同的是它可充電。
由於鬧鐘用於查看當前日期和時間,因此需要以易於理解的方式顯示日期和時間。以下設備用於顯示日期和時間。
根據設備的不同,有不同的庫和不同的程式設計方法。這還取決於它是否適合您想要製作的作品。例如,7段LED僅適合以低成本顯示數位,但不適合顯示詳情。其中,圖形液晶顯示器是最通用的,可以用於許多不同的項目,所以我這次決定使用它。
市面上有各種類型的液晶顯示器模組,但對於今天的項目,我們將使用一個名為“ILI9341”的控制器,並使用SPI介面(照片2)。此外,液晶顯示器通常以2.2英寸/2.4英寸/2.8英寸尺寸出售,因此,請根據您正在做的作品類型進行相應調整。
既然是鬧鐘,在指定時間發出鈴聲是必需的。您可以將蜂鳴器連接到ESP32以產生單一鈴聲,但如果您願意,也可以使用自己喜歡的鈴聲。為此,我們將使用一個名為“DFPlayer Mini”的模組,它可以播放任何MP3資料(照片3)。
DFPlayer Mini是一個可以透過串口發送命令來播放microSD卡中MP3的模組。可以將一個小型揚聲器連接到揚聲器輸出引腳以產生鈴聲。
讓我們進入實際構建吧。首先,給鬧鐘接線。
使用兩個麵包板,一個配有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”兩個引腳。
ESP32引腳 | 液晶顯示器引腳 |
5V | VCC |
GND | GND |
5 | CS |
4 | RESET |
2 | DC |
23 | MOSI |
18 | SCK |
19 | MISO |
表 1:ESP32 與液晶顯示器的連接
ESP32 引腳 | DS3231 引腳 |
5V | VCC |
GND | GND |
21 | SDA |
22 | SCL |
表 2:ESP32與DS3231的連接
ESP32 引腳 | DFPlayer Mini 引腳 |
5V | VCC |
GND | GND |
16 | TX |
17 | RX |
表 3:ESP32與DFPlayer Mini的連接
接下來,我們將給分機接線。分機接線應按圖3所示進行。您所要做的就是將開關和 LED連接到ESP32。將開關的一側連接到ESP32的3V3引腳,另一側連接到引腳4。透過電阻器→LED再透過ESP32的引腳13連接到GND。
在讀取開關狀態的電路上插入一個上拉電阻或下拉電阻。但是,由於ESP32可以透過內部電阻進行上拉/下拉,因此省略了外部電阻。
完成接線工作後,您可以創建程式。首先,從安裝下面的各個庫開始。
安裝步驟如下:
有幾個名稱相似的RTCLib和DFPlayer庫。RTCLib 安裝“RTCLib by Adafruit”,而DFPlayer安裝“DFRobotDFPlayerMini by DFRobot”。
此外,為透過大字元顯示時間,需要安裝字體檔。如果您下載並解壓縮以下zip檔,將可以獲得一個名為“FreeSans40pt7b.h”的文件。
打開Arduino IDE的標準草圖目的檔案夾,再打開“libraries”->“Adafruit_GFX_Library”->“Fonts”資料夾,將字體檔複製到那裡。
https://www.h-fj.com/deviceplus/font.zip
接下來,在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 路由器 SSID | my_wifi |
Wi-Fi 路由器密碼 | my_password |
分配給主機ESP32的IP位址 | 192.168.1.101 |
預設閘道器IP地址 | 192.168.1.1 |
分配給分機ESP32的IP地址 | 192.168.1.102 |
分機程式如清單3所示。
以與鬧鐘主機相同的方式重寫第5行到第9行。 但是,在第7行,指定分配給分機的IP地址。 此外,在第9行的“Main console IP address(主控台IP地址)”中指定鬧鐘的IP地址。
清單 3:分機程式
程式內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
#include #include #include #include <Fonts/FreeSans12pt7b.h> #include <Fonts/FreeSans18pt7b.h> #include <Fonts/FreeSans40pt7b.h> #include #include #include "time.h" #include #include #include #include #include // Initial setup const char *ssid = "Wi-Fi SSID"; const char *pass = "Wi-Fi password"; IPAddress ip(IP address assigned to main unit); IPAddress gateway(IP address of default gateway); const char* notify_url = "http://IP address of extension unit/alarm"; const char* adjust_time = "04:00:00"; #define DF_VOLUME 30 // Constants, etc. #define ALARM_SIG 25 #define TFT_DC 2 #define TFT_CS 5 #define TFT_RST 4 #define TFT_WIDTH 320 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST); RTC_DS3231 rtc; HardwareSerial hs(1); DFRobotDFPlayerMini myDFPlayer; WebServer server(80); char old_date[15]; char old_time[9]; char old_alarm[15]; char alarm_time[9]; char wdays[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; bool alarm_checked = false; bool alarm_on = false; bool ntp_adjusted = false; int alarm_ctr; // Set date and time using NTP void setTimeByNTP() { struct tm t; configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); if (!getLocalTime(&t)) { Serial.println("getLocalTime Error"); return; } rtc.adjust(DateTime(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)); } // Display string on LCD void showMessage(char* s_new, char* s_old, int y0, int height) { int16_t x1, y1; uint16_t w, w2, h; int x, y; if (strcmp(s_new, s_old) != 0) { tft.getTextBounds(s_old, 0, 0, &x1, &y1, &w, &h); w2 = w * 11 / 10; tft.fillRect((TFT_WIDTH - w2) / 2 , y0 - (height / 2) + 1, w2, height, ILI9341_BLACK), tft.getTextBounds(s_new, 0, 0, &x1, &y1, &w, &h); tft.setCursor((TFT_WIDTH - w) / 2, y0 + (h / 2) - 1); tft.print(s_new); strcpy(s_old, s_new); } } // Main settings page void handleRoot() { int i; String html = "<!DOCTYPE html>\n" "<html>\n" "<head>\n" "<meta charset=\"utf-8\">\n" "<title>Alarm clock settings</title>\n" "</head>\n" "<body>\n" "<form method=\"get\" action=\"/set\">\n" "<p>\n" "<select name=\"hour\">\n"; for (i = 0; i < 24; i++) { html += "<option value=\""; html += String(i); html += "\">"; html += String(i); html += "</option>\n"; } html += "</select>(h)\n"; html += "<select name=\"min\">\n"; for (i = 0; i < 60; i++) { html += "<option value=\""; html += String(i); html += "\">"; html += String(i); html += "</option>\n"; } html += "</select>(min) \n"; html += "<input type=\"submit\" name=\"submit\" value=\"Alarm setting\" />\n"; html += "</p>\n"; html += "</form>\n"; html += "<form method=\"get\" action=\"/set\">\n"; html += "<input type=\"hidden\" name=\"off\" value=\"1\">\n"; html += "<p><input type=\"submit\" name=\"submit\" value=\"Alarm off\" /></p>\n"; html += "</form>\n"; html += "</body>\n"; html += "</html>\n"; server.send(200, "text/html", html); } // Set alarm void handleSetAlarm() { int i, hour, min, sec; bool is_off = false; String s_hour = "", s_min = "", s_sec = ""; // Get "off/hour/min/sec" parameters from URL for (i = 0; i < server.args(); i++) { if (server.argName(i).compareTo("off") == 0) { is_off = true; break; } else if (server.argName(i).compareTo("hour") == 0) { s_hour = server.arg(i); } else if (server.argName(i).compareTo("min") == 0) { s_min = server.arg(i); } else if (server.argName(i).compareTo("sec") == 0) { s_sec = server.arg(i); } } // Turn off alarm if "off" parameter is set if (is_off) { strcpy(alarm_time, "Off"); server.send(200, "text/plain; charset=utf-8", "Alarm turned off."); } // Set alarm time else if (s_hour.length() > 0 && s_min.length() > 0) { hour = s_hour.toInt(); min = s_min.toInt(); if (s_sec.length() > 0) { sec = s_sec.toInt(); } else { sec = 0; } if (hour >= 0 && hour <= 23 && min >= 0 && min <= 59 && sec >= 0 && sec <= 59) { sprintf(alarm_time, "%02d:%02d:%02d", hour, min, sec); String msg = "Alarm set to "; msg.concat(alarm_time); msg.concat(" ."); server.send(200, "text/plain; charset=utf-8", msg); } else { server.send(200, "text/plain; charset=utf-8", "Incorrect date/time."); } } else { server.send(200, "text/plain; charset=utf-8", "Incorrect parameters."); } } / Stop alarm void handleStopAlarm() { myDFPlayer.pause(); alarm_on = false; tft.drawRect(30, 180, 260, 40, ILI9341_BLACK); server.send(200, "text/plain", "Alarm stop"); } // If an invalid URL is specified void handleNotFound() { String message = "Not Found : "; message += server.uri(); server.send(404, "text/plain", message); } // Setup void setup() { int16_t x1, y1; uint16_t w, h; Serial.begin(115200); strcpy(old_date, "00000000000000"); strcpy(old_time, "00000000"); strcpy(old_alarm, "00000000000000"); strcpy(alarm_time, "Off"); // Initialize display tft.begin(); tft.setRotation(3); tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_WHITE); tft.setFont(&FreeSans12pt7b); String s = "Initializing..."; tft.getTextBounds(s, 0, 0, &x1, &y1, &w, &h); tft.setCursor(0, h); tft.println(s); // Connect to WiFi WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } WiFi.config(ip, gateway, WiFi.subnetMask(), IPAddress(8, 8, 8, 8), IPAddress(8, 8, 4, 4)); Serial.println(""); Serial.println("WiFi Connected."); tft.println("WiFi Connected."); // Initialize DFPlayer hs.begin(9600, SERIAL_8N1, 16, 17); int count = 0; while (count < 10) { if (!myDFPlayer.begin(hs)) { count++; Serial.print("DFPlayer initialize attempt "); Serial.println(count); } else { break; } } if (count < 10) { Serial.println("DFPlayer Initialized."); tft.println("DFPlayer Initialized."); myDFPlayer.pause(); myDFPlayer.volume(DF_VOLUME); } else { Serial.println("DFPlayer Error."); tft.println("DFPlayer Error."); while(1); } // Initialize RTC if (!rtc.begin()) { Serial.println("Couldn't find RTC"); while (1); } Serial.println("RTC Initialized"); tft.println("RTC Initialized."); // Get current date/time via NTP and set to RTC setTimeByNTP(); // Initialize web server server.on("/", handleRoot); server.on("/set", handleSetAlarm); server.on("/stop", handleStopAlarm); server.onNotFound(handleNotFound); server.begin(); // Fill display with black tft.fillScreen(ILI9341_BLACK); } void loop() { char new_time[9], new_date[15], new_alarm[15]; // Launch web server server.handleClient(); // Display current date/time on LCD DateTime now = rtc.now(); sprintf(new_date, "%04d/%02d/%02d ", now.year(), now.month(), now.day()); strcat(new_date, wdays[now.dayOfTheWeek()]); sprintf(new_time, "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); strcpy(new_alarm, "Alarm "); strcat(new_alarm, alarm_time); tft.setFont(&FreeSans18pt7b); tft.setTextColor(ILI9341_WHITE); showMessage(new_date, old_date, 40, 28); showMessage(new_alarm, old_alarm, 200, 28); tft.setFont(&FreeSans40pt7b); showMessage(new_time, old_time, 120, 64); // Check if current time is time set for alarm if (strstr(new_time, alarm_time) != NULL) { if (!alarm_checked) { // If it's alarm time, ring out then send message to extension unit myDFPlayer.loop(1); alarm_checked = true; alarm_on = true; alarm_ctr = 0; HTTPClient http; http.begin(notify_url); int httpCode = http.GET(); } } else { alarm_checked = false; } // While alarm is sounding, make red frame flash around alarm time on display if (alarm_on) { if (alarm_ctr == 0) { tft.drawRect(30, 180, 260, 40, ILI9341_RED); } else if (alarm_ctr == ALARM_SIG) { tft.drawRect(30, 180, 260, 40, ILI9341_BLACK); } alarm_ctr++; alarm_ctr %= ALARM_SIG * 2; } // Update date/time using NTP at a specific time everyday if (strstr(new_time, adjust_time) != NULL) { if (!ntp_adjusted) { setTimeByNTP(); ntp_adjusted = true; } } else { ntp_adjusted = false; } delay(20); } |
程式寫入主機/分機後,我們來確認一下運行情況。步驟如下:
如要取消鬧鐘設置,請使用Web流覽器連接到位址“http: //主機IP位址/”,然後按一下“Alarm Off”按鈕。
ESP32可以透過Wi-Fi連接到網路。不僅可以聯網,還可以像今天的專案一樣,與網路上的各種適用的電子產品連接使用。
想繼續探索Arduino的功能嗎?歡迎查看相關的文章: