Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

nRF52840 / nRF52833

Nordic’s nRF52840 and nRF52833 are ARM Cortex-M4F SoCs with a built-in IEEE 802.15.4 radio. The zigbee-rs nRF backend uses Embassy’s radio driver for interrupt-driven, DMA-based TX/RX — no SoftDevice required.

✅ Hardware Verified: The nRF52840-DK has been tested end-to-end with Home Assistant + ZHA. Features include flash NV storage (survives reboots), NWK Leave handling (auto-erase + rejoin), default reporting configuration, Identify cluster with LED blink, and optional BME280/SHT31 I2C sensors.

Hardware Overview

nRF52840nRF52833
CoreARM Cortex-M4F, 64 MHzARM Cortex-M4F, 64 MHz
Flash1024 KB512 KB
RAM256 KB128 KB
RadioBLE 5.3 + 802.15.4 + NFCBLE 5.3 + 802.15.4 + NFC
Targetthumbv7em-none-eabihfthumbv7em-none-eabihf

Hardware Radio Features

  • Auto-CRC generation and checking
  • Hardware address filtering (PAN ID + short address)
  • Auto-ACK for frames with ACK request bit set
  • Energy Detection (ED) via EDREQ task
  • RSSI measurement per packet
  • DMA-driven TX/RX buffers
  • Factory-programmed IEEE address in FICR registers

Common Development Boards

  • nRF52840-DK (PCA10056) — J-Link debugger, 4 buttons, 4 LEDs
  • nRF52840 USB Dongle (PCA10059) — USB bootloader, compact form
  • nice!nano v2 — Pro Micro form factor, UF2 bootloader
  • Seeed XIAO nRF52840 — compact, USB-C
  • Makerdiary nRF52840 MDK USB Dongle — UF2 bootloader
  • nRF52833-DK (PCA10100) — J-Link debugger, 4 buttons, 4 LEDs

No SoftDevice Needed

Unlike BLE-only projects, zigbee-rs accesses the 802.15.4 radio peripheral directly through Embassy’s embassy-nrf radio driver. There is no dependency on Nordic’s SoftDevice. This gives full control over the radio and avoids the SoftDevice’s RAM/Flash overhead.

UF2 variant note: If your board has a SoftDevice-based UF2 bootloader (e.g., nice!nano with Adafruit bootloader), the nrf52840-sensor-uf2 example disables the SoftDevice at startup via an SVC call. See the UF2 section below.

Prerequisites

Rust Toolchain

rustup default nightly
rustup update nightly

# Add the ARM Cortex-M4F target
rustup target add thumbv7em-none-eabihf

Debug Probe (for DK boards)

# probe-rs handles flashing + defmt log viewing
cargo install probe-rs-tools

Supported probes:

  • On-board J-Link (nRF52840-DK, nRF52833-DK)
  • Any CMSIS-DAP probe
  • Segger J-Link (external)

For UF2 boards (no probe needed)

pip install intelhex   # for uf2conv.py

Building

nRF52840-DK (probe-rs)

cd examples/nrf52840-sensor
cargo build --release

nRF52833-DK (probe-rs)

cd examples/nrf52833-sensor
cargo build --release

nRF52840 UF2 (nice!nano / ProMicro / MDK Dongle)

cd examples/nrf52840-sensor-uf2
cargo build --release                                    # ProMicro (default)
cargo build --release --no-default-features --features board-mdk         # MDK Dongle
cargo build --release --no-default-features --features board-nrf-dongle  # PCA10059
cargo build --release --no-default-features --features board-nrf-dk      # DK (J-Link)

nRF52840 Router

cd examples/nrf52840-router
cargo build --release

nRF52840 Bridge (coordinator)

cd examples/nrf52840-bridge
cargo build --release

What .cargo/config.toml Sets

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

