PROGMEM: нужно ли копировать данные с флэш-памяти в RAM для чтения?

8

У меня есть некоторые трудности с пониманием управления памятью.

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

Но я озадачен этими инструкциями, чтобы просто читать и печатать данные из памяти программы:

strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i]))); // Necessary casts and dereferencing, just copy. 
    Serial.println( buffer );

С какой стати я должен скопировать этот чертов контент в оперативную память перед тем, как получить к нему доступ? И если это правда, что происходит со всем кодом тогда? Он также загружается в оперативную память перед выполнением? Как тогда обрабатывается код (32 кБ) только с 2 кБ ОЗУ? Где эти маленькие гоблины с дискетами?

И еще интереснее: что происходит с буквальными константами, как в этом выражении:

a = 5*(10+7)

5, 10 и 7 действительно копируются в ОЗУ перед загрузкой их в регистры? Я просто не могу в это поверить.

Ariser - восстановить Монику
источник
Глобальная переменная загружается в память и никогда не освобождается от нее. Приведенный выше код копирует данные в память только при необходимости и освобождает их по завершении. Также обратите внимание, что приведенный выше код читает только один байт из string_tableмассива. Этот массив может иметь размер 20 КБ и никогда не помещается в память (даже временно). Однако вы можете загрузить только один индекс, используя вышеуказанный метод.
Гербен
@ Гербен: Это реальный недостаток глобальных переменных, я еще не учел это. У меня сейчас болит голова. И фрагмент кода был всего лишь примером из документации. Я воздержался от программирования на что-либо. сам, прежде чем получить разъяснение о понятиях. Но теперь у меня есть понимание. Спасибо!
Ariser - восстановить Монику
Документация показалась мне несколько запутанной, когда я впервые прочитал ее. Попробуйте посмотреть на некоторые примеры из реальной жизни (например, в библиотеке).
Гербен

Ответы:

10

AVR - это модифицированное семейство гарвардской архитектуры , поэтому код хранится только во флэш-памяти, тогда как при манипулировании данные существуют в основном в оперативной памяти.

Имея это в виду, давайте ответим на ваши вопросы.

С какой стати я должен скопировать этот чертов контент в оперативную память перед тем, как получить к нему доступ?

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

И если это правда, что происходит со всем кодом тогда? Он также загружается в оперативную память перед выполнением?

Нет. Гарвардская архитектура. Смотрите страницу Википедии для получения полной информации.

Как тогда обрабатывается код (32 кБ) только с 2 кБ ОЗУ?

Преамбула, сгенерированная компилятором, копирует данные, которые должны быть модифицированы / модифицированы в SRAM перед запуском фактической программы.

Где эти маленькие гоблины с дискетами?

Не знаю. Но если ты увидишь их, я ничем не смогу помочь.

... 5, 10 и 7 действительно копируются в ОЗУ перед загрузкой их в регистры?

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

Игнасио Васкес-Абрамс
источник
Хорошо, я не знал, что AVR был Гарвардом. Но я знаком с этой концепцией. Помимо гоблинов, я думаю, что теперь знаю, когда использовать эти функции копирования. Я должен ограничить использование PROGMEM данными, которые редко используются для сохранения циклов ЦП.
Ariser - восстановить Монику
Или измените свой код, чтобы использовать его прямо из флэш-памяти.
Игнасио Васкес-Абрамс
Но как будет выглядеть этот код? скажем, у меня есть несколько массивов uint8_t, представляющих строки, которые я хочу поместить на ЖК-дисплей через SPI. const uint8_t test1[5]= { 0x54, 0x65, 0x73, 0x74, 0x31 }; const uint8_t bla[9]= { 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x62 }; const uint8_t Menu[4]= { 0x3d, 0x65, 0x6e, 0x75};как перенести эти данные во флэш-память, а затем в функцию SPI.transfer (), которая принимает один вызов uint8_t за вызов.
Ariser - восстановить Монику
2
nongnu.org/avr-libc/user-manual/pgmspace.html
Игнасио Васкес-Абрамс
8

Вот как Print::printпечатается из памяти программы в библиотеке Arduino:

