Русский
Русский
English
Статистика
Реклама

Работа с параметрами в EEPROM

Введение

Привет Хабр. Наконец-то у меня появилось свободное время и я могу еще немного поделиться своим опытом, возможно кому-то это будет полезно, и поможет в его работе, и я этому буду безусловно рад. Ну что же,....

Смотря на то, как студенты делают свои курсовые, я стараюсь замечать моменты, которые вызывают у них затруднения. Одним из таких моментов является работа с внешним EEPROM. Это то место, где хранятся пользовательские настройки и другая полезная информация, которая не должна быть уничтожена после выключения питания. Самый простой пример - изменение единиц измерения. Пользователь жмет на кнопку и меняет единицы измерения. Ну или записывает коэффициенты калибровки через какой-нибудь внешний протокол, типа Модбаса.

Всякий раз, когда студент решает что-то сохранить в EEPROM, это выливается во множество багов, связанных как с неверно выбранной архитектурой, так и просто человеческим фактором. Собственно обычно студент лезет в интернет и находит что-то типа этого:

int address = 0;float val1 = 123.456f;byte val2 = 64;char name[10] = "Arduino";EEPROM.put(address, val1);address += sizeof(val1); //+4EEPROM.put(address, val2);address += sizeof(val2); //+1EEPROM.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 символов по адресу параметра myStrDataauto returnStatus = myStrData.Set(tStr6{"Hello"}); if (!returnStatus){std::cout << "Ok"}//Записываем в EEPROM float параметр по адресу параметра myFloatDatareturnStatus = myFloatData.Set(37.2F); 

Ну что же приступим

Анализ требований и дизайн

Пока что не будем заморачиваться проблемой блокировок, постараемся сделать только удобный доступ к самим параметрам, а затем на основе этого уже можно будет модифицировать решение для любого использования.

Давайте поймем, что мы вообще хотим. Сформируем требования более детально:

  • Каждая наша переменная(параметр) должна иметь уникальный адрес в EEPROM

    • Мы не хотим руками задавать этот адрес, он должен высчитываться сам, на этапе компиляции, потому что мы не хотим, чтобы студент нечаянно задал неверный адрес и сбил все настройки

  • Мы не хотим постоянно лазить в EEPROM, когда пользователь хочет прочитать параметр

    • Обычно EEPROM подключается через I2C или SPI. Передача данных по этим интерфейсам тоже отнимает время, поэтому лучше кэшировать параметры в ОЗУ, и возвращать сразу копию из кеша.

  • При инициализации параметра, если не удалось прочитать данные с EEPROM, мы должны вернуть какое-то значение по умолчанию.

  • На плате могут быть несколько EEPROM, а может вообще и не EEPROM, а скажем второй процессор, где хранятся разные данные, поэтому мы должны предусмотреть, возможность того, чтобы параметр мог использовать заданный драйвер для записи и чтения в нужное место.

  • Все должно быть дружелюбным простым и понятным :)

Давайте прикинем дизайн класса, который будет описывать такой параметр и удовлетворять нашим требованиям: Назовем класс CaсhedNvData

CachedNvData

Вообще все должно быть понятно из картинки, но на всякий случай:

При вызове метода 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 declarationstruct 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/W5fPjh6ae

P.S Хотел еще рассказать про то, как можно реализовать драйвер работы с EEPROM через QSPI (студенты слишком долго понимали как он работает), но слишком разношерстный получался контекст, поэтому думаю описать это в другой статье, если конечно будет интересно.

Источник: habr.com
К списку статей
Опубликовано: 02.06.2021 22:14:13
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

C++

Программирование микроконтроллеров

Stm32

Микроконтроллеры

Eeprom

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru