Кто хотел сохранять какие-либо данные в 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 и увидим там прекрасную таблицу:
Flash memory characteristics

Нас интересует 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 ещё не был инициализирован или произошёл серьёзный сбой.
Граф состояний

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

По такому алгоритму гарантируется оптимальная сохранность данных в случае потери питания в середине этого процесса. На любой стадии мы будем иметь валидные данные и возможность однозначно определить страницу с ними, а это нам и нужно. Выглядит страшно, но, к сожалению это единственный метод иметь надёжную часто изменяемую область памяти внутри МК. Таков путь!

Давайте я лучше покажу это на уже реализованном примере. Тут я специально уменьшил размер страницы 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мс + время на копирование данных из страницы в страницу. Неплохо так, да? Хуже некуда.

Тем не менее, когда целостность данных важна он их сохранит. Надеюсь, кому-то это поможет и натолкнёт на какую-нибудь мысль. Спасибо за внимание.

Комментарии (28)


  1. GennPen
    29.09.2021 12:54
    +3

    Основная проблема использования Flash как EEPROM - это относительно малое кол-во циклов перезаписи. У F0 это около 10тыс циклов. Если за этим не следить - ресурс может очень быстро улететь.

    Лучше уж EEPROM развести дополнительно. Либо FRAM у которой кол-во циклов почти неограничено.


    1. Neoprog Автор
      29.09.2021 13:00

      Согласен. Внешняя микросхема всегда предпочтительнее в этом случае. Но всё же бывают случаи, когда нет места для нее. Я сталкивался с подобной ситуацией, когда делал миниатюрное устройство, там каждый мм был на счету. Решал задачу логгирования данных.

      Да и к тому же использование 2 страниц увеличивает количество циклов, но в итоге приведет к смерти сразу 2х страниц. Перспективы печальны.


      1. Nmilkin
        01.10.2021 20:06
        +1

        Опыт подсказывает что под EEPROM в корпусе SOT23-5 место в 99% случаев можно найти, единственное когда требуеться такое решение это если устройство уже готово и как обычно вдруг потребовался EEPROM.


    1. mctMaks
      29.09.2021 14:00
      +2

      Если все же места нет, либо девайс уже готов, то можно выделить чуть больше страниц и сделать аналог выравнивания износа. Или подключить littleFS (выравние износа из коробки) и использовать часть внутренней памяти как "внешний" носитель. Примеры вроде даже попадались с таким.

      у тех же нордиков есть FDS (flash data storage), идея та же что и у вас.


      1. Neoprog Автор
        29.09.2021 14:04

        выделить чуть больше страниц и сделать аналог выравнивания износа

        Конечно, почему бы и нет. Всё зависит от того, на сколько толстый FLASH и сколько объема можно пожертвовать.

        Я не стал усложнять текущий пример, хотел сделать его максимально простым. Выравнивание износа можно прикрутить как надстройку для текущего драйвера. Сделать это достаточно просто.


  1. VelocidadAbsurda
    29.09.2021 12:58

    Невозможность «дозаписи» в ранее записанное 16 бит слово - 100%? Дозаписывать нули в отдельные биты (и уместить состояние в одно слово) данный flash не позволяет?


    1. Neoprog Автор
      29.09.2021 13:03
      +2

      Нет. Проверено экспериментально и в Reference Manual об этом сказано.

      The Flash memory interface preliminarily reads the value at the addressed main Flash memory location and checks that it has been erased. If not, the program operation is skipped and a warning is issued by the PGERR bit in FLASH_SR register. The only exception to this is when 0x0000 is programmed. In this case, the location is correctly programmed to 0x0000 and the PGERR bit is not set.

      Изначально я так и хотел -- обойтись словом. Я вот прям помню, что встречал где-то FLASH с возможностью сбросов отдельных битов, но не в этом случае


      1. VelocidadAbsurda
        29.09.2021 16:33
        +1

        Да вот как раз потому и удивило - для flash дозапись битов (только в одну сторону - 1->0) вообще норма и «сама получается». А тут в явном виде добавили проверку зачем-то. Причём, судя по комментариям ниже о F103, проверка эта есть не во всех семействах. Ну будем знать.


  1. Gengenid
    29.09.2021 13:31
    +4

    1. Neoprog Автор
      29.09.2021 13:41

      Самый сок в том, что я только сейчас о нем узнал :) В целом подходы похожи (а есть ли другие?). Мне нужно больше времени разобраться в нем, возможно получится улучшить мой вариант

      Но что-то мне подсказывает, что 3 состояний будет недостаточно. Я пытался уложиться в меньшее количество и в некоторых моментах возникали неоднозначности -- поэтому у меня их больше


      1. INSTE
        29.09.2021 14:45

        Основной вопрос думаю в принципиальной ненадежности flash. Чтобы с ней уверенно работать лучше сделать wear levelling (страниц эдак 20-30 и писать в следующую циклично) + неплохо бы прям из flash-драйвера понимать что страница перешла в состояние «bad» и никогда не читать/не писать из нее, «обходить».


        1. Neoprog Автор
          29.09.2021 14:51

          Под состоянием bad имеется ввиду, когда страницу дотерли до дыр (циклы записи исчерпаны)?

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


          1. mctMaks
            29.09.2021 15:06

            Под состоянием bad имеется ввиду, когда страницу дотерли до дыр (циклы записи исчерпаны)?

            кстати, а может кто прояснить, правильно ли я понял: по исчерпанию лимита (допустим 10к) происходит примерно следующие:

            1. доступ к странице становиться дольше (как по чтению, так и по записи);

            2. не гарантируется что записанное значение сохранит свое значение в течении гарантированного времени (для флешей 5-10 лет).

            что-то ещё может быть?


            1. INSTE
              29.09.2021 15:32

              Она перестает стираться и записываться. У всех flash это происходит по-своему конечно, но обычно выглядит как «драйвер возвращает ошибку при записи/стирании» (если он умеет понимать что произошел bad) или «считанные данные после стирания и записи не меняются» (это обычно при тупом драйвере происходит).


          1. INSTE
            29.09.2021 15:35

            Предметно-специфичное хранилище в этом плане имеет ощутимое преимущество в том, что можно выделить т. н. writesize и разделить страницы на блоки размером «writesize + заголовок», и писать в ту же страницу кратно этому блоку.
            Кстати состояние VALID у вас содержит 16 бит, и можно эти 16 бит использовать как указатель того, сколько блоков занято. Делим страницу в 4к на 16 блоков, получаем 256 байт. Итого если в позиции «VALID» стоит один ноль — занят первый блок, два — занято два блока. Все 16 — вся страница занята. И обновление всегда идет от 1 к 0.
            А внутри блока уже хранить CRC и реальный размер занятого в заголовке.


          1. idubrov
            29.09.2021 16:06

            В апноуте выше, насколько я помню, предлагается дописывать на страницу. Идея в том, чтобы писать новые данные на ту же страницу, а при чтении брать последнюю "версию".

            Я по этому апноуту делал реализацию на Rust (https://github.com/idubrov/eeprom). Я правда, с потерей питания особо не заморачивался -- при записи, если кончилось место, была вероятность потерять "новые" данные пока мы копируем страницу.

            Почему-то мне тоже казалось, что можно биты с 1 на 0 по одному менять, но в моей реализации я вообще всего два состояния использую: активная страница и пустая.

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


            1. idubrov
              29.09.2021 16:19
              +1

              >Почему-то мне тоже казалось, что можно биты с 1 на 0 по одному менять, но в моей реализации я вообще всего два состояния использую: активная страница и пустая.

              А, вот почему. Они в своей реализации сами используют это:

              https://documentation.help/AN2594-EEPROM/eeprom_8h-source.html#l00057

              Состояние страницы переходит из 0xffff (пустая) => 0xeeee (копируем на эту страницу) => 0x0000 (активная).

              Так что попробуйте одно и то же слово несколько раз программировать (но важно что новое значение может только сбрасывать биты 1 в 0, но не наоборот), может, получится ???? (P.S. упс, я вижу, что вы написали, что уже пробовали...).

              P.P.S. Хм, если буквально читать, что они пишут, то 0x0000 они всегда разрешают записать? Может, три состояния всё-таки можно (0xffff => что-нибудь отличное от 0x0000 => 0x0000)?


            1. sim31r
              01.10.2021 01:08

              Rust для микроконтроллеров вообще очень интересная тема. Странно что популярность не приходит к языку в этой области.


              1. Neoprog Автор
                01.10.2021 01:12

                А зачем он там? С/С++ прекрасно справляются с задачами в этой области. Пока нет ощущения нехватки чего-либо, по крайней мере у меня. Это мое субъективное мнение.


                1. AnthonyMikh
                  01.10.2021 16:36
                  +1

                  Ну как минимум для того, чтобы на сложность предметной области не накладывались проблемы, присущие языку.


                  1. Neoprog Автор
                    01.10.2021 20:10

                    Например какие? Я чаще сталкивался с нехваткой функционала, чем с проблемами языка (не смог вспомнить с какими именно).


              1. idubrov
                01.10.2021 02:17
                -2

                Да, я в полном восторге от Rust на микроконтроллерах (STM32). Я от C++ уйти хотел ещё даже когда про Rust слыхом не слыхивал; C++ -- это всё-таки не тот язык на котором я бы хотел хобби-проект делать. Старый он, и кривой просто ужасно.

                В Rust https://rtic.rs/ понравилось (тогда оно ещё RTFM называлась). Вообще, идея языка где компилятор будет находить ошибки типа гонок состояний, в том числе и на микроконтроллерах, -- очень интересно. Даже если это и будет работать только для менее "требовательных" к производительности случаев.

                Но я так, сварщик не настоящий (на Rust три года коммерческого кода писал, но без микроконтроллеров -- REST сервисы).

                Профессиональная разработка для микроконтроллеров -- там, наверное, уже по-другому. Rust сырой, библиотек или нет или постоянно меняются, сертификации нет, опыта в индустрии -- тоже. Может быть, вот эти ребята https://oxide.computer/ что-то поменяют -- по-моему, они для микроконтроллеров тоже что-то делают.


  1. Sdima1357
    29.09.2021 19:31

    А еще можно писать лог транзакций прямо во флеш. Например я поддерживаю RAM в 1024 байта. Каждый доступ на запись пишу во флеш( "адрес, дата" или "адрес , дата, дата" ) и в RAM напрямую по адресу. При рестарте процессора раскручиваю записи в обратном порядке. Раз в какое то время( когда записей становится много) содежимое страницы сохраняется полностью , а записи сбрасываются. Можно прилично сэкономить ресурс флеша при некоторых паттернах доступа.


  1. PKav
    29.09.2021 21:20
    +1

    А можно ведь просто добавить конденсатор покрупнее и не сохранять информацию при уже отключенном питании и работе от конденсатора. Тогда либо информация будет записана корректно, либо не будет записана вообще.

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


    1. GennPen
      29.09.2021 23:17
      +1

      Если есть место под "конденсатор покрупнее", то значит есть место и под EEPROM.


      1. vvzvlad
        29.09.2021 23:53

        Иногда вопрос в цене, а не в свободном месте


    1. edo1h
      01.10.2021 17:37

      EEPROM не решает проблемы отключения питания посередине записи


  1. hhba
    05.10.2021 16:56

    Всё же это не 100% решает проблему сохранности данных, т.к. при старте МК мы не сможем определить, какая из страниц содержит актуальные данные

    Возможно я вас не понял, но разве постоянно растущий счетчик - не решение проблемы? Переписали все данные. на лету изменив нужную ячейку, и в последнюю ячейку записали увеличенное значение счетчика. При следующем старте определили, какая версия свежее - с ней и работаем. При этом заодно можно на лету считать CRC/XOR/Parity и писать туда же в конец.