size_t Print::print(const __FlashStringHelper *ifsh)
{
  const char PROGMEM *p = (const char PROGMEM *)ifsh;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

__FlashStringHelper*является пустым классом, который позволяет перегруженным функциям, таким как print, дифференцировать указатель на программную память от одной до обычной памяти, так как обе эти функции видятся const char*компилятором (см. /programming/16597437/arduino-f- что делает на самом деле )

Таким образом, вы можете перегрузить printфункцию для вашего ЖК-дисплея так, чтобы она принимала __FlashStringHelper*аргумент, давала его вызвать LCD::print, а затем использовала lcd.print(F("this is a string in progmem"));' to call it.F () `- макрос, который гарантирует, что строка находится в памяти программы.

Для предопределения строки (чтобы быть совместимым со встроенной печатью Arduino) я использовал:

const char firmware_version_s[] PROGMEM = {"1.0.2"};
__FlashStringHelper* firmware_version = (__FlashStringHelper*) firmware_version_s;
...
Serial.println(firmware_version);

Я думаю, что альтернативой будет что-то вроде

size_t LCD::print_from_flash(const char *pgms)
{
  const char PROGMEM *p = (const char PROGMEM *) pgms;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

что позволит избежать __FlashStringHelperброска.

geometrikal
источник
2

В документации Arduino говорится, что можно сохранять константы, такие как строки или что-либо, что я не хочу менять во время выполнения, в памяти программы.

Все константы изначально находятся в памяти программы. Где еще они будут, когда питание отключено?

Я думаю, что он встроен где-то в сегмент кода, что должно быть вполне возможно в архитектуре фон Неймана.

Это на самом деле гарвардская архитектура .

С какой стати я должен скопировать этот чертов контент в оперативную память перед тем, как получить к нему доступ?

Вы не Фактически существует аппаратная инструкция (LPM - Загрузка памяти программ), которая перемещает данные непосредственно из памяти программ в регистр.

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

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

Разборка этих строк показывает (частично):

  f1a:  e4 91           lpm r30, Z+
  f1c:  e0 93 c6 00     sts 0x00C6, r30

Вы можете видеть, что байт памяти программ был скопирован в R30, а затем немедленно сохранен в регистр USART UDR0. ОЗУ не задействовано.


Однако есть сложность. Для обычных строк компилятор ожидает найти данные в оперативной памяти, а не в PROGMEM. Это разные адресные пространства, поэтому 0x200 в ОЗУ отличается от 0x200 в PROGMEM. Таким образом, компилятор сталкивается с проблемой копирования констант (например, строк) в ОЗУ при запуске программы, поэтому ему не нужно беспокоиться о том, чтобы узнать разницу позже.

Как тогда обрабатывается код (32 кБ) только с 2 кБ ОЗУ?

Хороший вопрос. Вам не сойдет с рук более 2 КБ постоянных строк, потому что не будет места, чтобы скопировать их все.

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

Но я озадачен этими инструкциями, чтобы просто читать и печатать данные из памяти программы:

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

// Print a string from Program Memory directly to save RAM 
void printProgStr (const char * str)
{
  char c;
  if (!str) 
    return;
  while ((c = pgm_read_byte(str++)))
    Serial.print (c);
} // end of printProgStr

Если вы передаете этой функции указатель на строку в PROGMEM, она выполняет «специальное чтение» (pgm_read_byte), чтобы извлечь данные из PROGMEM, а не из ОЗУ, и распечатать их. Обратите внимание, что это занимает один дополнительный тактовый цикл на байт.

И еще интереснее: что происходит с литеральными константами, такими как в этом выражении a = 5*(10+7), когда 5, 10 и 7 действительно копируются в ОЗУ перед загрузкой их в регистры? Я просто не могу в это поверить.

Нет, потому что они не должны быть. Это скомпилирует в инструкцию «загрузить литерал в регистр». Эта инструкция уже есть в PROGMEM, поэтому с литералом теперь разбираются. Не нужно копировать его в оперативную память, а затем читать обратно.


У меня есть длинное описание этих вещей на странице. Сохранение постоянных данных в памяти программ (PROGMEM) . Это пример кода для настройки строк и массивов строк, достаточно легко.

Здесь также упоминается макрос F (), который является простым способом простой печати из PROGMEM:

Serial.println (F("Hello, world"));

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

Эту технику достаточно просто использовать для других целей, кроме последовательного (например, для вашего ЖК-дисплея), выводя печать из класса «Печать».

Например, в одной из библиотек LCD, которые я написал, я сделал именно это:

class I2C_graphical_LCD_display : public Print
{
...
    size_t write(uint8_t c);
};

Ключевым моментом здесь является получение из Print и переопределение функции «write». Теперь ваша переопределенная функция делает все, что нужно для вывода символа. Поскольку он получен из Print, теперь вы можете использовать макрос F (). например.

lcd.println (F("Hello, world"));
Ник Гаммон
источник