Getting Started with Silicon Labs Bluetooth SDK


This training introduces the Silicon Labs Bluetooth SDK. The most important use cases are demonstrated with sample applications that most important API commands are added to. The basics can be learned by implementing a GATT server while advanced skills can be acquired by implementing a GATT client.

Key Features

Bluetooth Basics

The Bluetooth connection is an asymmetric connection between a central device (e.g., a smartphone) and a peripheral device (e.g., a sensor). Typically the central device is the client that queries data from the peripheral device which is, in this case, a server.

The most common use case is the following:

  1. The peripheral device is advertising itself.

  2. The central device is scanning for devices and finds the peripheral device.

  3. The central device initiates a connection.

  4. The central device discovers the database of the peripheral device.

  5. The central device reads/writes the database of the peripheral device (e.g., reads sensor data).


In the latter use case, the central device is the server and the peripheral device is the client.

All Bluetooth devices that implement server functionality (basically all Bluetooth devices) have to implement a GATT database. This database has a fixed structure, which cannot be changed during a connection and is usually not changed during the lifetime of the device. When a client connects to the server and discovers the database, the following statements apply:

Subsequent sections explain how to implement the key steps of a Bluetooth connection with Silicon Labs Bluetooth SDK, such as:

Note: All Bluetooth connections have a master and a slave device. This is, however, not to be confused with the server and client roles. The master and slave roles are used in the physical layer (master is who sends packets first), while server and client roles are used in the application layer (client queries data from the server). These roles are independent of each other.

Start Development

Prepare your Device

This training assumes that you have already installed Simplicity Studio with Bluetooth stack on your computer. If you haven’t done so yet, follow the instructions of QSG139: Bluetooth Development with Simplicity Studio.

Before creating a Bluetooth project, note that in all Bluetooth projects, it is assumed that you have already flashed a bootloader to your device. Without a bootloader Bluetooth, projects will not start. Hence, if you have not flashed a bootloader yet, follow the procedure described in this section. If you have, then you can skip this section.

You can either flash

The dummy bootloader will do nothing, but start your application (it jumps directly to the application area). You can find the image of a dummy bootloader in the following directory:


Pick the one corresponding to your part and flash it to your device using Simplicity Commander.

Gecko Bootloader has many features including firmware update via UART and OTA (over-the-air), as described in UG266: Silicon Labs Gecko Bootloader User’s Guide. The recommended Gecko Bootloader configuration for Bluetooth applications is Bluetooth in-place OTA DFU Bootloader. The easiest way to flash this bootloader to your device is by starting the SoC – Empty demo from Simplicity Studio. Demos will flash both a bootloader and an application to your device.

To start the SoC – Empty demo:

  1. Open Simplicity Studio.

  2. Connect your device.

  3. Select your device on the Debug Adapters tab.

  4. Check that the Preferred SDK contains Bluetooth SDK v2.12.0 or later.

  5. Click on SoC – Empty in the Demos column.

  6. Select “Run” mode.

  7. Click Start.

SoC – Empty demo

Create a New Bluetooth Project

When starting development, start with a Software Example project instead of building a project from the beginning.

For a basic setup, use the SoC – Empty example.

  1. Open Simplicity Studio.

  2. Connect your device.

  3. Select your device on the Debug Adapters tab (or on the My Products tab).

  4. Check that the Preferred SDK contains Bluetooth SDK v2.12.0 or later.

  5. Click on New Project.

  6. Select SOC – Empty example, click Next.

  7. Name your project.

  8. Select the toolchain you want to use (IAR / GCC).

SoC – Empty example


  1. Open Simplicity Studio.

  2. Connect your device.

  3. Select your device on the Debug Adapters tab.

  4. Check that the Preferred SDK contains Bluetooth SDK v2.12.0 or later.

  5. Click on SOC – Empty in the Software Examples column.

Add Debug Messages

The easiest way to display debug messages is using the UART interface. If you have a WSTK, the UART interface of your device can be easily connected to your PC via USB using virtual COM port (VCOM). Since Bluetooth SDK v2.11.0, the SoC – Empty software example is prepared for logging debug messages via the virtual COM port of the WSTK. To enable debug messages, open app.h in your SoC – Empty project and define DEBUG_LEVEL to any value greater than 0. To add custom debug messages to the code, use the printLog() function the same way as you would use printf().

Build and Flash your Code

The SoC – Empty example is ready to build. To build and upload your project to your device, do the following:

  1. Click the build icon.

  2. Find the .hex file in your project in the Binaries directory.

  3. Right click and select Flash to Device.

  4. Click Program.


  1. Click the Debug icon (figure below).

  2. After the project was built and uploaded, click the start button.

  3. If you do not want to debug, disconnect your device with the disconnect button; and the code is still running on the device.

If you open the COM port on your PC with a terminal program (e.g., TeraTerm), you should see the debug messages printed.

Implementing the Server Side

The GATT Database

Every Bluetooth connection has a GATT client and a GATT server. The server holds a GATT database: a collection of Characteristics that can be read and written by the client. The Characteristics are grouped into Services, and the group of Services determines a Bluetooth Profile.

If you are implementing a GATT server (typically on the peripheral device), you have to define a GATT database structure. This structure cannot be modified during runtime (except that some services/characteristics can be temporarily disabled), so it has to be designed in advance. If you are implementing a GATT client, you can leave the GATT database as it is.

When creating a new project, or when opening the .isc file in a project, the BLE GATT configurator automatically opens. The GATT configurator is a simple-to-use tool to help you build your own GATT database. A list of predefined Profiles/Services/Characteristics/Descriptors is shown in a pane in the upper left and your current GATT database structure is shown in a pane in the upper right. An options menu is provided to the right of the Database pane.

Click an item in the Database pane to see and modify its settings in a pane in the lower right. To add a Profile/Service/Characteristic/Descriptor to your database, drag and drop it from the list to your database.

To get more information about a Profile/Service/Characteristic/Descriptor, click it either in the list or in your database. The description is displayed in the lower-left pane. You can find a detailed description of any Profile/Service/Characteristic/Descriptor on

To learn more about the GATT configurator, see UG365: GATT Configurator User’s Guide. GATT Configurator UI


To create a connection between two Bluetooth devices, one of the devices has to advertise itself. This has two purposes:

The advertisement packet is a 31 byte packet that usually contains the name of the advertiser device and the UUIDs of the most important services it has in its database. The advertisement packet is automatically assembled by the stack based on the GATT database — unless user type advertisement mode is selected.

To change the device name that will be advertised, do the following:

  1. Open the GATT configurator.

  2. Select the Device Name characteristic under Generic Access service.

  3. Change the Value field from “Empty Example” e.g. to “Bob’s device”. Use a custom name.

  4. Change the Length field to the length of the device name. For example, to 12 in case of “Bob’s device”.

  5. Click Generate.

To change the services to be advertised, do the following:

  1. Open the GATT configurator.

  2. Select the service to be advertised: select Silicon Labs OTA service.

  3. Tick the Advertise service checkbox.

  4. Click Generate.

To start advertisement, in app.c file:

  1. Set the advertisement interval and duration:

     gecko_cmd_le_gap_set_advertise_timing(0, 160, 160, 0, 0);

This will set the advertisement interval to 100 ms and the duration to infinite. For more information, see le_gap_set_advertise_timing.

  1. Start advertising in discoverable and connectable mode:

     gecko_cmd_le_gap_start_advertising(0, le_gap_general_discoverable, le_gap_undirected_connectable);

    These steps are already implemented in SOC – Empty example project in the boot event, so you do not have to modify the code!

You can stop advertising with the following command:


Note: the advertisement is automatically stopped after a connection is established. To connect to multiple devices, restart advertisement upon each connection establishment.

After modifications, build and flash your project to the device again and find your device with the Blue Gecko smartphone app. You should see your device name changed to e.g., “Bob’s De”. The “vice” is missing from the end, because you are advertising a service with 128bit UUID which uses up 16 byte from the 31-byte advertisement packet. Flags uses 3 more bytes, device name header uses 2 more bytes and service UUID header uses 2 more bytes again. This leaves only 8 bytes for the device name.

Add a Predefined Service

Bluetooth SIG has a defined a number of services with assigned 16bit UUIDs that can be used by any devices to provide interoperability between them. For example, if you want to set the current time on the device, add the predefined Current Time Service.

