nRF Connect SDK Bluetooth Low Energy tutorial part 1: Custom Service in Peripheral role

Scope

The goal of this tutorial is to set-up your own custom Bluetooth Low Energy service on an nRF5340 acting in a peripheral role. We will set up a new Bluetooth LE project from scratch. The finished project will be provided as a reference in a zipped folder and can be unzipped anywhere inside the nRF Connect SDK root's subdirectories. 
If you want to use another nRF5 device you can do that, just replace the target board in Part 7: Testing. 

Prerequisites

It is assumed that you’ve already gone through the nRF Connect SDK Tutorial series - Part 0 to 3, and that you have some previous experience with Bluetooth LE. 

This tutorial was created with ncs/nrf v1.2.99-dev1. To use this version of nRF Connect SDK checkout this commit, go to nRF Connect SDK root folder, and type "west update". Alternatively you can use the Toolchain Manager app in nRF Connect For Desktop.

The finished code is provided as a reference at the bottom of this tutorial. 

I suggest you use an editor that allows you to easily find definitions and declarations. My favorite is Microsoft’s Visual Studio Code. By opening the entire nRF Connect SDK folder in VS Code I can jump to definitions and declarations that are otherwise hard to find, and I can easily search through the entire nRF Connect SDK SDK or an arbitrary folder for text. 

Table of content

Scope

Prerequisites

How-to

    Step 1: Set up your projects build configuration

    Step 2: Enable the Bluetooth host stack

    Step 3: Set up connection callbacks

    Step 4: Create your service

    Step 5: Configure Advertisement and Scan Response data

    Step 6: Set up the main loop to send an incrementing number every 2000ms

    Part 7: Testing

Notes

Reference code

    Main.c

    my_service.c

    my_service.h

How-to

Step 1: Set up your projects build configuration

We will need to create a new folder in nRF Connect SDK's root or any of its subdirectories, called my_service that must contain four files; main.c, CMakeLists.txt, Kconfig, and prj.conf, and it must also contain your service source and header file. The main.c file must be placed in a folder called src as its path is referenced in CMakeLists.txt. 
For more information about CMakeLists, Kconfig and prj.conf read nRF Connect SDK Tutorial - Part 2.

Project Structure

|
|---CMakeLists.txt
|---Kconfig
|---prj.conf
|---services
   |---my_service.c
   |---my_service.h
|---src
   |---main.c

CMakeLists should contain the following:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
#
cmake_minimum_required(VERSION 3.8)
include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE)
project(NONE)
# NORDIC SDK APP START
target_sources(app PRIVATE
src/main.c services/my_service.c
)
# NORDIC SDK APP END
zephyr_library_include_directories(.)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

If you want to use SES you can now open the project with the CMake file that you just created.

Kconfig should contain the following:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
#
# Sources Kconfig.zephyr in the Zephyr root directory.
#
# Note: All 'source' statements work relative to the Zephyr root directory (due
# to the $srctree environment variable being set to $ZEPHYR_BASE). If you want
# to 'source' relative to the current Kconfig file instead, use 'rsource' (or a
# path relative to the Zephyr root).
source "$ZEPHYR_BASE/Kconfig.zephyr"
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

proj.conf should contain the following:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
#
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="My_Device"
CONFIG_BT_DEVICE_APPEARANCE=962
CONFIG_HEAP_MEM_POOL_SIZE=2048
# This example requires more workqueue stack
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Other useful configs are:

You can find the complete list of configs at https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/kconfig/index-all.html

Main.c should contain the following includes and defines:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Copyright (c) 2018 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
*/
#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 <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>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

If you want to use SES you can now open the project with the CMake file that you just created.

Step 2: Enable the Bluetooth host stack

You enable the stack by calling: err = bt_enable(bt_ready); in main() of main.c. 

bt_ready is a callback that contains code that you want to run after the Bluetooth host is enabled. For instance, you’ll want to catch any error code that the bt subsystem returns, as well as configure connection callbacks, initialize your service, and start advertising. 

