
Кто хотел сохранять какие-либо данные в FLASH микроконтроллера во время работы устройства сталкивались с особенностями работы с этим видом памяти. Из-за необходимости стирания страницы большого объёма для перезаписи ячейки FLASH памяти, возникает угроза потери данных из-за отключения питания во время процесса обновления (один из вариантов). В этой статье я расскажу как можно упростить работу с FLASH, да и ещё с гарантией сохранения данных при прерывании процедуры обновления на любом этапе. Реализуем это на STM32F030.
▍ Введение
Можно ли работать с FLASH памятью, так же как и с EEPROM? Почему бы и нет? Мы можем сделать прослойку в виде драйвера, который будет предоставлять интерфейс EEPROM, а внутри жонглировать FLASH памятью. Такая эмуляция EEPROM на FLASH памяти называется виртуальным EEPROM (VEEPROM). Такая шутка позволяет не только удобно работать с FLASH, но и даёт определённые гарантии на целостность данных.
Требования:
- Обеспечение сохранности данных при прерывании процесса чтения\записи на любом этапе
- Простой интерфейс, который даст возможность записывать по 1, 2, 4 и N байт
▍ Давайте немного подумаем
Страницы FLASH памяти имеют относительно большой размер (в STM32F030 страница имеет размер 1024 байта) и в случае необходимости записи 1 ячейки нам нужно стереть эти 1024 байта.
Стоит сделать небольшое отступление. Требование к стиранию страницы перед записью актуально для FLASH памяти МК (для STM32F030 точно). На данный момент имеются внешние микросхемы FLASH памяти, которые умеют выполнять byte write operation.
«Ну и что тут такого? Давайте сохраним данные в буфер RAM, сотрём страницу и запишем данные, делов-то». Для начала взглянем на datasheet STM32F030 и увидим там прекрасную таблицу:

Нас интересует Page erase time — 30 ms, Карл! Этого времени достаточно, чтобы выдернуть питание, пока мы стираем страницу. В результате мы потеряем данные, т.к. мы их храним во временном буфере в RAM. К тому же, выделить буфер в RAM размером 1024 байта далеко не всегда можно.
Сохранность данных можно обеспечить работая с двумя страницами FLASH памяти, не копируя данные в RAM во время процедуры обновления. При записи мы копируем данные из одной страницы в другую, попутно меняя нужные ячейки. В итоге в одной странице хранятся актуальные данные, а в другой устаревшие. Такая процедура продолжается по кругу из одной страницы в другую при каждой операции записи. Всё же это не 100% решает проблему сохранности данных, т.к. при старте МК мы не сможем определить, какая из страниц содержит актуальные данные. Давайте думать дальше.
В нашем примере есть 2 страницы по 1024 байта, в этом случае размер VEEPROM будет равен 1014 байт: 10 байт мы забираем для хранения нужной драйверу информации: 2 байта под контрольную сумму и 8 байт для состояния страницы. Хранить эту информацию мы будем в конце страницы:

Состояний страницы может быть 5: ERASED, WRITE, VALID, COPY, INVALID:
- INVALID — страница содержит устаревшие данные
- COPY — страница содержит актуальные данные, но уже началась процедура копирования данных в другую страницу
- VALID — страница содержит актуальные данные
- WRITE — в страницу пишутся данные
- ERASED — страница пустая
Почему так много байт под состояние? FLASH память в STM32F030 позволяет записывать данные только по 16 бит и после записи мы не сможем повторно как-то изменить значение этой ячейки без стирания целой страницы. Нужна возможность менять статус страницы без стирания, иначе мы вернёмся в начало. Так вот каждая ячейка по 16 бит кодирует одно из состояний. Думаю будет лучше показать их значения:
#define PAGE_STATE_INVALID ((uint64_t)(0x0000000000000000))
#define PAGE_STATE_COPY ((uint64_t)(0x000000000000FFFF))
#define PAGE_STATE_VALID ((uint64_t)(0x00000000FFFFFFFF))
#define PAGE_STATE_WRITE ((uint64_t)(0x0000FFFFFFFFFFFF))
#define PAGE_STATE_ERASED ((uint64_t)(0xFFFFFFFFFFFFFFFF))
Понимаете да о чём я? Нерационально, согласен. Я пытался это оптимизировать, но в голову ничего не пришло. Очень удачно, что статусы влезают в uint64_t, и мы можем работать с ними в коде без танцев с бубном.
Если взглянуть на граф, то переход между состояниями сопровождается сбросом значения ячейки в 0x0000. Переходы по верхним стрелкам выполняются только 1 раз, когда VEEPROM ещё не был инициализирован или произошёл серьёзный сбой.

