What Are the Different Types of Memory Used in Embedded Systems

What are the different types of memory used in embedded systems

One of the first design reviews I ever sat in on as a junior engineer involved a senior developer rejecting an entire board layout because the designer had put frequently-written runtime data in flash memory instead of RAM. It seemed like a minor detail to me at the time, but that single mistake would have worn out the flash’s limited write-erase cycles within weeks of deployment. Memory selection in embedded systems isn’t just about capacity — it’s about matching each type of data to the memory technology whose properties (speed, persistence, endurance, cost) actually fit that data’s role. In this article I’ll walk through every major memory type used in embedded systems and how I decide what goes where.

The Embedded Memory Map

graph TD
    A[Embedded System Memory] --> B[Volatile Memory]
    A --> C[Non-Volatile Memory]
    B --> B1[SRAM]
    B --> B2[DRAM/SDRAM]
    C --> C1[Flash - NOR/NAND]
    C --> C2[EEPROM]
    C --> C3[ROM/OTP/Fuses]
    C --> C4[FRAM/MRAM]

Volatile Memory (Loses Data Without Power)

SRAM (Static RAM)

SRAM is the workhorse memory inside every microcontroller — it holds the stack, heap, global variables, and runtime state. It’s fast (single-cycle access in many MCUs) and doesn’t need refresh cycles, but it’s more expensive per bit than DRAM and typically limited to tens or hundreds of kilobytes on-chip.

/* SRAM usage is implicit in nearly all runtime C code */
uint8_t sensor_buffer[256];      /* lives in SRAM (.bss or .data) */
int main(void) {
    uint32_t local_counter = 0;  /* lives on the stack, in SRAM */
    ...
}

I keep a close eye on SRAM usage in linker map files, since exceeding it causes stack/heap collisions — one of the most common and hardest-to-debug embedded crashes.

DRAM/SDRAM (Dynamic RAM)

Used on higher-end embedded processors (Cortex-A running Linux, application processors driving displays or doing image processing) where megabytes of RAM are needed beyond what on-chip SRAM can provide. DRAM is cheaper per bit than SRAM but requires periodic refresh circuitry and has higher access latency, plus a dedicated memory controller.

graph LR
    A[CPU Core] --> B[On-chip SRAM - fast, small, no refresh]
    A --> C[External SDRAM via Memory Controller - large, needs refresh, higher latency]

Non-Volatile Memory (Retains Data Without Power)

Flash Memory (NOR and NAND)

Flash is where firmware itself lives. There are two main types with very different characteristics:

FeatureNOR FlashNAND Flash
Read accessRandom access, fast (like RAM)Sequential/page-based, slower random access
Execute-in-place (XIP)Yes — MCU can run code directly from itNo — must be copied to RAM first
Density/cost per bitLower density, higher costHigher density, lower cost
Typical useMCU program storageBulk data storage (SD cards, SSDs)
Erase granularitySector-based (KB-scale)Block-based (larger, page writes)

Nearly every microcontroller’s internal program flash is NOR-type, chosen specifically because it supports execute-in-place — the CPU fetches and runs instructions directly from flash addresses without first copying them to RAM.

/* Writing to flash requires erase-then-write, and only in defined sectors --
   this is fundamentally different from RAM, which can be
   overwritten byte-by-byte at any time */
#include "stm32f4xx_hal.h"

void Flash_WriteConfig(uint32_t address, uint32_t *data, uint32_t words)
{
    HAL_FLASH_Unlock();

    FLASH_EraseInitTypeDef eraseInit;
    eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
    eraseInit.Sector = FLASH_SECTOR_5;
    eraseInit.NbSectors = 1;
    eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;

    uint32_t sectorError;
    HAL_FLASHEx_Erase(&eraseInit, &sectorError); /* must erase before writing */

    for (uint32_t i = 0; i < words; i++) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address + (i * 4), data[i]);
    }

    HAL_FLASH_Lock();
}

Flash has a limited write/erase endurance — typically 10,000 to 100,000 cycles per sector. This is exactly the mistake I mentioned in the introduction: frequently-changing runtime data written to flash directly will wear it out. The standard mitigation is wear leveling — spreading writes across multiple sectors instead of repeatedly hitting the same one.

