Comments 13
Спасибо за статью, идея хорошая, не могли бы пояснить, зачем вообще иметь древовидную структуру хранения параметров? Почему например нельзя просто сделать параметры индивидуальные и обращаться к ним по имени?
С2_12.backup(37.2F);
SuperComplexParameter.backup({1,10.f, tSring6"Hello"});
B1_1.backup(tString6{"Hello"});
LcdContarst.backup(50); //Пишем в EERPOM Один параметр c контрольной суммой
Если надо по индексу к ним обращаться то можно запихать их в список, ну типа такого.
using tParams1 = NvVarList1 <С2_12, B1_1, LcdConrast>;
tParams1::get(0).set(37.2F); //Запись в ОЗУ
tParams1::get(2).set(50);
tParams1::backup() - //пишем в EEPROM сразу все параметры с контрольной суммой для списка
Можно сделать несколько таких списков и делать ссылку друг на друга
using tParams = NvVarList <С2_12, B1_1, LcdConrast, Params1>;
Params1::get(0).set(10);
tParams::get(0).set(37.2F); //Запись в ОЗУ
tParams::get(2).set(50);
tParams::backup() - //пишем в EEPROM сразу все вместе со списком Params1
Но это правда на С++, зато там нет никаких указателей.
Если что описание параметра будет выглядеть как то так:
constexpr CachedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;
Начальное значение тут нужно для того, чтобы задать что-то в случае, если при считывании параметра будет ошибка контрольной суммы.
Ссылка на драйвер - можно подсовывать любой, который будет работать хоть с со страничной, хоть с обычной, хоть с ОЗУ и памятью программ. Он и будет обеспечивать особенности работы разных ПЗУ.
Мне кажется, вот это вот момент:
держать параметры в ОЗУ, считывая их при запуске и записывая при выключении устройства или по какому-нибудь событию
таит в себе огромную опасность. А что, если питание внезапно выключили? А что, если питание выключили посреди записи структуры?
Кажется, что в структуре нужно добавить поле count
, и когда возникает необходимость записать структуру, наращивать count
и записывать структуру рядом. При чтении считывать все доступные целые структуры, и выбирать экземпляр с максимальным (по соответствующему модулю, конечно( count
. Естественно, не забывать стирать их по кругу, оставляя, например, 3 экземпляра в памяти.
Но вот как решать проблему с тем, что при изменении одного параметра надо всю структуру переписать (изнашивая память), остаётся вопросом.
Для этого есть целый гайдлайны от производителей микроконтроллеров. И там обычно описывается варианты rotating записи в EEPROM. И сделано это вполне из осознанных вещей. У большинства EEPROM на рынке, плюс-минус количество гарантированных циклов перезаписи ячеек порядка 100000. Есть правда и high endurance варианты - но ценник выше. И этого может быть как много - так и мало, в зависимости от приложения (может оно раз в месяц будет обновлять ячейку,) а вот если обновлять раз в секунду - то пичалька наступит быстро - через примерно 28 часов работы устройства . А если EEPROM вшит в кристалл контроллера - ну как то будет жалко. Поэтому существуют множество вариантов как работать с EEPROM - как минимум используя кольцевую запись структур данных, создавая rotating EEPROM библиотеки, типа таких https://tinkerman.cat/post/eeprom-rotation-for-esp8266-and-esp32. Ну и плюсом будет, целосность данных - при частичной записи последний записаный вариант будет поврежден и будет взят предыдущий.
Вариант 10
Использовать easyflash, или flashDB https://github.com/armink/EasyFlash
Вариант 11
Использовать FatFs от Чана
Обычно в проектах на МК использую вариант:
#define SETTING_ADDR 0xROMADDRESS
#define SETTING_KEY 0xRANDOMKEY
struct myParamStruct {
uint32_t key;
size_t size;
struct param_pam_pam;
uint32_t crc;
} paramSet;
settings_read(¶m_pam_pam);
serrings_write(param_pam_pam);
В функции settings_read происходит чтение памяти по указанному адресу хранения настроек, после чего вычитывается размер записанных данных, этот размер сравнивается с sizeof(param_pam_pam) если равен — то сверяем CRC и если все проверки пройдены — выгружаем себе в работу param_pam_pam.
Если что то пошло не так, например размер или CRC не сходится — грузим дефолтные настройки в param_pam_pam и вызываем serrings_write(param_pam_pam);
В param_pam_pam можем так же делать структуры из структур.
Даже школьник разберется
В противовес иерархической структуре можно использовать плоскую таблицу с парой полей parent - child.
Да, проход по таблице займёт больше тактов чем по ветвям дерева. Но разница в этих расхода несущественны при количестве параметров в районе сотен. Но при этом сильно упростится автогенерация сорсов с таблицей параметров.
Вообще кардинальное решение - не писать вручную организацию и объявление параметров в исходниках. Тогда совершенно не будет важно представлены они деревом или простой структурой.
Параметры удобно создавать в специализированной утилите с GUI на PC. После чего утилита автоматически генерирует исходники для объявления, управления и представления параметров.
При чем ведь надо ещё помнить что параметры часто должны быть доступны для просмотра и редактирования из различных интерфейсов: через терминал, через WEB, через IoT протоколы типа MQTT... И тогда приходит идея хранить все в JSON, как самом гибком для структур данных. А потом этот формат транслировать в структуру на С для доступа из программы, и в чистом виде пересылать, если надо сделать WEB интерфейс.
Сам JSON в сжатом виде хранить во Flash микроконтроллера. Для этого даже специальная файловая система придумана
Для количества данных в тысячи и более уникальных записей применяют уже настоящие базы данных типа SQLite и полнофункциональную файловую систему FAT32 на uSD карте или eMMC чипе. Где-то видел портированную версию SQLite под STM32. Но не могу вспомнить где.
Целостность обеспечивается контрольной суммой
можно разделить реализацию настроек от подсистемы хранения, как например сделано в Zephyr Settings: есть некая абстракция для работы непосредственно с деревом настроек, а за целостность отвечает слой хранения.
Странно, что обычно никто не вспоминает о такой классной штуке, как ОЗУ с батарейкой типа DS1230. Если у контроллера есть полноценная параллельная шина (адрес и данные), то это прямо идеальный вариант. Пока на ОЗУ поступает питание, она читается и пишется обычными обращениями по шине без всяких SPI или I2C, т е. просто обращаемся по указателю в нужный адрес. Как только питание пропадает, включается встроенная батарейка. Гарантированный срок хранения данных - 10 лет, на практике намного больше. И никаких проблем с долгим стиранием и ограничениями на циклы перезаписи.
Храним настройки правильно или реестр параметров для встраиваемых систем