Pull to refresh

Заметка про сохранение структур во flash памяти на STM32

Level of difficultyEasy
Reading time5 min
Views10K

При разработке проекта для микроконтроллера часто возникает необходимость сохранения данных во 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. Изначально думал уместить это в размер поста/заметки, но написал слишком много буков.

Tags:
Hubs:
Total votes 16: ↑14 and ↓2+19
Comments18

Articles