Хочу поделиться своим вариантом способа хранения параметров. Мой вариант подходит не только для хранения в какой-то отдельной энергонезависимой памяти (далее Епром), он изначально придуман для хранения калибровочных значений в остатке флеш памяти программ.
Я рассматривал задачу не только с точки зрения хранения данных, а еще и с точки зрения эффективного использования Епром для их изменения.
Проблема конечности циклов записи-стирания
Почему-то автор исходной статьи умолчал о главном ограничении энергонезависимой памяти, об ограничении количества циклов записи-стирания, то есть если вы будете постоянно переписывать данные в одних и тех же физических адресах памяти, после определенного количества таких перезаписей память перестанет выполнять свою функцию, перестанет запоминать.
Собственно, именно это требование равномерной записи по всей доступной длине Епром-а и натолкнуло меня на идею организации работы с такой памятью.
Я думаю, вполне очевидно, что при ограничении количества перезаписей памяти самым эффективным способом ее использования было бы использование методом записи в память по кругу и, наверно, все бы так и использовали память если бы не одна маленькая, но неразрешимая проблема нельзя записывать данные по изменяющемуся адресу! Их же потом нельзя найти и прочитать!
Дело в том, что многие проблемы остаются не решенными просто потому, что трудно себе вообразить, что они имеют решение! Но достаточно просто попробовать их решить, чтобы понять, что решение существует, и оно в некоторых случаях даже проще чем другие очевидные решения.
Тут надо сделать небольшое отступление и рассказать дополнительные условия той задачи, для которой это решение было разработано и было особенно эффективно.
Что такое калибровка
Для тех, кто не знаком с термином «калибровка» следует пояснить что любое устройство, которое занимается измерениями (в нашем случае это часть некоторого процесса управления), должно получить некоторый набор параметров, которые позволяют устройству учитывать отклонения параметров элементной базы, на которой реализован измеритель. Собственно, измерение этих отклонений их пересчет и сохранение в виде поправок для устройства и составляют процесс калибровки.
Такой процесс калибровки может производиться периодически или при возникновении сомнений относительно качества проводимых устройством измерений, но в общем достаточно редко, обычно один раз после изготовления девайса!
Итого нам нужно поддерживать в памяти вот такую структуру:
struct myParamStruct {
uint16_t param1;
uint32_t param2;
uint16_t param3;
uint16_t tuneArray[8][8];
} paramSet;
В которой присутствуют несколько общих параметров и двумерный массив калибровочных параметров. Проблема была в том, что микросхема памяти, которая была реализована в схеме, на практике оказалась совершенно не работоспособна (в принципе я это знал заранее, но не переживал, потому что у меня уже был в голове этот интересный запасной вариант, который прям просился на проверку реализацией). Таким образом обращение к неиспользованной флеш-памяти, где программа не занимала и половины размера было единственным софтовым решением.
Проблема в том, что на флеш память программ — это ограничение на количество перезаписей намного более жесткое чем для Епром-типа памяти, для нашего контроллера это выражалось в тысячах раз. А вот памяти было очень много – пара сотен килобайт.
Так как же эффективно использовать 200 кбайт памяти для хранения и модификаций данных из структуры размером чуть больше 256 Байт, когда память нельзя перезаписывать по одним и тем же адресам?
Чем полезен XML
Решить эту странную задачу мне помог мой опыт работы с XML. В XML данные помечены идентификаторами и поэтому, на каком бы уровне мы ни получили XML узел мы всегда знаем, что с ним делать – куда его сохранить. И то же самое мы можем сделать при записи полей из структуры В ПРОИЗВОЛЬНОМ ПОРЯДКЕ по кругу! Достаточно любое вновь записываемое в конец поле предварять бинарным идентификатором этого поля.
Например, в нашей структуре уникальным идентификатором поля (в том числе каждого элемента массива) может являться относительное смещение поля в данной структуре. В коде определяется как:
paramSet parsVar;
short ID_Array_ij = (&parsVar.tuneArray[i][j]) - &parsVar;
short ID_param2 = (&parsVar.param2) - &parsVar;
Тогда идентификатор одновременно позволяет вычислять адрес, по которому надо сохранить прочитанное значение. Тут можно заметить что и размер данных поля (кол-во байт для чтения-сохранения) может определяться по идентификатору и его в принципе можно сделать изменяемым, а не как в этом моем примере, я намеренно опускаю этот аспект чтобы не загромождать повествование.
Конечно, для такой структуры и для такого ее использования надо убедиться, что к ней не будет применяться выравнивание полей компилятором (сплошная упаковка полей в структуре или как это там по-умному называется).
Обратите внимание, если поле помечено идентификатором, по которому легко определить как это поле записать в общую структуру, то порядок записи в последовательную память в принципе не важен, поэтому писать можно В ПРОИЗВОЛЬНОМ ПОРЯДКЕ по кругу.
Другое важное условие практической работы с настройками из Епром: нам достаточно прочитать данные из энергонезависимой памяти в структуру в оперативной памяти только один раз при включении устройства, поэтому возможные задержки связанные с тем что некоторые поля были несколько раз переписаны в Епроме и, соответственно, несколько раз будут читаться для нас не будет иметь ни какого значения, чтение Епром происходит достаточно быстро и даже тысячи лишних операций чтения не будут сравнимы с временем переключения тумблера питания.
Таким образом структура из Епром читается в оперативную память только один раз при включении устройства, потом структура используется из оперативной памяти:
для чтения как обычная структура,
а вот с записью в нее все сложнее, так как нужно же любые изменения в данных этой конфигурационной структуры сохранять в Епром-память (во флеш), так вот при том подходе, который я описываю, эта запись производится самым простым способом, по адресу текущей позиции в Епром (конечно нужна специальная переменная для хранения этого адреса), записывается:
Идентификатор поля подлежащего записи (заданного размера 2 байта, например),
И, следом, данные поля структуры.
Понятно, что тут нет ограничений на перезапись одного и того же поля конфигурационной структуры, при включении все будет последовательно прочитано и в оперативной памяти останутся последние актуальные значения.
Как найти конец круга
По процедуре чтения конфигурационной структуры остается вопрос как будет определяться конец записанных данных и начало области не инициализированной памяти Епром. Ответ, по-моему, очевиден, не инициализированная флеш заполнена FF-ами, поэтому идентификатор поля структуры, состоящий из всех FF-ов и будет являться признаком конца записанных данных.
Честно говоря, мне не пришлось делать алгоритм, который реализует запись в Епром по кругу (во флеш в нашем случае), потому что нам вполне хватило размера остатка флеш памяти для работы. А при достижении конца флеш памяти мы предусмотрели процедуру с перепрошивкой считанной структуры вместе с программой, нам все равно нужна была десктопная программа для проведения калибровки, нужно было чтение данных калибровки для анализа на ПК, поэтому не составило труда добавить в эту программу чтение прошивки и конфигурационной структуры для последующей перепрошивки.
Чтобы все-таки сделать возможной запись по кругу надо предусмотреть при чтении поиск конца записанных данных, после которого начать чтение данных, как это ни странно звучит. То есть когда запись данных по кругу переходит к началу уже записанных данных, это начало придется как то стирать с выравниванием на корректный элемент записи (ИД+поле), так чтобы в конце всегда оставалась запись с FF-ИД (кусок стертой памяти), и чтобы после стертой памяти начиналась корректная запись с корректным ИД-поля данных.
Что как-то сложно получается таким подходом, а главное не надежно вдруг какие-то данные сначала ни разу не переписывались – мы их потеряем. Видимо, проще всего по достижении конца памяти все-таки начать с сохранения полной конфигурационной структуры, так что бы всегда начинать чтение с начала адресного пространства памяти.
Некоторые вопросы программной реализации
К сожалению я не могу привести исходный код реализации для предложенной концепции поскольку во первых это было уже почти 3 года назад, и код этот уже мне не принадлежит, и написан он на очень специальном диалекте языка С для 8-битных PIC контроллеров с моделью памяти с переключаемыми банками.
Наверно всем понятно, что конфигурационная нужна нам в коде как обычная С-структура, и поэтому было бы очень нежелательно ограничивать ее использование посредством какого-то набора интерфейсных функций. Проблема в том, что при каждом обращении на запись в эту структуру мы должны вызвать соответствующую функцию дублирования такой записи в Епром, следить за позицией записи в Епром. Насколько я знаю у этой проблемы нет решения на уровне С или даже С++ компиляторов, нельзя подсунуть вызов функции на все операции присваивания полям некоторой структуры (оставив при этом все операции чтения этих полей в обычном виде). Поэтому я предложил бы не искать универсального решения для этой проблемы и решать ее в рамках конкретной задачи. В нашем случае, например, код, который перезаписывает данные конфигурации вполне локализован и вполне обозрим визуально, вполне поддается визуальному контролю. Это можно назвать организационным решением, код организован таким образом что его достаточно просто контролировать.
Чтобы по ИД поля данных найти куда это поле надо сохранить в С-структуру в оперативной памяти надо поддерживать своего рода таблицу метаданных по этой С-структуре. С метаданными на С/С++ как-то тоже не очень хорошо. Поэтому, опять же, надо исходить из условий конкретной задачи, и стараться упростить себе задачу в области программирования, например использовать вместо С-структуры, только массив. Но для какой-то супер глобальной задачи можно рассмотреть генерацию С/С++ кода специально разработанными скриптами, и таким образом создать в С/С++ коде нужные структуры с метаданными, например.
По поводу контроля целостности данных, может быть очень простое решение можно вести сквозной подсчет CRC при каждом сохранении в Епром, и добавлять CRC код под специальным ИД-кодом на все данные сохраненные от предыдущего сохраненного CRC или от начала, зависит за чем мы хотим следить. Можно вставлять этот CRC перед каждым выключении или при переделённой записанной длине.
Надеюсь, для кого то мой опыт может оказаться полезным.
Комментарии (26)
nixtonixto
08.11.2022 19:10+1Проще добавить в идентификатор счётчик с числом отсчётов больше количества умещающихся блоков и при старте искать идентификатор с максимальным значением счётчика (с учётом возможного переполнения). Или, если в системе есть RTC — добавить время. Тогда не надо ничего затирать — тупо пишем поверх устаревших данных.
rukhi7 Автор
08.11.2022 21:02так не будет это работать если какие то данные не изменялись с самого начала, если по моей концепции идти. Я для себя пришел к выводу, что надо при переходе в начало сохранять всю структуру с начального адреса. Если вы про другой метод организации записи то вообще ничего не понятно.
Indemsys
08.11.2022 19:48Концепция круга не единственная в таких вещах. Можно применить концепцию встречно растущих стеков. Она позволяют хранить много разных блоков настроек и калибровок независимо друг от друга.
rukhi7 Автор
08.11.2022 21:04в моей задаче вроде как только один блок настроек-калибровок.
А картинка прикольная :).
Indemsys
09.11.2022 01:28Бывает что выбранная концепция ограничивает функциональность. Линейные связные структуры вместо кольцевой дают больше возможностей и гибкости в управлении настройками, только и всего.
Кстати на заметку, в некоторых контроллерах в частности в Renesas, как в этой статье, после стирания FLASH в ней не 0xFF будут, а случайные числа, причем при каждом следующем чтении разные.Вот было бы интересно решить проблему файловой системы для такой FLASH.
mctMaks
10.11.2022 11:32если память не подводит, то в stm32l04 / stm32l07 стертая флешка забивалась 00.
для флешки типа NAND "пустое" значение FF, так как из 1 сделать 0 можно
для флешки типа NOR "пустое" значение 00, так как из 0 можно сделать 1а случайно содержимое для какого типа памяти характерно?
progchip666
08.11.2022 23:15Ну неужели нельзя какую-нибудь другую картинку для заставки подобрать ну ей богу уже достала она меня
rukhi7 Автор
09.11.2022 08:58картинка мне тоже не очень нравится, но я не специалист по картинкам, к сожалению
acesn
08.11.2022 23:56+1Насколько я знаю у этой проблемы нет решения на уровне С или даже С++ компиляторов, нельзя подсунуть вызов функции на все операции присваивания полям некоторой структуры
В С++ можно использовать в качестве типов параметров свой класс с перегрузкой оператора присваивания, а для чтения определить приведение этого класса к uint16_t
Или пойти более классическим путем и присваивать значения параметрам не напрямую, а через метод Set
rukhi7 Автор
09.11.2022 08:34ну да только тогда у вас все поля должны поменять тип с uint16_t -типа на тип специального класса с перегрузкой. То есть в структуре у нас будут не Шорты, а объекты специального класса, как это чудо будет компилироваться в указанных целях вы замучаетесь проверять.
Поэтому предложение это может рассматриваться как чисто теоритическое, требующее большой практической проработки.
acesn
09.11.2022 10:47В принципе, тут ни чего сложного нет, и что сделает компилятор вполне очевидно. Но если очень хочется проконтролировать что получилось, то можно пройти в отладке по шагам по ассемблерному коду. Там кода будет строк 30 на ассемблере, за пол часа можно во всех деталях рассмотреть.
Размер и расположение объектов можно посмотреть в map файле, который выдаст линкер.rukhi7 Автор
09.11.2022 11:16С тем что ничего сложного нет нельзя не согласиться! Я даже считаю что во всем программировании ничего сложного нет, просто берешь и пишешь программу, и она просто работает, а если не работает дебажишь ее, исправляешь, и она в конце концов все равно просто работает.
YDR
09.11.2022 00:26А вот LittleFS никто не разбирал? Они обещают Wear Leveling, как раз то, что нужно?
К предлагаемой автором концепции неплохо бы добавить контроль целостности. Дополнительно появится возможность записав любой бит в 0 вместо 1, испортить строку данных.
Если запись приближается к концу доступной памяти, нужно собрать данные в структуру, очистить кадры (*), и записать структуру целиком с начала.
(*) Следует обеспечить устойчивость системы к ошибкам записи после стирания и пропаданию питания между стиранием и успешной записью. Возможно, копию структуры надо также хранить в отдельном месте.
F376
09.11.2022 02:43"Не переписывать же всю структуру при изменении отдельного параметра"
Эту строчку надо разместить в тексте статьи во избежание.
TL/DR:
Си-структура конфига (настроек/параметров) хранится в RAM микроконтроллера.
WRITE.
В FLASH каждый раз записывается не вся си-структура целиком, но только изменившиеся поля структуры в виде цепочки {id1, data1}, {id2, data2}, ... где в качестве id выступает смещение поля от начала структуры. Размер data в байтах равен размеру поля и однозначно определяется по id. Запись возможна в произвольном порядке. Указатель записи нигде не хранится, свободное место отыскивается по "пустым" байтам 0xFF, (...) после операции ERASE страницы NAND FLASH (не так для EEPROM без bulk erase). Так как каждое сохранение во FLASH лишь дописывается друг за другом, выполняется техника распределения износа ячеек - Wear leveling. При исчерпании свободного места запись зацикливается с конца в начало по принципу Circular Buffer.
READ.
При чтении всегда начинаем с начала, пробегая по всей цепочке когда-либо сохраненных во FLASH пар {id,data} ... {id,data}. В зависимости от встретившегося id, считываются и многократно перезаписываются соответствующие поля си-структуры в RAM. Признаком конца служат байт(ы) 0xFF. В итоге в полях си-структуры в RAM останутся лишь последние сохраненные во FLASH значения, т.е. последний актуальный конфиг.rukhi7 Автор
09.11.2022 08:46все правильно, только это:
При исчерпании свободного места запись зацикливается с конца в начало по принципу Circular Buffer.
не надежно, потому что есть вероятность что какие то данные не переписывались с самого начала, поэтому при возвращении в начало надо( придется) начать с записи полной структуры. У меня так и сделано только не в прошивке, а через ПК эта операция производится (сброс памяти- перезапись структуры в начало), дело в том что такая операция все равно нужна и производится не только когда достигли конца памяти.
Получается что я понял почему это сделано именно так, только когда написал что же у меня сделано :), хорошо когда выясняется что что то сделано правильно :)
Хороший пример того что :
писать полезно не только код, но и описание того что этот код делает
Fox_exe
09.11.2022 09:22Один из интересных вариантов - это хранить размер блока данных и его версию. Ну и писать данные по кругу.
Ну а при старте контроллера - считывать весь фелш в поиске последнего (самого нового) блока данных.
Из плюсов - можно откатываться на предыдущий конфиг, если текущий невозможно прочитать. Из минусов - "лишние" 1-4 байта под хранение версии данных.rukhi7 Автор
09.11.2022 10:08версия имеет смысл когда у версии есть какое то описание:
зачем она создана, что в ней интересного, чем она отличается от предыдущей версии,
Описание вы не засунете в контроллер, поэтому вывод: версии должны регистрироваться на ПК и там и подписываться! Просто плодить версии по времени не очень умно (из моей практики).
если вы кокаго то подобного описания про версию не знаете то перебор версий будет приводить к эффекту как в анекдоте (замените "приборы" на "версия"):
летят Петька с Василий Иванычем в самолете.
Василий Иваныч спрашивает: "Петька, приборы!"
Петька: "Двадцать!"
Василий Иваныч : "ЧО двадцать?"
Петька: "а ЧО приборы?" "
Mishootk
09.11.2022 12:33Примерно на этой идеологии делал сохранение последнего состояние прибора и его настроек (после перезагрузки необходимо было подняться ровно в том же состоянии, что и до пропадания питания). Ради экономии ресурса носителя записывался не каждый чих по изменению состояния, а обрабатывалось прерывание пропадания питания. Времени было как раз записать подготовленный сектор. Очистка производилась заранее. Запись по кругу, блоки с растущей нумерацией, в конце блока последней записывалась его контрольная сумма. Автоматически решается проблема записи битого блока (не хватило времени на запись) - при загрузке берется самый свежий корректно подписанный блок. Использование устройства с предыдущими настройками тоже допускалось по ТЗ. Размер данных не обязательно кратен сектору - очистка была посекторная, а в чистый сектор можно было писать хоть по одному байту, главное держать вперед резерв достаточный для записи очередного блока настроек. В эту концепцию хорошо укладываеся и инкрементивная запись - пишем не весь блок данных, а только те, которые изменились от предыдущего. Естественно, контролируем размер кольца, чтобы в кольце был хотя бы один ключевой кадр.
Плюс функция перешагивания битого блока - исходим из того, что его размер тоже не корректен. Идет поиск очищенной области после последнего корректного блока и пометка начала после сбоя уникальным идентификатором (при чтении если блок не прочитался, идет поиск этой метки для позиционирования в следующий корректный блок). Или, если потеря размера сектора не страшна, то просто переход на следующий сектор начинающийся с FF.Fox_exe
09.11.2022 12:36Кстати, тоже вариант, если данных мало: В начало сектора пишем целый блок данных, далее дописываем только изменения.
rukhi7 Автор
09.11.2022 13:23это наверно маленько в другую сторону, у вас есть требование к схемотехнике обеспечить ток не менее Х в течении какого то времени после пропадания питания. Вы на основе этого схемотехнического решения решаете задачу сохранения данных. Я думаю этот аспект решения у вас основной, он все остальное определяет.
nerudo
При записи флэш обнуляются битики изначально стертые до FF. Поэтому при необходимости обновления параметров затираем нулями старую структуру, в конец дописываем новую. Для поиска используем ненулевой идентификатор. Если структуру выровнять по границам секторов, то еще проще. И не надо заморачиваться с XML и произвольным порядком.
rukhi7 Автор
нужно было изменять отдельные параметры в структуре, не переписывать же всю структуру при изменении отдельного параметра, хотя конечно тоже вариант, но мне кажется я сделал рациональнее.
devprodest
У вас же калибровка идет с участием большого брата(ПК) соответственно нет сложностей писать сразу все изменения за раз, а не по одному параметру?
rukhi7 Автор
ну да калибровка идет под управлением ПК, но бывает и перекалибровка, и просто какие то эксперименты, а память то ограниченная, и каждый байт во флеш пишется по процедуре - то есть относительно долго. Я ж говорю у меня получилось как то очень рационально все проблемы решить в совокупности при описанном подходе.Дело в том что если всю структуру переписывать, еще и надежность падает (сложно конечно оценить, но логичено ведь?), надо на достаточно длительное время записи блокировать программу на функции записи, все таки во флеш лучше поменьше писать.