BLE application with nRF51822: Remotely control a LED

Since we are now able to advertise data in BLE, let’s do some exciting stuff like controlling LED through our smartphone. The goal of this project is to create an application that will handle BLE data sent from our smartphone to turn on/off the green, blue and red LEDs mounted on the dev board. First thing to do, as usual, is to create a new project based on sd_adv_app, we’ll call it sd_led_app:

        $ cd nrf51/sd_adv_app
        $ make clean
        $ cp -r sd_adv_app/ sd_led_app/

Then change the project name accordingly in the Makefile and in main.c:

  • Makefile:
# Project name
PROJECT_NAME = sd_led_app
  • main.c:
#define APP_ADV_NAME	"SD_LED_APP"

As you may have seen in the sd_adv_app code. When we disconnect the device from LightBlue, it doesn’t show up again in the scan device list. The reason is when we’ve been connected to our device, the advertisement stopped at the same time, then when a disconnection occured the advertisement is not automatically started again. So we need to catch the right BLE event to restart advertisement manually. If you remember we declared in our code a callback function called ble_evt_dispatch, we’ll catch the disconnect event in that function as following:

static void ble_evt_dispatch(ble_evt_t * p_ble_evt)
{
    // Catch BLE events here
    switch (p_ble_evt->header.evt_id)
    {            
        case BLE_GAP_EVT_DISCONNECTED:
            // Manually restart advertisement when a disconnection occured
            ble_advertising_start(BLE_ADV_MODE_FAST);
            break;
        default:
            break;
    }
}

In BLE, to exchange data between two devices (a Peripheral and a Central), the concepts of Services and Characteristics are used. Actually a Service contains a list of Characteristics we can illustrate as follow:

BLE Services and Characteristics nrf51
BLE Service and Characteristics

An Characteristic is a data container that can be read or write depending of the privileges specified. The Characteristic value is called an Attribute. Like file permissions a Characteristic can be readable, writable or both at same time. For instance a temperature sensor could have a Characteristic containing an Attribute temperature coded on a unsigned int 16 bits with read only privilege. That means a Central could only read the temperature. In our case we want to control a led, so we’ll create a writable Characteristic containing an Attribute mirroring the led status. Services and Characteristics are designated by an UUID (16 bits for the Bluetooth official ones and 128 bits for the custom ones).

To handle data sent from the smartphone we’re gonna create a new BLE service called ble_led:

        $ cd nrf51/
        $ mkdir -p custom/ble_service/ble_led
        $ cd custom/ble_service/ble_led
        $ touch ble_led.c ble_led.h
        $ cd ../..
        $ touch custom.mk

As you can see we created three files, ble_led.c and ble_led.h will contain all the code mandatory to receive and transmit data from and to a Central, custom.mk is the makefile use to include ble_led source code in our project. Open custom.mk and add the following lines in order to properly include our new BLE Service:

CUSTOM_BLE_SRV_DIRECTORY = ../custom/ble_service

# BLE services
ifeq ($(USE_BLE_LED), y)
INCLUDES  += $(CUSTOM_BLE_SRV_DIRECTORY)/ble_led
SRC 	  += $(CUSTOM_BLE_SRV_DIRECTORY)/ble_led/ble_led.c
CPFLAGS   += -DUSE_BLE_LED

In project Makefile call custom.mk and add flag to include ble_led service:

...
# BLE services
USE_BLE_LED := y

include ../sdk/sdk.mk
include ../custom/custom.mk
# add option "-I" before each included folders   
...   

In ble_led.c we’re gonna add a init function that will declare the Service in the BLE stack:

  • ble_led.c:
#include <stddef.h>
#include "ble_led.h"
#include "nrf_error.h"
#include "ble_types.h"
#include "ble_gatts.h"
#include "ble_srv_common.h"

uint32_t ble_led_init(ble_led_t * p_led)
{
    ble_uuid_t service_uuid;
    uint32_t   ret;

    // check if parameters are correctly set up
    if ((p_led == NULL) || (p_led_cb == NULL))
    {
        return NRF_ERROR_NULL;
    }

    p_led->conn_handle = BLE_CONN_HANDLE_INVALID;

    // create a custom base BLE uuid for our service (ble_led_service in ascii)
    const ble_uuid128_t base_uuid128 =
    {
        {
            0x62, 0x6c, 0x65, 0x5f, 0x67, 0x64, 0x70, 0x5f,
            0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x00
        }
    };

    // setup LED service uuid
    service_uuid.uuid = BLE_LED_SERVICE_UUID;

    // add our custom services in BLE stack's table 
    ret = sd_ble_uuid_vs_add(&base_uuid128, &(service_uuid.type));
    if (ret != NRF_SUCCESS)
    {
        return ret;
    }

    // sdd led service declaration to the local server ATT table
    ret = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
                                        &service_uuid,
                                        &(p_led->service_handle));
    if (ret != NRF_SUCCESS)
    {
        return ret;
    }

    p_led->uuid_type = service_uuid.type;

    return NRF_SUCCESS;
}
  • ble_led.h:
#ifndef __BLE_LED_H__
#define __BLE_LED_H__

#include <stdint.h>

#define BLE_LED_SERVICE_UUID                 0x3560     /**< The UUID of the LED Service. */

typedef struct 
{
    uint16_t     conn_handle;        /**< Handle of the current connection (as provided by the S110 SoftDevice). This will be BLE_CONN_HANDLE_INVALID when not in a connection. */
    uint16_t     revision;           /**< Handle of LED Service (as provided by the S110 SoftDevice). */
    uint16_t     service_handle;     /**< Handle of LED Service (as provided by the S110 SoftDevice). */
    uint8_t      uuid_type;          /**< UUID type assigned for LED Service by the S110 SoftDevice. */
}ble_led_t;

#endif //__BLE_LED_H__

Then in main.c we’ll update advertising_init function to also advertise the service information:

...
#define GREEN_LED_PIN					12

static ble_led_t _led;
...
static void advertising_init(void)
{
    uint32_t      err_code;
    ble_advdata_t advdata;
    ble_advdata_t scanrsp;
    ble_advdata_manuf_data_t manfdata;
    ble_uuid_t adv_uuids[] = {{BLE_LED_SERVICE_UUID, _led.uuid_type}};
    ...
    options.ble_adv_fast_timeout  = BLE_GAP_ADV_TIMEOUT_GENERAL_UNLIMITED;

    memset(&scanrsp, 0, sizeof(scanrsp));

    // number of UUIDs
    scanrsp.uuids_complete.uuid_cnt = sizeof(adv_uuids) / sizeof(adv_uuids[0]);
    // UUID
    scanrsp.uuids_complete.p_uuids  = adv_uuids;

    err_code = ble_advertising_init(&advdata, &scanrsp, &options, advertisement_evt_cb, NULL);
    APP_ERROR_CHECK(err_code);
}

int main(void){
    softdevice_init();
    gap_params_init();
    ble_led_init(&_led);
    advertising_init();
    ...

To tell to the Central that the services changed (in our case we actually added a service), we need to add before calling sd_ble_enable function in softdevice_init the following line:

// Tell the Central that Services on Peripheral changed. Without this line, 
// the Central won't update the services list of our device.
ble_enable_params.gatts_enable_params.service_changed = 1;

If you compile an run your code you should now see in the Peripheral nearby list on LightBlue, SD_LED_APP device with 1 Service:

BLE Service
SD_LED_APP with 1 Service

If you click on advertisement data, the service UUID will be listed:

BLE Service
LED UUID Service

Since the Service is now declared, we’ll see how to add a Characteristics. This Characteristics will be the LED status stored in an unsigned int 8 bits, also this value can be write by the Central in order to control properly the LED. Below the code allowing to add this Characteristic to our Service:

  • ble_led.c:
// This function allows to add a Characteristic in our Service
static uint32_t ble_led_sta_char_add(ble_led_t * const p_led)
{
    ble_gatts_char_md_t char_md;
    ble_gatts_attr_t    attr_char_value;
    ble_uuid_t          char_uuid;
    ble_gatts_attr_md_t attr_md;
    uint8_t initial_led_status = 0;

    memset(&char_md, 0, sizeof(char_md));

    // set the Characteristic type to Write Without Response
    // it means Central won't except any acknowledgment from the Peripheral
    char_md.char_props.write_wo_resp = 1;

    // Here we set the Characteristic UUID
    char_uuid.type = p_led->uuid_type;
    char_uuid.uuid = BLE_LED_STA_CHAR_UUID;

    memset(&attr_md, 0, sizeof(attr_md));

    // add write permission
    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.write_perm);

    // Attribute configuration, the data is stored in stack memory space
    attr_md.vloc    = BLE_GATTS_VLOC_STACK;
    attr_md.rd_auth = 0;
    attr_md.wr_auth = 0;
    attr_md.vlen    = 0;

    memset(&attr_char_value, 0, sizeof(attr_char_value));

    // setup Attribute default value
    attr_char_value.p_uuid    = &char_uuid;
    attr_char_value.p_attr_md = &attr_md;
    attr_char_value.init_len  = sizeof(uint8_t);
    attr_char_value.init_offs = 0;
    attr_char_value.max_len   = sizeof(uint8_t);
    attr_char_value.p_value   = &initial_led_status;

    return sd_ble_gatts_characteristic_add(p_led->service_handle,
                                           &char_md,
                                           &attr_char_value,
                                           &p_led->led_status_handles);
}

uint32_t ble_led_init(ble_led_t * p_led, ble_led_cb_t * p_led_cb)
{
...
    // add LED status Characteristics
    ret = ble_led_sta_char_add(p_led);
    if (ret != NRF_SUCCESS)
    {
        return ret;
    }

    return NRF_SUCCESS;
}

By running your code now, you should be able to see the services on LightBlue app. You should also be able to click on the Service:

bluetooth low energy advertisement nrf51
New Service

