SPP (Serial Port Profile) over BLE


This example provides a simple template for SPP-like communication. To keep the code as short and simple as possible, the features are minimal. Users are expected to customize the code as needed to match their project requirements.

The associated sample code is a single application that implements both the server and client roles (in their own C-files). The role is selected dynamically at power-up using pushbuttons, as described in the section How to Use.

The client part of this example (spp_client_main) can also be used as a starting point for any generic BLE central implementation that scans and automatically connects to devices that advertise a specific service UUID. The client performs service discovery after the connection is established. This process works similarly for any GATT-based services. With small modifications, the spp_client_main code can be converted into a “heart rate client” or “thermometer client” code for example.

SPP Server

Because Bluetooth Low Energy does not have a standard SPP service, it needs to be implemented as a custom service. The custom service is as minimal as possible. Only one characteristic is used for both incoming and outgoing data. The service is defined in the gatt.xml file associated with this document and shown below.

  <!-- Our custom service is declared here -->
  <!-- UUID values generated with https://www.guidgenerator.com/ -->
  <service uuid="4880c12c-fdcb-4077-8920-a450d7f9b907" advertise="true">
    <description>SPP Service</description>
    <characteristic uuid="fec26ec4-6d71-4442-9f81-55bc21d658d6" id="xgatt_spp_data">
      <description>SPP Data</description>
      <properties write_no_response="true" notify="true" />
      <value variable_length="true" length="20" type="hex"></value>

At boot event, the server is put into advertisement mode. This example uses advertising packets that are automatically filled by the stack. In the custom SPP service definition (see above), advertise=true is set to true, meaning that the stack will automatically add the 128-bit UUID in advertisement packets. The SPP client will use this information to recognize the SPP server among other BLE peripherals.

For incoming data (data sent by the SPP client and written to UART in the SPP server), unacknowledged write transfers (write_no_response) are used, which provides a better performance than normal acknowledged writes because several write operations can fit into one connection interval.

For outgoing data (data received from UART and sent to SPP client), notifications with the command gatt_server_send_characteristic_notification are used. Notifications are unacknowledged, which again allows several notifications to fit into one connection interval.

Note that data transfers are unacknowledged at GATT level. This means that at application level, no acknowledgments occur. However, at the lower protocol layers, each packet is still acknowledged and retransmissions are used when needed to ensure that all packets are delivered.

The core of the SPP example implementation is a 256-byte FIFO buffer (data[] in send_spp_data function) used to manage outgoing data. Data is received from UART and pushed to the SPP client using notifications. Incoming data from client raises the gatt_server_attribute_value event. The received data is then copied to UART.

Simple overflow checking can be optionally included. If the number of bytes exceeds 256, FIFO overflow occurs. Depending on the application, the overflow may be handled differently. For some applications, it may be best to drop the bytes that do not fit in the buffer. For other applications, it may be better to immediately stop all data transfers to avoid any further damage. See comments in spp_utils.h for more information.

SPP Client

In terms of incoming/outgoing UART data, the SPP client works the same way as the SPP server. A similar 256-byte FIFO buffer is used with the following differences:

Data is received over the air by notifications (event gatt_characteristic_value). Data is sent by calling gatt_write_characteristic_value_without_response.

The client is more complex than the server because it needs to detect the SPP server by looking at the advertisement packets. Additionally, the client needs to do service discovery after connecting to the client to get the information about the remote GATT database.

To use the SPP service, the client needs to know the characteristic handle values. The handles are discovered dynamically so that hard-coded values are not needed. This is essential if the SPP server needs to be ported to some other BLE module and the handle values are not known in advance.

At startup, the client starts discovery by calling le_gap_start_discovery. For each received advertisement packet, the stack will raise event le_gap_scan_response. To recognize the SPP server, the client scans through each advertisement packet and searches for the 128-bit service UUID that is assigned for the custom SPP service.

Scanning advertisement packets is done with the function process_scan_response. Advertising packets include one or more advertising data elements that are encoded as defined in the BT specification. Each advertising element begins with a length byte that indicates the length of that field, which makes scanning through all elements in a while-loop easy.

The second byte in the advertising element is the AD type. The predefined types are listed in Blutetooth SIG website.

In this use case, types 0x06 and 0x07 that indicate incomplete/complete list of 128-bit service values are relevant. If this AD type is found, the AD payload is compared against the known 128-bit UUID of the SPP service to check if there is a match.

After finding a match in the advertising data, the client opens a connection by calling le_gap_connect. When the connection is opened, the next task is to discover services and figure out the handle value that corresponds to the SPP_data characteristic (see XML definition of our custom service attached).

The service discovery is implemented as a simple state machine and the sequence of operations after connection is opened and summarized below:

1) Call gatt_discover_primary_services_by_uuid to start service discovery

