Exercise 3

Sending data between a UART and a Bluetooth LE connection

This exercise will focus on Nordic UART Service (NUS). NUS is quite a popular and versatile custom GATT service. The service emulates a serial port over Bluetooth LE, which allows you to send any sort of data back and forth between a UART connection (or a virtual serial port emulated over USB) and a Bluetooth LE connection.

The service has two characteristics:

  • RX Characteristic (Write, Write w/o response): For sending data to the board
  • TX Characteristic (Notify): For receiving data from the board

When a Bluetooth LE connection is established between a peripheral and a central, NUS forwards any data received on the RX pin of the UART0 peripheral to the Bluetooth LE central as notifications through the TX Characteristic. Any data sent from the Bluetooth LE central through the RX Characteristic is sent out of the UART0 peripheral’s TX pin.

Remember that on Nordic DKs, the UART0 peripheral is typically gated through the SEGGER debugger/programmer chip (aka: interface MCU) to a USB CDC virtual serial port that you can connect directly to your PC.

Note

The code provided in this exercise uses NUS to forward/receive data to/from the UART0 peripheral. You could easily modify the exercise to use NUS with UART1 or other peripherals.

In this exercise, we will learn how to use NUS to exchange data over Bluetooth LE between your PC and your smartphone or tablet running nRF Connect for Mobile.

Exercise steps

In the GitHub repository for this course, go to the base code for this exercise, found in lesson4/blefund_less4_exer3.

1. Include NUS in your application.

Add the following line to your prj.conf file

CONFIG_BT_NUS=y

Enabling this Kconfig will make the build system include the nus.c and the nus.h files. Since nus.c already includes the static service declaration and its characteristics, including the source files will by itself add NUS to the attribute table of your application.

In the application code (main.c), we will mainly need to do the following tasks:

  • Initialize the UART peripheral.
  • Define and register an application callback function to forward the data received from a Bluetooth LE connection to UART.
  • Call a function to send data received from UART to a Bluetooth LE connection.

We will first spend some time examining the NUS service implementation.

2. Examine the NUS service declaration

This is declared in nus.c, (found in <install_path>nrfsubsysbluetoothservicesnus.c). We will not modify the source files of the NUS service.

The declaration statically creates and adds the service with the UUID BT_UUID_NUS_SERVICE (defined in nus.h). and its two characteristics, the RX Characteristic and TX Characteristic.

Notice the presence of the conditional compilation flag CONFIG_BT_NUS_AUTHEN. If enabled, it will do the following to the static configurations of the characteristics of the NUS service:

  • Change the characteristic access permission of the TX Characteristic from BT_GATT_PERM_READ to BT_GATT_PERM_READ_AUTHEN (requires authentication & encryption).
  • Change the characteristic access permission of the Client Characteristic Configuration Descriptor of the TX Characteristic from BT_GATT_PERM_READ | BT_GATT_PERM_WRITE to BT_GATT_PERM_READ_AUTHEN | BT_GATT_PERM_WRITE_AUTHEN (requires authentication & encryption).
  • Change the characteristic access permission of the RX Characteristic from BT_GATT_PERM_READ | BT_GATT_PERM_WRITE to BT_GATT_PERM_READ_AUTHEN | BT_GATT_PERM_WRITE_AUTHEN (requires authentication & encryption).

Authenticated connections and encryption will be the focus of the next lesson; therefore, we will disable the CONFIG_BT_NUS_AUTHEN flag in this lesson.

Also, notice that for the write callback of the RX Characteristic, we are registering the function on_receive(). This function is called every time the Bluetooth central device writes data to the RX Characteristic. We will dissect it in the next step.

3. Examine the write callback function of the RX Characteristic

Let’s examine the write callback function of the RX Characteristic on_receive() in nus.c . The function calls the application callback function and passes it three parameters. The connection handle (in case multiconnection is used), the data received over Bluetooth LE, and its size.

4. Examine the bt_nus_init() which is also defined in nus.c

This function and the structure bt_nus_cb (defined in nus.h) have a similar intent to the my_lbs_init() we saw in Exercise 1. The purpose of bt_nus_init() and the bt_nus_cb struct is to facilitate decoupling of the code responsible for actually reading/writing to the UART peripheral (application code, i.e main.c) from the Bluetooth LE connectivity code (nus.c). Code decoupling adds a bit of complexity, but it does make the code way easier to maintain and scale.

