Skip to content

nRF52840

The nRF52840 is a popular Cortex-M4F SoC from Nordic Semiconductor with 256 KB SRAM, 1 MB flash, BLE 5.0, and 802.15.4. This page covers everything specific to running ferrite-sdk on the nRF52840.

Memory layout

The nRF52840 has 256 KB of SRAM at 0x20000000 - 0x2003FFFF. The SRAM is divided into 8 sections of 32 KB each, but for most purposes it behaves as a single contiguous block.

Linker script

Reserve the last 256 bytes of SRAM for the ferrite-sdk retained block. Your memory.x should look like:

ld
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 1024K
  RAM   : ORIGIN = 0x20000000, LENGTH = 255K   /* 256K - 256 bytes */
  RETAINED (rwx) : ORIGIN = 0x2003FF00, LENGTH = 0x100
}

Then include the retained section definition. You can either inline it or include the pre-built fragment:

ld
/* Option A: inline */
SECTIONS
{
  .uninit.ferrite (NOLOAD) : {
    . = ALIGN(4);
    _ferrite_retained_start = .;
    KEEP(*(.uninit.ferrite))
    _ferrite_retained_end = .;
    . = ALIGN(4);
  } > RETAINED
}

/* Option B: include pre-built fragment */
/* INCLUDE nrf52840-retained.x */

WARNING

If you are using the nRF SoftDevice (BLE stack), the SoftDevice reserves RAM starting from 0x20000000. Check your SoftDevice configuration for the actual RAM start address and adjust both the RAM and RETAINED origins accordingly. For example, with SoftDevice S140 v7.3 and a typical configuration, RAM might start at 0x20002000.

RAM regions for fault handler

Pass the full SRAM range to SdkConfig.ram_regions so the fault handler knows which addresses are safe to read during a stack snapshot:

rust
ferrite_sdk::init(SdkConfig {
    // ...
    ram_regions: &[RamRegion {
        start: 0x2000_0000,
        end: 0x2004_0000,  // 256 KB
    }],
});

If you are using the SoftDevice, restrict this to your application's RAM range:

rust
ram_regions: &[RamRegion {
    start: 0x2000_2000,  // After SoftDevice RAM
    end: 0x2004_0000,
}],

RESETREAS register

The nRF52840's POWER peripheral has a RESETREAS register at address 0x40000400 that indicates why the last reset occurred. Read it at startup and map to ferrite-sdk reboot reasons:

rust
fn read_nrf_reset_reason() -> ferrite_sdk::RebootReason {
    // Read RESETREAS register
    let resetreas = unsafe {
        core::ptr::read_volatile(0x4000_0400 as *const u32)
    };

    // Clear the register (write 1 to clear each bit)
    unsafe {
        core::ptr::write_volatile(0x4000_0400 as *mut u32, 0xFFFF_FFFF);
    }

    // Map bits to reboot reasons (check in priority order)
    if resetreas & (1 << 18) != 0 {
        // NFC field detect (LPCOMP)
        ferrite_sdk::RebootReason::Unknown
    } else if resetreas & (1 << 17) != 0 {
        // Debug interface (DIF)
        ferrite_sdk::RebootReason::SoftwareReset
    } else if resetreas & (1 << 16) != 0 {
        // GPIO reset (SREQ)
        ferrite_sdk::RebootReason::SoftwareReset
    } else if resetreas & (1 << 2) != 0 {
        // Software reset (SREQ bit)
        ferrite_sdk::RebootReason::SoftwareReset
    } else if resetreas & (1 << 1) != 0 {
        // Watchdog (DOG bit)
        ferrite_sdk::RebootReason::WatchdogTimeout
    } else if resetreas & (1 << 0) != 0 {
        // Pin reset (RESETPIN bit)
        ferrite_sdk::RebootReason::PinReset
    } else {
        // No bits set = power-on reset
        ferrite_sdk::RebootReason::PowerOnReset
    }
}

RESETREAS bit map

BitNameDescription
0RESETPINReset from pin-reset detected
1DOGReset from watchdog detected
2SREQReset from software reset (AIRCR.SYSRESETREQ)
3LOCKUPReset from CPU lock-up
16OFFReset due to wake-up from System OFF (GPIO)
17DIFReset due to wake-up from System OFF (debug interface)
18LPCOMPReset due to wake-up from System OFF (LPCOMP)
19VBUSReset due to USB VBUS detection

TIP

The nRF52840 HAL crate (nrf52840-hal) provides a typed API for reading RESETREAS via pac::POWER.resetreas.read(). Using the PAC is safer than raw pointer access:

rust
let power = unsafe { &*nrf52840_hal::pac::POWER::ptr() };
let resetreas = power.resetreas.read();
if resetreas.dog().is_detected() {
    // Watchdog reset
}

Retained RAM behavior

On the nRF52840, SRAM is retained across:

  • Software reset (SCB::sys_reset() / AIRCR.SYSRESETREQ)
  • Watchdog reset
  • CPU lock-up reset
  • Pin reset
  • Debug interface reset