[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip nRF52840_xxAA"

[env]
DEFMT_LOG = "info"

CI Build Commands

From .github/workflows/ci.yml:

# nRF52840 sensor
cd examples/nrf52840-sensor
cargo build --release

# nRF52840 router
cd examples/nrf52840-router
cargo build --release

# nRF52833 sensor
cd examples/nrf52833-sensor
cargo build --release

# UF2 variant (includes .uf2 conversion)
cd examples/nrf52840-sensor-uf2
cargo build --release

# Firmware artifact extraction
OBJCOPY=$(find $(rustc --print sysroot) -name llvm-objcopy | head -1)
$OBJCOPY -O binary $ELF ${ELF}.bin
$OBJCOPY -O ihex   $ELF ${ELF}.hex

# UF2 conversion (CI uses uf2conv.py from Microsoft's UF2 repo)
python uf2conv.py -c -f 0xADA52840 ${ELF}.hex -o ${ELF}.uf2

Memory Layout

The memory.x linker script defines the memory regions:

nRF52840 (full chip, no bootloader):

FLASH : ORIGIN = 0x00000000, LENGTH = 1024K
RAM   : ORIGIN = 0x20000000, LENGTH = 256K

nRF52833:

FLASH : ORIGIN = 0x00000000, LENGTH = 512K
RAM   : ORIGIN = 0x20000000, LENGTH = 128K

nRF52840 UF2 (with SoftDevice S140 bootloader):

FLASH : ORIGIN = 0x00026000, LENGTH = 808K    ← app starts after SoftDevice
RAM   : ORIGIN = 0x20002000, LENGTH = 248K

The UF2 example’s build.rs selects the memory layout based on the board feature.

Flashing

probe-rs (DK boards)

cd examples/nrf52840-sensor

# Flash + live defmt log output
cargo run --release

# Or flash only
probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/nrf52840-sensor

Tip: Plug in the DK before running cargo run. probe-rs auto-detects the probe. Check with probe-rs list if detection fails.

UF2 Drag-and-Drop Flash

For boards with UF2 bootloaders (nice!nano, ProMicro, MDK Dongle):

  1. Build the firmware:

    cd examples/nrf52840-sensor-uf2
    cargo build --release
    
  2. Convert to UF2:

    # Extract binary
    OBJCOPY=$(find $(rustc --print sysroot) -name llvm-objcopy | head -1)
    $OBJCOPY -O ihex target/thumbv7em-none-eabihf/release/nrf52840-sensor-uf2 fw.hex
    
    # Convert to UF2 (download uf2conv.py from Microsoft's UF2 repo)
    python uf2conv.py -c -f 0xADA52840 fw.hex -o fw.uf2
    
  3. Enter bootloader mode: Double-tap the RESET button on the board. A USB mass storage device appears (e.g., NICENANO).

  4. Copy the .uf2 file to the USB drive. The board flashes automatically and reboots into your firmware.

nrfjprog --program target/thumbv7em-none-eabihf/release/nrf52840-sensor.hex --chiperase --verify
nrfjprog --reset

MAC Backend Notes

The nRF MAC backend lives in zigbee-mac/src/nrf/mod.rs (single file — no separate driver module needed since Embassy provides the radio abstraction).

Feature Flags

FeatureChipCargo.toml dependency
nrf52840nRF52840zigbee-mac = { features = ["nrf52840"] }
nrf52833nRF52833zigbee-mac = { features = ["nrf52833"] }

Key Dependencies

embassy-nrf = { version = "0.3", features = ["nrf52840", "time-driver-rtc1", "gpiote"] }
embassy-executor = { version = "0.7", features = ["arch-cortex-m", "executor-thread"] }

How It Works

  1. NrfMac<T: Instance> wraps Embassy’s Radio<T> and implements MacDriver
  2. Radio TX/RX is fully interrupt-driven with DMA — no polling needed
  3. Hardware auto-ACK is enabled for frames with the ACK request bit
  4. Hardware address filtering is configured through the radio peripheral
  5. The factory-programmed IEEE address is read from FICR registers
  6. Embassy’s time-driver-rtc1 provides async timers via RTC1

Embassy Integration

The nRF examples use Embassy’s cooperative async executor:

bind_interrupts!(struct Irqs {
    RADIO => radio::InterruptHandler<peripherals::RADIO>;
    TEMP => embassy_nrf::temp::InterruptHandler;
});

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_nrf::init(Default::default());
    let radio = radio::ieee802154::Radio::new(p.RADIO, Irqs);
    let mac = zigbee_mac::nrf::NrfMac::new(radio);
    // ...
}

