FLASH Programming in STM32 Microcontrollers

Flash memory is the non-volatile memory where your program code and persistent data are stored.ย  It is a critical component of any microcontroller, including the STM32 family. STM32 microcontrollers, embed their own flash memory, tailored to meet deterministic execution, code retention, and in-application programming (IAP) needs.

Understanding how to properly program, erase, and manage flash memory is essential for developing robust STM32 applications, especially when you need to:

  • Storing configuration data that must survive power cycles.
  • Performing firmware updates in the field.
  • Implementing bootloaders for application upgrades.
  • Managing large datasets that do not fit in RAM.

In this blog post I will introduce the fundamentals of STM32 Flash memory programming โ€” from key concepts to hands-on implementation โ€” and is designed for beginners starting their journey with STM32 microcontrollers.

๐Ÿ’กย Note: I am taking STM32H573xx MCU for the reference of this blog post.

 

Flash Memory Architecture in STM32

Before diving into Flash programming, itโ€™s essential to understand the underlying memory architecture. STM32 devices organize Flash memory into sectors or pages, depending on the family, with specific alignment, access size, and erase requirements. This structure directly impacts how code is stored, how updates are applied, and how memory operations are managed during runtime.

A solid grasp of this architecture ensures safe, efficient, and reliable use of Flash โ€” whether you’re updating firmware, storing configuration data, or implementing a bootloader.

Key Characteristics of STM32H573xx Flash Memory:

  • Up to 2 MB Flash, split into two 1 MB banks
  • Flexible read sizes: 128-bit down to 8-bit
  • Flash Programming: 128-bit (main/user), 16-bit (OTP/high-cycle areas)
  • Erase options (Page/Sector granularity): 8 KB sectors, full bank, or dual-bank mass erase
  • Dual-bank support:
    • Read-while-write (not write-while-write)
    • Bank swapping with preserved security flags
  • ECC protection: 1-bit correction, 2-bit detection per 128-bit word
  • Configurable option bytes for Flash security and protection
  • Flash security features:
    • Debug access restriction
    • Write protection per sector group
    • Secure-only areas per bank
    • HDP (Hide Protection) for secure startup code
  • 2 KB OTP area + read-only area defined by ST
  • Instruction prefetch for faster execution.

 

๐Ÿ“– Flash Organization:

The Flash is organized into 512 sector of 8 KB each (256 sector per bank). This means:

  • Each sector is the minimum erasable unit.
  • You cannot erase part of a sector.
  • Programming is done in 128-bit aligned blocks โ€” anything else will fail.

Flash memory in STM32H573xx microcontrollers is typically organized as follows:

Component Description
Banks Flash is divided into 1 or 2 banks (e.g., 1 MB each in dual-bank devices).
Sectors / Pages Each bank is split into sectors or pages (granularity varies by series).
Erase Units Smallest erasable unit โ€” typically a sector (e.g., 8 KB on STM32H5).
Program Units Smallest programmable unit โ€” typically 128 bits (16 bytes) in user Flash.
Option Bytes Dedicated memory for boot config, protection settings, and other flags.
OTP Area One-Time Programmable region for storing permanent data like serial numbers.
Read-Only Area Reserved region set by STMicroelectronics, not user-writable.

 

Note: The granularity of memory access is the size of the data chunk (in bytes) that a processor or system accesses at once. Example, if a CPU has a 4-byte granularity, it reads or writes 4 bytes in one operationโ€”even if the program only needs 1 byte.

 

๐Ÿ”ง Basic Flash Operations:

Interacting with Flash memory in STM32 involves three key operations: reading, writing (programming), and erasing. While reading is simple and behaves like accessing normal memory, writing and erasing require specific sequences to ensure data integrity and prevent corruption.

โœ๏ธ Writing to Flash Memory โ€” The Sequence

Unlike reading, writing to Flash memory requires a specific sequence to ensure data integrity and prevent corruption. The general steps are:

  • Unlock the Flash control registers.
  • Erase the target sector (Flash must be erased before programming).
  • Program the data (in fixed-size units, depending on device).
  • Lock the Flash to protect against unintended writes.
  • Address must be aligned to 16 bytes,
  • Data must be 16 bytes in size and aligned.

 

๐Ÿ‘‰ STM32H5 flash requires 128-bit aligned writes because its internal program granularity is 16 bytes โ€” thatโ€™s the smallest unit the flash hardware can safely and efficiently program.

 

๐Ÿ”น HAL-Based Flash Write Example (128-bit aligned)

Below function is a safe and optimized routine for programming the internal Flash of the STM32H5 series, which requires 128-bit (16-byte) aligned writes.

Before using this function:

  • The target Flash region must be erased.
  • The power supply must be stable, and
  • Cache invalidation may be needed for fresh reads.

 

