Skip to content

nRF Platform Guide

Xylolabs SDK integration for Nordic Semiconductor nRF microcontrollers.


Supported Targets

MCU Core Clock RAM Connectivity Audio Capability
nRF52840 Cortex-M4F 64 MHz 256 KB BLE 5.0, 802.15.4 2ch XAP @48kHz (DSP-assisted)
nRF9160 Cortex-M33 64 MHz 256 KB LTE-M / NB-IoT Sensors only

Hardware Setup

SPI Sensor Connection — LIS2DH12 Accelerometer (nRF52840)

The SDK example uses an LIS2DH12 3-axis accelerometer over SPI. The Zephyr device tree alias st_lis2dh maps to this device; the driver handles SPI framing and register access.

nRF52840 GPIO          LIS2DH12
──────────────────     ─────────────────────
P0.27 (SPI_SCK)  ───── SCL / SPC
P0.26 (SPI_MOSI) ───── SDA / SDI
P0.06 (SPI_MISO) ───── SDO
P0.08 (GPIO CS)  ───── CS (active low)
3V3              ───── VDD / VDDIO
GND              ───── GND
GND              ───── INT1 (pull to GND if unused)

Alternatively, LIS2DH12 supports I2C (SA0 low → 0x18, SA0 high → 0x19). Set the compatible property in the DTS overlay accordingly:

/* boards/nrf52840dk_nrf52840.overlay */
&spi1 {
    status = "okay";
    cs-gpios = <&gpio0 8 GPIO_ACTIVE_LOW>;

    lis2dh12: lis2dh12@0 {
        compatible = "st,lis2dh12", "st,lis2dh";
        reg = <0>;
        spi-max-frequency = <8000000>;
        label = "LIS2DH12";
    };
};

Sensor Buses

Bus Pins (nRF52840-DK) Typical devices
SPI1 P0.27/P0.26/P0.06 + CS LIS2DH12, MAX31865
I2C0 (TWI0) P0.26 (SDA), P0.27 (SCL) BME280, MPU6050
ADC (SAADC) AIN0–AIN7 Analog sensors, battery voltage

Transport Modes

BLE GATT (nRF52840)

XMBP packets are sent as BLE GATT notifications to a gateway device (e.g. ESP32, Raspberry Pi) which relays them to the Xylolabs API over WiFi or Ethernet. The nRF52840 never connects directly to the Internet.

nRF52840 (this device)
  → BLE GATT notifications (XMBP packets, 2M PHY)
    → Gateway (ESP32 / RPi)
      → HTTP POST to Xylolabs API

Key BLE parameters:

Parameter Default Range
Connection interval 15 ms (12 × 1.25 ms) 7.5 ms – 4 s
MTU 247 bytes 23 – 247 bytes
PHY 2M 1M / 2M / Coded
TX power 0 dBm −20 dBm to +8 dBm

For XMBP packets larger than the negotiated MTU, the platform layer automatically fragments across multiple notifications using a sequence header. The gateway reassembles before forwarding.

Configure BLE transport at init:

// Rust (Embassy) - Recommended
use nrf_softdevice::ble::{gatt_server, peripheral};
use nrf_softdevice::Softdevice;

// Configure BLE transport at init
let sd = Softdevice::enable(&softdevice_config());
let server = XylolabsGattServer::new(sd).unwrap();

// Start advertising with 15ms connection interval, MTU 247
let adv_config = peripheral::Config {
    interval: 160, // 100ms advertising interval
    ..Default::default()
};
let conn = peripheral::advertise_connectable(sd, adv_config, &adv_data).await.unwrap();
gatt_server::run(&conn, &server, |event| { /* handle XMBP notifications */ }).await;
Legacy C equivalent
platform_nrf_config_t cfg = {
    .transport      = NRF_TRANSPORT_BLE_GATT,
    .wdt_timeout_ms = 8000,
    .conn_interval  = 12,   /* 15 ms */
    .mtu            = 247,
    .apn            = NULL,
};
platform_nrf_init(&platform, &cfg);
platform_nrf_ble_start_advertising();

LTE Modem (nRF9160)

The nRF9160 SiP integrates a Cortex-M33 application core and a dedicated LTE modem on the same die. The application core communicates with the modem via an IPC link (shared memory + interrupts). Zephyr's nrf_modem library and socket API abstract the modem AT commands.

