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. Only Core.ST.L0 has it today (512 bytes of true EEPROM). Flash emulation for the L4, W5 and H5 is planned, not built.
  • 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 0 on success or -1 if the range falls outside the region. core_nvm_size() says how big the region is on this Core:

#include "core_nvm.h"

  struct { uint16_t boots; int16_t offset_mv; } cfg;

  if (core_nvm_size() >= sizeof cfg &&
      core_nvm_read(0, &cfg, sizeof cfg) == 0) {
      cfg.boots++;
      core_nvm_write(0, &cfg, sizeof cfg);   // ~3.2 ms per byte on the L0
  }
Core.ST.L4 / W5 / H5: no NVM yet
On these Cores core_nvm_size() is 0 and every read or write returns -1 without touching flash. Until flash emulation lands, keep settings in the backup registers or on an external storage tile.

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○H5M330/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.

mediumC APINo NVM on Core.ST.H5 yet

core_nvm_write returns -1 on Core.ST.H5 (CORE_NVM_SIZE is 0). The flash emulation behind Core.ST.L4 and Core.ST.W5 has not been ported to the H523's 8 KB sectors yet. Until it is, H5 programs that need persistent state are limited to backup registers.

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@6af026f.

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 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: 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, 0 on Core.ST.L0). Core.ST.H5 has no NVM yet: this always returns CORE_NVM_ERR_RANGE there.
int core_nvm_write(uint32_t offset, const void * data, uint32_t len);
Write bytes to NVM. On Core.ST.L4 / W5 (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) 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). Core.ST.H5 has no NVM yet.
int core_nvm_erase_all(void);
Erase the whole NVM region: every byte reads CORE_NVM_ERASED afterwards. Core.ST.L4 / W5: 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).

Generated from core_nvm.h — tiles@60254ad.

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.
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.

Generated from core_uid.h — tiles@f70bca2.