ESP32: библиотека энергонезависимого хранилища данных |
Добавил(а) microsin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Библиотека для энергонезависимого хранилища данных (Non-volatile storage, NVS) была разработана для хранения пар ключ-значение в памяти flash. В этой статье показаны некоторые концепции NVS (перевод документации [1]). [Варианты хранилища нижнего уровня] В настоящее время NVS использует часть основной памяти flash через вызовы esp_partition API. Библиотека использует все разделы (partitions) с типом data и субтипом nvs. Приложение может выбрать использование раздела с меткой nvs вызовом API-функции nvs_open(), или выбрать любой другой раздел путем указания его имени с использованием API-функции nvs_open_from_partition(). Будущие версии этой библиотеки смогут использовать другие системы хранения данных в различных типах микросхем (SPI или I2C), RTC, FRAM, и т. д. Замечание: если раздел NVS урезается (например, когда меняется конфигурация таблицы разделов), то его содержимое должно быть очищено. Система сборки проекта в среде разработки ESP-IDF [2] предоставляет цель idf.py erase-flash для стирания всего содержимого чипа flash. NVS лучше всего работает для очень маленьких значений, но не с большими массивами данных наподобие типов string и blob. Если нужно работать с большими порциями данных, то рассмотрите вариант хранилища на основе файловой системы FAT, реализованной поверх библиотеки выравнивания износа секторов FLASH (wear levelling library). [Ключи и значения] Как уже упоминалось, NVS работает с данными по принципу пар ключ-значение. Ключи это строки ASCII, т. е. имена значений. Максимальная длина ключа в настоящий момент составляет 15 символов. Значения могут быть одним из следующих типов (дополнительные типы, такие как float и double, могут быть добавлены позже): • Целочисленные типы: uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t. В настоящее время значения ограничены 4000 байтами, включая null-терминатор. Blob-значения ограничены 508000 байтами, или 97.6% от размера раздела - 4000 байт, в зависимости от того, что меньше. Ключи должны быть уникальными. Назначение нового значения существующему ключу работает следующим образом: • Если новое значение такого же типа, что и старое, то новое значение обновляется. Проверка типа данных выполняется при чтении значения. Будет возвращена ошибка, если тип данных операции чтения не соответствует типу данных значения. Пространства имен. Чтобы уменьшить потенциальные конфликты в именах ключей между различными компонентами, NVS назначает каждую пару ключ-значение одному из пространств имен (namespace). Имена пространств следуют тем же правилам, что и имена ключей, например их максимальная длина составляет 15 символов. Имя для namespace указывается в вызове nvs_open() или nvs_open_from_partition(). Этот вызов вернет непрозрачный дескриптор, который используется в последующих вызовах функций nvs_get_*, nvs_set_* и nvs_commit(). Таким методом дескриптор связывается с namespace, и имена ключей не конфликтуют с такими же именами в других namespace. Обратите внимание, что пространства имен с одинаковым именем в разных разделах NVS считаются разными namespace. Итераторы NVS. Итераторы позволяют пролистать пары ключ-значение, сохраненные в NVS, основываясь на имени раздела, namespace, и типе данных. Для этого доступны следующие функции: nvs_entry_find() вернет непрозрачный дескриптор, который используется в последующих вызовах функций nvs_entry_next() и nvs_entry_info(). Если не найдена никакая другая пара ключ-значение по заданному критерию, то nvs_entry_find() и nvs_entry_next() вернут NULL. В таком случае итератор освобождать не нужно. Если итератор больше не нужен, его можно освободить вызовом функции nvs_release_iterator(). Безопасность, взлом и надежность. NVS не совместима напрямую с системой шифрования flash (ESP32 flash encryption system). Однако данные все еще можно хранить в зашифрованном виде, если шифрование NVS используется вместе с ESP32 flash encryption (подробнее см. далее "Шифрование NVS"). Если шифрование NVS не используется, то любой взломщик, который получил физический доступ к микросхеме flash, может изменить, удалить или добавить пары ключ-значение. С разрешенным шифрованием NVS невозможно изменить или добавить пару ключ-значение, чтобы они потом были распознаны как достоверные, не зная соответствующих ключей шифрования NVS. Однако от операции стирания не существует защиты. Библиотека сделает попытку восстановления из ситуаций, когда память flash находится в несогласованном состоянии. В частности, необходимо иметь возможность выключить питание устройства, и потом его снова включить. Это не должно привести к потере данных, за исключением новой пары ключ-значение, если она была записана в момент выключения питания. Библиотека должна быть способна инициализировать любые случайные данные, которые присутствуют в памяти flash. [Шифрование NVS] Данные, сохраненные в разделах (NVS partitions), могут быть зашифрованы с использованием AES-XTS способом, описанным в стандарте шифрования диска IEEE P1619. Для шифрования каждая запись рассматривается как один сектор и относительный адрес записи (w.r.t. partition-start), и в алгоритм шифрования подается как номер сектора. NVS Encryption можно разрешить опцией CONFIG_NVS_ENCRYPTION. Ключи, требуемые для шифрования, сохраняются в другом разделе, который защищен Flash Encryption. Таким образом, включение Flash Encryption является обязательным условием для работы NVS encryption. По умолчанию NVS Encryption разрешается, когда разрешено Flash Encryption. Это осуществляется потому, что драйвер Wi-Fi сохраняет учетные данные (наподобие SSID и пароля) в разделе по умолчанию (default NVS partition). Важно зашифровать их по умолчанию, если на уровне платформы шифрование уже включено. Для использования шифрования NVS таблица разделов должна содержать раздел ключа (NVS key partition). Для шифрования предоставляются две таблицы разделов, содержащие раздел ключа NVS (опция menuconfig -> Partition Table). Они могут быть выбраны через меню конфигурации проекта (командой idf.py menuconfig). Как конфигурировать и использовать функцию шифрования NVS, см. пример проекта security/flash_encryption (находится среди примеров каталога установки ESP-IDF [2]). Раздел ключа NVS. Приложению, которому требуется поддержка шифрования NVS, должно быть скомпилировано с разделом ключа (key-partition) типа data и субтипом key. Этот раздел должен быть помечен как зашифрованный, и его размер должен быть минимальным для раздела (4KB), подробности см. в [3]. Две дополнительные таблицы разделов, которые содержат NVS key partition, предоставляются опцией menuconfig -> Partition Table. Они могут непосредственно использоваться для NVS Encryption, и их структура следующая: +-----------+--------------+-------------+----+ Ключи шифрования (XTS encryption keys) в NVS key partition можно сгенерировать одним из двух способов. 1. Генерация ключей на чипе ESP. Когда шифрование разрешено, API-функция nvs_flash_init() может использоваться для инициализации зашифрованного раздела NVS по умолчанию. Эта API-функция генерирует внутри себя ключи шифрования XTS на чипе ESP. API-функция находит первый раздел ключей NVS, за. Then the API functionем она автоматически генерирует и сохраняет ключи NVS в этом разделе с помощью API-функции nvs_flash_generate_keys(), предоставленной в nvs_flash/include/nvs_flash.h. Новые ключи генерируются и сохраняются только когда соответствующий раздел ключей пуст. Тот же самый раздел ключей может затем использоваться для чтения конфигураций безопасности, чтобы инициализировать пользовательский зашифрованный раздел NVS, с помощью nvs_flash_secure_init_partition(). API-функции nvs_flash_secure_init() и nvs_flash_secure_init_partition() не генерируют ключи внутри себя. Когда эти API-функции используются для инициализации зашифрованных разделов NVS, ключи могут быть сгенерированы после запуска системы (startup) с использованием API-функции nvs_flash_generate_keys(), предоставленной в nvs_flash.h. Эта API-функция затем запишет эти ключи на key-partition в зашифрованном виде. 2. Использование предварительно сгенерированного раздела ключей. Этот способ потребуется, когда ключи в NVS key partition не генерируются приложением. NVS key partition, содержащий ключи хранить сгенерированный раздел ключей во flash с помощью следующих двух команд. i) Сборка и прошивка таблицы разделов: idf.py partition-table partition-table-flash ii) Сохранение ключей в NVS key partition (на flash) с помощью скрипта parttool.py (см. секцию Partition Tool в [3] для получения дополнительной информации). parttool.py --port /dev/ttyUSB0 --partition-table-offset "nvs_key partition offset" write_partition --partition-name="name of nvs_key partition" --input "nvs_key partition" Поскольку раздел ключа помечен как зашифрованный, и разрешено Flash Encryption is enabled, загрузчик (bootloader) зашифрует этот раздел, используя flash encryption key при первой загрузке приложения (boot). Приложение может использовать разные ключи для разных разделов NVS, и иметь для этого соответственно несколько разделов ключей (key-partitions). Однако в зоне ответственности приложения находится предоставление корректных key-partition/ключей для шифрования/дешифровки. [Шифрованные чтение/запись] Те же функции NVS API nvs_get_ * или nvs_set_ * могут также использоваться для чтения и записи в зашифрованный раздел NVS. Шифрование раздела NVS по умолчанию: чтобы разрешить шифрование default NVS partition, не требуется предпринимать дополнительные шаги. Когда разрешена опция CONFIG_NVS_ENCRYPTION, API-функция nvs_flash_init() внутри себя выполнит некоторые дополнительные шаги, используя первый найденный раздел ключей (NVS key partition), чтобы разрешить шифрование для default NVS partition (подробнее см. документацию по API [1]). Альтернативно также может использоваться API-функция nvs_flash_secure_init() для разрешения шифрования default NVS partition. Шифрование пользовательского раздела NVS: чтобы разрешить шифрование custom NVS partition, используется API-функция nvs_flash_secure_init_partition() вместо nvs_flash_init_partition(). Когда используются API-функции nvs_flash_secure_init() и nvs_flash_secure_init_partition(), от приложения ожидаются следующие шаги, чтобы выполнить операции чтения/записи NVS с разрешенным шифрованием: 1. Найдите раздел ключа (key partition) и раздел данных (data partition) NVS, используя API-функции esp_partition_find*. 2. Заполните структуру nvs_sec_cfg_t, используя API-функции nvs_flash_read_security_cfg() или nvs_flash_generate_keys(). 3. Инициализируйте NVS flash partition, используя API-функции nvs_flash_secure_init() или nvs_flash_secure_init_partition(). 4. Откройте namespace, используя API-функции nvs_open() или nvs_open_from_partition(). 5. Выполните операции чтения/записи NVS, используя nvs_get_* или nvs_set_*. 6. Отмените инициализацию раздела NVS, используя nvs_flash_deinit(). [Утилита NVS Partition Generator] Эта утилита помогает генерировать двоичные файлы раздела NVS, которые можно отдельно прошить в выделенный раздел partition binary с помощью скрипта программирования. Пары ключ-значение для прошивки в раздел могут быть предоставлены посредством файла CSV. Для подробностей см. NVS Partition Generator Utility [4]. [Пример приложения] Примеры кода можно найти в каталоге storage, среди прочих примеров ESP-IDF [2]. storage/nvs_rw_value Этот пример демонстрирует, как прочитать одно целое значение из NVS, и как его записывать в NVS. Значение, проверяемое в этом примере, хранит количество перезагрузок модуля ESP32. Функция значения в качестве счетчика возможна только благодаря его хранению в NVS. Пример также показывает, что проверить, была ли успешной операция чтения / записи, и было ли инициализировано значение в NVS. Процедура диагностики предоставляется в виде обычного текста, чтобы помочь разобраться в ходе выполнения программы, и легче исправлять возникающие проблемы. storage/nvs_rw_blob Пример демонстрирует, как читать одиночное целое значение и объект blob (binary large object), и как их записывать в NVS, чтобы сохранять значения между перезапусками модуля ESP32. value - отслеживает количество программных (soft) и аппаратных (hard) перезагрузок модуля ESP32. blob - содержит информацию о реальном времени выполнения. Таблица считывается из NVS в динамически выделенную область RAM. Новая run time информация добавляется в таблицу при каждом запущенном вручную программном рестарте, и затем эта информация записывается в NVS. Этот процесс запускается притягиванием к лог. 0 ножки GPIO0. Этот пример также показывает, как реализовать процедуру диагностики, чтобы проверить, была ли успешной операция чтения / записи. storage/nvs_rw_value_cxx Этот пример делает то же самое, что и storage/nvs_rw_value, отличие только в том, что используется дескриптор класса C++ NVS. [Внутренняя организация NVS] Журнал пар ключ-значение. NVS сохраняет пары key-value последовательно, добавляя новые пары в конец заполненной области. Когда обновляется значение любого указанного ключа, в конец журнала добавляется новая пара key-value, и старая пара key-value помечается как стертая (erased). Страницы (pages) и записи (entries). Библиотека NVS в своей работе использует 2 основных объекта: страницы и записи. Страница это логическая структура, которая хранит порцию общего журнала. Логическая страница соответствует одному физическому сектору памяти flash. С используемыми страницами связан порядковый номер. Этот номер позволяет упорядочить страницы. Чем больше порядковый номер, тем позже была создана страница. Каждая страница может быть в одном из следующих состояний: Empty/uninitialized Active Full Erasing Corrupted Сопоставление секторов flash-памяти с логическими страницами не имеет определенного порядка. Библиотека будет проверять порядковые номера страниц, найденных в каждом секторе памяти flash, и упорядочивать страницы в списке на основе этих номеров. +--------+ +--------+ +--------+ +--------+ Структура страницы. На данный момент мы предполагаем, что размер сектора памяти flash составляет 4096 байт, и что оборудование ESP32 шифрования flash-памяти работает на 32-байтовых блоках. Можно ввести некоторые настройки, конфигурируемые во время компиляции (например, через menuconfig) для установки микросхем flash с различными размерами секторов (хотя неясно, могут ли другие компоненты в системе, например драйвер flash-памяти SPI и кэш flash-памяти SPI, поддерживать эти другие размеры). Страница состоит из 3 частей: заголовок (header), биты состояния записи (entry state bitmap), и сами записи (entries). Для поддержки совместимости шифрования ESP32 flash, размер записи равен 32 байтам. Для целочисленных типов одна запись хранит одну пару ключ-значение (key-value). Для строк (string) и двоичных данных (blob) запись хранит часть пары key-value (подробнее об этом в описании структуры записи). На следующей диаграмме показана структура страницы. Числа в круглых скобках показывают размер в байтах для каждой части. +-----------+---------------+-------------+-------------------------+ Заголовок страницы (header) и entry state bitmap всегда записываются во flash не зашифрованными. Записи (Entry n) шифруются, если используются шифрование flash ESP32. Значения для поля state определены таким образом, что изменение состояния фиксируется записью 0 в некоторые биты. Поэтому нет необходимости стирать страницу, чтобы изменить её состояние, если это не является изменением для перехода в состояние erased. Поле version в заголовке отражает используемую версию формата NVS. Для обеспечения обратной совместимости при каждом обновлении поле версии декрементируется, начиная с 0xff (например, 0xff для version-1, 0xfe для version-2, и так далее). Значение контрольной суммы CRC32 в заголовке вычисляется от части страницы, которая не включает поле state и поле самой контрольной суммы (байты от 4 до 28). Не используемая часть сейчас заполняется байтами 0xff. Следующие секции описывают структуру поля состояния записи (entry state bitmap) и саму запись (entry). [Entry и entry state bitmap] Каждая запись (entry) может быть в одном из следующих состояний, которые представлены двумя битами в entry state bitmap. Последние 4 бита bitmap (256 - 2 * 126) не используются. Empty (11) Written (10) Erased (00) Структура записи. Для значений примитивных типов (в настоящий момент это целые числа размером от 1 до 8 байт), запись хранит одну пару key-value. Для строк и типов blob запись хранит часть от всей пары key-value. Для строк, когда пара key-value распространяется на несколько записей, все эти записи хранятся на одной и той же странице. Данным blob разрешается распространятся между несколькими страницами, путем разделения их на отдельные куски. Для отслеживания этих кусков сохраняется дополнительная запись метаданных фиксированного размера, которая называется "blob index". Ранние форматы blob все еще поддерживаются (могут быть прочитаны и изменены). Однако после того, как blob был изменен, он будет сохранен уже в новом формате. +--------+----------+----------+----------------+-----------+---------------+----------+ Примитивные +--------------------------------+ Отдельные поля в структуре записи имеют следующее назначение: NS Type Span ChunkIndex CRC32 Key Data Для записи "blob index" эти 8 байт хранят следующую информацию о кусках данных: • Size • ChunkCount • ChunkStart Для кусков строк и кусков данных blob эти 8 байт хранят дополнительные описательные данные для value: • Size • CRC32 Value с переменной длиной (строки и blob) сохраняются в последующих записях, по 32 байта на запись. Поле Span последней записи показывает, сколько записей используется. [Пространства имен (namespace)] Как упоминалось выше, каждая пара key-value принадлежит одному из пространств имен (namespace). Идентификаторы пространства имен (строки) сохраняются как ключи пар key-value в namespace с индексом 0. Значения, соответствующие этим ключам, будут индексами для этих namespace. +-------------------------------------------+ Hash-список элементов. Для уменьшения количества чтений из памяти flash, каждый элемент класса Page поддерживает список пар: индекс элемента (item index); хэш элемента (item hash). Этот список значительно ускоряет поиск. Вместо того, чтобы выполнять итерацию по всем записям, считывая записи из flash по одной, функция Page::findItem сначала осуществляет поиск хэша элемента в hash-списке. Это дает индекс элемента на странице, если такой элемент существует. Из-за хеш-коллизии есть вероятность, что будет найден не тот элемент. Эта ситуация разрешается путем отката на итерацию по элементам из flash-памяти. Каждый узел в hash-списке содержит 24-битный хэш и 8-битный индекс элемента. Хэш вычисляется от namespace элемента, имени ключа и ChunkIndex. Для вычисления используется CRC32, результат обрезается до 24 бит. Для снижения чрезмерных расходов на сохранение 32-битных записей в связанном списке, этот список реализован как двойной список (double-linked list) массивов. Каждый массив содержит 29 записей, общий размер которых составляет 128 байт, вместе со связанным списком указателей и 32-битным полем счетчика. Минимальный объем дополнительного использования RAM на страницу составляет 128 байт, максимальный 640 байт. [NVS API] Заголовочные файлы: components/nvs_flash/include/nvs_flash.h В таблице ниже приведено общее описание API-функций NVS, полное описание см. в документации [1].
[Ссылки] 1. ESP32 Non-volatile storage library site:docs.espressif.com. |