
Содержание
Работа с PROGMEM памятью Arduino
Часто бывает нужно сохранить в памяти микроконтроллера большой объём данных, которые не будут меняться в процессе работы, например:
- Калибровочный массив
- Текст названий пунктов меню
- Просто какой-то текст
- Посчитанная тригонометрия (синус, косинус)
- Изображения для дисплея (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"))
) и для программ с консольным управлением.
Важные страницы
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
- Articles coming soon