Embedded

Энергопотребление и режим глубокого сна ESP32 для автономных датчиков

Введение

Устройство на батарейках, постоянно подключённое к Wi-Fi и непрерывно работающее в обычном режиме, разряжает батарею очень быстро — Wi-Fi-модуль один из самых энергоёмких компонентов ESP32. Для по-настоящему автономных, питающихся от батареи датчиков критична поддержка режима глубокого сна (deep sleep), в котором устройство периодически «просыпается», быстро отправляет показания и снова «засыпает» на длительное время.

Концепция

Deep sleep отключает почти все компоненты ESP32, кроме минимального «сторожевого» контроллера (RTC), потребление энергии падает на порядки по сравнению с обычным активным режимом — пробуждение происходит либо по таймеру (наиболее частый случай для периодических измерений), либо по внешнему сигналу (например, изменение состояния определённого пина). Критичная особенность — после выхода из deep sleep ESP32 фактически перезапускается с начала setup(), а не «продолжает» работу с того места, где заснул, что требует другого подхода к организации кода по сравнению с обычным, непрерывно работающим loop().

Пример кода: Энергопотребление и режим

// firmware/src/main.cpp — автономный датчик с deep sleep между измерениями
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>

#define DHT_PIN 4
#define uS_TO_S_FACTOR 1000000ULL
#define DEEP_SLEEP_INTERVAL_SECONDS 300 // публикация раз в 5 минут вместо непрерывной работы

DHT dht(DHT_PIN, DHT11);
WiFiClient espClient;
PubSubClient mqttClient(espClient);

void setup()
{
    Serial.begin(115200);
    dht.begin();

    // Поскольку весь setup() выполняется заново после каждого пробуждения,
    // вся логика "одного цикла измерения" помещается прямо здесь, а не в loop()
    connectToWiFi();
    mqttClient.setServer("192.168.1.100", 1883);

    if (mqttClient.connect("esp32_garden_sensor")) {
        publishReading();
    } else {
        Serial.println("Не удалось подключиться к MQTT — пропускаем эту публикацию");
    }

    Serial.println("Уход в глубокий сон");
    esp_sleep_enable_timer_wakeup(DEEP_SLEEP_INTERVAL_SECONDS * uS_TO_S_FACTOR);
    esp_deep_sleep_start(); // выполнение кода ниже этой строки уже никогда не происходит
}

void loop()
{
    // Пустой — вся логика выполняется один раз в setup() перед каждым уходом в сон
}

void connectToWiFi()
{
    WiFi.begin("MyHomeNetwork", "secure_password_here");
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        attempts++;
    }
}

void publishReading()
{
    const float temperature = dht.readTemperature();
    if (!isnan(temperature)) {
        char tempStr[8];
        dtostrf(temperature, 4, 1, tempStr);
        mqttClient.publish("garden/soil/temperature", tempStr);
        mqttClient.loop(); // даём время на фактическую отправку перед немедленным уходом в сон
        delay(100);
    }
}

Пояснения к коду

Архитектурное отличие от всех предыдущих прошивок цикла — вся логика одного измерительного цикла (подключение к Wi-Fi, подключение к MQTT, публикация, и финальный уход в сон) помещена прямо внутрь setup(), а loop() остаётся фактически пустым, поскольку до него выполнение просто никогда не доходит — esp_deep_sleep_start() немедленно переводит устройство в сон, и после DEEP_SLEEP_INTERVAL_SECONDS секунд устройство не «возвращается» в код после этой строки, а полностью перезапускается с самого начала setup(), как при обычном включении питания.

esp_sleep_enable_timer_wakeup() принимает время в микросекундах, поэтому секунды умножаются на константу uS_TO_S_FACTOR (один миллион) для перевода в нужные единицы — короткая дополнительная delay(100) после mqttClient.loop() в publishReading() даёт время на фактическую передачу данных по сети перед немедленным уходом в сон, поскольку публикация через publish() инициирует отправку, но сама передача по факту происходит асинхронно, и слишком быстрый переход в deep sleep непосредственно после вызова publish() рискует прервать передачу данных до того, как она реально успеет завершиться.

Подводные камни

  • Полное игнорирование того факта, что после deep sleep выполнение начинается заново с setup(), и попытка написать код в привычном стиле «обычной», непрерывно работающей прошивки (с состоянием, сохраняемым в обычных глобальных переменных между итерациями loop()) — любое состояние, которое должно сохраняться между циклами сна (счётчик количества пробуждений, накопленная статистика), требует специальной памяти, сохраняющейся через deep sleep (RTC_DATA_ATTR для соответствующих переменных), а не обычных глобальных переменных, которые при каждом пробуждении инициализируются заново.
  • Подключение к Wi-Fi при каждом пробуждении занимает заметное время и энергию (обычно несколько секунд) относительно общего энергетического бюджета цикла — для устройств с очень частыми циклами пробуждения (например, каждую минуту) накладные расходы на повторное Wi-Fi-подключение при каждом пробуждении могут составлять существенную долю общего энергопотребления; для оптимизации иногда используют сохранение Wi-Fi credentials и параметров подключения в RTC-памяти между циклами сна, что может несколько ускорить повторное подключение по сравнению с полным циклом первого подключения.
  • Недостаточная задержка перед уходом в сон после публикации MQTT-сообщения, обрезающая фактическую передачу данных по сети до её реального завершения — поскольку MQTT-публикация и особенно установление TCP-соединения с брокером не происходят мгновенно, слишком быстрый переход в deep sleep сразу после вызова publish() без какой-либо паузы может привести к ситуации, когда устройство уже «уснуло», а сообщение реально не было отправлено или не было полностью доставлено.
  • Полное отсутствие физической возможности удалённо управлять устройством, находящимся в deep sleep, поскольку MQTT-подключение (и сам Wi-Fi-модуль) полностью отключены в этом режиме — устройство, работающее в режиме периодических пробуждений для отправки данных, физически не может принимать команды управления в промежутках между пробуждениями; для устройств, которым одновременно нужна и энергоэффективность, и оперативная реакция на внешние команды, нужен более сложный компромисс (например, более частые, но короткие пробуждения специально для проверки входящих команд, или принятие того, что команда будет выполнена только при следующем плановом пробуждении, а не немедленно).