Fullscreen
1
2
3
4
5
6
7
8
9
static void bt_ready(int err)
{
if (err) {
printk("BLE init failed with error code %d\n", err);
return;
}
k_sem_give(&ble_init_ok);
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Step 3: Set up connection callbacks

Add to main.c a ble connection status callback, struct bt_conn_cb, with bt_conn_cb_register(). They are called by the Bluetooth host when one of the following Bluetooth LE events have occurred;

  • A connection has been established
  • A connection has been broken
  • There’s a pending connection parameter update request
  • A connection’s parameter has been updated
  • Remote information has been retrieved from the remote peer.
  • A remote identity has been resolved
  • A connection’s security level has changed.

In this tutorial, we won’t cover any security aspects, so the latter two events are not included, nor will we cover the event where remote information has been retrieved by the peer.
The connection parameter update request callback can return true or false to accept or reject the requested parameters. The callback can also adjust the parameters prior to accepting the request. 


Several bt_conn_cb can be registered to enable other parts of the application to get callbacks, like your services. By passing callback handlers with NULL you can opt-out of receiving certain callbacks, this is useful if you’re only interested in handling a subset of the callbacks in a part of the application, like the connected and disconnected events. 

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct bt_conn *my_connection;
static void connected(struct bt_conn *conn, u8_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));
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Add the following to bt_ready() in main.c:

Fullscreen
1
2
//Configure connection callbacks
bt_conn_cb_register(&conn_callbacks);
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The application is now set up to forward Bluetooth LE events to our event handlers in main.c.

Step 4: Create your service

Right now we have an empty header and source file my_service.c and my_service.h, we start by adding the following snippet to both the source and header file:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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 <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/addr.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We need an initialization function to call from main, add the following function definition to my_service.c:

