zephyr-bluetooth-le-conceptos

Bluetooth Low Energy in Zephyr: Basic Concepts

  • 5 min

Bluetooth Low Energy is a short-range wireless protocol optimized for low-power data exchange.

Zephyr provides an open-source Bluetooth stack that includes the Host and a Controller implementation. The combination used depends on the SoC and configuration, and software qualification does not automatically certify the final product.

Bluetooth Low Energy has its own vocabulary and architecture. Before sending data, we need to understand its main components.

BLE Architecture in Zephyr

To understand how the code works, we first need to see how the system is organized internally. The BLE stack is divided into two major parts:

  1. Host: This is the logical (software) part. It handles profiles, security, and high-level data (GAP, GATT). In Zephyr, this runs as part of the Kernel.
  2. Controller: This is the part that communicates with the radio hardware. It manages the exact timing of packet transmission and reception.

In a chip like the nRF52 or ESP32-C3, the Host and Controller run on the same CPU. In more complex configurations, they can be on separate chips communicating via UART (HCI). Zephyr transparently supports both models.

Basic Dictionary: GAP and GATT

If you’re starting with BLE, it’s helpful to understand these two acronyms from the beginning.

GAP (Generic Access Profile)

It defines who connects to whom. It is the “bouncer” of the club. It defines the roles:

  • Central: The one that initiates and scans (usually the Phone).
  • Peripheral: The device that announces its presence and consumes little power (your IoT sensor).

It also defines the Advertising process. Before connecting, the Peripheral must be periodically shouting: “I’m here! My name is Zephyr! I have these services!”.

GATT (Generic Attribute Profile)

It defines how data is exchanged once connected. It is the “waiter” who brings the dishes.

  • Server: The one that has the data (your sensor).
  • Client: The one that requests or writes data (the Phone).

Common confusion: Generally, the Peripheral (GAP) acts as the Server (GATT), and the Central acts as the Client. But technically, the GATT roles could be swapped.

Project Configuration (prj.conf)

To enable Bluetooth in Zephyr, we need to activate the subsystem in the configuration. It’s a bit more verbose than in Arduino, but it gives us full control over the binary size.

# 1. Activate the Bluetooth subsystem
CONFIG_BT=y

# 2. Define the role (We want to be a Peripheral)
CONFIG_BT_PERIPHERAL=y

# 3. Device name (What we will see on the phone)
CONFIG_BT_DEVICE_NAME="Zephyr Demo"
Copied!

Stack Initialization

In our main.c code, the first step is to include the headers and start the stack. Unlike Serial.begin(), in Zephyr initialization is asynchronous (although we can wait for it to complete).

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

int main(void)
{
    int err;

    /* Initialize the Bluetooth stack */
    /* Pass NULL for synchronous operation (blocks until started)
       or a callback function if we want to be notified later */
    err = bt_enable(NULL);

    if (err) {
        printk("Failed to initialize Bluetooth (err %d)\n", err);
        return err;
    }

    printk("Bluetooth initialized successfully\n");

    /* ... here we can start advertising ... */
    return 0;
}
Copied!

The Concept of UUID

In BLE, everything has a unique 128-bit identifier called a UUID.

  • Standard Services: They have short 16-bit UUIDs (e.g., 0x180D is Heart Rate).
  • Custom Services: We must generate our own long UUIDs (to avoid conflicts with those from Philips or Garmin).

Zephyr has macros to define this easily, but we’ll see it in depth when we create our own service. For now, we’ll focus on making the device “visible”.

Hello World in BLE: Advertising

The “Hello World” in Bluetooth is not sending data; it’s making your phone detect the device. To do this, we need to configure the Advertising packet.

In legacy advertising, each advertising data packet has a maximum of 31 bytes. Extended advertising allows larger payloads when both the controller and observer support it.

  1. Flags: Indicate if the device is “General Discoverable”.
  2. Name: The complete or short name.
  3. UUIDs: List of services we offer (optional).
/* Define the advertising packet (Advertisement Data) */
static const struct bt_data ad[] = {
    /* Flags: General Discoverable (visible) and BR/EDR Not Supported (BLE only) */
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),

    /* Full device name */
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};

int main(void)
{
    int err = bt_enable(NULL);
    if (err) {
        printk("Failed to initialize Bluetooth (err %d)\n", err);
        return err;
    }

    /* Start advertising */
    /* BT_LE_ADV_CONN: Connectable (the phone can request to connect) */
    err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);

    if (err) {
        printk("Failed to start advertising (err %d)\n", err);
        return err;
    }

    printk("Advertising started... Find me on your phone!\n");

    /* The main thread can sleep, the radio controller works autonomously */
    while (1) {
        k_sleep(K_FOREVER);
    }
}
Copied!

Testing Tools: nRF Connect

To test this, we don’t need to write an Android/iOS App yet. Download the nRF Connect for Mobile application (from Nordic Semiconductor). It’s the Swiss Army knife for debugging BLE.

  1. Compile and flash your board.
  2. Open nRF Connect on your phone.
  3. Scan.
  4. You should see a device named “Zephyr Demo”.