Настал первый случай за 12 лет, когда в программировании микроконтроллеров понадобились бинарные деревья поиска (BST). В этом тексте поговорим о том как можно построить эффективную программную реализацию энергонезависимой Key-Value Map(ки) над дешевой SPI NOR Flash для микроконтроллерных проектов. Суть проста. Нужна NVRAM. Требуется абстрактная структура данных. Ключ-значение. В качестве ключа 32-битное целое число. В качестве данных - бинарный массив произвольной длинны. У меня уже был текст про простую, компактную, наивную, медленную O(n) on-chip NOR Flash NVRAM  https://habr.com/ru/articles/706972/, которая в общем-то отлично работает, когда в распоряжение 16kByte...128kByte. Теперь же потребовалось создать полноценную быструю O(Log2(n)) NVRAM для off-chip NOR Flash(ей) c размерами 4....16 MByte. Принцип работы NVRAM. Даешь виртуальный 32bit адрес, данные, размер данных, записываешь данные. Отключаешь питание. Включаешь питание. Даешь виртуальный адрес, получаешь данные и их размер.  Должны остаться предыдущие данные с ключом ADDR, если питание пропадет в момент записывания очередного блока. Можно хранить данные разного размера.  Например 512 байт и более в одной записи. Данные должны сохраняться при пропадании питания. Узел должен быстро вставляться Узел должен быстро находиться Узел должен быстро удаляться Не должно быть пустых мест в памяти. Записи следуют одна за другой, впритык. Без какого бы то ни было выравнивания. Данные должны быть оснащены CRC16. Это позволит выявить такие события как пропадание питания в момент записи узла, да и просто отличить кадр от случайных чиселок. Данные должны храниться в зашифрованном виде. Это позволит защитить данные, если кто-то решит обклеить микросхему оранжевой каптоновой лентой и сдуть термо феном микросхему SPI-NOR Flash(а) для последующего считывания dump(а) другим микроконтроллером и разбора типов данных внутри.  Данные могут сжиматься. То есть подвергаться компрессии. Это позволит загрузить в NOR-Flash больше байт payload(а). В качестве физического число хранилища будет выступать обыкновенные дешевые микросхемы SPI NOR Flash. Например MX25R6435F В NOR Flash когда прописывается NOR Flash, то биты меняются по следующему закону. То есть записью считается обнуление бита. Отдельный обнуленный бит взвести обратно в единицу нельзя. Надо стирать всю страницу. А это порядка 4kByte. На самом деле задача разработки NVRAM сложнее чем кажется. Потребуются десятки итераций отладки и диагностики. Тут надо эффективные методы отладки софтвера.  Для отладки чисто программной составляющей надо сначала накропать имитатор SPI-Nor Flash на PC. В качестве SPI-Flash просто определить массив RAM памяти процесса. Всю логику работы надо отладить на NetTop(е) а потом с уверенностью собрать этот код на Tagret платформе. Плюс эта DeskTop утилита пригодится вам если вы захотите скомпилировать образ готовой файловой системы в виде hex массива, а затем прошить её во flash вместе с прошивкой. Также подобного рода задачи как разработка NVRAM отлично покрываются модульными тестами. Тут идеально ложится методология TDD. Поэтому сразу подготовим как можно больше тестов. Эти же тесты будут выполнять роль документации и скреп при рефакторинге и помогут понять, когда настало время остановиться в разработке кода NVRAM. Для начала определимся с терминологией. Термин Термин Определение Заголовок header Метаданные про полезную нагрузку: преамбула, адрес, CRC и прочее Полезная нагрузка  payload Сами полезные бинарные данные, которые надо хранить кадр  frame Заголовок и следующие за ним впритык полезные данные Каркас skeleton Структура бинарного дерева поиска. Указатели на правый и левый листы бинарного дерева. NVRAM страница NVRAM page непрерывный диапазон NOR-Flash памяти в которой находится только одно бинарное дерево поиска. Как правило имеет размер в целое количество секторов. Один и более секторов сектор sector минимальный размер NOR-Flash которую можно удалить за раз. Обычно 4kByte, 32kByte, 64kByte. сбалансированное бинарное дерево поиска balanced binary search tree это бинарное дерево поиска, где высота каждого листа отличается от остальных листов не более чем на 1 уровень Какой API мы хотим получить? Наверное что-то простое как это: Теперь определимся со структурой кадра, которую мы будем хранить в энергонезависимой памяти.  Под увеличением эту таблицу можно посмотреть тут. В заголовке всё только самое нужное: преамбула (параметризуемая), адрес, данные, контрольная сумма, тип данных, временная отметка, состояние переменной, указатели на листья. Всего 36 байт на заголовок. Хорошо. С заголовком плюс/минус определились. Но как хранить сами фреймы то? Новые узлы всегда записываются в конец страницы NOR Flash памяти. Впритирку, впритык к последнему прописанному кадру. Никакого выравнивая чтобы не потерять впустую драгоценнейшие байты. Теперь важный момент. Основная идея NVRAM заключается в том, чтобы использовать для связи узлов бинарное дерево поиска (BST). Бинарное дерево поиска, помимо всего прочего, в данном случае хорошо и тем, что указатели на правый и левый узлы в каждом узле прописываются только один раз. Это отлично ложиться на физические ограничения SPI-NOR Flash(а). В качестве ключа будет выступать всё тот же 32битный адрес данных. Мы же делаем NVRAM.  А RAM работает по принципу: "даешь адрес, получаешь данные". Перед вставной надо проверить, есть ли уже такие данные в NOR Flash(е). В этом деле нам поможет контрольная сумма CRC16 в каждом Frame(е). Нет смысла вставлять то же самое. Это противоречит требованию заботы об износе памяти. Называется такая технология lazy write (ленивая запись). Это нужно для равномерного износа микросхем NOR Flash. Вставка данных в NVRAM сводится к вставке в бинарное дерево. Перед вставкой надо проверить что в дереве есть уже такой адрес. Он может быть с данными другого размера и другой контрольной суммой. Если да, то его надо отметить как устаревший. После чего только и  вставить в дерево новый лист и отметить вставленный frame как новый. На рисунке это зелёный цвет. Вот пример организации узлов вот пример вставки пары кадров UART-CLI командой nvrw Поиск узла сводится к поиску внутри бинарного дерева поиска. В качестве ключа выступает 32-битный адрес переменной в NVRAM. Время поиска ограничено максимальной высотой бинарного дерева. Также можно запросто обойти все узлы по возрастанию или по убыванию. В дереве происходит быстрый поиск.  В лучшем случае O(Log2(N)), когда дерево сбалансировано в худшем O(N) когда дерево вырождается в связанный список. Чтобы удалить запись в NVRAM надо пройти по всем узлам с адресом Addr и сбросить в ноль бит, который отвечает за активность адреса. Также следует обнулить данные, которые соответствуют этому узлу. При этом сам адрес и указатели следует сохранить как есть, чтобы осталась связь с теми валидными узлами, которые больше и меньше удаленного узла.    Каркас должен сохраняться. NVRAM на основе бинарного дерева поиска (BST) хороша тем, что хранит не только данные, но и историю работы с данными. Когда создавали, что удаляли. Всё это останется в краткосрочной перспективе до момента переполнения и переключения страниц.  Вся память которая есть в распоряжении драйвера NVRAM будет поделена на 2 равные NVRAM страницы: aктивная и пассивная. Активная страница это та в которую и будут записываться переменные. Пассивная страница это то временное хранилище которое понадобится при перестроении дерева, чтобы очистить устаревшие и удаленные заголовки. В самом начале страниц NVRAM будет одно двойное слово (qword).  Интерпретация Значение Активная страница 0xFEFEFEF пассивная страница 0xFCFCFCFC Просто кусок памяти, которой еще не пользовались 0xFFFFFFFF При подаче питания драйвер NVRAM в функции nvram_init() проверит эти числа и поймет с какой страницей предстоит работать сегодня. С первой или со второй. Вот LookUp таблица принятия решения в функции nvram_init() При переполнении активной страницы драйвер отчистит пассивную страницу и запишет в пассивную страницу все самые новые переменные из активной страницы. Сформируется новое дерево без удаленных и устаревших кадров.   При этом при пере копировании страницы NVRAM надо обходить бинарное дерево в порядке level order. Таким образом в противоположной странице сформируется дерево той же высоты по отношению к активным узлам как и в изначальной странице. Это очень важно. Так как если при перебросе кадров обходить изначальное дерево в порядке in order, то в новой странице сформируется не дерево, а длиннющий связанный список и это замедлит время всех операций.  В идеале надо вообще придумать алгоритм потокового формирования сбалансированного дерева в новой странице при переключении страниц, при условии что нельзя переписывать указатели листьев, но это тема очень трудная, потянет на магистерское исследование какого-нибудь европейского ВУЗ(овца). Получается, что операция стирания страницы это достаточно редкая операция, которая происходит только при заполнении половины всего NOR-Flash. Этим и достигается защита памяти от износа. При этом количество переключений страниц можно также хранить тут же в NVRAM в переменной с определенным зарезервированным виртуальным адресом и предсказывать тот чудный день, когда NOR Flash откажет. Например, если наступит 80k переключений, то можно выдать предупреждение в UART консоль. NVRAM не может работать в вакууме. NVRAM это надстройка над драйвером какого-либо NOR-Flash(а). Будь-то on или off chip.  Также нужен компонент расчета контрольных сумм, функции которые возвращают время (UTC или Up-Time), и классический код для обслуживания бинарного дерева поиска.  Каждый компонент в отдельности должен, разумеется, отрабатывать безупречно, поэтому для всего этого естественно нужны модульные тесты. Вот так, исключительно благодаря софтверу, из практически одноразовой дешевой памяти ценой 20 RUR/MByte получили многоразовую дорогую память ценой 2000 RUR/MByte (в 100 раз дороже)! Волшебство!  Теперь, когда есть NVRAM можно спокойно запустить на SPI-Flash FatFs и создавать привычные всем текстовые и бинарные файлики прямо внутри микросхемы.  Для работы с NVRAM надо ориентироваться в следующих акронимах Акроним Расшифровка NVRAM Non-volatile random-access memory BST Binary search tree RAM  random-access memory API  Application Programming Interface SPI Serial Peripheral Interface PC personal computer TDD Test-driven development NOR  not or   LinksСайт для визуализации построения абстрактных структур данных https://www.cs.usfca.edu/~galles/visualization/Algorithms.html Простая NVRAM для on-chip NOR Flash https://habr.com/ru/articles/706972/ Сайт отрисовки графов https://dreampuf.github.io/GraphvizOnline Структура узла для NVRAM https://docs.google.com/spreadsheets/d/1UmJThfkCRQpiLWmO9J47Z5gdim99YF0lR8Lr8KIgXAY/edit#gid=0 https://habr.com/ru/articles/730232/ https://habr.com/ru/articles/479044/ https://habr.com/ru/articles/584156/ https://habr.com/ru/articles/731604/ https://www.youtube.com/watch?v=0U6eT9vTitc Контрольные вопросы: Какие бывают обходы бинарных деревьев? Какова алгоритмическая сложность поиска узла в бинарном дереве поиска? Какова алгоритмическая сложность вставки узла в бинарное дереве поиска? Как отладить большой кусок кода, если нет возможности(не хочется) пройтись по коду пошаговым отладчиком? На страница "A" произвольное бинарное дерево поиска. Как нарисовать эквивалентное сбалансированное бинарное дерево поиска на странице "B"? Ограничения: Дерево на странице B надо нарисовать с первой попытки, за одни раз. Дерево на странице "A" нельзя менять. RAM памяти в 300 раз меньше, чем размер дерева на странице "A".   Embedded SW/Firmware Engineer