BLE 5.0 Extended Advertising: Larger Payloads and Multiple Advertising Sets

BLE 5.0 extended advertising packet structure showing larger payloads and multiple advertising sets

Every firmware engineer who’s worked with BLE advertising has hit the same wall: 31 bytes. You need to broadcast a sensor reading with metadata, a manufacturer-specific data block with a device signature, or a URI that’s just a little too long, and you’re stuffing bits into a payload that was sized in 2010 for a very different world. You resort to ugly workarounds: splitting data across advertising and scan response, rotating payloads on a timer, or just truncating what you actually need to send.

What’s worse: while you’re juggling that one cramped advertising set, you also can’t advertise different data to different audiences simultaneously. Need a connectable advertisement for your app AND a non-connectable beacon for asset tracking? Legacy BLE says pick one.

BLE 5.0’s extended advertising solves both problems. It gives you payloads up to ~1,650 bytes and multiple concurrent advertising sets, each with independent parameters, PHY selection, and addressing. This article walks through the protocol mechanics, then shows you how to implement it on both the nRF5 SDK (SoftDevice S140) and Zephyr RTOS, with code you can drop into a project today.

Prerequisites: you’re comfortable with GAP roles and have configured legacy advertising before. If you need a refresher, check the BLE Protocol Deep Dives pillar.

How Extended Advertising Moves Data Off the Primary Channels

In legacy advertising, an ADV_IND PDU carries your payload directly on channels 37, 38, and 39. The entire payload (ad data, flags, name) must fit in 31 bytes. One advertising set, one PHY (1M), no exceptions.

Extended advertising redesigns this with a two-tier architecture. The primary channels now carry a pointer, not payload. The actual data moves to secondary (data) channels.

Here’s the flow:

PRIMARY CHANNELS (37, 38, 39)         SECONDARY DATA CHANNELS (0–36)
┌─────────────────────┐               ┌──────────────────────────┐
│   ADV_EXT_IND       │  AuxPtr ───►  │   AUX_ADV_IND            │
│   (no ad data)      │               │   (up to 255 bytes data)  │
│   PHY: 1M or Coded  │               │   PHY: 1M, 2M, or Coded  │
└─────────────────────┘               └──────────┬───────────────┘
                                                  │ AuxPtr (if chained)
                                                  ▼
                                      ┌──────────────────────────┐
                                      │   AUX_CHAIN_IND          │
                                      │   (next fragment)         │
                                      └──────────────────────────┘

ADV_EXT_IND goes out on the three primary channels. It contains zero advertising data, just an AuxPtr field that tells scanners which secondary data channel to tune to, the time offset, and which PHY to use.

AUX_ADV_IND is transmitted on the indicated secondary channel. This PDU carries your actual advertising data, up to 255 bytes in a single auxiliary PDU. If your payload is larger, the AUX_ADV_IND includes its own AuxPtr pointing to an AUX_CHAIN_IND, which carries the next fragment. Chains can continue up to roughly 1,650 bytes total, though the exact limit is controller-dependent.

PHY independence is a key detail. The primary channel PDU can use 1M or Coded PHY. The secondary channel can independently use 1M, 2M, or Coded PHY. This means you can use 2M on the secondary channel to blast through a large payload faster, reducing radio-on time, while still using 1M on the primary for broad scanner compatibility. Each advertising set chooses its own PHY combination.

The host stack handles fragmentation transparently via HCI. You hand it a buffer (say, 500 bytes), and the controller splits it across AUX_ADV_IND + AUX_CHAIN_IND packets. You don’t manage chaining yourself.

What Advertising Sets Actually Are (And Why You Want Several)

An advertising set is an independent advertising state machine with its own:

  • Handle — controller-assigned identifier
  • Address — type and value (can differ per set)
  • Advertising data and scan response data
  • Parameters — interval, PHY, TX power, advertising type (connectable, scannable, non-connectable)

The controller time-division multiplexes sets across the radio. They’re logically independent: starting, stopping, or updating one doesn’t affect the others.

Practical limits are hardware-dependent. The nRF52840 with SoftDevice S140 supports roughly 4–6 concurrent sets depending on memory configuration. In Zephyr, you control this with CONFIG_BT_EXT_ADV_MAX_ADV_SET.

Real use cases make this concrete:

  • Set 1: Connectable extended advertising on 1M PHY. Broadcasts device name and GATT service UUIDs for phone app pairing.
  • Set 2: Non-connectable, non-scannable on 2M PHY. Broadcasts a 200-byte sensor payload every 5 seconds for nearby gateways.
  • Set 3: Non-connectable on Coded PHY. Long-range beacon for asset tracking across a warehouse.

All three run concurrently on the same radio.

FeatureLegacy AdvertisingExtended Advertising
Max payload31 bytes~1,650 bytes
Concurrent sets1Multiple (HW-dependent)
PHY options1M only1M, 2M, Coded
Channels used37, 38, 3937–39 + data channels
BLE 4.x compatibleYesNo
Scan response31 bytes~1,650 bytes

Implementation on nRF5 SDK with SoftDevice S140

This targets nRF5 SDK 17.x with SoftDevice S140 v7.x on nRF52840. Extended advertising is configured through ble_gap_adv_set_configure() and the extended-specific fields in ble_gap_adv_params_t.

The following snippet sets up two concurrent advertising sets: one non-connectable extended set with a 100-byte payload on 2M secondary PHY, and a second with different data.

#include "ble_gap.h"
#include "nrf_sdh_ble.h"
#include <string.h>

static uint8_t adv_handle_1 = BLE_GAP_ADV_SET_HANDLE_NOT_SET;
static uint8_t adv_handle_2 = BLE_GAP_ADV_SET_HANDLE_NOT_SET;

// 100-byte manufacturer-specific payload
static uint8_t adv_data_1[100];
static uint8_t adv_data_2[60];

static ble_gap_adv_data_t gap_adv_data_1;
static ble_gap_adv_data_t gap_adv_data_2;

void ext_adv_init(void)
{
    ret_code_t err;

    // --- Advertising Set 1: 100-byte payload, 2M secondary PHY ---
    memset(adv_data_1, 0xAA, sizeof(adv_data_1)); // placeholder payload

    gap_adv_data_1.adv_data.p_data = adv_data_1;
    gap_adv_data_1.adv_data.len    = sizeof(adv_data_1);
    gap_adv_data_1.scan_rsp_data.p_data = NULL;
    gap_adv_data_1.scan_rsp_data.len    = 0;

    ble_gap_adv_params_t adv_params_1 = {0};
    adv_params_1.properties.type  = BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED;
    adv_params_1.primary_phy      = BLE_GAP_PHY_1MBPS;
    adv_params_1.secondary_phy    = BLE_GAP_PHY_2MBPS;
    adv_params_1.interval         = MSEC_TO_UNITS(200, UNIT_0_625_MS);
    adv_params_1.duration         = 0; // advertise indefinitely
    adv_params_1.filter_policy    = BLE_GAP_ADV_FP_ANY;

    err = sd_ble_gap_adv_set_configure(&adv_handle_1, &gap_adv_data_1, &adv_params_1);
    APP_ERROR_CHECK(err);

    // --- Advertising Set 2: 60-byte payload, Coded secondary PHY ---
    memset(adv_data_2, 0xBB, sizeof(adv_data_2));

    gap_adv_data_2.adv_data.p_data = adv_data_2;
    gap_adv_data_2.adv_data.len    = sizeof(adv_data_2);
    gap_adv_data_2.scan_rsp_data.p_data = NULL;
    gap_adv_data_2.scan_rsp_data.len    = 0;

    ble_gap_adv_params_t adv_params_2 = {0};
    adv_params_2.properties.type  = BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED;
    adv_params_2.primary_phy      = BLE_GAP_PHY_CODED;
    adv_params_2.secondary_phy    = BLE_GAP_PHY_CODED;
    adv_params_2.interval         = MSEC_TO_UNITS(1000, UNIT_0_625_MS);
    adv_params_2.duration         = 0;

    err = sd_ble_gap_adv_set_configure(&adv_handle_2, &gap_adv_data_2, &adv_params_2);
    APP_ERROR_CHECK(err);

    // Start both sets
    err = sd_ble_gap_adv_start(adv_handle_1, BLE_CONN_CFG_TAG_DEFAULT);
    APP_ERROR_CHECK(err);
    err = sd_ble_gap_adv_start(adv_handle_2, BLE_CONN_CFG_TAG_DEFAULT);
    APP_ERROR_CHECK(err);
}