// Rust (Embassy) - Recommended
use nrf_modem::{ConnectionPreference, SystemMode};

// Configure LTE modem transport (nRF9160)
nrf_modem::init(SystemMode {
    lte_support: true,
    nbiot_support: true,
    gnss_support: false,
    preference: ConnectionPreference::Lte,
}).await.unwrap();

// Set APN and connect
nrf_modem::send_at("AT+CGDCONT=1,\"IP\",\"your.apn.here\"").await.unwrap();
let socket = nrf_modem::TcpStream::connect(host, port).await.unwrap();
Legacy C equivalent
platform_nrf_config_t cfg = {
    .transport      = NRF_TRANSPORT_LTE_MODEM,
    .wdt_timeout_ms = 30000,
    .conn_interval  = 0,
    .mtu            = 1280,
    .apn            = "your.apn.here",  /* or NULL for automatic */
};
platform_nrf_init(&platform, &cfg);

The SDK uses Zephyr's BSD socket API (zsock_connect, zsock_send, zsock_recv) over the LTE modem. TLS offloading to the modem is supported for low application-core overhead.


# Install dependencies
rustup target add thumbv7em-none-eabihf  # For nRF52840
rustup target add thumbv8m.main-none-eabihf  # For nRF9160
cargo install probe-rs-tools

# Build
cd sdk/examples/nrf52840-ble
cargo build --release

# Flash via J-Link
cargo run --release

See sdk/examples/ for all nRF examples.


Legacy C Build System

Zephyr / nRF Connect SDK (west)

The SDK is packaged as a Zephyr module. Add it to your application's west.yml:

# west.yml
manifest:
  projects:
    - name: xylolabs-sdk
      url: https://github.com/your-org/xylolabs-sdk
      path: modules/xylolabs
      revision: main

Or reference it locally in CMakeLists.txt before the Zephyr find_package:

cmake_minimum_required(VERSION 3.20)

# Add SDK as a Zephyr module
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../sdk/c/nrf)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_xylolabs_device)

target_sources(app PRIVATE
    src/main.c
    src/platform_impl.c
)
target_compile_definitions(app PRIVATE NRF_PLATFORM)

Build Commands

# Install nRF Connect SDK (ncs) and initialize west workspace
west init -m https://github.com/nrfconnect/sdk-nrf --mr v2.6.0 ncs
cd ncs && west update

# Build for nRF52840-DK (BLE transport)
west build -b nrf52840dk/nrf52840 -- -DCONF_FILE=prj_ble.conf

# Build for nRF9160-DK (LTE transport)
west build -b nrf9160dk/nrf9160/ns -- -DCONF_FILE=prj_lte.conf

# Flash
west flash

# Serial console
west espressif monitor   # or: minicom -D /dev/ttyACM0 -b 115200

Kconfig Options (prj.conf)

For BLE (nRF52840):

# prj_ble.conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_GATT_NOTIFY_MULTIPLE=y
CONFIG_BT_L2CAP_TX_MTU=247
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_BUF_ACL_RX_SIZE=251

CONFIG_SPI=y
CONFIG_SENSOR=y
CONFIG_LIS2DH=y

CONFIG_XYLOLABS_TRANSPORT_BLE=y
CONFIG_XYLOLABS_AUDIO_CHANNELS=2

For LTE (nRF9160):

# prj_lte.conf
CONFIG_NRF_MODEM_LIB=y
CONFIG_LTE_LINK_CONTROL=y
CONFIG_MODEM_INFO=y
CONFIG_NET_SOCKETS_OFFLOAD=y
CONFIG_NET_SOCKETS_POSIX_NAMES=y

CONFIG_XYLOLABS_TRANSPORT_LTE=y
CONFIG_XYLOLABS_AUDIO_CHANNELS=0


XAP Audio — nRF52840

Capability

The nRF52840 Cortex-M4F runs at 64 MHz with a single-precision FPU. With DSP-assisted LC3 spectral operations (used by XAP internally):

Configuration CPU Usage Feasibility
2ch XAP @48kHz ~22% (14 MIPS of 64) Supported
4ch XAP @96kHz ~88% (56 MIPS of 64) Not supported — insufficient headroom
1ch XAP @16kHz ~5% Supported (voice-grade)

4ch @96kHz is not supported on nRF52840. The 64 MHz M4F does not leave adequate headroom for BLE stack + sensor management + XAP at that rate. Use the ESP32-S3 (240 MHz, dual-core, vector extensions) for 4-channel deployments.