Later, we will call this function in main.c and pass it a pointer to our application callback functions. Notice that the bt_nus_init() function can register three application callback functions:

  • Data received callback (Mandatory). The data has been received as a write request on the RX Characteristic, so the application callback function must do the action needed to forward the data to the UART peripheral.
  • Data sent callback (Optional). This function pointer can allow you to register an application callback function to be triggered when data has been sent as a notification to the TX Characteristic. We will not use it in this exercise.
  • Send state callback (Optional). This function pointer can allow you to register an application callback function to be triggered when a remote Bluetooth LE device subscribes or unsubscribes to the TX Characteristic notifications. We will not use it in this exercise.

5. Examine the function responsible for sending notifications

The last function we will examine in nus.c , is the function responsible for sending notifications over a Bluetooth LE connection bt_nus_send(). We will call bt_nus_send() from the application code (main.c) to forward the data received from the UART to a remote device over a Bluetooth LE connection.

Unlike in LBS, where we supported a single Bluetooth LE connection, NUS can support simultaneous connections. Therefore, the implementation of sending notifications is slightly different.

We will check the connection parameter conn (of type struct bt_conn) if it equals NULL, we will send notifications to all connected devices. On the other hand, if a specific connection is passed to the bt_nus_send() function, we will manually check if notification is enabled by the client on that connection and then send notification to that particular client.

With this, we have a good understanding of the implementation of Nordic UART Service. In the next steps, we will cover how to use it in an application code.

6. Declare two FIFO data structures

Declare two FIFOs data structures and the FIFO data item to hold the following:

  • The data received from the UART peripheral, and we want to send over a Bluetooth LE connection (fifo_uart_rx_data).
  • The data was received from a Bluetooth LE connection, and we want to send over UART (fifo_uart_tx_data).

6.1 Declare the FIFOs

Add the following code in main.c

static K_FIFO_DEFINE(fifo_uart_tx_data); 
static K_FIFO_DEFINE(fifo_uart_rx_data);

6.2 Declare the struct of the data item of the FIFOs

Add the following code in main.c

struct uart_data_t {
	void *fifo_reserved;
	uint8_t data[CONFIG_BT_NUS_UART_BUFFER_SIZE];
	uint16_t len;
};

Notice that the 1st word is reserved for use by FIFO as required by the FIFO implementation. The second member of the structure, which will hold the actual data, is an array of bytes of size CONFIG_BT_NUS_UART_BUFFER_SIZE. It is user configurable. The default size is 40 bytes.

7. Initialize the UART peripheral

Setting up the UART peripheral driver, using its asynchronous API, and assigning an application callback function is covered thoroughly in the nRF Connect SDK Fundementals course – Lesson 5. Feel free to revisit the Lesson in the nRF Connect SDK Fundamentals course to refresh the information if needed.

Add the call to uart_init() in main() as shown below

	err = uart_init();
	if (err) {
		error();
	}

8. Forward the data received from a Bluetooth LE connection to the UART peripheral.

8.1 Create a variable of type bt_nus_cb and initialize it.

This variable will hold the application callback functions for the NUS service.

Add the following code in main.c

static struct bt_nus_cb nus_cb = {
	.received = bt_receive_cb,
};

We will set the data received callback to bt_receive_cb, which is covered in a following step.

8.2 Pass your application callback functions stored in nus_cb to the NUS service by calling bt_nus_init()

Add the following code in main()

	err = bt_nus_init(&nus_cb);
	if (err) {
		LOG_ERR("Failed to initialize UART service (err: %d)", err);
		return;
	}

8.3 The bt_receive_cb() function will be called by NUS when data has been received as a write request on the RX Characteristic. The data received from a Bluetooth LE connection will be available through the pointer data with the length len . We will call the UART peripheral function uart_tx to forward the data received over Bluetooth LE to the UART peripheral.

Add the following code inside bt_receive_cb() function.

		err = uart_tx(uart, tx->data, tx->len, SYS_FOREVER_MS);
		if (err) {
			k_fifo_put(&fifo_uart_tx_data, tx);
		}

In case there is an ongoing transfer already or an error, we will put the data in the fifo_uart_tx_data and try to send it again inside the uart_cb UART callback.

9. Receiving data from the UART peripheral and sending it to a Bluetooth LE connection.

