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.

BLE application with nRF51822: Advertising data

One of the feature (let’s say most important feature) brought by BLE specifications is the advertisement principle. BLE advertising allows a device to periodically broadcast data to every devices around it. Between each advertisement (the interval time can be set from 20ms to 10 seconds) the device can go in sleep mode, that helps to drastically reduce the power consumption. Before going deeper with advertisement, it’s important to explain some BLE vocabularies. A BLE device can be either a Peripheral or a Central. The Peripheral is mostly the battery powered up device that needs to keep its power consumption low such as a temperature sensor or a heart rate monitor. Since its power consumption has to remains low, it needs to send data to a much powerful device (a smartphone, computer etc…) to process the data, called the Central. BLE advertising is by definition unidirectional that means only the Peripheral can transmit data to the Central. If the Central needs to transmit data to the Peripheral as well, a connection needs to be done between both devices.

Let’s start by creating a new project based on sd_app project. We will call it sd_adv_app.

        $ cd nrf51/sd_app
        $ make clean
        $ cd ..
        $ cp -r sd_app/ sd_adv_app

Update the Makefile with the new project name:

# Project name
PROJECT_NAME = sd_adv_app

To support BLE advertisement in our code we also need to add tow more flags in the Makefile. USE_BLE_ADV to add corresponding advertisement code and USE_DRV_PSTORAGE to support the persistent storage, needed by BLE advertising.

# Drivers
USE_DRV_NOSD := n
USE_LIB_SCHEDULER := y
USE_SD110 := y
USE_BLE_COMMON := y
USE_LIB_TRACE := y
USE_DRV_BLE_FLASH := y
USE_LIB_TIMER := y
USE_BLE_ADV := y
USE_DRV_PSTORAGE := y

If you try to compile your code right now, you’ll get the following error:

../sdk/nRF51_SDK_9.0.0_2e23562/components/drivers_nrf/pstorage/pstorage.h:28:31: fatal error: pstorage_platform.h: No such file or directory
 #include "pstorage_platform.h";
                               ^
compilation terminated.
make: *** [../sdk/nRF51_SDK_9.0.0_2e23562/components/drivers_nrf/pstorage/pstorage.o] Error 1

As you can see pstorage_platform.h is missing, this file has to be added as a project dependent include containing the infromation where the application data is stored in flash. This module is really useful if you need to store persistent data in flash memory.

Create a folder named include in sd_adv_app/ and then copy pstorage_platform.h from an example code provided by Nordic.

        $ cd nrf51/sd_adv_app
        $ mkdir include
        $ cp ../sdk/nRF51_SDK_9.0.0_2e23562/examples/ble_peripheral/ble_app_template/config/pstorage_platform.h include/

The compilation should now works well. And we are now ready to code our first BLE app. To do that open main.c file, we’ll start by enabling Softdevice:

#include "softdevice_handler.h";
...
static void ble_evt_dispatch(ble_evt_t * p_ble_evt)
{
    // Catch BLE event here
}

static void softdevice_init(void)
{
    uint32_t err_code;

    // Initialize the SoftDevice handler module.
    SOFTDEVICE_HANDLER_INIT(NRF_CLOCK_LFCLKSRC_XTAL_20_PPM, NULL);

    // Enable BLE stack.
    ble_enable_params_t ble_enable_params;
    memset(&ble_enable_params, 0, sizeof(ble_enable_params));

    err_code = sd_ble_enable(&ble_enable_params);
    APP_ERROR_CHECK(err_code);

    // Subscribe for BLE events.
    err_code = softdevice_ble_evt_handler_set(ble_evt_dispatch);
    APP_ERROR_CHECK(err_code);
}

int main(void){
    softdevice_init();
...

The code above initialize the BLE stack Softdevice and setup the callback function needed to catch BLE events.

Once Softdevice initialized we can setup the GAP (Generic Access Profile) parameters (name of the device, security, connection parameters, etc…).

#define APP_ADV_NAME   "SD_ADV_APP";
...
static void gap_params_init(void)
{
    uint32_t                err_code;
    ble_gap_conn_sec_mode_t sec_mode;

    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);

    err_code = sd_ble_gap_device_name_set(&sec_mode,
                                          (const uint8_t *)APP_ADV_NAME,
                                          strlen(APP_ADV_NAME));
    APP_ERROR_CHECK(err_code);
}