To add the predefined service (group of characteristics), do the following:

  1. Open GATT configurator by double clicking on the .isc file of the project.

  2. Select the Services tab on the left pane.

  3. Drag and drop the service from the left pane to the right pane. For example, add Current Time Service to your GATT database.

To make the Current Time characteristic writable (needed if you want to set time on the device), do the following:

  1. Select Current Time characteristic in your database.

  2. Click on the State of the Write property in the lower left pane.

  3. Set it to True.

  4. Set the Length to 10.

  5. Set the Value to D0070101000000000000 (which corresponds to 2000-01-01 00:00:00).

Adding predefined service in GATT configurator

Add a Custom Service

Often you need a characteristic that you cannot find among the predefined ones. For example, to read the voltage of your board in millivolts, you can create custom characteristics within a custom service.

To define a custom characteristic for this specific case, do the following:

  1. Open the GATT configurator by double clicking on the .isc file of the project.

  2. Click the create new item icon.

  3. Click New Service.

  4. Select the new Service and rename it in the name field, e.g., MyVoltageService.

  5. Note down the 128-bit UUID of your service for future reference.

  6. Click the create new item icon.

  7. Click New Characteristic.

  8. Select the new Characteristic and rename it in the name field, e.g., BoardVoltage.

  9. Tick the checkbox near to ID, and give it an ID, e.g., board_voltage.

  10. Set the length field to 2 (16bit will be enough to describe the voltage about 3300).

  11. Set the type field to hex.

  12. Add Read property by clicking Add new item in the Properties tab , and selecting Read.

  13. Set the state of the Read property to True.

  14. Click again the create new item icon.

  15. Click New Characteristic.

  16. Select the new Characteristic and rename it in the name field, e.g., BoardVoltageNotification.

  17. Tick the checkbox near to ID, and give it an ID, e.g., board_voltage_notification.

  18. Set the length field to 2.

  19. Set the type field to hex.

  20. Add Notify property by clicking Add new item in the Properties tab , and selecting Notify.

  21. Set the state of the Notify property to True.

Adding a Custom Service in GATT Configurator

Generate Database

When done editing the database, click Generate in the upper-right corner of the GATT editor. This generates the following files:

gatt.xml – an xml format description of your database structure.

gatt_db.h – a header file that contains the definitions for your characteristic handles. You can read and write the values of your characteristics by referring to these definitions. The definition names are generated from the IDs given in the GATT editor.

gatt_db.c – a source file defining the database and the default values of the characteristics. Your GATT database is ready for build.

To check your database, use your smartphone:

  1. Build your project and flash it to your device.

  2. Download the EFR Connect app to your smartphone, and open it.

  3. Select Bluetooth Browser.

  4. Connect to your device (find your device name, and tap it).

  5. Browse your database.

  6. Check the entries you have added. Note, that your custom services and characteristics are listed as unknown service/characteristic, because the service/characteristic type is determined based on the UUIDs, and not on the names you are using in your database.

  7. Service list is usually cached by applications. If you can't find your newly added services, click Refresh services in the local menu while you are connected to the device.

Reading the Database

In peripheral devices, most of the time, something is either measured or controlled on the device.

To make a measurement readable by remote devices, the values have to be written into the local GATT database. To monitor the voltage of the board, voltage is measured every second and the value written to the database in the custom characteristic (BoardVoltage) every second. This value can be then read by the client any time.

The client can also be notified that the value has changed / has been updated. Notifications are sent via the BoartVoltageNotification characteristic after each measurement. Here, the new value is automatically sent to the client without a request.

