EmbeddedRelated.com
Blogs

Getting Started With Zephyr: Bluetooth Low Energy

Mohammed BillooJanuary 29, 2024

Overview

Bluetooth Low Energy (BLE) is one of the most popular communication protocols for IoT devices. While it may share a name with Bluetooth Classic, which is the traditional Bluetooth technology, Bluetooth Low Energy is considered an entirely different technology and is targeted for different devices altogether. The key difference between Bluetooth Classic and BLE is duty cycle. 

Quick Links

BLE achieves its "low energy" characteristic by trying to keep the radio module powered off as much as possible. The radio module is usually the component that draws the most power on an IoT device, and thus keeping it off as much as possible reduces the overall power consumption significantly. On the other hand, Bluetooth Classic aims to keep the radio on as much as possible, to reduce latency or to maximize bandwidth. While the details of BLE are beyond the scope of this blog post, NovelBits (https://novelbits.io/bluetooth-low-energy-ble-complete-guide/) offers a thorough review.

This article is available in PDF format for easy printing

The Zephyr Project RTOS (https://zephyrproject.org/) has a mature BLE subsystem with support for popular devices. In this blog post, I will demonstrate how to configure our application for BLE support, running on a Nordic nRF52840 development kit (https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dk). 

Kconfig

As with most applications based on Zephyr, we need to enable the necessary features using Kconfig. We need to set the following Kconfig options using the prj.conf file:

  • CONFIG_BT: Enables the BLE subsystem.
  • CONFIG_BT_PERIPHERAL: Configures the device to be a BLE peripheral.
  • CONFIG_BT_DEVICE_NAME:  The string that will be advertised as the device name.
  • CONFIG_BT_HCI: Enables the host controller interface subsystem.

Zephyr also contains configuration options that enable common BLE services. For example, the device information service (DIS) provides manufacturer information about the device. The DIS can be enabled by enabling the following configuration options in our application:

  • CONFIG_BT_DIS: Enables the DIS.
  • CONFIG_BT_DIS_MANUF: Sets the name of the device manufacturer, which is advertised as part of the DIS.
  • CONFIG_BT_DIS_MODEL: Sets the model number of the device.
  • CONFIG_BT_DIS_SERIAL_NUMBER: Sets the serial number of the device.

Source Code

Zephyr offers some straightforward macros and functions to allow us to customize our BLE offering in the application. Before we review the source code, we need to review some BLE terminology:

  • GATT: Stands for Generic ATTribute Profile, which specifies the details of the information that the device sends over BLE.
  • Service: Is a collection of information, such as sensor data, to be transmitted over BLE.
  • Characteristic: Where and how the actual information is presented.
  • UUID: Stands for Universally Unique ID, which is a number to identify services and characteristics.
  • Read Permission:  Property associated with iInformation that can be asynchronously queried by a remote device over BLE.
  • Notify Permission: Property associated with information can be immediately notified to a remote device over BLE on a change.

We can implement a custom service and a custom characteristic using a few macros. First, we need to define some base UUIDs, that can serve as the basis for the UUIDs that we can use for our service and characteristics. 

We can define the base UUIDs using the following macros:

#define CUSTOM_BASE_UUID_w32  0x12345678
#define CUSTOM_BASE_UUID_w1   0x90AB
#define CUSTOM_BASE_UUID_w2   0xCDEF
#define CUSTOM_BASE_UUID_w3   0x0123
#define CUSTOM_BASE_UUID_w48  0x456789ABCDEF

We can then define the UUIDs corresponding to our service and characteristics using macros that are available in Zephyr, as shown below:

#define CUSTOM_SERVICE_UUID                   0x1
#define CUSTOM_CHARACTERISTIC_UUID            0x2
#define BT_UUID_CUSTOM_SERVICE             BT_UUID_128_ENCODE(CUSTOM_BASE_UUID_w32, CUSTOM_BASE_UUID_w1, CUSTOM_BASE_UUID_w2, CUSTOM_BASE_UUID_w3, CUSTOM_BASE_UUID_w48 + CUSTOM_SERVICE_UUID)
#define BT_UUID_CUSTOM_CHARACTERISTIC      BT_UUID_128_ENCODE(CUSTOM_BASE_UUID_w32, CUSTOM_BASE_UUID_w1, CUSTOM_BASE_UUID_w2, CUSTOM_BASE_UUID_w3, CUSTOM_BASE_UUID_w48 + CUSTOM_CHARACTERISTIC)

Finally, we can use the above macros to initialize data structures for the UUIDs that will ultimately used to instantiate the BLE service and characteristic.

static struct bt_uuid_128 custom_service_uuid = BT_UUID_INIT_128(BT_UUID_CUSTOM_SERVICE);
static struct bt_uuid_128 custom_characteristic_uuid = BT_UUID_INIT_128(BT_UUID_CUSTOM_CHARACTERISTIC)

Then, we can use macros that Zephyr provides to create our custom service with our custom characteristic.

BT_GATT_SERVICE_DEFINE(custom_service,
    BT_GATT_PRIMARY_SERVICE(&custom_uuid),
    BT_GATT_CHARACTERISTIC(&custom_characteristic_uuid.uuid,
                           BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
                           BT_GATT_PERM_READ,
                           custom_data_callback, NULL, custom_data),
    BT_GATT_CCC(nextiles_motion_notify_cb, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)
);

The following list describes the permission macros and data types defined above:

  • BT_GATT_CHRC_READ: Allows remote devices to read a particular characteristic value.
  • BT_GATT_CHRC_NOTIFY: Allows notification of the corresponding data to a remote device.
  • BT_GATT_PERM_READ: Sets the attribute read permission for the characteristic.
  • custom_data_callback: A function that is invoked when a remote device attempts to read the characteristic.
  • custom_data: An array of bytes used as part of the notification to the remote device.

Responding to a characteristic read and notification can be implemented in the following manner:

static uint8_t custom_data[DATA_LEN];
static ssize_t custom_data_callback(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset)
{
    return bt_gatt_attr_read(conn, attr, buf, len, offset, custom_data, DATA_LEN);
}
void some_thread(void *p1, void *p2, void *p3)
{
    while (1) {
        struct bt_gatt_attr *custom_char = bt_gatt_find_by_uuid(attr_custom_bt_service, 0 &custom_characteristic_uuid);
        bt_gatt_notify(NULL, custom_char, custom_data, sizeof(motion_data));
        k_msleep(MSEC(1000));
    }
}

In this blog post, I showed how we can set up a Zephyr application with BLE support. I showed what configuration options need to be enabled and configured, and I demonstrated how we can use macros and functions provided by Zephyr to configure a custom BLE service and characteristic.



To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: