Как функция/метод может определить, является ли передаваемый массив const PROGMEM (flash) или нет (RAM)?

Может ли функция/метод узнать, находится ли переданный массив констант во флэш-памяти или в ОЗУ?

Если у меня есть метод или функция, которая получает массив констант, который находится в оперативной памяти, массив объявляется следующим образом: const uint8_t MY_ARRAY[] = { 1, 2, 3, 4 };

передается в метод или функцию следующим образом: MyFunction(MY_ARRAY);

и метод или функция могут обрабатывать это следующим образом:

void MyFunction(const uint8_t *MY_ARRAY) {
     uint8_t secondElement = MY_ARRAY[1];
     ...

Однако, если массив находится во Flash (поскольку это постоянный массив, поэтому он должен быть там), то объявление добавляет PROGMEM: const uint8_t PROGMEM MY_ARRAY[] = { 1, 2, 3, 4 };

Пропуск выглядит так же: MyFunction(MY_ARRAY);

Объявление функции выглядит так же, но для получения правильных значений необходимо использовать pgm_read_byte_near:

void MyFunction(const uint8_t *MY_ARRAY) {
     uint8_t secondElement = pgm_read_byte_near(MY_ARRAY+1);
     ...

Как метод получения может узнать, находится ли массив во флэш-памяти (PROGMEM) или в ОЗУ (без PROGMEM), чтобы он знал, использовать pgm_read_byte_near или нет? В идеале мне нужна ошибка компилятора, но тип тот же (оба являются константными массивами uint8_t).

Если pgm_read_byte_near используется, когда этого не должно быть, или не используется, когда должно быть, результаты будут мусором.

Некоторые актуальные вопросы: Как передать статический массив const (programmem) в функцию

Вопрос об использовании оперативной памяти: PROGMEM, const и #define

Сохранение массива в PROGMEM

ПРОГРАММА: нужно ли копировать данные из флэш-памяти в ОЗУ для чтения?

ОБНОВЛЕНИЕ: Похоже, то, что я хочу сделать, невозможно. Ответы ниже представляют собой очень интересные варианты: использование __flash на простом C, использование ООП для создания держателей данных PROGMEM и не-PROGMEM или попытка (каким-то образом?) использовать неустаревшие типы данных, специфичные для программ, из avr/gpmspace.h.

Лучший ответ на мой первоначальный вопрос заключается в том, что метод не может сделать это определение. Лучшим решением, согласно комментарию Delta_G, является создание обычных (ram) и "_P" (flash/programmem) версий функций и максимально очевидных имен этих функций.

, 👍5

Обсуждение

Вы можете использовать тот же трюк, что и классы Arduino. класс __FlashStringHelper; #define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal))) и перегрузка функции, @KIIV

@KIIV Звучит очень многообещающе. Можете ли вы показать мне, как будет выглядеть объявление функции, или дать несколько ссылок о том, как это сделать?, @Casey

Посмотрите на класс Print в ядре Arduino., @Delta_G

void someFunction(const __FlashStringHelper *str); и назовите его как someFunction(F("soometning"));, @KIIV

Но ввод представляет собой массив uint8_t, а не строку. Он все еще работает?, @Casey

Не совсем так, но если вы посмотрите на ядро Arduino, как все это работает, вы сможете сделать что-то подобное. Вы можете сделать это или у вас могут быть две разные функции. Но вы не можете сделать так, чтобы пользователь не должен был знать разницу. Они либо должны будут объявить свои данные с вашим конкретным типом данных, который вы создаете для своей перегрузки, либо им придется привести к нему, либо им нужно будет знать, чтобы вызвать другую функцию. Вы не можете снять это решение с них в любом случае., @Delta_G

Вам придется создать свой собственный фиктивный класс, аналогичный __FlashStringHelper, для каждого типа, который вы хотите сохранить во флэш-памяти. Мягко говоря не очень удобно..., @Edgar Bonet

Или вы можете использовать arduino на базе Atmega4809, так как адресное пространство флэш-памяти отображается в адресное пространство памяти с начальным смещением 0x4000 (и компилятор позаботится об этом, если это константная переменная), @KIIV

После изучения WString.h и pgmspace.h это выходит за рамки моих возможностей C++. Также может показаться, что вызывающая сторона вынуждена использовать макрос типа F(), и если я могу быть уверен, что они это сделают, то я могу быть уверен, что он будет использовать PROGMEM. Думаю, я могу добавить некоторые обозначения, вероятно, включив слово «PROGMEM» в имя функции, а также во входное имя для массива flash const, и просто надеюсь, что все вызывающие абоненты помнят контракт PROGMEM., @Casey

Это то, что вам в конечном итоге придется сделать. Вы выпускаете с двумя версиями кода и примечанием, что если пользователь хочет использовать PROGMEM, он может, но он должен вызывать версию функции _P или как бы вы ее ни называли. Часто это проще, чем пытаться использовать оба способа, потому что это напоминает пользователю прямо в названии функции, и это заставляет их вернуться к документации, чтобы увидеть, как ее нужно настроить., @Delta_G

@Delta_G Спасибо. Я испортил использование или неиспользование pgm достаточное количество раз, и я надеялся, что есть способ решить эту проблему раз и навсегда (ожидая, что я сделаю это снова). О, ладно, больше аппаратного тестирования. Если вы напишите это как решение, я могу принять его., @Casey

все в этом вопросе, комментариях и ответах предполагает классический AVR. он отличается для esp8266 и пакета плат Arduino megaavr, @Juraj

@KIIV Пожалуйста, не отвечайте на вопросы в комментариях. См. [Как работают комментарии?](https://meta.stackexchange.com/questions/19756/how-do-comments-work). Они **не** для ответа на вопрос., @Nick Gammon

@Delta_G Ты тоже. Это не форум. Вся идея здесь в том, что вы делаете **фактический ответ**. Затем люди могут проголосовать за него, и он может быть принят. **Бонус** — за это вы получаете репутацию. Не от ответов в комментариях., @Nick Gammon

И вы, другие ребята., @Nick Gammon


4 ответа


7

Боюсь, что у этой проблемы нет хорошего решения. Один вариант делаю например, использовать квалификатор __flash вместо PROGMEM:

const uint8_t ram_array[] = { 1, 2, 3, 4 };
__flash const uint8_t flash_array[] = { 5, 6, 7, 8 };

void function_reading_ram(const uint8_t *array)
{
    uint8_t secondElement = array[1];
    // ...
}

void function_reading_flash(__flash const uint8_t *array)
{
    uint8_t secondElement = array[1];
    // ...
}

int main(void)
{
    function_reading_ram(ram_array);      // ХОРОШО
    function_reading_flash(flash_array);  // ХОРОШО
    function_reading_ram(flash_array);    // Предупреждение
    function_reading_flash(ram_array);    // Предупреждение
}

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

Предупреждения, генерируемые gcc:

warning: conversion from address space ‘__flash’ to address space ‘generic’ [-Waddr-space-convert]
     function_reading_ram(flash_array);
     ^
warning: conversion from address space ‘generic’ to address space ‘__flash’ [-Waddr-space-convert]
     function_reading_flash(ram_array);
     ^

Учитывая, насколько хорошо это решение, вы можете удивиться, почему я написал: «Нет хорошее решение». Ну, здесь есть две оговорки, одна маленькая и одна огромный. Небольшое предостережение заключается в том, что вам нужно явно передать опцию -Waddr-space-convert в компилятор, если вы хотите, чтобы он генерировал предупреждения. Это не подразумевается даже с -Стена -Wextra. Огромная оговорка заключается в том, что это работает только на чистом C. Если вы попытаетесь использовать этот трюк в C++, вы получите:

error: ‘__flash’ does not name a type

Это реализация расширения «именованных адресных пространств» для стандартный C. Поскольку ничего подобного в C++ не стандартизировано, авторы gcc предположил, что это не будет полезно для пользователей C++. :-( Вы можете смешивать C и исходники C++ в Arduino, но вы не можете вызывать методы класса из C.

,

Это хорошо знать. Я раньше не слышал о _flash. Тем не менее, у меня есть классы в моих классах, поэтому я застрял на С++ в этом., @Casey


4

Возможно также объектно-ориентированное решение с использованием шаблона проектирования стратегии, но оно сопряжено с некоторыми (небольшими) потерями памяти и производительности. Для этого вам нужно использовать C++.

  • Поместите каждый массив, который вы хотите различать между флэш-памятью/SRAM, в отдельный класс (по одному для каждого типа). Вы получаете сокрытие данных и возможное разделение ответственности бесплатно.
  • Вместо того, чтобы помещать массив в каждый класс, поместите ссылку/указатель или используйте его напрямую.
  • Вы можете использовать наследование:
    • Создайте класс Flash Array со средством чтения/записи массива SRAM
    • Создайте класс массива SRAM с помощью устройства чтения/записи массива флэш-памяти
    • Наследуйте каждый из используемых вами типов массивов от одного из этих классов, где базовый класс заботится о чтении/записи.
  • Альтернативное решение: используйте шаблон стратегии; требует немного больше работы, но вы можете предотвратить множественное наследование (хотя, поскольку вы сейчас используете C, вы не используете наследование (пока)). Это будет включать примерно:
    • Создайте средство чтения классов Flash и SRAM Array, как указано выше.
    • Вместо наследования создайте указатель на один из этих классов, соответствующий типу массива.

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

Объяснение использования наследования

(запрошено комментарием)

@Casey Я полагаю, вы имеете в виду способ наследования. Предположим, что в классе у вас есть x_array типа FlashArray. Вы просто вызываете x_array.Read(...). Этот метод реализован в FlashArray, но также с теми же параметрами в RamArray. Вы можете применить это, унаследовав как FlashArray, так и RamArray от базового класса (например, ArduinoArray и создав (чистую) виртуальную функцию Read с теми же аргументами. В любом случае реализация метода Read в FlashArray должна читать из Flash, а тот же метод в RamArray должен читать из оперативной памяти.

При изменении типа x_array с FlashArray на RamArray или наоборот ничего менять не нужно (кроме виды). Чтобы упростить эту задачу, рассмотрите возможность определения типа для x_array, чтобы впоследствии, когда вам понадобится его изменить, вам нужно будет сделать это только в одном месте.

,

Преимущество двух отдельных классов заключается в том, что вы можете создать политику доступа к данным для каждого класса, например, «для этого класса я хотел бы скопировать данные в текущий фрейм стека, прежде чем работать с ним». Учитывая обычно небольшой объем программ Arduino, дублирование кода, вероятно, все еще более читабельно, чем шаблонный монстр, который пытается его избежать. Типичные классы данных, с которыми я работаю, имеют конструктор "из флэш-памяти", в котором они копируют все элементы из структуры без каких-либо методов, а компилятор оптимизирует копии для неиспользуемых элементов., @Simon Richter

@SimonRichter Спасибо за дополнительный комментарий. Я почти никогда не программирую производительность заранее, если только не знаю, что это будет проблемой. Как вы сказали, в настоящее время компиляторы многое оптимизируют., @Michel Keijzers

Да, в этом его прелесть: я могу позже изменить характеристики производительности, изменив связь между классами «in-flash» и «in-ram», не меняя пользователей этих классов., @Simon Richter

@SimonRichter, вы даже можете изменить его динамически во время выполнения скетча)., @Michel Keijzers

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

@Casey Я добавил пример в свой ответ., @Michel Keijzers

Или вы можете иметь один класс и перегрузить метод read()., @Edgar Bonet

@EdgarBonet Это также возможно, хотя мне больше нравится более аккуратное разделение, поскольку ArduinoArray больше подходит в качестве абстрактного базового класса. Но на самом деле возможны оба пути., @Michel Keijzers


2

Классический способ avr-gcc – это специальные функции с разными именами

например, void* memcpy(void* dest, const void* src, size_t n); // Работает, только если src — это оперативная память

по сравнению с void* memcpy_P(void *dest, PGM_VOID_P src, size_t n); // Работает, только если src равен PROGMEM

Кстати: это уже упоминалось в комментарии к вопросу @Delta_G. Подробнее о примере memcpy_P здесь

,

PGM_VOID_P очень похож на то, что мне нужно. Могу ли я просто использовать это, чтобы убедиться, что указатель (на самом деле массив) находится в PROGMEM? Или мне нужна другая версия для массивов uint8_t?, @Casey

Я попытался заменить объявление «const uint8_t *» на «PGM_VOID_P», но это не сработало, потому что это разные типы. В avr/pgmspace.h есть несколько типов данных PROGMEM, таких как prog_uint8_t, но все они устарели. pgmspace.h рекомендует использовать атрибут во время объявления. Итак, похоже, что то, что я хочу сделать, возможно, но это больше не так., @Casey


5

Здесь я расширю комментарий KIIV.

Эта проблема известна тем, кто написал платформу Arduino. По сути, указатели на PROGMEM и RAM являются указателями данных (думайте об указателе как об индексе массива, но для всего RAM/Flash, а не для одного его фрагмента, которым вы управляете) одинакового размера и типа. Их нельзя отличить друг от друга, поскольку типы указателей определяются только размером данных, а не их расположением.

Итак, они использовали умное решение: сделать указатель другим указателем, который не совместим с обычным указателем RAM. Это делается путем приведения к классу __FlashStringHelper (и последующего приведения, когда его необходимо прочитать). Класс FSH на самом деле не имеет никаких методов и ничего не делает; он просто используется для определения пользовательского типа указателя, отличного от обычного const *.

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

Во-первых, вам нужно привести массив к классу FSH:

const byte MyProgmemArray[] PROGMEM = {4,4,4,4,4,4,4,4}; //Исправляет проблему №82 (добавляем случайность); подробности см. на https://xkcd.com/221/: D
__FlashStringHelper * MyArray = (__FlashStringHelper *) MyProgmemArray;

Я не знаю, есть ли какой-либо метод (и я искал), чтобы объявить их на месте, то есть сделать исходное объявление массива напрямую * __FlashStringHelper. Таким образом, вам все равно нужно, как программисту/пользователю, знать, какие строки находятся во флэш-памяти, когда вы их добавляете, и добавлять строку преобразования. Это означает, что то, что вы хотели (полностью автоматическое определение ОЗУ/флэш-памяти), вполне невозможно, но это, насколько я думаю, близко. Еще одно предупреждение заключается в том, что исходный массив все еще существует, а это означает, что он может быть случайно использован вместо него (все равно вызовет исходную проблему несоответствия), а новое определение занимает немного больше памяти для дополнительного указателя (если компилятор не оптимизирует его?).

Как ни странно, он отлично работает, если вы хотите сохранить строку:

__FlashStringHelper * MyString = PSTR("yaddayaddayadda...");

Строки могут быть встроенными, но я думаю, что это из-за макроса PSTR и потому, что определения строк могут обрабатываться иначе, чем массив значений (даже если они физически одно и то же). Возможно, вы могли бы поэкспериментировать с тем, как работает PSTR (на самом деле это макрос, который что-то делает, а не просто ключевое слово) и посмотреть, сможете ли вы реализовать это в своем собственном коде.


Теперь вам все еще нужно получить данные из массива в той функции, которую вы упомянули. Для этого мы фактически используем две версии этой функции. Это называется перегрузкой (и я думаю, что это только C++, но это Arduino, который по определению является C++, так что это безопасно).

Функция ОЗУ, которую вы объявляете как обычную:

void MyFunction(const uint8_t *MY_ARRAY){
  byte index_two = MY_ARRAY[1];
  ...
}

Функция PROGMEM, которую вы объявляете следующим образом (при условии, что pgm_read_byte_near - это правильная функция. Также обратите внимание, что она усложняется, если вам нужны многобайтовые значения, такие как int или что-то в этом роде...):

void MyFunction(__FlashStringHelper *MY_PROGMEM_ARRAY){
  const byte *MY_ARRAY = (const byte *)MY_PROGMEM_ARRAY; // конвертируем обратно в обычный
  byte index_two = pgm_read_byte_near(MY_ARRAY+1); //конечно, все равно нужно читать как flash
  ...
}

В качестве бонуса к информации, которая не связана с вопросом, заданным, но может заинтересовать вас/быть актуальным позже:

Есть еще несколько замечаний, если вы используете это для строковых данных (или притворяетесь, что это строка для этой части, что работает, если ваши данные не содержат нулей и заканчиваются единицей). Под последним я подразумеваю тот факт, что строки в C (НЕ C++ std::string или String Arduino, так что имейте это в виду) в основном являются char массивы с 0 на конце. Все функции обработки строк выполняют бесконечный цикл до тех пор, пока не прочитают 0 в своем текущем индексе, что позволяет им работать со строками любой длины, но при этом знать, когда они сделаны. В результате при чтении вы можете обращаться с данными как со строкой, но с тремя оговорками:

  1. Он никогда не может содержать 0, за исключением самого последнего элемента, который не будет прочитан.
  2. При его определении вам все равно придется использовать {} вместо "", поскольку большинство значений не содержат печатных символов ASCII. (И даже в этом случае вам придется написать " " для 32, например.)
  3. На самом деле вы не можете напечатать предполагаемую строку, так как данные не будут содержать печатных символов. Вы получите любое сочетание цифр, букв, знаков препинания и забавных вещей, таких как пробел и новая строка, смешанные вместе. Вам нужно будет прочитать каждый символ и преобразовать его (например, с помощью String num = <your value here> или itoa() и т. д.).

Если вы хотите напечатать его, используя существующую функцию Arduino, например Serial.print, и это фактически строка (а не данные, притворяющиеся строкой), она изначально поддерживает __FlashStringHelper * , поэтому вам не нужен шаг преобразования (это позволяет вам выполнить Serial.print(F("Здесь не используется статическая RAM!")); и тому подобное).

Однако существует два способа чтения данных, если они предназначены для вашей собственной функции обработки строк. Первый, простой, не использует память и создает полную копию данных или строки в ОЗУ во время использования. В результате он не способен обрабатывать строки, которые слишком велики для ОЗУ, и могут возникнуть проблемы, если он вызывается, когда много ОЗУ уже используется, или он вызывается глубоко внутри набора вложенных функций. Однако это очень удобно:

//преобразование из __FlashStringHelper * как указано выше, но давайте предположим, что здесь это char*
int size = strlen_P(MY_ARRAY);
if (size!=0){
  char * data = new char[size];
  if (data!=NULL){
    strcpy_P(data,MY_ARRAY);
    // делаем что-то со строкой
    delete data;
  }
}

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

,