To implement this, do the following:

  1. Create a new event handler for the le_connection_opened event. This event will be triggered when a device connected to our device. Find the switch() statement in appMain() in app.c and find the following case:

     case gecko_evt_le_connection_opened_id:
  2. If the connection is opened, the voltage is measured every second. Within the le_connection_opened event handler, set up a soft timer that will fire every second:

     case gecko_evt_le_connection_opened_id:
  3. Create a new event handler for the expired timer. This will be triggered every second:

     case gecko_evt_hardware_soft_timer_id:
  4. Copy em_adc.c and em_adc.h from C:\SiliconLabs\SimplicityStudio\v4\developer\sdks\gecko_sdk_suite\v2.x\platform\emlib\ into the project and add the following line to app.c:

     #include "em_adc.h"
     #include "em_cmu.h"
  5. Declare variables and initialize ADC in appMain() before gecko_init(pconfig);.

     uint32_t adcData;
     uint16 boardVoltage;
     ADC_InitSingle_TypeDef initSingle = ADC_INITSINGLE_DEFAULT;
     initSingle.acqTime = adcAcqTime16;
     initSingle.reference = adcRef5VDIFF;
     initSingle.posSel = adcPosSelAVDD;
     initSingle.negSel = adcNegSelVSS;
     CMU_ClockEnable(cmuClock_ADC0, true);
  6. Within the gecko_evt_hardware_soft_timer event handler, read the voltage.

     ADC_InitSingle(ADC0, &initSingle);
     ADC_Start(ADC0, adcStartSingle);
     while((ADC_IntGet(ADC0) & ADC_IF_SINGLE) != ADC_IF_SINGLE);
     adcData = ADC_DataSingleGet(ADC0);
     boardVoltage = (uint16)(adcData * 5000 / 4096);
     printLog("voltage: %d mV\r\n",boardVoltage);
  7. Write it to the local database (BoardVoltage). Also, send a notification with the new value (BoardVoltageNotification).

     boardVoltage =  ((boardVoltage & 0x00FF) << 8) | ((boardVoltage & 0xFF00) >> 8);
     gecko_cmd_gatt_server_write_attribute_value(gattdb_board_voltage, 0, 2, (const uint8*)&boardVoltage);
     gecko_cmd_gatt_server_send_characteristic_notification(0xFF, gattdb_board_voltage_notification, 2,
     (const uint8*)&boardVoltage);

