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 байта (т.е. они всегда остаются выровненными).

Надеюсь, эта статья была для вас полезна.

См.также

Внешние ссылки