PDM Microphone Input

The nRF52840 integrates a PDM (Pulse-Density Modulation) interface for MEMS microphones. Connect a digital PDM mic (e.g. MP34DT01-M) to the PDM peripheral:

nRF52840 GPIO          PDM Microphone (e.g. MP34DT01-M)
──────────────────     ──────────────────────────────────
P0.13 (PDM_CLK)  ───── CLK
P0.14 (PDM_DIN)  ───── DATA
3V3              ───── VDD
GND              ───── GND
GND              ───── SEL (left channel)
3V3              ───── SEL (right channel, second mic)

Enable in Kconfig:

CONFIG_AUDIO_PDM_NRFX=y
CONFIG_AUDIO_CODEC_LC3=y
CONFIG_XYLOLABS_AUDIO_CHANNELS=2
CONFIG_XYLOLABS_AUDIO_SAMPLE_RATE=48000

XAP Memory Overhead (2ch @48kHz)

Region Size
LC3 encoder state (2ch) ~2 KB
Audio ring buffer 16 KB
LC3 batch buffer 2 KB
Total audio overhead ~20 KB

Memory Budget

nRF52840 — BLE Sensor Beacon (sensor-only)

Region Size
BLE SoftDevice (S140) ~40 KB
Zephyr kernel ~16 KB
XMBP packet buffer 4 KB
Sensor sample buffers ~2 KB
Application stack ~4 KB
Total used ~66 KB
Available ~190 KB

Recommended overrides for sensor-only beacon:

// Rust (Embassy) - Recommended
// Sensor-only beacon config — set in build.rs or Cargo.toml features
const AUDIO_CHANNELS: usize = 0;
const AUDIO_RING_SIZE: usize = 0;
const XMBP_BUF_SIZE: usize = 4096;
const HTTP_BUF_SIZE: usize = 512;
const SENSOR_CHANNELS: usize = 4;
const MOTOR_CHANNELS: usize = 0;
const SENSOR_RATE_HZ: u32 = 50;
const META_BATCH_MS: u32 = 2000;

Legacy C equivalent
#define XYLOLABS_AUDIO_CHANNELS    0
#define XYLOLABS_AUDIO_RING_SIZE   0
#define XYLOLABS_XMBP_BUF_SIZE   4096
#define XYLOLABS_HTTP_BUF_SIZE     512
#define XYLOLABS_SENSOR_CHANNELS   4
#define XYLOLABS_MOTOR_CHANNELS    0
#define XYLOLABS_SENSOR_RATE_HZ   50
#define XYLOLABS_META_BATCH_MS   2000

nRF52840 — BLE Audio Node (2ch XAP @48kHz)

Region Size
BLE SoftDevice (S140) ~40 KB
Zephyr kernel ~16 KB
LC3 encoder state (2ch) ~2 KB
Audio ring buffer 16 KB
LC3 batch buffer 2 KB
XMBP packet buffer 8 KB
Application stack ~6 KB
Total used ~90 KB
Available ~166 KB

nRF9160 — LTE Sensor Node

Region Size
nrf_modem library ~32 KB
Zephyr kernel + net stack ~24 KB
XMBP packet buffer 4 KB
TLS credential storage ~4 KB
Sensor buffers ~2 KB
Application stack ~4 KB
Total used ~70 KB
Available ~186 KB

Power Consumption

Mode nRF52840 nRF9160
Active (sensors, M4F running) ~3 mA ~3 mA
BLE TX (0 dBm, 2M PHY) ~5 mA N/A
LTE-M TX (peak) N/A ~220 mA
LTE-M RX N/A ~6 mA
Sleep (BLE connection retained) ~1.5 µA N/A
PSM (LTE, RRC idle) N/A ~2.5 µA
System OFF (nRF52840) ~0.5 µA N/A

The nRF52840 in sleep with BLE connection retained draws ~1.5 µA average — enabling multi-year operation on a CR2032 coin cell for low-duty-cycle sensor beacons.

eDRX and PSM (nRF9160)

Configure eDRX and PSM for minimal LTE-M power draw between transmissions:

// Rust (Embassy) - Recommended
use nrf_modem::send_at;

// Enable PSM: periodic TAU 1 hour, active time 10 s
send_at("AT+CPSMS=1,,,\"00100001\",\"00000101\"").await.unwrap();

// Enable eDRX: 40.96 s paging cycle
send_at("AT+CEDRXS=2,4,\"0101\"").await.unwrap();
Legacy C equivalent
/* Enable PSM: periodic TAU 1 hour, active time 10 s */
lte_lc_psm_req(true);

/* Enable eDRX: 40.96 s paging cycle */
lte_lc_edrx_req(LTE_LC_LTE_MODE_LTEM, "0101");

When PSM is active, the modem powers down between scheduled transmissions. The application core also enters low-power state via Zephyr's k_sleep() until the next batch interval.


Example Code Overview

The reference example is in sdk/c/nrf/examples/nrf52840_ble.c. It demonstrates:

  1. XMBP batch encoding — accumulates 100 accelerometer samples then encodes a compact binary batch for BLE transmission.
  2. Multi-rate sampling — accelerometer at 50 Hz (20 ms intervals) and die temperature at 1 Hz, using Zephyr's k_uptime_get() for precise scheduling.
  3. BLE fragmentation — large XMBP packets are split across MTU-sized GATT notifications with sequence headers for reassembly at the gateway.
  4. Zephyr sensor APIsensor_sample_fetch() + sensor_channel_get() against the LIS2DH12 driver; unit conversion from m/s² to g.
  5. Cooperative power managementk_sleep() surrenders CPU between sampling events; the BLE SoftDevice and Zephyr scheduler handle wake-up.

Key stream definitions from the example:

// Rust (Embassy) - Recommended
let streams = [
    StreamDef::new(0, "accel_x",     ValueType::F32, "g",       50.0),
    StreamDef::new(1, "accel_y",     ValueType::F32, "g",       50.0),
    StreamDef::new(2, "accel_z",     ValueType::F32, "g",       50.0),
    StreamDef::new(3, "temperature", ValueType::F32, "celsius",  1.0),
];
Legacy C equivalent
static const xylolabs_stream_def_t streams[] = {
    { 0, "accel_x",     XMBP_VT_F32, "g",       50.0f },
    { 1, "accel_y",     XMBP_VT_F32, "g",       50.0f },
    { 2, "accel_z",     XMBP_VT_F32, "g",       50.0f },
    { 3, "temperature", XMBP_VT_F32, "celsius",  1.0f },
};

Platform init:

// Rust (Embassy) - Recommended
use nrf_softdevice::ble::peripheral;

let sd = Softdevice::enable(&softdevice_config());
let server = XylolabsGattServer::new(sd).unwrap();

// Configure: 15ms connection interval, MTU 247, 8s watchdog
let adv = peripheral::ConnectableAdvertisement::ScannableUndirected {
    adv_data: &ADV_DATA,
    scan_data: &SCAN_DATA,
};
let conn = peripheral::advertise_connectable(sd, adv, &Default::default()).await.unwrap();
Legacy C equivalent
platform_nrf_config_t cfg = {
    .transport      = NRF_TRANSPORT_BLE_GATT,
    .wdt_timeout_ms = 8000,
    .conn_interval  = 12,   /* 15 ms */
    .mtu            = 247,
    .apn            = NULL,
};
int ret = platform_nrf_init(&platform, &cfg);
platform_nrf_ble_start_advertising();

Troubleshooting

Symptom Likely cause Fix
BLE notifications dropped MTU mismatch between peripheral and gateway Negotiate MTU explicitly with bt_gatt_exchange_mtu(); ensure gateway requests 247 bytes
LIS2DH12 not found at boot DTS overlay not applied or SPI CS pin wrong Check west build overlay path; verify cs-gpios matches your board wiring
LC3 encode latency spikes BLE SoftDevice preempting M4F during encode Raise LC3 thread priority; use Zephyr's cooperative scheduling to block during encode
nRF9160 modem init timeout SIM not inserted or APN misconfigured Verify SIM and APN; run AT+CEREG? via UART shell to check registration state
High current in sleep GPIO leakage or peripheral not suspended Ensure SPI CS is driven high, disable LIS2DH12 via powerdown mode before sleep
XMBP buffer overflow Batch flush interval too long Reduce XYLOLABS_META_BATCH_MS or increase XYLOLABS_XMBP_BUF_SIZE
Watchdog reset during LTE connect LTE attach takes >30 s on cold start Increase wdt_timeout_ms to 60000 for first boot; feed WDT during modem init