• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

Делюсь опытом Использование RTC-памяти и режим глубокого сна.

Andrey L

Member
С одной стороны хочу поделиться своим "опытом", а с другой стороны, может быть у меня есть ошибка, которую я не заметил, хоть скетч и работает.

Так что если у меня появится ошибка, то прошу мне на неё указать. В этом случае я исправлю это первое сообщение, так что в последствии коментарии могут указывать на уже исправленные ошибки.

Зачем надо использовать RTC?
ESP8266 выходит из режима "глубокого сна" путём "перезагрузки", а следовательно код выполняется заного, да и старые значения переменых теряются, даже внутренние счётчики millis() и micros() сбрасываются.

Для хранения некоторых данных можно воспользоваться RTC памятью, но надо помнить, что и эта память сбрасывается, когда ESP будет полностью отключена от сети.

Так же размер RTC-памяти окраничена 512 байтами и советуется объявлять там типы по 4 байт. (Это я не полностью понимаю, поскольку с переменной типа bite это тоже работает.)

Что надо дополнительно учитывать при работе с RTC?
При подачи питания на модуль, нельзя рассчитывать, что в RTC будет хранится одни "нули", старых данных там тоже не будет. А следовательно надо научиться определять хранятся ли там наши данные или какие-то случайные "шумы".

Для этого используется дополнительная функция для подчёта проверочной суммы. Но и это магическое число для проверки тоже будет храниться в RTC памяти. В примере эта функция называется calculateCRC32().

Структура для RTC
Как я понимаю, отдельные переменные можно хранить в RTC-памяти, но для этого надо строго следить за тем какие данные по какому адресу хранятся, а так же чтобы они не пересекались.

Чтобы об этом не заботиться, мы создадим свою структуру для этого. Главное, чтобы эта структура не занимала больше 512 байт в памяти.

Код
Код:
/**
* Структура, которая будет хранится в RTC-памяти.
* byte counter - счётчик запусков программы
* unsigned long RunningTime - счётчик примерного времени в миллисекундах
*
* uint32_t crc32 - хранилище проверочной суммы, ОБЯЗАТЕЛЬНО должно стоять вконце структуры (ограничение моей функции по проверке этой суммы).
*
* Так же следим за общим размером структуры, её размер не должен превышать 512 байт. (Физический размер RTC-памяти.)
*/
struct {
  byte counter;               // 1 byte
  unsigned long RunningTime;  // 4 byte

  uint32_t crc32;             // 4 byte
} rtcData;


// Время в микросекундах, на которое мы отправляем наш модуль "спать".
// 1e6=10^6 - мультипликатор, который превращает секунды в микросекунды.
#define sleepTime 10*1e6


// Мультипликатор, который переводит микросекунды в миллисекунды
#define mikroToMillis 1/1e3


void setup() {
  // Без "большого серийного брата" во время тестов никак нельзя.
  Serial.begin(115200);
  Serial.println(""); // Первая строчка вывода - абра-кадабра
  Serial.println("Hallo!!!"); // Привет-привет

  // Считываем данные из RTC-памяти.
  if (ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
    Serial.println("Прочёл!!!");
    Serial.println(rtcData.RunningTime);
    Serial.println(rtcData.counter);
  } else {
    Serial.println("Паника! Прочесть не получилось.");
  }

  // Проверяем проверочную сумму
  if (rtcData.crc32 != calculateCRC32((uint8_t*) &rtcData)) {
    // Если проверочные суммы не сошлись, то на модуль только что подали питание,
    // и мы сохраняем "нулевые" значения.
    rtcData.RunningTime = 0;
    rtcData.counter = 0;
    // Обязательно перед записью пересчитываем проверочную сумму
    rtcData.crc32 = calculateCRC32((uint8_t*) &rtcData);
    // Сохраняем нашу структуру в RTC-память
    ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData));

    Serial.println("Проверочные суммы не сошлись, все данные сбросили.");
  } else {
    Serial.println("Проверочные суммы сошлись.");
  }

}


void loop() {
  // Увеличиваем время счётчика.
  // sleepTime (время "сна" в микросекундах)
  // * mikroToMillis (переводит микросекунды в миллисекунды)
  // + millis() не забываем добавить время, которое прошло с момета запуска программы
  // ещё можно добавить некое число на выполнение следующих команд до того, как модуль "заснёт"
  rtcData.RunningTime += sleepTime * mikroToMillis + millis();
  // Увеличиваем счётчик
  rtcData.counter++;
  // Не забываем перед записью обновить проверочную сумму
  rtcData.crc32 = calculateCRC32((uint8_t*) &rtcData);
  // Сохраняем нашу структуру в RTC-памяти
  ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData));

  // Идём "спать"
  ESP.deepSleep(sleepTime);
}


