søndag den 11. august 2019

BLE From Scratch part 3: Advertising

BLE advertising


Previous installment can be found here

A BLE peripheral needs to advertise in order for other devices (such as phones or computers - central devices) to connect to it, so this operation is fundamental to the operation of the device. Advertising means sending out small packets of data on three fixed channels at minimum 20 ms apart. This data can contain various information, such as the name of the device, which service(s) it supports or manufacturer specific data that allows someone with knowledge of the data format to discern features or state from the device.

For bluetooth versions prior to 5.0, advertising packets can contain between 0 and 31 bytes of payload. Bluetooth 5.0 expanded this via a new feature called "advertising extensions" but for maximum compatibility, the Bluetooth 4.x format is described here.

When a device is advertising, another device can send a "scan request" to obtain more information than can be held in the 31 bytes. This allows for another 31 bytes to be sent from the peripheral to the scanning  central device.

The advertising system is one-way only (peripheral to central) and can only contain limited data, hence it serves only to carry enough information for a central to know whether it should connect to the device or not.
The format of the data that can be sent in the advertising and scan response packets are described in the Bluetooth specification Vol 3, Part C section 11, and the types of data that can be sent is defined in the Bluetooth Assigned Numbers document. In short, it is a simple tagged format where each block of data has a length byte as header, followed by a Data Type byte and then N bytes of data depending on the data type. The length of the data blocks must add up to 31 bytes or less as described above. Exceeding this means that you will have to reshuffle/move data to the scan response block instead. 

All this may seem like a lot of detail, but when you only have 2x31 bytes to play with, you have to put some thought into what to put in there. For example, we would like to advertise our UI service which has a 16 byte UUID - that will eat 18 bytes of our 31 bytes (1 for length, 1 for type and the 16 bytes UUID). Advertising two custom UUIDS can not be done in one packet, so therefore if your application supports several proprietary services you usually only advertise one (or use 32-bit shortened UUIDs, but support for that on older phones can be spotty).

Building the Advertise packet - and switching to C++

The SoftDevice expects two pointers to advertise and scan response data when setting up advertising. It is therefore up to the user of the SoftDevice to build the packets to be sent according to the specification.

Building the data structure can create a lot of boilerplate, simply because C doesn't lend itself very well to expressing the build-up of a hierachy of objects in a data structure. Therefore as per Part 3, the project is switched to C++. Note that I am a huge fan of designated initializers, which is a C99 feature that only appear in C++ in a future version, but is supported already now in the latest versions of both CLANG and GCC. Hence these will be used in the code I post here for readability reasons.

Now - the intention is not to use all the advanced features of C++, but the data structures are constructed in BLE is really object oriented and forcing it into C is backwards:

  • An advertise packet consists of a number of AD data structures of various types, which again can contain other objects such as strings (for name) or UUIDs of various types.
  • The GATT database (to be described in next installment) consists of a number of Services, which in turn consists of a number of Characteristics, that may or may not have Descriptors attached, and that have various properties depending on their use. 
By writing a few simple class libraries to encapsulate those relationships, a huge amount of boiler plate can be avoided in the actual application.

For building AD data, I created this simple library: adv_helper.h.

This library allows constructing the AD data like this:



So - as can be seen by the code - we construct an AdvData object, add a Flags field (which should always be there in any connectable advertise), add the device name with true meaning that the device name is the full name and not shortened, and add the list of services, with false meaning that the list is incomplete and true meaning that it is 128 bit UUIDs (this library does not support 32 bit UUIDs - that is left as an excercise for the reader). ui_service is a variable from the uuids.cc file:

I group all UUIDs in one place since we will need them in more than one spot once we start to build the GATT database.

If you decide to inspect the data in the debugger, you will see that the 128-bit UUID is byte-reversed. This is due to the rule that all numbers are sent little-endian in BLE, and the UUID is seen as a 128 bit number. Similarly there is no NULL terminator on strings. All strings in BLE are UTF-8 and have their length encoded implicitly by the data structures around it - in the case of AD data, by the length field of the AD data block.

As it can be seen here the scan data is empty.

Configuring the SoftDevice as a simple peripheral

Before starting advertising, the softdevice is configured to be a simple peripheral, i.e. one that only allows one incoming connection. To do this, we use the following snippet of code.



Here it can be seen why I am such a huge fan of the designated initalizers. It allows setup of complex structs (in this case for feeding to the sd_ble_cfg_set() ) without loosing sight of what is actually the meaning of all the values.

