BLE using nRF51: Creating a BLE Peripheral
How to create a custom BLE GATT peripheral using the nRF51 by creating a service and characteristics.
How to create a custom BLE peripheral using the nRF51 by creating a service and characteristics.
Overview
This is part of a series of articles on the nRF51. The nRF51 is a system-on-chip with a Cortex M0 and a BLE radio chip all in one. This article demonstrates how to create a peripheral by setting up a BLE service and two characteristics.
Previous article: BLE using nRF51: ARM-GCC Build Environment
Requirements
- Device that has the nRF51
- Used in article: nRF-Dongle
- Build environment, tutorial here.
- Mobile app: Master Control Panel by Nordic
- Used in article: Android v5.1.1
BLE Review
All pre-defined BLE peripherals have certain properties associated with them. For example, all heart rate monitors need to support a data field that contains the heart rate data. These properties make up a BLE profile. This way, all devices made by different companies can be interoperable. The profile contains definitions for services and characteristics. A profile has one or more services, and each service has one or more characteristics.
Services
A service is a logical grouping of similar capabilities or features of a device. The service is defined using a UUID, or unique number, that allows other devices access. A service can have several characteristics within it.
Characteristic
A characteristic is where the actual data transfer takes place. A characteristic can be up to 20 bytes long and is can be setup to read, write, or read/write. A characteristic can also be set to notify when the data has changed. This way, new data is immedately sent to the phone/tablet.
Software
The software consists of two source files and relies on the SDK softdevice for the BLE connectivity. The SDK has to be downloaded separately per the Nordic SDK license agreement. Directions of how to do this are in the build environment article. The folder structure for this project will look like the following:
custom_peripheral_service.c
This file has two functions. One is called custom_service_init that is responsible for starting a custom service and two characteristics. The second is custom_service_update_data which updates data in the characteristic so it can be read by the mobile device. In the init function, a new service is created with UUID "BLE_UUID_CUSTOM_SERVICE" (defined in "custom_peripheral_service.h") using the function sd_ble_gatts_service_add:
/*create custom service*/
BLE_UUID_BLE_ASSIGN(ble_uuid, BLE_UUID_CUSTOM_SERVICE);
err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &service_handle);
if (err_code != NRF_SUCCESS)
{
return err_code;
}
Then two characteristics are created with UUID "BLE_UUID_CUSTOM_CHAR1" and "BLE_UUID_CUSTOM_CHAR2" which are both defined in the header file. The data size of the characteristics is set to the maximum of 20 bytes with the define "CHARACTERISTIC_SIZE".
#define BLE_UUID_CUSTOM_SERVICE 0x1110
#define BLE_UUID_CUSTOM_CHAR1 0x0001
#define BLE_UUID_CUSTOM_CHAR2 0x0002
#define CHARACTERISTIC_SIZE 20
To create a new characteristic, you have to fill in certain properties for the characteristic. These include whether or not you can read and write to the data, how many bytes are available and if you can supply notifications. In the code below, CHAR1 is setup to be writeable by modifying the parameters "write_wo_resp" and "write" in the "ble_gatts_char_md_t" variable. The data is initialized to the numbers 0-19 by pointing to the variable "initial_char_values" in the property "p_value".
/*char1 will be for writing*/
memset(&char_md, 0, sizeof(char_md));
char_md.char_props.write_wo_resp = 1;
char_md.char_props.write = 1;
char_md.p_char_user_desc = NULL;
char_md.p_char_pf = NULL;
char_md.p_user_desc_md = NULL;
char_md.p_cccd_md = NULL;
char_md.p_sccd_md = NULL;
BLE_UUID_BLE_ASSIGN(ble_uuid, BLE_UUID_CUSTOM_CHAR1);
memset(&attr_md, 0, sizeof(attr_md));
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&dis_attr_md.read_perm);
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&dis_attr_md.write_perm);
attr_md.read_perm = dis_attr_md.read_perm;
attr_md.write_perm = dis_attr_md.write_perm;
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));
attr_char_value.p_uuid = &ble_uuid;
attr_char_value.p_attr_md = &attr_md;
attr_char_value.init_len = CHARACTERISTIC_SIZE;
attr_char_value.init_offs = 0;
attr_char_value.max_len = CHARACTERISTIC_SIZE;
attr_char_value.p_value = initial_char_values;
err_code = sd_ble_gatts_characteristic_add(service_handle, &char_md, &attr_char_value, &char1_handles);
if (err_code != NRF_SUCCESS)
{
return err_code;
}
The same is done for CHAR2, except the write properties are not set to 1. Instead, the read property is set to 1 with the following line:
char_md.char_props.read = 1;
The function custom_service_update_data uses the softdevice function sd_ble_gatts_value_set in order to update the data that the mobile device can read.
uint32_t custom_service_update_data(uint16_t conn_handle,uint8_t *new_data)
{
uint32_t err_code = NRF_SUCCESS;
ble_gatts_value_t gatts_value;
memset(&gatts_value, 0, sizeof(gatts_value));
gatts_value.len = CHARACTERISTIC_SIZE;
gatts_value.offset = 0;
gatts_value.p_value = new_data;
if(conn_handle!=BLE_CONN_HANDLE_INVALID)
{
err_code = sd_ble_gatts_value_set(conn_handle,
char2_handles.value_handle,
&gatts_value);
}
return err_code;
}
main.c
The main application sets up a UART debug terminal, starts the BLE stack in the softdevice, and writes to the characteristics during a connection. If the phone/tablet writes to CHAR1, it is displayed in the debug console.
ble_stack_init
This function initializes the softdevice and enables BLE.
gap_params_init
The GAP parameters are things like the device name and connection interval. The connection interval is how fast the mobile device and peripheral communicate. A faster connection allows more data throughput and lower latency at the expense of power consumption.
advertising_init
Advertising is what a peripheral does when it wants to be found by a mobile device. This function sets up how often the device advertises and how soon to give up advertising if no connection is made. This example never times out.
services_init
The services are initiated. Only the custom service is initiatied in this example by calling custom_service_init.
ble_advertising_start
Nothing actually happens until the device begins advertising. A mobile device would not be able to find the peripheral until it advertises.
The rest of the app is just an infinite loop that updates data in CHAR2. Power can be saved by using the function sd_app_evt_wait, but the data updating would have to be moved to some sort of background timer. This function sleeps the device until an event occurs.
while(1)
{
nrf_delay_ms(1000);
for(i=0; i < CHARACTERISTIC_SIZE; i++)
{
char2_data[i] = char2_data[i]+1;
}
err_code = custom_service_update_data(m_conn_handle,char2_data);
APP_ERROR_CHECK(err_code);
}
Building
Build the application by typing make in the console. Flash the device by typing make flash. This assumes you have already uploaded the S110 softdevice to the device.
make
make flash
Terminal Output
After programming, you should see the following on the terminal when connecting to the Nordic mobile app and writing some data. The steps that were taken are:
- Open Nordic application
- Open the "Scanner"
- Find the device "Custom BLE"
- Click "Connect"
- Click "Unknown Service"
- Click the up arrow next to Unknown Charateristic.
- Now you can type in data by using "New value" and "Add Value".
Conclusion
Next in the series will be how to create an Android application to read and write the characteristic data. Then, using the Android application and the knowledge from this article as a template, we can create applications to control other devices and read sensor data!
Next Article in Series: Introduction to FreeRTOS on the nRF51
Give this project a try for yourself! Get the BOM.
How can I change the service UUID to a 128bit one?