/**
* Магическая функция по подсчёту проверочной суммы нашей структуры.
* Основа её взята из примера ESP8266/RTCUserMemory.
*/
uint32_t calculateCRC32(const uint8_t *data) {
  // Обрабатываем все данные, кроме последних четырёх байтов,
  // где и будет храниться проверочная сумма.
  size_t length = sizeof(rtcData)-4;
 
  uint32_t crc = 0xffffffff;
  while (length--) {
    uint8_t c = *data++;
    for (uint32_t i = 0x80; i > 0; i >>= 1) {
      bool bit = crc & 0x80000000;
      if (c & i) {
        bit = !bit;
      }
      crc <<= 1;
      if (bit) {
        crc ^= 0x04c11db7;
      }
    }
  }
  return crc;
}

Вкатце что делает программа
  1. setup()
    Читаем, что хранится в RTC памяти.
    - Если данные не проходят валидацию, то мы эти данные "обнуляем"
  2. loop()
    Увеличиваем счётчики,
    пересчитываем новую проверочную сумму,
    сохраняем всё.
    Идём спать.

Альтернатива RTC
Внутренняя файловая система, работать можно через библиотеку "FS.h" (SPIFFS). Эта система как код скетча энергонезависимая, где можно хранить данные и без питания. Но для неё надо отдельно выделять память от 4МБ модуля.
 
Последнее редактирование модератором:

Andrey L

Member
@Сергей_Ф Спасибо, учту.

Но следующая версия программы должна просыпаться по таймеру или при нажатой кнопке, запускать ВЕБ-сервер, и после 10 минут после последнего запроса к ВЕБ-серверу вновь "засыпать". Пока я думаю как это можно сделать при помощи одной кнопки.
 

Сергей_Ф

Moderator
Команда форума
@Andrey L нет проблем, смотрите статус просыпания. А кнопку повесьте на chip_en - это обеспечит другой код при просыпания.
Тут loop использовать будет логично для веб сервера.
 

nikolz

Well-known member
@Andrey L нет проблем, смотрите статус просыпания. А кнопку повесьте на chip_en - это обеспечит другой код при просыпания.
Тут loop использовать будет логично для веб сервера.
надо учитывать, что просыпание по сигналу chip-en приводит к обнулению памяти RTC (здесь называемой TRC)
 

Сергей_Ф

Moderator
Команда форума
просыпание по сигналу chip-en приводит к обнулению памяти RTC
Это да. Иначе можно обойти, если допустимо двойное нажатие на кнопку подключенную к RST. Или кнопку от RST подключить к одному из gpio и считывать состояние пина при старте. Но тут без дополнительных схемных элементов для развязки RST и gpio не обойтись.
 

tretyakov_sa

Moderator
Команда форума
Для хранения некоторых данных можно воспользоваться TRC памятью, но надо помнить, что и эта память сбрасывается, когда ESP будет полностью отключена от сети.
Что надо дополнительно учитывать при работе с TRC?
При подачи питания на модуль, нельзя рассчитывать, что в TRC будет хранится одни "нули", старых данных там тоже не будет. А следовательно надо научиться определять хранятся ли там наши данные или какие-то случайные "шумы"..
Вы можете определить, как ESP стартовала, и если это включение по питанию, то в памяти точно нет данных.
ESP.getResetReason();
 

nikolz

Well-known member
Это да. Иначе можно обойти, если допустимо двойное нажатие на кнопку подключенную к RST. Или кнопку от RST подключить к одному из gpio и считывать состояние пина при старте. Но тут без дополнительных схемных элементов для развязки RST и gpio не обойтись.
на самом деле, согласно документации,
включение питания предполагает подачу High на RST и потом с задержкой на CH, что приводит к очистке RTC.
 

Andrey L

Member
Да, я ошибся когда RTC неправильно назвал, но первое сообщение я уже изменить не могу, только теги подправил.

Так же я нашёл ESP.getResetReason(); , но с этим мне надо будет отдельно поэксперементировать.

