65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2020年11月22日

MIT

9分钟阅读

viewsIcon

12200

downloadIcon

217

用更现代的硬件重新审视我们支持 WiFi 的智能时钟项目

ESP32 IoT Smart Clock

引言

在本文中,我们将使用更新的硬件重新审视 物联网智能时钟。具体来说,我们将使用 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 日 - 初始提交
© . All rights reserved.