Subject: MPSL ASSERT: 69, 108 and HardFault when using ESB (CONFIG_ESB_MPSL_TIMESLOT) with BLE dual connection and ZMS on NCS v3.3.0 / nRF54L10

Hi,

I'm running NCS v3.3.0 on a Nordic nRF54L10 (nRF54L Series, Cortex-M33) with:
- BLE peripheral role, max 2 connections (dual connected simultaneously)
- ESB in CONFIG_ESB_MPSL_TIMESLOT mode
- ZMS library for non-volatile storage
- ESB PTX/PRX role switching every ~4 ms
(check if TX data pending → enter PTX → after TX done/callback → switch back to PRX)

Under combined BLE traffic (2 connections) + ESB communication, the device randomly crashes:

<err> mpsl_init: MPSL ASSERT: 69, 108
<err> os: ***** HARD FAULT *****
<err> os: Fault escalation (see below)
<err> os: ARCH_EXCEPT with reason 3
>>> ZEPHYR FATAL ERROR 3: Kernel oops on CPU 0
Current thread: 0x20007010 (idle)
Halting system

Key configuration:

# BLE
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_MAX_CONN=2
CONFIG_BT_DEVICE_NAME="RD-QED2"
CONFIG_BT_GATT_CLIENT=y
CONFIG_BT_USER_DATA_LEN_UPDATE=y
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_L2CAP_TX_MTU=247
CONFIG_BT_CTLR_SDC_MAX_CONN_EVENT_LEN_DEFAULT=5000
CONFIG_BT_RADIO_NOTIFICATION_CONN_CB=y

# MPSL / ESB
CONFIG_MPSL=y
CONFIG_ESB_MPSL_TIMESLOT=y
CONFIG_MPSL_TIMESLOT_SESSION_COUNT=1
CONFIG_MPSL_WORK_STACK_SIZE=2048
CONFIG_ESB=y
CONFIG_ESB_CLOCK_INIT=y
CONFIG_ESB_TX_FIFO_SIZE=16
CONFIG_ESB_DYNAMIC_INTERRUPTS=y
CONFIG_ESB_RADIO_IRQ_PRIORITY=0
CONFIG_ESB_EVENT_IRQ_PRIORITY=1
CONFIG_NRFX_GPPI=y
CONFIG_DYNAMIC_INTERRUPTS=y
CONFIG_DYNAMIC_DIRECT_INTERRUPTS=y
CONFIG_MPSL_ASSERT_HANDLER=n

# ZMS
CONFIG_ZMS=y
CONFIG_PM_PARTITION_SIZE_ZMS_STORAGE=0x4000

My questions:
1. What does "MPSL ASSERT: 69, 108" mean in MPSL timeslot context? Is it an invalid timeslot request or incorrect return from the timeslot signal callback (e.g. p_next == NULL / bad extend action)?
2. Are there known issues/recommendations when toggling ESB PTX⇄PRX inside an MPSL timeslot while BLE (2 connections) is active on nRF54L Series?
3. Could BT_CTLR_SDC_MAX_CONN_EVENT_LEN_DEFAULT=5000 (5 ms) conflict with the 4 ms ESB role-check cycle and cause timeslot overruns / rejected requests?
4. What is the correct / safe sequence to stop ESB (PTX→PRX switch) and re-enter the MPSL timeslot to avoid this MPSL assertion?

Any clarification or reference would be greatly appreciated.