Что же касается "chip_en", то я нашёл, где этот пин на ESP-модуле находится. Но где он на моём NodeMCU и зачем он нужен, я так и не понял.
 

Сергей_Ф

Moderator
Команда форума
на моём NodeMCU и зачем он нужен, я так и не понял.
на вашем NodeMCU он уже притянут +3.3В резистором. Пин находится рядом с RST, называется EN (если китайцы не ошиблись, конечно). При низком уровне на нём esp будет вЫключена и будет потреблять очень мало, при высоком - включена.
 

Andrey L

Member
@Andrey L ...смотрите статус просыпания. А кнопку повесьте на chip_en - это обеспечит другой код при просыпания.
В соседней ветке я выяснил какие "статусы просыпания" получатюся при замыкании пинов "RST" и "EN" с "GND", Вы это имели ввиду?

А то я сейчас пытаюсь замыкать пины "RST" и "EN" с одним из пинов "D", но тут у меня ничего не получается (лишь иногда "Hardware Watchdog").

Вообще-то я хотел, чтобы при нажатии на кнопку модуль "просыпался" (помимо таймера), а если модуль при нажатии на эту же кнопку не был в режиме "сна", то он бы ещё на неё как-нибудь смог реагировать.
 

Сергей_Ф

Moderator
Команда форума
хотел, чтобы при нажатии на кнопку модуль "просыпался" (помимо таймера), а если модуль при нажатии на эту же кнопку не был в режиме "сна", то он бы ещё на неё как-нибудь смог реагировать.
Если модуль спал - код один, если нет - другой. Никаких проблем. Правда все через reset.
ESP8266 Deep Sleep with Arduino IDE
 

Сергей_Ф

Moderator
Команда форума
сейчас пытаюсь замыкать пины "RST" и "EN" с одним из пинов "D", но тут у меня ничего не получается (лишь иногда "Hardware Watchdog").
а что должно получаться? Вы должно понимать что и для чего соединяете и поддержать это как аппаратно, так и программно. Вы же, судя по написанному, не понимаете и используете метод тыка. Тут так не пройдет.
 

Andrey L

Member
а что должно получаться?..
Надеялся, что при Digitalwrite(LOW) или же Pinmode(input) пин будет притягиваться к "GND".

...Вы же, судя по написанному, не понимаете и используете метод тыка...
Да, если честно, половину ответов мне я не понимаю.
 

Andrey L

Member
@Сергей_Ф Спасибо.
Моя ошибка была в том, что железу надо было давать время на "выполнение", чего я не делал. Вот у меня и с Digitalwrite(LOW) не получилось. После чего, я и решился попробовать с Pinmode(input).
 

p-a-h-a

Member
Здравствуйте, такая проблема с RTC памятью:
Пытаюсь сохранить переменную типа IPAddress или String в той же структуре из кода в 1м посте и при первом включении функция ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData)) забивает эти переменные мусором да так, что потом что им не присваивай, они остаются с мусором и в процессе чтения структуры срабатывает сторожевая собака.
Вопрос: как лучше сохранить ssid, pass, static ip в rtc памяти?
Примерно такие данные:
"Ssid" "Password" "192.168.1.1" все строки.
 

nikolz

Well-known member
Здравствуйте, такая проблема с RTC памятью:
Пытаюсь сохранить переменную типа IPAddress или String в той же структуре из кода в 1м посте и при первом включении функция ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData)) забивает эти переменные мусором да так, что потом что им не присваивай, они остаются с мусором и в процессе чтения структуры срабатывает сторожевая собака.
Вопрос: как лучше сохранить ssid, pass, static ip в rtc памяти?
Примерно такие данные:
"Ssid" "Password" "192.168.1.1" все строки.
ssid и pass сохранять не надо, т к их сохраняет OS во флеш.
статик ip тоже сохранять не надо - он же статик, а не динамик.
 

p-a-h-a

Member
ssid и pass сохранять не надо, т к их сохраняет OS во флеш.
статик ip тоже сохранять не надо - он же статик, а не динамик.
Вот это прозрение наступило. Действительно.
C++:
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);}
void loop() {}
До этого момента был залит скетч с статическим IP.
И в Serial вижу:
Код:
connected with ssidxxx, channel 6
dhcp client start...
ip:192.168.0.103,mask:255.255.255.0,gw:192.168.0.1
pm open,type:2 0
 
Сверху Снизу