Check the value with your smartphone, as follows:

  1. Open the EFR Connect app ( on your smartphone/

  2. Select the Bluetooth Browser.

  3. Connect to your device.

  4. Find the unknown service with the UUID of your custom service, open it.

  5. Open the first characteristic. This will automatically read its value.

  6. Convert the hex value to decimal, the value should be around 3300mV.

  7. Open the second characteristic. Here you can see the voltage automatically updated every second.

  8. Convert the hex value to decimal, the value should be around 3300 mV.

    Voltage reading via blugeckoapp

Alternatively, if debugging is enabled, the board voltage levels can be read over UART:

Voltage reading via terminal emulator Software

Writing the Database

To control the device / set parameters of the device, you can use writable characteristic. If you want to set the current time on the device, you can write the Current Time characteristic from your smartphone. Your application will be notified about the changes, you can read the new value from the GATT database, and you can configure your device according to it. To process e.g., the updated Current Time written from your smartphone, implement the following.

  1. Create a new event handler for attribute changes:

     case gecko_evt_gatt_server_attribute_value_id:

This will be triggered, for example, when a characteristic was written by a remote device.

  1. Define a structure that corresponds to the Current Time characteristic structure as it is defined by Bluetooth SIG ( This is important to be compatible with the standard. You can put the following code into app.c.

     PACKSTRUCT(struct date_time_t{
         uint16 year;
         uint8 month;
         uint8 day;
         uint8 hours;
         uint8 minutes;
         uint8 seconds;
     PACKSTRUCT(struct day_of_week_t {
         uint8 day;
     PACKSTRUCT(struct day_date_time_t {
         struct date_time_t date_time;
         struct day_of_week_t day_of_week;
     PACKSTRUCT(struct exact_time_256_t {
         struct day_date_time_t day_date_time;
         uint8 fractions_256;
     PACKSTRUCT(struct current_time_t {
         struct exact_time_256_t exact_time_256;
         uint8 adjust_reason;
  2. Process the received value within the gecko_evt_gatt_server_attribute_value_id event handler:

     if (evt->data.evt_gatt_server_attribute_value.attribute == gattdb_current_time){
             struct current_time_t* current_time =
                 (struct current_time_t*)(evt->;
             struct date_time_t* datetime =
             printLog("current time modified: %4d-%2d-%2d %2d:%2d:%2d\r\n",
                 datetime->year, datetime->month, datetime->day, datetime->hours, datetime->minutes, datetime->seconds);

Now you can test it with your smartphone, as follows:

  1. Open the EFR Connect app on your smartphone.

  2. Select the Bluetooth Browser.

  3. Connect to your device.

  4. Find the Current Time Service open it.

  5. Find the Current Time characteristic.

  6. Set the value to the current date.

  7. Check the value in the debug message on the COM port.

    current time characterstics

Implementing the Client Side [Advanced]

The GATT server is basically only responding to requests from the client, which is relatively easy to implement. The GATT client, however, generates a series of requests, basically implementing a state machine. Hence, this section requires some more insights in the code, and recommended only for advanced training. The client code can be added to the server code, since a Bluetooth device can be used both as a server and as a client at the same time. However, to get a cleaner implementation, it is suggested starting the implementation of the client code from a new SoC-Empty project.


If you are implementing a central device, (a client) which will connect to an advertising peripheral device first, you have to start scanning. In scanning mode, the Bluetooth device is searching for nearby devices that are currently advertising to create a connection with one or more device. To start scanning, do the following:

  1. Set the scanning parameters in the system_boot event handler in appMain() in app.c:

    case gecko_evt_system_boot_id:
         gecko_cmd_le_gap_set_discovery_timing(le_gap_phy_1m, 160, 160);
         gecko_cmd_le_gap_set_discovery_type(le_gap_phy_1m, 1);

    This will result in a continuous active scanning. For more details, see: le_gap_set_discovery_timing, le_gap_set_discovery_type.

  2. Start scanning right after setting the setting parameters with le_gap_start_discovery.

     case gecko_evt_system_boot_id:
         gecko_cmd_le_gap_set_discovery_timing(le_gap_phy_1m, 160, 160);
         gecko_cmd_le_gap_set_discovery_type(le_gap_phy_1m, 1);
         gecko_cmd_le_gap_start_discovery(le_gap_phy_1m, le_gap_discover_observation);
         printLog("Scanning started\r\n");
  3. Set up a timer to stop scanning, for example, after 5 seconds.

     case gecko_evt_system_boot_id:
         gecko_cmd_le_gap_set_discovery_timing(le_gap_phy_1m, 160, 160);
         gecko_cmd_le_gap_set_discovery_type(le_gap_phy_1m, 1);
         gecko_cmd_le_gap_start_discovery(le_gap_phy_1m, le_gap_discover_observation);
         printLog("Scanning started\r\n");
  4. Create a new event handler for the expired timer:

     case gecko_evt_hardware_soft_timer_id:
  5. In the event handler stop scanning.

     case gecko_evt_hardware_soft_timer_id:
         if (evt->data.evt_hardware_soft_timer.handle == 1){
             printLog("Scanning stopped\r\n");

While the device is scanning for nearby devices, a new le_gap_scan_response event is raised by the stack for each advertisement packet received. To handle these events, do the following:

  1. Create an array for scanned devices in a global variable.

     #define MAX_SCANNED_DEVICES 10
     struct gecko_msg_le_gap_scan_response_evt_t scanned_devices[MAX_SCANNED_DEVICES];
     uint8 num_scanned_devices = 0, i, *addr;
  2. Create an event handler for scan responses.

     case gecko_evt_le_gap_scan_response_id:
  3. Within the event handler check if the device was already scanned, and add to the list if it was not.

     case gecko_evt_le_gap_scan_response_id:
           addr = evt->data.evt_le_gap_scan_response.address.addr;
            for (i=0; i<num_scanned_devices; i++){
                if (memcmp(scanned_devices[i].address.addr,addr,6)==0)
            if (i == num_scanned_devices && num_scanned_devices < MAX_SCANNED_DEVICES){
                memcpy(scanned_devices[num_scanned_devices].address.addr, addr,6);
                scanned_devices[num_scanned_devices].address_type = evt->data.evt_le_gap_scan_response.address_type;
                scanned_devices[num_scanned_devices].rssi = evt->data.evt_le_gap_scan_response.rssi;
                printLog("%d) %02x:%02x:%02x:%02x:%02x:%02x – rssi: %d\r\n",
                        num_scanned_devices, addr[5], addr[4], addr[3], addr[2], addr[1], addr[0],

Now, you can build your code and flash it to the device. Connect to the serial port and take a look at the list of scanned devices. If you have a peripheral device in the near advertising, you have to see it listed.

When scanning is completed, onnect to any discovered device. For the sake of simplicity, connect to the closest device, the RSSI of which is at least -50dBm:

  1. Declare variables:

     int8 max_rssi;
     uint8 closest_device;
  2. Find the closest device, right after scanning was stopped in the hardware_soft_timer event handler:

     max_rssi = -50;
     closest_device = num_scanned_devices;
     for (i = 0; i < num_scanned_devices; i++){
         if (scanned_devices[i].rssi > max_rssi){
             max_rssi = scanned_devices[i].rssi;
             closest_device = i;
  3. And connect to it with le_gap_connect.

     if (closest_device < num_scanned_devices)
         scanned_devices[closest_device].address_type, 1);
  4. Declare a global variable for the connection handle.

      uint8 conn_handle;
  5. Save the connection handle within the le_connection_opened event handler for future reference.

     case gecko_evt_le_connection_opened_id:
         conn_handle = evt->data.evt_le_connection_opened.connection;

Place a server and a client next to each other and see on the terminal if they are connected. If you have created a new project for the client, remember to enable the debug messages by defining DEBUG_LEVEL in app.h to any value greater than 0.

Discovering Remote Database

After the connection was established, the structure of the remote database is unknown for the client. To discover the database, a service discovery has to be run on it. This will return with UUIDs of implemented services/characteristics and with handles of services and characteristics – which can be used as reference while reading/writing.

To start service discovery, do the following:

  1. Declare global variables indicating service discovery state, and storing service handles:

     uint8 service_discovery = 0;
     uint32 service_handles[1] = ;
     uint8 service_to_find[2] = {0x00,0x18};
  2. Start service discovery in the le_connection_parameters (connection is established) event handler:

     case gecko_evt_le_connection_parameters_id:
         printLog("connection established\r\n");
         service_discovery = 1;
  3. Create a new event handler for discovered services:

     case gecko_evt_gatt_service_id:
  4. Save service handles within the gatt_service event handler for services you are interested in. For example, for Generic Access service:

     if (memcmp(evt->, service_to_find, 2) == 0){
         service_handles[0] = evt->data.evt_gatt_service.service;
  5. Create a new event handler for the end of the discovery process:

     case gecko_evt_gatt_procedure_completed_id:
         if (service_discovery){
         service_discovery = 0;
         //characteristic discovery can be started here

To start characteristic discovery:

  1. Declare global variables indicating characteristic discovery state, and storing characteristic handles:

     uint8 characteristic_discovery = 0;
     uint16 characteristic_handles[1] = ;
     uint8 char_to_find[2] = {0x00,0x2a};
  2. Start characteristic discovery in the gatt_procedure_completed event handler:

     gecko_cmd_gatt_discover_characteristics(conn_handle, service_handles[0]);
     characteristic_discovery = 1;
  3. Create a new event handler for discovered characteristics:

     case gecko_evt_gatt_characteristic_id:
  4. Save characteristic handles within the gatt_characteristic event handler for characteristics you are interested in. For example, for Device Name characteristic:

    if (memcmp(evt->, char_to_find, 2) == 0){
         characteristic_handles[0] = evt->data.evt_gatt_characteristic.characteristic;
  5. Extend the gatt_procedure_completed event handler:

    else if (characteristic_discovery){
     characteristic_discovery = 0;
     //reading/writing remote database can be started here

Reading/Writing Remote Database

After service discovery, any characteristic can be read/written, provided that you have saved the characteristic handle and you have rights to read/write the given characteristic.

For example, to read the Device Name characteristic of the remote device, do the following:

  1. Declare a receive buffer.

    uint8 remote_device_name[50];
  2. Initiate a read process, for example, in the gatt_procedure_completed event handler:

  3. Create a new event handler for the received data.

     case gecko_evt_gatt_characteristic_value_id:
  4. Process the received data within the event handler:

     memcpy(remote_device_name, evt->,
     remote_device_name[evt->data.evt_gatt_characteristic_value.value.len] = '\0';
     printLog("remote device name: %s\r\n", remote_device_name);

Similarly, you can write to characteristics using gecko_cmd_gatt_write_characteristic_value(conn_handle,char_handle,len,data). Build and flash your code, place a server and a client next to each other and see on the terminal if the client is able to connect and find the device name of the server.