Following things happening in this function:

  • Unlocks the Flash controller,
  • Writes the buffer to Flash 16 bytes at a time using HAL_FLASH_Program(),
  • Performs read-back verification after each write, and
  • Locks the Flash when finished.

 

#include "stm32h5xx_hal.h"
#include <stdint.h>
#include <string.h>

#define FLASH_PROGRAM_UNIT           16  // 128 bits
#define IS_16BYTE_ALIGNED(x)         (((x) & 0xF) == 0)

/**
 * @brief Programs STM32H5 internal Flash using 128-bit (quad-word) aligned writes.
 * @note  The target Flash region must be erased before calling this.
 * @param address: Starting Flash address (must be 16-byte aligned)
 * @param buffer: Pointer to aligned data buffer
 * @param length: Total bytes to write (must be multiple of 16)
 * @retval HAL_StatusTypeDef
 */
HAL_StatusTypeDef flashProgram(uint32_t address, const uint8_t *buffer, uint32_t length)
{
    HAL_StatusTypeDef status = HAL_OK;

    // Validate alignment and size upfront
    if (!IS_16BYTE_ALIGNED(address) ||
        !IS_16BYTE_ALIGNED((uintptr_t)buffer) ||
        (length == 0) ||
        (length % FLASH_PROGRAM_UNIT) != 0)
    {
        return HAL_ERROR;
    }

    // Calculate programming loop bounds
    const uint32_t startUnit = address;
    const uint32_t endUnit   = address + length;

    // Unlock Flash memory
    status = HAL_FLASH_Unlock();
    if (status != HAL_OK)
        return status;

    __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);

    // Program loop: 16 bytes at a time
    for (uint32_t addr = startUnit; addr < endUnit; addr += FLASH_PROGRAM_UNIT)
    {
        const uint32_t *data = (const uint32_t *)(buffer + (addr - startUnit));

        status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, addr, (uint32_t *)data);
        if (status != HAL_OK)
            break;

        // Optional: read-back verification
        if (memcmp((void *)addr, data, FLASH_PROGRAM_UNIT) != 0)
        {
            status = HAL_ERROR;
            break;
        }
    }

    HAL_FLASH_Lock();
    return status;
}

 

๐Ÿ“– Reading Flash Memory:

Reading from Flash memory is straightforward โ€” it’s similar to reading from any memory-mapped location. Flash is addressable like normal memory, so standard pointer dereferencing can be used:

// Read a 32-bit value from a specific Flash address

uint32_t readValue = *(volatile uint32_t*)(FLASH_ADDRESS);

No special procedure is needed for reads, and there is no performance penalty under normal conditions, unless access latency is introduced due to wait states or if prefetch is disabled.

 

Note:ย A memory access boundary is the starting address of a memory chunk that aligns with the systemโ€™s access granularity โ€” the smallest unit of memory the system can read or write efficiently. Example, with 4-byte granularity, valid boundaries are addresses like 0x00, 0x04, 0x08, etc.

 

๐Ÿงน Erasing Flash:

Before writing new data, Flash memory must be erased. You can erase:

  • A single sector/page.
  • A full bank.
  • Or both banks (mass erase).

Typical erase procedure (using HAL):

FLASH_EraseInitTypeDef erase;
uint32_t pageError = 0;

// or FLASH_TYPEERASE_PAGES (on some families)
erase.TypeErase     = FLASH_TYPEERASE_SECTORS;

// Make sure this matches your target address   
erase.Banks         = FLASH_BANK_1; 

// Use sector number (STM32F4) or page number (STM32L4/H5)                 
erase.Sector        = sector_number;  

// Number of sectors/pages to erase               
erase.NbSectors     = 1;

// For 2.7Vโ€“3.6V supply range (check your hardware)
erase.VoltageRange  = FLASH_VOLTAGE_RANGE_3;

HAL_FLASH_Unlock();

if (HAL_FLASHEx_Erase(&erase, &pageError) != HAL_OK)
{
    // Handle error โ€” pageError contains the failing sector/page number
}

HAL_FLASH_Lock();

๐Ÿ“– Note: Before performing any write or erase operations, you must unlock the flash control register:

 

This function offers a flexible way to erase Flash memory on STM32H5 devices. By dynamically computing sector indices and bank associations, it ensures proper erasure across one or more banks, making it ideal for firmware update or data storage scenarios.

#include "stm32h5xx_hal.h"

#define FLASH_BASE_ADDRESS     0x08000000U
#define FLASH_SECTOR_SIZE      0x2000U         // 8 KB

 // Adjust based on your MCU (e.g., 2MB)
