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

Для начала рассмотрим работу с flash на контроллере STM32F103C8T6 (blue pill). Начнем с изучения документации. Смотрим, как разбита память, в reference manual, в разделе Embedded Flash memory. Либо можно подключить контроллер через программатор и посмотреть в Cube Programmer.

Таблица памяти из datasheet
Таблица памяти из datasheet

Память контроллера разбита на страницы объемом 1 Кбайт. В datasheet приведена длительность на операцию стирания - максимум 40 мс.

Тайминги работы с Flash на F103
Тайминги работы с Flash на F103

Пользовательская прошивка хранится в начале памяти, поэтому для записи данных я использую страницы с конца микроконтроллера. Будем писать в 126 страницу по адресу 0x0801F800. 127 страница остается как запасная, если данные не помещаются на одну страницу.

#define flashADDR   0x0801F800

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

struct
{
	uint8_t  var1;
	uint16_t var2;
	uint32_t var3;
	double   var4;
} test_struct;

Заполняю ее случайными значениями:

test_struct.var1 = 200;
test_struct.var2 = 59999;
test_struct.var3 = 98765;
test_struct.var4 = 45.11;

Теперь перейдем к записи во Flash. Более подробно про это можно почитать тут и тут, но если коротко, то в начале нам необходимо разблокировать память, стереть нужную нам страницу, после уже произвести запись и заблокировать обратно. 

Создадим отдельную функцию и переменные:

uint8_t writeFlash (uint32_t addr)
{
	HAL_StatusTypeDef status;
	uint32_t structureSize = sizeof(test_struct);
	FLASH_EraseInitTypeDef FlashErase; 
	uint32_t pageError = 0;

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

__disable_irq();
status = HAL_FLASH_Unlock();

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

FlashErase.TypeErase = FLASH_TYPEERASE_PAGES;
FlashErase.PageAddress = addr; 
FlashErase.NbPages = structureSize / 1024 + 1; 

Теперь очистим необходимое нам пространство. Если случилась ошибка в процессе стирания, то закрываем память и сообщаем об этом.

if (HAL_FLASHEx_Erase(&FlashErase, &pageError) != HAL_OK)
	{
		HAL_FLASH_Lock(); 
        __enable_irq();
		return HAL_ERROR;
	}

Теперь все готово к записи нашей структуры. Создаем указатель на неё и с помощью функции HAL_FLASH_Program записываем все её содержимое. Функция первым аргументом запрашивает, каким объемом будут писать данные - по 1, 2 или четырем байтам за раз (FLASH_TYPEPROGRAM_BYTE/HALFWORD/WORD соответственно). Забавно то, что дальше эта функция, вне зависимости от вашего выбора, будет писать строго по 2 байта ?(конкретно на этом камне). Я думаю, это сделано для того, чтобы поддержать единообразие hal от камня к камню, потому что на F401 эта функция уже умеет работать с различными режимами записи. После записи всех данных мы закрываем память, включаем все прерывания и возвращаем статус. 

uint32_t *dataPtr = (uint32_t *)&test_struct;
for (int i = 0; i < structureSize / 4; i++)  
{
    status += HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, dataPtr[i]);
    addr += 4; 
}
__enable_irq();
HAL_FLASH_Lock();  
return status;

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

void readFlash (uint32_t addr)
{
	uint32_t structureSize = sizeof(test_struct);
	uint32_t *dataPtr = (uint32_t *)&test_struct;
	for (int i = 0; i < structureSize / 4; i++)
	{
		dataPtr[i] = *(__IO uint32_t*)addr;
		addr += 4;
	}
}

Тут аналогично: в начале определяется размер структуры, а после считываем по 4 байта за раз. 

Теперь возьмем контроллер посолиднее - F401 (black pill). Тут механизм записи будет выглядеть немного иначе, так как там деление памяти идет уже на сектора (reference):

Таблица памяти из datasheet на F401
Таблица памяти из datasheet на F401

Конкретно в моем камне есть только первые 6 секторов, при этом последний из них довольно крупный - 128 Кбайт. Изучим тайминги стирания/записи тут.

Стирание сектора может занимать несколько секунд, однако. Можно увидеть, что этот контроллер поддерживает запись по одному, двум и четырем байтам. Самое интересное тут то, что чем ниже напряжение питания, тем меньше байт за раз можно будет записать. Я попробовал запись по одному байту при VCC = 1.76 В - процесс заметно медленный, но все работает. 

Перейдем к коду. От предыдущего он будет отличаться только тем, что страницы необходимо заменить на сектора. Пример для записи по 4 байта. Функции чтения аналогичны.

uint8_t writeFlash (uint32_t addr)
{
	HAL_StatusTypeDef status;
	uint32_t structureSize = sizeof(test_struct);
	FLASH_EraseInitTypeDef FlashErase;
	uint32_t sectorError = 0; 

	__disable_irq();
	HAL_FLASH_Unlock();  

	FlashErase.TypeErase = FLASH_TYPEERASE_SECTORS;
	FlashErase.NbSectors = 1;
	FlashErase.Sector = FLASH_SECTOR_5;
	FlashErase.VoltageRange = VOLTAGE_RANGE_3;
	if (HAL_FLASHEx_Erase(&FlashErase, &sectorError) != HAL_OK)  
	{
		HAL_FLASH_Lock();
        __enable_irq();
		return HAL_ERROR;
	}
	uint32_t *dataPtr = (uint32_t *)&test_struct;
	for (uint8_t i = 0; i < structureSize / 4; i++)
	{
		status += HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, dataPtr[i]);
		addr += 4;
	}
	__enable_irq();  
	return status;
}

Запись побайтово:

Hidden text
/*----------------------------------------------------------------------------*/
uint8_t writeFlash (uint32_t addr)
{
	HAL_StatusTypeDef status;
	uint32_t structureSize = sizeof(test_struct);
	FLASH_EraseInitTypeDef FlashErase; 
	uint32_t sectorError = 0;

	__disable_irq(); 
	HAL_FLASH_Unlock();   

	FlashErase.TypeErase = FLASH_TYPEERASE_SECTORS;
	FlashErase.NbSectors = 1;
	FlashErase.Sector = FLASH_SECTOR_5;
	FlashErase.VoltageRange = VOLTAGE_RANGE_1;
	if (HAL_FLASHEx_Erase(&FlashErase, &sectorError) != HAL_OK) 
	{
		HAL_FLASH_Lock();
        __enable_irq();
		return HAL_ERROR;
	}
	uint8_t *dataPtr = (uint8_t *)&test_struct; 
	for (uint8_t i = 0; i < structureSize; i++)
	{
		status += HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr, dataPtr[i]);
		addr++;
	}
	__enable_irq(); 
	return status;
}

/*----------------------------------------------------------------------------*/
void readFlash (uint32_t addr)
{
	uint32_t structureSize = sizeof(test_struct);
	uint8_t *dataPtr = (uint8_t *)&test_struct;
	for (int i = 0; i < structureSize; i++)
	{
		dataPtr[i] = *(__IO uint32_t*)addr;
		addr++;
	}
}

Ссылка на проекты

P.S. В процессе работы заметил, что размер данного массива 16 байт вместо рассчитанных 15 (1 + 2 + 4 + 8). Проведя небольшой поиск в интернете, я наткнулся на статью, где объясняется, почему так происходит и как оптимизировать структуры на C для экономии места. Главный совет из этой статьи - располагать элементы в порядке убывания их размера, т.е. с точностью наоборот, как у меня (например: double, double, uint32_t, uint8_t, а не в разнобой) 

P.S.S. Изначально думал уместить это в размер поста/заметки, но написал слишком много буков.