В итоге мы получаем следующий алгоритм работы:
- Инициализация
- Ищем страницу в состоянии VALID или COPY путём проверки состояния страницы. Состояние VALID имеет приоритет;
- Если нашли, то сохраняем её адрес для последующей работы и завершаем на этом инициализацию;
- Если не нашли, то берём любую страницу и стираем её. Этим мы автоматически переводим её в ERASED. В эту страницу и будут записываться новые данные. Запоминаем и завершаем инициализацию.
- Запись данных
- Стираем вторую страницу, которая в INVALID, ERASED или WRITTING (при первом запуске тут мы получаем 2 пустые страницы, то что нужно);
Текущее состояние страниц: [VALID] [ERASED] или [ERASED] [ERASED];
- Сбрасываем ячейку состояние первой страницы в 0х0000, переводя её в COPY. Состояние COPY нужно, чтобы избежать ситуации, при которой мы получим 2 страницы в состоянии VALID, при этом одна будет содержать более свежие данные. Нам нужна однозначность.
Текущее состояние страниц: [COPY] [ERASED];
- Сбрасываем ячейку второй страницы в 0x0000, переводя её в WRITE. Это состояние нужно, на случай пропадания питания во время записи данных во вторую страницу. Без этого состояния страница останется в состоянии ERASED в процессе записи. Пробегаться каждый раз по всей странице, чтобы убедиться в её пустоте не очень хорошее решение.
Текущее состояние страниц: [COPY] [WRITE]; - Копируем данные во вторую страницу и не забываем во время этого изменять требуемые ячейки на новые значения.
Текущее состояние страниц: [COPY] [WRITE]; - Сбрасываем ячейку второй страницы в 0x0000, переводя её в VALID. Вот тут у нас может получиться 2 страницы в VALID состоянии, если бы не было состояния COPY
Текущее состояние страниц: [COPY] [VALID]; - Сбрасываем ячейку второй страницы в 0x0000, переводя её в INVALID.
Текущее состояние страниц: [INVALID] [VALID];
- Стираем вторую страницу, которая в INVALID, ERASED или WRITTING (при первом запуске тут мы получаем 2 пустые страницы, то что нужно);
По такому алгоритму гарантируется оптимальная сохранность данных в случае потери питания в середине этого процесса. На любой стадии мы будем иметь валидные данные и возможность однозначно определить страницу с ними, а это нам и нужно. Выглядит страшно, но, к сожалению это единственный метод иметь надёжную часто изменяемую область памяти внутри МК. Таков путь!
Давайте я лучше покажу это на уже реализованном примере. Тут я специально уменьшил размер страницы VEEPROM до 256 байт, чтобы удобно было смотреть.
1. Инициализация. Обе страницы пустые

2. Началась процедура записи данных. Стираем вторую страницу от прошлых итераций и переводим первую в состояние COPY

3. Переводим вторую страницу в состояние WRITE

4. Пишем данные

5. Незабываем про checksum (считаем и пишем)

6. Переводим вторую страницу в состояние VALID (почему бы и нет, она же содержит актуальные данные)

7. Переводим первую страницу в состояние INVALID (данные там устарели)

8. Делаем вторую страницу активной. Профит!
▍ Разделяй и властвуй
Итак, алгоритм работы понятен, но какие же нам страницы FLASH памяти выбрать для размещения VEEPROM в них? В 95% случаев берут либо первые, либо последние (я предпочитаю последние). Хорошо, теперь нам нужны адреса страниц — окунёмся в документацию.
Большое «спасибо» STM за запихивание в один reference manual несколько МК, в итоге мне приходится тратить время на чтение великолепных сносок под таблицами. Ну сделайте вы эти таблицы отдельными, чтобы глазами глянуть и всё было сразу видно

