MartinBL gravatar image

Posted 2016-03-18 15:52:06 +0200

Bluetooth low energy Characteristics, a beginner's tutorial

The purpose of this tutorial is to get you started with the basics of the nRF5x and Bluetooth Low Energy (BLE). More specifically we will go through the bare minimum of steps to get your first complete BLE application up and running. My goal in this tutorial is to have as little theory as possible, but still get you up and running with a “springboard” application. While doing this we will have a brief look at the attribute protocols, how services and characteristics are structured, and how it all looks in Master Control Panel. Hopefully it will be a fun way to get started with something functional, and maybe inspire you to go further with BLE. The tutorial is intended to be a natural continuation of the "BLE Services, a beginner's tutorial".

Before we begin

Table of content

Topics that will be covered include:

    1. Before we begin
    2. Basic theory
    3. Attribute tables in Master Control Panel
    4. Description of the example
    5. Adding a characteristic
    6. Updating the characteristic and sending notifications

Equipment and software

To complete this tutorial you will need the following:

Other kits, dongles and software versions might work as well, but this is what I have used. This tutorial will not cover how to install and setup the software.

Necessary prior knowledge

This tutorial is intended to be a natural continuation of the BLE Services, a beginner's tutorial and BLE Advertising, a beginner's tutorial. It is recommended, but not necessary, to go through these tutorials first. It is expected that you have basic knowledge of how to use Keil, MCP, and nRFgo Studio. Read the tutorial Setting up an example project on the nRF51 DK to learn how to use your equipment and compile your first BLE application.

If you run into troubles please browse devzone, look for documentation on our Infocenter, and read the user guides for your kits. You can also check out the "CompletedCode" branch on github for a proposed solution. I urge you to post any questions you might have on the forum and not below the tutorial. This makes it easier for other users and Nordic employees to see and search for your questions and you will actually most likely get a faster response(!).

Change Log:

  • 2016.04.06: Minor changes to code and text to make GCC compile without errors. Clarifications regarding presentation of temperature value shown in MCP.

Some basic theory

The Attribute Protocol and the Generic Attribute Profile

My goal in this tutorial is to have as little theory as possible, but still get you up and running with a “springboard” application. An application it is easy to customize and expand upon. Yet the Attribute Protocol (ATT) and Generic Attribute Profile (GATT) are so essential to BLE that there is no way around a brief introduction. Basic knowledge of the ATT and the GATT is useful later in this tutorial, although when you make simple applications and work with Nordic’s SoftDevices and SDKs these are not concepts you have to worry much about. You should be able to skip the theory and still complete the tutorial, but as mentioned we will refer to the following concepts as we go.

Attribute Protocol (ATT)

As shown in the figure below the BLE protocol is built upon a number of layers.

BLE Layer Model

Notice how the application layer is right above the GATT which in turn is built upon the ATT. The ATT is based on a Client <–> Server relationship. The server holds information like sensor values, the state of a light switch, position data, etc. This information is organized in a table, referred to as an attribute table. Each attribute in the table is a value or piece of information with some associated properties. So when a client wants the sensor data it refers to e.g. row 11 in the table. If it wants the state of the light switch it might refer to row 16, and so on. Here is an excerpt from the Bluetooth Core Specification V4.2 (Hereafter referred to as BCS):

Vol 3:, Part F, Ch. 2, “Protocol Overview”:

The attribute protocol defines two roles; a server role and a client role. It allows a server to expose a set of attributes to a client that are accessible using the attribute protocol. An attribute is a discrete value that has the following three properties associated with it: (1) an attribute type, defined by a UUID, (2) an attribute handle, (3) a set of permissions that are defined by each higher layer specification that utilizes the attribute; these permissions cannot be accessed using the Attribute Protocol.

The attribute type specifies what the attribute represents. Bluetooth SIG defined attribute types are defined in the Bluetooth SIG assigned numbers page, and used by an associated higher layer specification. Non-Bluetooth SIG attribute types may also be defined.

Let us relate this to a typical application, namely the Heart Rate Profile. In Table 1 each and every row is an attribute, and each attribute has a handle, a type, a set of permissions, and a value.

Table 1:

HRS Attribute Table

Attribute Handles

The attribute handle uniquely identifies an attribute on a server, allowing a client to reference the attribute in read or write requests. To simplify things then handle can be considered as the row number in the attribute table. Although the handle number might not be sequential. The handles are 16-bit numbers and you will see later on that the SoftDevice use handles extensively to refer to various attributes. From a programmer’s perspective it is actually a quite effective way to pass values and information between functions. It makes it easy for your application to keep track of the attributes and to grab whatever information it needs. The handle numbers vary depending on how many attributes you have.

Attribute Types (UUIDs)

A Universally Unique ID (UUID) is a 16 or 128-bit value used to identify the type of every attribute. In Table 1 there are five different types of attributes; one of type “Service Declaration” (0x2800), two of type “Characteristic Declaration” (0x2803), one of type “Heart Rate Measurement Characteristic Value” (0x2A37), one of type “Body Sensor Location Characteristic Value” (0x2A38), and finally one of type “Descriptor Declaration” (0x2902). Six attributes in total. There are more attribute types, and later on in the tutorial we will make our own.

Attribute Permissions

Permissions define some rules of how you can interact with a specific attribute. It defines whether an attribute should be readable and/or writeable and what kind of authorization is required to do the operations. Note that these permissions only apply to the attribute value, not the handle, type, and the permission field itself. This allows a client to look through a server’s attribute table and discover what the server can provide. Although not necessarily read and write the values.

Attribute Values

The value can be anything. It can be a heart rate value measured in beats per minute, the state of a light switch, or a string like “Hello World”. And sometimes it is information about where to find other attributes and their properties. For example, in the Service Declaration attribute in Table 1 (handle 0x000C) the value holds a UUID (0x180D) identifying what kind of service it is (notice the "Assigned Number" field almost at the top of this page). The Characteristic Declaration value (handle 0x000D) holds information about the subsequent Characteristic Value Declaration (Properties, Handle, and Type). Finally, the Heart Rate Measurement Characteristic Value (handle 0x000E) eventually holds the actual number of heart beats per minute.

