[Программирование, C++, Программирование микроконтроллеров] Работа с параметрами в EEPROM
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
ВведениеПривет Хабр. Наконец-то у меня появилось свободное время и я могу еще немного поделиться своим опытом, возможно кому-то это будет полезно, и поможет в его работе, и я этому буду безусловно рад. Ну что же,....Смотря на то, как студенты делают свои курсовые, я стараюсь замечать моменты, которые вызывают у них затруднения. Одним из таких моментов является работа с внешним EEPROM. Это то место, где хранятся пользовательские настройки и другая полезная информация, которая не должна быть уничтожена после выключения питания. Самый простой пример - изменение единиц измерения. Пользователь жмет на кнопку и меняет единицы измерения. Ну или записывает коэффициенты калибровки через какой-нибудь внешний протокол, типа Модбаса.Всякий раз, когда студент решает что-то сохранить в EEPROM, это выливается во множество багов, связанных как с неверно выбранной архитектурой, так и просто человеческим фактором. Собственно обычно студент лезет в интернет и находит что-то типа этого:
int address = 0;
float val1 = 123.456f;
byte val2 = 64;
char name[10] = "Arduino";
EEPROM.put(address, val1);
address += sizeof(val1); //+4
EEPROM.put(address, val2);
address += sizeof(val2); //+1
EEPROM.put(address, name);
address += sizeof(name); //+10
Этот замечательный код лапшой разрастается по всему проекту, применяясь к месту и не совсем в каждом из 100 EEPROM параметров, имеющих разный тип, длину и адрес. Немудрено, что где-то да и допустит торопливый студент ошибку.Кроме того, обычно студенты используют РТОС, а потому нужно понимать, что обращение к EEPROM из разных потоков может привести либо к фейлам, либо ко всяким там дедлокам. Поэтому если студент использует EEPROM, я вначале прошу нарисовать дизайн, чтобы показать как он собирается работать этой подсистемой. Обычно все сводится к двум вариантам:
- Доступ к EEPROM только из одного места. Типа такой EepromManager , который запускается в отдельной задаче и проходится по списку кешеруемых EEPROM параметров и смотрит, было ли в них изменение, и если да, то пишет его в EEPROM. Тут очень большой и толстый плюс: Не нужно блокировать работу с EEPROM, все делается в одном месте. Но есть и минусы: обычно запись таких параметров происходит, когда пользователь что-то послал через внешний интерфейс, скажем Modbus протокол, ну типа он говорит, что я тут записал новые единицы измерения - Ок отвечает ему программа, я их закешировала, но на самом деле запись еще не прошла. Она пройдет позже, а ответить пользователю нужно прямо сейчас. И вот пользователь получает ответ, что все хорошо, новые единицы установлены, но вдруг во время отложенной записи происходит сбой и реальные единицы на самом деле не записались. В итоге устройство как бы обмануло пользователя. Понятно, что оно должно выставить ошибку и как-то об этом сообщить в своем статусе, но все же, пользователь уже немного с недоверием начинает смотреть на ваше устройство.
- Второй способ - пишем всегда сразу по месту.Плюс в том, что пользователь всегда получает достоверный ответ. Мы не задумываясь пишем параметр в EEPROM там где надо, и это выглядит просто.Но проблем от этого не меньше: так как мы можем писать хоть что, хоть откуда, хоть куда - скажем журнал ошибок из разных подсистем из разных задач, то придется задуматься о блокировке ресурса EEPROM.Кроме того возможно проблема с быстрыми протоколами, когда ответить нам нужно в течении ограниченного времени, скажем 5 мс, а те кто работал с EEPROM знают, что записывается там все постранично. Ну точнее, чтобы записать однобайтовый параметр, EEPROM, копирует целую страницу во свой буфер, меняет в этом буфере этот один несчастный байт, стирает страницу, и затем записывает буфер (ну т.е. всю страницу) и того на запись одной страницы сразу тратится от 5 до 10 мс, в зависимости от размера страницы.
Но в обоих этих способах, мы хотим, чтобы доступ к параметрам не был похож, на тот код с Ардуино, что я привел, а был простым и понятным, в идеале, чтобы было вообще так:
//Записываем 10.0F в EEPROM по адресу, где лежит myEEPROMData параметр
myEEPROMData = 10.0F;
Но так мы делать не будем, потому что иногда нам понадобится по месту вернуть статус операции записи, вдруг EEPROM битая или проводки отпаялись. И посему мы будем делать, что-то похожее на это:
//Записываем в EEPROM строку из 5 символов по адресу параметра myStrData
auto returnStatus = myStrData.Set(tStr6{"Hello"});
if (!returnStatus)
{
std::cout << "Ok"
}
//Записываем в EEPROM float параметр по адресу параметра myFloatData
returnStatus = myFloatData.Set(37.2F);
Ну что же приступимАнализ требований и дизайнПока что не будем заморачиваться проблемой блокировок, постараемся сделать только удобный доступ к самим параметрам, а затем на основе этого уже можно будет модифицировать решение для любого использования.Давайте поймем, что мы вообще хотим. Сформируем требования более детально:
- Каждая наша переменная(параметр) должна иметь уникальный адрес в EEPROM
- Мы не хотим руками задавать этот адрес, он должен высчитываться сам, на этапе компиляции, потому что мы не хотим, чтобы студент нечаянно задал неверный адрес и сбил все настройки
- Мы не хотим постоянно лазить в EEPROM, когда пользователь хочет прочитать параметр
- Обычно EEPROM подключается через I2C или SPI. Передача данных по этим интерфейсам тоже отнимает время, поэтому лучше кэшировать параметры в ОЗУ, и возвращать сразу копию из кеша.
- При инициализации параметра, если не удалось прочитать данные с EEPROM, мы должны вернуть какое-то значение по умолчанию.
- На плате могут быть несколько EEPROM, а может вообще и не EEPROM, а скажем второй процессор, где хранятся разные данные, поэтому мы должны предусмотреть, возможность того, чтобы параметр мог использовать заданный драйвер для записи и чтения в нужное место.
- Все должно быть дружелюбным простым и понятным :)
Давайте прикинем дизайн класса, который будет описывать такой параметр и удовлетворять нашим требованиям: Назовем класс CaсhedNvDataCachedNvData
Вообще все должно быть понятно из картинки, но на всякий случай:При вызове метода Init() мы должны полезть в EEPROM и считать оттуда нужный параметр с нужного адреса. Адрес будет высчитываться на этапе компиляции, пока эту магию пропустим. Прочитанное значение хранится в data, и как только кому-то понадобится, оно возвращается немедленно из копии в ОЗУ с помощью метода Get(). А при записи, мы уже будем работать с EEPROM через nvDriver. Можно подсунуть любой nvDriver, главное, чтобы у него были методы Set() и Get(). Вот например, такой драйвер подойдет.NvDriver
Остался еще один штрих, придумать, как автоматически формировать адрес каждого параметра. Для того, чтобы адрес высчитывался автоматически, необходимо, чтобы все параметры для EEPROM были зарегистрированы в каком-нибудь списке. Тогда список может сам посчитать адрес параметра по его положению в списке и собственно вернуть его когда надо.Например, если у нас есть 3 параметра:
//Длина параметра 6 байт
constexpr CachedNvData<NvVarList, tString6, myStrDefaultValue, nvDriver> myStrData;
//Длина параметра 4 байта
constexpr CachedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;
//Длина параметра 4 байт
constexpr CachedNvData<NvVarList, std::uint32_t, myUint32DefaultValue, nvDriver> myUint32Data;
То когда мы сделаем какой-то такой список:
NvVarList<100U, myStrData, myFloatData, myUint32Data>
У нас бы у myStrData был бы адрес 100, у myFloatData - 106, а у myUint32Data - 110. Ну и соответственно список мог бы его вернуть для каждого из параметра.Собственно нужно чтобы этому списку передавался начальный адрес, и список параметров в EEPROM. Также нужно чтобы у списка был метод GetAdress(), который возвращал бы адрес нужного параметра. Идея этого метода в том, чтобы найти в списке тип равный типу самого параметра, и по номеру этого элемента автоматически рассчитать адрес. Важно, чтобы типы всех параметров были разные, это добивается тем, что ссылка на значение по умолчанию должна быть уникальная для каждого параметра.Сделаем такой базовый класс, назовем его NvVarListBase:NvVarListBase
В прицнипе то и все.КодА теперь самая простая часть - пишем код. Комментировать не буду, вроде бы и так понятноCaсhedNvData
template<typename NvList, typename T, const T& defaultValue, const auto& nvDriver>
class CaсhedNvData
{
public:
ReturnCode Set(T value) const
{
//Ищем адрес EEPROM параметра в списке
constexpr auto address =
NvList::template GetAddress<NvList,T,defaultValue,nvDriver>();
//Записываем новое значение в EEPROM
ReturnCode returnCode = nvDriver.Set(
address,
reinterpret_cast<const tNvData*>(&value), sizeof(T));
//Если значение записалось успешно, обновляем копию в ОЗУ
if (!returnCode)
{
memcpy((void*)&data, (void*)&value, sizeof(T));
}
return returnCode;
}
ReturnCode Init() const
{
constexpr auto address =
NvList::template GetAddress<NvList,T,defaultValue,nvDriver>();
//Читаем значение из EEPROM
ReturnCode returnCode = nvDriver.Get(
address,
reinterpret_cast<tNvData*>(&data), sizeof(T));
//Tесли значение не прочиталось из EEPROM, устанавливаем значение по умолчанию
if (returnCode)
{
data = defaultValue;
}
return returnCode;
}
T Get() const
{
return data;
}
using Type = T;
private:
inline static T data = defaultValue;
};
template<const tNvAddress startAddress, const auto& ...nvVars>
struct NvVarListBase
{
template<typename NvList, typename T, const T& defaultValue, const auto& nvDriver>
constexpr static size_t GetAddress()
{
//Ищем EEPROM адрес параметра с типом
//CaсhedNvData<NvList, T, defaultValue, nvDriver>
using tQueriedType = CaсhedNvData<NvList, T, defaultValue, nvDriver>;
return startAddress +
GetAddressOffset<tQueriedType>(NvVarListBase<startAddress,nvVars...>());
}
private:
template <typename QueriedType, const auto& arg, const auto&... args>
constexpr static size_t GetAddressOffset(NvVarListBase<startAddress, arg, args...>)
{
//Чтобы узнать тип первого аргумента в списке,
//создаем объект такого же типа как и первый аргумент
auto test = arg;
//если тип созданного объекта такой же как и искомый, то заканчиваем итерации
if constexpr (std::is_same<decltype(test), QueriedType>::value)
{
return 0U;
} else
{
//Иначе увеличиваем адрес на размер типа параметра и переходим к
//следующему параметру в списке.
return sizeof(typename decltype(test)::Type) +
GetAddressOffset<QueriedType>(NvVarListBase<startAddress, args...>());
}
}
};
ИспользованиеА теперь встанем не место студента и попробуем это все дело использовать.Задаем начальные значения параметров:
using tString6 = std::array<char, 6U>;
inline constexpr float myFloatDataDefaultValue = 10.0f;
inline constexpr tString6 myStrDefaultValue = {"Habr "};
inline constexpr std::uint32_t myUint32DefaultValue = 0x30313233;
Зададем сами параметры:
//поскольку список ссылается на параметры, а параметры на список.
//Используем forward declaration
struct NvVarList;
constexpr NvDriver nvDriver;
//Теперь можем использовать NvVarList в шаблоне EEPROM параметров
constexpr CaсhedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;
constexpr CaсhedNvData<NvVarList, tString6, myStrDefaultValue, nvDriver> myStrData;
constexpr CaсhedNvData<NvVarList, uint32_t, myUint32DefaultValue, nvDriver> myUint32Data;
Теперь осталось определить сам список параметров. Важно, чтобы все EEPROM параметры были разных типов. Можно в принципе вставить статическую проверку на это в NvVarListBase, но не будем.
struct NvVarList : public NvVarListBase<0, myStrData, myFloatData, myUint32Data>
{
};
А теперь можем использовать наши параметры хоть где, очень просто и элементарно:
struct NvVarList;
constexpr NvDriver nvDriver;
using tString6 = std::array<char, 6U>;
inline constexpr float myFloatDataDefaultValue = 10.0f;
inline constexpr tString6 myStrDefaultValue = {"Habr "};
inline constexpr uint32_t myUint32DefaultValue = 0x30313233;
constexpr CaсhedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData;
constexpr CaсhedNvData<NvVarList, tString6, myStrDefaultValue, nvDriver> myStrData;
constexpr CaсhedNvData<NvVarList, uint32_t, myUint32DefaultValue, nvDriver> myUint32Data;
struct NvVarList : public NvVarListBase<0, myStrData, myFloatData, myUint32Data>
{
};
int main()
{
myStrData.Init();
myFloatData.Init();
myUint32Data.Init()
myStrData.Get();
returnCode = myStrData.Set(tString6{"Hello"});
if (!returnCode)
{
std::cout << "Hello has been written" << std::endl;
}
myStrData.Get();
myFloatData.Set(37.2F);
myUint32Data.Set(0x30313233);
return 1;
}
Можно передавать ссылку на них в любой класс, через конструктор или шаблон.
template<const auto& param>
struct SuperSubsystem
{
void SomeMethod()
{
std::cout << "SuperSubsystem read param" << param.Get() << std::endl;
}
};
int main()
{
SuperSubsystem<myFloatData> superSystem;
superSystem.SomeMethod();
}
Собственно и все. Теперь студенты могут работать с EEPROM более юзерфрендли и допускать меньше ошибок, ведь часть проверок за них сделает компилятор.Ссылка на пример кода тут: https://godbolt.org/z/W5fPjh6aeP.S Хотел еще рассказать про то, как можно реализовать драйвер работы с EEPROM через QSPI (студенты слишком долго понимали как он работает), но слишком разношерстный получался контекст, поэтому думаю описать это в другой статье, если конечно будет интересно.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Программирование] Доступные текстовые метки для всех (перевод)
- [Высокая производительность, Программирование, Алгоритмы, Научно-популярное] Либо быстро, либо неправильно (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, TypeScript] Как мы потерпели неудачу, а затем преуспели в переходе на TypeScript (перевод)
- [Программирование, Учебный процесс в IT, Карьера в IT-индустрии] В разработчики пойду, пусть меня научат. Бакалаврские программы при поддержке JetBrains
- [Программирование, Go] Дженерики в языке Go
- [Программирование, Машинное обучение] Балансировка массива для ML при недостаточном количестве миноритарных объектов в массиве
- [C++, Сетевые технологии, Разработка под Linux] Свой лунапарк TFTP с блэкджеком и С++17
- [Программирование, Java, .NET] Как я выбирал между .NET и Java
- [Программирование, Разработка систем связи, Видеоконференцсвязь] WebRTC для любопытных (часть 1) (перевод)
- [Программирование микроконтроллеров, Разработка на Raspberry Pi, Производство и разработка электроники] Raspberry Pi Foundation начала продажу чипов RP2040 по доллару штука
Теги для поиска: #_programmirovanie (Программирование), #_c++, #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_stm32, #_mikrokontrollery (микроконтроллеры), #_c++, #_eeprom, #_programmirovanie (
Программирование
), #_c++, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:18
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
ВведениеПривет Хабр. Наконец-то у меня появилось свободное время и я могу еще немного поделиться своим опытом, возможно кому-то это будет полезно, и поможет в его работе, и я этому буду безусловно рад. Ну что же,....Смотря на то, как студенты делают свои курсовые, я стараюсь замечать моменты, которые вызывают у них затруднения. Одним из таких моментов является работа с внешним EEPROM. Это то место, где хранятся пользовательские настройки и другая полезная информация, которая не должна быть уничтожена после выключения питания. Самый простой пример - изменение единиц измерения. Пользователь жмет на кнопку и меняет единицы измерения. Ну или записывает коэффициенты калибровки через какой-нибудь внешний протокол, типа Модбаса.Всякий раз, когда студент решает что-то сохранить в EEPROM, это выливается во множество багов, связанных как с неверно выбранной архитектурой, так и просто человеческим фактором. Собственно обычно студент лезет в интернет и находит что-то типа этого: int address = 0;
float val1 = 123.456f; byte val2 = 64; char name[10] = "Arduino"; EEPROM.put(address, val1); address += sizeof(val1); //+4 EEPROM.put(address, val2); address += sizeof(val2); //+1 EEPROM.put(address, name); address += sizeof(name); //+10
//Записываем 10.0F в EEPROM по адресу, где лежит myEEPROMData параметр
myEEPROMData = 10.0F; //Записываем в EEPROM строку из 5 символов по адресу параметра myStrData
auto returnStatus = myStrData.Set(tStr6{"Hello"}); if (!returnStatus) { std::cout << "Ok" } //Записываем в EEPROM float параметр по адресу параметра myFloatData returnStatus = myFloatData.Set(37.2F);
Вообще все должно быть понятно из картинки, но на всякий случай:При вызове метода Init() мы должны полезть в EEPROM и считать оттуда нужный параметр с нужного адреса. Адрес будет высчитываться на этапе компиляции, пока эту магию пропустим. Прочитанное значение хранится в data, и как только кому-то понадобится, оно возвращается немедленно из копии в ОЗУ с помощью метода Get(). А при записи, мы уже будем работать с EEPROM через nvDriver. Можно подсунуть любой nvDriver, главное, чтобы у него были методы Set() и Get(). Вот например, такой драйвер подойдет.NvDriver Остался еще один штрих, придумать, как автоматически формировать адрес каждого параметра. Для того, чтобы адрес высчитывался автоматически, необходимо, чтобы все параметры для EEPROM были зарегистрированы в каком-нибудь списке. Тогда список может сам посчитать адрес параметра по его положению в списке и собственно вернуть его когда надо.Например, если у нас есть 3 параметра: //Длина параметра 6 байт
constexpr CachedNvData<NvVarList, tString6, myStrDefaultValue, nvDriver> myStrData; //Длина параметра 4 байта constexpr CachedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData; //Длина параметра 4 байт constexpr CachedNvData<NvVarList, std::uint32_t, myUint32DefaultValue, nvDriver> myUint32Data; NvVarList<100U, myStrData, myFloatData, myUint32Data>
В прицнипе то и все.КодА теперь самая простая часть - пишем код. Комментировать не буду, вроде бы и так понятноCaсhedNvData template<typename NvList, typename T, const T& defaultValue, const auto& nvDriver>
class CaсhedNvData { public: ReturnCode Set(T value) const { //Ищем адрес EEPROM параметра в списке constexpr auto address = NvList::template GetAddress<NvList,T,defaultValue,nvDriver>(); //Записываем новое значение в EEPROM ReturnCode returnCode = nvDriver.Set( address, reinterpret_cast<const tNvData*>(&value), sizeof(T)); //Если значение записалось успешно, обновляем копию в ОЗУ if (!returnCode) { memcpy((void*)&data, (void*)&value, sizeof(T)); } return returnCode; } ReturnCode Init() const { constexpr auto address = NvList::template GetAddress<NvList,T,defaultValue,nvDriver>(); //Читаем значение из EEPROM ReturnCode returnCode = nvDriver.Get( address, reinterpret_cast<tNvData*>(&data), sizeof(T)); //Tесли значение не прочиталось из EEPROM, устанавливаем значение по умолчанию if (returnCode) { data = defaultValue; } return returnCode; } T Get() const { return data; } using Type = T; private: inline static T data = defaultValue; }; template<const tNvAddress startAddress, const auto& ...nvVars>
struct NvVarListBase { template<typename NvList, typename T, const T& defaultValue, const auto& nvDriver> constexpr static size_t GetAddress() { //Ищем EEPROM адрес параметра с типом //CaсhedNvData<NvList, T, defaultValue, nvDriver> using tQueriedType = CaсhedNvData<NvList, T, defaultValue, nvDriver>; return startAddress + GetAddressOffset<tQueriedType>(NvVarListBase<startAddress,nvVars...>()); } private: template <typename QueriedType, const auto& arg, const auto&... args> constexpr static size_t GetAddressOffset(NvVarListBase<startAddress, arg, args...>) { //Чтобы узнать тип первого аргумента в списке, //создаем объект такого же типа как и первый аргумент auto test = arg; //если тип созданного объекта такой же как и искомый, то заканчиваем итерации if constexpr (std::is_same<decltype(test), QueriedType>::value) { return 0U; } else { //Иначе увеличиваем адрес на размер типа параметра и переходим к //следующему параметру в списке. return sizeof(typename decltype(test)::Type) + GetAddressOffset<QueriedType>(NvVarListBase<startAddress, args...>()); } } }; using tString6 = std::array<char, 6U>;
inline constexpr float myFloatDataDefaultValue = 10.0f; inline constexpr tString6 myStrDefaultValue = {"Habr "}; inline constexpr std::uint32_t myUint32DefaultValue = 0x30313233; //поскольку список ссылается на параметры, а параметры на список.
//Используем forward declaration struct NvVarList; constexpr NvDriver nvDriver; //Теперь можем использовать NvVarList в шаблоне EEPROM параметров constexpr CaсhedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData; constexpr CaсhedNvData<NvVarList, tString6, myStrDefaultValue, nvDriver> myStrData; constexpr CaсhedNvData<NvVarList, uint32_t, myUint32DefaultValue, nvDriver> myUint32Data; struct NvVarList : public NvVarListBase<0, myStrData, myFloatData, myUint32Data>
{ }; struct NvVarList;
constexpr NvDriver nvDriver; using tString6 = std::array<char, 6U>; inline constexpr float myFloatDataDefaultValue = 10.0f; inline constexpr tString6 myStrDefaultValue = {"Habr "}; inline constexpr uint32_t myUint32DefaultValue = 0x30313233; constexpr CaсhedNvData<NvVarList, float, myFloatDataDefaultValue, nvDriver> myFloatData; constexpr CaсhedNvData<NvVarList, tString6, myStrDefaultValue, nvDriver> myStrData; constexpr CaсhedNvData<NvVarList, uint32_t, myUint32DefaultValue, nvDriver> myUint32Data; struct NvVarList : public NvVarListBase<0, myStrData, myFloatData, myUint32Data> { }; int main() { myStrData.Init(); myFloatData.Init(); myUint32Data.Init() myStrData.Get(); returnCode = myStrData.Set(tString6{"Hello"}); if (!returnCode) { std::cout << "Hello has been written" << std::endl; } myStrData.Get(); myFloatData.Set(37.2F); myUint32Data.Set(0x30313233); return 1; } template<const auto& param>
struct SuperSubsystem { void SomeMethod() { std::cout << "SuperSubsystem read param" << param.Get() << std::endl; } }; int main() { SuperSubsystem<myFloatData> superSystem; superSystem.SomeMethod(); } =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_c++, #_programmirovanie_mikrokontrollerov ( Программирование микроконтроллеров ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:18
Часовой пояс: UTC + 5