Common pitfalls on nRF5 SDK:

  • Handle initialization: Always initialize handles to BLE_GAP_ADV_SET_HANDLE_NOT_SET so the SoftDevice auto-assigns them. Reusing a handle without stopping the set first causes NRF_ERROR_INVALID_STATE.
  • Memory allocation: Each advertising set consumes SoftDevice RAM. If you run out, sd_ble_gap_adv_set_configure returns NRF_ERROR_NO_MEM. Increase NRF_SDH_BLE_GAP_EVENT_LENGTH and total SoftDevice RAM allocation in sdk_config.h.
  • Connectable extended sets count against NRF_SDH_BLE_PERIPHERAL_LINK_COUNT. Size it correctly or your connection will be rejected after the advertisement succeeds.

Implementation on Zephyr RTOS

Targeting Zephyr v3.5+. First, enable extended advertising in your prj.conf:

CONFIG_BT=y
CONFIG_BT_EXT_ADV=y
CONFIG_BT_EXT_ADV_MAX_ADV_SET=2
CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=255

CONFIG_BT_CTLR_ADV_DATA_LEN_MAX is the critical one people miss. It controls the controller’s maximum advertising data buffer. Default is often 31 bytes even with extended advertising enabled.

Here’s dual-set extended advertising in Zephyr:

#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <string.h>

static struct bt_le_ext_adv *adv_set_1;
static struct bt_le_ext_adv *adv_set_2;

static const struct bt_le_adv_param adv_params_1 = {
    .options = BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_NO_2M,
    .interval_min = BT_GAP_ADV_FAST_INT_MIN_2,
    .interval_max = BT_GAP_ADV_FAST_INT_MAX_2,
};

static const struct bt_le_adv_param adv_params_2 = {
    .options = BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_CODED,
    .interval_min = BT_GAP_ADV_SLOW_INT_MIN,
    .interval_max = BT_GAP_ADV_SLOW_INT_MAX,
};

/* 120-byte and 80-byte payloads */
static uint8_t mfg_data_1[120];
static uint8_t mfg_data_2[80];

static const struct bt_data ad_set1[] = {
    BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data_1, sizeof(mfg_data_1)),
};

static const struct bt_data ad_set2[] = {
    BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data_2, sizeof(mfg_data_2)),
};

void start_ext_adv(void)
{
    int err;

    memset(mfg_data_1, 0xAA, sizeof(mfg_data_1));
    memset(mfg_data_2, 0xBB, sizeof(mfg_data_2));

    /* Create and configure set 1 */
    err = bt_le_ext_adv_create(&adv_params_1, NULL, &adv_set_1);
    __ASSERT(err == 0, "Set 1 create failed (err %d)", err);

    err = bt_le_ext_adv_set_data(adv_set_1, ad_set1, ARRAY_SIZE(ad_set1),
                                  NULL, 0);
    __ASSERT(err == 0, "Set 1 data failed (err %d)", err);

    err = bt_le_ext_adv_start(adv_set_1, BT_LE_EXT_ADV_START_DEFAULT);
    __ASSERT(err == 0, "Set 1 start failed (err %d)", err);

    /* Create and configure set 2 */
    err = bt_le_ext_adv_create(&adv_params_2, NULL, &adv_set_2);
    __ASSERT(err == 0, "Set 2 create failed (err %d)", err);

    err = bt_le_ext_adv_set_data(adv_set_2, ad_set2, ARRAY_SIZE(ad_set2),
                                  NULL, 0);
    __ASSERT(err == 0, "Set 2 data failed (err %d)", err);

    err = bt_le_ext_adv_start(adv_set_2, BT_LE_EXT_ADV_START_DEFAULT);
    __ASSERT(err == 0, "Set 2 start failed (err %d)", err);
}

