Top.Mail.Ru

Эмуляция EEPROM на базе Flash-памяти микроконтроллеров STM32.

Очень часто возникает вопрос, связанный с использованием 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

Непосредственно в памяти данные будут выглядеть так:

Эмуляция EEPROM.

Для каждой переменной сохраняется пара ключ-значение.

А если нам потребуется обновить значение переменной data_2 на новое, к примеру - 0x4567, то мы просто записываем еще одну пару ключ-значение в свободное место памяти:

Переменные в Flash-памяти.

Получается, что для переменной data_2 сохранено два значения, при этом актуальным является второе, записанное позже. Но при этом первое не удаляется. Таким образом, значения переменных будут обновляться (записываться в свободные адреса) до тех пор, пока страница памяти не окажется полностью заполненной:

Заполненная страница Flash.

Далее в дело вступает второй ключевой аспект. Он заключается в использовании двух страниц Flash-памяти. По заполнению 1-ой страницы мы переносим данные из нее во 2-ую страницу. Но! Переносим не все значения подряд, а только актуальные, то есть записанные последними. И так для каждой переменной:

Эмулятор EEPROM.

Заполняем 2-ую страницу этими величинами и очищаем страницу 1:

Очистка памяти.

И далее все эти действия повторяются до тех пор, пока не заполнится и вторая страница, после чего они снова меняются местами.

Что это дает в итоге?

  • В первую очередь, количество циклов перезаписи изначально увеличивается на порядки, поскольку мы не очищаем Flash каждый раз, когда нам нужно обновить значение одной переменной. Очистка происходит только по фактическому заполнению страницы.
  • Кроме того, мы используем две страницы, что позволяет распределить эти циклы перезаписи между ними.

Для того, чтобы идентифицировать, в каком состоянии находятся страницы Flash-памяти, которые мы используем, добавим в начальные адреса каждой из них ее статус:

Статус Значение Описание
CLEARED 0xFFFFFFFF Страница очищена.
ACTIVE 0x00000000 Страница активна.
RECEIVING_DATA 0x55555555 Страница в процессе копирования данных из другой страницы.

В итоге первые 4 байта у нас заняты текущим статусом страницы. Поскольку для каждого значения переменной суммарно у нас используются 8 байт, то добавим смещение еще на 4 байта относительно начала страницы (для удобства). Результат будет таким:

Структура Flash при эмуляции EEPROM.

Жизнедеятельность страниц может выглядеть так:

Состояния страниц памяти.

И здесь мы получаем еще один бонус от использование нескольких страниц. Рассмотрим такую ситуацию - 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 EEPROM_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-памяти выглядит следующим образом:

STM32 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-памяти:

Содержимое Flash-памяти.

Обновим значение второго параметра и снова считаем:

  EEPROM_Write(PARAM_2, 0x3333);
  EEPROM_Read(PARAM_1, &readData[0]);
  EEPROM_Read(PARAM_2, &readData[1]);

Результат логичен:

Отладка эмулятора EEPROM.

И в памяти видим, что новое значение записалось в свободное место страницы, при этом и старое физически никуда не делось, хотя оно нам больше и не нужно:

Flash.

Здесь мы просто передавали числа в функцию записи, соответственно, в общем случае там будут некие переменные:

EEPROM_Write(PARAM_1, var1);
EEPROM_Write(PARAM_2, var2);
// ...
EEPROM_Write(PARAM_2, var2);

Из ближайших доработок - необходимо добавить в записываемые данные контрольную сумму для проверки корректности сохраненных значений. Как вариант - сократить ключ на 4 байта и освободившееся место использовать под CRC. И на этом заканчиваем на сегодня - до скорого 🤝 (также не упускайте из виду комментарии, там есть отличные советы по оптимизации и модификации процессов).

Полный проект - MT_Eeprom.

Подписаться
Уведомить о
guest

28 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Alex
Alex
3 лет назад

Хорошая статья! Есть одно дополнение: что бы каждый раз при чтении/записи не шерстить весь банк, удобно сделать таблицу привязки переменной к последнему адресу расположения. В итоге, при инициализации сканируем всё, заполняем таблицу значениями: varPtr, flashAddr. При записи - обновляем, а при чтении просто берём последний адрес оттуда. По такому же приципу можно держать в уме адрес последней записанной страницы. Конечно, целесообразность этого зависит от того, что дороже- время выполнения или ОЗУ.

Ярослав
Ярослав
1 год назад

Приветствую.
Вот вместо того, чтобы лить тонны воды, разъясняя что и кк работает, лучше бы нарисовал и выложил НОРМАЛЬНУЮ библиотеку работы с EEPROM платки stm32f103c8t6 под arduino ide с человеческим описанием и примерами!
Я уверен, что люди сказали бы тебе за это ГРОМАДНОЕ спасибо!!!

Антон
Антон
Ответ на комментарий  Ярослав
1 месяц назад

А смысл переходить на STM используя по прежнему достаточно убогую среду ArduinoIDE, если вы не хотите развиваться, сидите себе на Arduino NANO дальше. STM нужно изучать гораздо глубже, тогда будет толк. Но если вы дальше digitalWrite не продвинулись, то это явно не ваше

Александр
Александр
10 месяцев назад

Спасибо, работает замечательно!

Сергей
Сергей
9 месяцев назад

На мой взгляд, Вы зря пишете сначала адрес, а потом значение переменно (У STM было наоборот). Вполне возможно случится так, что адрес Вы записать успели, а во время записи данных пропадёт питание. В результате Вы получите сохраненную переменную с неправильным значением. У STM такого "глюка" не произошло бы.

Сергей
Сергей
5 месяцев назад

Перед записью добавил проверку на совпадение хранящегося и записываемого значений. Если совпадают - ничего не пишем, выходим с результатом ОК.

Pavel
Pavel
2 месяцев назад

Уважаемый, автор. Подскажите, как записывать и считывать переменные типа float?

Маесим
Маесим
1 месяц назад

Добрый день! Как посмотреть память в Сube IDE?

Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

Спасибо большое. Но у меня вместо данных вопросики стоят.

Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

Вот скрин:

snimok
Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

Да, также!

Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

Да, разобрался! Только на паузе есть данные. Или в пошаговой отладке с точками прерывания. Когда запускаю на непрерывный цикл - данные становятся вопросиками!

Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

Спасибо Вам большое за помощь!

Максим
Максим
1 месяц назад

Добрый день! А есть готовый пример для МК STM32F407x?

Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

У меня возникает при записи данных DOUBLE_WORD проблема, а при записи данных WORD всё проходит!

Максим
Максим
1 месяц назад

Добрый день! У меня возникает ошибка при записи данных DOUBLE_WORD , а при записи данных WORD всё проходит! Контроллер STM32F407ZGT6. Может кто сталкивался с этой проблемой?

Максим
Максим
Ответ на комментарий  Aveal
1 месяц назад

Пока разбираюсь, где может быть загвоздка, использую запись данных WORD.

28
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x