The Generic Attribute Profile (GATT)

The concept of the GATT is to group attributes in an attribute table together in a very specific and logical order. The heart rate profile in Table 1 is an example of such a group.

Service Declaration attribute

At the top of each group you always have a Service Declaration attribute. Its type is always 0x2800, the standard UUID for Service Declarations. Its handle depends on how many attributes are already present in the table. Its permissions are always Read Only without any authentication or authorization required. The value is another UUID defining what kind of service this is. In Table 1 the value is 0x180D, the Bluetooth SIG defined UUID for a Heart Rate Service. Later in the tutorial we will make our own attribute table with a service declaration containing our own custom UUID.

Characteristic Declaration

Immediately after this follows a Characteristic Declaration (there is a rare exception to this rule, but that is out of scope for this tutorial). The Characteristic Declaration is similar to the Service Declaration. The type is always 0x2803, the standard UUID for Characteristic Declarations. And the permissions are always Read Only without any authentication or authorization required. The value however, contains some interesting data. It always contains a handle, a UUID, and a set of properties. These three elements describe the subsequent Characteristic Value Declaration. The handle naturally points to the Characteristic Value Declaration’s place in the attribute table. The UUID describes what type of information or value we can expect to find in the Characteristic Value Declaration. For example, a temperature value, the state of a light switch, or some custom arbitrary value. And finally, the properties describe how the characteristic value can be interacted with. Table 2 shows the characteristic properties bit field. Don’t worry about understanding the details of the table now. We will circle back to it later.

Table of Properties

Now you might wonder why we have read/write permissions for an attribute and read/write properties for the characteristic value. Shouldn’t they always be the same? And that is a legitimate question. The properties for the characteristic value are actually only guidelines for the client, used in the GATT and application layers. They are just clues, if you will, of what the client can expect from the Characteristic Value Declaration attribute. The permissions for the attribute (on the ATT layer) will always overrule the characteristic value properties (on the GATT layer). Now you might ask again “but why do we need both permissions and properties?”. And the simple, but disappointing, answer is: “Because the Bluetooth Core Specification says so”. It is confusing, but has implications for how we will set up our characteristic later so it needs to be said.

Characteristic Value Declaration

After the Characteristic Declaration follows the Characteristic Value Declaration. This is the attribute that finally contains the actual value. And again, the value might be a temperature value, the state of a light switch, etc., etc. The Characteristic Value Declaration’s type is the same as specified in the Characteristic Declaration’s value field and the permissions are defined in the application layer (or us).

Descriptor Declaration

After the Characteristic Value Declaration follows either

  1. a new Characteristic Declaration (there can be many characteristics grouped in a service).
  2. a new Service Declaration (there can be many services in a table).
  3. a Descriptor Declaration.

In the case of the Heart Rate Measurement Characteristic in Table 1 the Characteristic Value Declaration is followed by a Descriptor Declaration. The descriptor is an attribute with additional information about the characteristic. There are several kinds of descriptors, but in this tutorial we will only deal with the Client Characteristic Configuration Descriptor (CCCD). More about this later.

Attribute tables in Master Control Panel

Let us now compare what we just learned with the information displayed in MCP when we run the Heart Rate Service example from the SDK. After you have connected your device and ran a service discovery the MCP is actually showing the entire attribute table. Each line is an attribute and they are grouped in services and with characteristics as subgroups. The following screenshots show the data displayed in MCP when connected to my nRF51 DK board running the HRS example.

Figure 1:

Service Declaration

In Figure 1 above I have marked the Service Declaration attribute. As you can see inside the red square the UUID of this attribute is 0x2800, the handle is 0x000C and the value is 0x180D (Least Significant Byte first (LSB) in the value field). This is exactly what we would expect after going through the attribute table in Table 1. The UUID shows that the attribute is a service declaration and the value shows that it is a Heart Rate Service.

Figure 2:

Characteristic Declaration

In Figure 2 I have marked the first Characteristic Declaration attribute. As expected, the UUID is 0x2803 showing us that this is indeed a characteristic declaration. The value here is interesting. Although the meaning of the values is written in plain text to the right let's decode the value ourselves anyway. The value shown in the 'Value:' field is a number of bytes shown LSB first. To make it more readable we will reorder them to Most Significant Byte (MSB) first: 2A37-000E-10. You might already have noticed that 2A37 is the Heart Rate Measurement characteristic UUID and 000E is the handle identifying where this characteristic value declaration can be found in the attribute table. 0x10 is defining the properties. Comparing the value 0x10 to the bit field in Table 2 we can see that this represents Notification. So everything is still consistent with the attribute table in Table 1.

Figure 3:


In Figure 3 you can see that the characteristic value declaration's UUID is indeed 0x2A37 and the handle value is 0x000E, as specified in the characteristic declaration in Figure 2.

Figure 4:


Finally, in Figure 4 you can see the values of the CCCD attribute. You can see the predefined Descriptor UUID (0x2902) and that the value is 0x0000 This means that notification is currently switched off. More on this later.

The example

It is time to get our hands dirty! I figured that as an example we will make a readable and writeable characteristic first. Then, after we have enjoyed manually reading and writing some data, we will include the temperature of the nRF5x's CPU core in the characteristic. In case you didn't know already the nRF5x actually has its own internal temperature sensor. It is not very accurate, but it is the simplest way for us to get started with some actual dynamic sensor values. Finally, we will enable notification to update the temperature values. First thing first: download the example code. To compile it, extract the example to "your_SDK_folder\examples\ble_peripheral". If you need help with this please have a look at this thread on devzone: Compiling github projects. Open the project file and hit “Build”. It should compile without any errors and if you browse through the files you will see that it pretty much continues where the service tutorial ended.

The important files in this tutorial are our_service.c, our_service.h, and of course main.c. In these files you will find various comments beginning with:

// OUR_JOB: Step X.X

FROM_SERVICE_TUTORIAL means that this is code we looked at in the previous tutorial. A few places you will see ALREADY_DONE_FOR_YOU. This marks very basic code that I have already prepared for you so that we don't have to go through the less important stuff in detail. Finally, OUR_JOB: Step X.X indicates what and when things need to be done in the tutorial.