sequenceDiagram
    participant FW as Firmware
    participant WL as Wear-Leveling Layer
    participant Flash
    FW->>WL: Write config value
    WL->>WL: Check current write pointer/sector usage
    WL->>Flash: Write to least-used sector, mark old sector for erase
    Note over WL,Flash: Spreads wear evenly instead of hammering one sector

EEPROM (Electrically Erasable Programmable ROM)

EEPROM allows byte-level erase and rewrite (unlike flash’s sector-level erase), making it ideal for small amounts of frequently-changing configuration data — calibration values, device settings, small logs. It’s slower and more expensive per bit than flash, so it’s used for kilobytes, not megabytes, of data.

/* Many MCUs emulate EEPROM behavior using a reserved flash region
   plus a wear-leveling driver, since true EEPROM cells take more die area */
void EEPROM_WriteCalibration(uint16_t offset_value)
{
    HAL_FLASHEx_DATAEEPROM_Unlock();
    HAL_FLASHEx_DATAEEPROM_Program(FLASH_TYPEPROGRAM_HALFWORD,
                                     EEPROM_CAL_ADDR, offset_value);
    HAL_FLASHEx_DATAEEPROM_Lock();
}

ROM, OTP, and Fuses

Mask ROM is programmed at chip manufacture time and can never be changed — rare in modern designs except for boot ROM code. One-Time Programmable (OTP) memory and fuses can be written exactly once in the field (or at manufacturing) and are commonly used for permanent identifiers, calibration trim values, and — as covered in the security article — secure boot public key hashes and anti-rollback counters, specifically because their write-once nature makes them tamper-resistant.

FRAM and MRAM (Emerging Non-Volatile Memory)

Ferroelectric RAM (FRAM) and Magnetoresistive RAM (MRAM) combine the best properties of SRAM and flash: fast, byte-addressable read/write like RAM, but non-volatile like flash, with vastly higher endurance (often 10^14+ write cycles versus flash’s 10^5). They’re more expensive per bit and still lower density than flash, so I reach for them specifically for frequently-updated non-volatile data — event logs, counters, or state that must survive power loss but changes very often.

/* FRAM behaves like directly-addressable non-volatile RAM --
   no erase cycle needed, unlike flash */
#define FRAM_LOG_COUNTER_ADDR 0x0000

void FRAM_IncrementCounter(void)
{
    uint32_t count;
    SPI_FRAM_Read(FRAM_LOG_COUNTER_ADDR, (uint8_t *)&count, 4);
    count++;
    SPI_FRAM_Write(FRAM_LOG_COUNTER_ADDR, (uint8_t *)&count, 4); /* direct rewrite, no erase */
}

Memory Hierarchy and Typical Placement

graph TD
    A[Fastest / Smallest / Most Volatile] --> B[CPU Registers]
    B --> C[Cache / TCM - if present]
    C --> D[On-chip SRAM - stack, heap, buffers]
    D --> E[On-chip Flash - firmware code, constants]
    E --> F[On-chip EEPROM/emulated - config, calibration]
    F --> G[External SPI Flash/SD Card - logs, large data, OTA images]
    G --> H[Slowest / Largest / Most Persistent]

Choosing the Right Memory for Each Data Type

I go through this checklist for every piece of data a design needs to store:

Data TypeBest FitReasoning
Program codeNOR flash (internal)XIP support, non-volatile
Stack, heap, runtime buffersSRAMFast, byte-writable, doesn’t need persistence
Rarely-changed config (set once at manufacture)Flash or OTPNon-volatile, endurance not a concern
Frequently-changed config/calibrationEEPROM or FRAMByte-level rewrite, high endurance
Security keys, anti-rollback countersOTP/Fuses or secure elementTamper-resistant, write-once or hardware-protected
Data logs, event historyExternal SPI flash/FRAM, wear-leveledHigh volume or high write frequency
OTA firmware update stagingExternal flash (separate from running firmware)Allows safe fallback if update fails

Memory Protection and Isolation