int main(void){
    softdevice_init();
    gap_params_init();
...

This code allows to setup the connection security in open mode (anyone can initiate a connection to the device) and to set the device name (the name we will see on a host during a BLE scan).

Then we can initialize the data that will be broadcasted during BLE advertising. We’ll setup the advertisement behaviour at the same time (advertisement interval, timeout, type, ect…)

#include "ble_advdata.h";
#include "ble_advertising.h";
...
#define APP_ADV_INTERVAL                320        // 320 * 0.625ms = 200ms
...
static void advertisement_evt_cb(ble_adv_evt_t ble_adv_evt)
{
	// Catch Advertisement events here
}
...
static void advertising_init(void)
{
    uint32_t      err_code;
    ble_advdata_t advdata;
    memset(&advdata, 0, sizeof(advdata));

    // advertise the the full device name
    advdata.name_type = BLE_ADVDATA_FULL_NAME;
    //LE General Discoverable Mode, BR/EDR not supported.
    advdata.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;

    ble_adv_modes_config_t options = {0};

    // enable fast advertisement
    options.ble_adv_fast_enabled  = BLE_ADV_FAST_ENABLED;
    // set advertisement interval to 200 ms, 320 * 0.625ms
    options.ble_adv_fast_interval = APP_ADV_INTERVAL;
    // set advertisement timeout to infinite
    options.ble_adv_fast_timeout  = BLE_GAP_ADV_TIMEOUT_GENERAL_UNLIMITED;

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

int main(void){
    softdevice_init();
    gap_params_init();
    advertising_init();
...

Finally we just need to call ble_advertising_start function to start advertisement:

int main(void){
    softdevice_init();
    gap_params_init();
    advertising_init();
    // start fast advertisement
    ble_advertising_start(BLE_ADV_MODE_FAST);

Compile your code and flash it on your dev board:

        $ cd nrf51/
        $ JLinkExe sh/nrf51_load_sd.sh
        J-Link>loadbin sd_adv_app/sd_adv_app.bin 0x18000
        J-Link>r
        J-Link>g

The green led should blink and now thanks to LightBLue Explorer app (ios/mac) you should be able to see your device advertising device name data:

LightBlue on Mac:

bluetooth low energy advertisement nrf51

LightBlue on ios:

bluetooth low energy advertisement nrf51

For Android users you can use a similar application called BLE Scanner.

If you click on SD_ADV_APP you’ll open a BLE connection with the device. Then if you click on Show advertisement data you’ll see the data advertised by the device.

bluetooth low energy advertisement nrf51

So far only the local name is broadcasted. We can add some data thanks to the manufacturer specific data field in an advertisement packet. To do that we just need to add our data in advdata.p_manuf_specific_data field in main.c file:

#define APP_ADV_CUSTOM_DATA     "hello world!";
...
static void advertising_init(void)
{
    uint32_t err_code;
    ble_advdata_t advdata;
    ble_advdata_manuf_data_t manfdata;

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

    // advertise the the full device name
    advdata.name_type = BLE_ADVDATA_FULL_NAME;
    // LE General Discoverable Mode, BR/EDR not supported.
    advdata.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;

    // Set the manufacturer specific data below
    memset(&manfdata, 0, sizeof(manfdata));

    manfdata.company_identifier = 0x1234;
    manfdata.data.size = strlen(APP_ADV_CUSTOM_DATA);
    manfdata.data.p_data = (uint8_t *)APP_ADV_CUSTOM_DATA;

    advdata.p_manuf_specific_data = &manfdata;

    ble_adv_modes_config_t options = {0};
...

The result on LightBlue Explorer application should be as below:

bluetooth low energy advertisement nrf51

Here we are! We are now able to advertise data over BLE, in the next post we’ll see how to transfert data from our smartphone to the device in order to control remotely the LED.

Links:

LightBlue Explorer

BLE Scanner

You can retrieve the source code on the following github link: github

BLE application with nRF51822: Softdevice

One advantage of using Nordic Semiconductor BLE solution is Softdevice. Softdevice is a BLE stack running on the nRF51 chip, allowing you to develop a custom BLE application really easily. Sofdevice binary is provided free of charge with the nRF51 chip, a bunch of different public APIs are provided to let your application set up and communicate with it. Different declinations of Sofdevice are available depending the features you need for your project. In our case we will use S110 (BLE stack only). To get BLE working with our current project set up, we need to create a new linker script since Softdevice has to be stored at a specific memory location.

  • nRF51 Softdevice memory map:

nrf51822 bluetooth softdevice s110 memory map

To work properly Softdevice needs a part of the flash memory and a part of the RAM memory as well. So we will create a new linker script based on the first one we already developed:

        $ cd nrf51/linker_script/
        $ cp nrf51_ld_script.ld nrf51_sd_ld_script.ld

We now have to do some modifications in nrf51_sd_ld_script.ld file in order to share the memory. In Softdevice specifications document we can retrieve information regarding the memory usage when S110 is enabled.

nrf51822 bluetooth stack softdevice memory usage

As described below S110 + MBR need 96kB of Flash memory and 8kB of RAM memory. So the RAM starting point for our application is now 0x20002000 (0x20000000 + 8kB), therefore, the total RAM available for our application is not 16kB anymore but 8kB. Same for the Flash memory, the new starting point for our application is now 0x00180000 (0x00000000 + 96kB), so, the amount of Flash available is now 160kB. Below the modifications to do accordingly the specifications:

        MEMORY
        {
             RAM (xrw) : ORIGIN = 0x20000000 + 8k, LENGTH = 16k - 8k
             FLASH (rx) : ORIGIN = 0x00000000 + 96k, LENGTH = 256k - 96k
        }

That’s it for the linker script. We’re gonna create a new project based on the led blinking sources.

        $ cd nrf51/
        $ cd led_blinking/
        $ make clean         // In order to do a clean copy, remove all the object files 
        $ cd ../
        $ cp -r led_blinking/ sd_app

We need to do some modifications in the Makefile to call the correct ld script, change the project name and add few mandatory flags.

# linker script directory
LDSCRIPT = ../linker_script/nrf51_sd_ld_script.ld   
# Project name
PROJECT_NAME = sd_app
# Drivers
USE_DRV_NOSD := n
USE_LIB_SCHEDULER := y
USE_SD110 := y
USE_BLE_COMMON := y
USE_LIB_TRACE := y
USE_DRV_BLE_FLASH := y
USE_LIB_TIMER := y
...
CPFLAGS = -mcpu=cortex-m0 ...
CPFLAGS += -std=gnu99
...
# Define target processor
CPFLAGS += -D$(TARGET)
CPFLAGS	+= -DBLE_STACK_SUPPORT_REQD 

Now, compile your project and look at the generated sd_app.map file, you will see that the “.isr_vector” section is now starting at 0x180000, same for the “.bss” section which is now starting at 0x20002000.

Some modifications have been done in sdk.mk as well, in order to fix few include issues. You can find all these modifications at the following link:

https://github.com/jocelynmass/nrf51/blob/master/sdk/sdk.mk

Now, we need to flash Softdevice S110 (the MBR section is already included in S110) in the nrf51 flash memory. Start by copying the stack hex file in a new directory named stack_img.

        $ cd nrf51/
        $ mkdir stack_img
        $ cp sdk/nRF51_SDK_9.0.0_2e23562/components/softdevice/s110/hex/s110_softdevice.hex stack_img/

Then we’re gonna create a new JLink script file (based on nrf51_erase.sh script) that will help us to upload Sofdevice in flash memory.

        $ cd nrf51/sh/
        $ cp nrf51_erase.sh nrf51_load_sd.sh

In nrf51_load_sd.sh add the following lines:

loadfile stack_img/s110_softdevice.hex 0
r 

Finally we’ll check that our blinking led application is still working with Sofdevice loaded in the flash memory. Be careful to correctly load your application at address 0x18000, if not you’ll most likely corrupt Sofdevice.

        $ cd nrf51/
        $ JLinkExe sh/nrf51_load_sd.sh
        J-Link>loadbin sd_app/sd_app.bin 0x18000
        J-Link>r
        J-Link>g

You should see the green led blinking now. I know, nothing really exciting yet but that means we can now start using BLE ;-). In the next post we’ll see how to advertise some data with BLE.

BLE application with nRF51822: Firmware flashing

In the last post we have developed our first led blinking application, now we will see how to flash the firmware in the nRF51822 memory. You can use different technics to upgrade the firmware, either by using a SWD key or by using the pre installed DFU bootloader supporting OTA update (update over the air), shipped by default with the nRF51822 Smart Beacon Kit. In this post I’ll only explain how to use a SWD key to flash and debug your program.

First if you don’t have any SWD key compatible with Jlink, you can buy one for only 13$ on the following website : http://www.hotmcu.com/usbminijtagex-jlink-jtagswd-debuggeremula%E2%80%8Btor-p-46.html?cPath=3_25. The first thing you need to do is soldering 4 wires on the Smart Beacon Kit in order to have access to the nRF51822’s SWD port:

nrf51 swd jtag pinout

Here is the result on my Beacon Kit:

bluetooth low energy nrf51 beacon kit

You now have to connect your key to the SWD port of the Beacon kit. The connection depends of the key you are using but the pinout should be as following:

segger jtag swd pinout

A photo of my USB-MiniJTAG-EX JTAG/SWD Debugger/Emula​tor (I removed the PCB from its plastic case):

jtag swd usb programer arm

Finally a photo with the SWD key connected to the Beacon kit:

bluetooth nrf51 beacon kit jtag swd

The application we will use to flash our kit is Jlink Debugger developed by Segger, here is the link where you can download this software: https://www.segger.com/jlink-software.html

Once you’ve downloaded and installed Jlink Debugger, you should have in the terminal path JLinkExe, if it’s not the case please be sure JLinkExe binary directory is present in the PATH variable of your computer.

We are now ready to flash the led blinking firmware into the nRF51’s memory. Place the battery in your Smart Beacon kit battery holder and connect your SWD key to your computer.

        $ cd nrf51/
        $ JLinkExe
        $ J-Link> device nrf51
        $ J-Link> r
        $ J-Link> w4 4001e504 2
        $ J-Link> w4 4001e50c 1
        $ J-Link> w4 4001e514 1
        $ J-Link> w4 4001e504 0
        $ J-Link> r
        $ J-Link> loadbin led_blinking/led_blinking.bin 0x00000000
        $ J-Link> r

By typing the command JLinkExe you will enter into the Jlink terminal. From this terminal you’ll be able to flash, execute and debug your program. The command “device nrf51” allows to tell the debugger which target we will use. The command “r” allows to reset the target. Then we need to erase the whole flash memory, to do that we directly wrote at specific register address. If you look in the reference manual you will see that the NVMC config register, NVMC eraseall register and NVMC eraseuicr register are respectively located at 0x4001e504, 0x4001e50c and 0x4001e514 adresses. Writing the value 2 in NVMC config register enables the erase command. Writing the value 1 in NVMC eraseall register starts the chip erase. Writing the value 1 in NVMC eraseuicr register starts uicr erase (I’ll explain in another post what is the purpose of the UICR on the nRF51). Finally by writing the value 0 in NVMC config register we set back the chip memory in read only mode. After having erased the chip memory we can flash our firmware by typing the “loadbin” command following by two arguments, the first one is the firmare binary path and the second one the memory address at which the firmware will be placed, in our case 0x00000000.

  • To execute the firmware we just need to type the following command:
        $ J-Link> g

You should now see the green led blinking on the Smart Beacon kit ;-). The “g” command allows you to run the firmware on the target. If you want to halt the target to read some reasons you can type “h”. By typing “?” command in the Jlink terminal you’ll be abe to see all the commands supported by the target.

Since it’s pretty annoying to type all these commands before being able to execute and test a firmware, we can place all these commands in a file which will be passed as argument to Jlink.

        $ cd nrf51/
        $ mkdir sh
        $ cd sh/
        $ touch nrf51_erase.sh
  • Add the following commands in nrf51_erase.sh file:
device nrf51822
r
w4 4001e504 2
Sleep 200
w4 4001e50c 1
Sleep 200
w4 4001e514 1
Sleep 200
w4 4001e504 0
Sleep 200
r

To use this script during JLinkExe start, you just have to type the following command in a terminal:

        $ cd nrf51/
        $ JLinkExe sh/nrf51_erase.sh   

BLE application with nRF51822: Blinking a led

Since your development environment is almost completely setup, we can now start to develop a simple led blinking application for our demo board. The purpose of this application is extremely simple but allows to setup the nRF51’s SDK and see how to manipulate nRF51’s registers. First you need to download the SDK v9.0 at the following address: http://developer.nordicsemi.com/nRF51_SDK/

  • Uncompress and place the nRF51’s SDK in sdk folder:
        $ cd nrf51/
        $ mkdir sdk
        $ unzip nRF51_SDK_9.0.0_2e23562.zip -d nRF51_SDK_9.0.0_2e23562
        $ mv nRF51_SDK_9.0.0_2e23562 YOUR_PROJECT_PATH/nrf51/sdk

To be able to use the SDK from our application we need to include SDK’s source files in our Makefile. To do that we will develop a new Makefile which will be included from our project Makefile. The reason to split the Makefile is to manage dependencies in a better way. For example in a case we use only one Makefile located in led_blinking project directory, it would mean that for each new project we would create, we would need to copy the Makefile part corresponding to the SDK sources. Thanks to this new dedicated Makefile we will not have to copy it to each of our future projects we will develop.

  • Create new Makefile in sdk/ folder
        $ cd sdk/
        $ touch sdk.mk
  • Add the following code in sdk.mk
SDK_DIRECTORY = ../sdk/nRF51_SDK_9.0.0_2e23562/components

# System
INCLUDES  += $(SDK_DIRECTORY)/toolchain
INCLUDES  += $(SDK_DIRECTORY)/toolchain/arm
INCLUDES  += $(SDK_DIRECTORY)/toolchain/gcc
INCLUDES  += $(SDK_DIRECTORY)/device

SRC       += $(SDK_DIRECTORY)/toolchain/system_nrf51.c

# Drivers
ifeq ($(USE_DRV_BLE_FLASH), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/ble_flash
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/ble_flash -name *.c)
CPFLAGS   += -DUSE_DRV_BLE_FLASH
endif
ifeq ($(USE_DRV_CLK), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/clock
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/clock -name *.c)
CPFLAGS   += -DUSE_DRV_CLK
endif
ifeq ($(USE_DRV_GPIOTE), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/gpiote
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/gpiote -name *.c)
CPFLAGS   += -DUSE_DRV_GPIOTE
endif
ifeq ($(USE_DRV_LPCOMP), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/lpcomp
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/lpcomp -name *.c)
CPFLAGS   += -DUSE_DRV_LPCOMP
endif
ifeq ($(USE_DRV_NOSD), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/nrf_soc_nosd
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/nrf_soc_nosd -name *.c)
CPFLAGS   += -DUSE_DRV_NOSD
endif
ifeq ($(USE_DRV_PPI), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/ppi
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/ppi -name *.c)
CPFLAGS   += -DUSE_DRV_PPI
endif
ifeq ($(USE_DRV_PSTORAGE), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/pstorage
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/pstorage -name *.c)
CPFLAGS   += -DUSE_DRV_PSTORAGE
endif
ifeq ($(USE_DRV_QDEC), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/qdec
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/qdec -name *.c)
CPFLAGS   += -DUSE_DRV_QDEC
endif
ifeq ($(USE_DRV_RADIO_CFG), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/radio_config
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/radio_config -name *.c)
CPFLAGS   += -DUSE_DRV_RADIO_CFG
endif
ifeq ($(USE_DRV_RNG), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/rng
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/rng -name *.c)
CPFLAGS   += -DUSE_DRV_RNG
endif
ifeq ($(USE_DRV_RTC), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/rtc
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/rtc -name *.c)
CPFLAGS   += -DUSE_DRV_RTC
endif
ifeq ($(USE_DRV_SDIO), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/sdio
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/sdio -name *.c)
CPFLAGS   += -DUSE_DRV_SDIO
endif
ifeq ($(USE_DRV_SPI_SLV), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/spi_slave
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/spi_slave -name *.c)
CPFLAGS   += -DUSE_DRV_SPI_SLV
endif
ifeq ($(USE_DRV_SPI_MSTR), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/spi_master
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/spi_master -name *.c)
CPFLAGS   += -DUSE_DRV_SPI_MSTR
endif
ifeq ($(USE_DRV_SWI), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/swi
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/swi -name *.c)
CPFLAGS   += -DUSE_DRV_SWI
endif
ifeq ($(USE_DRV_TIMER), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/timer
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/timer -name *.c)
CPFLAGS   += -DUSE_DRV_TIMER
endif
ifeq ($(USE_DRV_TWI_MSTR), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/twi_master
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/twi_master -name *.c)
CPFLAGS   += -DUSE_DRV_TWI_MSTR
endif
ifeq ($(USE_DRV_UART), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/uart
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/uart -name *.c)
CPFLAGS   += -DUSE_DRV_UART
endif
ifeq ($(USE_DRV_WDT), y)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/wdt
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/wdt -name *.c)
CPFLAGS   += -DUSE_DRV_WDT
endif

INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/hal
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/hal -name *.c)
INCLUDES  += $(SDK_DIRECTORY)/drivers_nrf/common
SRC 	  += $(shell find $(SDK_DIRECTORY)/drivers_nrf/common -name *.c)


# Libraries
ifeq ($(USE_LIB_DFU), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/bootloader_dfu
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/bootloader_dfu -name *.c)
CPFLAGS   += -DUSE_LIB_DFU
endif
ifeq ($(USE_LIB_BUTTON), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/button
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/button -name *.c)
CPFLAGS   += -DUSE_LIB_BUTTON
endif
ifeq ($(USE_LIB_CONSOLE), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/console
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/console -name *.c)
CPFLAGS   += -DUSE_LIB_CONSOLE
endif
ifeq ($(USE_LIB_CRC16), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/crc16
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/crc16 -name *.c)
CPFLAGS   += -DUSE_LIB_CRC16
endif
ifeq ($(USE_LIB_FIFO), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/fifo
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/fifo -name *.c)
CPFLAGS   += -DUSE_LIB_FIFO
endif
ifeq ($(USE_LIB_GPIOTE), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/gpiote
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/gpiote -name *.c)
CPFLAGS   += -DUSE_LIB_GPIOTE
endif
ifeq ($(USE_LIB_HCI), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/hci
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/hci -name *.c)
CPFLAGS   += -DUSE_LIB_HCI
endif
ifeq ($(USE_LIB_IC_INFO), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/ic_info
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/ic_info -name *.c)
CPFLAGS   += -DUSE_LIB_IC_INFO
endif
ifeq ($(USE_LIB_MEM_MNG), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/mem_manager
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/mem_manager -name *.c)
CPFLAGS   += -DUSE_LIB_MEM_MNG
endif
ifeq ($(USE_LIB_PWM), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/pwm
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/pwm -name *.c)
CPFLAGS   += -DUSE_LIB_PWM
endif
ifeq ($(USE_LIB_SCHEDULER), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/scheduler
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/scheduler -name *.c)
CPFLAGS   += -DUSE_LIB_SCHEDULER
endif
ifeq ($(USE_LIB_SENSORSIM), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/sensorsim
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/sensorsim -name *.c)
CPFLAGS   += -DUSE_LIB_SENSORSIM
endif
ifeq ($(USE_LIB_SH256), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/sha256
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/sha256 -name *.c)
CPFLAGS   += -DUSE_LIB_SH256
endif
ifeq ($(USE_LIB_SIMPLE_TIMER), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/simple_timer
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/simple_timer -name *.c)
CPFLAGS   += -DUSE_LIB_SIMPLE_TIMER
endif
ifeq ($(USE_LIB_TIMER), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/timer
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/timer -name *.c)
CPFLAGS   += -DUSE_LIB_TIMER
endif
ifeq ($(USE_LIB_TRACE), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/trace
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/trace -name *.c)
CPFLAGS   += -DUSE_LIB_TRACE
endif
ifeq ($(USE_LIB_UART), y)
INCLUDES  += $(SDK_DIRECTORY)/libraries/uart
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/uart -name *.c)
CPFLAGS   += -DUSE_LIB_UART
endif