SRAM is not retained across:

  • Power-on reset (VDD goes below the brownout threshold and back up)
  • Brownout reset (if configured)
  • System OFF mode (SRAM is powered down unless specific RAM sections are configured for retention)

System OFF retention

If your application uses System OFF mode, you can configure specific RAM sections to remain powered. This is done via the POWER.RAM[n].POWER registers. However, this is a separate mechanism from the ferrite-sdk retained block and requires additional configuration. For most applications that only use soft resets (not System OFF), the default behavior is sufficient.

probe-rs configuration

probe-rs supports the nRF52840 out of the box. Create a .cargo/config.toml in your project:

toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run"
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "link-arg=-Tdefmt.x",
]

[build]
target = "thumbv7em-none-eabihf"

Flash and view RTT logs:

bash
# Build and flash
cargo run --release

# Or flash without running
cargo flash --release --chip nRF52840_xxAA

# View RTT logs only
probe-rs attach --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/my-firmware

Common probe-rs issues

"No probe found" -- Ensure your J-Link or CMSIS-DAP probe is connected and your user has permissions. On Linux, add a udev rule:

bash
# /etc/udev/rules.d/99-probe-rs.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="1366", MODE="0666"  # SEGGER J-Link
SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", MODE="0666"  # CMSIS-DAP (DAPLink)

"Flash write failed" -- The nRF52840 has flash protection (APPROTECT). If enabled, you need to mass-erase first:

bash
probe-rs erase --chip nRF52840_xxAA

defmt output garbled -- Ensure your defmt version matches between firmware and defmt-rtt. Check that defmt.x is included in the linker arguments.

Complete Embassy example

Here is a complete, working example for the nRF52840-DK with Embassy:

rust
#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_nrf::{self as _, gpio, uarte};
use embassy_time::Duration;
use ferrite_sdk::{SdkConfig, RamRegion, RebootReason};
use ferrite_sdk::transport::AsyncChunkTransport;
use ferrite_embassy::upload_task::ferrite_upload_task;
use defmt_rtt as _;
use panic_probe as _;

struct UartTransport {
    tx: uarte::UarteTx<'static, embassy_nrf::peripherals::UARTE0>,
}

impl AsyncChunkTransport for UartTransport {
    type Error = uarte::Error;

    async fn send_chunk(&mut self, chunk: &[u8]) -> Result<(), Self::Error> {
        self.tx.write(chunk).await
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_nrf::init(Default::default());

    // Configure UART
    let mut config = uarte::Config::default();
    config.baudrate = uarte::Baudrate::BAUD115200;
    let irq = embassy_nrf::bind_interrupts!(struct Irqs {
        UARTE0_UART0 => uarte::InterruptHandler<embassy_nrf::peripherals::UARTE0>;
    });
    let uart = uarte::Uarte::new(p.UARTE0, Irqs, p.P0_08, p.P0_06, config);
    let (tx, _rx) = uart.split();

    // Read and record reboot reason
    let reason = read_nrf_reset_reason();

    // Initialize SDK
    ferrite_sdk::init(SdkConfig {
        device_id: "nrf52840-dk-01",
        firmware_version: env!("CARGO_PKG_VERSION"),
        build_id: 0,
        ticks_fn: || embassy_time::Instant::now().as_ticks(),
        ram_regions: &[RamRegion {
            start: 0x2000_0000,
            end: 0x2004_0000,
        }],
    });

    ferrite_sdk::reboot_reason::record_reboot_reason(reason);
    defmt::info!("SDK initialized, reboot reason: {:?}", reason as u8);

    // Spawn upload task
    let transport = UartTransport { tx };
    spawner.spawn(ferrite_upload_task(transport, Duration::from_secs(30))).unwrap();

    // Application loop
    loop {
        let _ = ferrite_sdk::metric_increment!("heartbeat");
        let _ = ferrite_sdk::metric_gauge!("vdd_mv", 3300.0);
        embassy_time::Timer::after(Duration::from_secs(5)).await;
    }
}

fn read_nrf_reset_reason() -> RebootReason {
    let resetreas = unsafe { core::ptr::read_volatile(0x4000_0400 as *const u32) };
    unsafe { core::ptr::write_volatile(0x4000_0400 as *mut u32, 0xFFFF_FFFF); }

    if resetreas & (1 << 2) != 0 {
        RebootReason::SoftwareReset
    } else if resetreas & (1 << 1) != 0 {
        RebootReason::WatchdogTimeout
    } else if resetreas & (1 << 0) != 0 {
        RebootReason::PinReset
    } else {
        RebootReason::PowerOnReset
    }
}

Memory usage on nRF52840

Measured with cargo size on a release build of the Embassy example above:

SectionSize
.text (flash)~42 KB (includes Embassy runtime)
.rodata (flash)~8 KB
.data (RAM, initialized)~256 bytes
.bss (RAM, zero-init)~1.6 KB (SDK state + Embassy)
.uninit.ferrite (retained)256 bytes

The SDK's contribution to flash is approximately 6 KB. The rest is Embassy, cortex-m-rt, and defmt.

Released under the MIT License.