PROGMEM
PROGMEM размещает константные данные во flash-памяти AVR-плат, а не в SRAM — незаменим, когда оперативная память заканчивается из-за больших таблиц, строк или шрифтов.
Полная статья
Описание
PROGMEM — это модификатор, который указывает компилятору разместить константные данные во flash-памяти программы, а не загружать их в SRAM при старте. Это особенно важно на AVR-платах — Arduino Uno R3, Nano, Leonardo и им подобных, — где оперативная память измеряется буквально единицами килобайт.
Один-два числа погоды не делают, но как только в программе появляются большие таблицы, массивы строк, шрифты для дисплея или длинные списки сообщений — SRAM начинает таять на глазах. Именно для таких постоянных, неизменяемых данных и предназначен PROGMEM.
PROGMEM — часть возможностей PROGMEM. В Arduino IDE нужные заголовки для AVR обычно подключаются автоматически, но при необходимости можно добавить явно:
#include <avr/pgmspace.h>
На современных не-AVR платах — Due, MKR, GIGA R1 WiFi и других — обычный pgmspace.h нередко и так хранится в памяти программы. const там может присутствовать ради совместимости со старым кодом, но реальный смысл он несёт именно на AVR.
Синтаксис
const dataType variableName[] PROGMEM = {data0, data1, data2};
PROGMEM — это модификатор переменной. Разные версии компилятора допускают разное расположение ключевого слова, но на практике лучше выбрать один из проверенных вариантов и придерживаться его:
PROGMEMconst dataType variableName[] PROGMEM = {}; // use this formconst PROGMEM dataType variableName[] = {}; // or this one
Параметры
const dataType PROGMEM variableName[] = {}; // not this one: тип данных, которые нужно хранить во flash.dataType: имя массива или переменной.
Как читать данные из variableName
Переменная в PROGMEM физически находится в другой области памяти, поэтому обратиться к ней как к обычной переменной из SRAM не получится — вы получите мусор или ноль. Для чтения используются специальные функции: PROGMEM, pgm_read_byte_near(), pgm_read_word_near() и строковые функции с суффиксом pgm_read_ptr().
Выбор функции зависит от размера элемента: байт читается через pgm_read_byte_near(), слово (2 байта) — через pgm_read_word_near(), указатель — через pgm_read_ptr(). Не угадаете с размером — данные придут неправильными.
Пример чтения чисел и символов из flash-памяти:
// сохраняем несколько unsigned int
const PROGMEM uint16_t charSet[] = { 65000, 32796, 16843, 10, 11234};
// сохраняем несколько char
const char signMessage[] PROGMEM = {"I AM PREDATOR, UNSEEN COMBATANT. CREATED BY THE UNITED STATES DEPART"};
unsigned int displayInt;
char myChar;
void setup() {
Serial.begin(9600);
while (!Serial); // ожидаем подключения serial-порта. Необходимо для native USB
// код инициализации, выполняется один раз:
// считываем обратно 2-байтный int
for (byte k = 0; k < 5; k++) {
displayInt = pgm_read_word_near(charSet + k);
Serial.println(displayInt);
}
Serial.println();
// считываем обратно char
int signMessageLength = strlen_P(signMessage);
for (byte k = 0; k < signMessageLength; k++) {
myChar = pgm_read_byte_near(signMessage + k);
Serial.print(myChar);
}
Serial.println();
}
void loop() {
// основной код, выполняется в цикле:
}
Массивы строк
Таблицы строк — пожалуй, самый распространённый сценарий для _P: пункты меню на LCD, сообщения об ошибках, подсказки пользователю, названия режимов работы устройства. Строка сама по себе — это массив символов, а таблица строк превращается в двумерную структуру.
Типичный шаблон выглядит так: сначала каждая строка объявляется отдельно в PROGMEM, затем создаётся массив указателей на эти строки (тоже в PROGMEM), а при выводе нужная строка копируется во временный буфер в RAM с помощью strcpy_P или аналогичной функции.
/*
Демонстрация строк в PROGMEM
Как хранить таблицу строк в памяти программ (flash)
и извлекать их.
Информация взята из:
http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
Создание таблицы (массива) строк в памяти программ немного сложнее обычного,
но ниже приведён удобный шаблон для этого.
Настройка строк выполняется в два шага. Сначала объявляем сами строки.
*/
#include <avr/pgmspace.h>
const char string_0[] PROGMEM = "String 0"; // «String 0» и т.д. — строки для хранения, измените по необходимости.
const char string_1[] PROGMEM = "String 1";
const char string_2[] PROGMEM = "String 2";
const char string_3[] PROGMEM = "String 3";
const char string_4[] PROGMEM = "String 4";
const char string_5[] PROGMEM = "String 5";
// затем создаём таблицу ссылок на эти строки.
const char *const string_table[] PROGMEM = {string_0, string_1, string_2, string_3, string_4, string_5};
char buffer[30]; // убедитесь, что буфер достаточно велик для самой длинной строки
void setup() {
Serial.begin(9600);
while (!Serial); // ожидаем подключения serial-порта. Необходимо для native USB
Serial.println("OK");
}
void loop() {
/* Работа со строковой таблицей в памяти программ требует специальных функций для получения данных.
Функция strcpy_P копирует строку из памяти программ в строку в RAM («buffer»).
Убедитесь, что принимающая строка в RAM достаточно велика для хранения
извлекаемых данных из памяти программ. */
for (int i = 0; i < 6; i++) {
strcpy_P(buffer, (char *) pgm_read_ptr(&(string_table[i]))); // необходимые приведения типов и разыменование — просто скопируйте.
Serial.println(buffer);
delay(500);
}
}
Важное ограничение области видимости
Переменные с PROGMEM должны быть либо глобальными, либо объявленными как static внутри функции. Обычная локальная переменная для этого не подходит.
Такой код внутри функции работать не будет:
const char long_str[] PROGMEM = "Hi, I would like to tell you a bit about myself.\n";
А вот статическая переменная — допустимый вариант:
const static char long_str[] PROGMEM = "Hi, I would like to tell you a bit about myself.\n";
Макрос F()
Если вы пишете:
Serial.print("Write something on the Serial Monitor");
строковый литерал по умолчанию попадает в SRAM. Когда таких строк много — память заканчивается быстро. Макрос F() позволяет оставить строку во flash и загружать её только в момент вывода:
Serial.print(F("Write something on the Serial Monitor that is stored in FLASH"));
Это самый простой способ сэкономить SRAM на строковых константах без ручного объявления переменных.
Где применяется
- Таблицы данных: коэффициенты калибровки, таблицы синусов, гамма-коррекция — любые числовые массивы, которые не меняются.
- Строки интерфейса: пункты меню, сообщения на дисплее, подсказки — особенно актуально при работе с LCD или OLED.
- Шрифты и битмапы: графические данные для дисплеев занимают много места и идеально подходят для flash.
- Сообщения отладки: если используете много
Serial.print()с длинными строками,F()поможет не переполнить SRAM.
Практические замечания
- Используйте
PROGMEM, когда на AVR-плате заканчивается SRAM из-за больших константных массивов или строк. Если SRAM хватает — лишнее усложнение не нужно. - Всегда читайте данные через функции
pgm_read_*или строковые функции с суффиксом_P— прямое обращение к переменной даст неверный результат. - Следите за размером временного буфера в RAM: он должен вмещать самую длинную строку, которую вы копируете из flash, включая завершающий нулевой байт.
- Для современных не-AVR плат сначала проверьте, достаточно ли обычного
const— там архитектура памяти другая иPROGMEMможет не давать никакого выигрыша. - Если программа ведёт себя странно после добавления
PROGMEM-переменных, проверьте: не читаете ли вы данные обычным способом вместоpgm_read_*.