odesa-light/fds.md

4.4 KiB
Raw Permalink Blame History

Alright, lets do a minimal, no-magic FDS example that actually works for nRF52840 + SoftDevice + “user might pull power instantly”.

Ill 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 ~56 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 dont 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) → youre 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 (dont 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, its more code than 6 bytes deserve
  • But its 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.