Is BLE + raw IEEE 802.15.4 TX (PHY only, no Thread/Zigbee) officially supported on nRF52840 with MPSL?

Hello Nordic team,

I am working on an nRF52840 project using nRF Connect SDK (Zephyr-based, v2.x / v3.x) and I would like to clarify the official support status of a specific multiprotocol use case.

I have intentionally limited this question to two focused points to avoid ambiguity.

1. Target Use Case

  • Device: nRF52840

  • SDK: nRF Connect SDK (Zephyr)

  • Protocols:

    • Bluetooth LE (SoftDevice Controller)

    • IEEE 802.15.4 PHY only (raw TX)

      • No Thread

      • No Zigbee

      • No MAC / NET stack usage

  • Radio sharing via MPSL (dynamic multiprotocol)

The 802.15.4 side is used only for custom raw frame transmission (driver-level TX), not for any standardized 802.15.4-based stack.


2. Observations So Far

  • BLE and IEEE 802.15.4 TX can both be enabled and initialized.

  • A single 802.15.4 TX call works while BLE is active.

  • However, periodic or continuous TX loops do not behave reliably:

    • TX returns success (0)

    • But repeated transmissions do not appear on a sniffer

    • No explicit runtime error is reported

  • All development was done using the nRF IEEE 802.15.4 radio driver + MPSL, not the legacy raw radio HAL.


3. Documentation Gap

From available documentation and samples, I understand that:

  • nRF52840 officially supports BLE + Thread/Zigbee concurrency via MPSL.

  • Multiprotocol support is clearly documented for full 802.15.4 stacks.

  • However, I cannot find an explicit statement or reference confirming:

    BLE + custom raw IEEE 802.15.4 TX (PHY only) is officially supported and validated


4. Questions (Primary Scope)

  1. Is BLE + raw IEEE 802.15.4 TX (PHY only, no Thread/Zigbee) an officially supported use case on nRF52840 when using MPSL?

  2. If supported in principle, are there any known limitations or design constraints (e.g. scheduling, TX frequency, timeslot behavior) that would explain why periodic TX may silently stop or not appear on a sniffer?

I am intentionally not asking for code review or full implementation guidance yet.
I would like to first understand the official support boundary and expectations.


5. Next Steps

Depending on the answers to the two questions above, I plan to:

  • Adjust the architecture (e.g. move to Thread/Zigbee, or dedicated timeslot handling), or

  • Continue debugging within the supported configuration and ask follow-up implementation questions.

Thank you for your time and clarification.

Best regards,

Ryan

