Many developers who are moving to STM32 from smaller microcontroller families expect to find a real internal EEPROM block that they can use to save user preferences, calibration data, configuration settings, or counters.
That special EEPROM doesn’t exist on a lot of STM32 devices. Instead, internal flash memory is usually used for non-volatile storage. This makes it hard to design because flash memory is written to differently than EEPROM.
EEPROM is made for frequent updates at the byte level. Flash memory, on the other hand, is organised into larger erasable units and has different limits on how long it can last and how many times it can be written to.
To fix this, STM32 projects often use EEPROM emulation, which is a software technique that makes flash memory act like a small, permanent data store.
To make strong embedded applications that can keep data even after resets and power cycles, you need to know how EEPROM emulation works.
What EEPROM Emulation Means on STM32
EEPROM emulation is a software-managed method of storing non-volatile values inside internal flash. Instead of writing individual bytes directly as you would with a dedicated EEPROM peripheral, the firmware reserves one or more pages of flash memory and uses those pages to hold variables in a structured format.
The software keeps track of what has been written, which values are current, and when a page needs to be cleaned or recycled. In effect, the application sees a persistent key-value storage system, even though the underlying medium is ordinary flash.
This emulation layer is necessary because flash memory has important limitations. A flash cell normally cannot be rewritten directly from one arbitrary value to another. Bits can generally move from a programmed state toward zero during a write operation, but returning them to one requires erasing an entire flash page or sector. T
hat means frequent updates to individual values cannot be done the same way as EEPROM unless software is carefully managing the storage format.
EEPROM emulation bridges that gap by appending updates, tracking the latest valid entry for each variable, and periodically reclaiming space when needed.
Why STM32 Developers Use EEPROM Emulation
Embedded systems often have persistent storage. A device might need to remember its serial number, operating mode, calibration coefficients, Wi-Fi credentials, thresholds set by the user, runtime counters, or maintenance data.
These values need to stay the same after resets and power outages, but they don’t change very often, so a small flash-based persistence mechanism works.
When you don’t want to add external memory parts, EEPROM emulation is very helpful. Using internal flash makes hardware simpler, cheaper, and doesn’t take up extra board space.
It also lets a lot of products store small amounts of important data without having to add I2C EEPROM or SPI flash devices. The trade-off is that the firmware has to be careful with flash erase cycles and stay within the limits of flash endurance.
A good EEPROM emulation design finds the right balance between simplicity, data integrity, and wear management.
The Difference Between Flash and EEPROM
Knowing why flash is not the same as EEPROM can help you understand EEPROM emulation better. EEPROM is made for writing small values over and over again, and it usually lets you write small amounts of data, often one byte at a time. In contrast, flash memory is usually erased one page or sector at a time.
Even though the programming operation can write smaller units like words or double words, erasing needs a much bigger block operation.
This changes how firmware has to work. If you want to change one variable in flash, you usually can’t just delete that variable.
You need to keep all the other information on the page, delete the whole page, and then write what needs to stay. That would be a waste of time and would quickly wear out the flash if done without thinking. EEPROM emulation gets around that by using a log structure that adds things to the end.
When a variable changes, flash adds a new record. The old record stays where it is, but it is no longer seen as current. When the page is full, valid records are copied to a different place, and the old page is erased so it can be used again.
This method makes flash act more like a permanent journal than a byte array that can be accessed at random.
Basic EEPROM Emulation Concept
A simple EEPROM emulation scheme on STM32 usually reserves one or two flash pages. Each stored variable is represented by a small record containing some form of identifier and value. When the application wants to write a variable, it appends a new record into the active flash page.
When the application wants to read a variable, it scans the records and finds the most recent valid entry for that identifier. This means writes are straightforward, but reads may require searching.
A single-page scheme is the simplest but has limitations. Once the page is full, you must erase it, which risks data loss unless you first preserve all active values elsewhere. A two-page scheme is more robust. One page is active and receives new writes.
When it fills, the firmware transfers the latest valid entries to the second page, marks the second page as active, and erases the old one. This is often called page transfer. It improves reliability and supports wear leveling better than rewriting a single page repeatedly.
Data Format for Emulated Variables
A common record format includes a virtual address and a data value. The virtual address identifies which logical variable the record belongs to, while the value contains the data itself.
Some designs also include status words, sequence counters, CRC fields, or validity markers. These additions improve fault tolerance, especially if power is lost during a write or page transfer.
For example, suppose you want to store three parameters: device mode, threshold, and calibration value. Each one can be assigned a virtual address such as 0x0001, 0x0002, and 0x0003. Each time one of them changes, the firmware writes a new record with that address and the latest value.
When the system starts, it scans the records and determines the latest valid entry for each address. That reconstructed state becomes the current configuration.
This approach gives flexibility because the physical storage location of a variable is no longer fixed. The latest record defines the current value, not a hardcoded flash address. That is one of the core ideas behind EEPROM emulation.
A Simple Bare-Metal Example Using a Reserved Flash Area
The following example shows a simplified approach for storing 16-bit values in flash using fixed records. This is not a full industrial-strength EEPROM emulation driver, but it demonstrates the essential idea of writing and reading logical variables from a reserved flash region.
#include "stm32f4xx.h"
#include <stdint.h>
#define EEPROM_START_ADDR 0x08060000U
#define EEPROM_PAGE_SIZE 0x4000U
typedef struct {
uint16_t virt_addr;
uint16_t data;
} EE_Record;
static uint32_t ee_find_free_address(void) {
uint32_t addr = EEPROM_START_ADDR;
while (addr < (EEPROM_START_ADDR + EEPROM_PAGE_SIZE)) {
EE_Record *rec = (EE_Record *)addr;
if (rec->virt_addr == 0xFFFF && rec->data == 0xFFFF) {
return addr;
}
addr += sizeof(EE_Record);
}
return 0;
}
static uint16_t ee_read_variable(uint16_t virt_addr, uint16_t *data) {
uint32_t addr = EEPROM_START_ADDR;
uint16_t found = 0;
while (addr < (EEPROM_START_ADDR + EEPROM_PAGE_SIZE)) {
EE_Record *rec = (EE_Record *)addr;
if (rec->virt_addr == virt_addr) {
*data = rec->data;
found = 1;
}
addr += sizeof(EE_Record);
}
return found;
}
static int ee_write_variable(uint16_t virt_addr, uint16_t data) {
uint32_t addr = ee_find_free_address();
if (addr == 0) {
return -1;
}
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
while (FLASH->SR & FLASH_SR_BSY) {
}
FLASH->CR |= FLASH_CR_PG;
*(volatile uint16_t *)addr = virt_addr;
while (FLASH->SR & FLASH_SR_BSY) {
}
*(volatile uint16_t *)(addr + 2) = data;
while (FLASH->SR & FLASH_SR_BSY) {
}
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
return 0;
}
This example scans a reserved flash page to find the next free slot, appends a new record, and reads the latest value of a given virtual address by scanning all records. It is intentionally simple so the storage model is easy to follow.
It works as a conceptual starting point, but it lacks page transfer, error recovery, and wear management. In real applications, those features become important.
Using the Simple Example
Here is how the simple functions could be used in an application to store and retrieve settings.
#include "stm32f4xx.h"
#include <stdint.h>
#define EE_MODE_ADDR 0x0001
#define EE_THRESHOLD_ADDR 0x0002
extern uint16_t ee_read_variable(uint16_t virt_addr, uint16_t *data);
extern int ee_write_variable(uint16_t virt_addr, uint16_t data);
int main(void) {
uint16_t mode = 0;
uint16_t threshold = 0;
ee_write_variable(EE_MODE_ADDR, 3);
ee_write_variable(EE_THRESHOLD_ADDR, 150);
if (ee_read_variable(EE_MODE_ADDR, &mode)) {
// mode now contains latest stored value
}
if (ee_read_variable(EE_THRESHOLD_ADDR, &threshold)) {
// threshold now contains latest stored value
}
while (1) {
}
}
This demonstrates the user-facing model. The application does not need to know where the latest value is physically located. It only refers to logical identifiers, and the EEPROM emulation layer reconstructs the most recent value.
Why a One-Page Approach Is Not Enough for Production
The simple example above is helpful for learning, but it is incomplete. Once the page fills, no more data can be written unless the page is erased. Erasing the page directly would destroy all stored values. To solve this properly, a production EEPROM emulation scheme usually uses at least two pages.
When the active page becomes full, the firmware copies the most recent valid values for all variables into the second page, marks the second page as the new active page, and then erases the old one. This preserves data while reclaiming space.
This two-page transfer mechanism also provides better resilience during power loss. If power fails while transferring, page status markers can help determine which page was active and whether recovery is possible.
Many vendor-provided EEPROM emulation libraries use this structure because it provides a practical balance of robustness and implementation complexity.
A Conceptual Two-Page Design
In a two-page EEPROM emulation design, each page has a header indicating its status, such as erased, receive-data, or valid-page. One page is considered the current valid page and receives appended records. When it becomes full, the system starts a transfer to the other page.
During the transfer, only the latest version of each logical variable is copied. After copying completes successfully, the new page becomes valid and the old page is erased.
This means the storage is self-compacting over time. Obsolete records are discarded during transfer, while current values are preserved. The system can thus continue handling updates over many erase cycles.
This method is much closer to true EEPROM behavior than a fixed-address flash write scheme.
Example Using HAL Flash APIs
Many STM32 developers prefer using HAL rather than direct register access. The next example shows a simplified write operation using HAL to store one 32-bit value at a known flash address. This is not full EEPROM emulation by itself, but it is a useful building block for understanding flash-based persistence.
#include "stm32f4xx_hal.h"
#include <stdint.h>
#define CONFIG_FLASH_ADDR 0x08060000U
static HAL_StatusTypeDef flash_write_word(uint32_t address, uint32_t data) {
HAL_StatusTypeDef status;
HAL_FLASH_Unlock();
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);
HAL_FLASH_Lock();
return status;
}
static uint32_t flash_read_word(uint32_t address) {
return *(volatile uint32_t *)address;
}
And here is an example of how it might be used:
#include "stm32f4xx_hal.h"
#include <stdint.h>
extern HAL_StatusTypeDef flash_write_word(uint32_t address, uint32_t data);
extern uint32_t flash_read_word(uint32_t address);
int main(void) {
HAL_Init();
uint32_t stored_value = 0;
flash_write_word(0x08060000U, 0x12345678U);
stored_value = flash_read_word(0x08060000U);
while (1) {
}
}
This example helps illustrate the raw mechanism of reading and writing flash, but it still does not solve the EEPROM problem on its own. If you repeatedly write to the same address, you will quickly run into flash programming rules and erase requirements. EEPROM emulation exists specifically to avoid that naive pattern.
Example of a Logical EEPROM Read and Write Interface
A cleaner application architecture is to create an EEPROM-style API that hides the flash details. The application then uses EEPROM_Write and EEPROM_Read style functions while the implementation handles flash internally.
#include <stdint.h>
#define EE_DEVICE_ID_ADDR 0x0001
#define EE_BAUDRATE_ADDR 0x0002
#define EE_CALIB_ADDR 0x0003
uint16_t EEPROM_Read(uint16_t virt_addr, uint16_t *value);
int EEPROM_Write(uint16_t virt_addr, uint16_t value);
void app_store_settings(void) {
EEPROM_Write(EE_DEVICE_ID_ADDR, 25);
EEPROM_Write(EE_BAUDRATE_ADDR, 96);
EEPROM_Write(EE_CALIB_ADDR, 412);
}
void app_load_settings(void) {
uint16_t value;
if (EEPROM_Read(EE_DEVICE_ID_ADDR, &value)) {
// use device ID
}
if (EEPROM_Read(EE_BAUDRATE_ADDR, &value)) {
// use baudrate parameter
}
if (EEPROM_Read(EE_CALIB_ADDR, &value)) {
// use calibration value
}
}
This interface is what most developers actually want. The application thinks in terms of persistent variables, not flash sectors and programming sequences. A well-designed emulation layer makes the rest of the firmware cleaner and safer.
Handling Flash Erase Operations
One of the most important aspects of EEPROM emulation is erase management. Flash pages or sectors can only endure a limited number of erase cycles.
The exact endurance depends on the STM32 family, but it is always finite. This means you should avoid designs that erase flash too often. If a variable changes every few milliseconds, flash-based EEPROM emulation is probably the wrong storage choice unless aggressive wear-leveling and logging are used.
For moderate-frequency updates such as configuration saves or calibration changes, flash-based emulation is often perfectly acceptable.
The key is to batch writes when possible and avoid rewriting values unnecessarily. If a setting has not changed, there is no reason to append a new record. If multiple settings change together, writing them as a group can also be more efficient.
Erase operations are also relatively slow compared with normal RAM access. In time-sensitive systems, this must be considered carefully. Some applications perform flash maintenance only at controlled times, such as during idle periods or user-initiated save operations.
Protecting Against Power Loss During Writes
Power loss during a flash write or page transfer is one of the biggest risks in EEPROM emulation. If the device loses power while a record is being written or while pages are being swapped, the storage state may become inconsistent.
Good EEPROM emulation designs include validity markers, page headers, sequence ordering, or checksums so the system can detect incomplete operations and recover gracefully after reboot.
For example, a record might be considered valid only if both its address and data fields are programmed correctly and possibly followed by a completion marker.
Similarly, a page transfer might use explicit states such as receiving and valid so the boot-time recovery logic can determine which page still contains the trustworthy data. These mechanisms are critical in systems where data integrity matters.
Using Vendor EEPROM Emulation Libraries
ST has provided EEPROM emulation application approaches for various STM32 families over the years. These libraries typically implement a two-page scheme with virtual addresses and page transfer management.
Using a proven library can save significant development time and reduce the chance of subtle bugs. It also gives you a more complete implementation than a quick custom flash log.
Even when using a vendor library, it is still worth understanding the underlying concept. That way you can tune the number of variables, choose flash pages appropriately, estimate endurance, and understand how recovery works after resets or power interruptions.
A library should simplify development, not replace architectural understanding.
When Not to Use EEPROM Emulation
Emulating EEPROM is very helpful, but it’s not always the best option. If the app needs to be updated very often, like when counters change all the time or data logs are written many times per second, the internal flash might wear out too quickly. In these situations, FRAM, external EEPROM, battery-backed RAM, or a file/log structure in larger external flash might be better options.
Also, if the data set is big, the internal flash set aside for emulation may not work as well. EEPROM emulation works best for small, important values that don’t change often. Knowing that boundary can help you avoid making bad design choices early on in a project.
Practical Design Advice
A good design for STM32 EEPROM emulation starts with a clear idea of what data needs to be stored. Some things don’t need storage that doesn’t go away. RAM is where temporary state should be. Persistent state should only include values that are important across power cycles.
Once you know that set, give each logical variable a stable virtual address and guess how often each one will be changed. After that, set aside the right amount of flash space and pick either a simple custom implementation for learning or a more complex two-page approach for production.
It’s also a good idea to keep the configuration storage separate from the code and any bootloader area in flash. This lowers the risk of accidentally overwriting data during firmware updates and makes it easier to keep the memory map up to date.
Finally, test in real-world situations, such as writing multiple times, turning the power off and on again, and stopping the save process on purpose.
It seems easy to emulate EEPROM until you add fault conditions, which is why thorough testing is important.