The select3 combinator handles concurrent events:

#![allow(unused)]
fn main() {
match select3(
    device.receive(),                                    // Radio RX
    button.wait_for_falling_edge(),                      // Button press
    Timer::after(Duration::from_secs(REPORT_INTERVAL)),  // Periodic report
).await {
    Either3::First(event)  => { /* handle stack event */ }
    Either3::Second(_)     => { /* handle button press */ }
    Either3::Third(_)      => { /* read sensor, update clusters */ }
}
}

Power Optimization

Sensor (End Device)

The nRF52840 sensor example includes several hardware-level power optimizations that bring the average current draw down to ~5 µA. See the Power Management chapter for full details.

DC-DC Converter

The nRF52840’s internal DC-DC converter replaces the default LDO regulators, reducing current draw by ~40%. Both reg0 (main supply) and reg1 (radio supply) are enabled at startup:

#![allow(unused)]
fn main() {
config.dcdc = embassy_nrf::config::DcdcConfig {
    reg0: true,
    reg0_voltage: None,
    reg1: true,
};
}

TX Power

TX power is set to 0 dBm (down from the default +8 dBm), which cuts TX current roughly in half while maintaining adequate range for home environments:

#![allow(unused)]
fn main() {
mac.set_tx_power(0); // 0 dBm — saves ~50% TX current vs +8 dBm
}

HFCLK Source

The high-frequency clock source is set to the internal RC oscillator. The radio peripheral automatically requests the external crystal when it needs high accuracy (during TX/RX), saving ~250 µA during idle periods:

#![allow(unused)]
fn main() {
config.hfclk_source = embassy_nrf::config::HfclkSource::Internal;
}

Poll and Report Intervals

The sensor uses a two-phase polling scheme:

PhasePoll IntervalDurationCurrent
Fast poll250 ms120 s after join/activityHigher (responsive)
Slow poll30 sSteady stateVery low (~5 µA avg)

Reports are sent every 60 seconds, but only when sensor values change by more than the configured thresholds (±0.5 °C temperature, ±1% humidity, ±2% battery). This suppresses unnecessary transmissions in stable environments.

RAM Power-Down

Unused RAM banks are powered down at startup, saving ~190 KB of unpowered SRAM. This was already implemented in earlier versions.

Radio Sleep

Between polls, the radio is disabled via TASKS_DISABLE register write, saving ~4-8 mA of radio RX/idle current. The radio_wake() method re-applies the channel setting and re-enables the radio before the next TX/RX operation.

Router (Always-On)

The nRF52840 router uses PowerMode::AlwaysOn — the radio is always on since routers must relay frames continuously. DC-DC converters are still enabled for lower power, but no sleep logic is applied. Typical current draw with DC-DC enabled is ~5-7 mA (radio RX idle).


Example Walkthrough

nrf52840-sensor

The flagship example: an Embassy-based Zigbee 3.0 end device that reads the on-chip temperature sensor and reports simulated humidity. Includes:

  • Flash NV storage — network state persists across power cycles (last 8 KB of flash)
  • NWK Leave handler — auto-erases NV and rejoins when coordinator sends Leave
  • Default reporting — configures report intervals at boot (temp/hum: 60–300 s, battery: 300–3600 s)
  • Identify cluster (0x0003) — LED blinks during Identify
  • Battery monitoring via SAADC (VDD internal divider)
  • Optional external sensors — BME280 (temp + humidity + pressure) or SHT31 (temp + humidity)