INCLUDES  += $(SDK_DIRECTORY)/libraries/util
SRC 	  += $(shell find $(SDK_DIRECTORY)/libraries/util -name *.c)

ifeq ($(USE_SD110), y)
# SD110, BLE stack
INCLUDES  += $(SDK_DIRECTORY)/softdevice/common
INCLUDES  += $(SDK_DIRECTORY)/softdevice/s110
endif

Because each project doesn’t necessarily need to use same drivers and/or libraries, drivers and libraries sources are compiled only if a dedicated flag is defined. Fo example if we want to use a uart driver in our project we will need to add USE_DRV_UART := y in the project’ Makefile.
We also need to specify our processor family to the SDK since it has been developed to support several processors. If you have a look to nrf.h file placed in sdk/nRF51_SDK_9.0.0_2e23562/components/device/nrf.h you will see the following code:

/* Family selection for family includes. */
#if defined (NRF51)
    #include "nrf51.h"
    #include "nrf51_bitfields.h"
    #include "nrf51_deprecated.h"
#elif defined (NRF52)
    #include "nrf52.h"
    #include "nrf52_bitfields.h"
    #include "nrf51_to_nrf52.h"
#else
    #error "Device family must be defined. See nrf.h."
#endif /* NRF51, NRF52 */

As you can see if we don’t specify the processor family it will raise an exception during build. So we need to add a flag in our project Makefile as following:

...
# Define target processor
TARGET 	:= 	NRF51
CPFLAGS += -D$(TARGET) 

# Generate dependency information
CPFLAGS += -MD -MP -MF .dep\$(@F).d
...

If you look in sdk/nRF51_SDK_9.0.0_2e23562/components/drivers_nrf/config directory you’ll find the nrf_drv_config.h file. This file allows you to setup some specific drivers, since it’s project dependent we will copy this file directly in our project directory:

        $ cd nrf51/led_blinking/
        $ mkdir cfg
        $ cp sdk/nRF51_SDK_9.0.0_2e23562/components/drivers_nrf/config/nrf_drv_config.h nrf51/led_blinking/cfg/

To include this header in our project we will add “INCLUDES += cfg/ ” in the project’ makefile:

...
# include directory
INCLUDES = include/   
INCLUDES += cfg/    
...

Since our first project won’t use the bluetooth low energy stack we have to use nrf_soc_nosd driver. To use it we just have to add “USE_DRV_NOSD := y” flag in the project’s makefile, as below:

...
# Drivers
USE_DRV_NOSD := y

include ../sdk/sdk.mk
...

We can now code the led blinking feature in our project. In a first time We’re gonna use the green led as blinking led. As you can see in PCA20006_Schematic_And_PCB.pdf document the green led is connected to gpio P0.12 (you can download this document from Nordic semiconductor website). The goal of the code below is to set gpio P0.12 to 1 wait 300ms then set gpio P0.12 to 0 and wait again 300ms. This code will be place in an infinite loop.

#include "nrf51.h"
#include "nrf51_bitfields.h"  // nrf51 register declaration
#include "nrf_delay.h"

#define GREEN_LED_PIN	12  // Pin P0.12

int main(void){
	// setup led 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);

	do{
		// set to 1 led gpio (bit 12 corresponding to pin 12)
		NRF_GPIO->OUTSET = (1UL << GREEN_LED_PIN);
		// wait 300ms
		nrf_delay_ms(300);
		// set to 0 led gpio (bit 12 corresponding to pin 12)
		NRF_GPIO->OUTCLR = (1UL << GREEN_LED_PIN);
		// wait 300ms
		nrf_delay_ms(300);
	}while(1); //infinite loop

    return 0;
}

As explained in nrf51 reference manual to setup a pin you need to access to PIN_CNF register and set different parameters as the pin direction, input buffer activation or not, pull configuration, drive configuration or pin sensing mechanism. In our example we just need to setup the led pin in output mode, we don’t need to use a pull up or a pull down resistor neither an input buffer and since we are in output mode we don’t need the sensing mechanism. So to set up pin 12 in output mode, accordingly to the reference manual, we need to set bit 0 to 1. Since we don’t use other features the register’s bit remain to 0.

Screen Shot 2015-11-22 at 6.41.56 PM
Screen Shot 2015-11-22 at 6.48.55 PM

It’s extremely simple to put a gpio to a high level, thanks to OUTSET register, we only need to set to 1 the bit corresponding to the pin number. To set a gpio to a low level, we use the same mechanism than the one we use for setting a high level except that we use OUTCLR register instead.

You can now make your project, in the next post I will explain how to flash the binary file into nrf51 flash memory.

        $ cd nrf51/
        $ make

If every thing is og you should be able to read this line on you terminal:

../toolchain/arm_cm0/bin/arm-none-eabi-objcopy     -O binary -S       led_blinking.elf led_blinking.bin

BLE application with nRF51822: Makefile

The last element mandatory before being able to compile a nrf51 project is the Makefile. This file allows to tell the compiler what and where are the source and object files, it also allows to tell the linker how to generate the executable regarding the linker script. All the target information are also described in this file. I’m not going to explain all the options used in this Makefile but if you want more information you can visit the following website: http://www.opussoftware.com/tutorial/TutMakefile.htm#Rules

  • Create a Makefile file in led_blinking project folder:
        $ cd led_blinking
        $ touch Makefile
  • Declare the toolchain information and options:
# relative location of the toolchain binary
TOOLCHAIN = ../toolchain/arm_cm0/bin/arm-none-eabi-
# arm-none-eabi-gcc executable
CC   = $(TOOLCHAIN)gcc
# arm-none-eabi-objcopy executable          
CP   = $(TOOLCHAIN)objcopy    
# output format is raw binary and we strip the symbol information from source files                      
BIN  = $(CP) -O binary -S     
  • Declare the source directories:
# Sources
# linker script directory
LDSCRIPT = ../linker_script/nrf51_ld_script.ld   
# startup folder directory     
STARTUP_DIR = ../startup

# main.c directory
SRC  = src/main.c          
# startup.c directory                         
SRC  += $(STARTUP_DIR)/startup.c                      

# include directory
INCLUDES = include/   
# add option "-I" before each included folders                    
INCLUDE_DIR  = $(patsubst %,-I%,$(INCLUDES))   
  • Declare the compiler options
# Project name
PROJECT_NAME = led_blinking

# Options
OBJS  = $(SRC:.c=.o)

CPFLAGS = -mcpu=cortex-m0 -Os -gdwarf-2 -mthumb -fomit-frame-pointer -Wall -Wstrict-prototypes -fverbose-asm -Wa,-ahlms=$(<:.c=.lst) $(DEFS)
LDFLAGS = -mcpu=cortex-m0 -lc -mthumb -nostartfiles -T$(LDSCRIPT) -Wl,-Map=$(PROJECT_NAME).map,--cref,--no-warn-mismatch $(LIBDIR)
 
# Generate dependency information
CPFLAGS += -MD -MP -MF .dep\$(@F).d
  • Declare the project rules
# Rules
all: $(OBJS) $(PROJECT_NAME).elf  $(PROJECT_NAME).bin $(PROJECT_NAME).map
 
 %o: %c
	$(CC) -c $(CPFLAGS) -I . $(INCLUDE_DIR) $< -o $@

%elf: $(OBJS)
	$(CC) $(OBJS) $(LDFLAGS) -o $@
	
%bin: %elf
	$(BIN)  $< $@

clean:
	find ./ -name '*~' | xargs rm -f
	rm -f $(OBJS)
	rm -f $(PROJECT_NAME).elf
	rm -f $(PROJECT_NAME).map
	rm -f $(PROJECT_NAME).hex
	rm -f $(PROJECT_NAME).bin
	rm $(SRC:.c=.lst)
	rm -r  .dep*

At this point we should be able to build our simple project. To do so, in the project folder directory type make and press enter.

  • Build your project:
        $ cd led_blinking
        $ make
  • The output you should see:
-Iinclude/ -I../startup
../toolchain/arm_cm0/bin/arm-none-eabi-gcc -c -mcpu=cortex-m0 -Os -gdwarf-2 -mthumb -fomit-frame-pointer -Wall -Wstrict-prototypes -fverbose-asm -Wa,-ahlms=src/main.lst  -MD -MP -MF .dep\main.o.d -I . -Iinclude/ -I../startup            src/main.c -o src/main.o
../toolchain/arm_cm0/bin/arm-none-eabi-gcc -c -mcpu=cortex-m0 -Os -gdwarf-2 -mthumb -fomit-frame-pointer -Wall -Wstrict-prototypes -fverbose-asm -Wa,-ahlms=../startup/startup.lst  -MD -MP -MF .dep\startup.o.d -I . -Iinclude/ -I../startup            ../startup/startup.c -o ../startup/startup.o
../startup/startup.c:54:2: warning: taking address of expression of type 'void'
  &_end_of_stack,        // The initial stack pointer
  ^
../toolchain/arm_cm0/bin/arm-none-eabi-gcc src/main.o ../startup/startup.o -mcpu=cortex-m0 -lc -mthumb -nostartfiles -T../linker_script/nrf51_ld_script.ld    -Wl,-Map=led_blinking.map,--cref,--no-warn-mismatch  -o led_blinking.elf
../toolchain/arm_cm0/bin/arm-none-eabi-objcopy     -O binary -S       led_blinking.elf led_blinking.bin

As you can see, thanks to the Makefile, 3 new files have been generated. The most important one is the “.bin” file since it will be the one you will use to flash the nrf51. The “.map” file contains the memory map of your project, it could help you to find linker script bugs or to debug your application.

BLE application with nRF51822: Startup code and the Main entry

If you remember what we talked about in the previous post, before running the application, the processor needs to setup several things, like initialize the “.data” section in RAM and set “.bss” values to zero. Also if you remember we defined a section for the ISR vectors, so now we need to declare all the nRF51822 supported ISR.

  • Create a startup.c file in startup folder:
        $ cd nrf51
        $ mkdir startup
        $ cd startup
        $ touch startup.c

We need to declare few variables to properly setup “.data”, “.bss” sections and the top of the stack address. For your information, these variables are defined and set by the linker script. We also need to declare the main function as an extern function.

extern unsigned long _etext; // End of the code section
extern unsigned long _sdata; // Start address for the .data section
extern unsigned long _edata; // End address for the .data section
extern unsigned long _sbss; // Start address for the .bss section
extern unsigned long _ebss; // End address for the .bss section
extern void _end_of_stack; // Top of stack

extern int main(void);

I’ll speak later about how to setup the nRF51 SDK provided by Nordic but the following informations are coming from the SDK source code. In the linker script we declare a specific section for the interrupt vectors. As we need to have access to these vectors in our application we can declare a function pointer table in startup.c. In that way we will be able to implement these function later in our code.

  • ISR vectors prototypes:
void default_irq(void);
void Reset_Handler(void);
void NMI_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void HardFault_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void SVC_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void PendSV_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void SysTick_Handler(void) __attribute__ ((weak, alias ("default_irq")));
void POWER_CLOCK_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RADIO_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void UART0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SPI0_TWI0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SPI1_TWI1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void GPIOTE_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void ADC_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TIMER0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TIMER1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TIMER2_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RTC0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void TEMP_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RNG_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void ECB_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void CCM_AAR_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void WDT_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void RTC1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void QDEC_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void LPCOMP_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI0_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI1_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI2_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI3_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI4_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));
void SWI5_IRQHandler(void) __attribute__ ((weak, alias ("default_irq")));

