Your First BLE Advertisement: A Complete Walkthrough

You’ve spent hours getting your toolchain set up, and your reward was a blinking LED. Satisfying for about ninety seconds. Here’s the thing most BLE tutorials won’t tell you upfront: the jump from “blinky” to “wireless device” is far smaller than it looks. The nRF Connect SDK handles the hard parts: the radio timing, the frequency hopping, the packet formatting. Your job is roughly 25 lines of C and three lines of configuration.
By the end of this walkthrough, your nRF52 dev board will broadcast its name over Bluetooth Low Energy, and you’ll see it appear on your phone. No connections, no services, no pairing. Just your board announcing itself to the world. That’s the foundation every BLE product starts from.
What you’ll need:
- An nRF52 DK (nRF52832 or nRF52840) or similar nRF52-based board
- The nRF Connect SDK installed and building (if not, start with the SDK setup guide)
- The free nRF Connect mobile app on your phone (iOS or Android)
- Basic C knowledge and the ability to flash a project to your board
We’ll cover just enough BLE theory to make the code make sense. No more.
What Actually Happens When Your Phone Finds a Bluetooth Device?
Before you write a line of code, you need three concepts. Just three.
Peripheral vs. Central
Think of a peripheral as a shop with a sign in its window. It’s broadcasting a message to anyone who walks by: “I’m here, here’s my name, here’s what I offer.” A central is the person walking down the street, reading signs. The shop doesn’t know who’s reading its sign. It doesn’t care. It just keeps broadcasting.
Your nRF52 board is the shop. Your phone is the person on the street. The board will advertise. The phone will scan. That’s it.
The Advertising State
BLE devices follow a simple state progression. For now, only three states matter:
Standby → Advertising → Connected
Your board starts in standby. You tell it to advertise. It starts broadcasting. If a central (your phone) decides to connect, the device moves to the connected state. This tutorial gets you to Advertising and stops there. Connection is a separate topic for a follow-up.
What’s Inside an Advertisement?
An advertisement is a small, structured packet, just 31 bytes maximum in legacy BLE. That’s not a lot. It gets broadcast repeatedly on three dedicated radio channels (37, 38, and 39), and any scanning device in range can pick it up.
Inside those 31 bytes, you pack structured fields. The most common ones:
- Flags tell scanners about your device’s capabilities (e.g., “I support BLE, I’m not also using classic Bluetooth”)
- Complete Local Name is the human-readable name that shows up in a scanner app
- Service UUIDs advertise what services you offer (not needed yet)
You don’t need to understand the byte-level encoding. The SDK’s macros handle that. Just know that space is tight and every field costs bytes.
One more thing: advertisements can be connectable (inviting a central to connect) or non-connectable (just broadcasting data, like a beacon). Our example will be connectable, since it’s the default and the most common starting point.
[Diagram: nRF52 board icon broadcasting wavy arrows on three channels (37, 38, 39) toward a phone icon. A dashed arrow between them is labeled “Connection (next tutorial!)”]
Setting Up the Project
Create a new application in the nRF Connect SDK, or copy from a blank template. Set your board target, for example nrf52dk_nrf52832 or nrf52840dk_nrf52840.
The only file you need to touch for configuration is prj.conf. Here’s the complete contents:
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="MyFirstBLE"Three lines. Here’s what each one does:
CONFIG_BT=yenables the Bluetooth stack in the build. Without this, none of the BLE APIs exist.CONFIG_BT_PERIPHERAL=ytells the stack your device will act as a peripheral (the advertiser, the shop with the sign).CONFIG_BT_DEVICE_NAME="MyFirstBLE"sets the device name that will appear on your phone’s scanner. Change this to whatever you want.
You shouldn’t need to modify CMakeLists.txt beyond what the template provides.
The Code: Start Advertising in Under 30 Lines
Here’s the entire main.c. Read it first, notice how small it is, then we’ll walk through each piece.
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/gap.h>
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, CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
static void bt_ready(int err)
{
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
printk("Bluetooth initialized\n");
err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Advertising failed to start (err %d)\n", err);
return;
}
printk("Advertising successfully started\n");
}
int main(void)
{
int err = bt_enable(bt_ready);
if (err) {
printk("bt_enable returned (err %d)\n", err);
}
return 0;
}Let’s break it down.
The Includes
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/gap.h>These pull in the SDK’s BLE APIs. The first gives you bt_enable() and bt_le_adv_start(). The second gives you the advertising data type constants like BT_DATA_FLAGS and BT_DATA_NAME_COMPLETE.
Defining the Advertising Data
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, CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};This is what your phone will see when it scans. Two fields:
- Flags:
BT_LE_AD_GENERALmeans “I’m discoverable by anyone.”BT_LE_AD_NO_BREDRmeans “I only support BLE, not classic Bluetooth.” This combination is standard for almost every BLE peripheral. - Complete Local Name: This pulls the name you set in
prj.confviaCONFIG_BT_DEVICE_NAME. Thesizeof(...) - 1drops the null terminator, since BLE ad fields don’t use one.
The BT_DATA_BYTES and BT_DATA macros handle all the byte-level formatting for you. You describe what to advertise; the SDK figures out how.
Initializing the BLE Stack
int err = bt_enable(bt_ready);bt_enable() initializes the Bluetooth stack. This is asynchronous: the stack needs time to configure the radio hardware. You pass it a callback function (bt_ready) that fires when initialization is complete. Don’t try to start advertising before this callback fires; the stack isn’t ready yet.
Starting Advertising
err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);This is where the radio comes alive. The parameters:
BT_LE_ADV_CONN— Use connectable undirected advertising. This means any central can see you and connect.ad, ARRAY_SIZE(ad)— Your advertising data array and its size.NULL, 0— No scan response data. Scan response is a second 31-byte packet a central can request to get more information. It’s useful, but we’re keeping things minimal.
The Main Function
int main(void)
{
int err = bt_enable(bt_ready);
if (err) {
printk("bt_enable returned (err %d)\n", err);
}
return 0;
}Returning from main() in Zephyr doesn’t kill your application. The BLE stack runs on its own threads in the background. Once advertising starts, the stack handles everything: packet timing, channel hopping, retransmission. Your main thread doesn’t need to babysit it. This surprises most beginners: the radio just runs.
See It on Your Phone
Build and flash your project:
west build -b nrf52dk_nrf52832
west flash(Substitute your board target if you’re using an nRF52840 DK or another board.)
Open the nRF Connect app on your phone and tap Scan. Within a few seconds, you should see “MyFirstBLE” appear in the list of discovered devices, along with an RSSI value (signal strength).
[Screenshot: nRF Connect mobile app scan results showing “MyFirstBLE” in the device list with RSSI indicator]
That’s it. Your board is a wireless device.
If It Doesn’t Work
- “I don’t see my device.” Make sure the board is powered and the flash succeeded. Check the serial console for the “Advertising successfully started” message. If you see an error code, the stack didn’t initialize properly.
- “Name shows as ‘N/A’ or ‘Unknown’.” Verify
CONFIG_BT_DEVICE_NAMEis set inprj.confand that your advertising data array includes theBT_DATA_NAME_COMPLETEfield. - “Build errors about Bluetooth symbols.” Confirm
CONFIG_BT=yis in yourprj.conf, you’re targeting a BLE-capable board, and your SDK version is up to date. - “The app shows the device but the name is wrong.” Do a clean build (
west build --pristine) after changingprj.conf. Kconfig changes sometimes don’t propagate on incremental builds.
A Peek Beneath the Surface
Here’s what’s actually happening on the radio. The BLE stack is transmitting your advertising packet on channels 37, 38, and 39, three dedicated advertising channels spread across the 2.4 GHz band to avoid interference. It cycles through them at a default advertising interval (typically around 100 ms to 1 second, depending on SDK defaults for the BT_LE_ADV_CONN preset).
Your phone’s BLE radio periodically scans those same channels. When it hears your packet, it decodes the flags, reads the name field, and shows it in the app. The whole exchange happens without any connection being established. It’s purely one-way broadcast at this stage.
The advertising interval, transmit power, and payload contents are all configurable. The defaults are sensible for development. Tuning them matters for production (battery life, discovery speed), but not right now.
Since our advertisement is connectable, tapping “Connect” in the app would establish a connection. But since we haven’t defined any GATT services or characteristics, there’s nothing useful to do once connected. That’s exactly what the next tutorial on adding your first GATT service covers.
Next Steps: From Broadcast to Useful Product
You’ve crossed a real threshold. Your board isn’t a blinking LED anymore; it’s a BLE peripheral, discoverable by any Bluetooth device in range. Here’s where to go from here:
Customize the advertising payload. You can add service UUIDs, manufacturer-specific data, TX power level, and more. The advertising data fields are highly configurable. See our guide on configuring BLE advertisement payloads for a deeper dive.
Add scan response data. Scan response is a second 31-byte packet a central can request, effectively doubling your advertising data. You’d pass it as the fourth parameter to bt_le_adv_start() instead of NULL.
Add GATT services. To make a connection actually do something, you need services and characteristics, the structured data model of BLE. That’s the natural next article in this series on adding your first GATT service.
Experiment with non-connectable advertising. Change BT_LE_ADV_CONN to BT_LE_ADV_NCONN and you’ve got a beacon, broadcasting data with no intention of connecting. Useful for sensor broadcasts, indoor positioning, and asset tracking.
For now, try the simplest experiment: change CONFIG_BT_DEVICE_NAME in prj.conf to something new, rebuild, flash, and watch it update on your phone. That feedback loop, changing code and seeing the result on a real wireless device, is the foundation you’ll build every BLE project on.
Hubble Network enables BLE devices to transmit data directly to satellites—no gateways, no terrestrial infrastructure. See how it works →