Zephyr-specific gotchas:

  • CONFIG_BT_CTLR_ADV_DATA_LEN_MAX: If you don’t increase this, bt_le_ext_adv_set_data will return -ENOMEM on payloads above 31 bytes even though extended advertising is enabled. This trips up nearly everyone the first time.
  • Board controller support: Not all Zephyr-supported boards have controllers that implement extended advertising. If you’re using an HCI-based split architecture, verify the controller firmware supports it. On Nordic chips with Zephyr’s built-in controller, you’re fine.
  • BT_LE_ADV_OPT_NO_2M: If omitted, the secondary PHY defaults to 2M when available. Set this flag if you want to force 1M on secondary. Use BT_LE_ADV_OPT_CODED for Coded PHY.

Trade-offs and Practical Guidance

Power consumption scales with what you’re asking the radio to do. Each advertising set consumes its own radio time slots. A rough rule: each additional non-connectable set at a 1-second interval adds approximately 15–30 µA on an nRF52840, depending on payload size and PHY. Larger payloads on secondary channels keep the radio on longer per advertising event. Using 2M PHY on the secondary channel reduces this, since the payload transmits in half the time compared to 1M.

BLE 4.x scanners cannot see extended advertisements. The ADV_EXT_IND PDU type is undefined in the 4.x spec, so legacy scanners simply ignore it. If you must support older scanners, run a legacy advertising set alongside your extended sets. BLE 5.0 explicitly supports this: one legacy set plus one or more extended sets concurrently.

Data updates within a single advertising set are atomic. The controller swaps the entire buffer at the next advertising event boundary when you call the set-data API. Updates across different sets are not coordinated. If you need consistency across sets, stop them, update, and restart.

The scanning side matters too. A BLE 5.0 scanner must have extended scanning enabled to follow the AuxPtr to secondary channels. In Zephyr, this means using BT_LE_SCAN_OPT_CODED or ensuring extended scan is active. In the nRF SDK, use ble_gap_scan_params_t with extended set to 1.

For debugging, the nRF Connect mobile app (both iOS and Android) decodes extended advertising PDUs and shows the auxiliary channel data. For over-the-air analysis, Wireshark with the nRF Sniffer v4 captures extended PDUs including chain fragments. This is invaluable for verifying your payload is being transmitted as expected.

Putting This Into Your Next Firmware Build

Extended advertising gives you two capabilities that directly affect what you can ship: payloads up to ~1,650 bytes (practically, 255 bytes covers most use cases without chaining), and multiple concurrent advertising sets with independent PHY, addressing, and data.

The implementation path is straightforward on both stacks:

  • nRF5 SDK: Call sd_ble_gap_adv_set_configure() with extended type properties, allocate handles with BLE_GAP_ADV_SET_HANDLE_NOT_SET, and bump SoftDevice RAM if you add sets.
  • Zephyr: Enable CONFIG_BT_EXT_ADV, set CONFIG_BT_CTLR_ADV_DATA_LEN_MAX to your actual maximum payload, and create sets with bt_le_ext_adv_create().

If you’re currently working around the 31-byte limit with scan response tricks or payload rotation timers, extended advertising eliminates that complexity. If you’re running a single advertising set and wishing you could serve different audiences, multiple sets solve that cleanly.

Start with one extended set replacing your legacy advertising, verify it with nRF Connect, then add sets as your product requires. For deeper coverage of PHY selection trade-offs and connection parameter optimization, see the BLE Protocol Deep Dives series.


Hubble Network enables BLE devices to transmit data directly to satellites—no gateways, no infrastructure. See how it works →