To-do list

This is what needs to be done to get our characteristic up and running:

  1. Step 1: Add service. This was completed in the last tutorial when we made our own custom service with a custom base UUID and a service UUID.
  2. Step 2: Declare and configure the characteristic.
  3. Step 3: Add a CCCD to let the characteristic send notifications at regular intervals or whenever the temperature values are changing.

This is what our attribute table should include to achieve all this:

Table: 3

Our Attribute Table

Step 1: Add the service

As mentioned above this step was completed in the last tutorial, but let us take a quick look at what our work looks like in MCP so far. It should look something like this:

Service Declaration

The highlighted line is the Service Declaration declaring our service. As you can see the attribute type is 0x2800, the attribute handle is 0x000C, and the attribute value is our 128-bit custom UUID.

Step 2: Add the Characteristic

Calling the SoftDevice function sd_ble_gatts_characteristic_add() is our first goal. This function will add both the Characteristic Declaration and the Characteristic Value Declaration to our attribute table. However, in order to get there we have to do a few things first. sd_ble_gatts_characteristic_add() takes four parameters:

@param[in]  uint16_t                        service_handle.
@param[in]  ble_gatts_char_md_t const *     p_char_md
@param[in]  ble_gatts_attr_t const *        p_attr_char_value
@param[out] ble_gatts_char_handles_t *      p_handles

As you can see the function has three input parameters and one output parameter. The three input parameters need to be populated with details that will define the characteristic attributes. These parameters define the properties, read/write permissions, descriptors, characteristic values, etc. In fact, you have more than 70 properties at your disposal when you are defining your characteristic. It sounds daunting, but don't give up and throw away your dev kit just yet. Most of the parameters are optional and almost all of them work just fine if you initialize them to zero using memset(&parameter, 0, sizeof(parameter));. Pretty neat!

We will start by populating only the essential parameters we need to get started. In fact, all we need to do is to choose where in memory to store the characteristic attributes and define a characteristic value type using our custom UUID. In the function our_char_add() I have already declared all the necessary variables for you and initialized them to zero. The order of which I have declared them might be a little confusing, but since the necessary parameters are nested into structures, and these structures might be nested into other structures, this is unfortunately the way it has to be. I have also selected names that might seem a little cryptic, but these are naming conventions used throughout the SDKs and I chose to go for consistency.

The three most important variables are:

  1. ble_gatts_attr_md_t attr_md, The Attribute Metadata: This is a structure holding permissions and authorization levels required by characteristic value attributes. It also holds information on whether or not the characteristic value is of variable length and where in memory it is stored.
  2. ble_gatts_char_md_t char_md, The Characteristic Metadata: This is a structure holding the value properties of the characteristic value. It also holds metadata of the CCCD and possibly other kinds of descriptors.
  3. ble_gatts_attr_t attr_char_value, The Characteristic Value Attribute: This structure holds the actual value of the characteristic (like the temperature value). It also holds the maximum length of the value (it might e.g. be four bytes long) and it's UUID.

So let's start populating the parameters. To make things a little easier I have tried to assign each step a number:

Step 2.A, Use custom UUID to define characteristic value type

The characteristic will need its own UUID just as the service did. And in fact, we can add this the exact same way as we did with the service UUID. So type in the following at Step 2.A inside the our_char_add() function:

uint32_t            err_code;
ble_uuid_t          char_uuid;
ble_uuid128_t       base_uuid = BLE_UUID_OUR_BASE_UUID;
char_uuid.uuid      = BLE_UUID_OUR_CHARACTERISTC_UUID;
err_code = sd_ble_uuid_vs_add(&base_uuid, &char_uuid.type);

This will use the same base UUID as the service, but add a different 16-bit UUID for the characteristic. The UUID is defined as 0xBEEF in our_service.h. Note that this base UUID was added to the vendor specific table in the previous tutorial when we created the custom service. All subsequent calls with the same base UUID will return a reference to the same ID in the table. This way we will save some memory by not needing to store a large array of 128-bit long IDs.

Step 2.B, Configure the Attribute Metadata

The following three lines are the bare minimum of what we need to describe the attributes of the characteristic. The only thing the lines do is to decide where in memory to store the attributes. We are going to store it in the SoftDevice (aka the stack) controlled part of memory and hence, we use BLE_GATTS_VLOC_STACK. The only other valid option is to use BLE_GATTS_VLOC_USER to store the attributes in the user controlled part of memory.

ble_gatts_attr_md_t attr_md;
memset(&attr_md, 0, sizeof(attr_md));
attr_md.vloc        = BLE_GATTS_VLOC_STACK;

In the attribute metadata structure ble_gatts_attr_md_t you also have the option to define the permissions with associated authorization requirements. For example if you need Man In The Middle protection (MITM) or a passkey to access your attribute. We will circle back to this.

Step 2.C, Configure the Characteristic Value Attribute

Now that we have made our self a new UUID and decided where to store the characteristic we will store this information in the Characteristic Value Attribute:

ble_gatts_attr_t    attr_char_value;
memset(&attr_char_value, 0, sizeof(attr_char_value));    
attr_char_value.p_uuid      = &char_uuid;
attr_char_value.p_attr_md   = &attr_md;

Step 2.D, Add handles for the characteristic to our struct

We need to add a variable holding the relevant handles for our characteristic in our service structure. So head over to the ble_os_t definition in our_service.h and add the line shown below:

typedef struct
    uint16_t                    conn_handle; 
    uint16_t                    service_handle;        
    // OUR_JOB: Step 2.D, Add handles for our characteristic
    ble_gatts_char_handles_t    char_handles;

As you can see, the ble_os_t struct already has a field for the service declaration handle. The connection handle, conn_handle, is there to keep track of the current connection and don't really have anything to do with the attribute table handles. If you go to the definition of ble_gatts_char_handles_t you can see that our new variable can hold 16-bit handles for the characteristic value, user descriptor, its CCCD, and also something called Server Characteristic Configuration Descriptor (SCCD) which is not within the scope of this tutorial.

Step 2.E, Add the new characteristic to the service

Then we are ready to add the new characteristic to our attribute table and it is as simple as this:

err_code = sd_ble_gatts_characteristic_add(p_our_service->service_handle,

As you can see, we are giving the SoftDevice information about what service the characteristic belongs to (the service_handle), the Characteristic Metadata, and the Characteristic Value Attributes. The stack then processes the parameters and initiates the characteristic. Then it stores the handle values of our characteristic into our p_our_serice structure.

Now compile and download your application to the kit. Make sure that you have remembered to program the SoftDevice to the kit as well. Open MCP, connect to your kit, and do a service discovery. You should see something like this:

Minimal characteristic

As you can see there is a new characteristic in our service, but it doesn't do anything useful at all. It has no value and you can neither read from it nor write to it. You can try for yourself by selecting the characteristic value line, type in some hexadecimal numbers in the "Value:" field, and hit the "Write" or "Read" button. MCP will send a write request or attempt a read, but since we have not defined any read or write permissions yet your kit will deny any read or write requests by default. This is what happens when we initialize the read/write attribute permissions to zero. The log window in MCP should show something like this:

Read and write not permitted

Step 2.F, Add read/write properties to our characteristic value

To be able to read and write to the characteristic there are a couple of things we need to do. First go to Step 2.F in our_char_add() and add the following lines:

ble_gatts_char_md_t char_md;
memset(&char_md, 0, sizeof(char_md)); = 1;
char_md.char_props.write = 1;

This will populate the characteristic metadata with read and write properties. Do another service discovery in MCP and note that the "Properties" field in the Characteristic Declaration has changed to "Read, Write":

Read write properties

Now try to read and write some values to the characteristic again.

But, wait, what is this?? You are still getting READ and WRITE_NOT_PERMITTED errors?

You should, if you have followed the tutorial to the letter so far. This is because what we just did was just setting the properties in the characteristic declaration and as discussed earlier they are just guidelines. So even though these properties are exposed when your MCP does a service discovery we have yet to set the required permissions for read and write operations. Before we have done this the SoftDevice doesn't know what to allow and simply denies any reads and writes of the characteristic.

Step 2.G, Set read/write permissions to our characteristic

So let's add some read/write permissions. Since this is a beginners tutorial we will keep it simple and leave the doors wide open. No security, encryption, or passkey needed. A simple way of doing this is to use the macro BLE_GAP_CONN_SEC_MODE_SET_OPEN(). Go to Step 2.G in our_char_add() and add the following two lines:


Now do yet another service discovery and note how the word "Value: " is added to the characteristic value line in MCP:

Value field

When you try to read from the characteristic now then this should pop up in the log window in MCP:

Read attribute value

Great success! We have just read 0, zero(!), bytes from our new characteristic because we have not yet assigned a value or a value length to it.

Next, try to write e.g. the value 0x12 to the characteristic. You should get the following error because the value attribute length is initialized to zero:

Write attribute value

Step 2.H, Set characteristic length

So how do we fix the errors above? Go to Step 2.H and add the following lines:

attr_char_value.max_len     = 4;
attr_char_value.init_len    = 4;
uint8_t value[4]            = {0x12,0x34,0x56,0x78};
attr_char_value.p_value     = value;

This will set the initial length, and also the maximum allowed length, of the characteristic value to 4 bytes. And just to be a little thorough we will also set the initial value to 12-34-56-78. Once again, try to read from your characteristic and then write a couple of bytes. You should see that the value is updated. Even if you disconnect and reconnect the new value should be retained.

Challenge 1:

  1. Try to write a single byte to the characteristic. E.g. '12'.
  2. Try to write four bytes to the characteristic. E.g. '12-34-56-78'.
  3. Try to write five or more bytes to the characteristic. E.g. '12-34-56-78-90'.

What happens? Why are you not allowed to write more than four bytes to your characteristic and how can you fix it?

Challenge 2:

Try to experiment with the attr_md permissions and the following permission macros:


Try to do some reads and writes. See what happens and watch out for error messages in the MCP log window. Hint on number 3: If you click "Bond" in MCP and bond with your kit you will automatically add encryption (ENC) to your BLE link. However, you are not required to use Man In The Middle (MITM) protection when bonding.

Step 3: Client Characteristic Configuration Descriptor (CCCD)

So this is pretty cool, right? Not cool enough though. Let us add some dynamic temperature data, and while we are at it, let's also make our kit a little more independent and useful by making it periodically push data to our client. To achieve this we will need to:

  1. Add some sort of push functionality.
  2. Do some housekeeping of our BLE connection and services.
  3. Set up a timer to trigger the temperature updates.

The BCS defines two ways of "pushing" data:

Vol 3: Part G, Ch. 4.10 & 4.11:

Indication - This sub-procedure is used when a server is configured to indicate a Characteristic Value to a client and expects an Attribute Protocol layer acknowledgement that the indication was successfully received.

Notification - This sub-procedure is used when a server is configured to notify a Characteristic Value to a client without expecting any Attribute Protocol layer acknowledgment that the notification was successfully received.

The subtle, but important difference here is that by using indication your kit (the server) will require an application level acknowledgment in return after every transmission of updated data. By using notification, on the other hand, our kit will just "mindlessly" transmit data and not care about whether it is acknowledged by the application in the other end or not. So sticking to the "keep it simple"-philosophy of this tutorial we will use the latter. To add notification functionality we need to add a descriptor to our attribute table, namely the Client Characteristic Configuration Descriptor (CCCD). The BLE Core Specifications has the following to say about the CCCD:

Vol 3: Part G, Ch

The Client Characteristic Configuration declaration is an optional characteristic descriptor that defines how the characteristic may be configured by a specific client [...]

What is written in between these cryptic lines is that the CCCD is a writable descriptor that allows the client, i.e. your MCP or phone, to enable or disable notification or indication, on your kit.

Step 3.A, Configuring CCCD metadata

This is what we need to do to add the CCCD to our characteristic.

ble_gatts_attr_md_t cccd_md;
memset(&cccd_md, 0, sizeof(cccd_md));
cccd_md.vloc                = BLE_GATTS_VLOC_STACK;    
char_md.p_cccd_md           = &cccd_md;
char_md.char_props.notify   = 1;
  1. We declare a metadata structure for the CCCD to hold our configuration.
  2. Using the same method as we did on the characteristic, we make sure that no security, encryption or passkeys are necessary to read or write to the CCCD.
  3. Also as we did with the characteristic, we decide where to store the descriptor, and once again we store it in the SoftDevice controlled part of memory.
  4. Then we store the CCCD metadata structure in our characteristic metadata structure.
  5. Enable notification by setting the notify property bit.

Compile, download, run, and connect to your kit. Now you should be able to see the CCCD added to your characteristic:

image description

Notice also that "Notify" is added to the Characteristic Declaration's properties field. The UUID value of the CCCD is 0x2902 as we have seen earlier and the value is 0x0000 meaning that notification and indication is currently switched off. If you want you can enable notification right away, but since we have not yet set up any mechanisms updating the values nothing fancy will happen. Anyway, try to click the "Enable services" button. You should see that the value field of the CCCD is updated to 0x0001, meaning that notification is indeed enabled. You can also use the "Write" button and write to the CCCD manually.

Step 3.B, Housekeeping part 1: Give our service connection handle a default value

In a simple example like ours housekeeping like this might actually not be necessary. However, it is crucial when developing slightly more advanced applications. Furthermore, since this sort of housekeeping is done in virtually every BLE examples in Nordic's SDKs I thought I would make an exception from our "keep it simple"-philosophy and show you how it can be done.

Our service structure, the ble_os_t, has a field called "conn_handle". This shall hold the handle of the current connection as provided by the BLE stack and we have to initialize it during system startup. Naturally, since we are not in a connection at system startup, the handle should be initialized to some "invalid" value. In the SDKs this value is defined as BLE_CONN_HANDLE_INVALID (with value 0xFFFF). So head over to the our_service_init() function in our_service.c and type in the following.

p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID;

Step 3.C, Housekeeping part 2: Responding to connect and disconnect events

In main.c there is a function called ble_evt_dispatch(). This function is called by the SoftDevice when a BLE stack event has occurs. To keep our service up to date with the latest connection events we will call ble_our_service_on_ble_evt() from the dispatch function:

ble_our_service_on_ble_evt(&m_our_service, p_ble_evt);

Step 3.D, Housekeeping part 3: Handling BLE events related to our service

Go back to our_service.c and find the function ble_our_service_on_ble_evt(). Inside we will make a short switch case statement. In our example the only thing the statement will do is to update the connection handle stored in the service structure. On a connection event we will store the current connection handle as provided by the BLE stack. On a disconnect event we will set the handle back to "invalid".

switch (p_ble_evt->header.evt_id)
        p_our_service->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
        p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID;
        // No implementation needed.

Step 3.E, Update characteristic value

We are closing in on our goal. Now find the function our_termperature_characteristic_update(). This is where we will implement the notification. First implement an if-statement like this:

if (p_our_service->conn_handle != BLE_CONN_HANDLE_INVALID)


Here we check whether or not we are in a valid connection, and this is where the housekeeping comes in handy. If you try to send out notifications when not in a connection the SoftDevice will get grumpy and give you errors. That is why we need this if-statement and why we implemented the housekeeping to keep the connection handle updated. Now, inside the if-statement type in the following:

uint16_t               len = 4;
ble_gatts_hvx_params_t hvx_params;
memset(&hvx_params, 0, sizeof(hvx_params));

hvx_params.handle = p_our_service->char_handles.value_handle;
hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
hvx_params.offset = 0;
hvx_params.p_len  = &len;
hvx_params.p_data = (uint8_t*)temperature_value;  

sd_ble_gatts_hvx(p_our_service->conn_handle, &hvx_params);

First you might wonder what hvx stands for? It is not very intuitive, but it stands for Handle Value X, where X symbolize either notification or indication as the struct and function can be used for both. So to do a notification we declare a variable, hvx_params, of type ble_gatts_hvx_params_t. This will hold the necessary parameters to do a notification and provide them to the sd_ble_gatts_hvx() function. Here is what we will store in the variable:

  1. handle: The SoftDevice needs to know what characteristic value we are working on. In applications with two or more characteristics naturally we will need to reference the handle of the specific characteristic value we want to use. Our example only has one characteristic and we will use the handle stored in p_our_service->char_handles.value_handle.
  2. type: The SoftDevice needs to know what "hvx type" we want to do; a notification or indication. As we are doing a notification we use BLE_GATT_HVX_NOTIFICATION. The other option would be BLE_GATT_HVX_INDICATION.
  3. offset: Your characteristic value might be a sequence of many bytes. If you want to transmit only a couple of these bytes and the bytes are located in the middle of the sequence you can use the offset to extract them. Since we want to update all of our four bytes we will set the offset to zero.
  4. p_len: The SoftDevice needs to know how many bytes to transmit. There is no need to send 20 bytes every time if you only have four bytes of relevant data. As an example, let's say you have a characteristic with the following sequence of bytes: 0x01, 0x02, 0x03, 0x04, 0x05 and you want to send just the 3rd and the 4th byte. Then set offset to 2 and len to 2.
  5. p_data: Here we add a pointer to the actual data.

Finally we pass this structure into the sd_ble_gatts_hvx(). We also provide the function with the relevant connection handle. In some applications you might work with several concurrent connections and this is why the function also needs to know what handle to use. Now we have set up everything that has to do with the characteristic and notification.

Step 3.F, Update the characteristic with temperature data

The rest is simply a matter of providing some data to send. We need to measure and collect the data we want to send and we need some mechanism to trigger each measurement. So find and uncomment the function timer_timeout_handler() in main.c and type in the following:

int32_t temperature = 0;   
our_termperature_characteristic_update(&m_our_service, &temperature);
  1. temperature: We need a temporary variable to store the measured temperature.
  2. sd_temp_get(): This is a very simple SoftDevice function that does everything for us. Just pass a pointer to our temperature variable and your measurements are done. Note that the result of the sd_temp_get() function is the nRF5 die temperature in 0.25 degrees celsius as documented here. I will leave it up to you to decide how to present the value. You can e.g. make a function to convert the value to Fahrenheit.
  3. our_termperature_characteristic_update(): Make a call to the characteristic update function we made in Step 3.E and pass along a pointer to our service variable and the measured temperature.
  4. nrf_gpio_pin_toggle(): Toggle LED 4 on the nRF5x DK kit just for show.

Step 3.G, Declare a timer ID and a timer interval

Not quite there yet. We need something to trigger the measurements. A trigger could for example be the push of a button or some command received from the client side. In our case though, we will use a timer to periodically measure the temperature. The first thing we need to do is defining the timer interval and we need a timer ID to identify this particular timer. So, locate Step 3.G somewhere at the top of main.c and type in the following:


This will instantiate a timer ID variable and define a timer interval of 1000 ms.

Step 3.H, Initiate the timer

To initialize our timer this is all we have to do:

app_timer_create(&m_our_char_timer_id, APP_TIMER_MODE_REPEATED, timer_timeout_handler);

We pass:

  1. Our timer ID
  2. An enumeration called APP_TIMER_MODE_REPEATED. This tells the timer library to set up a timer that triggers at regular intervals. The other option is APP_TIMER_MODE_SINGLE_SHOT which sets up a timer that triggers only once.
  3. A pointer to our timer timeout handler which is supposed to be executed on every timer event.

Step 3.I, Start our timer

Now it is time to start our engines!

Most often in Nordic's libraries the fact that a module is initiated does not mean that it is started, so we have to call one last function to reach our goal:

app_timer_start(m_our_char_timer_id, OUR_CHAR_TIMER_INTERVAL, NULL);

We pass:

  1. Our timer ID variable
  2. Our defined timer interval
  3. You have the option to pass a general purpose pointer that will be passed to the timeout handler when the timer expires. We won't bother with this so we will just ignore it and exercise our right to pass a simple NULL.

Compile, download, connect, and discover services. Now to the moment of truth: Click "Enable services". If we have done everything right the characteristic value should update every second. You should see LED 4 and the value line in MCP blink green every second. You should also see values ticking in in the Log window in MCP like this:

image description

If you now put your finger on the nRF5 chip you should also see that the values are changing. The reason why the values are Least Significant Byte first is discussed here.

Challenge 1:

Try to alter our_termperature_characteristic_update() so that you only send a notification when the temperature has changed. Remember that BLE is all about saving energy so why spend resources on transmitting the same value over and over again? Hint: Use a variable to store the current temperature value and compare it to the new one on the next measurement.

Challenge 2:

Try to modify the timer so that the temperature value is only measured when you are in a connection. I.e. start the timer on a connection event and stop it on a disconnect event. Why spend energy on measurements if you don't use them, right? Hint: Look for a timer start and stop function in the app_timer library. Then see if you can do some magic in the on_ble_evt() function in main.c.


And that is it! You have now made a custom attribute table with a basic characteristic transmitting data from your server to your client. It is somewhat limited though, in the sense that it is only one way communication and it has no security or other advanced features. However I hope that I achieved my goal; that you gained at least some new knowledge and that the code is something you can expand upon.

Once again I urge you to post any questions you have in the Questions-section on the forum, not here. You will most likely get faster response that way. Positive or negative critique though is very welcome in the comment section below. Also remember to check out the "CompletedCode" branch on github if you have any issues.


wolfman24 gravatar image

Posted Dec. 5, 2015, 3:42 p.m.

Hey Nordic! just wanted to say thanks for all the previews and tutorials, they have been a great big help on me trying to finish my project. I'm here on the characteristics aspect (obviously) and i understand the code for the most part just not really sure where it all goes. If you guys could post the code for this tutorial, even though i know you're still working on it, that would save me a lot of time. Thanks again!

Travistsui gravatar image

Posted March 11, 2016, 5:05 p.m.

Thanks Martin for this helpful characteristic tutorial! I have question related to "Challenge 1:

Try to alter our_termperature_characteristic_update() so that you only send a notification when the temperature has changed. Remember that BLE is all about saving energy so why spend resources on transmitting the same value over and over again?"

I have tried few methods but it still did not work properly. Can you help to solve the my problem?

rph8 gravatar image

Posted March 14, 2016, 10:56 p.m.

There is another error in Step 3D:

case BLE_GAP_EVT_DISCONNECTED: p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID;

is missing a break statement after the case and before the default. In this very specific case one may not notice the difference at once, but it will lead to a bug when default is anything else than a nop.

MartinBL gravatar image

Posted March 17, 2016, 1:51 p.m.

@Travistsui: You can do something like this: static int32_t previous_temperature_value = 0;

// If new temperature value is different from previous value then send notification
if(*temperature_value != previous_temperature_value )
    // Send value if connected and notifying
    if (p_our_service->conn_handle != BLE_CONN_HANDLE_INVALID)
        uint16_t               len = 4;
        ble_gatts_hvx_params_t hvx_params;
        memset(&hvx_params, 0, sizeof(hvx_params));

        hvx_params.handle = p_our_service->char_handles.value_handle;
        hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
        hvx_params.offset = 0;
        hvx_params.p_len  = &len;
        hvx_params.p_data = (uint8_t*)temperature_value;  

        sd_ble_gatts_hvx(p_our_service->conn_handle, &hvx_params);

// Store current temperature value until next updated. 
previous_temperature_value = *temperature_value;

@Raphael: I believe this is a bug. At least it has been reported as such internally. However, sd_ble_uuid_vs_add() might not be intended to be called more than once. You can see e.g. in the ble_app_uart tutorial that it is only called once even though the UUID is reused. The returned uuid_type is then stored in the ble_nus_t service structure and passed around to where it is needed. I chose to call it twice instead for consistency in the code and to make the ble_os_t service structure as short as possible. Anyway, I believe the bug is fixed in the new SDK 11 and S13x V2.0.0.

MartinBL gravatar image

Posted March 23, 2016, 9:19 a.m.

Thanks Raphael. I fixed the break; bug now.

Revo gravatar image

Posted April 5, 2016, 11:39 a.m.

Hi all, i followed the tutorial and did the challanges, however there is a detail i don't understand (totally newbie here!): the value of temperature that i read on the master control panel (phone app) is in hexadecimal and Fahrenheit (this is a supposion, it's right?), i tried to implement the conversion in Celsius by subtracting 32 and dividing 1.8, but what i read on the master control panel is " (0x) 00-00-51-07 ". Can you explain me why, and how to get the correct Celsius value and receive it in decimal (so a conversion from hex to decimal i guess?) on my control panel? Thanks in advance!