2) Call gatt_discover_characteristics to find the characteristics in the SPP service (in FIND_SERVICE state)

3) Call gatt_set_characteristic_notification to enable notifications for spp_data characteristic (in FIND_CHAR state)

Note that in step one above, only services that match the specific UUID are relevant for service discovery. Another option is to call cmd_gatt_discover_primary_services and return list of all services in the remote GATT database.

After each procedure in the above sequence is completed, the stack will raise event gatt_procedure_completed. The client uses a variable main_state to keep track of the current state. The gatt_procedure_completed event will trigger the state machine to move on to the next logical state.

When notifications are enabled, the application is in transparent SPP mode, which is indicated by writing string "SPP Mode ON" to UART. After this point, any data that is received from UART is sent to server using non-acknowledged write transfer. Similarly, all data received via notifications (event gatt_characteristic_value) is copied to the local UART.

Note that on the server application the "SPP Mode ON" string is printed to the console. On the server side, this is done when the remote client has enabled notifications for the spp_data characteristic.

Data is handled transparently, meaning that the transmitted values can be either ASCII strings or binary data, or a mixture of these. The connection can be closed by pressing either of the pushbuttons on the client.

Power Management

USART peripheral is not accessible in EM2 sleep mode. For this reason, both the client and the server applications disable sleeping (EM2 mode) temporarily when the SPP mode is active. SPP mode in this context means that the client and server are connected and that the client has enabled notifications for the SPP_data characteristics.

When SPP mode is entered, the code calls SLEEP_SleepBlockBegin(sleepEM2) to temporarily disable sleeping. When connection is closed (or client disables notifications), SLEEP_SleepBlockEnd(sleepEM2) is called to re-enable sleeping.

For more details on power management, see Using Energy Modes with Bluetooth Stack.

Known Issues

This example implementation does not guarantee 100% reliable transfer. The implementation uses retargetserial driver for reading data from UART. The driver is found in <project_root>/hardware/kit/common/drivers/ . For incoming data, the driver uses a FIFO buffer whose size is defined using symbol RXBUFSIZE (default value 8).

To get a more reliable operation, increase the RXBUFSIZE value. However, even with a large FIFO buffer, some data may get lost if the data rate is very high. If the FIFO buffer in RAM becomes full, the driver will simply drop the bytes that do not fit.

Setting up

To run this example you need the following:

  1. Create a new SoC - Empty application project with Bluetooth SDK version 2.12 or newer.

  2. Click on the .isc file in your project tree, select the Custom BLE GATT field on the right side of the configurator and finally select Import GATT from .bgproj file from the icons next to the field (the bottommost icon).

  3. Select the provided gatt.xml, click Save, and press Generate. You should now have a new SPP service and within it one characteristic.

  4. Copy the following files to your project:

    • app.c
    • spp_utils.h
    • spp_utils.c
    • spp_client_main.c
    • spp_server_main.c
  5. Enable printing to console by setting DEBUG_LEVEL from 0 to 1 in app.h

  6. Build and flash the project to both of your devices. (If you have two different radio boards, then you have to create two projects, one for each board, and repeat the steps. If you use same radio boards, you can flash the same image.)


When the application boots, it checks the state of pushbuttons PB0 and PB1. If the buttons are not pressed, the application starts in SPP server role, and you should see this displayed at the serial output: * SPP server mode *

By keeping either PB0 or PB1 pressed during reboot, the application starts in SPP client mode, and you should see this displayed at the serial output: * SPP client mode *

In server mode, the device advertises the custom SPP service and waits for incoming connections.

In client mode, the device starts scanning and searches for the custom SPP UUID in the scan responses. If a match is found, the client connects to the target, discovers the SPP service and characteristics and enables notifications for the SPP_data characteristic. At this point, any data input in the client side is sent over the air to the server and printed on the remote UART. Similarly, any data input to the server UART is transmitted back to the client.

To connect to the kit using the terminal program, use the following UART settings: baud rate 115200, 8N1, no flow control.

NOTE: Make sure that you are using the same baud rate and flow control settings in your starter kit and radio board or module firmware as well as your terminal program. For WSTK, this can be checked in Debug Adapters->Launch Console->serial vcom (config speed/handshake)

Launch Console

WSTK Configuration

The animation below illustrates what happens when the client and server boards are powered up. The client is on the left side and the server on the right. Shortly after power up, the client will find the server and open a connection automatically. When the connection is set up properly and notifications are enabled, both applications will output string "SPP Mode ON", which indicates that the transparent serial connection is open. From this point on, any data you type into the client terminal will appear on the server terminal and vice versa.

Terminal Demo Animation

You can try pressing reset button on either of the boards to see what happens. The connection should be restored automatically when both units are back online. Pressing either of the buttons on the client board will close the connection and print the stats for the last session to the terminal.