ESP8266:Прошивки/Arduino/PROGMEM
Черновик |
Руководство по PROGMEM в аддоне ESP8266 для IDE Arduino[1]
PROGMEM – это функция AVR-процессоров Arduino, портированная на ESP8266 для сохранения совместимости с библиотеками Arduino, а также для экономии RAM-памяти. На ESP8266 объявление строки вроде const char * xyz = "это строка" поместит эту строку в RAM, а не во flash-память. Впрочем, есть способ поместить строку сначала во flash-память, а затем, если понадобится, в RAM. На 8-битных процессорах ARM этот процесс очень прост. На 32-битном ESP8266 для считывания с flash-памяти должны быть соблюдены определенные условия.
На ESP8266 для PROGMEM используется следующий макрос:
#define PROGMEM ICACHE_RODATA_ATTR
ICACHE_RODATA_ATTR задается при помощи:
#define ICACHE_RODATA_ATTR __attribute__((section(".irom.text")))
Это поместит переменную в секцию «.irom.text» во flash-памяти. Для сохранения строки во flash-память можно использовать оба этих метода.
Чтобы объявить глобальную переменную, сохраняемую во flash-памяти, нужно написать следующее:
static const char xyz[] PROGMEM = "Это строка, сохраненная на flash";
Как объявить flash-строку внутри кода
Для этого можно воспользоваться макросом PSTR(). Он задан в файле pgmspace.h.
#define PGM_P const char *
#define PGM_VOID_P const void *
#define PSTR(s) (__extension__({static const char __c[] PROGMEM = (s); &__c[0];}))
На практике:
void myfunction(void) {
PGM_P xyz = PSTR("Сохраняем эту строку во flash-памяти");
const char * abc = PSTR("Эту строку тоже сохраняем во flash-памяти");
}
Вот два способа, с помощью которых строки можно сохранять во flash-памяти. Чтобы считывать и манипулировать этими flash-строками, они должны быть в виде 4-байтных слов. В аддоне ESP8266 для IDE Arduino есть несколько функций, которые позволяют извлекать из flash-памяти строки, сохраненные туда при помощи PROGMEM. Оба примера выше возвращают данные типа const char *. Впрочем, если использовать эти указатели без правильного 32-битного выравнивания, это повлечет ошибку сегментации, и ESP8266 крашнется. Данные, считываемые из flash-памяти, должны быть выровнены под 32 бита.
Функции для считывания из PROGMEM
Все эти функции заданы в заголовочном файле pgmspace.h.
int memcmp_P(const void* buf1, PGM_VOID_P buf2P, size_t size);
void* memccpy_P(void* dest, PGM_VOID_P src, int c, size_t count);
void* memmem_P(const void* buf, size_t bufSize, PGM_VOID_P findP, size_t findPSize);
void* memcpy_P(void* dest, PGM_VOID_P src, size_t count);
char* strncpy_P(char* dest, PGM_P src, size_t size);
char* strcpy_P(dest, src)
char* strncat_P(char* dest, PGM_P src, size_t size);
char* strcat_P(dest, src)
int strncmp_P(const char* str1, PGM_P str2P, size_t size);
int strcmp_P(str1, str2P)
int strncasecmp_P(const char* str1, PGM_P str2P, size_t size);
int strcasecmp_P(str1, str2P)
size_t strnlen_P(PGM_P s, size_t size);
size_t strlen_P(strP)
char* strstr_P(const char* haystack, PGM_P needle);
int printf_P(PGM_P formatP, ...);
int sprintf_P(char *str, PGM_P formatP, ...);
int snprintf_P(char *str, size_t strSize, PGM_P formatP, ...);
int vsnprintf_P(char *str, size_t strSize, PGM_P formatP, va_list ap);
Как видите, здесь много функций, но фактически это _P версии стандартных C-функций, адаптированные для считывания выровненных данных из 32-битной flash-памяти ESP8266. Все они используют тип данных PGM_P, который в сущности является const char *. Под «капотом» всех этих функций работает процесс, который считывает из flash-памяти 4 байта, но возвращает только тот байт, что был запрошен пользователем.
Но этот процесс исправно работает, только если ваша функция создана аналогично тем, что показаны выше. Все они адаптированы для работы с указателями PROGMEM, но в них нет проверки на тип данных, за исключением проверки на соответствие с const char*. Таким образом, вы вполне можете (пока разрешает компилятор) использовать в этих функциях строки с указателями типа const char *, но штука в том, что из-за этого программа может начать вести себя странно. Вы не сможете создавать перегруженные функции, способные использовать flash-строки, когда они заданы как PGM_P. Если попытаться сделать это, вы получите двусмысленную ошибку перегрузки, т.к. тип данных PGM_P, в сущности, идентичен типу данных const char*.
Но тут на помощь приходит __FlashStringHelper. Это класс-обертка, позволяющий использовать flash-строки как класс. Благодаря ему с flash-строками можно делать и проверку типа данных, и перегрузку функций. Думаю, многие знакомы с макросом F() и, возможно, с макросом FPSTR(). Оба этих макроса заданы в файле WString.h.
#define FPSTR(pstr_pointer) (reinterpret_cast<const __FlashStringHelper *>(pstr_pointer))
#define F(string_literal) (FPSTR(PSTR(string_literal)))
Итак, макрос FPDTR() берет PROGMEM-указатель на строку и преобразует его в класс __FlashStringHelper. Следовательно, если вы задали строку как xyz, то можете воспользоваться FPSTR() для преобразования этой строки в класс __FlashStringHelper, чтобы ее можно было использовать в функциях, совместимых с этих классом.
static const char xyz[] PROGMEM = "Это строка, сохраненная на flash";
Serial.println(FPSTR(xyz));
Макрос F() объединяет в себе оба этих метода, создавая простой и быстрый способ для записи встраиваемых строк на flash-память, а также возвращения типа __FlashStringHelper.
Например:
Serial.println(F("Это строка, сохраненная на flash"));
Хотя обе эти функции выполняют одну и ту же задачу, роли у них разные. Если FPSTR() позволяет задать глобальную flash-строку, а затем использовать ее в функции, принимающей класс __FlashStringHelper, то F() позволяет задать flash-строку лишь локально (т.е. ее нельзя будет использовать больше нигде). В результате FPSTR() позволяет обмениваться общими строками, а F() – нет. Класс String использует __FlashStringHelper, чтобы перегрузить свой конструктор (т.е. создать его разные версии).
String(const char *cstr = ""); // конструктор из const char *
String(const String &str); // копирующий конструктор
String(const __FlashStringHelper *str); // конструктор для flash-строк
В результате в коде можно написать следующее:
String mystring(F("Эта строка сохранена во flash-памяти"));
Как я написал функцию, использующую __FlashStringHelper? Я преобразовал указатель обратно в PGM_P и воспользовался функциями _P, показанными выше. Ниже это показано на примере класса String и функции конкатенации:
unsigned char String::concat(const __FlashStringHelper * str) {
if (!str) return 0; // это значение возвращается,
// если указатель пуст
int length = strlen_P((PGM_P)str); // преобразуем обратно
// в PGM_P, который в сущности
// является const char *,
// и измеряем при помощи
// _P версии функции strlen()
if (length == 0) return 1;
unsigned int newlen = len + length;
if (!reserve(newlen)) return 0; // создаем буфер
// корректной длины
strcpy_P(buffer + len, (PGM_P)str); // копируем строку в буфер
// при помощи strcpy_P()
len = newlen;
return 1;
}
Как объявить глобальную flash-строку и использовать ее?
static const char xyz[] PROGMEM = "Эта строка сохранена на flash. Len = %u";
void setup() {
Serial.begin(115200); Serial.println();
Serial.println( FPSTR(xyz) ); // просто печатаем строку,
// но сначала конвертируем ее
// в класс FlashStringHelper
// при помощи FPSTR()
Serial.printf_P( xyz, strlen_P(xyz)); // используем printf_P()
// со строкой PROGMEM
}
Как использовать встраиваемые flash-строки?
void setup() {
Serial.begin(115200); Serial.println();
Serial.println( F("Это встраиваемая строка")); //
Serial.printf_P( PSTR("Это встраиваемая строка при помощи printf %s"), "привет");
}
Как объявить и использовать данные в PROGMEM?
const size_t len_xyz = 30;
const uint8_t xyz[] PROGMEM = {
0x53, 0x61, 0x79, 0x20, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20,
0x74, 0x6f, 0x20, 0x4d, 0x79, 0x20, 0x4c, 0x69, 0x74, 0x74,
0x6c, 0x65, 0x20, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x00};
void setup() {
Serial.begin(115200); Serial.println();
uint8_t * buf = new uint8_t[len_xyz];
if (buf) {
memcpy_P(buf, xyz, len_xyz);
Serial.write(buf, len_xyz); // выгружаем буфер
}
}
Как объявить данные в PROGMEM, а затем извлечь оттуда один байт?
Об объявлении данных говорилось выше. Чтобы извлечь данные, воспользуйтесь pgm_read_byte.
const size_t len_xyz = 30;
const uint8_t xyz[] PROGMEM = {
0x53, 0x61, 0x79, 0x20, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20,
0x74, 0x6f, 0x20, 0x4d, 0x79, 0x20, 0x4c, 0x69, 0x74, 0x74,
0x6c, 0x65, 0x20, 0x46, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x00
};
void setup() {
Serial.begin(115200); Serial.println();
for (int i = 0; i < len_xyz; i++) {
uint8_t byteval = pgm_read_byte(xyz + i);
Serial.write(byteval); // выгружаем буфер
}
}
Итого В использовании PROGMEM и PSTR() для сохранения строк на flash-память нет ничего сложного. Но PROGMEM и PSTR() генерируют указатели, поэтому вам понадобятся функции, которые специальным образом используют эти указатели, т.к. они являются, по сути, const char*. С другой стороны, FPSTR() и F() дают вам класс, из которого можно сделать неявные преобразования, что очень полезно при перегрузке функций. Стоит добавить, что если вам нужно сохранить int, float или указатель, то их можно сохранять и считывать напрямую, потому что их размер составляет 4 байта (т.е. они всегда остаются выровненными).
Надеюсь, эта статья была для вас полезна.