MartinBL gravatar image

Posted April 5, 2016, 1:44 p.m.

@Revo: I answered your question here.

brookyoh1 gravatar image

Posted April 5, 2016, 10:48 p.m.

@MartinBL , i like the tutorial but i need armgcc/makefile is the makefile for this project available ? i dont see it in github ?

MartinBL gravatar image

Posted April 6, 2016, 1:37 p.m.

Added GCC support now.

brookyoh1 gravatar image

Posted April 7, 2016, 2:53 p.m.

@MartinBL , the current or default UUID is ABCDxxxx-EF12-3456-7890-ABCDEF123456 as a "base UUID" then we can change xxxx for characteristic.

is it possible to make the base uuid like this ABCD3456-xxxx-3456-7890-ABCDEF123456 to change the xxxx position ?

MartinBL gravatar image

Posted April 8, 2016, 5:18 p.m.

The 16-bit UUID has to be inserted right after ABCD in your example. However, you can use the same 16 UUID (e.g. 0x3456) for both characteristics, but use different base UUIDs and that way I think you can achieve what you want.

brookyoh1 gravatar image

Posted April 19, 2016, 12:27 a.m.

@MartinBL , your idea makes sense but the problem that am having is i can set the same charcterstic for both that is fine.

but when i use a diffrent base UUID for the char it doesnt work

 static uint32_t our_char_add(ble_os_t * p_our_service)
    //Add a custom characteristic UUID   step 2.A
    uint32_t            err_code;
    ble_uuid_t          char_uuid;
    ble_uuid128_t       base_uuid = UUID_DIFFERENT_FROM_BASE; ***//base address here is diffrent***
    char_uuid.uuid      = BLE_UUID_DEFAULT_UUID2;
    err_code = sd_ble_uuid_vs_add(&base_uuid, &char_uuid.type);

so as showed above i want different UUID for my char like : ABCD3456-xxxx-3456-7890-ABCDEF123456

3456 can be made the same for all , but to have a diffrent xxxx for the charcterstic defining diffrent base uuid dont work at all (it stops advertising)

the problem is it expects the base uuid defined to be the same as this one below

void our_service_init(ble_os_t * p_our_service)
    uint32_t   err_code; // Variable to hold return codes from library and softdevice functions

    // FROM_SERVICE_TUTORIAL: Declare 16-bit service and 128-bit base UUIDs and add them to the BLE stack
    ble_uuid_t        service_uuid;
    ble_uuid128_t     base_uuid = BLE_UUID_OUR_BASE_UUID; // if this UUID is the same as above it works

if both the uuid in the function defined are different it dont work.

hope to get your feedback soon

MartinBL gravatar image

Posted April 19, 2016, 9:10 a.m.

To use two or more vendor specific UUIDs you will need to define how many such UUIDs you need. In the typical SDK example (and in the tutorial code) you do this in the function ble_stack_init() found in main.c. Add the line

ble_enable_params.common_enable_params.vs_uuid_count = 2;

and you should be able to use two vs UUIDs.

brookyoh1 gravatar image

Posted April 19, 2016, 2:25 p.m.


I modified it in softdevicehandler.c and now it works.

Milton Nonis gravatar image

Posted April 29, 2016, 7:52 p.m.

Hi Martin, Thanks for this excellent and detailied guide.

I was able to add a custom service and verify it successfully on MCP. However, when I included the code to add a characteristic to the service, I always get "NRF_ERROR_INVALID_PARAM'.

Issue with "Invalid parameters" error when calling, sd_ble_gatts_characteristic_add(). Development Environment: IAR Embedded Workbench IDE Custom board with nRF52832 - QFAABA0 (0x00ac) SDK : nRF5_SDK_11.0.0-2.alpha_bc3f6a0 SoftDevice : nRF5_SDK_11.0.0-2.alpha_bc3f6a0/components/softdevice/s132/hex/s132_nrf52_2.0.0-7.alpha_softdevice.hex

Using code from nrf5-ble-tutorial-characteristic-CompletedCode. Built the our_service.c/our_service.h with my project main.c, with no modifications made to the code in our_service.c

Code builds and runs OK with just the service added. However, when our_char_add() is included and sd_ble_gatts_characteristic_add() is called to add the new characteristic to the service, I always gets err_code 'NRF_ERROR_INVALID_PARAM' [err_code == 7].

our_char_add() is called right after sd_ble_gatts_service_add() without error.

Here is the code fragments that invokes this:

