Главная chevron_right Переменные и константы chevron_right Утилиты chevron_right PROGMEM

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 — это модификатор переменной. Разные версии компилятора допускают разное расположение ключевого слова, но на практике лучше выбрать один из проверенных вариантов и придерживаться его:

  • PROGMEM
  • const dataType variableName[] PROGMEM = {}; // use this form
  • const 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_*.

Полезные материалы