Initialization:

#![allow(unused)]
fn main() {
let p = embassy_nrf::init(Default::default());

// On-chip temperature sensor (real hardware reading)
let mut temp_sensor = Temp::new(p.TEMP, Irqs);

// Button 1 on nRF52840-DK (P0.11, active low)
let mut button = gpio::Input::new(p.P0_11, gpio::Pull::Up);

// IEEE 802.15.4 MAC driver (interrupt-driven, DMA-based)
let radio = radio::ieee802154::Radio::new(p.RADIO, Irqs);
let mac = zigbee_mac::nrf::NrfMac::new(radio);
}

Real temperature reading:

#![allow(unused)]
fn main() {
// Read actual die temperature (°C with 0.25° resolution)
let temp_c = temp_sensor.read().await;
let temp_hundredths = (temp_c.to_num::<f32>() * 100.0) as i16;
temp_cluster.set_temperature(temp_hundredths);
}

nrf52840-sensor-uf2

The UF2 variant supports multiple boards via cargo features:

FeatureBoardLEDFlash Origin
board-promicroProMicro / nice!nanoP0.15 (HIGH)0x26000
board-mdkMakerdiary MDK DongleP0.22 (LOW)0x1000
board-nrf-dongleNordic PCA10059P0.06 (LOW)0x1000
board-nrf-dkNordic DK (PCA10056)P0.13 (LOW)0x0000

This variant auto-joins on boot (no button press needed) and includes a logdefmt bridge so internal stack log messages appear in RTT output.

nrf52840-bridge

A coordinator/bridge example that exposes the Zigbee network over USB serial.

nrf52840-router

A Zigbee 3.0 router that extends network range. Key differences from the sensor examples:

  • Device type: Router (FFD) instead of End Device
  • Power mode: AlwaysOn — radio is never turned off
  • Frame relay: Relays unicast, broadcast, and indirect frames
  • Child management: Accepts end device joins, buffers frames for sleepy children
  • Link Status: Sends periodic broadcasts (every 15 seconds)
  • RREQ rebroadcast: Participates in AODV route discovery
  • LEDs: LED1 = joined status, LED2 = blink on frame relay

Troubleshooting

SymptomCauseFix
probe-rs can’t find deviceProbe not connectedCheck USB; run probe-rs list
probe-rs permission deniedMissing udev rules (Linux)See probe-rs setup
292 / RAM overflowToo many features enabledCheck Embassy feature flags, reduce arena size
defmt output garbledVersion mismatchEnsure defmt, defmt-rtt, panic-probe versions match
UF2 board not appearingNot in bootloaderDouble-tap RESET quickly; look for USB drive
Device doesn’t joinCoordinator not permittingEnable permit-join on coordinator
No temperature readingTEMP interrupt not boundEnsure bind_interrupts! includes TEMP handler

Adjusting Log Level

# Via environment variable
DEFMT_LOG=trace cargo run --release

# Or set in .cargo/config.toml
[env]
DEFMT_LOG = "debug"

Expected Serial Output (via RTT)

INFO  Zigbee-RS nRF52840 sensor starting…
INFO  Radio ready
INFO  NV: restored network state from flash
INFO  Default reporting configured (temp: 60-300s, hum: 60-300s, battery: 300-3600s)
INFO  Device ready — press Button 1 to join/leave
INFO  [btn] Joining network…
INFO  [scan] Scanning channels 11-26…
INFO  [scan] Found network: ch=15, PAN=0x1AAA
INFO  [join] Association successful, addr=0x1234
INFO  [sensor] T=23.75°C  H=52.30%  Battery=100%
INFO  [nv] State saved to flash