Beyond just choosing memory types, professionally-built embedded systems typically use a Memory Protection Unit (MPU) — or a full MMU on Cortex-A parts — to enforce boundaries between different pieces of firmware, catching bugs before they corrupt unrelated memory regions.

/* Example: protecting a critical configuration data region from
   accidental writes by application-level code, using the MPU
   to mark it read-only except during an explicit, controlled
   update routine */
MPU_Region_InitTypeDef configRegion = {0};
configRegion.Enable = MPU_REGION_ENABLE;
configRegion.BaseAddress = CONFIG_DATA_ADDR;
configRegion.Size = MPU_REGION_SIZE_4KB;
configRegion.AccessPermission = MPU_REGION_PRIV_RO; /* read-only for normal execution */
HAL_MPU_ConfigRegion(&configRegion);

This kind of protection is especially valuable for catching stack overflows before they silently corrupt adjacent heap or global data — I typically place a small MPU-protected guard region immediately after each task’s stack in an RTOS-based design, so an overflow triggers an immediate fault rather than quietly corrupting neighboring memory (a bug class that can take days to track down without this protection).

Dual-Bank Flash and Safe OTA Update Staging

For any product supporting over-the-air firmware updates, memory layout planning becomes a safety-critical design decision. The standard professional pattern is a dual-bank (or A/B partition) flash layout, where a new firmware image is fully written and verified in an inactive bank before the bootloader ever switches execution to it — ensuring a failed or interrupted update never leaves the device unable to boot.

graph TD
    A[Bootloader] --> B{Which bank is active?}
    B -->|Bank A active| C[Run firmware from Bank A]
    B -->|Bank B active| D[Run firmware from Bank B]
    E[OTA Update Downloads] --> F[Write to Inactive Bank]
    F --> G[Verify Signature/Checksum of Inactive Bank]
    G -->|Valid| H[Update Active Bank Flag, Reboot]
    G -->|Invalid| I[Discard Update, Keep Running Current Bank]
typedef struct {
    uint32_t active_bank;      /* 0 = Bank A, 1 = Bank B */
    uint32_t bank_a_valid;
    uint32_t bank_b_valid;
} boot_config_t;

int Bootloader_SelectBank(boot_config_t *cfg)
{
    if (cfg->active_bank == 0 && cfg->bank_a_valid) return BANK_A_ADDR;
    if (cfg->active_bank == 1 && cfg->bank_b_valid) return BANK_B_ADDR;
    return FACTORY_RECOVERY_ADDR; /* fallback if both banks are somehow invalid */
}

This memory architecture decision directly ties back to the security discussion earlier in this series — signature verification of the inactive bank before switching to it is exactly the secure boot chain of trust applied to the update process itself.

Memory Budgeting During Firmware Development

Before writing significant application code, I always establish a memory budget by reviewing the linker map file, allocating expected SRAM and flash usage across major subsystems (RTOS kernel and task stacks, communication buffers, sensor data structures, application logic) with deliberate margin reserved for growth:

Flash Budget (512KB total):
  Bootloader:              32KB
  Application firmware:   380KB
  OTA staging reserve:     80KB (matches application size for dual-bank)
  Reserved/margin:         20KB

SRAM Budget (128KB total):
  RTOS kernel + task stacks: 24KB
  Communication buffers:     16KB
  Sensor data structures:     8KB
  Heap (dynamic allocation): 32KB
  Application globals:       20KB
  Reserved/margin:           28KB

Tracking this budget continuously (many build systems can generate a map file summary automatically as part of CI) catches memory growth problems early, well before a design gets uncomfortably close to its hard capacity limits.

Memory Diagnostics and Self-Test at Boot

For products where memory integrity genuinely matters (safety-critical or long-deployment-lifetime devices), I include boot-time or periodic memory self-tests to catch hardware degradation before it causes a field failure:

/* Simple SRAM march test - writes and reads back a pattern
   to detect stuck bits or addressing faults, commonly run
   at boot in safety-relevant embedded designs */
