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_word(&(names[1])));
  Serial.println(arrayBuf); // выведет Work

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

void loop() {}

Рассмотрим ещё пример, в котором будем выводить строку из памяти без тяжёлых дополнительных функций. Также этот пример корректно работает при выводе через цикл (в отличие от предыдущих примеров), за пример спасибо Виктору Струговцу:

// объявляем наши "строки"
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);  
  for (int i = 0; i < 3; i++) {   // цикл
    uint16_t ptr = pgm_read_word(&(names[i]));// получаем адрес из таблицы ссылок
    while (pgm_read_byte(ptr) != NULL) {      // всю строку до нулевого символа
      Serial.print(char(pgm_read_byte(ptr))); // выводим в монитор или куда нам надо
      ptr++;                                  // следующий символ
    }
    Serial.println();
  }
}

void loop() {}

Функцию для печати строк в сериал или на дисплей из 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);

  for (int i = 0; i < 3; i++) {
    printFromPGM(&names[i]);
    Serial.println();
  }
}

void loop() {
}

// функция для печати из PROGMEM
void printFromPGM(int charMap) {
  uint16_t ptr = pgm_read_word(charMap);    // получаем адрес из таблицы ссылок
  while (pgm_read_byte(ptr) != NULL) {      // всю строку до нулевого символа
    Serial.print(char(pgm_read_byte(ptr))); // выводим в монитор или куда нам надо
    ptr++;                                  // следующий символ
  }
}

[ОБНОВЛЕНИЕ!] Чуть исправленный финальный пример, работает более корректно:

// объявляем наши "строки"
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);
  for (int i = 0; i < 3; i++) {
    printFromPGM(&names[i]);
    Serial.println();
  }
}

void loop() {
}

// функция для печати из PROGMEM
void printFromPGM(int charMap) {
  char buffer[10];      // буфер для хранения строки
  uint16_t ptr = pgm_read_word(charMap); // получаем адрес из таблицы ссылок
  uint8_t i = 0;        // переменная - индекс массива буфера
  do {
    buffer[i] = (char)(pgm_read_byte(ptr++)); // прочитать символ из PGM в ячейку буфера, подвинуть указатель
  } while (buffer[i++] != NULL);              // повторять пока прочитанный символ не нулевой, подвинуть индекс буфера

  Serial.print(buffer); // печатаем готовую строку
}

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

F() макро


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

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

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

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

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