Очень часто возникает вопрос, связанный с использованием Flash-памяти микроконтроллеров STM32 в качестве EEPROM. И в этой статье рассмотрим один из вариантов эмуляции памяти EEPROM.
Итак, в чем изначально заключается проблема, и почему нельзя просто выделить сектор/страницу во Flash и использовать ее? Ответ, по большому счету, кроется в трех словах - количество циклов перезаписи. Для Flash максимально допустимое число на порядки меньше, чем для EEPROM, навскидку, 10000 циклов против 1000000.
Конечно, есть и другие отличия - скорость записи, чтения и т. д. Но упирается все именно в продолжительность жизни (что соответствует кол-ву циклов перезаписи).
Поэтому и возникает необходимость в программных алгоритмах, которые позволят искусственно увеличить это число для Flash-памяти микроконтроллера. Собственно, у ST есть целый application note, посвященный данному вопросу, более того с готовым примером кода. Так что, в принципе, можно спокойно использовать решение от производителя.
Но так получилось, что я начал править этот проект под себя и в итоге переделал все с нуля. Не буду судить, насколько это оказалось оправданно, просто распишу основные идеи, которые предлагает ST, и затем, как обычно, создадим пример для проверки работоспособности. И заодно разберем основные функции.
Первый ключевой аспект заключается в том, как будут храниться данные. Для каждой переменной существует определенное фиксированное значение - ключ. Выделим под ключ и под саму переменную по 4 байта. Допустим нам надо хранить во Flash значения трех переменных:
Переменная | Значение переменной | Значение ключа |
---|---|---|
data_1 | 0x1010 | 0x11111111 |
data_2 | 0x2020 | 0x22222222 |
data_3 | 0x3030 | 0x33333333 |
Непосредственно в памяти данные будут выглядеть так:
Для каждой переменной сохраняется пара ключ-значение.
А если нам потребуется обновить значение переменной data_2
на новое, к примеру - 0x4567, то мы просто записываем еще одну пару ключ-значение в свободное место памяти:
Получается, что для переменной data_2
сохранено два значения, при этом актуальным является второе, записанное позже. Но при этом первое не удаляется. Таким образом, значения переменных будут обновляться (записываться в свободные адреса) до тех пор, пока страница памяти не окажется полностью заполненной:
Далее в дело вступает второй ключевой аспект. Он заключается в использовании двух страниц Flash-памяти. По заполнению 1-ой страницы мы переносим данные из нее во 2-ую страницу. Но! Переносим не все значения подряд, а только актуальные, то есть записанные последними. И так для каждой переменной:
Заполняем 2-ую страницу этими величинами и очищаем страницу 1:
И далее все эти действия повторяются до тех пор, пока не заполнится и вторая страница, после чего они снова меняются местами.
Что это дает в итоге?
- В первую очередь, количество циклов перезаписи изначально уменьшается на порядки, поскольку мы не очищаем Flash каждый раз, когда нам нужно обновить значение одной переменной. Очистка происходит только по фактическому заполнению страницы.
- Кроме того, мы используем две страницы, что позволяет распределить эти циклы перезаписи между ними.
Для того, чтобы идентифицировать, в каком состоянии находятся страницы Flash-памяти, которые мы используем, добавим в начальные адреса каждой из них ее статус:
Статус | Значение | Описание |
---|---|---|
CLEARED | 0xFFFFFFFF | Страница очищена. |
ACTIVE | 0x00000000 | Страница активна. |
RECEIVING_DATA | 0x55555555 | Страница в процессе копирования данных из другой страницы. |
В итоге первые 4 байта у нас заняты текущим статусом страницы. Поскольку для каждого значения переменной суммарно у нас используются 8 байт, то добавим смещение еще на 4 байта относительно начала страницы (для удобства). Результат будет таким:
Жизнедеятельность страниц может выглядеть так:
И здесь мы получаем еще один бонус от использование нескольких страниц. Рассмотрим такую ситуацию - 1-я страница заполнена и начинается копирование ее данных во 2-ю страницу. И в середине этого процесса возникает сбой питания, и процесс обрывается. После устранения проблемы и включения платы первая страница окажется в состоянии Active, а вторая - в состоянии Cleared. Что просто приведет к повторной попытке переноса данных, то есть данные не утеряны, а по-прежнему спокойно себе лежат в странице 1.
А если бы у нас была только одна страница, то по ее заполнению нам нужно было бы перенести данные в некий массив в оперативной памяти (RAM) контроллера, чтобы затем очистить страницу и обновить ее же из этого массива (оставив таким образом только актуальные, то есть последние, значения переменных). Так вот при обрыве питания в момент, когда страница уже очищена, а данные еще в RAM, они были бы безвозвратно потеряны.
Итак, возвращаемся к эмуляции EEPROM и теперь бегло рассмотрим конкретные функции, которые все это реализуют.
Все функции возвращают одно из значений:
typedef enum { EEPROM_OK = 0, EEPROM_ERROR = 1, } EepromResult;
Для части функций просто рассмотрим их назначение, а некоторые разберем подробнее, если это важно для понимания сути процессов.
EepromResult EEPROM_ClearPage(PageIdx idx)
. Производит очистку страницы, номер которой передан в качестве аргумента.EepromResult EEPROM_CopyPageData(PageIdx oldPage, PageIdx newPage)
. Копирует содержимое страницы oldPage в newPage. При этом обновляет статусы обеих страниц по результатам копирования/очистки.EepromResult EEPROM_Format()
. Полная очистка EEPROM - то есть очистка страниц Flash, которые используются для эмуляции EEPROM.-
EepromResult EEPROM_GetActivePageIdx(PageIdx *idx)
. Позволяет определить номер активной страницы. EepromResult EEPROM_Init()
. Функция инициализации, на ней как раз и остановимся подробнее.
EepromResult EEPROM_Init() { EepromResult res = EEPROM_OK; PageState pageStates[PAGES_NUM]; for (uint8_t i = 0; i < PAGES_NUM; i++) { pageStates[i] = EEPROM_ReadPageState((PageIdx)i); } if (((pageStates[PAGE_0] == PAGE_CLEARED) && (pageStates[PAGE_1] == PAGE_CLEARED)) || ((pageStates[PAGE_0] == PAGE_ACTIVE) && (pageStates[PAGE_1] == PAGE_ACTIVE)) || ((pageStates[PAGE_0] == PAGE_RECEIVING_DATA) && (pageStates[PAGE_1] == PAGE_RECEIVING_DATA))) { res = EEPROM_Format(); if (res != EEPROM_OK) { return res; } res = EEPROM_SetPageState(PAGE_0, PAGE_ACTIVE); } if ((pageStates[PAGE_0] == PAGE_RECEIVING_DATA) && (pageStates[PAGE_1] == PAGE_CLEARED)) { res = EEPROM_SetPageState(PAGE_0, PAGE_ACTIVE); } if ((pageStates[PAGE_0] == PAGE_CLEARED) && (pageStates[PAGE_1] == PAGE_RECEIVING_DATA)) { res = EEPROM_SetPageState(PAGE_1, PAGE_ACTIVE); } if ((pageStates[PAGE_0] == PAGE_RECEIVING_DATA) && (pageStates[PAGE_1] == PAGE_ACTIVE)) { res = EEPROM_CopyPageData(PAGE_1, PAGE_0); } if ((pageStates[PAGE_0] == PAGE_ACTIVE) && (pageStates[PAGE_1] == PAGE_RECEIVING_DATA)) { res = EEPROM_CopyPageData(PAGE_0, PAGE_1); } return res; }
Здесь мы пробегаем по возможным комбинациям состояний страниц. Если статус страниц совпадает, то это сигнализирует об ошибке, форматируем память полностью и начинаем с чистого листа:
if (((pageStates[PAGE_0] == PAGE_CLEARED) && (pageStates[PAGE_1] == PAGE_CLEARED)) || ((pageStates[PAGE_0] == PAGE_ACTIVE) && (pageStates[PAGE_1] == PAGE_ACTIVE)) || ((pageStates[PAGE_0] == PAGE_RECEIVING_DATA) && (pageStates[PAGE_1] == PAGE_RECEIVING_DATA))) { res = EEPROM_Format(); if (res != EEPROM_OK) { return res; } res = EEPROM_SetPageState(PAGE_0, PAGE_ACTIVE); }
Если каким-то образом случилось так, что одна страница в состоянии Receiving Data, а вторая очищена - Cleared, то лучшее из того, что мы можем предпринять, это сделать 1-ую из них активной, поскольку в ней по сравнению со 2-ой явно хранятся какие-то полезные данные:
if ((pageStates[PAGE_0] == PAGE_RECEIVING_DATA) && (pageStates[PAGE_1] == PAGE_CLEARED)) { res = EEPROM_SetPageState(PAGE_0, PAGE_ACTIVE); } if ((pageStates[PAGE_0] == PAGE_CLEARED) && (pageStates[PAGE_1] == PAGE_RECEIVING_DATA)) { res = EEPROM_SetPageState(PAGE_1, PAGE_ACTIVE); }
И, наконец, ситуация, когда одна из страниц - Receiving Data, а вторая - Active, как раз соответствует рассмотренному нами случаю с обрывом питания. Повторяем операцию копирования:
if ((pageStates[PAGE_0] == PAGE_RECEIVING_DATA) && (pageStates[PAGE_1] == PAGE_ACTIVE)) { res = EEPROM_CopyPageData(PAGE_1, PAGE_0); } if ((pageStates[PAGE_0] == PAGE_ACTIVE) && (pageStates[PAGE_1] == PAGE_RECEIVING_DATA)) { res = EEPROM_CopyPageData(PAGE_0, PAGE_1); }
Идем дальше по функциям:
- EepromResult EEPR
OM_PageTransfer(PageIdx activePage, uint32_t varId, uint32_t varValue)
. Выполняет перенос актуальных данных из одной страницы в другую, кроме того в новую страницу добавляет пару ключ - значение, переданную через аргументы (varId
/varValue
). Осуществляется это так:- Записываем упомянутые данные из аргументов:
varId
/varValue
. - Проходим по всему списку ключей и для каждого находим актуальное значение, которое и пишем в новую страницу.
- Стираем старую страницу.
- Записываем упомянутые данные из аргументов:
- Функции управления состоянием страницы:
PageState EEPROM_ReadPageState(PageIdx idx)
EepromResult EEPROM_SetPageState(PageIdx idx, PageState state).
- И напоследок осталось самое интересное - чтение и запись данных.
Код функции записи:
EepromResult EEPROM_Write(uint32_t varId, uint32_t varValue) { EepromResult res = EEPROM_OK; uint8_t validId = 0; for (uint32_t curVar = 0; curVar < VAR_NUM; curVar++) { if (varIdList[curVar] == varId) { validId = 1; break; } } if (validId == 0) { res = EEPROM_ERROR; return res; } PageIdx activePage = PAGE_0; res = EEPROM_GetActivePageIdx(&activePage); if (res != EEPROM_OK) { return res; } uint32_t startAddr = pageAddress[activePage] + PAGE_DATA_OFFSET; uint32_t endAddr = pageAddress[activePage] + PAGE_SIZE - PAGE_DATA_SIZE; uint32_t addr = startAddr; uint8_t freeSpaceFound = 0; while (addr <= endAddr) { uint32_t idData = FLASH_Read(addr); if (idData == 0xFFFFFFFF) { freeSpaceFound = 1; break; } else { addr += PAGE_DATA_SIZE; } } if (freeSpaceFound == 1) { res = EEPROM_WriteData(addr, varId, varValue); } else { res = EEPROM_PageTransfer(activePage, varId, varValue); } return res; }
Первым делом проверяем, что переданный в функцию ключ varId
является валидным, то есть находится в списке допустимых ключей (этот момент мы разберем подробнее на практическом примере ниже).
Далее определяем номер активной страницы. Остается найти свободное место и записать туда новые данные. При поиске свободного места читаем данные, которые располагаются там, где должны быть ключи. Соответственно, по тому адресу, где на месте ключа значение 0xFFFFFFFF (ячейка памяти пуста), и производим запись.
Если же свободного места не обнаружено, то переходим к процессу переноса данных в другую страницу - EEPROM_PageTransfer()
.
И код для чтения данных:
EepromResult EEPROM_Read(uint32_t varId, uint32_t *varValue) { EepromResult res = EEPROM_OK; PageIdx activePage = PAGE_0; res = EEPROM_GetActivePageIdx(&activePage); if (res != EEPROM_OK) { return res; } uint32_t startAddr = pageAddress[activePage] + PAGE_DATA_OFFSET; uint32_t endAddr = pageAddress[activePage] + PAGE_SIZE - PAGE_DATA_SIZE; uint32_t addr = endAddr; uint8_t dataFound = 0; while (addr >= startAddr) { uint32_t idData = FLASH_Read(addr); if (idData == varId) { dataFound = 1; *varValue = FLASH_Read(addr + 4); break; } else { addr -= PAGE_DATA_SIZE; } } if (dataFound == 0) { res = EEPROM_ERROR; } return res; }
Находим номер активной страницы и начинаем поиск нужного нам ключа (varId
) среди сохраненных в памяти. При этом в отличие от функции записи поиск ведем от конца страницы к началу, то есть в обратном направлении. Связано это с тем, что как мы уже разобрали в начале статьи, при обновлении значений для определенного ключа новые значения записываются в память последовательно. То есть среди всех значений с одним и тем же ключом актуальным будет последнее из них (которое ближе к концу страницы).
Вот и все, находим ключ, вычисляем адрес, где хранится значение (добавив смещение на длину ключа, равную 4 байтам) и читаем данные.
Переходим к практическому примеру использования - будем сохранять величины двух переменных, каждой из которых поставим в соответствие значение ключа.
В файле eeprom.h нам нужно выполнить конфигурацию эмулятора:
#define PARAM_1 0x12121212 #define PARAM_2 0x34343434 #define VAR_NUM 2
VAR_NUM
- это количество ключей, а значит разных переменных, которые мы будем сохранять.- Для каждого из этих ключей задаем значения:
PARAM_1
,PARAM_2
.
В файле eeprom.c помещаем их в массив:
static uint32_t varIdList[VAR_NUM] = {PARAM_1, PARAM_2};
Помимо этого указываем адреса использующихся страниц. У меня для этого примера микроконтроллер STM32F103C8, и адресация Flash-памяти выглядит следующим образом:
Берем две последние страницы:
#define PAGE_0_ADDRESS 0x0801F800 #define PAGE_1_ADDRESS 0x0801FC00 #define PAGE_SIZE 1024
И переходим в функцию main()
:
EEPROM_Init(); uint32_t readData[2] = {0, 0}; EEPROM_Write(PARAM_1, 0x1111); EEPROM_Write(PARAM_2, 0x2222); EEPROM_Read(PARAM_1, &readData[0]); EEPROM_Read(PARAM_2, &readData[1]);
Инициализируем эмуляцию EEPROM, записываем значения для наших ключей и сразу же считываем их из памяти. Ожидаемо получаем:
И в Flash-памяти:
Обновим значение второго параметра и снова считаем:
EEPROM_Write(PARAM_2, 0x3333); EEPROM_Read(PARAM_1, &readData[0]); EEPROM_Read(PARAM_2, &readData[1]);
Результат логичен:
И в памяти видим, что новое значение записалось в свободное место страницы, при этом и старое физически никуда не делось, хотя оно нам больше и не нужно:
Здесь мы просто передавали числа в функцию записи, соответственно, в общем случае там будут некие переменные:
EEPROM_Write(PARAM_1, var1); EEPROM_Write(PARAM_2, var2); // ... EEPROM_Write(PARAM_2, var2);
Из ближайших доработок - необходимо добавить в записываемые данные контрольную сумму для проверки корректности сохраненных значений. Как вариант - сократить ключ на 4 байта и освободившееся место использовать под CRC. И на этом заканчиваем на сегодня - до скорого 🤝 (также не упускайте из виду комментарии, там есть отличные советы по оптимизации и модификации процессов).
Полный проект - MT_Eeprom.
Хорошая статья! Есть одно дополнение: что бы каждый раз при чтении/записи не шерстить весь банк, удобно сделать таблицу привязки переменной к последнему адресу расположения. В итоге, при инициализации сканируем всё, заполняем таблицу значениями: varPtr, flashAddr. При записи - обновляем, а при чтении просто берём последний адрес оттуда. По такому же приципу можно держать в уме адрес последней записанной страницы. Конечно, целесообразность этого зависит от того, что дороже- время выполнения или ОЗУ.
Приветствую.
Вот вместо того, чтобы лить тонны воды, разъясняя что и кк работает, лучше бы нарисовал и выложил НОРМАЛЬНУЮ библиотеку работы с EEPROM платки stm32f103c8t6 под arduino ide с человеческим описанием и примерами!
Я уверен, что люди сказали бы тебе за это ГРОМАДНОЕ спасибо!!!
Доброго дня. В конце статьи готовый пример с использованием созданной библиотеки (после текста "Переходим к практическому примеру использования...").
Arduino IDE не использую, тем более для STM32.
А смысл переходить на STM используя по прежнему достаточно убогую среду ArduinoIDE, если вы не хотите развиваться, сидите себе на Arduino NANO дальше. STM нужно изучать гораздо глубже, тогда будет толк. Но если вы дальше digitalWrite не продвинулись, то это явно не ваше
Спасибо, работает замечательно!
Отлично!
На мой взгляд, Вы зря пишете сначала адрес, а потом значение переменно (У STM было наоборот). Вполне возможно случится так, что адрес Вы записать успели, а во время записи данных пропадёт питание. В результате Вы получите сохраненную переменную с неправильным значением. У STM такого "глюка" не произошло бы.
Доброго времени, да, звучит более чем разумно )
Перед записью добавил проверку на совпадение хранящегося и записываемого значений. Если совпадают - ничего не пишем, выходим с результатом ОК.
Уважаемый, автор. Подскажите, как записывать и считывать переменные типа float?
Если на скорую руку, то как вариант - запись:
Чтение:
Добрый день! Как посмотреть память в Сube IDE?
Доброго времени суток!
Window > ShowView > Memory и там задавать нужные адреса, прилагаю скрин.
Спасибо большое. Но у меня вместо данных вопросики стоят.
Вот скрин:
Поставь адрес 0x08000000, также будет?
Да, также!
А программа на паузе?
Да, разобрался! Только на паузе есть данные. Или в пошаговой отладке с точками прерывания. Когда запускаю на непрерывный цикл - данные становятся вопросиками!
Ага, так и должно быть)
Спасибо Вам большое за помощь!
Обращайся если что!
Добрый день! А есть готовый пример для МК STM32F407x?
Доброго времени суток )
Готового нет, но в целом там без проблем перенести можно, практически без изменений.
У меня возникает при записи данных DOUBLE_WORD проблема, а при записи данных WORD всё проходит!
Добрый день! У меня возникает ошибка при записи данных DOUBLE_WORD , а при записи данных WORD всё проходит! Контроллер STM32F407ZGT6. Может кто сталкивался с этой проблемой?
Можешь проект на почту скинуть? Могу вечером посмотреть, есть F407 тем более под рукой.
Пока разбираюсь, где может быть загвоздка, использую запись данных WORD.
просьба к автору обратить внимание на семантическую неточность применения глагола 'увеличивается' в тексте: "количество циклов перезаписи изначально увеличивается на порядки" - согласно контекста речь идет о результате применения автором метода, снижающего количество _осуществляемых_ ПО циклов перезаписи ячеек, поэтому, следовало бы употребить глаголы "уменьшается", "снижается".
Да, действительно... 😀
Спасибо!