4.4 KiB
Alright, let’s do a minimal, no-magic FDS example that actually works for nRF52840 + SoftDevice + “user might pull power instantly”.
I’ll keep this focused on one tiny config record.
What this example assumes
- You already have SoftDevice enabled
- You can handle SoftDevice events somewhere in your app
- You want to store ~5–6 bytes reliably
This is pure SoftDevice FDS, no Embassy storage abstractions.
1️⃣ Define your config format
Keep it boring and explicit:
#[repr(C)]
#[derive(Clone, Copy)]
struct Config {
magic: u8,
version: u8,
data: [u8; 4],
}
const CONFIG_MAGIC: u8 = 0x42;
const CONFIG_VERSION: u8 = 1;
Total: 6 bytes 👍
2️⃣ FDS constants
These must be non-zero and stable forever:
const FDS_FILE_ID: u16 = 0x1111;
const FDS_RECORD_KEY: u16 = 0x2222;
3️⃣ Initialize FDS
Call this after SoftDevice is enabled:
use nrf_softdevice::raw;
pub fn fds_init() {
unsafe {
let ret = raw::fds_init();
assert_eq!(ret, raw::NRF_SUCCESS);
}
}
⚠️ FDS is async — init finishes when you receive FDS_EVT_INIT.
4️⃣ Handling FDS events (important!)
You must forward SoftDevice events to FDS:
pub fn on_sd_evt(evt: *const raw::ble_evt_t) {
unsafe {
raw::fds_sys_evt_handler(evt as u32);
}
}
And somewhere in your SD event handler:
match evt_id {
raw::FDS_EVT_INIT => {
// FDS ready
}
raw::FDS_EVT_WRITE => {
// Write completed safely
}
raw::FDS_EVT_UPDATE => {
// Update completed
}
_ => {}
}
You don’t need to block — just observe completion.
5️⃣ Read config from flash
pub fn load_config() -> Option<Config> {
unsafe {
let mut desc = core::mem::zeroed::<raw::fds_record_desc_t>();
let mut token = core::mem::zeroed::<raw::fds_find_token_t>();
let ret = raw::fds_record_find(
FDS_FILE_ID,
FDS_RECORD_KEY,
&mut desc,
&mut token,
);
if ret != raw::NRF_SUCCESS {
return None;
}
let mut record = core::mem::zeroed::<raw::fds_flash_record_t>();
raw::fds_record_open(&desc, &mut record);
let cfg = *(record.p_data as *const Config);
raw::fds_record_close(&desc);
if cfg.magic == CONFIG_MAGIC && cfg.version == CONFIG_VERSION {
Some(cfg)
} else {
None
}
}
}
On boot:
- If
None→ use defaults - If
Some(cfg)→ you’re golden
6️⃣ Write / update config (power-loss safe)
This is the core value of FDS.
pub fn save_config(cfg: &Config) {
unsafe {
let record = raw::fds_record_t {
file_id: FDS_FILE_ID,
key: FDS_RECORD_KEY,
data: raw::fds_record_chunk_t {
p_data: cfg as *const _ as *const core::ffi::c_void,
length_words: (core::mem::size_of::<Config>() + 3) / 4,
},
};
let mut desc = core::mem::zeroed::<raw::fds_record_desc_t>();
let mut token = core::mem::zeroed::<raw::fds_find_token_t>();
let ret = raw::fds_record_find(
FDS_FILE_ID,
FDS_RECORD_KEY,
&mut desc,
&mut token,
);
if ret == raw::NRF_SUCCESS {
raw::fds_record_update(&mut desc, &record);
} else {
raw::fds_record_write(&mut desc, &record);
}
}
}
Why this is safe 🔒
- Old record stays valid until new one is fully written
- Power loss = either old config or new config
- Never corrupted data
7️⃣ BLE write flow (typical)
BLE write received
→ validate data
→ build Config
→ save_config()
→ wait for FDS_EVT_UPDATE
→ ACK to user
If power dies anywhere → still safe.
8️⃣ Flash usage (don’t forget this!)
In your SoftDevice config:
- Enable FDS
- Reserve at least 2 pages (3 is safer)
FDS will garbage-collect automatically.
TL;DR (real talk)
For your exact requirements:
- Yes, this is the right solution
- Yes, it’s more code than 6 bytes deserve
- But it’s bulletproof
If you want next:
- 🧵 Wrap this in an Embassy async interface
- 🧪 Add CRC / version migration
- 🔄 Reduce boilerplate further
- 🧠 Explain FDS GC & page sizing
Just say the word.