As you can see the attribute weak and alias are used for the ISR vector prototypes. Actually for an application we don’t necessary need all the IRQ, so by default, thanks to alias attribute, the function called in case of interruption will be default_irq. The weak attribute means that we can override this function. For example POWER_CLOCK_IRQHandler can either point to default_irq (in case we don’t used it in our code) or point to POWER_CLOCK_IRQHandler if we have implemented it.

  • ISR vectors table:
__attribute__ ((section(".isr_vector")))
void (* const g_pfnVectors[])(void) =
{
	&_end_of_stack,        // The initial stack pointer
	Reset_Handler,         // Reset handler
	NMI_Handler,           // NMI handler
	HardFault_Handler,     // Hard fault handler
	0,0,0,0,0,0,0,         // 7 unused addresses   
	SVC_Handler,           // SVC handler
	0,0,                   // 2 unused addresses
	PendSV_Handler,        // Pend SV handler
	SysTick_Handler,       // Sys tick handler
	POWER_CLOCK_IRQHandler,// Power clock handler
	RADIO_IRQHandler,      // Radio handler
	UART0_IRQHandler,      // Uart 0 handler
	SPI0_TWI0_IRQHandler,  // SPI 0 handler
	SPI1_TWI1_IRQHandler,  // SPI 1 handler
	0,                     // 1 unused address
	GPIOTE_IRQHandler,     // GPIO handler
	ADC_IRQHandler,        // ADC handler
	TIMER0_IRQHandler,     // TIMER 0 handler
	TIMER1_IRQHandler,     // TIMER 1 handler
	TIMER2_IRQHandler,     // TIMER 2 handler
	RTC0_IRQHandler,       // RTC0 handler
	TEMP_IRQHandler,       // TEMP handler
	RNG_IRQHandler,        // RNG handler
	ECB_IRQHandler,        // ECB handler
	CCM_AAR_IRQHandler,    // CCM AAR handler
	WDT_IRQHandler,        // WDT handler
	RTC1_IRQHandler,       // RTC1 handler
	QDEC_IRQHandler,       // QDEC handler
	LPCOMP_IRQHandler,     // LDCOMP handler
	SWI0_IRQHandler,       // SWI0 handler
	SWI1_IRQHandler,       // SWI1 handler
	SWI2_IRQHandler,       // SWI2 handler
	SWI3_IRQHandler,       // SWI3 handler
	SWI4_IRQHandler,       // SWI4 handler
	SWI5_IRQHandler,       // SWI5 handler
	0,0,0,0,0,0,           // 6 unused addresses
};

The attribute section is used to explicitly tell the compiler to place this tab into “.isr_section”.

Now we have declared all the variables and prototype functions in our code we can implement the Reset_Handler function. On ARM Cortex M0 architecture the first instruction called is the reset IRQ. In that function we have to initialize “.data” and “.bss” sections.

  • Reset handler:
void Reset_Handler(void)
{
	unsigned long *src, *dest;
	
	src = &_etext; // place pointer to the end of .text section, data value are stored after 
                       // code section
	
	//init RAM data with value contained in FLASH
	for(dest = &_sdata; dest < &_edata; )
	{
		*(dest++) = *(src++);
	}
	
	//init .bss section with 0 value
	for(dest = &_sbss; dest < &_ebss; )
	{
		*(dest++) = 0;
	}
	
	main(); // call main application entry point
}

The last function we need to implement is the default_isr one. This is a dummy function just here in case an interrupt is triggered without the appropriate handler.

  • Default isr handler:
void default_irq(void){
    // place warning debug here
}

This is it for the startup code. The last function called by the reset handler is the main function which is the entry point of your application. So we will create a main.c file and we will place it in led_blinking/src folder.

  • Create main.c file in led_blinking/src:
        $ cd nrf51</code>
        $ mkdir led_blinking
        $ cd led_blinking
        $ mkdir src
        $ cd src
        $ touch main.c

In the main.c source file we only for the moment declare the main function.

  • main.c:
int main(void){
    return 0;
}

To resume, to properly boot up the processor we need to retrieve specific memory addresses and sections from the linker script and then declare the interrupt vectors table and finally implement the reset handler to be able to call the main function of our application.

BLE application with nRF51822 – The linker script

linker script c code
In embedded C langage development, you have to convert the humain readable file into a binary file that could be executed on microcontroller. The compiling step allows to convert this C code into a machine understandable code. But that’s not enough to get this code running on a microcontroller because the memory mapping is still not done. That’s why it’s mandatory to write the linker script in order to help you to place functions and variables at the right place in memory.

  • A quick look of the nRF51822′ memory mapping:

nrf51 memory mapping

Regarding the memory mapping above, code start in flash at 0x00000000 and end at start + 256ko (nRF51 flash size memory), this means functions could be placed between addresses 0x00000000 and 0x00040000 (hex values). The RAM memory is place at 0x20000000 and end at 0x20000000 + 16ko (nRF51 RAM size memory), this means variables could be placed between addresses 0x20000000 and 0x20002000.

Basically with GCC there are three sections. The first one is called .text, and contains functions. The second one is called .data and contains global and static variables initialized by application, and values are placed at compilation in flash. This means that during boot these variables should init from FLASH to RAM. The third one is called .bss and contains non initialized global and static variables. This means that during boot these variables should be explicitly set to zero. So now let’s see how we can specifies these addresses to linker script.

  • First we are gonna create the linker script file:
        $ cd nrf51</code>
        $ mkdir linker_script</code>
        $ cd linker_script</code>
        $ touch nrf51_ld_script.ld</code>
  • Once the file is created, open it and add the followingnRF51822′ memory layout description:
        MEMORY
        {
             RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 16k
             FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256k
        }

The first memory region is the RAM with orgin 0x20000000 and length 16ko, x parameter means this region can execute function, r and w parameters mean that we can read and write in it. The second region is the FLASH with origin 0x00000000 and length 256ko, r parameter means that we can read in it and x means that we can execute function in it.