Parents
  • Hello,

    You should be able to use 802154_phy_test together with MPSL+BLE. It is only BLE that has hard timing requirements, while the rest of the time can be spent on 802154. We don't have any samples showing exactly how to do this, but I imagine that you can take any of the ble+Zigbee/Thread samples, strip out the zigbee/thread, and replace it with what you find in NCS\nrf\samples\peripheral\802154_phy_test, and go from there.

    For follow up questions, please don't use chatGPT or other AI tools to format your questions. It makes it difficult to know what you are really asking for, and what the AI made up.

    Best regards,

    Edvin

  • Hello.

    I understand that there is no official reference code available for this specific use case.
    As a first step, I am trying to implement periodic TX using IEEE 802.15.4 only, but even this basic periodic transmission is not working yet.

    If I upload the source code I have been working on, would it be possible to receive support or feedback on it?

    Best regards,

    Ryan

  • Hello.

    I implemented the 0xFFFF broadcast frame as you suggested.

    **Frame structure (19 bytes total):**
    - PHR: 0x13 (19 bytes)
    - FCF: 0x41 0x88 (Data, no ACK, short dest, no src)
    - Seq: incrementing
    - Dst PAN: 0xFF 0xFF
    - Dst Addr: 0xFF 0xFF
    - Payload: 10 bytes (0xA0-0xA9)

    [log]
    00> ============================================
    00> IEEE 802.15.4 Broadcast TX Test
    00> NCS v3.2.1 - 0xFFFF Broadcast
    00> ============================================
    00>
    00> Step 1: Init radio
    00> Step 2: Set channel 20, power 0 dBm
    00> Step 3: Set PAN ID to 0xFFFF
    00> Step 4: Start RX mode
    00> Ready! Starting TX loop...
    00>
    00> [1102] TX seq=1, len=19... Queued
    00> [2103] TX seq=2, len=19... ERROR 1
    00> [3103] TX seq=3, len=19... ERROR 1
    00> [4103] TX seq=4, len=19... ERROR 1
    00> [5104] TX seq=5, len=19... ERROR 1
    00> [6104] TX seq=6, len=19... ERROR 1
    00> [7104] TX seq=7, len=19... ERROR 1
    00> [8105] TX seq=8, len=19... ERROR 1
    00> [9105] TX seq=9, len=19... ERROR 1
    00> [10105] TX seq=10, len=19... ERROR 1
    00>
    00> === Sent 10 packets ===

    Best regards,

    Ryan

    #include <zephyr/kernel.h>
    #include <zephyr/logging/log.h>
    #include <zephyr/sys/printk.h>
    #include <nrf_802154.h>
    
    LOG_MODULE_REGISTER(ieee802154_tx, LOG_LEVEL_INF);
    
    #define TX_INTERVAL_MS 1000
    #define PAYLOAD_SIZE   10
    
    static uint8_t tx_frame[128];
    static uint8_t seq = 0;
    
    void mpsl_assert_handle(const char *const file, const uint32_t line)
    {
        printk("MPSL ASSERT: %s:%u\n", file, line);
        k_panic();
    }
    
    void nrf_802154_transmitted_raw(
        uint8_t *p_frame,
        const nrf_802154_transmit_done_metadata_t *p_metadata)
    {
        printk("\n>>> TX SUCCESS (seq=%u) <<<\n\n", p_frame[3]);
    }
    
    void nrf_802154_transmit_failed(
        uint8_t *p_frame,
        nrf_802154_tx_error_t error,
        const nrf_802154_transmit_done_metadata_t *p_metadata)
    {
        printk("\n>>> TX FAILED (seq=%u, err=%d) <<<\n\n", p_frame[3], error);
    }
    
    void nrf_802154_received_raw(uint8_t *p_data, int8_t power, uint8_t lqi)
    {
        printk("RX packet received\n");
        nrf_802154_buffer_free_raw(p_data);
    }
    
    void nrf_802154_receive_failed(nrf_802154_rx_error_t error, uint32_t id)
    {
        ARG_UNUSED(error);
        ARG_UNUSED(id);
    }
    
    void nrf_802154_cca_done(bool channel_free)
    {
        ARG_UNUSED(channel_free);
    }
    
    void nrf_802154_cca_failed(nrf_802154_cca_error_t error)
    {
        ARG_UNUSED(error);
    }
    
    void nrf_802154_energy_detected(const nrf_802154_energy_detected_t *p_result)
    {
        ARG_UNUSED(p_result);
    }
    
    void nrf_802154_energy_detection_failed(nrf_802154_ed_error_t error)
    {
        ARG_UNUSED(error);
    }
    
    int main(void)
    {
        printk("\n============================================\n");
        printk("IEEE 802.15.4 Broadcast TX Test\n");
        printk("NCS v3.2.1 - 0xFFFF Broadcast\n");
        printk("============================================\n\n");
        
        k_msleep(500);
        
        printk("Step 1: Init radio\n");
        nrf_802154_init();
        k_msleep(100);
        
        printk("Step 2: Set channel 20, power 0 dBm\n");
        nrf_802154_channel_set(20);
        nrf_802154_tx_power_set(0);
        
        printk("Step 3: Set PAN ID to 0xFFFF\n");
        uint8_t pan[2] = {0xFF, 0xFF};
        nrf_802154_pan_id_set(pan);
        
        printk("Step 4: Start RX mode\n");
        if (!nrf_802154_receive()) {
            printk("ERROR: Failed to start RX!\n");
            return -1;
        }
        
        k_msleep(500);
        printk("Ready! Starting TX loop...\n\n");
        
        while (1) {
            uint8_t idx = 0;
            seq++;
            
            // PHR: Length
            tx_frame[idx++] = 19;  // FCF(2) + Seq(1) + PAN(2) + Addr(2) + Payload(10) + FCS(2)
            
            // FCF
            tx_frame[idx++] = 0x41;  // Data, no ACK
            tx_frame[idx++] = 0x88;  // Short dest, no src
            
            // Sequence
            tx_frame[idx++] = seq;
            
            // Dst PAN = 0xFFFF
            tx_frame[idx++] = 0xFF;
            tx_frame[idx++] = 0xFF;
            
            // Dst Addr = 0xFFFF
            tx_frame[idx++] = 0xFF;
            tx_frame[idx++] = 0xFF;
            
            // Payload
            for (uint8_t i = 0; i < PAYLOAD_SIZE; i++) {
                tx_frame[idx++] = 0xA0 + i;
            }
            
            printk("[%u] TX seq=%u, len=%u... ", k_uptime_get_32(), seq, tx_frame[0]);
            
            nrf_802154_transmit_metadata_t metadata = {
                .frame_props = {
                    .is_secured = false,
                    .dynamic_data_is_set = true
                },
                .cca = false,
                .tx_power = {
                    .use_metadata_value = false,
                    .power = 0
                },
                .tx_channel = {
                    .use_metadata_value = false,
                    .channel = 20
                },
                .tx_timestamp_encode = false
            };
            
            nrf_802154_tx_error_t err = nrf_802154_transmit_raw(tx_frame, &metadata);
            
            if (err != NRF_802154_TX_ERROR_NONE) {
                printk("ERROR %d\n", err);
            } else {
                printk("Queued\n");
            }
            
            k_msleep(TX_INTERVAL_MS);
            
            if (seq % 10 == 0) {
                printk("\n=== Sent %u packets ===\n\n", seq);
            }
        }
        
        return 0;
    }

  • Hello,

    It has been a really long time since I last looked into this, but nrf_802154_transmit_raw() is something that you need to handle very carefully in your application when you don't use a protocol on top of it. If you read the description carefully from:

    v3.2.1\modules\hal\nordic\drivers\nrf_802154\common\include\nrf_802154.h:

    /**
     * @brief Changes the radio state to @ref RADIO_STATE_TX.
     *
     * @note If the CPU is halted or interrupted while this function is executed,
     *       @ref nrf_802154_transmitted_raw or @ref nrf_802154_transmit_failed can be called before this
     *       function returns a result.
     *
     * @note This function is implemented in zero-copy fashion. It passes the given buffer pointer to
     *       the RADIO peripheral.
     *
     * @note Setting @p tx_timestamp_encode to true is only allowed if
     *       @ref NRF_802154_TX_TIMESTAMP_PROVIDER_ENABLED is enabled.
     *       If this condition is not met, any attempt to transmit a frame will fail unconditionally.
     *
     * In the transmit state, the radio transmits a given frame. If requested, it waits for
     * an ACK frame. Depending on @ref NRF_802154_ACK_TIMEOUT_ENABLED, the radio driver automatically
     * stops waiting for an ACK frame or waits indefinitely for an ACK frame. If it is configured to
     * wait, the MAC layer is responsible for calling @ref nrf_802154_receive or
     * @ref nrf_802154_sleep after the ACK timeout.
     * The transmission result is reported to the higher layer by calls to @ref nrf_802154_transmitted_raw
     * or @ref nrf_802154_transmit_failed.
     *
     * @verbatim
     * p_data
     * v
     * +-----+-----------------------------------------------------------+------------+
     * | PHR | MAC header and payload                                    | FCS        |
     * +-----+-----------------------------------------------------------+------------+
     *       |                                                                        |
     *       | <---------------------------- PHR -----------------------------------> |
     * @endverbatim
     *
     * @param[in]  p_data      Pointer to the array with data to transmit. The first byte must contain
     *                         frame length (including FCS). The following bytes contain data.
     *                         The CRC is computed automatically by the radio hardware. Therefore,
     *                         the FCS field can contain any bytes.
     * @param[in]  p_metadata  Pointer to metadata structure. Contains detailed properties of data
     *                         to transmit. If @c NULL following metadata are used:
     *                         Field           | Value
     *                         ----------------|-----------------------------------------------------
     *                         @c frame_props  | @ref NRF_802154_TRANSMITTED_FRAME_PROPS_DEFAULT_INIT
     *                         @c cca          | @c true
     *                         @c tx_timestamp_encode | @c false
     *
     * @retval NRF_802154_TX_ERROR_NONE  The TX request was successful.
     *                                   Transmit success or failure will be indicated by the callout.
     *
     * @returns Error that prevented the frame from being transmitted.
     *          No callout will be called.
     */
    nrf_802154_tx_error_t nrf_802154_transmit_raw(uint8_t                              * p_data,
                                                  const nrf_802154_transmit_metadata_t * p_metadata);

    So first of all, after the first packet (TX seq=1), you should not call nrf_802154_transmit_raw() again before you receive any of the callbacks.

    If you stop sending packet after the first nrf_802154_transmit_raw(), do you see any of the interrupts then? You should see either nrf_802154_transmitted() or nrf_802154_transmit_failed() occur once. As far as I know, this is the way openthread and Zigbee uses it.

    See if that helps.

    Best regards,

    Edvin

  • According to IEEE 802.15.4, a broadcast data frame is defined by a destination short address of 0xFFFF, while still using a valid PAN ID and including a source address.
    The previous version of the code omitted the source address and used PAN ID 0xFFFF, which is not a valid broadcast data frame configuration.
    The updated code below uses a standard data frame with short destination and source addresses, destination address 0xFFFF, ACK disabled, and a valid PAN ID.

    [change code]

    /*
    * PHR length = MHR + Payload + FCS(2)
    * MHR = FCF(2) + Seq(1) + DstPAN(2) + DstAddr(2) + SrcAddr(2)
    */
    tx_frame[idx++] = 2 + 1 + 2 + 2 + 2 + PAYLOAD_SIZE + 2;

    /* FCF: Data frame
    * - Data frame
    * - No ACK request
    * - Short destination address
    * - Short source address
    */
    tx_frame[idx++] = 0x41;
    tx_frame[idx++] = 0x88;

    /* Sequence number */
    tx_frame[idx++] = seq;

    /* Destination PAN ID = local PAN */
    tx_frame[idx++] = 0x34;
    tx_frame[idx++] = 0x12; // PAN ID = 0x1234

    /* Destination Address = Broadcast */
    tx_frame[idx++] = 0xFF;
    tx_frame[idx++] = 0xFF;

    /* Source Address */
    tx_frame[idx++] = 0x01;
    tx_frame[idx++] = 0x00;

    Based on the results from the various test cases so far, it does not appear that the issue is caused by an incorrectly constructed broadcast frame.

    Best regards,

    Ryan

  • Edvin said:
    If you stop sending packet after the first nrf_802154_transmit_raw(), do you see any of the interrupts then? You should see either nrf_802154_transmitted() or nrf_802154_transmit_failed() occur once. As far as I know, this is the way openthread and Zigbee uses it.

    Did you test queuing up only one packet, and see if you get any of the callbacks?

    Best regards,

    Edvin

  • Yes. transmit_raw() returns success (err=0), but no callback is triggered at all.

    Best regards,

    Ryan