Нас интересует сноска под таблицей. В нашем случае память заканчивается на адресе 0x08003FFF (16 страниц FLASH по 1024 байта). Берём две последние страницы для VEEPROM с адресами 0x08003800-0x08003BFF и 0x08003C00-0x08003FFF.
У нас есть адреса страниц и нужно их зарезервировать, т.е. нужно дать знать линкеру, что в эти страницы код программы записывать нельзя. Я использую IAR для разработки и там есть .icf файл, в котором определены области памяти для каждого МК. Добавляем туда новый регион и не забываем уменьшить размер ROM региона на 2048 байт:
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__ = 0x080037FF;
define symbol __ICFEDIT_region_VEEPROM_start__ = 0x08003800;
define symbol __ICFEDIT_region_VEEPROM_end__ = 0x08003FFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x20000FFF;
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__ ];
define region VEEPROM_region = mem:[from __ICFEDIT_region_VEEPROM_start__ to __ICFEDIT_region_VEEPROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__ ];
В целом можно просто уменьшить ROM, но лучше создать отдельный именованный регион, чтобы потом быстро вспомнить распределение FLASH памяти.
▍ Глянем реализацию
Начнём с лица — интерфейс:
// ***************************************************************************
/// @brief VEEPROM driver initializetion
/// @return true - init success, false - fail
// ***************************************************************************
extern bool veeprom_init();
// ***************************************************************************
/// @brief Mass erase VEEPROM
/// @return true - init success, false - fail
// ***************************************************************************
extern bool veeprom_mass_erase();
// ***************************************************************************
/// @brief Read data from VEEPROM
/// @param [in] veeprom_addr: virtual address [0x0000...size-1]
/// @param [out] buffer: pointer to buffer for data
/// @param [in] bytes_count: bytes count for read
/// @return true - init success, false - fail
// ***************************************************************************
extern bool veeprom_read(uint32_t veeprom_addr, uint8_t* buffer, uint32_t bytes_count);
extern uint8_t veeprom_read_8(uint32_t veeprom_addr);
extern uint16_t veeprom_read_16(uint32_t veeprom_addr);
extern uint32_t veeprom_read_32(uint32_t veeprom_addr);
// ***************************************************************************
/// @brief Write data to VEEPROM
/// @param [in] veeprom_addr: virtual address [0x0000...size-1]
/// @param [out] data: pointer to data for write
/// @param [in] bytes_count: bytes count for write
/// @return true - init success, false - fail
// ***************************************************************************
extern bool veeprom_write(uint32_t veeprom_addr, uint8_t* data, uint32_t bytes_count);
extern bool veeprom_write_8(uint32_t veeprom_addr, uint8_t value);
extern bool veeprom_write_16(uint32_t veeprom_addr, uint16_t value);
extern bool veeprom_write_32(uint32_t veeprom_addr, uint32_t value);
Прекрасно, не правда ли? Всё просто и без лишних параметров, адреса тут относительные [0x0000… SIZE — 1]. И ведь не подумаешь, что за таким интерфейсом скрываются ужасы работы с FLASH памятью.
Глянем реализацию. Весь замес происходит в функциях veeprom_write и veeprom_init, остальные функции либо обёртки, либо функции для сокращения кода.
Код. Много кода
// ***************************************************************************
/// @file veeprom.c
/// @author NeoProg
// ***************************************************************************
#include "veeprom.h"
#include "project_base.h"
#define FLASH_PAGE_SIZE (1024)
#define VEEPROM_SERVICE_HEADER_SIZE (10)
#define VEEPROM_PAGE_1_ADDR (0x08003800)
#define VEEPROM_PAGE_2_ADDR (0x08003C00)
#define VEEPROM_PAGE_SIZE (FLASH_PAGE_SIZE - VEEPROM_SERVICE_HEADER_SIZE)
#define PAGE_CHECKSUM_OFFSET (VEEPROM_PAGE_SIZE)
#define PAGE_STATE_OFFSET (VEEPROM_PAGE_SIZE + 2)
#define PAGE_STATE_INVALID ((uint64_t)(0x0000000000000000))
#define PAGE_STATE_COPY ((uint64_t)(0x000000000000FFFF))
#define PAGE_STATE_VALID ((uint64_t)(0x00000000FFFFFFFF))
#define PAGE_STATE_WRITE ((uint64_t)(0x0000FFFFFFFFFFFF))
#define PAGE_STATE_ERASED ((uint64_t)(0xFFFFFFFFFFFFFFFF))
static uint32_t active_page_addr = 0;
static uint32_t inactive_page_addr = 0;
static bool flash_lock();
static bool flash_unlock();
static bool flash_wait_and_check();
static bool flash_page_erase(uint32_t flash_addr);
static uint64_t flash_page_get_state(uint32_t flash_addr);
static bool flash_page_set_state(uint32_t flash_addr, uint64_t state);
static uint16_t flash_page_calc_checksum(uint32_t flash_addr);
static uint16_t flash_page_read_checksum(uint32_t flash_addr);
static bool flash_page_write_checksum(uint32_t flash_addr, uint16_t checksum);
static uint8_t flash_read_8(uint32_t flash_addr);
static uint16_t flash_read_16(uint32_t flash_addr);
static uint32_t flash_read_32(uint32_t flash_addr);
static bool flash_write_16(uint32_t flash_addr, uint16_t value);
// ***************************************************************************
/// @brief VEEPROM driver initializetion
/// @return true - init success, false - fail
// ***************************************************************************
bool veeprom_init() {
// Search active page
uint64_t page1_state = flash_page_get_state(VEEPROM_PAGE_1_ADDR);
uint64_t page2_state = flash_page_get_state(VEEPROM_PAGE_2_ADDR);
if (page1_state == PAGE_STATE_VALID) {
active_page_addr = VEEPROM_PAGE_1_ADDR;
inactive_page_addr = VEEPROM_PAGE_2_ADDR;
}
else if (page2_state == PAGE_STATE_VALID) {
active_page_addr = VEEPROM_PAGE_2_ADDR;
inactive_page_addr = VEEPROM_PAGE_1_ADDR;
}
else if (page1_state == PAGE_STATE_COPY) {
active_page_addr = VEEPROM_PAGE_1_ADDR;
inactive_page_addr = VEEPROM_PAGE_2_ADDR;
}
else if (page2_state == PAGE_STATE_COPY) {
active_page_addr = VEEPROM_PAGE_2_ADDR;
inactive_page_addr = VEEPROM_PAGE_1_ADDR;
}
else {
if (!flash_page_erase(VEEPROM_PAGE_1_ADDR)) {
return false;
}
active_page_addr = VEEPROM_PAGE_1_ADDR;
inactive_page_addr = VEEPROM_PAGE_2_ADDR;
return true;
}
// Check checksum
return flash_page_read_checksum(active_page_addr) != flash_page_calc_checksum(active_page_addr);
}
// ***************************************************************************
/// @brief Mass erase VEEPROM
/// @return true - init success, false - fail
// ***************************************************************************
bool veeprom_mass_erase() {
return flash_page_erase(VEEPROM_PAGE_1_ADDR) && flash_page_erase(VEEPROM_PAGE_2_ADDR);
}
// ***************************************************************************
/// @brief Read data from VEEPROM
/// @param [in] veeprom_addr: virtual address [0x0000...size-1]
/// @param [out] buffer: pointer to buffer for data
/// @param [in] bytes_count: bytes count for read
/// @return true - init success, false - fail
// ***************************************************************************
bool veeprom_read(uint32_t veeprom_addr, uint8_t* buffer, uint32_t bytes_count) {
if (veeprom_addr + bytes_count >= VEEPROM_PAGE_SIZE || !active_page_addr) {
return false;
}
while (bytes_count) {
*buffer = flash_read_8(active_page_addr + veeprom_addr);
++buffer;
--bytes_count;
}
return true;
}
uint8_t veeprom_read_8(uint32_t veeprom_addr) {
uint8_t data = 0;
veeprom_read(veeprom_addr, &data, sizeof(data));
return data;
}
uint16_t veeprom_read_16(uint32_t veeprom_addr) {
uint16_t data = 0;
veeprom_read(veeprom_addr, (uint8_t*)&data, sizeof(data));
return data;
}
uint32_t veeprom_read_32(uint32_t veeprom_addr) {
uint32_t data = 0;
veeprom_read(veeprom_addr, (uint8_t*)&data, sizeof(data));
return data;
}
// ***************************************************************************
/// @brief Write data to VEEPROM
/// @param [in] veeprom_addr: virtual address [0x0000...size-1]
/// @param [out] data: pointer to data for write
/// @param [in] bytes_count: bytes count for write
/// @return true - init success, false - fail
// ***************************************************************************
bool veeprom_write(uint32_t veeprom_addr, uint8_t* data, uint32_t bytes_count) {
// Erase inactive page (set ERASED state)
if (!flash_page_erase(inactive_page_addr)) {
return false;
}
flash_unlock();
// Set COPY state for active page
if (!flash_page_set_state(active_page_addr, PAGE_STATE_COPY)) {
flash_lock();
return false;
}
// Set WRITE state for inactive page
if (!flash_page_set_state(inactive_page_addr, PAGE_STATE_WRITE)) {
flash_lock();
return false;
}
// Copy data from active page into inactive with change data
for (uint32_t offset = 0; offset < VEEPROM_PAGE_SIZE; /* NONE */) {
uint8_t byte[2] = {0};
for (uint32_t i = 0; i < 2; ++i) {
if (offset >= veeprom_addr && offset < veeprom_addr + bytes_count) {
byte[i] = *data;
++data;
} else {
byte[i] = flash_read_8(active_page_addr + offset);
}
++offset;
}
uint16_t word = ((byte[0] << 8) & 0xFF00) | byte[1];
if (word != flash_read_16(inactive_page_addr + offset - 2)) {
// Write data
if (!flash_write_16(inactive_page_addr + offset - 2, word)) {
flash_lock();
return false;
}
}
}
// Calc checksum for inactive page
uint16_t checksum = flash_page_calc_checksum(inactive_page_addr);
if (!flash_page_write_checksum(inactive_page_addr, checksum)) {
flash_lock();
return false;
}
// Set VALID state for inactive page
if (!flash_page_set_state(inactive_page_addr, PAGE_STATE_VALID)) {
flash_lock();
return false;
}
// Set INVALID state for active page
if (!flash_page_set_state(active_page_addr, PAGE_STATE_INVALID)) {
flash_lock();
return false;
}
// Swap pages
uint32_t tmp = inactive_page_addr;
inactive_page_addr = active_page_addr;
active_page_addr = tmp;
flash_lock();
return true;
}
bool veeprom_write_8(uint32_t veeprom_addr, uint8_t value) {
return veeprom_write(veeprom_addr, &value, 1);
}
bool veeprom_write_16(uint32_t veeprom_addr, uint16_t value) {
return veeprom_write(veeprom_addr, (uint8_t*)&value, 2);
}
bool veeprom_write_32(uint32_t veeprom_addr, uint32_t value) {
return veeprom_write(veeprom_addr, (uint8_t*)&value, 4);
}
// ***************************************************************************
/// @brief Lock/unlock FLASH
/// @return true - init success, false - fail
// ***************************************************************************
static bool flash_lock() {
FLASH->CR |= FLASH_CR_LOCK;
return (FLASH->CR & FLASH_CR_LOCK) == FLASH_CR_LOCK;
}
static bool flash_unlock() {
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
}
return (FLASH->CR & FLASH_CR_LOCK) != FLASH_CR_LOCK;
}
// ***************************************************************************
/// @brief Wait FLASH operation complete
/// @return true - operation comleted, false - operation comleted with error
// ***************************************************************************
static bool flash_wait_and_check() {
while (FLASH->SR & FLASH_SR_BSY);
if (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) {
FLASH->SR |= FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP;
return false;
}
FLASH->SR |= FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP;
return true;
}
// ***************************************************************************
/// @brief Erase FLASH page
/// @param [in] flash_addr: page address for erase
/// @return true - success, false - fail
// ***************************************************************************
static bool flash_page_erase(uint32_t flash_addr) {
flash_unlock();
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = flash_addr;
FLASH->CR |= FLASH_CR_STRT;
bool result = flash_wait_and_check();
FLASH->CR &= ~FLASH_CR_PER;
flash_lock();
return result;
}
// ***************************************************************************
/// @brief Get/set FLASH page state
/// @param [in] flash_addr: page address
/// @param [in] state: new page state (for flash_page_set_state)
/// @return true - success, false - fail
// ***************************************************************************
static uint64_t flash_page_get_state(uint32_t flash_addr) {
uint64_t state = 0;
for (uint8_t i = 0; i < 4; ++i) {
state = (state << 16) | flash_read_16(flash_addr + PAGE_STATE_OFFSET + i * 2);
}
return state;
}
static bool flash_page_set_state(uint32_t flash_addr, uint64_t state) {
uint64_t mask = 0xFFFF000000000000;
for (uint8_t i = 0; i < 4; ++i) {
if (state & mask) {
if (flash_read_16(flash_addr + PAGE_STATE_OFFSET + i * 2) != 0xFFFF) {
return false;
}
continue;
}
if (!flash_write_16(flash_addr + PAGE_STATE_OFFSET + i * 2, 0x0000)) {
return false;
}
mask >>= 16;
}
return true;
}
// ***************************************************************************
/// @brief Calc/read/write checksum
/// @param [in] flash_addr: page address
/// @param [in] checksum: new page checksum (for flash_page_write_checksum)
/// @return true - success, false - fail
// ***************************************************************************
static uint16_t flash_page_calc_checksum(uint32_t flash_addr) {
uint32_t bytes_count = VEEPROM_PAGE_SIZE;
uint16_t checksum = 0;
while (bytes_count) {
checksum += flash_read_8(flash_addr);
++flash_addr;
--bytes_count;
}
return checksum;
}
static uint16_t flash_page_read_checksum(uint32_t flash_addr) {
return flash_read_16(flash_addr + PAGE_CHECKSUM_OFFSET);
}
static bool flash_page_write_checksum(uint32_t flash_addr, uint16_t checksum) {
return flash_write_16(flash_addr + PAGE_CHECKSUM_OFFSET, checksum);
}
// ***************************************************************************
/// @brief Read data from FLASH in BE format
/// @param [in] flash_addr: page address
/// @param [in] state: new page state (for flash_page_set_state)
/// @return cell value
// ***************************************************************************
static uint8_t flash_read_8(uint32_t flash_addr) {
return *((uint8_t*)flash_addr);
}
static uint16_t flash_read_16(uint32_t flash_addr) {
return __REV16(*((uint16_t*)flash_addr));
}
static uint32_t flash_read_32(uint32_t flash_addr) {
return __REV(*((uint32_t*)flash_addr));
}
// ***************************************************************************
/// @brief Write word to FLASH in LE format
/// @param [in] flash_addr: page address
/// @param [in] value: new cell value
/// @return true - success, false - fail
// ***************************************************************************
static bool flash_write_16(uint32_t flash_addr, uint16_t value) {
FLASH->CR |= FLASH_CR_PG;
*((uint16_t*)flash_addr) = __REV16(value);
bool result = flash_wait_and_check();
FLASH->CR &= ~FLASH_CR_PG;
if (flash_read_16(flash_addr) != value) {
return false;
}
return result;
}
Есть крайне важное замечание, которое стоит сказать. В даташите на МК сказано следующее:

Если своими словами, то МК не может обращаться к FLASH памяти, пока мы с ней что-то делаем (стираем, пишем), в том числе и не может читать инструкции для выполнения. Т.е. по сути МК залипает. Для решения этой проблемы нужно использовать __ramfunc — функции, инструкции которых хранятся в RAM, а не в FLASH и в этом случае программа будет продолжать работать. При использовании VEEPROM рекомендуется скопировать всю таблицу прерываний и все критичные обработчики в RAM. И не забываем про функции, которые вызываются из этих обработчиков. Не должно быть никакого обращения к FLASH, это важно. В нашем случае я не стал так глубоко закапываться и __ramfunc опустил.
▍ Итоги
Какие из этого можно сделать выводы? VEEPROM требователен к ресурсам (2 физических страниц на 1 виртуальную) и на все эти танцы вокруг страниц создают кучу процессорных инструкций.
Производительность у него тоже не очень. Поэтому лучше всего писать данные большими пачками, иначе на каждый байт будут меняться страницы, а это 30мс + время на копирование данных из страницы в страницу. Неплохо так, да? Хуже некуда.
Тем не менее, когда целостность данных важна он их сохранит. Надеюсь, кому-то это поможет и натолкнёт на какую-нибудь мысль. Спасибо за внимание.
