Использование программной памяти в ESP8266 по сравнению с AVR, а также как обрабатывать большие динамические строки

Это довольно сложный вопрос, связанный с реализацией и лучшими практиками ESP8266. В идеале я надеюсь, что на него ответят люди, хорошо разбирающиеся в проблеме.

Как вы, надеюсь, знаете, платформа Arduino (AVR) требует использования инструкций PROGMEM или F(), чтобы избежать копирования всех строк в ОЗУ. Платформа ESP8266 также предоставляет инструкцию PROGMEM, но она отображается в памяти.

Поэтому у меня есть несколько вопросов о том, что на самом деле делает инструкция PROGMEM на ESP8266.

В следующих нескольких вопросах, если не указано иное, я имею в виду строки C в стиле символьного массива, а не тип строки Arduino.

  1. Хранение постоянных строк. Если бы я объявил строку (пока глобальную) на ESP8266, будет ли она считываться из флэш-памяти только при доступе к ней или всегда будет занимать ОЗУ (как если бы на AVR. Предположим, что в этом случае я НЕ объявляю его как PROGMEM; я хочу узнать, каково поведение по умолчанию на ESP8266)?

  2. Локальные константные строки. Что происходит со строками, используемыми внутри функции? Я совершенно уверен, что они ТАКЖЕ существуют в ОЗУ, по крайней мере, в AVR. Делает ли ESP8266 то же самое или он (потенциально) сохраняет его во флэш-памяти до тех пор, пока он не будет прочитан (это нормально, если он окажется где-то внутри кеша, если его можно будет вытеснить, если это необходимо)?

  3. Использование String(F("mystring")): Если я таким образом оборачиваю строку PROGMEM, действительно ли это преобразует строку в строку на основе RAM только при достижении этой строки, или это по какой-то причине инициализировать String() раньше (и все равно потреблять ОЗУ)? Различается ли такое поведение систем?

  4. Если я определяю несколько экземпляров вышеперечисленного (например, идентичная обернутая строка, показанная выше, появляется несколько раз, возможно, в разных функциях), все ли они указывают на ОДИНАКОВЫЙ экземпляр F() (поскольку они идентичны) или он дублируется, поскольку является частью кода, а не переменной, на которую ссылаются? На ESP8266? На AVR?

  5. На ESP8266 я обнаружил, что функция server.on() для встроенного веб-сервера прекрасно считывала константные строки F(), но не работала, когда размер файла превышал размер БАРАН. Он был явно сохранен правильно, но не мог быть прочитан. Таким образом, я предполагаю, что этот метод создавал локальную копию в ОЗУ, даже если файл хранился во флэш-памяти. Была также версия on_P, которая работала после этого момента (я полагаю, что на этом этапе я также использовал PSTR, а не F, поскольку - поправьте меня, если я ошибаюсь - они несовместимы). Это реальная цель PROGMEM на ESP8266? Что данные хранятся только во Flash, но также сообщают читающей их функции, что нужно делать как можно меньше копий или разбивать их на части?


Мне также любопытно, что происходит с большими строками, которые не являются постоянными. Веб-сервер может возвращать данные, которые были переданы, например, через строку запроса, которая в моем случае может быть довольно большой. Если я копирую эти данные в строку (а затем в функцию), передается ли указатель кучи напрямую или каждая строка создает новую копию? Меня беспокоит фрагментация или другие забавные способы исчерпания оперативной памяти, даже если теоретически должна поместиться одна копия.

Помогает ли выполнение чего-то вроде bool myFunction(String &str){...}? Какой-то способ фактически гарантировать, что это одна и та же строка, а не копирование (и могу ли я сделать String &myStr=server.arg(...) -- это возвращаемое значение, отличное от назначение)? Меня это сбивает с толку, поскольку строки явно передаются по значению, несмотря на то, что это сложный тип данных с динамической памятью где-то там. Итак, переопределили ли они оператор присваивания (и/или тот, который обрабатывает передачу ссылки, поскольку я думаю, что их больше одного), чтобы дублировать строку, как обычно делает передача по значению базового типа, или они совместно используют указатели? (и, следовательно, память), как обычно делает класс, передаваемый по значению?

, 👍0


1 ответ


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

2

Флэш-память ESP8266 — это особый случай. Поскольку сам ESP8266 не имеет флэш-памяти, а вместо этого использует внешний чип флэш-памяти, подключенный через SPI, доступ к нему не является простой или технически очевидной операцией.

