Работа с PROGMEM памятью

Часто бывает нужно сохранить в памяти микроконтроллера большой объём данных, которые не будут меняться в процессе работы, например:

  • Калибровочный массив
  • Текст названий пунктов меню
  • Просто какой-то текст
  • Посчитанная тригонометрия (синус, косинус)
  • Изображения для дисплея (bitmap)
  • И многое другое

Хранить такие данные в оперативной памяти (в виде обычной переменной) – не самая лучшая идея, ведь они не будут меняться, а место займут! Напомню, что оперативной памяти всегда гораздо меньше, чем программной (Flash) памяти: в той же ATmega328 (Arduino UNO/Nano/Pro mini) – 32 кб Flash и 2 кб SRAM, SRAM в 16 раз меньше! Так что гораздо эффективнее хранить такие данные во Flash, он же программная память, он же program memory, он же PROGMEM. Но как? Мы привыкли к тому, что переменные мы можем менять во время выполнения программы, на то они и переменные, на то и память называется динамической. А вот с Flash памятью всё не так просто – писать в неё может только программатор, при помощи которого загружается код программы, либо загрузчик (bootloader), который практически выполняет функцию программатора. Есть кстати модифицированный загрузчик, который позволяет иметь доступ к Flash памяти прямо из программы, но в этих уроках мы рассматриваем стандартные средства, в данном случае – утилиту PROGMEM. Для работы с PROGMEM используется встроенная библиотека avr/pgmspace.h, подключать её не нужно, она подключится сама (в версиях Arduino IDE выше 1.0).

Запись


Ключевое слово (модификатор переменной) PROGMEM позволяет записать данные во Flash память. Синтаксис такой:

const тип_данных data[] PROGMEM = {};   // так 
const PROGMEM тип_данных data[] = {};   // или так

Всё! Данные, в показанном случае массивы тип_данных будут помещены во Flash память. PROGMEM может работать со всеми целочисленными типами (8, 16, 32, 64 бита), float и char. Важный момент! Модификатор PROGMEM можно применять только к глобальным (определённым вне функций) или статическим (глобальным или локальным, но со словом static) переменным! Читай урок про типы данных, если забыл. Полный список возможностей pgmspace можно посмотреть в документации.

Чтение


Если с записью всё очень просто (добавляется ОДНО ключевое слово), то с чтением всё гораздо интереснее: оно осуществляется при помощи специальной функции. Основная функция чтения из progmem – pgm_read_тип(адрес). Мы можем использовать вот эти 4:

  • pgm_read_byte(data); – для 1-го байта (char, byte, int8_t, uint8_t)
  • pgm_read_word(data); – для 2-х байт (int, word, unsigned int, int16_t, int16_t)
  • pgm_read_dword(data); – для 4-х байт (long, unsigned long, int32_t, int32_t)
  • pgm_read_float(data); – для чисел с плавающей точкой

Где data – адрес (или указатель) сохранённого блока данных! Вспомните урок про указатели, чтобы понимать, о чём речь. Полный список возможностей pgmspace можно посмотреть в документации.

Одиночные числа


Рассмотрим простой пример – запись и чтение одиночных чисел:

const uint16_t data PROGMEM = 125;
const int16_t signed_data PROGMEM = -654;
const float float_data PROGMEM = 3.14;

void setup() {
  Serial.begin(9600);
  Serial.println(pgm_read_word(&data)); // выведет 125

  uint16_t *dataPtr = &data;  // попробуем через указатель
  Serial.println(pgm_read_word(dataPtr));  // выведет 125

  Serial.println(pgm_read_word(&signed_data));  // выведет 64882
  Serial.println((int16_t)pgm_read_word(&signed_data));  // выведет -654

  Serial.println(pgm_read_float(&float_data));  // выведет 3.14
}

void loop() {}

Что здесь важно помнить: читая отрицательные числа (например типы int и long) нужно обязательно приводить тип, потому что PROGMEM хранит числа в беззнаковом представлении. Обратите внимание на чтение signed_data из примера выше, без приведения к int число выводилось некорректно!

Одномерные массивы


С массивами чисел всё весьма ожидаемо:

const uint8_t data[] PROGMEM = {10, 20, 30, 40};

void setup() {
  Serial.begin(9600);
  for (byte i = 0; i < 4; i++) {
    Serial.println(pgm_read_byte(&data[i]));
  }
  // выведет 10 20 30 40
}

void loop() {}

Читай урок по массивам в блоке базовых уроков программирования.

Двумерные массивы


При создании двумерного массива нужно обязательно указывать размер хотя бы одной из размерностей!

const uint16_t data[][5] PROGMEM = {
  {10, 20, 30, 40, 50},
  {60, 70, 80, 90, 100},
  {110, 120, 130, 140, 150},
};


void setup() {
  Serial.begin(9600);
  // выведет 70, вторая строка второй столбец
  Serial.println(pgm_read_word(&data[1][1]));

  // выведет 150, третья строка пятый столбец
  Serial.println(pgm_read_word(&data[2][4]));
}

void loop() {}

Массив массивов


