Storage & ID
Three ways a Core keeps something across resets: core_nvm for settings you rewrite, core_otp for identity burned once at production, and core_uid for the ID the chip was born with. How much of each a Core has depends on its MCU, so read the support notes before relying on one.
Overview
- NVM — byte-addressed storage that survives power loss and reflashing, with the same API on every Core: 512 bytes of true EEPROM on Core.ST.L0, 1 KB of flash emulation on the L4, W5 and H5.
- OTP — write-once bytes outside the main flash, so even a full chip erase can’t clear them. Core.ST.W5 only, today.
- Unique ID — the 96-bit factory ID, read-only, on every Core.
For small state that only has to survive a reset or Standby (not power loss), the backup registers work on every Core.
NVM
core_nvm_read / core_nvm_write take a byte offset and a buffer, and return CORE_NVM_OK (0) or a negative CORE_NVM_ERR_* code. core_nvm_size() says how big the region is on this Core. Bytes never written read as CORE_NVM_ERASED (0xFF on the flash Cores, 0x00 on the L0), so check for it on first boot:
#include "core_nvm.h"
struct { uint16_t boots; int16_t offset_mv; } cfg;
if (core_nvm_read(0, &cfg, sizeof cfg) == CORE_NVM_OK) {
if (cfg.boots == 0xFFFF) cfg.boots = 0; // never written (flash Cores)
cfg.boots++;
core_nvm_write(0, &cfg, sizeof cfg);
}Flash emulation (L4, W5, H5)
Each write appends a record (offset, length, data, CRC) to a log in one of two flash pages; when the page fills, the current contents move to the other page (one erase). That makes a write atomic: after a reset or power cut at any instant, the bytes it covered read all old or all new. Rewriting bytes that are already stored costs nothing, and the two pages share the wear.
- Survives reflashing. Every linker script ends the application below the NVM pages, so
flash-serial,flash-dfu, Studio and probes leave it alone. A full-chip erase clears it. - Timing. A small write takes a few hundred microseconds. A compaction takes tens of milliseconds, and on the L4 (one flash bank) the CPU and every interrupt stall for its ~22 ms erase. The watchdog is fed just before it.
- Core.ST.W5 with BLE. Erases and writes go through ST’s flash manager, between radio events, so a connection survives. Call from the main loop or a BLE callback, never from an interrupt.
- Core.ST.H5. The store lives in the flash’s 100k-cycle data area. The first time an H5 boots an SDK image,
core_init()switches that area on (one option-byte change, then one reset); later boots just use it. - Not re-entrant. A call made while another is running (from an interrupt) returns
CORE_NVM_ERR_BUSY.
EEPROM (L0)
On the L0 the EEPROM handles erase-before-write itself. At the low clock level (voltage range 3, which can’t program it) a write steps up to range 2 for the program and back, automatically.
OTP identity
On Core.ST.W5 the OTP area is 512 bytes at 0x0BF90000, split into 32 slots of 16 bytes. Reads are a plain memory load. core_otp_program_slot() burns one slot, refuses a slot that isn’t blank, and checks the read-back — but there is no undo: bits only go from 1 to 0, and a programmed slot can never be rewritten. To amend a record, write a new one to the next blank slot and read the last valid one.
#include "core_otp.h"
uint8_t rec[16];
for (uint32_t s = 0; s < core_otp_slot_count(); s++) {
if (core_otp_slot_is_blank(s) == 1) break; // first unused slot
core_otp_read(s * 16, rec, sizeof rec); // a burned record
}The record format (magic, CRC, fields) is the application’s to define. Burning is deliberately C-only: the DSL can query OTP (size, slot count, blank check) but can’t program it, so a stray Studio block can’t consume a slot. Reads and the slot geometry were checked on a W5; the program path has never run on silicon, since every test burn is permanent.
core_otp_size() returns 0 and reads and programs return an error. The L0 has no OTP.Unique ID
Every STM32 carries a 96-bit ID set at the factory. It needs no init and never changes, so it makes a good serial number, a seed for a unique BLE name, or a basis for per-device keys:
#include "core_uid.h"
uint32_t w[3];
core_uid_read(w); // the three 32-bit words
uint32_t tok = core_uid_hash(); // stable 32-bit fold of the full ID
char serial[25];
core_uid_hex(serial, sizeof serial); // 24 hex characters + NULCross-architecture support
Storage, by Core (hover a chip for each feature):
The unique ID:
See the implementation status for the full matrix.
Known gaps
What the SDK itself lists as missing here, straight from the headers:
Tier 2 covers the byte-level pair (read_byte, write_byte) — enough for boot counters, single flags, small struct fields. Multi-byte read_buf / write_buf still need the array-IN / array-OUT host-call ABI prototyped on the tile-driver side. Tracked with the DSL Capability Coverage close.
read_byte / write_byte round-trip within a single run, but the per-slot store is cleared on every project reload — DSL programs that rely on NVM state surviving a soft reset (boot counters, crash flags) can't be exercised end-to-end in the IDE. Same issue the Backup register gap calls out; closing both needs persistent per-slot state across worker resets.
core_nvm_erase_all clears the region, but nothing reports the page erase count or the remaining write budget. Long-running data loggers budget their write rate from the figures in core_nvm.h.
CORE_OTP_* is backed only on Core.ST.W5 (STM32WBA55, 512 B @ 0x0BF90000). L0 / L4 / H5 report size 0 and every read/program returns an error until their OTP geometry is added. WBA55 is the only Core that currently ships a product using OTP identity.
Reads (size, slot_count, slot_is_blank, read) are safe to surface, but the burn (core_otp_program_slot) is irreversible and write-once, so it is intentionally NOT exposed to the DSL — a stray palette call could permanently consume a slot. Provisioning burns happen from C / the production programmer, not from a Studio program.
From the @studio unsupported notes in core_nvm.h, core_otp.h — tiles@217a38f.
API reference
NVM
uint32_t core_nvm_size(void);int core_nvm_read_byte(uint32_t offset);int core_nvm_write_byte(uint32_t offset, uint8_t value);int core_nvm_read(uint32_t offset, void * buf, uint32_t len);int core_nvm_write(uint32_t offset, const void * data, uint32_t len);int core_nvm_erase_all(void);| Name | Value | Description |
|---|---|---|
CORE_NVM_OK | 0 | Success. |
CORE_NVM_ERR_RANGE | (-1) | Offset / length outside the region, or this Core has no NVM. |
CORE_NVM_ERR_FLASH | (-2) | Erase or program failed (FLASH_SR error, or the read-back did not match). |
CORE_NVM_ERR_BUSY | (-3) | Called while another NVM call was in progress (e.g. from an interrupt). |
CORE_NVM_ERR_LAYOUT | (-4) | The linker script does not reserve the NVM pages (a custom .ld). |
CORE_NVM_ERASED | 0x00u | RM0377 §3.3.4: an erased EEPROM byte reads 0. |
Generated from core_nvm.h — tiles@77fd3f3.
OTP
uint32_t core_otp_size(void);uint32_t core_otp_slot_count(void);int core_otp_slot_is_blank(uint32_t slot);uint32_t core_otp_slot_addr(uint32_t slot);int core_otp_read(uint32_t offset, void * buf, uint32_t len);int core_otp_program_slot(uint32_t slot, const void * rec);Generated from core_otp.h — tiles@f70bca2.
Unique ID
void core_uid_read(uint32_t out[3]);uint32_t core_uid_hash(void);int core_uid_hex(char * buf, uint16_t buflen);Generated from core_uid.h — tiles@77fd3f3.