bluetooth low energy advertisement nrf51
Service content

As you can see, there is a field called “Write new value”, we’ll use it to send write data to our Service and thus change the led status. All we need is to catch the correct BLE event in our Service and transfer it to our user application. In ble_led.c we’re gonna add a function to parse Attributes event, this function will be called by ble_evt_dispatch function declared in main.c. We’re actually interested by only one event, the one telling us the Attribute has been written, it’s called BLE_GATTS_EVT_WRITE.

  • main.c:
static void ble_evt_dispatch(ble_evt_t * p_ble_evt)
{
...
    ble_led_on_ble_evt(&_led, p_ble_evt);
}
  • ble_led.c:

void ble_led_on_ble_evt(ble_led_t * p_led, ble_evt_t * p_ble_evt)
{
    if ((p_led == NULL) || (p_ble_evt == NULL))
    {
        return;
    }

    if (p_led->set_led_value != NULL)
    { 
        // parse event id
        switch (p_ble_evt->header.evt_id)
        {
            case BLE_GATTS_EVT_WRITE:
                // check if the event is coming from the corresponding attribute handle
                if (p_ble_evt->evt.gatts_evt.params.write.handle == p_led->led_status_handles.value_handle)
                {        
                    // transfer the event to the user application with the value received
                    p_led->set_led_value(p_ble_evt->evt.gatts_evt.params.write.data[0]);  
                }
                break;

            default:
                // No implementation needed.
                break;
        }
    }
}
  • ble_led.h:
...

typedef struct 
{
    uint16_t     conn_handle;        /**< Handle of the current connection (as provided by the S110 SoftDevice). This will be BLE_CONN_HANDLE_INVALID when not in a connection. */
    uint16_t     revision;           /**< Handle of LED Service (as provided by the S110 SoftDevice). */
    uint16_t     service_handle;     /**< Handle of LED Service (as provided by the S110 SoftDevice). */
    uint8_t      uuid_type;          /**< UUID type assigned for LED Service by the S110 SoftDevice. */
    ble_gatts_char_handles_t     led_status_handles;  /**< Handles related to the LED Packet characteristic. */
    void (*set_led_value) (uint8_t led_value);  /**< Callback function to set the led value in user application */ 
}ble_led_t;

...
void ble_led_on_ble_evt(ble_led_t * p_led, ble_evt_t * p_ble_evt);

#endif //__BLE_LED_H__

In this code we used a callback function called set_led_value, this function will help us to change the led value in the user application. So we just need to set and implement this callback in main.c:

  • main.c:

#define LED_OFF          0
#define LED_SET_RED      1
#define LED_SET_GREEN    2
#define LED_SET_BLUE     3


static void led_set_cb(uint8_t value)
{
    NRF_GPIO->OUTSET = (1UL << RED_LED_PIN);
    NRF_GPIO->OUTSET = (1UL << BLUE_LED_PIN);
    NRF_GPIO->OUTSET = (1UL << 12);

        
    switch(value)
    {
        case LED_OFF: 
            NRF_GPIO->OUTSET = (1UL << RED_LED_PIN);
            NRF_GPIO->OUTSET = (1UL << BLUE_LED_PIN);
            NRF_GPIO->OUTSET = (1UL << GREEN_LED_PIN);
            break;
        
        case LED_SET_RED: 
            NRF_GPIO->OUTCLR = (1UL << RED_LED_PIN);
            break;
        
        case LED_SET_GREEN: 
            NRF_GPIO->OUTCLR = (1UL << GREEN_LED_PIN);
            break;
        
        case LED_SET_BLUE: 
            NRF_GPIO->OUTCLR = (1UL << BLUE_LED_PIN);
            break;
    }
}


static void led_init(void)
{
    // setup red led gpio in output mode
    NRF_GPIO->PIN_CNF[RED_LED_PIN] = (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos)
                                            | (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos)
                                            | (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos)
                                            | (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos)
                                            | (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
    // setup blue led gpio in output mode
    NRF_GPIO->PIN_CNF[BLUE_LED_PIN] = (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos)
                                            | (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos)
                                            | (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos)
                                            | (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos)
                                            | (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
    // setup red green gpio in output mode
    NRF_GPIO->PIN_CNF[GREEN_LED_PIN] = (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos)
                                            | (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos)
                                            | (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos)
                                            | (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos)
                                            | (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);

    NRF_GPIO->OUTSET |= (1UL << RED_LED_PIN);
    NRF_GPIO->OUTSET |= (1UL << BLUE_LED_PIN);
    NRF_GPIO->OUTSET |= (1UL << GREEN_LED_PIN);
}

int main(void){
    
...
    // set the callback function
    _led.set_led_value = led_set_cb;
    ble_led_init(&_led);
    led_init();
}

Finally if you compile and run this application here what you should be able to do:

nrf51 bluetootg low energy led advertisement
Results

In a next post we’ll see how to use the PWM in order to select exactly the led color we want, and by the same time how to handle data packets and not only a uint8 value.

Advertisements