Можно хранить несколько массивов в одном, т.н. таблица ссылок.

// массивы
const uint16_t data0[] PROGMEM = {10, 20, 30, 40, 50};
const uint16_t data1[] PROGMEM = {60, 70, 80, 90, 100};
const uint16_t data2[] PROGMEM = {110, 120, 130, 140, 150};
const uint16_t data3[] PROGMEM = {160, 170, 180, 190, 200};

// таблица ссылок
const uint16_t* const data_array[] PROGMEM = {data0, data1, data2, data3};

void setup() {
  Serial.begin(9600);
  // выведет 170, второй элемент четвёртого массива
  Serial.println(pgm_read_word(&data_array[3][1]));
}


void loop() {}

Строки в PROGMEM


Строка (как массив символов) хранится в оперативной памяти программы. Речь идёт о строках вида:

char str[] = "Hello!";

PROGMEM позволяет хранить символьные строки в программной памяти. Это делается глобально, то есть вне функций в программе:

const char message[] PROGMEM = "Hello!";

Строка располагается в памяти программы, и для манипуляций с ней (вывод, сложение с другими строками, передача в String) нужно скопировать её в оперативную память. Для этого можно:

  • Создать буфер – массив char нужного размера (находим длину строки в PROGMEM при помощи функции strlen_P()).
  • Скопировать в буфер строку из программной памяти при помощи strcpy_P().
  • Пользоваться!
const char message[] PROGMEM = "Hello!";

void setup() {
  Serial.begin(9600);
  char buf[strlen_P(message)];
  strcpy_P(buf, message);
  Serial.println(buf);
}

void loop() {}

Мы использовали “прогмемные” аналоги строковых функций из урока про строки, это те же самые функции, но имеют постфикс _P. Полный набор инструментов для работы со строками в PROGMEM можно посмотреть в документации, их там много.

Массивы строк


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

  • Cоздаём строки, кладём их в PROGMEM.
  • Создаём “таблицу ссылок” на эти строки.
  • Для чтения строк из таблицы сначала получаем адрес нужной строки при помощи pgm_read_word().
  • Полученный PROGMEM-адрес сохраняем в переменную типа const char*, рекомендуется использовать его синоним – PGM_P, чтобы не запутаться и не работать с ним при помощи обычных строковых функций.
  • Далее точно так же переписываем в буфер и используем.

Для удобства можно вынести всё в отдельную функцию, которая принимает номер строки в таблице:

// объявляем наши "строки"
const char str1[] PROGMEM = "Period";
const char str2[] PROGMEM = "Work";
const char str3[] PROGMEM = "Stop";

// объявляем таблицу ссылок
const char* const names[] PROGMEM = {
  str1, str2, str3,
};

void setup() {
  Serial.begin(9600);
  printPGM(0);  // выведет Period
  printPGM(1);  // выведет Work
  printPGM(2);  // выведет Stop
}

void printPGM(byte idx) {
  PGM_P p = pgm_read_word(names + idx);
  char buf[strlen_P(p)];
  strcpy_P(buf, p);
  Serial.println(buf);
}

void loop() {}

Вместо вывода в порт можно работать со строками как обычно, основная задача – вытащить их из программной памяти.

Локальные PGM строки


Иногда бывает удобно хранить и использовать строки локально, внутри функции. Для этого нужно обернуть её в макрос PSTR(), который “вернёт” указатель типа const char* (PGM_P). Для примера создадим строку в программной памяти и точно так же перепишем её в локальный буфер:

void f() {
  PGM_P strp = PSTR("local pgm string");
  char buf[strlen_P(strp)];
  strcpy_P(buf, strp);
  Serial.println(buf);
}

Таким образом строка не занимает место в оперативной памяти, пока не будет вызвана функция. В функции мы достаём строку в локальный буфер, делаем с ним какие-то действия, и при выходе из функции буфер удаляется из оперативной памяти.

F() макро


Так называемый “F() macro” позволяет ещё проще хранить строки в программной памяти:

// данный вывод (строка, текст) занимает в оперативной памяти 18 байт
Serial.println("Hello <username>!");

// данный вывод ничего не занимает в оперативной памяти, благодаря F()
Serial.println(F("Type /help to help"));

Удобно! Но F() макро работает только в тех случаях, когда нужно передать текст в функцию, которая умеет с ним работать. Макрос конвертирует текст в объект __FlashStringHelper, поддержка которого есть во многих библиотеках. Например Print.h (наследуется Serial и почти всеми дисплеями и библиотеками вывода), а также String. То есть можно собирать стрингу из строк и делать это экономно:

String s;
s += F("https://alexgyver.ru/");
s += 1234;
s += F("/test");

Важный момент: обёрнутые в F() строки оптимизируются, то есть одинаковые строки не дублируются в памяти! Поэтому можно смело использовать макрос в разных участках программы, одинаковые строки не нужно выносить глобально и делать их общими – за вас это сделает компилятор.

Полезные страницы


  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
5/5 - (10 голосов)
Назад Работа с EEPROM памятью
Вперёд Увеличение частоты ШИМ
Подписаться
Уведомить о
guest
2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии