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

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

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

Ярослав
Ярослав
6 месяцев назад

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

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

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

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

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

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