Как исправить код утечки памяти в ESP8266/NodeMCU, вызванный концентрацией строк?

У меня есть следующий код в loop() в NodeMCU.

Эта часть перед установкой():

String serial_data_read = ""; // для входящих последовательных строковых данных
String serial_data = ""; // для входящих последовательных строковых данных

Эта часть находится в цикле():

if ((last_weight_time < millis() - 1000)) {
   last_weight_time = millis();
   serial_data_read = "";
   Serial.print("P");
   while(Serial.available()) {
      character = Serial.read();
      serial_data_read.concat(character);
   }
   serial_data = serial_data_read;
}

Я не очень хорошо разбираюсь в программировании на языке Си, но думаю, что это как-то связано с утечкой памяти и ее окончательным исчерпанием. Он работает около 20 минут, а затем просто останавливается. Когда я вручную перезапускаю NodeMCU, он снова работает нормально.

Я был бы очень благодарен за любую помощь, как переписать мой код, чтобы он не пропускал память. Большое спасибо.

, 👍0

Обсуждение

Просто не используйте массивы "String`, а массивы "char", @chrisl

https://majenko.co.uk/blog/evils-arduino-strings, @Majenko

Re if ((last_weight_time < millis() - 1000)): This can fail badly if last_weight_time is right before a millis() rollover event. Use instead the idiom if (millis() - last_weight_time >1000), что является безопасным опрокидыванием., @Edgar Bonet


3 ответа


Лучший ответ:

3

Чтобы написать код, который не вызовет утечку памяти, вам сначала нужно понять, где и что вызвало утечку памяти. Утечка памяти в данном конкретном случае вызвана строкой

serial_data_read.concat(character); 

Когда вы создаете экземпляр строкового объекта с помощью String serial_data_read = "", класс Arduino String создает выделение памяти, используя динамическое выделение памяти в куче, и, поскольку вы инициализировали его с помощью"", он, следовательно, создает минимальный размер выделения. Все это хорошо, если вы знаете, как создается динамическая память и как она будет использоваться.

Проблема появилась, когда вы начинаете делать конкатенацию строк с линией serial_data_read.concat(character), оригинал распределения для глобальных переменных serial_data_read уже не хватает места для составной переменной, так это создать временную переменную в куче, а не конкатенации, а затем в конечном итоге поместить результат обратно к недавно выделенной памяти для serial_data_read в куче, оставляя отверстие в кучи для оригинального serial_data_read переменной (и переменную Temp).

Хуже всего то, что это происходит для каждой итерации каждые 16 итераций(см. Комментарий Эдгара Бонета ниже) цикла while. Теоретически старый serial_data_read до конкатенация должна быть бесплатной-в какой-то момент (например, если ваш цикл while в функции части кучи может быть бесплатным при выходе из функции), он тем не менее не стоит быть так, потому что вновь созданной serial_data_read оставаться на вершине кучи, чтобы быть свободной памяти, что блокирование высвобождения(представьте себе кусок сыра с большим количеством дырочек на нем), так что памяти меньше и меньше на каждой итерации.

Краткосрочное исправление - не выполняйте связывание строк в цикле

Класс String не всегда является злым, если вы знаете, что происходит и как его использовать. Одно из быстрых решений вашей проблемы заключается в том, чтобы просто не выполнять связывание строк в цикле. И если вы используете его в функции, сохраняйте его локальным. Это сведет к минимуму фрагментацию кучи.

if ((last_weight_time < millis() - 1000)) {
   last_weight_time = millis();
   if (Serial.available() > 0) {
      String serial_data_read = Serial.readStringUntil('\n');
   }
}

Кстати, вы можете добавить ESP.getFreeHeap() в свой цикл (), чтобы проверить, есть ли еще утечка памяти.

Долгосрочное исправление - Не используйте класс String

Для лучшего исправления, и особенно для кого-то, кто не знаком с программированием или C/C++, не используйте класс String и узнайте, как использовать строку c и массив. Вот версия без использования класса String.

length = 80;
char buffer[length] = {0};
if ((last_weight_time < millis() - 1000)) {
   last_weight_time = millis();
   while(Serial.available()) {
      Serial.readBytesUntil('\n', buffer, length);
   }
}

Другие ресурсы, о которых можно узнать

https://www.cplusplus.com/reference/cstring/ все функции для управления строками c и массивами для замены класса строк Arduino.

https://hackingmajenkoblog.wordpress.com/2016/02/04/the-evils-of-arduino-strings/

,

Re “_[распределение кучи] происходит для каждой итерации цикла while”: на ESP8266 это происходит один раз каждые 16 итераций, так как выделенный размер увеличивается кратно 16 байтам. Повторите “ _ если ваш цикл while находится в функции, часть кучи может освободиться при выходе функции_”: когда concat() не сможет вырастить строку на месте, он выделит новый буфер, скопирует существующие данные и освободит предыдущий буфер _ прямо_. Однако это не отменяет вашу точку зрения о риске фрагментации памяти., @Edgar Bonet


1

Короткий ответ: вообще не используйте класс String. Вам нужно ознакомиться с использованием строк C (которые являются просто массивами символов, заканчивающимися на нуль. Существуют функции библиотеки C для управления строками C. Вы захотите избежать printf, scanf и связанных с ними функций, так как библиотека, которая их поддерживает, довольно большая

Основная идея состоит в том, чтобы использовать статически выделенные строки C в качестве буферов. Определите наибольшее количество символов, которое вам нужно сохранить, и создайте статический массив символов, по крайней мере, на один символ больше (для завершающего значения null), и используйте его для сбора выходных данных, которые вы создаете.

,

printf() и друзья не оказывают большого влияния на пространство данных и не вызывают его фрагментации. Они занимают больше места в коде, чем Serial.print (), но большинство из нас не пишут скетчи, которые превышают лимит места в коде., @JRobert

@Duncan C - Большое вам спасибо за ваш ответ. Не могли бы вы, пожалуйста, добавить пример кода, как мой код должен быть переписан ?, @Frodik

Что сказал @JRobert. Избегать строки-хороший совет, избегать printf, scanf-плохой совет в данном случае; они не вызывают проблем с кучей, и в этом заключается эта проблема., @romkey

Кроме того, ESP8266 имеет достаточно места для кода (flash) даже на небольшом варианте. На небольших микросхемах AVR семейство printf использует урезанную версию с удаленной поддержкой плавающей запятой для экономии места., @Majenko

Приятно узнать о сокращенной версии printf/scanf. К оператору: не обращайте внимания на эту часть моего ответа, @Duncan C

даже при включении поддержки printf с плавающей запятой с помощью флагов компоновщика это влияет только на размер флэш-памяти, добавляя около 1500 байт, это не влияет на использование SRAM., @hcheung


0

SafeString-отличный способ пойти.. только один экземпляр каждого SafeString может когда-либо существовать, и u может объявить длину каждого SafeString..

,