Reply Children
  • Can you please zip and upload the entire application folder (Not just some of the files)? This way I can try to reproduce what you are seeing. Are you only missing the replies when you use the 0xFFFF address? Or is it for all messages?

    Best regards,

    Edvin

  • Sure, I will zip the entire application folder (including all files) and upload it here shortly.

    Regarding your question, the issue happens not only with 0xFFFF, but also with unicast messages.
    In other words, I am missing replies for all messages.

    Best regards,

    Ryan

    802154_tx_test.zip

  • This is the log from the unmodified application that you sent:

    So apparently, nrf_802154_transmitted() is being triggered.

    However, I do see in your main loop:

            if (!nrf_802154_transmit_raw(tx_frame, &metadata)) {
                LOG_ERR("TX rejected! Attempting driver reset...");                
            }

    But if nrf_802154_transmit_raw returns NRF_802154_TX_ERROR_NONE (=0), then it is a success. I did some changes, but mostly in error checking and logging. Try the attached project, and let me know the log output:

    802154_tx_test2.zip

    Note that I enabled UART logging in prj.conf. Disable it if you don't need it.

    Best regards,

    Edvin

  • It looks like the issue was related to the RF performance of the nRF52840 DK board.
    After switching to our custom board, everything is working properly and the RF behavior is stable.

    It was a bit unexpected and confusing at first, but now the root cause seems clear.
    Thank you very much for your support and guidance throughout the debugging process — it was really helpful.

    Best regards,

    Ryan

    00> *** Booting nRF Connect SDK v3.2.1-d8887f6f32df ***
    00> *** Using Zephyr OS v4.2.99-ec78104f1569 ***
    00> [00:00:00.000,305] <inf> ieee802154_tx: === Step 1: Starting ===
    00> [00:00:00.100,463] <inf> ieee802154_tx: === Step 2: Initializing Radio ===
    00> [00:00:00.200,836] <inf> ieee802154_tx: === Step 3: Setting Channel ===
    00> [00:00:00.300,964] <inf> ieee802154_tx: === Step 4: Setting TX Power ===
    00> [00:00:00.401,092] <inf> ieee802154_tx: === Step 5: Setting Promiscuous Mode ===
    00> [00:00:00.501,220] <inf> ieee802154_tx: === Step 6: Entering RX Mode ===
    00> [00:00:00.501,373] <inf> ieee802154_tx: RX mode entered successfully.
    00> [00:00:01.001,495] <inf> ieee802154_tx: === Radio Ready! Starting TX Loop ===
    00> 
    00> [00:00:01.001,586] <inf> ieee802154_tx: TX Packet #1 [Dest: 0xFFFF, Src: 0x1111, PAN: 0x2435]
    00> [00:00:01.001,678] <inf> ieee802154_tx: Transmit request accepted. (ret = 0)
    00> [00:00:01.003,601] <inf> ieee802154_tx: >>> TX Success!
    00> ]
    00> 
    00> 
    00> 
    00> [00:00:28.007,690] <inf> ieee802154_tx: TX Packet #28 [Dest: 0xFFFF, Src: 0x1111, PAN: 0x2435]
    00> [00:00:28.007,812] <inf> ieee802154_tx: Transmit request accepted. (ret = 0)
    00> [00:00:28.009,735] <inf> ieee802154_tx: >>> TX Success!
    00> [00:00:29.007,965] <inf> ieee802154_tx: TX Packet #29 [Dest: 0xFFFF, Src: 0x1111, PAN: 0x2435]
    00> [00:00:29.008,056] <inf> ieee802154_tx: Transmit request accepted. (ret = 0)
    00> [00:00:29.009,979] <inf> ieee802154_tx: >>> TX Success!
    00> [00:00:30.008,178] <inf> ieee802154_tx: TX Packet #30 [Dest: 0xFFFF, Src: 0x1111, PAN: 0x2435]
    00> [00:00:30.008,300] <inf> ieee802154_tx: Transmit request accepted. (ret = 0)
    00> [00:00:30.010,223] <inf> ieee802154_tx: >>> TX Success!
    00> [00:00:31.008,453] <inf> ieee802154_tx: TX Packet #31 [Dest: 0xFFFF, Src: 0x1111, PAN: 0x2435]
    00> [00:00:31.008,544] <inf> ieee802154_tx: Transmit request accepted. (ret = 0)

Related