
При разработке проекта для микроконтроллера часто возникает необходимость сохранения данных во Flash-память перед выключением устройства, например глобальные структуры, содержащие информацию о настройках различной периферии, данные с внешних датчиков и прочее. В этом посте я хочу показать простой механизм записи структуры во FLASH память микроконтроллера STM32, которым я сам часто пользуюсь в своих проектах.
Для начала рассмотрим работу с flash на контроллере STM32F103C8T6 (blue pill). Начнем с изучения документации. Смотрим, как разбита память, в reference manual, в разделе Embedded Flash memory. Либо можно подключить контроллер через программатор и посмотреть в Cube Programmer.

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

Пользовательская прошивка хранится в начале памяти, поэтому для записи данных я использую страницы с конца микроконтроллера. Будем писать в 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):

Конкретно в моем камне есть только первые 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, §orError) != 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, §orError) != 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. Изначально думал уместить это в размер поста/заметки, но написал слишком много буков.