9.1 Push the data received from the UART peripheral into the fifo_uart_rx_data FIFO.

On the UART_RX_BUF_RELEASED event in the uart_cb callback function of the UART peripheral, we will put the data received from UART into the FIFO by calling k_fifo_put(). The UART_RX_BUF_RELEASED event is triggered when the buffer is no longer used by UART driver.

Add the following line inside the UART callback function in the UART_RX_BUF_RELEASED event.

k_fifo_put(&fifo_uart_rx_data, buf);

9.2 Create a dedicated thread for sending the data over Bluetooth LE.

We will create a thread and associate it with the function ble_write_thread() which we will develop in the next step. The thread is assigned the stack STACKSIZE (1024 by default), and the priority PRIORITY (7 by default).

Add the following line of code in main.c

K_THREAD_DEFINE(ble_write_thread_id, STACKSIZE, ble_write_thread, NULL, NULL,
		NULL, PRIORITY, 0, 0);

Note that the thread will be automatically scheduled for execution by the scheduler.

9.3 Define the thread function

void ble_write_thread(void)
{
	/* Don't go any further until BLE is initialized */
	k_sem_take(&ble_init_ok, K_FOREVER);

	for (;;) {
		/* Wait indefinitely for data from the UART peripheral */
		struct uart_data_t *buf = k_fifo_get(&fifo_uart_rx_data,
						     K_FOREVER);
        /* Send data over Bluetooth LE to remote device(s) */
		if (bt_nus_send(NULL, buf->data, buf->len)) {
			LOG_WRN("Failed to send data over BLE connection");
		}

		k_free(buf);
	}
}

In this thread, we have an infinite loop where we call k_fifo_get() to get the data from the FIFO. We will send the data to connected Bluetooth LE device(s) as notification by calling the NUS function bt_nus_send().

Notice that we passed K_FOREVER as the second parameter for k_fifo_get(). This means the thread will be scheduled out if there is no data in the FIFO. Once the UART peripheral callback function uart_cb puts data in the FIFO, the thread will be scheduled back for execution.

Also, notice that the thread will only start after the Bluetooth LE stack has been initialized through the use of the semaphore: k_sem_take(&ble_init_ok, K_FOREVER).

10. Build and flash the application on your board.

You should notice that LED1 on your board is blinking now, indicating that your board is advertising.

Testing

11. Open a terminal to view the log output from the application

11.1 Connect to the virtual serial COM port of your DK

Just like we have done in previous exercises, connect to the COM port of your DK in VS Code by expanding your device under Connected Devices and selecting the COM port for the device. Note on some development kits; there might be more than one COM port. Use the one that you see the Starting Nordic UART service example log on.

11.2 Make sure that your serial terminal is using line mode. This is because in the UART peripheral callback uart_cb(), we are releasing the buffer only when a new line (aka: 'n' , LF) or a carriage return (aka: 'r', CR) is received.

To switch to line mode in nRF Terminal, invoke the command palette in VScode (Ctrl+Shift+P) and type nRF Terminal Switch to Line Mode . This setting will make sure to include r and n at each message you send.

Your log should look like below:

12. Connect to your device via your smartphone.

Open nRF Connect for Mobile on your smartphone. In the Scanner tab, locate the device, now named “Nordic_UART_Service” and connect to it, as done in the previous exercises.

13. Send data from your phone to the board.

In nRF Connect for Mobile, press on the arrow next to the RX Characteristic. You will be prompted with a small window.

Android

Type a message in the Write value box, select the type of operation: Request (write with response) or Command (write with no response), and press SEND.

nRF Connect for Android
iOS

Select UTF8 to send a string. Type a message in the box, select the type of operation: Request (write with response) or Command (write with no response), and press Write.

nRF Connect for iOS

The message will be forwarded to the UART peripheral.

14. Send data from your PC to your phone through the board.

14.1 In nRF Connect for mobile, subscribe to the TX Characteristic by pressing on the icon next to the TX Characteristic

14.2 In nRF Terminal, type a message to send to the remote device (for example Hello from PC!) and hit enter (to send an end-of-line and carriage return).

The message will be forwarded from the UART peripheral through the Bluetooth LE connection to the central device running nRF Connect for Mobile and show up there.

Android
nRF Connect for Android
iOS
nRF Connect for iOS
Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.