#define FLASH_TOTAL_SIZE       (2 * 1024 * 1024U)
#define FLASH_TOTAL_SECTORS    (FLASH_TOTAL_SIZE / FLASH_SECTOR_SIZE)
#define SECTORS_PER_BANK       (FLASH_TOTAL_SECTORS / 2)

/**
 * @brief  Calculate sector number from address.
 */
static uint32_t GetSector(uint32_t address) 
{
    return (address - FLASH_BASE_ADDRESS) / FLASH_SECTOR_SIZE;
}

/**
 * @brief  Calculate bank from sector number.
 */
static uint32_t GetBank(uint32_t sector) 
{
    return (sector < SECTORS_PER_BANK) ? FLASH_BANK_1 : FLASH_BANK_2;
}

/**
 * @brief  Erase flash sectors across banks if necessary.
 * @param  start_address: Starting address to erase from
 * @param  num_bytes: Number of bytes to erase
 * @retval HAL status
 */
HAL_StatusTypeDef Flash_Erase(uint32_t start_address, uint32_t num_bytes) 
{
    if ((start_address < FLASH_BASE_ADDRESS) ||
        (start_address + num_bytes > FLASH_BASE_ADDRESS + FLASH_TOTAL_SIZE)) 
  {
        return HAL_ERROR;  // Invalid address range
    }

    HAL_FLASH_Unlock();

    uint32_t start_sector = GetSector(start_address);
    uint32_t end_sector   = GetSector(start_address + num_bytes - 1);

    HAL_StatusTypeDef status = HAL_OK;
    FLASH_EraseInitTypeDef erase_init;
    uint32_t page_error;

    while (start_sector <= end_sector) 
  {
        uint32_t current_bank = GetBank(start_sector);
    
        uint32_t bank_limit = (current_bank == FLASH_BANK_1)
                            ? SECTORS_PER_BANK
                            : FLASH_TOTAL_SECTORS;

        uint32_t last_in_bank = (end_sector < bank_limit - 1) ? end_sector : (bank_limit - 1);	
        uint32_t sectors_to_erase = last_in_bank - start_sector + 1;

        erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
        erase_init.Page      = start_sector;
        erase_init.NbPages   = sectors_to_erase;
        erase_init.Banks     = current_bank;

        status = HAL_FLASHEx_Erase(&erase_init, &page_error);
        if (status != HAL_OK) 
    {
            break;
        }

        start_sector = last_in_bank + 1;
    }

    HAL_FLASH_Lock();
    return status;
}

 

๐Ÿ” Option Bytes & EEPROM-Like eData in STM32H5:

When working with configuration storage or persistent counters in embedded systems, developers often rely on EEPROM. STM32H5 offers the eData area โ€” a section of Flash that behaves like EEPROM when properly configured.

๐Ÿง  What is the eData Area?

STM32H5 devices allow you to reserve a part of Flash memory for non-volatile data storage โ€” essentially emulating EEPROM. This feature is especially useful for:

  • Device configuration parameters
  • Calibration data
  • Usage counters or logs
  • Any small, persistent values that need to survive resets or power loss

The memory behaves like EEPROM: you can write to it without erasing the full Flash page manually and read it easily using dedicated HAL APIs.

๐Ÿ”ง How Do You Enable It?

To use this feature, you first need to configure Option Bytes โ€” special, non-volatile settings in STM32 that determine things like protection modes, boot configuration, and now, eData support.

Hereโ€™s how to enable the eData area:

FLASH_OBProgramInitTypeDef ob;
ob.OptionType      = OPTIONBYTE_EDATA;
ob.EDATAConfig     = OB_EDATA_ENABLE;
ob.EDATAStartPage  = 240;     // Reserve last 4 pages of Flash
ob.EDATASize       = 4;

This configuration tells the microcontroller:

โ€œEnable the eData (EEPROM-like) area, starting from Flash page 240, and reserve 4 pages for it.โ€

Now apply it with the following code:

HAL_FLASH_OB_Unlock();                   // Unlock access to Option Bytes

HAL_FLASHEx_OBProgram(&ob);             // Program the eData settings

HAL_FLASH_OB_Launch();                  // Apply changes (this triggers a reset)

After the reset, your eData area is ready to use.

 

๐Ÿ’ก Reading and Writing to eData:

Once configured and enabled, you can use STM32โ€™s HAL APIs to treat this eData area like EEPROM:

// Write a 64-bit value to eData
HAL_FLASHEx_DATAEEPROM_Write(address, data);

// Read the value back
uint64_t data = HAL_FLASHEx_DATAEEPROM_Read(address);

โš ๏ธ Make sure the address falls within the eData region you reserved earlier. Otherwise, access might fail or corrupt memory.

 

