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
Step 1: Set up your projects build configuration
Step 2: Enable the Bluetooth host stack
Step 3: Set up connection callbacks
Step 5: Configure Advertisement and Scan Response data
Step 6: Set up the main loop to send an incrementing number every 2000ms
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:
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:
proj.conf should contain the following:
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:
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.
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.
Add the following to bt_ready() in main.c:
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:
We need an initialization function to call from main, add the following function definition to my_service.c:
And the declaration to my_service.h:
We will require at least two transmission buffers, one for transmitting and one for receiving, add the following to my_service.c:
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:
We also need two UUIDs for our TX and RX characteristics, generate them and add them to my_service.h:
We need to declare our UUIDs, add the following to my_service.c:
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:
Next we create functions for sending and receiving data. Add the following to my_service.c:
Add the following function declaration to my_service.h, it is the function we call from main whenever we want to send a notification:
We initialize our service by adding the following to bt_ready() in main.c:
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:
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:
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
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:
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
my_service.c
my_service.h
If you have any questions or feedback please post a new Question in Devzone :)
Top Comments