BLE with Zephyr on an nRF52

Nov, 23 2021

A lot of this section will be borrowed from an article posted on Nordic's Dev Zone describing how to configure a custom peripheral in Zephyr. If you get stuck, it's a great reference.

We'll be assuming you already have a Zephyr environment set up, if not you can follow this post on how to configure your workspace.

Start by copying the blinky sample into your workspace (for me that is in the ~/ncs/eric_applications directory). You can rename the blinky folder anything you like.

I'll be naming this project led_ws2812 as I am controlling LEDs with this project.

In the src folder create a folder named services. In services create two files (1) led_service.c (2) led_service.h

Your workspace directory should now look like the following: workspace directory

Our project will have one service and one characteristic. The characteristic will be write only.

You can think about a peripheral BLE device as a database. The phone acts as a client that reads and writes information to the database.

The service and characteristic acts as the peripheral device's API. But, don't get too caught up with these details right now.

The characteristic is what is actually being written to in order to update the database. A single service may contain multiple characteristics.

In our application, we'll define the following scheme for controlling LED colors:

4 bytes

  • Byte 0 = The led index (0...24)
    • OR if Byte 0 = 255 turn on "partyin' mode"
  • Byte 1 = The red intensity (0...255)
  • Byte 2 = The green intensity (0...255)
  • Byte 3 = The blue intensity (0...255)

zephyr ble diagram

Open up led_service.h and add the following code:

#include <zephyr/types.h> #include <stddef.h> #include <string.h> #include <errno.h> #include <zephyr.h> #include <soc.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/conn.h> #include <bluetooth/uuid.h> #include <bluetooth/gatt.h> #define LED_SERVICE_UUID 0xd4, 0x86, 0x48, 0x24, 0x54, 0xB3, 0x43, 0xA1, \ 0xBC, 0x20, 0x97, 0x8F, 0xC3, 0x76, 0xC2, 0x75 #define RX_CHARACTERISTIC_UUID 0xA6, 0xE8, 0xC4, 0x60, 0x7E, 0xAA, 0x41, 0x6B, \ 0x95, 0xD4, 0x9D, 0xCC, 0x08, 0x4F, 0xCF, 0x6A /** @brief Callback type for when new data is received. */ typedef void (*data_rx_cb_t)(uint8_t *data, uint8_t length); /** @brief Callback struct used by the led_service Service. */ struct led_service_cb { /** Data received callback. */ data_rx_cb_t data_rx_cb; }; // Public functions: int led_service_init(void); struct led_rgb* get_led_data(void); bool is_fresh_data(void); void new_data_serviced(void); bool is_partying_time(void);

Next, head on over to led_service.c and add the following:

#include <zephyr/types.h> #include <stddef.h> #include <string.h> #include <errno.h> #include <sys/printk.h> #include <sys/byteorder.h> #include <zephyr.h> #include <soc.h> #include <drivers/led_strip.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/conn.h> #include <bluetooth/uuid.h> #include <bluetooth/addr.h> #include <bluetooth/gatt.h> #include "led_service.h" #define BT_UUID_MY_SERVICE BT_UUID_DECLARE_128(LED_SERVICE_UUID) #define BT_UUID_MY_SERVICE_RX BT_UUID_DECLARE_128(RX_CHARACTERISTIC_UUID) #define MAX_TRANSMIT_SIZE 240 struct led_rgb led_data[25]; bool fresh_data = false; bool party_mode = false; uint8_t data_rx[MAX_TRANSMIT_SIZE]; // Public functions: int led_service_init(void) { int err = 0; memset(&data_rx, 0, MAX_TRANSMIT_SIZE); memset(&led_data, 0x00, sizeof(led_data)); return err; } // Return the latest LED array struct led_rgb* get_led_data(void){ return led_data; } // Indicate to the main function that the LED array has changed bool is_fresh_data(void){ return fresh_data; } // Who's trying to party? bool is_partying_time(void){ return party_mode; } // Did we make our update? void new_data_serviced(void){ fresh_data = false; } /* This function is called whenever the RX Characteristic has been written to by a Client */ static ssize_t on_receive(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { // Data coming in off the BLE write const uint8_t * buffer = buf; printk("Received data, handle %d, conn %p, data: 0x", attr->handle, conn); for(uint8_t i = 0; i < len; i++){ printk("%02X", buffer[i]); } printk("\n"); // 0x01 ff 00 ff -- example write format uint8_t led_index = buffer[0]; // Did we send a party command? if(led_index == 255){ party_mode = true; } // If not, pull in the data for the LED index struct led_rgb color; color.r = buffer[1]; color.g = buffer[2]; color.b = buffer[3]; // Stuff it in our global array led_data[led_index] = color; printk("led number %d was set with r: %d, g: %d, b: %d \n", led_index, color.r, color.g, color.b); fresh_data = true; return len; } /* This function is called whenever the CCCD register has been changed by the client*/ static void on_cccd_changed(const struct bt_gatt_attr *attr, uint16_t value) { ARG_UNUSED(attr); switch(value) { case BT_GATT_CCC_NOTIFY: // Start sending stuff! break; case BT_GATT_CCC_INDICATE: // Start sending stuff via indications break; case 0: // Stop sending stuff break; default: printk("Error, CCCD has been set to an invalid value"); } } /* LED Service Declaration and Registration */ BT_GATT_SERVICE_DEFINE(led_service, BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERVICE), BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERVICE_RX, BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, NULL, on_receive, NULL), BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), );

Now, open up the CMakeLists.txt file in your root workspace directory. For me this file would be located here:

eric_applications/led_ws2812/CMakeLists.txt

Replace everything in the CMakeLists.txt file with the following code:

# # Copyright (c) 2018 Nordic Semiconductor # # SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic # cmake_minimum_required(VERSION 3.13.1) include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) project(NONE) # NORDIC SDK APP START target_sources(app PRIVATE src/main.c src/services/led_service.c ) # NORDIC SDK APP END zephyr_library_include_directories(.)

Then, head over the prj.conf file. For me this would be located here:

eric_applications/led_ws2812/prj.conf

Add the necessary Bluetooth configuration settings to your project:

# Bluetooth configuration CONFIG_BT=y CONFIG_BT_DEBUG_LOG=y CONFIG_BT_MAX_CONN=1 CONFIG_BT_L2CAP_TX_BUF_COUNT=5 CONFIG_BT_PERIPHERAL=y CONFIG_BT_DEVICE_NAME="UBIQUE" CONFIG_BT_DEVICE_APPEARANCE=962 CONFIG_HEAP_MEM_POOL_SIZE=2048 # This example requires more workqueue stack CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048

Finally finish out the main application by adding the following code to main.c:

#include <errno.h> #include <string.h> #include <zephyr.h> #include <sys/util.h> #include <zephyr/types.h> #include <stddef.h> #include <sys/printk.h> #include <sys/byteorder.h> #include <drivers/gpio.h> #include <soc.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/conn.h> #include <bluetooth/uuid.h> #include <bluetooth/gatt.h> #include "services/led_service.h" // Pull in some properties from the .prj file // Plus various settings #define DELAY_TIME K_MSEC(50) #define DEVICE_NAME CONFIG_BT_DEVICE_NAME #define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1) // A necessary definition for BLE stack static K_SEM_DEFINE(ble_init_ok, 0, 1); // Define the connection we'll be establishing struct bt_conn *my_connection; // Array containing the advertising data static const struct bt_data ad[] = { BT_DATA_BYTES(BT_DATA_UUID128_ALL, LED_SERVICE_UUID), BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), }; // Array containing the scan response data static const struct bt_data sd[] = { BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), }; // What happens when we connect to the device static void connected(struct bt_conn *conn, uint8_t err) { struct bt_conn_info info; char addr[BT_ADDR_LE_STR_LEN]; my_connection = conn; if (err) { printk("Connection failed (err %u)\n", err); return; } else if(bt_conn_get_info(conn, &info)) { printk("Could not parse connection info\n"); } else { bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); printk("Connection established! \n\ Connected to: %s \n\ Role: %u \n\ Connection interval: %u \n\ Slave latency: %u \n\ Connection supervisory timeout: %u \n" , addr, info.role, info.le.interval, info.le.latency, info.le.timeout); } } // What happens when the device is disconnected static void disconnected(struct bt_conn *conn, uint8_t reason) { printk("Disconnected (reason %u)\n", reason); } // Callback for BLE update request static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param) { //If acceptable params, return true, otherwise return false. return true; } // Callback for BLE parameter update static void le_param_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency, uint16_t timeout) { struct bt_conn_info info; char addr[BT_ADDR_LE_STR_LEN]; if(bt_conn_get_info(conn, &info)) { printk("Could not parse connection info\n"); } else { bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); printk("Connection parameters updated! \n\ Connected to: %s \n\ New Connection Interval: %u \n\ New Slave Latency: %u \n\ New Connection Supervisory Timeout: %u \n" , addr, info.le.interval, info.le.latency, info.le.timeout); } } // Wire up all the callbacks we defined above to the bt struct static struct bt_conn_cb conn_callbacks = { .connected = connected, .disconnected = disconnected, .le_param_req = le_param_req, .le_param_updated = le_param_updated }; // This will be called to initalize our bluetooth stack static void bt_ready(int err) { if (err) { printk("BLE init failed with error code %d\n", err); return; } //Configure connection callbacks bt_conn_cb_register(&conn_callbacks); //Initalize services err = led_service_init(); if (err) { printk("Failed to init lightbox (err:%d)\n", err); return; } //Start advertising err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); if (err) { printk("Advertising failed to start (err %d)\n", err); return; } printk("Advertising successfully started\n"); k_sem_give(&ble_init_ok); } static void error(void) { while (true) { printk("Error!\n"); k_sleep(K_MSEC(1000)); } } void main(void) { int err = 0; int rc; printk("Starting Nordic Lightbox\n"); err = bt_enable(bt_ready); if (err) { printk("BLE initialization failed\n"); error(); //Catch error } /* Bluetooth stack should be ready in less than 100 msec. \ \ We use this semaphore to wait for bt_enable to call bt_ready before we proceed \ to the main loop. By using the semaphore to block execution we allow the RTOS to \ execute other tasks while we wait. */ err = k_sem_take(&ble_init_ok, K_MSEC(500)); if (!err) { printk("Bluetooth initialized\n"); } else { printk("BLE initialization did not complete in time\n"); error(); //Catch error } }

Build your project to make sure there are no errors:

west build -p auto -b nrf52dk_nrf52832

Flash it to your dev kit:

west flash

Now let's make sure it took. Using your mobile phone, head to your app store and download an app called Lightblue.

Open it up and swipe down to scan for devices. If everything worked, you should see a device named "UBIQUE" being advertised.

Notice that this is the same string as defined in our prj.conf file.

This is then passed to the advertising data via a macro ⤵️

#define DEVICE_NAME CONFIG_BT_DEVICE_NAME 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, DEVICE_NAME, DEVICE_NAME_LEN), };

You should now be able to connect to your device!

The on_receive function in led_service.c will be called whenever you write to your characteristic using Lightblue.

/* This function is called whenever the RX Characteristic has been written to by a Client */ static ssize_t on_receive(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) {