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
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
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.
Rust Build (Recommended)¶
# 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
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
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:
- XMBP batch encoding — accumulates 100 accelerometer samples then encodes a compact binary batch for BLE transmission.
- 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. - BLE fragmentation — large XMBP packets are split across MTU-sized GATT notifications with sequence headers for reassembly at the gateway.
- Zephyr sensor API —
sensor_sample_fetch()+sensor_channel_get()against the LIS2DH12 driver; unit conversion from m/s² to g. - Cooperative power management —
k_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
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
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 |