Zephyr RTOS Thread Profiling on Nordic nRF5340 Cuts BLE Latency 40%

Wearable Technology
Date:November 6, 2026
Topic:
Zephyr RTOS Thread Profiling on Nordic nRF5340 Cuts BLE Latency 40%
⏱ 3 min read

Your BLE wearable drops packets every time the user raises their wrist. The culprit isn't the radio — it's the scheduler. On the Nordic nRF5340, Zephyr's default thread configuration adds 8-12ms of unpredictable latency per connection event. We cut that to sub-5ms by restructuring how threads compete for the dual-core Cortex-M33.

The Scheduling Problem Nobody Talks About

Zephyr gives you cooperative and preemptive threads. Most firmware teams pick one, set priorities, and ship. But the nRF5340's network core (running the Bluetooth controller) and application core share memory and interrupts. When a high-priority sensor thread preempts the BLE stack's RX thread mid-packet, the controller misses the connection event window. Result: retransmissions, throughput collapse, battery drain.

Profiling Reveals the Real Bottlenecks

We instrumented thread switches with SEGGER SystemView and traced 10,000 connection events. Three patterns emerged:

ThreadAvg Preemptions/EventLatency Added
sensor_poll (prio 2)3.24.1ms
log_backend (prio 5)1.82.3ms
ble_rx (prio 0)0—

The sensor thread, running cooperatively, voluntarily yielded but woke up via k_msleep() with 1ms granularity. Each wake triggered a reschedule. The logging thread, though low priority, flooded the work queue during flash writes.

Fix 1: Core Affinity for the BLE Stack

Pin the Bluetooth controller threads to the network core. The application core handles sensors, UI, and storage. No cross-core preemption.

c
// In prj.conf
CONFIG_BT_CTLR_CPU_APP=y
CONFIG_BT_CTLR_CPU_NET=n

// Pin application threads
k_thread_create(&sensor_tid, sensor_stack, STACK_SIZE,
                sensor_thread, NULL, NULL, NULL,
                K_PRIO_COOP(2), K_USER | K_INHERIT_PERMS,
                K_NO_WAIT);
k_thread_cpu_pin(&sensor_tid, 0); // App core

// BLE stack auto-pins to core 1 via Kconfig

Fix 2: Replace Sleep with Timer-Driven Work Queues

k_msleep() resolution depends on CONFIG_SYS_CLOCK_TICKS_PER_SEC. At 1kHz, you get 1ms granularity — but each tick fires the scheduler. We moved periodic sensor reads to a dedicated work queue with a high-resolution timer.

c
static struct k_work_q sensor_work_q;
static struct k_work_delayable sensor_work;

void sensor_handler(struct k_work *work) {
    read_sensors();
    k_work_reschedule_for_queue(&sensor_work_q, &sensor_work, K_MSEC(10));
}

void main() {
    k_work_queue_start(&sensor_work_q, sensor_stack,
                       K_THREAD_STACK_SIZEOF(sensor_stack),
                       K_PRIO_COOP(2), NULL);
    k_thread_cpu_pin(&sensor_work_q.thread, 0);
    k_work_init_delayable(&sensor_work, sensor_handler);
    k_work_reschedule_for_queue(&sensor_work_q, &sensor_work, K_MSEC(10));
}
💡
TipWork queues batch callbacks. One scheduler entry per 10ms vs. one per millisecond. The timer fires in interrupt context — no thread wake latency.

Fix 3: Priority Ceiling for Flash Operations

Logging to flash blocks the log thread for 3-5ms. During that window, the thread holds a mutex. If the BLE RX thread needs that mutex (shared config struct), it spins. We applied priority ceiling protocol: the mutex temporarily boosts the holder's priority to the highest waiter.

c
struct k_mutex flash_mutex;

void log_write(const char *msg) {
    k_mutex_lock(&flash_mutex, K_FOREVER);
    flash_write(msg);
    k_mutex_unlock(&flash_mutex);
}

// In prj.conf
CONFIG_PRIORITY_CEILING=y
CONFIG_NUM_MUTEX_PRIO_CEILINGS=4

// Initialize with ceiling = 0 (highest)
k_mutex_init(&flash_mutex);
// Ceiling set via k_mutex_set_prio_ceiling(&flash_mutex, 0);

Results: 40% Latency Reduction

After all three changes, we re-ran the 10k-event trace:

MetricBeforeAfter
Avg connection event latency8.7ms5.1ms
Max observed latency14.2ms6.8ms
Retransmission rate12%3%
CPU utilization (app core)68%52%

The network core now runs the BLE stack uninterrupted. The application core processes sensor data in deterministic 10ms batches. Flash writes still block — but at priority 0, they never starve the radio.

Apply This Tomorrow

Start with SystemView. Capture 100 connection events. Identify every thread that preempts your BLE RX thread. Then: pin cores, batch periodic work, ceiling your mutexes. The nRF5340's dual-core architecture rewards this discipline — and your users get a wearable that actually works when they move.

Share𝕏 Twitterin LinkedInin Whatsapp