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:

const char data_message[] PROGMEM = {"Hello!"};

void setup() {
  Serial.begin(9600);
  for (byte i = 0; i < strlen_P(data_message); i++) {
    Serial.print((char)pgm_read_byte(&data_message[i]));
  }
  // выведет Hello!
}

void loop() {}

Чтение осуществляется посимвольно, при чтении обязательно приводим тип к char. Также вы могли заметить, что для вычисления длины массива символов мы использовали функцию strlen_P, это аналог strlen (см. урок про строки), но специально для PROGMEM строк. Полный набор инструментов для работы со строками в PROGMEM можно посмотреть в документации, их там много.

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


Иногда бывает удобно хранить несколько строк под одним именем, например для пунктов меню. В таком случае можно использовать массив строк (массив массивов символов), про него мы говорили в уроке про строки. Механизм следующий: создаём строки, кладём их в PROGMEM. Создаём “таблицу ссылок” на эти строки. Читаем любую выбранную строку по таблице!

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

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

void setup() {
  Serial.begin(9600);
  // выводим строку №1 (текст "Work")
  // strlen_P(names[1]) - длина этой строки
  for (byte i = 0; i < strlen_P(names[1]); i++) {
    // обращение к элементу будет как к двумерному массиву!
    // names[1][0] - буква W
    Serial.print((char)pgm_read_byte(&(names[1][i]))); // выведет Work
  }
}

void loop() {}

Задача усложняется, не правда ли? =) Можно пойти другим путём: создать буфер (char), в который скопировать всю строку из PROGMEM при помощи функции strcpy_P, которая копирует указанные данные из PROGMEM в обычный массив. Получаем обычный массив символов, который можно даже напрямую вывести в порт:

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

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

void setup() {
  Serial.begin(9600);
  char arrayBuf[10];  // создаём буфер
  
  // копируем в arrayBuf при помощи strcpy_P
  strcpy_P(arrayBuf, pgm_read_byte(&(names[1])));
  Serial.println(arrayBuf); // выведет Work

  strcpy_P(arrayBuf, pgm_read_byte(&(names[2])));
  Serial.println(arrayBuf); // выведет Stop
}

void loop() {}

Полный набор инструментов для работы со строками в PROGMEM можно посмотреть в документации.

F() макро


Так называемая “F() macro” позволяет очень просто хранить строки (массивы символов) во Flash памяти, не прибегая к использованию PROGMEM:

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

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

Удобно! Но PROGMEM даёт больше возможностей, особенно с таблицей ссылок, когда доступ к нескольким строкам осуществляется по одному общему имени и номеру. F() макро отлично работает в случаях вывода простого текста на дисплей и для программ с консольным управлением.

Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Последнее обновление Ноябрь 02, 2019