We also have to set manually the stack and heap sizes. To be short, the stack in the RAM area dedicated for allocated memory during function called (local variables), while heap RAM area is dedicated for dynamic memory allocation.

        _end_of_stack = (0x20000000 + 16k);/*RAM start address+16k*/
        _stack_size = 2k;
        _heap_size = 2k;
  • Once we specified the memory region we have to detail section regions:
        SECTIONS
        {
        }

Between these brackets we can declare all sections needed by application. The first section we have to declare is called .isr_vector and should be placed at the beginning of flash memory since the first instruction executed by ARM Cortex M0 is the Reset handler.

        .isr_vector : <
        {
            KEEP(*(.isr_vector)) /* vector table */
            . = ALIGN(4);
        } > FLASH</code>

KEEP command allow to tell ldscript that this section should not be eliminated (by garbage collection for example). ALIGN(4) command is used to tell to ldscript that this section is 4 bytes aligned. The >FLASH symbol means the section have to be placed in Flash memory.

  • After interrupt vectors we can directly place functions and constants located in flash:
        .text :
        { /*code and constants*/
            . = ALIGN(4);
            *(.text) /*.text sections (code)*/
            *(.text*) /*.text sections (code) */
            *(.rodata) /*.rodata sections (constants, strings, etc.)*/
            *(.rodata*) /*.rodata sections (constants, strings, etc.)*/
            . = ALIGN(4);
            _etext = .;/*Used by .data, to place initialized variable RAM values*/
        } > FLASH
  • We can now defined the section used to store uninitialized data in RAM:
        .bss :
        { /* code and constants */
            . = ALIGN(4);
            _sbss = .;/*Used by the startup code to initialize the .bss section*/
            *(.bss)
            *(.bss*)
            *(COMMON)
            . = ALIGN(4);
            _ebss = .;/*Used by the startup code to initialize the .bss section*/
        } > RAM
  • The .data section contains initialized data. This section is located in RAM but initialized values are placed in FLASH so at startup, the processor has to relocated these values in RAM.
        .data : AT ( _etext )
        {
            . = ALIGN(4);
            _sdata = .;/* Used by the startup in order to initialize the .data */
            *(.data)
            *(.data*)
            . = ALIGN(4);
            _edata = .;/* Used by the startup in order to initialize the .data */
        } > RAM
  • We can also add a DISCARD section in order to avoid some informations generated by standard librairies:
        /DISCARD/ :
        {
            libc.a ( * )
            libm.a ( * )
            libgcc.a ( * )
        }

After this post you should have a basic and functional linker script. This one works fine with ARM cortex M0 core and especially with the nRF51822 processor. Just keep in mind that with slight modifications this script can work with pretty much all ARM Cortex M family since the boot process is the same. If you need more informations about linker script I recommend you the following web site:

http://www.scoberlin.de/content/media/http/informatik/gcc_docs/ld_3.html#SEC5

If you want to learn more about the Cortex M0 boot up process:

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0553a/index.html

You can retrieve linker script file directly on my github repo:

https://github.com/jocelynmass/nrf51

BLE application with nRF51822 – Toolchain setup

In this short post I will explain you how to setup Mac OS X environnement development with GCC for ARM Cortex M0 architecture (for lazy guys ;-) you can checkout my git repository on https://github.com/jocelynmass/nrf51). All the commands have to be launched in a Mac OS terminal.
  • Create your project folders:
        $ mkdir nrf51
        $ cd nrf51</code>
        $ mkdir toolchain</code>

Download ARM Cortex M0 toolchain in nrf51/toolchain/ folder on https://launchpad.net/gcc-arm-embedded and select gcc-arm-none-eabi-4_8-2014q1-20140314-mac.tar.bz2. Unzip it in nrf51/toolchain. As you can see that toolchain is also available for Linux and Window.

  • Uncompress the toolchain:
        $ tar -jxvf gcc-arm-none-eabi-4_8-2014q1-20140314-mac.tar.bz2
        $ mv gcc-arm-none-eabi-4_8-2014q1 arm_cm0
  • Now you should be able to check toolchain version
        $ arm_cm0/bin/arm-none-eabi-gcc --version
        $./arm-none-eabi-gcc (GNU Tools for ARM Embedded Processors) 4.7.3 20130312 (release)        [ARM/embedded-4_7-branch revision 196615]
Here we are! After this post every one should be able to compile projects for ARM Cortex M0 with GCC. In the next post I’ll talk about the linker script.

BLE application with nRF51822 – Introduction

In wireless communication protocols area, there are a lot of « standard » and let’s be honest, it’s pretty difficult to know what is the best fit for DIY project. The goal of this tutorial is to explain in painless way how developers can bring Bluetooth Low Energy technology in home projects thanks to nRF51822 Smart Beacon Kit.


nrf51 Nordic Semiconductor bluetooth smart beacon
nRF51822 Smart Beacon Kit


nRF51 is a BLE products family provided by Nordic Semiconductor.  It embed a ARM cortex M0 and a 2.4GHz radio frontend. Below the complete specifications of this chip:
  • 32-bit ARM Cortex M0 processor
  • 256kB flash / 16kB RAM
  • Full set of digital interfaces including: SPI/2-wire/UART
  • 10-bit ADC
  • 128-bit AES ECB/CCM/AAR co-processor
  • Quadrature demodulator
  • Low cost external crystal 16MHz 40ppm
  • Low power 16MHz crystal and RC oscillators
  • Ultra low-power 32kHz crystal and RC oscillators
  • Wide supply voltage range (1.8 V to 3.6 V)
  • On-chip DC/DC buck converter
  • Flexible and configurable 31 pin GPIO
  • Programmable Peripheral Interface PPI
  • Multi-protocol 2.4GHz radio
Nordic also provides BLE stack in order to develop quickly BLE applications. This means that developers don’t need to think about stack layers (HCI/L2CAP/GATT/GAP as you can see on the image below).
bluetooth and bluetooth low energy stack description
Indeed developing Bluetooth/BLE stack from scratch could be really difficult and a huge source of troubles. This evaluation board is an ideal product for every one who wants to develop on ARM based project. In the next post I will explain how to setup development environment on Mac OSX with GCC.