使用 ESP32 开发板的物联网智能时钟





5.00/5 (6投票s)
用更现代的硬件重新审视我们支持 WiFi 的智能时钟项目
引言
在本文中,我们将使用更新的硬件重新审视 物联网智能时钟。具体来说,我们将使用 ESP32 开发板,而不是 Arduino Mega 2560 WiFi,并将使用一块全新的 128x32 像素单色 OLED 显示屏,而不是旧的液晶显示屏。与它们所替代的零件一样,这些新零件价格便宜,在在线电子产品零售商处很容易买到。
Espressif ESP32 是 ESP8266 的继任者。还记得我们在上一个项目中劫持了其 CPU 以在其上运行 Web 服务器的 WiFi 模块吗?就是那个。这次无需劫持,因为 ESP32 从一开始就不是作为 WiFi 模块设计的,而是作为一款一流的物联网板,具备 WiFi 功能。
标准的 ESP32 芯片是 3.3VDC 的 SoC,配备 4MB 闪存,定制芯片可支持高达 16MB。它们还具有先进的“深度睡眠”功能,由一个小型协处理器辅助,该协处理器可以在许多可配置事件上唤醒 CPU,并且可以以最高 240MHz 的频率运行。除了 WiFi b/g/n,它们还支持蓝牙(包括 BLE)以及 ESP-NOW - 一个内置的专有无线通信协议。芯片包含一个霍尔效应传感器,可以检测磁场,多个触摸电容引脚,I/O 引脚复用等等。
它们通常装在开发板上,开发板包含用于编程或串行访问的 USB 桥接器,也可以用作 3.3VDC 电源的替代品。我最近以每块 5 美元的价格买了 5 块。
必备组件
您需要一块基于 ESP32 的开发板,例如 NodeMCU ESP32s 或 DOIT devkit 01。
您需要一块 DS1307 或类似的实时时钟。
您需要一块 DHT11 温湿度传感器。
您需要一块带有 I2C 接口的 SSD1306 128x32 OLED 显示屏。
您需要常用的导线和原型板。
您需要 Arduino IDE,并且需要从 **工具 | 板管理器...** 安装“esp32”板管理器。
您还需要下载几个库,但我已在上面的源代码中每个 include 语句上方添加了库的链接。将它们下载到一个文件夹,然后在存储草图的目录中,查找或创建一个“libraries”文件夹,并将每个库放在其自己的文件夹中。
概念化这个混乱的局面
ESP32 导航
ESP32 是一款强大的 32 位 240MHz 最大处理器,集成了 WiFi 和蓝牙以及 ESP-NOW,一种专有的无线通信功能。它通常配备 4MB 闪存,并拥有一组丰富的复用 I/O 引脚,可用于与任何设备接口。
在我们之前的文章中,我们不得不进行一些 hack 来使项目正常工作,因为我们使用 WiFi 处理器来完成大部分实际工作,并将 ATmega 处理器作为其从属。您还必须摆弄 DIP 开关才能对设备进行编程。使用 ESP32 开发板,所有这些都结束了。我们可以直接使用 ESP32,无需 hack 任何东西,它自己驱动 I/O,不像以前 ESP8266 通过串行委托 ATmega 来驱动 I/O。
关于电压的重要通知
与典型的 Arduino 产品不同,ESP32 是一个 3.3VDC 芯片,而不是 5VDC。大多数开发板都带有通过 USB 供电的 5VDC 电源,但不要将该电压反馈到输入引脚。如果您将 5VDC 输入到芯片,很可能不会损坏您的芯片,但您也不必冒着不必要的硬件风险。担心的是更换一块 5 美元芯片的成本,而是您将浪费时间去追踪接线问题,而真正的问题是芯片烧毁了。您不想那样。
老式板的一个小修复
您不需要去弄 DIP 开关,尽管如果板在编程时出现超时,那么在 EN 和 GND 之间放置一个 10uF 的电容器应该可以很好地解决问题。如果您没有电容,您只需要在整个上传过程中按住开发板上的 boot 按钮。较新的板子则无需这样做。
哪个引脚是哪个?
另一个注意事项是,每个开发板都有自己的引脚布局,而且并非所有引脚都容易识别。有时,如果您获得一块通用板,它可能没有引脚图或板上没有标签,您必须查阅数据手册或找到它所克隆的板(所有通用板似乎都是 NodeMCU 等“品牌”板的克隆,但它们通常不会告诉您它们是哪个克隆)。一些通用板,如 Hiletgo,会附带引脚图。
无论引脚布局如何,引脚都有或多或少通用的标签。“GPIO1”(有时称为“D1”)将始终是 GPIO1,无论其在板上的物理位置如何。接线时,请尝试找出引脚是哪个 GPIO 号。这样,无论您使用哪个板,接线都将保持一致。
关于库和工具的说明
ESP32 的一些库和工具不如 Arduino 和 ESP8266 的成熟。这意味着有时需要我们自己实现功能,例如 WiFi WPS 支持。ESP32 另一个缺乏的工具是可用的 SPIFFS 文件上传器。SPIFFS 库本身适用于 ESP32,但上传器工具不适用于此设置。因此,我们在这里不使用 SPIFFS。
处理外形尺寸
这些开发板对于在标准无焊面包板上进行原型设计来说可能有点宽。通常,当使用这些芯片时,一侧只有一个引脚行暴露出来。在这种情况下,您可能会发现最好将芯片跨在两个原型板上,至少在一个原型板上可能需要移除电源轨。
时钟功能
这在很大程度上是早期物联网时钟文章中信息的重述,但在此包含以保持完整性,并更新以反映新设计。
我们的时钟以 24 小时格式报告 UTC 时间、温度和湿度。它在 OLED 显示屏上显示,也可以通过 http://clock.local 访问。HTTPS 尚不支持。“时钟”的“Web 服务”只是 http://clock.local/time.json。
首次开机后,然后每 5 分钟,时钟将与 NTP 服务器同步,以确保时间始终是最新的。
除了已提到的显示项目外,屏幕还将显示一个连接时的小图标和一个同步时间时的额外图标。每个图标都将显示在屏幕的右侧。
要为您的网络配置时钟,只需打开时钟电源,然后触摸无线路由器上的 WPS 按钮。
工作原理
最棘手的部分是如何在不使用 delay()
的情况下管理 WiFi 连接。我们希望所有操作都不会阻塞,以便 CPU 能够自由地每秒更新时钟。我们使用状态机和一些时间戳变量来完成繁重的工作。在尝试连接时,时钟将在尝试连接和尝试查找 WPS 信号之间循环。
剩余部分中最难的部分是格式化和显示。经过一些 trial and error,我只是决定了最终结果,看看我喜欢什么。
使用各种库,从时钟和传感器收集信息非常简单。等待 clock.local 的传入 HTTP 请求也是如此。
这里的主要障碍是等待连接到网络,然后才初始化 UDP 或 Web 服务器。
关于构建,就这么多了。
构建这个大杂烩
连接这个“大杂烩”
我在 .ino 文件的末尾包含了接线说明,但我们在这里也会过一遍。与上一个项目相比,这个接线更简单,主要是因为屏幕使用了 I2C 接口而不是 Hitachi 16 针接口。
SSD1306
GND ➜ ESP32 GND
VCC ➜ ESP32 3.3VDC
SCL ➜ ESP32 GPIO22
SDA ➜ ESP32 GPIO21
DS1307
GND ➜ ESP32 GND
VCC ➜ ESP32 5VDC VIN
SCL ➜ SSD1306 SCL
SDA ➜ SSD1306 SDA
DHT11
GND (right) ➜ ESP32 GND
VCC (middle) ➜ ESP32 3.3VDC
S (left) ➜ ESP32 GPIO4
编写这个混乱的程序
从某种程度上说,这里的代码比上一个项目更简单,因为它不需要 hack,但是由于我们无法有效地使用 SPIFFS,Web 内容和屏幕图标已直接嵌入源代码中。这使得源代码有点尴尬,无法直接全部塞进一篇文章,因此我们将分块探索。让我们从 setup()
中的一些初始化代码开始。
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
while (true);
}
// look for the clock
if (! rtc.begin()) {
Serial.println(F("Couldn't find RTC"));
while (true);
}
// try to jumpstart if not running
if (! rtc.isrunning()) {
rtc.adjust(DateTime((uint32_t)0));
}
// halt if not running
if (! rtc.isrunning()) {
Serial.println(F("RTC is NOT running!"));
while(true);
}
// Clear the buffer
display.setTextColor(SSD1306_WHITE);
_tmp = DHT.temperature;
_hum = DHT.humidity;
display.clearDisplay();
display.setTextSize(2);
DateTime now = rtc.now();
char sz[16];
sprintf(sz, "%d:%02d:%02d",
now.hour(),
now.minute(),
now.second());
uint16_t h = drawCentered(sz, display.width(), 0);
display.setTextSize(1);
h = drawCentered("Temp: --/--", display.width(), h) + h;
drawCentered("Humidity: --", display.width(), h);
display.display();
_ts = _ntpts= millis();
这里我们主要是在初始化硬件。如果 RTC 硬件没有运行,我们会重置时间使其运行。最后,我们在屏幕上显示初始显示。最后一行只是对我们使用的各种时间戳进行初始化。
接下来是 loop()
例程,这是我们完成大部分工作的地方。首先,有一个状态机,在我们未连接时会运行。状态机的任务是在连接和 WPS 搜索阶段之间移动,直到可以建立网络连接。在该例程中,我们使用 _connect_ts
来跟踪连接尝试何时超时。
if (WL_CONNECTED != WiFi.status())
{
switch (_connect_state)
{
case 0: // connect start
_connect_ts = millis();
WiFi.begin();
_connect_state = 1; // connecting
_connect_ts = millis();
break;
case 1: // connect continue
if (WL_CONNECTED == WiFi.status())
{
_connect_state = 4; // connected
}
else if (millis() - _connect_ts > (CONNECT_TIMEOUT * 1000))
{
WiFi.disconnect();
_connect_ts = millis();
_connect_state = 2; // begin wps search
}
break;
case 2: // begin WPS search
_connect_ts = millis();
esp_wifi_wps_enable(&_wps_config);
esp_wifi_wps_start(0);
_connect_state = 3; // continue WPS search
break;
case 3: // continue WPS search
// handled by callback
break;
case 4: // got disconnected
_connect_state = 1; // connecting
_connect_ts = millis();
WiFi.reconnect();
break;
}
}
接下来,如果过去了 5 分钟,我们会发送一个 NTP 数据包 - 我应该把它放到一个 #define
中。
// if we are connected, send an NTP request every 5 minutes
if (WL_CONNECTED == WiFi.status())
{
if (millis() - _ntpts > 300000)
{
_ntpts = millis();
_trafficNTP = true;
WiFi.hostByName(NTP_SERVER, _ntpIP);
sendNTPpacket(_ntpIP); // send an NTP packet to a time server
}
}
现在,每过去一秒,我们需要读取 RTC 和 DHT11 的值,然后更新显示。
// update the display every second
if (millis() - _ts > 1000)
{
_ts = millis();
DateTime now = rtc.now();
DHT.read11(DHT11PIN);
uint16_t h = 0;
float t = DHT.temperature;
float hm = DHT.humidity;
// sometimes the values the DHT11
// returns are fugazi so we have
// to check for that
if (-999.0 < t)
_tmp = t;
if (-999.0 < hm)
_hum = hm;
// now draw the clock:
display.clearDisplay();
display.setTextSize(2);
char sz[16];
sprintf(sz, "%d:%02d:%02d",
now.hour(),
now.minute(),
now.second());
h = drawCentered(sz, display.width(), h);
display.setTextSize(1);
if (-999.0 < _tmp)
{
sprintf(sz, "Temp: %dC/%dF", (int)(_tmp + .5), (int)((_tmp * 1.8) + 32.5));
h = drawCentered(sz, display.width(), h) + h;
}
else
h = drawCentered("Temp: --/--", display.width(), h) + h;
if (-999.0 < _hum)
{
sprintf(sz, "Humidity: %d%%", (int)(_hum + .5));
h = drawCentered(sz, display.width(), h) + h;
}
else
h = drawCentered("Humidity: --", display.width(), h);
请注意,上面的操作都不会更改显示屏上的内容,直到调用 display.display()
。这样,我们可以排队多个绘图操作,并一次性全部绘制,减少闪烁。
现在,如果我们已连接,一旦我们意识到已连接,就需要启动 Web 服务器。如果需要,我们需要绘制在线和同步指示器。最后,我们需要侦听我们可以用来同步时钟的传入 NTP 数据包。
// if we're connected
if (WL_CONNECTED == WiFi.status())
{
if (_firstConnect)
{
// if this is the first connection:
_firstConnect = false;
// start our UDP listener
udp.begin(UDP_LOCALPORT);
// start clock.local domain
MDNS.begin("clock");
// web handlers
_server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
digitalWrite(2, HIGH);
request->send_P(200, "text/html", index_html, process);
digitalWrite(2, LOW);
});
_server.on("/time.json", HTTP_GET, [](AsyncWebServerRequest *request) {
digitalWrite(2, HIGH);
request->send_P(200, "application/json", time_json, process);
digitalWrite(2, LOW);
});
// start HTTP server
_server.begin();
_ntpts = millis();
// sync the clock
WiFi.hostByName(NTP_SERVER, _ntpIP);
_trafficNTP = true;
sendNTPpacket(_ntpIP); // send an NTP packet to a time server
}
// if we're online, show the connected bmp
display.drawBitmap(
display.width() - 10,
0,
wifi_bmp, 8, 8, 1);
// if we're waiting for an NTP response
// and it hasn't been 5 seconds yet display
// the syncing icon
if (_trafficNTP && millis() - _ntpts < 5000)
{
display.drawBitmap(
display.width() - 10,
10,
sync_bmp, 8, 8, 1);
}
// if we got a packet from NTP, read it
if (0 < udp.parsePacket())
{
_trafficNTP = false;
udp.read(_packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
//the timestamp starts at byte 40 of the received packet and is four bytes,
// or two words, long. First, esxtract the two words:
unsigned long highWord = word(_packetBuffer[40], _packetBuffer[41]);
unsigned long lowWord = word(_packetBuffer[42], _packetBuffer[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
// Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
const unsigned long seventyYears = 2208988800UL;
// subtract seventy years:
unsigned long epoch = secsSince1900 - seventyYears;
// use the data to set the clock
rtc.adjust(DateTime(epoch));
}
}
display.display();
上面可能不那么明显的一点是我们对 sync_bmp
绘制的超时。这样做是因为我们使用的是 UDP,它是会丢失数据的。如果我们丢失了一个数据包,我们不希望同步图标一直亮着 5 分钟 - 直到下一个同步数据包发送。我们希望它会消失。这就是为什么它会超时。
我们在这里没有涵盖 HTML 和 JSON 内容,但它是从上一个项目复制粘贴的,并且工作方式完全相同。唯一的区别是,它不再是从闪存中的文件提供,而是直接嵌入到源代码的顶部。
历史
- 2020 年 11 月 22 日 - 初始提交