// OUR_JOB: Step 2.E, Add our new characteristic to the service
err_code = sd_ble_gatts_characteristic_add(p_our_service->service_handle,

I looked through the SD call prototype in ble_gatts.h and do not see any conflict at least in terms of parameter type and format.

SVCALL(SD_BLE_GATTS_CHARACTERISTIC_ADD, uint32_t, sd_ble_gatts_characteristic_add(uint16_t service_handle, ble_gatts_char_md_t const *p_char_md, ble_gatts_attr_t const *p_attr_char_value, ble_gatts_char_handles_t *p_handles));

Here is the complete function that adds the characterstic:

static uint32_t our_char_add(ble_os_t * p_our_service)

{ uint32_t err_code = 0; // Variable to hold return codes from library and softdevice functions

// OUR_JOB: Step 2.A, Add a custom characteristic UUID
ble_uuid_t          char_uuid;
ble_uuid128_t       base_uuid = BLE_UUID_OUR_BASE_UUID;
char_uuid.uuid      = BLE_UUID_OUR_CHARACTERISTC_UUID;
sd_ble_uuid_vs_add(&base_uuid, &char_uuid.type);

// OUR_JOB: Step 2.F Add read/write properties to our characteristic
ble_gatts_char_md_t char_md;
memset(&char_md, 0, sizeof(char_md)); = 1;
char_md.char_props.write = 1;

// OUR_JOB: Step 3.A, Configuring Client Characteristic Configuration Descriptor metadata and add to char_md structure
ble_gatts_attr_md_t cccd_md;
memset(&cccd_md, 0, sizeof(cccd_md));
cccd_md.vloc                = BLE_GATTS_VLOC_STACK;    
char_md.p_cccd_md           = &cccd_md;
char_md.char_props.notify   = 1;

// OUR_JOB: Step 2.B, Configure the attribute metadata
ble_gatts_attr_md_t attr_md;
memset(&attr_md, 0, sizeof(attr_md)); 
attr_md.vloc        = BLE_GATTS_VLOC_STACK;   

// OUR_JOB: Step 2.G, Set read/write security levels to our characteristic

// OUR_JOB: Step 2.C, Configure the characteristic value attribute
ble_gatts_attr_t    attr_char_value;
memset(&attr_char_value, 0, sizeof(attr_char_value));        
attr_char_value.p_uuid      = &char_uuid;
attr_char_value.p_attr_md   = &attr_md;

// OUR_JOB: Step 2.H, Set characteristic length in number of bytes
attr_char_value.max_len     = 4;
attr_char_value.init_len    = 4;
uint8_t value[4]            = {0x12,0x34,0x56,0x78};
attr_char_value.p_value     = value;

// OUR_JOB: Step 2.E, Add our new characteristic to the service
err_code = sd_ble_gatts_characteristic_add(p_our_service->service_handle,

SEGGER_RTT_printf(0, "\r\nService handle: %#x\n\r", p_our_service->service_handle);
SEGGER_RTT_printf(0, "Char value handle: %#x\r\n", p_our_service->char_handles.value_handle);
SEGGER_RTT_printf(0, "Char cccd handle: %#x\r\n\r\n", p_our_service->char_handles.cccd_handle);



Any suggestions on where to look to get past this issue?



Milton Nonis gravatar image

Posted April 30, 2016, 2:55 p.m.

Found the problem.

Noticed that the call to add a vendor specific UUID,

err_code = sd_ble_uuid_vs_add(&base_uuid, &service_uuid.type);

was being called twice -- once in the our_service_init() and the second time in our_char_add().

The second one ended up creating a new uuid index, and since the characteristic was added to a different uuid than the service, the call,

err_code = sd_ble_gatts_characteristic_add(p_nuzzle_services->service_handle,


So, by saving the vendor specific uuid.type (index) in m_base_uuid_type, and using the same one for both service and characteristic worked.

For the service:

err_code = sd_ble_uuid_vs_add(&base_uuid, &m_base_uuid_type);
uuid.type   = m_base_uuid_type;

For the characteristic:

char_uuid.uuid      = BLE_UUID_OUR_CHARACTERISTC_UUID;
char_uuid.type      = m_base_uuid_type;
brookyoh1 gravatar image

Posted May 5, 2016, 6:50 p.m.


i wanted to print my characteristic value via uart and i tried

hvx_params.handle = p_our_service->mycharHandler.value_handle;
    hvx_params.type = BLE_GATT_HVX_NOTIFICATION;
    hvx_params.offset = 0;
    hvx_params.p_len = &len;
    sd_ble_gatts_hvx(p_our_service->conn_handle, &hvx_params);

  **printf(" my char value:  %d\r\n", *hvx_params.p_data)**;

but this prints the same number even if am changing the char value from the app. it dont change it . Am i printing the right char value ?

MartinBL gravatar image

Posted May 6, 2016, 10:37 a.m.

It looks like you are missing a very essential line?

hvx_params.p_data = (uint8_t*)temperature_value;

And you should also cast the data back into an uint32_t:

printf("hvx data: %d\r\n", (uint32_t)*hvx_params.p_data);
brookyoh1 gravatar image

Posted May 6, 2016, 11:21 a.m.


yes you are right , in that case it works

but what i wanted is to do write to the char value from the MCP mobile app so i removed hvx_params.p_data = (uint8_t*)temperature_value; since i want to write to the char value from the app , and i want to print it via uart using printf("hvx data: %d\r\n", (uint32_t)*hvx_params.p_data); and this prints the same value even if i change the char value from the app side.

this is where am stuck at

babaly gravatar image

Posted June 24, 2016, 12:46 a.m.


How can I invoke multiple notification sending from within one characteristic write event? I tried to call 2 different characteristic update but for the second one I received NRF_BUSY_ERROR. Is there any solution for this?

Thanks, Laszlo

Posted July 21, 2016, 1:26 a.m.

I have a generic question about notifications. Is it OK to have the client turn on notifications for one or more characteristic at connect time right after I get the callback from the the services query on the client and just leave them on? OR is it better to turn on and off notifications as needed.


mroavi gravatar image

Posted Oct. 29, 2016, 10:53 p.m.

There is an error in the Attribute Types (UUIDs) section. UUID stands for Universally Unique Identifier, and not for User Unique ID like it is mentioned.

Atrigen gravatar image

Posted Nov. 8, 2016, 8:59 p.m.


How can I enable the timer based on the state of the CCCD notification flag ? Ie, when the user wants to be notified about temp changes.

MartinBL gravatar image

Posted Nov. 29, 2016, 9:27 a.m.

@chrisdsimpson: As long as the peripheral is sending out notifications you burn power. Hence, if you want to save power, then only enable CCCD when necessary.

@Martin Roa Villescas: Thanks. Fixed it now.

@Or: I suppose you can add "case BLE_GATTS_EVT_WRITE:" in on_ble_evt() in main.c. Then check if the handle being written to equals the CCCD handle of your characteristic

p_ble_evt->evt.gatts_evt.params.write.handle == p_our_service->char_handles.cccd_handle

Then enable the timer if the value is what you want.

For newcomers: Please ask questions in the forum for increased visibility. The question sections in the tutorials are not being monitored very closely (the system doesn't notify me about new activity here). The forum section is patrolled by Nordic employees as well as some incredibly skilled users every day.

lubos.pejchal gravatar image

Posted Jan. 19, 2017, 3:09 p.m.

Hi, I have problem with discover services in advertising mode on NRF51822-BEACON. There is default firmware in this beacon. I using Xamarin on Visual Studio 2015. It is function in config mode. Do you know where can be problem ? Thank you.

Sign in to comment.

Related posts by tag