Чтение с флэш-чипа происходит медленно. Очень-очень медленно (по сравнению с чтением ОЗУ). Из-за этого, где это возможно, некоторые вещи копируются в оперативную память. Кроме того, существуют разные разделы оперативной памяти, предназначенные для разных целей.

  • dram0 — это ваша оперативная память «Данные». Здесь хранятся ваши переменные и тому подобное (всего 80 КБ).
  • iram1 — ОЗУ инструкций. Именно здесь необходимо хранить подпрограммы обработки прерываний для быстрого выполнения (32 КБ).
  • icache — кэш инструкций. 16 КБ данных, которые кэшируются при чтении с флэш-чипа. Исполняемый код обычно копируется сюда.

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

Из-за этого разъединения ЦП и строковых констант флэш-памяти, если не указано иное, во время запуска они будут скопированы в dram0. Это позволяет очень быстро и просто получить данные. Все, что вы не укажете как PROGMEM, будет скопировано в dram0. Обратите внимание, что вы не получаете все 80 КБ ОЗУ для игры, так как SDK использует его часть. Вы получаете максимум около 50 КБ (режим станции - режим softap будет использовать больше).

Теперь... что делает PROGMEM на ESP8266? Ну, он добавляет к определению атрибут, который указывает компоновщику поместить данные в определенный раздел во флэш-памяти (.irom.text.<filename>.<line>.< ;число>). Это предотвращает его копирование в dram0 при запуске.

Итак, как вы к этому пришли? Вот где в игру вступают варианты *_P строковых функций (и некоторые перегруженные функции в ядре Arduino).

Вся микросхема флэш-памяти отображается (или может быть, в зависимости от настроек регистра) в карту памяти ESP8266 (адрес 0x40200000). Это означает, что можно получить данные напрямую. Однако, поскольку этот доступ не проходит через блок оперативной памяти icache, он работает медленно. Очень очень медленно. По некоторым данным примерно в 12 раз медленнее. Кроме того, поскольку данные могут находиться в разных местах внутри чипа, адреса ваших строковых данных не могут быть напрямую жестко запрограммированы. Должна быть какая-то форма перенаправления «это там» при чтении данных (например, если вы используете обновления OTA, вы получаете два отдельных блока флэш-памяти, где может храниться ваш код — текущий исполняемый код и ранее исполняемая версия). Варианты строковых функций *_P принимают это во внимание и ищут, где во flash находится текущий код, прежде чем читать его.

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

Чтобы конкретно ответить на ваши вопросы:

  1. Строковые данные копируются в ОЗУ при запуске.
  2. Строковые данные копируются в ОЗУ при запуске.
  3. Строковые данные загружаются из флэш-памяти (с использованием вариантов функций *_P) при создании объекта String (ОЗУ выделяется в куче в этот момент времени). ). Оперативная память освобождается, когда объект String уничтожается (выходит за пределы области видимости).
  4. Компилятор недавно получил обновление, которое будет дедуплицировать константные строки PROGMEM.
  5. Как упоминалось выше, *_P копируется напрямую из флэш-памяти и работает медленно, но не имеет ограничений по размеру.
,

Это было очень полезно. Раздражает, что это не гарвардская система, но имеет тот же конечный эффект... :D Единственное, в чем я до сих пор не уверен, так это в том, что веб-сервер прочитал веб-страницу PROGMEM без необходимости в функции _P. Но, учитывая, что у него есть эта опция, я подозреваю, что он передает данные из флэш-памяти для последнего, а первый сначала делает копию в ОЗУ, что, конечно, не удалось, потому что страница очень большая., @RDragonrydr

@RDragonrydr Функция не должна иметь _P для чтения из флэш-памяти - это просто соглашение об именах в библиотеке C. Веб-сервер имеет несколько перегруженных функций, которые принимают разные типы данных, одним из которых является «флеш-строка»., @Majenko

Да, это то, что я заметил. Я полагаю, что это справедливое предупреждение, что они, похоже, оптимизировали версию _P. Другой работает до тех пор, пока вы не попытаетесь обслужить действительно большую страницу, и она внезапно выйдет из строя, вероятно, из-за невозможности выделить где-то буфер. Из-за этого мне пришлось переключиться на специальную версию программы, несмотря на совместимость., @RDragonrydr

Происходит ли вообще дедупликация строк? Я видел это (https://forum.arduino.cc/index.php?topic=194603.0) и задавался вопросом, было ли это реализовано для какой-либо системы или были бы добавлены упомянутые «изменения компилятора»., @RDragonrydr

@RDragonrydr Я только что провел несколько тестов, и да, похоже, теперь он выполняет дедупликацию строк для переменных const char *foo PROGMEM. Я только что сделал небольшой набросок, и с двумя разными строками («это foo» и «это bar») они в конечном итоге указывают на разные места. С обеими строками, содержащими «это foo», они обе указывают на один и тот же адрес., @Majenko