Хочу поделиться своим вариантом способа хранения параметров. Мой вариант подходит не только для хранения в какой-то отдельной энергонезависимой памяти (далее Епром), он изначально придуман для хранения калибровочных значений в остатке флеш памяти программ.
Я рассматривал задачу не только с точки зрения хранения данных, а еще и с точки зрения эффективного использования Епром для их изменения.
Проблема конечности циклов записи-стирания
Почему-то автор исходной статьи умолчал о главном ограничении энергонезависимой памяти, об ограничении количества циклов записи-стирания, то есть если вы будете постоянно переписывать данные в одних и тех же физических адресах памяти, после определенного количества таких перезаписей память перестанет выполнять свою функцию, перестанет запоминать.
Собственно, именно это требование равномерной записи по всей доступной длине Епром-а и натолкнуло меня на идею организации работы с такой памятью.
Я думаю, вполне очевидно, что при ограничении количества перезаписей памяти самым эффективным способом ее использования было бы использование методом записи в память по кругу и, наверно, все бы так и использовали память если бы не одна маленькая, но неразрешимая проблема нельзя записывать данные по изменяющемуся адресу! Их же потом нельзя найти и прочитать!
Дело в том, что многие проблемы остаются не решенными просто потому, что трудно себе вообразить, что они имеют решение! Но достаточно просто попробовать их решить, чтобы понять, что решение существует, и оно в некоторых случаях даже проще чем другие очевидные решения.
Тут надо сделать небольшое отступление и рассказать дополнительные условия той задачи, для которой это решение было разработано и было особенно эффективно.
Что такое калибровка
Для тех, кто не знаком с термином «калибровка» следует пояснить что любое устройство, которое занимается измерениями (в нашем случае это часть некоторого процесса управления), должно получить некоторый набор параметров, которые позволяют устройству учитывать отклонения параметров элементной базы, на которой реализован измеритель. Собственно, измерение этих отклонений их пересчет и сохранение в виде поправок для устройства и составляют процесс калибровки.
Такой процесс калибровки может производиться периодически или при возникновении сомнений относительно качества проводимых устройством измерений, но в общем достаточно редко, обычно один раз после изготовления девайса!
Итого нам нужно поддерживать в памяти вот такую структуру:
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 перед каждым выключении или при переделённой записанной длине.
Надеюсь, для кого то мой опыт может оказаться полезным.