Thanks!

  • Hello,

    Can you try to set CONFIG_MPSL_HFCLK_LATENCY=1650 in the prj.conf file and try again to run the sample?

    Thanks.

    BR
    Kazi

  • Hi Nordic team,
    Regarding the first issue:
    It appears the root cause is related to the 32.768kHz clock offset on my board. Before applying the correction, stopping BLE advertising triggers an assert. This also happens during BLE and ESB coexistence — stopping either BLE or ESB at runtime results in the same assertion failure.
    Second question regarding NCS migration:
    When migrating from NCS 3.0.0 to NCS 3.3.0, I noticed that the compiled partitions.ymllayout has changed compared to the previous version. Using the default configuration, this difference causes issues when upgrading from older firmware versions.
    To maintain compatibility, I added a pm_static.ymlbased on the old partition layout, but compilation fails with the following error:
    -- Found partition manager static configuration : D:/00_work/03_multi_tx_rx_shift_optimize/f-e-wireless-shifting/pm_static_nrf54l15dk_nrf54l10_cpuapp.yml
    Partition 'mcuboot' is not included in the dynamic resolving since it is statically defined.
    Partition 'mcuboot_pad' is not included in the dynamic resolving since it is statically defined.
    Partition 'mcuboot_primary' is not included in the dynamic resolving since it is statically defined.
    Partition 'mcuboot_primary_app' is not included in the dynamic resolving since it is statically defined.
    Partition 'mcuboot_secondary' is not included in the dynamic resolving since it is statically defined.
    Partition 'zms_storage' is not included in the dynamic resolving since it is statically defined.
    Partition manager failed: Incorrect amount of gaps found in static configuration. There must be exactly one gap in the static configuration to support placing the dynamic partitions (such as 'app'). Gaps found (3):0x10800-0x85000 0xfa000-0xfb000 0xff000-0xfd000 The most common solution to this problem is to fill the smallest of these gaps with statically defined partition(s) until there is only one gap left. Alternatively re-order the already defined static partitions so that only one gap remains.
    Failed to partition region flash_primary, size of region: 1036288
    Partition Configuration:
    mcuboot:
      placement:
        align:
          end: 4096
        before:
        - mcuboot_primary
      size: 65536
    mcuboot_pad:
      placement:
        before:
        - mcuboot_primary_app
      size: 2048
    mcuboot_primary:
      size: 479232
    mcuboot_primary_app:
      size: 477184
    mcuboot_secondary:
      placement:
        after:
        - mcuboot_primary
        align:
          start: 4096
      size: 479232
    zms_storage:
      placement:
        align:
          start: 4096
        before:
        - end
      size: 16384
    Why did the default partition layout change after the migration? Is there a recommended way to keep using the original NCS 3.0.0 partition layout in NCS 3.3.0? Which configurations or files need to be updated?
    For reference, I will attach the partitions.ymlgenerated by NCS 3.0.0, the one generated by NCS 3.3.0, and my current pm_static.yml.
    Looking forward to your reply.
    Thanks!
    EMPTY_0:
      address: 0xf8000
      end_address: 0xf9000
      placement:
        after:
        - mcuboot_secondary
      region: flash_primary
      size: 0x1000
    app:
      address: 0x10800
      end_address: 0x84000
      region: flash_primary
      size: 0x73800
    bootconf:
      address: 0xffd080
      end_address: 0xffd084
      region: bootconf
      size: 0x4
    mcuboot:
      address: 0x0
      end_address: 0x10000
      placement:
        align:
          end: 0x1000
        before:
        - mcuboot_primary
      region: flash_primary
      size: 0x10000
    mcuboot_pad:
      address: 0x10000
      end_address: 0x10800
      placement:
        before:
        - mcuboot_primary_app
      region: flash_primary
      size: 0x800
    mcuboot_primary:
      address: 0x10000
      end_address: 0x84000
      orig_span: &id001
      - app
      - mcuboot_pad
      region: flash_primary
      sharers: 0x1
      size: 0x74000
      span: *id001
    mcuboot_primary_app:
      address: 0x10800
      end_address: 0x84000
      orig_span: &id002
      - app
      region: flash_primary
      size: 0x73800
      span: *id002
    mcuboot_secondary:
      address: 0x84000
      end_address: 0xf8000
      placement:
        after:
        - mcuboot_primary
        align:
          start: 0x1000
      region: flash_primary
      share_size:
      - mcuboot_primary
      size: 0x74000
    otp:
      address: 0xffd500
      end_address: 0xffd980
      region: otp
      size: 0x480
    sram_primary:
      address: 0x20000000
      end_address: 0x20030000
      region: sram_primary
      size: 0x30000
    zms_storage:
      address: 0xf9000
      end_address: 0xfd000
      placement:
        align:
          start: 0x1000
        before:
        - end
      region: flash_primary
      size: 0x4000
    
    EMPTY_0:
      address: 0xff000
      end_address: 0xff800
      placement:
        after:
        - zms_storage
      region: flash_primary
      size: 0x800
    EMPTY_1:
      address: 0xfa000
      end_address: 0xfb000
      placement:
        after:
        - mcuboot_secondary
      region: flash_primary
      size: 0x1000
    app:
      address: 0x10800
      end_address: 0x85000
      region: flash_primary
      size: 0x74800
    bootconf:
      address: 0xffd080
      end_address: 0xffd084
      region: bootconf
      size: 0x4
    mcuboot:
      address: 0x0
      end_address: 0x10000
      placement:
        align:
          end: 0x1000
        before:
        - mcuboot_primary
      region: flash_primary
      size: 0x10000
    mcuboot_pad:
      address: 0x10000
      end_address: 0x10800
      placement:
        before:
        - mcuboot_primary_app
      region: flash_primary
      size: 0x800
    mcuboot_primary:
      address: 0x10000
      end_address: 0x85000
      orig_span: &id001
      - mcuboot_pad
      - app
      region: flash_primary
      sharers: 0x1
      size: 0x75000
      span: *id001
    mcuboot_primary_app:
      address: 0x10800
      end_address: 0x85000
      orig_span: &id002
      - app
      region: flash_primary
      size: 0x74800
      span: *id002
    mcuboot_secondary:
      address: 0x85000
      end_address: 0xfa000
      placement:
        after:
        - mcuboot_primary
        align:
          start: 0x1000
      region: flash_primary
      share_size:
      - mcuboot_primary
      size: 0x75000
    sram_primary:
      address: 0x20000000
      end_address: 0x20030000
      region: sram_primary
      size: 0x30000
    zms_storage:
      address: 0xfb000
      end_address: 0xff000
      placement:
        align:
          start: 0x1000
        before:
        - end
      region: flash_primary
      size: 0x4000
    
    mcuboot:
      address: 0x0
      end_address: 0x10000
      placement:
        align:
          end: 0x1000
        before:
        - mcuboot_primary
      region: flash_primary
      size: 0x10000
    
    mcuboot_pad:
      address: 0x10000
      end_address: 0x10800
      placement:
        before:
        - mcuboot_primary_app
      region: flash_primary
      size: 0x800
    
    mcuboot_primary:
      address: 0x10000
      end_address: 0x85000
      orig_span: &id001
      - mcuboot_pad
      - app
      region: flash_primary
      sharers: 0x1
      size: 0x75000
      span: *id001
    
    mcuboot_primary_app:
      address: 0x10800
      end_address: 0x85000
      orig_span: &id002
      - app
      region: flash_primary
      size: 0x74800
      span: *id002
    
    mcuboot_secondary:
      address: 0x85000
      end_address: 0xfa000
      placement:
        after:
        - mcuboot_primary
        align:
          start: 0x1000
      region: flash_primary
      share_size:
      - mcuboot_primary
      size: 0x75000
    
    zms_storage:
      address: 0xfb000
      end_address: 0xff000
      placement:
        align:
          start: 0x1000
        before:
        - end
      region: flash_primary
      size: 0x4000
  • We also encountered this issue on NRF52840 (nRF Connect SDK v3.3.0).

    00> [00:11:55.572,296] <err> mpsl_init: MPSL ASSERT: 69, 108[0m
    00> [00:11:55.572,296] <err> os: ***** HARD FAULT *****
    00> [00:11:55.572,296] <err> os:   Fault escalation (see below)
    00> [00:11:55.572,326] <err> os: ARCH_EXCEPT with reason 3
    00> 
    00> [00:11:55.572,357] <err> os: r0/a1:  0x00000003  r1/a2:  0x00000000  r2/a3:  0x00000007
    00> [00:11:55.572,387] <err> os: r3/a4:  0x00000003 r12/ip:  0x2abc7cee r14/lr:  0x0000ee7d
    00> [00:11:55.572,387] <err> os:  xpsr:  0x01000011
    00> [00:11:55.572,418] <err> os: Faulting instruction address (r15/pc): 0x0001b5a0
    00> [00:11:55.572,448] <err> os: >>> ZEPHYR FATAL ERROR 3: Kernel oops on CPU 0
    00> [00:11:55.572,448] <err> os: Fault during interrupt handling
    00> 
    00> [00:11:55.572,509] <err> os: Current thread: 0x20002868 (idle)
    00> [00:11:56.082,794] <err> fatal_error: Resetting system

    No BLE connection.

    /*
     * Copyright (c) 2025 Nordic Semiconductor ASA
     *
     * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
     */
    
    #include <zephyr/bluetooth/bluetooth.h>
    #include <zephyr/bluetooth/conn.h>
    #include <zephyr/bluetooth/gatt.h>
    #include <zephyr/bluetooth/hci.h>
    #include <zephyr/kernel.h>
    #include <zephyr/logging/log.h>
    #include <zephyr/logging/log_ctrl.h>
    #include <zephyr/sys/reboot.h>
    #include <zephyr/random/random.h>
    #include <bluetooth/services/lbs.h>
    #include <dk_buttons_and_leds.h>
    #include <esb.h>
    
    LOG_MODULE_REGISTER(esb_prx_ble, CONFIG_ESB_PRX_BLE_LOG_LEVEL);
    
    #define DEVICE_NAME             CONFIG_BT_DEVICE_NAME "-ESB-PRX"
    #define DEVICE_NAME_LEN         (sizeof(DEVICE_NAME) - 1)
    
    #define CON_STATUS_LED          DK_LED1
    #define USER_LED                DK_LED2
    #define ESB_LED_1               DK_LED3
    #define ESB_LED_2               DK_LED4
    #define USER_BUTTON             DK_BTN1_MSK
    
    static bool app_button_state;
    static struct k_work adv_work;
    
    static struct esb_payload rx_payload;
    static struct esb_payload tx_payload = ESB_CREATE_PAYLOAD(0,
    	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17);
    
    static bool rx_active = false;
    static void rx_work_handler(struct k_work* work);
    K_WORK_DELAYABLE_DEFINE(rx_work, rx_work_handler);
    
    static const struct bt_data ad[] = {
    	BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    	BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
    };
    
    static const struct bt_data sd[] = {
    	BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_LBS_VAL),
    };
    
    static void esb_err_stat(int err)
    {
        static uint8_t esb_err_count = 0;
        static int err_buf[10] = { 0 };
        if (err) {
            if (esb_err_count >= 10) {
                esb_err_count = 0;
            }
            err_buf[esb_err_count] = err;
            esb_err_count++;
        }
        for (int i = 0; i < 10 && err_buf[i] != 0; i++) {
            LOG_ERR("ESB error buffer[%d] = %d", i, err_buf[i]);
        }
    }
    static void rx_work_handler(struct k_work* work)
    {
        int err;
        static uint8_t start_rx_failed_count = 0;
        if (rx_active) {
            err = esb_stop_rx();
            if (err) {
                LOG_INF("RX stop failed, err %d", err);
            }
    
            uint32_t jitter = sys_rand32_get() % 20;
            k_work_schedule(&rx_work, K_MSEC(40 + jitter));
        } else {
            err = esb_start_rx();
            esb_err_stat(err);
            if (err) {
                start_rx_failed_count++;
                LOG_ERR("RX setup failed %d times, err %d", start_rx_failed_count, err);
                log_panic();
    
                if(start_rx_failed_count >= 30) {
                    sys_reboot(SYS_REBOOT_COLD);
                }
            } else {
                start_rx_failed_count = 0;
            }
            
            k_work_schedule(&rx_work, K_MSEC(14));
        }
        rx_active = !rx_active;
    }
    
    static void adv_work_handler(struct k_work* work)
    {
        int err = bt_le_adv_start(
            BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN,
                BT_GAP_ADV_SLOW_INT_MIN,
                BT_GAP_ADV_SLOW_INT_MAX, NULL), ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    
    	if (err) {
    		LOG_ERR("Advertising failed to start (err %d)", err);
    		return;
    	}
    
    	LOG_INF("Advertising successfully started");
    }
    
    static void advertising_start(void)
    {
    	k_work_submit(&adv_work);
    }
    
    static void connected(struct bt_conn *conn, uint8_t err)
    {
    	if (err) {
    		LOG_ERR("Connection failed, err 0x%02x %s", err, bt_hci_err_to_str(err));
    		return;
    	}
    
    	LOG_INF("Connected");
    
    	dk_set_led_on(CON_STATUS_LED);
    }
    
    static void disconnected(struct bt_conn *conn, uint8_t reason)
    {
    	LOG_INF("Disconnected, reason 0x%02x %s", reason, bt_hci_err_to_str(reason));
    
    	dk_set_led_off(CON_STATUS_LED);
    }
    
    static void recycled_cb(void)
    {
    	LOG_INF("Connection object available from previous conn. Disconnect is complete!");
    	advertising_start();
    }
    
    BT_CONN_CB_DEFINE(conn_callbacks) = {
    	.connected        = connected,
    	.disconnected     = disconnected,
    	.recycled         = recycled_cb,
    };
    
    static void app_led_cb(bool led_state)
    {
    	dk_set_led(USER_LED, led_state);
    }
    
    static bool app_button_cb(void)
    {
    	return app_button_state;
    }
    
    static struct bt_lbs_cb lbs_callbacks = {
    	.led_cb    = app_led_cb,
    	.button_cb = app_button_cb,
    };
    
    static void button_changed(uint32_t button_state, uint32_t has_changed)
    {
    	if (has_changed & USER_BUTTON) {
    		uint32_t user_button_state = button_state & USER_BUTTON;
    
    		bt_lbs_send_button_state(user_button_state);
    		app_button_state = user_button_state ? true : false;
    	}
    }
    
    static void leds_update(uint8_t value)
    {
    	const bool led1_state = !(value % 8 > 2 && value % 8 <= 6);
    	const bool led2_state = !(value % 8 > 3);
    
    	dk_set_led(ESB_LED_1, led1_state);
    	dk_set_led(ESB_LED_2, led2_state);
    }
    
    void event_handler(struct esb_evt const *event)
    {
    	int err;
    
    	switch (event->evt_id) {
    	case ESB_EVENT_TX_SUCCESS:
    		LOG_DBG("TX SUCCESS EVENT");
    		break;
    	case ESB_EVENT_TX_FAILED:
    		LOG_DBG("TX FAILED EVENT");
    		break;
    	case ESB_EVENT_RX_RECEIVED:
            while ((err = esb_read_rx_payload(&rx_payload)) == 0) {
                static uint8_t target_buf[3] = { 0x6e, 0x26, 0xb1 };
                if (memcmp(rx_payload.data, target_buf, sizeof(target_buf))) {
                    continue;
                }
                static uint32_t rx_count = 0, last_uptime = 0;
                static uint16_t last_seq_num = 0x8000, missed_count = 0;
                uint16_t new_seq_num = rx_payload.data[6] | (rx_payload.data[5] << 8);
                uint32_t uptime = k_uptime_seconds();
                rx_count++;
                if(new_seq_num  - last_seq_num > 1 && last_seq_num != 0x8000) {
                    missed_count += new_seq_num - last_seq_num - 1;
                    LOG_WRN("Missed %d packets, total missed %d", new_seq_num - last_seq_num - 1, missed_count);
                    LOG_HEXDUMP_WRN(rx_payload.data, rx_payload.length, "New packet data");
                }
                LOG_INF("T:%u, RX %u, last_seq 0x%04x, new_seq 0x%04x, missed %u", uptime, rx_count, last_seq_num, new_seq_num, missed_count);
                last_seq_num = new_seq_num;
    
                static bool last_led_state = false;
                if (uptime - last_uptime > 2) {
                    dk_set_led(DK_LED1, last_led_state);
                    last_led_state = !last_led_state;
                    last_uptime = uptime;
                }
    		}
    		if (err && err != -ENODATA) {
    			LOG_ERR("Error while reading rx packet");
    		}
    		break;
    	case ESB_EVENT_TIMESLOT_FAILED:
    		LOG_ERR("TIMESLOT FAILED EVENT");
    		break;
    	}
    }
    
    int esb_initialize(void)
    {
    	int err;
    	/* These are arbitrary default addresses. In end user products
    	 * different addresses should be used for each set of devices.
    	 */
    	const uint8_t base_addr_0[4] = {0xE7, 0xE7, 0xE7, 0xE7};
    	const uint8_t base_addr_1[4] = {0xC2, 0xC2, 0xC2, 0xC2};
    	const uint8_t addr_prefix[8] = {0xE7, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8};
    
        const struct esb_config config = {
            .protocol           = ESB_PROTOCOL_ESB_DPL,
            .mode               = ESB_MODE_PRX,
            .event_handler      = event_handler,
            .bitrate            = ESB_BITRATE_1MBPS,
            .crc                = ESB_CRC_16BIT,
            .tx_output_power    = ESB_TX_POWER_8DBM,
            .retransmit_delay   = 600,
            .retransmit_count   = 1,
            .tx_mode            = ESB_TXMODE_AUTO,
            .payload_length     = CONFIG_ESB_MAX_PAYLOAD_LENGTH,
            .selective_auto_ack = true,
            .use_fast_ramp_up = IS_ENABLED(CONFIG_ESB_FAST_SWITCHING)
        };
    
    	err = esb_init(&config);
    	if (err) {
    		return err;
        }
        
        err = esb_set_address_length(ARRAY_SIZE(base_addr_0));
        if (err) {
            LOG_ERR("esb set address length failed %d", err);
            return err;
        }
        
        err = esb_set_base_address_0(base_addr_0);
    	if (err) {
    		return err;
    	}
    
    	err = esb_set_base_address_1(base_addr_1);
    	if (err) {
    		return err;
    	}
    
    	err = esb_set_prefixes(addr_prefix, ARRAY_SIZE(addr_prefix));
    	if (err) {
    		return err;
    	}
    
        err = esb_set_rf_channel(81);
        if (err) {
            return err;
        }
    
        return 0;
    }
    
    int main(void)
    {
    	int err;
    
        LOG_INF("Build date: %s, build time: %s", __DATE__, __TIME__);
    	// LOG_INF("Starting Enhanced ShockBurst prx with Peripheral LBS sample");
    
    	err = dk_leds_init();
    	if (err) {
    		LOG_ERR("LEDs init failed (err %d)", err);
    		return 0;
    	}
    
    	err = dk_buttons_init(button_changed);
    	if (err) {
    		LOG_ERR("Cannot init buttons (err: %d)", err);
    	}
    
    	err = bt_enable(NULL);
    	if (err) {
    		LOG_ERR("Bluetooth init failed (err %d)", err);
    		return 0;
    	}
    
    	LOG_INF("Bluetooth initialized");
    
    	err = bt_lbs_init(&lbs_callbacks);
    	if (err) {
    		LOG_ERR("Failed to init LBS (err:%d)", err);
    		return 0;
    	}
    
    	k_work_init(&adv_work, adv_work_handler);
    	advertising_start();
    
    	err = esb_initialize();
    	if (err) {
    		LOG_ERR("ESB initialization failed, err %d", err);
    		return 0;
    	}
    
    	// LOG_INF("ESB initialized");
    
    	// err = esb_write_payload(&tx_payload);
    	// if (err) {
    	// 	LOG_ERR("Write payload, err %d", err);
    	// 	return 0;
    	// }
    
    	// LOG_INF("Setting up for packet reception");
    
    	// err = esb_start_rx();
    	// if (err) {
    	// 	LOG_ERR("RX setup failed, err %d", err);
    	// 	return 0;
    	// }
        k_work_schedule(&rx_work, K_MSEC(3000));
    
    	return 0;
    }

  • Hello,

    I have asked team about this migration issue. I will get back to you lately. Sorry for late reply as its summer vacation time here. 

    Thanks.

    BR
    Kazi

Related