๐Ÿ“Œ Things to Keep in Mind

  • Flash space is sacrificed: eData takes over a part of your Flash. If you reserve 4 pages, your code/data area is reduced accordingly.
  • Changes require a reset: When you launch the Option Bytes configuration (HAL_FLASH_OB_Launch()), the system resets to apply the new settings.
  • No need to erase manually: HAL handles write operations internally, without forcing you to do page erases like normal Flash operations.

 

โœ… Best Practices and Considerations for Flash Programming (STM32):

Working directly with Flash โ€” especially for configuration or EEPROM-like emulation โ€” demands caution and precision. Below are key practices every experienced firmware engineer should follow when dealing with Flash on STM32, especially when using eData.

 

1. ๐Ÿงฎ Memory Alignment:

Flash programming typically requires aligned access, depending on the Flash word size for the target STM32 series:

32-bit alignment: Common across STM32F0/F1/F3/L0 families

64-bit alignment: Required in STM32F7, H7, and H5 (for HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD))

Failing to align properly can cause HAL_FLASH_Program() to return errors or silently corrupt data.

โœ… Tip: Always align your write addresses to the word size required by the FLASH_TYPEPROGRAM_* mode you’re using.

 

2. ๐Ÿ”‹ Power Supply Stability

Flash writes and erases are sensitive to voltage drops. Unstable power โ€” especially in battery-powered systems โ€” can corrupt Flash content or trigger incomplete operations.

Example safeguard:

if (HAL_PWREx_GetVoltageRange() == FLASH_VOLTAGE_RANGE_3) 
{
    // Safe to program
} 
else 
{
    // Voltage too low โ€” delay, retry, or abort
}

๐Ÿ”ง Additional Considerations:

  • Ensure brown-out reset (BOR) is properly configured.
  • Use a low-dropout regulator (LDO) if supply margin is narrow.
  • Avoid programming during sleep modes unless properly configured.

 

3. ๐Ÿงท Wear Leveling:

Flash endurance is limited (typically 10,000โ€“100,000 cycles) per sector or page. To prevent premature wear in frequently written areas:

๐ŸŒ€ Simple Static Wear Leveling:

#define NUM_STORAGE_SLOTS 10
#define SLOT_SIZE         1024  // Must match Flash page size
#define CONFIG_FLASH_ADDR 0x080E0000  // Example start address

uint32_t FindNextFreeSlot(void) 
{
    for (int i = 0; i < NUM_STORAGE_SLOTS; i++) 
       {
         uint32_t addr = CONFIG_FLASH_ADDR + (i * SLOT_SIZE);
         if (*(uint32_t*)addr == 0xFFFFFFFF) 
           {
             return addr; // Unused slot
           }
       }
    return CONFIG_FLASH_ADDR; // All full, trigger erase
}

For dynamic data (e.g., counters, logs), implement circular buffer logic or use a file system layer (like LittleFS) on top of Flash for true wear leveling.

 

4. โ— Robust Error Handling

Never assume Flash operations will succeed. Always check return values from HAL APIs:

HAL_StatusTypeDef status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, data);
if (status != HAL_OK) 
{
    switch (status) 
     {
        case HAL_ERROR:
            // Likely voltage issue, alignment, or protection error
            break;
        case HAL_BUSY:
            // Possibly still erasing; retry after delay
            break;
        case HAL_TIMEOUT:
            // Could be clock config issue or stuck peripheral
            break;
        default:
            // Unknown error โ€” add logging/assert
            break;
    }
}

๐Ÿ’ก Pro Tip: Use __disable_irq() and __enable_irq() around write/erase operations to prevent ISR interruption, especially in latency-sensitive systems.

โš ๏ธ Other Considerations Seasoned Engineers Know

  • Flash Read-While-Write (RWW) is not always supported. If your MCU executes from the same bank you’re writing to, you may stall the CPU. Consider dual-bank mode if available.
  • Always unlock and lock Flash appropriately using HAL_FLASH_Unlock() and HAL_FLASH_Lock().
  • After any Flash write/erase, verify the written value using a read-back check โ€” donโ€™t trust blindly.
  • Monitor temperature, as Flash behavior can degrade or fail outside safe thermal limits (especially in automotive or industrial environments).

 

Additional Resources and Exercises:

 

Resources:

  • STM32 Reference Manuals: Check the specific manual for your STM32 device family
  • AN3969: EEPROM emulation techniques for STM32 microcontrollers

Exercises:

  • Basic Flash Writing: Create a program that stores a counter value in flash that increments each time the device boots up.
  • Configuration Manager: Implement a complete configuration system that allows storing and retrieving multiple parameters with different data types.
  • EEPROM Emulation: Use flash memory to emulate EEPROM behavior with proper wear leveling.
  • Firmware Update: Develop a simple bootloader that can receive a new application image over UART and program it into flash.
  • Data Logging: Create a circular buffer in flash memory that can store sensor readings even when power is lost.