The first struct enables 1 link with 5 ms connection event length. The connection event length determines how many BLE packets that can be sent in one connection event, and therefore ultimately how high bandwidth that is allowed.

The next struct sets up the maximum transfer unit size of the ATT protocol. This allows larger chunks of data to be read/written without fragmentation on the ATT layer, and hence allows us to use the longer connection events.

Third struct restricts us to one advertising set and one connection in the peripheral role.

Fourth configuration sets the GAP device name for the mandatory GAP service.

Then BLE is enabled, and amount of free RAM is reported. It is necessary to increase the RAM for the softdevice slightly from 8K to 10K to accommodate the configuration listed above.

Configuring and starting advertising

To configure advertising another large struct has to be populated. This aspect is not very well documented, so below you can see the commented version that allows advertising fast and indefinitely. Note that in a real application you will want to throttle advertising after some time to not waste battery, but we will return to this aspect in a future blog:



As it can be seen, setting up data for the advertising configuration is the most involved part of this code, and is pure boiler plate. Note that when a central device connects, the internal buffers in the softdevice holding the information on what and how to advertise is freed, so it is necessary to reconfigure advertising again after a disconnect.

This code is enough to start advertising, but as soon as you will try to connect to the device, the softdevice will start to emit events that needs handling.

Minimal event handling

To handle events, you loop over sd_ble_evt_get() until no more events can be found, and then you handle them one by one:


You minimally have to handle five events:

BLE_EVT_GAP_DISCONNECTED

This event is issued when the remote device is disconnected for some reason. The peripheral then need to restart advertising.

BLE_EVT_GAP_CONNECTED

Not strictly required to handle, but nice to have a debug printout that a connection was established

BLE_EVT_GAP_PHY_UPDATE_REQUEST

This event means that the remote device has requested an update of the PHY. BLE 5 can run on both 1Mbit and 2Mbit phy. Range and power consumption is slightly lower on 2MBit, but in this example we reply that we would like to stay on 1Mbit. iOS devices will issue this request on all connections, hence it needs to be handled in order to avoid timeouts.

BLE_EVT_GAP_PHY_UPDATE

This event is issued when the new PHY setup is in effect. Again it is good for debugging.

BLE_EVT_GATTS_EXCHANGE_MTU_REQUEST

This request is issued by virtually all connecting devices and will have to be answered. If the requested MTU is less than the maximum capability of your peripheral, you have to report that value back - otherwise report you own capability. The effective MTU will be the minimum of the incoming and your own.

By supporting these five events you can run a connection without disconnects, but also without any information in the GATT database. That we will address in the next installment which can be found here


lørdag den 3. august 2019

BLE from scratch part 2: Integrating the softdevice

Continued from part 1

The SoftDevice

A SoftDevice in Nordic lingo is a binary that is installed in the lower parts of flash and RAM in the SoC. The SoftDevice contains the bluetooth stack and hooks for building your application on top. There are several flavors, but the one we will be using here is the S112 which is the simplest of them, and which is meant to build peripherals - the kind of BLE application we try to build here.

The various SoftDevices can be found in the SDK in components/softdevice. To ease the setup in the project, we define a Project Macro called SoftDevice:

Open the project options, make sure to choose the common configuration. Then choose 
build->project macros and set up a macro like this:


The SoftDeviceDir should point to the location of where you installed the SDK of course, but this gives an easy way of retargeting when new SDKs come out.

We then need to set up the include path to include the headers from the softdevice. This is done in Preprocessor->User Include Directories. Here we use the macro we just defined:



In order for the SoftDevice to work correctly, we need to allow it to configure the interrupt vectors. This is done by setting the NO_VTOR_CONFIG macro in Preprocessor->Preprocessor Definitions:

Then we need to configure the debugger to download the softdevice. This is done by adding an additional load file in the project options under Debug->Loader:



Last we need to tell the debugger not to start at the entry point for our application. This is done to allow the softdevice to be configured prior to starting the application. To do this choose 
Debug->Debugger and then set "Start from entry point symbol" to no:


Error handling

Interacting with the softdevice can generate errors, and in order to get insight to how the system is running, it is nice to have printed debug output when any error occurs. Hence I have implemented a generic framework for error handling that can be seen here:

There are three important elements:

A hardfault handler void fault_handler(uint32_t id, uint32_t pc, uint32_t info); that is used by the softdevice to handle hardfaults. This is always enabled in any configuration

An error checking macro CHK_ERR(x) that will check if x is NRF_SUCCESS. If it is not, a relevant error message will be printed in the debug terminal in SES during debug, whereafter the device will reset. If the project is built in release mode the device will just reset.

Lastly an information macro INFO(x) will print the string x to the debug terminal during debug, or just no-op during release. 

The full implementation can be seen here:


Starting the softdevice

The code for starting the softdevice is surprisingly simple. The management functions for the softdevice is declared in nrf_sdm.h and the code looks like this:

These lines: actually starts the softdevice. If the hardware has a 32.768kHz crystal, the softdevice should use this for timing as it provides more precise timing and therefore allows for less "slack" in the radio and thereby lower power consumption. If such a crystal is not available, these lines should be commented in instead of These lines enables the less precise internal RC and sets up the internal calibration procedure for that in the SoftDevice.

The loop in the end is an event loop that will keep the processor in sleep unless events happen in the BLE stack. We will populate the functions around this in a future post.

The next part can be found here

tirsdag den 23. juli 2019

BLE from scratch Part 1: Basic build setup

Preface

It has been some time since I last did a post on this blog - there has been way too many things going on for me to focus on this hobby, but finally things have cleared up.

I have for a long time wanted to dive more into BLE development - there are a lot of cool things that can be done with a cheap radio and an app. For this endeavour I have chosen to buy the nRF52-DK from Nordic Semiconductor. I have worked professionally with devices from Nordic before (I'm looking at you ReSound LiNX) and they are well documented and easy to use, so that is an obvious choice for me.

Nordic have teamed up with Segger to provide a free unlimited version of the Segger Embedded Studio also for commercial use. Although this is a hobby project, free and unlimited for all and also built on CLANG/LLVM is too good to pass, so that will be the tool chain I will be focusing on in the coming posts

Nordic provides lots and lots of examples on how to build software for their devices, but I would like to start a project from scratch and then add functionality as needed. This is for several reasons:


  • Starting with an example and modifying that doesn't really bring the understanding of how things work and are put together. That understanding is needed when you want do debug stuff
  • I am not a big fan of board support packages - they carry complexity that are in many cases not needed.
  • Although powerful, the Nordic devices don't have endless RAM and Flash ressources so I would like to be able to run a lean ship
I will be storing the source code and projects for this endeavour on https://github.com/bdpedersen/ble-from-scratch

Quick spec

I will not be building the entire BLE stack from scratch - that is too time consuming. Instead I will be using what is called a SoftDevice in Nordic lingo. Basically a prebuilt binary that you can interface with using SVC and task with various BLE and basic system tasks. The SoftDevice I will be using is the S112 - the simplest of the SoftDevices that Nordic offers. On top of this I will build a simple application with the following features:

  • One service to control a LED and read a button state with:
    • One writeable characteristic that controls the LED
    • One read/notify characteristic that reads the state of the button
The application will end up supporting bonding with LE secure connections as well as persistence of bonds. It will run with the loweste possible power consumption allowed by the Nordic Device while still supporting IO.

Prerequisites for building the code in this blog

Downloads:
  • The latest version of the Nordic nRF5 SDK. Documentation can be found here
  • Segger Embedded Studio, which can be found here. During installation you will be asked to acquire a license - just state that you use a Nordic device in this step.

I extracted the two Nordic files to c:\nrf. 

... And now to some action

To set up a project from scratch in SES, choose "New Project..." and then choose a C/C++ executable for Nordic Semiconductor nRF. Choose a proper name and location for your project, then press Next.

In the target setup make sure that the Target processor matches your target. In the case of the nRF52-DK, the default nRF52832_xxAA is the correct one. In "Additional output format" choose to create a hex file. Adjust heap size if needed, but set stack size to at least 1536 bytes (2048 is safe) as this is needed by the softdevice, and press Next.

For additional files to add to the project, you can choose to leave out the Segger RTT files if you don't need realtime logging and debugging in your app, but otherwise the defaults are fine, so just press Next.

Finally create the two configurations, debug and release by pressing Next.

You can actually build and run this project, just make sure the nRF52-DK is connected to USB and on, and then choose "build -> build and run". If you then choose "Debug -> Go" the debug terminal will show up and runningn the application will show the Hello World prints via RTT

Part 2 can be found here