bool SRAM_MarchTest(uint32_t *start, uint32_t size_words)
{
    for (uint32_t i = 0; i < size_words; i++) {
        start[i] = 0xAAAAAAAA;
        if (start[i] != 0xAAAAAAAA) return false;
        start[i] = 0x55555555;
        if (start[i] != 0x55555555) return false;
    }
    return true;
}

/* Flash CRC verification - confirms stored firmware hasn't
   been corrupted by a partial write, wear-related bit flip,
   or memory fault since it was last verified */
bool Flash_IntegrityCheck(uint32_t addr, uint32_t size, uint32_t expected_crc)
{
    uint32_t computed = crc32((uint8_t *)addr, size);
    return computed == expected_crc;
}

These checks matter most for memory technologies with known wear or degradation mechanisms over a product’s deployment lifetime — flash bit errors accumulate with erase/write cycling, and even SRAM can develop faults from radiation-induced upsets in certain environments (aerospace and high-altitude applications specifically design around this risk using error-correcting code, or ECC, memory).

Cost Comparison Across Memory Technologies

Bill-of-materials cost is a real constraint that shapes memory decisions in high-volume products, and I keep an approximate mental cost ranking (per bit, at production volumes, roughly true across the industry at any given point in time even as absolute prices shift): NAND flash is cheapest per bit, followed by NOR flash, then SRAM, with EEPROM and especially FRAM/MRAM commanding the highest per-bit premium. This is exactly why designs use each type sparingly for what it’s uniquely good at — a few kilobytes of FRAM for high-endurance counters rather than trying to use it for bulk data storage, a small internal EEPROM-emulated region for calibration rather than large configuration datasets, and NAND/external flash reserved for genuinely large data volumes like data logs or media storage. Getting this allocation wrong in either direction — over-provisioning expensive memory types unnecessarily, or under-provisioning high-endurance memory for genuinely high-write-frequency data — shows up directly in either inflated product cost or premature field failures from memory wear.

Real-World Applications

Performance and Reliability Considerations

Execute-in-place from NOR flash is convenient but slower than executing from RAM, since flash access on many MCUs requires wait states at higher clock frequencies (mitigated by the prefetch/cache mechanisms discussed in the previous article). For reliability, any non-volatile write operation should be treated as something that can be interrupted by power loss mid-write — critical configuration writes often use a “write new, verify, then mark old data invalid” pattern (similar to a journaling filesystem) rather than overwriting the sole copy of important data directly.

Frequently Asked Questions

What’s the difference between flash and EEPROM if both are non-volatile? Flash must be erased in large blocks/sectors before rewriting, while true EEPROM can erase and rewrite individual bytes — making EEPROM better suited to small, frequently-changing data and flash better suited to large, infrequently-changed program storage.

Why can’t microcontrollers execute code directly from RAM by default? They actually can, and sometimes do for performance-critical routines — but RAM is volatile and typically much smaller than flash, so most firmware code stays in flash and only specific hot routines get copied to RAM at startup when needed.

Is external memory (SPI flash, SDRAM) less reliable than internal on-chip memory? Not inherently less reliable, but it does add a physical connection (solder joints, PCB traces) that can fail and adds latency compared to on-chip memory, so the choice usually comes down to needing more capacity than what fits on-chip.

When should I use FRAM/MRAM instead of standard flash or EEPROM? When data changes very frequently (many times per second or per minute) and needs to survive power loss — standard flash would wear out quickly under that write pattern, while FRAM/MRAM’s much higher endurance handles it comfortably, at a higher per-bit cost.

Summary

Embedded systems rely on a whole hierarchy of memory types, each with a distinct trade-off between speed, capacity, cost, persistence, and write endurance — SRAM for fast volatile runtime data, NOR flash for executable firmware, EEPROM or emulated EEPROM for small frequently-changed settings, OTP/fuses for permanent tamper-resistant data, and increasingly FRAM/MRAM for high-endurance non-volatile logging. Matching each piece of application data to the memory technology that actually fits its access pattern — rather than defaulting to “whatever’s convenient” — is one of those unglamorous decisions that quietly determines whether a product survives its full deployment lifetime or fails early in the field.

References

Exit mobile version