Building Your First BLE Beacon from Scratch with an nRF54L15 Dev Kit

You’ve built BLE projects on the nRF52840. Maybe the nRF5340 too. You know west build, you know Kconfig, you’ve called bt_le_adv_start() plenty of times. But here’s the thing: most developers who’ve shipped BLE products can’t describe what actually goes over the air when they start advertising. They know the API, not the packet. The nRF54L15 DK just landed on your desk, and that makes this the perfect excuse to build a beacon the right way: understanding every byte between your C code and the 2.4 GHz radio waves.
We’ll build a non-connectable beacon broadcasting custom manufacturer-specific data. Along the way, you’ll get a mental model of BLE advertising PDUs that’ll serve you regardless of which Nordic chip you’re targeting.
nRF54L15 Quick Orientation for nRF52/53 Developers
The nRF54L15 looks familiar from the SDK side but is a different animal underneath.
Dual-core architecture: an Arm Cortex-M33 application core paired with a RISC-V coprocessor. The RISC-V core handles peripheral tasks and can run while the M33 sleeps. You don’t need to think about it for a beacon project, but it matters for power optimization later.
The peripheral interconnect is new too. The nRF54L15 replaces the older PPI with DPPI (Distributed Programmable Peripheral Interconnect). DPPI introduces PPIB bridges that connect peripheral domains across the chip, so peripherals in different power domains can trigger each other without waking the CPU. There’s also CRACEN, a dedicated crypto engine replacing the nRF5340’s CryptoCell.
What doesn’t change: the Zephyr/nRF Connect SDK BLE Host and Controller API surface. Your bt_le_adv_start() calls look identical. The SDK handles the silicon differences below the API.
Board target matters. For the nRF54L15 DK, you’ll use:
nrf54l15dk/nrf54l15/cpuappThat three-level path (board/SoC/core) is how nRF Connect SDK v2.7+ identifies build targets. Get it wrong and the build fails with confusing errors. Minimum SDK version: v2.7.0.
The devicetree structure has shifted too. If you want to blink an LED to indicate advertising (which we will), you’ll reference the DK’s LEDs through the standard Zephyr gpio_dt_spec pattern, same as nRF52/53, just with different node paths in the .dts files.
What Actually Goes Over the Air
BLE advertising happens on exactly 3 channels: 37, 38, and 39, at 2402 MHz, 2426 MHz, and 2480 MHz respectively.
2.4 GHz ISM Band — BLE Channel Map
────────────────────────────────────────────────────────────────
2402 MHz 2426 MHz 2480 MHz
▼ ▼ ▼
CH37 CH38 CH39
[ADV] |..data channels 0-10..| [ADV] |..11-36..| [ADV]
◄── 2404–2424 MHz ──► ◄─ 2428-2478 ─►They’re deliberately spread across the band to dodge Wi-Fi channels 1, 6, and 11. When your beacon advertises, it transmits the same PDU on all three channels in rapid succession.
Here’s the PDU structure for ADV_NONCONN_IND (Bluetooth Core Spec Vol 6, Part B, §2.3.1.3):
┌──────────┬────────────┬──────────────────────────────────────┬─────┐
│ Preamble │ Access Addr│ PDU │ CRC │
│ 1 byte │ 4 bytes │ │ 3 B │
│ (0xAA) │(0x8E89BED6)│ │ │
└──────────┴────────────┼────────┬─────────┬───────────────────┼─────┘
│ Header │ AdvA │ AdvData │
│ 2 B │ 6 B │ 0–31 bytes │
│PDU Type│(Device │ │
│ = 0x02 │ Addr) │ │
└────────┴─────────┼───────┬───────┬───┘
│ AD #1 │ AD #2 │...
└───────┴───────┘The preamble, access address, and CRC are fixed for all advertising PDUs. You never set these; the radio hardware handles them.
The part you control is AdvData, which contains AD structures in Length-Type-Value format:
┌─────┬──────┬──────────────┐
│ Len │ Type │ Value │
│ 1 B │ 1 B │ (Len-1) B │
├─────┼──────┼──────────────┤
│0x02 │ 0x01 │ 0x06 │ ← Flags: LE General Disc. + BR/EDR Not Supported
├─────┼──────┼──────────────┤
│0x07 │ 0xFF │ <Company ID> │ ← Manufacturer Specific Data
│ │ │ <Payload...> │
└─────┴──────┴──────────────┘We pick ADV_NONCONN_IND (PDU type 0b0010) because a beacon doesn’t accept connections. This tells scanners not to send connect requests.
The SDK’s BT_DATA() and BT_DATA_BYTES() macros map directly onto these LTV triplets. Each macro call produces one AD structure, bridging your C code and the bytes on the wire.
Project Setup
You’ll need nRF Connect SDK v2.7+, an nRF54L15 DK, and either nRF Connect for VS Code or command-line west.
Create a project folder with CMakeLists.txt:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrf54_beacon)
target_sources(app PRIVATE src/main.c)Your prj.conf:
# Core BLE stack
CONFIG_BT=y
CONFIG_BT_BROADCASTER=y
CONFIG_BT_DEVICE_NAME="nRF54-Beacon"
# Strip what we don't need
CONFIG_BT_PERIPHERAL=n
CONFIG_BT_CENTRAL=n
CONFIG_BT_OBSERVER=n
CONFIG_BT_CONN_CTX=n
# Logging (helpful during dev, disable for production)
CONFIG_LOG=y
CONFIG_BT_LOG_LEVEL_INF=yDisabling PERIPHERAL, CENTRAL, and OBSERVER shrinks your firmware image and reduces RAM usage. A beacon only broadcasts; it doesn’t need connection or scanning support.
No devicetree overlay is strictly required. The DK’s default .dts gives you LEDs and buttons out of the box.
Writing the Beacon Firmware
Here’s the complete main.c, kept tight but annotated where it matters.
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(beacon, LOG_LEVEL_INF);
/* Custom payload: Nordic's company ID (0x0059) + 4 bytes of data */
static const uint8_t mfg_data[] = {
0x59, 0x00, /* Company ID: Nordic Semiconductor (little-endian) */
0xDE, 0xAD, 0xBE, 0xEF /* Your payload goes here */
};
/* Advertising data: these become AD structures in AdvData */
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_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)),
};
/* Scan response: device name (sent only if scanner sends SCAN_REQ) */
static const struct bt_data sd[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
void main(void)
{
int err;
err = bt_enable(NULL);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)", err);
return;
}
LOG_INF("Bluetooth initialized");
/* Non-connectable, non-scannable advertising
* Interval: 100ms min, 150ms max (in 0.625ms units: 160 / 240) */
struct bt_le_adv_param adv_param = BT_LE_ADV_PARAM_INIT(
BT_LE_ADV_OPT_USE_IDENTITY, /* Use device's public/static address */
160, /* 100ms min interval */
240, /* 150ms max interval */
NULL /* No directed advertising */
);
err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (err) {
LOG_ERR("Advertising failed to start (err %d)", err);
return;
}
LOG_INF("Beacon started");
}Here’s how the code maps to the PDU:
Code (bt_data array) → Over-the-Air Bytes
─────────────────────────────────────────────────────
BT_DATA_BYTES( → AD Structure #1:
BT_DATA_FLAGS, → Len=0x02, Type=0x01
BT_LE_AD_GENERAL | → Value=0x06
BT_LE_AD_NO_BREDR)
BT_DATA( → AD Structure #2:
BT_DATA_MANUFACTURER_DATA, → Len=0x07, Type=0xFF
mfg_data, → Value=0x5900 (Nordic ID)
sizeof(mfg_data)) → + 4 bytes payloadEach BT_DATA call creates one LTV triplet. The SDK calculates the length byte for you.
The nRF54L15 supports TX power levels from -20 dBm to +8 dBm, different from the nRF52 series range. Set it via Kconfig with CONFIG_BT_CTLR_TX_PWR or at runtime through HCI commands. For a beacon, start at 0 dBm and adjust based on range needs.
Build, Flash, and Verify
Build:
west build -b nrf54l15dk/nrf54l15/cpuappFlash:
west flashIf the build fails, the two most common causes are: wrong board target string (check for typos in that three-part path), or outdated DK firmware. Open nRF Connect for Desktop’s Programmer app and update the DK’s J-Link firmware if needed.
Three ways to verify your beacon:
1. nRF Connect for Mobile (Android/iOS). Open the app, start scanning, and look for “nRF54-Beacon.” Tap it to see the advertising data parsed: Flags = 0x06, Manufacturer Specific Data with company ID 0x0059 and your 4-byte payload. This is the fastest sanity check.
2. A second DK running the scanner sample. Build Zephyr’s observer sample for another DK and watch the serial output. Your beacon’s address and advertising data will appear in the log, confirming the PDU content without needing a phone.
3. nRF Sniffer for Wireshark. Flash an nRF52840 Dongle with the sniffer firmware, open Wireshark, and filter for advertising packets. You’ll see the raw bytes matching the PDU diagram from earlier: preamble, access address, header with PDU type 0x02, your device address, then the AD structures byte-for-byte. Cross-reference them with your bt_data arrays. Everything should line up.
Tuning and Extending the Beacon
Advertising interval is your biggest power lever. The 100-150ms interval we set is reasonable for quick discovery. Bump it to 1000ms and battery life improves dramatically, but scanners take longer to find you. Profile the difference with Nordic’s Power Profiler Kit II.
Extended advertising lets you break past the 31-byte AdvData limit. The nRF54L15 supports BLE 5.4, so you can use BT_LE_ADV_OPT_EXT_ADV in your advertising parameters to send payloads up to 254 bytes per advertising set, useful for richer sensor data.
Want to encode iBeacon format? It’s the same LTV structure with Apple’s company ID (0x004C), a specific type byte (0x02), and a 21-byte payload containing UUID, major, minor, and TX power. Swap out the mfg_data array and you’re there.
If you’re thinking about connecting your beacon to a larger network, the Hubble device SDK supports BLE devices on Nordic silicon and includes reference firmware worth studying. For understanding how advertising packets are structured for network consumption, the advertising packet documentation covers the format details.
From bt_data to Radio Waves
You’ve got a working nRF54L15 beacon, but more importantly, you can trace every byte from your bt_data array through the PDU structure to the radio waves on channels 37, 38, and 39. When you debug advertising issues on any BLE project (wrong flags, truncated data, scanners not finding your device) you’ll know exactly where to look. The SDK API is consistent across nRF52, nRF53, and nRF54, so chip migrations come down to updating a board target string and checking a few Kconfig values.
Hubble Network enables your BLE beacons to reach the cloud from anywhere—no gateways, no backhaul infrastructure. See how it works →