Fullscreen
1
2
3
4
5
6
7
8
9
int my_service_init(void)
{
int err = 0;
memset(&data_rx, 0, MAX_TRANSMIT_SIZE);
memset(&data_tx, 0, MAX_TRANSMIT_SIZE);
return err;
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

And the declaration to my_service.h:

Fullscreen
1
int my_service_init(void);
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We will require at least two transmission buffers, one for transmitting and one for receiving, add the following to my_service.c:

Fullscreen
1
2
3
#define MAX_TRANSMIT_SIZE 240
u8_t data_rx[MAX_TRANSMIT_SIZE];
u8_t data_tx[MAX_TRANSMIT_SIZE];
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We need a 128-bit UUID for our service, I used https://uuidonline.net/ to generate my UUID, 75c276c3-8f97-20bc-a143-b354244886d4.
Add it to my_service.h:

Fullscreen
1
2
3
/*Note that UUIDs have Least Significant Byte ordering */
#define MY_SERVICE_UUID 0xd4, 0x86, 0x48, 0x24, 0x54, 0xB3, 0x43, 0xA1, \
0xBC, 0x20, 0x97, 0x8F, 0xC3, 0x76, 0xC2, 0x75
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We also need two UUIDs for our TX and RX characteristics, generate them and add them to my_service.h:

Fullscreen
1
2
3
4
5
#define RX_CHARACTERISTIC_UUID 0xA6, 0xE8, 0xC4, 0x60, 0x7E, 0xAA, 0x41, 0x6B, \
0x95, 0xD4, 0x9D, 0xCC, 0x08, 0x4F, 0xCF, 0x6A
#define TX_CHARACTERISTIC_UUID 0xED, 0xAA, 0x20, 0x11, 0x92, 0xE7, 0x43, 0x5A, \
0xAA, 0xE9, 0x94, 0x43, 0x35, 0x6A, 0xD4, 0xD3
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We need to declare our UUIDs, add the following to my_service.c: 

Fullscreen
1
2
3
#define BT_UUID_MY_SERIVCE BT_UUID_DECLARE_128(MY_SERVICE_UUID)
#define BT_UUID_MY_SERIVCE_RX BT_UUID_DECLARE_128(RX_CHARACTERISTIC_UUID)
#define BT_UUID_MY_SERIVCE_TX BT_UUID_DECLARE_128(TX_CHARACTERISTIC_UUID)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We need to define and register our service and its characteristics. By using the following helper macro we statically register our Service in our BLE host stack.
Add the following to my_service.c:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* LED Button Service Declaration and Registration */
BT_GATT_SERVICE_DEFINE(my_service,
BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERIVCE),
BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERIVCE_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_CHARACTERISTIC(BT_UUID_MY_SERIVCE_TX,
BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ,
NULL, NULL, NULL),
BT_GATT_CCC(lbslc_ccc_cfg_changed,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Next we create functions for sending and receiving data. Add the following to my_service.c:

Fullscreen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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,
u16_t len,
u16_t offset,
u8_t flags)
{
const u8_t * buffer = buf;
printk("Received data, handle %d, conn %p, data: 0x", attr->handle, conn);
for(u8_t i = 0; i < len; i++){
printk("%02X", buffer[i]);
}
printk("\n");
return len;
}
/* This function is called whenever a Notification has been sent by the TX Characteristic */
static void on_sent(struct bt_conn *conn, void *user_data)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Add the following function declaration to my_service.h, it is the function we call from main whenever we want to send a notification:

Fullscreen
1
void my_service_send(struct bt_conn *conn, const u8_t *data, uint16_t len);
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We initialize our service by adding the following to bt_ready() in main.c:

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX


You can find the complete reference code at the end of this tutorial. 

Step 5: Configure Advertisement and Scan Response data

We want to advertise as connectable, with the device name, stop advertising when connected, and allow anyone to connect or request scan data. We also want to advertise for a minimum interval of 100ms and a maximum of 1000ms. 
Add the following to bt_ready() in main.c:

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Now that we’ve got the advertising params we want, we will set the advertising data.

The advertising data format is an array of bt_data type. We want to advertise some flags,  our name, and a UUID of a service that we support. This will require two sets of bt_data structs because we can’t fit both our full name and UUID in a single advertisement packet, we will therefore have use scan response data to fit our second bt_data struct.

First, we need to set our advertising flags, BT_LE_AD_GENERAL and BT_LE_AD_NO_BREDR.
BT_LE_AD_GENERAL means we’re advertising indefinitely, as opposed to BT_LE_AD_LIMITED. BT_LE_AD_NO_BREDR means that we do not support BR/EDR (aka BT classic).

The definitions from Advertisement Data Types and Flags are found in hci.h.

Add the following to main.c:

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The define DEVICE_NAME is set in proj.conf as the define CONFIG_BT_DEVICE_NAME.

Step 6: Set up the main loop to send an incrementing number every 2000ms

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX


Part 7: Testing

The snippets of code are not provided in sequential order and you will need to move them around a bit to allow the compiler to include them accordingly, see the reference code for comparison. 

Build the application, either using SES or command line: west build -b nrf5340_dk_nrf5340_cpuapp

Flash the application, either using SES or command line: west flash. 

You also need to build and flash the Bluetooth LE Controller stack that runs on the network core. Build and flash the sample in <..>\ncs\zephyr\samples\bluetooth\hci_rpmsg.


To test your code you will need a Bluetooth LE client that is able to interact with your service, the nRF Connect app for mobile or desktop is suited for this task. You will also need to open a UART terminal and connect to the nRF5340 DK.

Our device is named "My_Device" and it will advertise as soon as the Bluetooth host stack has been initialized. Connect to it and enable notifications for the TX characteristic. You should now see that the client is receiving a number that increments every 2 seconds. 

You should also write to the RX characteristic in the client app and be able to observe the data printed out in the UART terminal. 

Notes

In main you will see the use of a semaphore:

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This semaphore is used as a blocking global flag. By using the Zephyr kernel's semaphore implementation instead of a variable such as a bool, we allow the RTOS to prioritize the execution of another thread or putting the CPU to sleep while we wait for the semaphore to become available. This is used throughout the Zephyr RTOS and sample applications. 
See https://docs.zephyrproject.org/1.9.0/kernel/synchronization/semaphores.html for more details. 


Reference code

Main.c

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

my_service.c

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

my_service.h

Fullscreen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

If you have any questions or feedback please post a new Question in Devzone :) 

ncs_ble_tutorial_part1.zip