Skip to main content

Secure Storage

SITCore exposes two secure storage regions inside the chip's internal flash — separate from the deployment region and not accessible externally. Both are addressed in 32-byte blocks through the same SecureStorageController API.

  • Configuration storage — mutable. Read, write, erase. Wiped on Erase All. Use for runtime configuration that needs to persist across reboots but can change over time (network settings, calibration data, device serial numbers).
  • OTP storage — write-once. Survives Erase All. Use for immutable per-device data set at manufacturing (factory keys, customer IDs, hardware revision).

NuGet package: GHIElectronics.TinyCLR.Devices.SecureStorage.

Sizes​

ChipsetConfiguration storageOTP storage
SC20100, SC20260128 KB (4096 × 32 B blocks)64 KB (2048 × 32 B blocks)
SC130488 KB (256 × 32 B blocks)2 KB (64 × 32 B blocks)

Configuration storage​

Configuration storage can be read, written, and erased. Because the entire region is one flash sector, partial erases aren't possible — Erase() wipes the whole region.

The example below queries the size, verifies the region is blank (erasing if not), writes a test pattern, and reads it back to verify:

using GHIElectronics.TinyCLR.Devices.SecureStorage;
using System;
using System.Diagnostics;
using System.Threading;

var configStorage = new SecureStorageController(SecureStorage.Configuration);

Debug.WriteLine("Total size: " + configStorage.TotalSize + " bytes");
Debug.WriteLine("Block size: " + configStorage.BlockSize + " bytes");

var totalBlocks = configStorage.TotalSize / configStorage.BlockSize;

// Check if every block is blank.
var isBlank = true;
for (uint block = 0; block < totalBlocks; block++) {
if (!configStorage.IsBlank(block)) {
isBlank = false;
break;
}
}

if (!isBlank) {
Debug.WriteLine("Erasing...");
configStorage.Erase();
}

// Fill the first 32 KB of the region with a known pattern.
const uint testBytes = 32 * 1024;
var data = new byte[testBytes];
for (var i = 0; i < data.Length; i++)
data[i] = (byte)(i % 255);

// Write in 32-byte chunks.
var chunk = new byte[configStorage.BlockSize];
for (uint block = 0; block < data.Length / configStorage.BlockSize; block++) {
Array.Copy(data, (int)(block * configStorage.BlockSize), chunk, 0, (int)configStorage.BlockSize);
configStorage.Write(block, chunk);
}

// Read back and verify.
for (uint block = 0; block < data.Length / configStorage.BlockSize; block++) {
configStorage.Read(block, chunk);
for (var i = 0; i < configStorage.BlockSize; i++) {
if (data[block * configStorage.BlockSize + i] != chunk[i]) {
Debug.WriteLine("Mismatch — data is corrupted");
Thread.Sleep(Timeout.Infinite);
}
}
}

Debug.WriteLine("Verified");

One-time programmable (OTP) storage​

OTP blocks cannot be erased or rewritten once written. There's no way to undo a write — not from your application, not from TinyCLR Config's Erase All, not from the bootloader. This is what makes OTP useful for per-device identity values you want to fix at manufacturing.

The API is identical to configuration storage except for the constructor argument and the absence of Erase():

using GHIElectronics.TinyCLR.Devices.SecureStorage;

var otpStorage = new SecureStorageController(SecureStorage.Otp);

// Read and Write work the same as configuration storage.
otpStorage.Write(block: 0, data: factoryKey);

Per-chip differences​

SC20100 / SC20260 — a block containing all 0xFF is treated as unused and can be written again. Practically, this means writing all-0xFF to a block doesn't burn it, so accidental writes of empty buffers don't lose a block.

SC13048 — every Write() call burns the block, even if the data being written is all 0xFF. There's no safety net here; check your data before writing.

warning

OTP writes are permanent. Verify your code, double-check your inputs, and consider writing to a non-OTP block first as a dry run before committing the value to OTP.

API reference​

NamespaceDescription
GHIElectronics.TinyCLR.Devices.SecureStorageSecure one-time-programmable storage classes