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

  • Did you test the sample in my previous reply? Are you able to send the 0xFFFF packets there?

    Best regards,

    Edvin

  • Yes, I tested the 802154_phy_test sample without any custom modifications.

    The sample builds and runs, and I can access the shell commands (e.g. custom channel, custom rx start).
    However, I am not able to confirm successful transmission or reception of 0xFFFF broadcast packets.

    On the RX side, I only see repeated logs like:
    “rf_proc: rx failed error X”
    and I never see any successful RX indication.

    So at the moment, I am not able to verify that 0xFFFF packets are actually transmitted or received,
    even when using the 802154_phy_test sample as-is.

    Best regards,

    Ryan

  • It may be correct, but can you please point to where it says that 0xFFFF packets should be broadcast packets?

    Best regards,

    Edvin

  • 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

Reply
  • 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

Children
No Data
Related