В документации 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"));
string_table
массива. Этот массив может иметь размер 20 КБ и никогда не помещается в память (даже временно). Однако вы можете загрузить только один индекс, используя вышеуказанный метод.