BERGSONNE

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.

Other Cores
The L4 and H5 chips have OTP areas, but the SDK doesn’t map them yet: 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 + NUL
Core.ST.L0 IDs before 25 September 2026
Firmware built before then read the L0’s third ID word from the wrong address, so IDs from different chips could collide. Rebuild against a current SDK if you rely on L0 IDs being unique.

Cross-architecture support

Storage, by Core (hover a chip for each feature):

●L0M0+1/1●L4M41/2●W5M332/2●H5M331/2WCH (RISC-V) · Nordic (nRF54) — in development

The unique ID:

●L0M0+●L4M4●W5M33●H5M33WCH (RISC-V) · Nordic (nRF54) — in development

See the implementation status for the full matrix.

Known gaps

What the SDK itself lists as missing here, straight from the headers:

NVM
mediumDSLNo bulk read / write Tier 2 yet

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.

lowDSLTwin NVM state wipes on Reset

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.

lowC APINo wear-tracking API

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.

OTP
highC APIOTP only on Core.ST.W5

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.

mediumDSLprogram_slot is escape-to-C (destructive)

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

Default-instance · Tier 2
uint32_t core_nvm_size(void);
Returns the total NVM size in bytes for this Core.
int core_nvm_read_byte(uint32_t offset);
Read a single byte from NVM at `offset`. Returns the byte (0..255) on success or -1 on any error (offset out of range, NVM disabled). A byte never written reads 255 (0xFF) on Core.ST.L4 / W5 / H5 and 0 on Core.ST.L0. The signed return lets DSL programs branch on `< 0` without an out-pointer.
int core_nvm_write_byte(uint32_t offset, uint8_t value);
Write a single byte to NVM at `offset`. Returns 1 on success or -1 on any error (offset out of range, flash error, no NVM on this Core). Each call is one flash record on Core.ST.L4 / W5 / H5: fine for a setting or a boot counter, wasteful in a tight loop (see core_nvm.h on wear).
Lower-level · Tier 1
int core_nvm_read(uint32_t offset, void * buf, uint32_t len);
Read bytes from NVM. Bytes never written read as CORE_NVM_ERASED (0xFF on Core.ST.L4 / W5 / H5, 0 on Core.ST.L0).
Returns: 0 on success, CORE_NVM_ERR_RANGE (-1) if the range falls outside the NVM region, CORE_NVM_ERR_FLASH if the store can't be read.
int core_nvm_write(uint32_t offset, const void * data, uint32_t len);
Write bytes to NVM. On Core.ST.L4 / W5 / H5 (1 KB, flash-emulated) a write is atomic: after a reset or power loss the range reads all old or all new, and the data survives reflashing. Most writes take well under a millisecond; about one small write in a hundred (L4; one in 500 on the W5, one in 350 on the H5) compacts the store, which erases a flash page and stalls the Core.ST.L4 for ~22 ms. Core.ST.L0 writes its EEPROM byte by byte (~3.2 ms each).
Returns: 0 on success, or a negative CORE_NVM_ERR_* code: RANGE (-1) if the range falls outside the region or the Core has no NVM, FLASH (-2) if the hardware reported an error, BUSY (-3), LAYOUT (-4).
int core_nvm_erase_all(void);
Erase the whole NVM region: every byte reads CORE_NVM_ERASED afterwards. Core.ST.L4 / W5 / H5: one page erase, or nothing if the region is already empty. Core.ST.L0: clears the EEPROM word by word (up to ~0.4 s; words already clear are skipped).
Returns: 0 on success, or a negative CORE_NVM_ERR_* code.
Constants
NameValueDescription
CORE_NVM_OK0Success.
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_ERASED0x00uRM0377 §3.3.4: an erased EEPROM byte reads 0.

Generated from core_nvm.h — tiles@77fd3f3.

OTP

Default-instance · Tier 2
uint32_t core_otp_size(void);
Total OTP size in bytes for this Core (0 where unavailable).
uint32_t core_otp_slot_count(void);
Number of write-once quad-word slots (CORE_OTP_SIZE / 16).
int core_otp_slot_is_blank(uint32_t slot);
Return 1 if `slot` is entirely erased (all 0xFF), 0 if any bit is programmed, or -1 if the slot index is out of range / OTP unavailable. A blank slot is programmable; a non-blank slot is not (write-once).
Lower-level · Tier 1
uint32_t core_otp_slot_addr(uint32_t slot);
Absolute address of a slot (no bounds check).
int core_otp_read(uint32_t offset, void * buf, uint32_t len);
Read bytes from OTP into `buf`. Memory-mapped load; no unlock needed. Returns 0 on success, -1 if the range is out of bounds or OTP is unavailable on this Core.
int core_otp_program_slot(uint32_t slot, const void * rec);
Program one 16-byte quad-word slot (write-once) and verify the read-back. IRREVERSIBLE. Refuses a slot that is not blank (returns -2) so a second write can't corrupt an existing record. On WBA55 this unlocks flash, issues a single 128-bit quad-word program, re-locks, and confirms the stored bytes match `rec`. There is no retry and no undo — callers should treat a failure as "advance to the next slot", not "try again here". slot: 0 .. CORE_OTP_SLOT_COUNT-1 rec: pointer to exactly CORE_OTP_SLOT_SIZE (16) bytes. Returns 0 on success, -1 on range/program/verify error, -2 if the slot is not blank, -3 if OTP programming is unavailable on this Core. Deliberately NOT `@studio expose`d: a destructive, irreversible burn does not belong in the DSL palette. It is escape-to-C only (see @studio unsupported below).

Generated from core_otp.h — tiles@f70bca2.

Unique ID

Lower-level · Tier 1
void core_uid_read(uint32_t out[3]);
Read the 96-bit unique device ID as three 32-bit words.
uint32_t core_uid_hash(void);
Fold the full 96-bit ID into a single stable 32-bit value (the three words XORed together). Better mixed than any single word — use it to derive short tokens (e.g. a 4-hex-digit name suffix) where the raw low bytes might be correlated across a manufacturing batch.
Returns: A 32-bit value, stable for the life of the chip.
int core_uid_hex(char * buf, uint16_t buflen);
Format the full 96-bit ID as an uppercase hex string (24 hex characters, word 0 first, most-significant nibble first within each word) plus a NUL terminator. The canonical form for a device serial number.
Returns: Number of hex characters written (24), or -1 if buf is NULL or buflen is too small.

Generated from core_uid.h — tiles@77fd3f3.