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:
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.
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.
In the GitHub repository for this course, go to the base code for this exercise, found in l4/l4_e3
of whichever version directory you are using.
1. Include NUS in your application.
Add the following line to your prj.conf
file
CONFIG_BT_NUS=y
KconfigEnabling 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:
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>\nrf\subsys\bluetooth\services\nus.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:
BT_GATT_PERM_READ
to BT_GATT_PERM_READ_AUTHEN
(requires authentication & encryption).BT_GATT_PERM_READ | BT_GATT_PERM_WRITE
to BT_GATT_PERM_READ_AUTHEN | BT_GATT_PERM_WRITE_AUTHEN
(requires authentication & encryption).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:
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:
fifo_uart_rx_data
).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);
C6.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[UART_BUF_SIZE];
uint16_t len;
};
CNotice 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();
}
C8. 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,
};
CWe 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 0;
}
C8.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);
}
CIn 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);
C9.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);
CNote 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);
struct uart_data_t nus_data = {
.len = 0,
};
for (;;) {
/* Wait indefinitely for data to be sent over bluetooth */
struct uart_data_t *buf = k_fifo_get(&fifo_uart_rx_data,
K_FOREVER);
int plen = MIN(sizeof(nus_data.data) - nus_data.len, buf->len);
int loc = 0;
while (plen > 0) {
memcpy(&nus_data.data[nus_data.len], &buf->data[loc], plen);
nus_data.len += plen;
loc += plen;
if (nus_data.len >= sizeof(nus_data.data) ||
(nus_data.data[nus_data.len - 1] == '\n') ||
(nus_data.data[nus_data.len - 1] == '\r')) {
if (bt_nus_send(NULL, nus_data.data, nus_data.len)) {
LOG_WRN("Failed to send data over BLE connection");
}
nus_data.len = 0;
}
plen = MIN(sizeof(nus_data.data), buf->len - loc);
}
k_free(buf);
}
}
CIn 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. (Only for nRF54L15 DK) Set ZMS as the storage backend.
CONFI_ZMS=y
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
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.
Your log should look like below:
*** Booting nRF Connect SDK ***
Starting Nordic UART service example
TerminalIf you don’t see that log on the terminal, since it only prints on bootup once, you can press the reset button on the board to see it.
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.
The message will be forwarded to the UART peripheral.
*** Booting nRF Connect SDK ***
Starting Nordic UART service example
Hello from phone!
Terminal14. 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).
IMPORTANT! Please note that the data you type will not be visible in the terminal since most terminals are in char mode by default. Once you hit enter on your keyboard, you will be able to see the data on the Smartphone/Tablet side using nRF Connect for Mobile.
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.
The message will be forwarded to the UART peripheral.
*** Booting nRF Connect SDK ***
Starting Nordic UART service example
Hello from phone!
Terminal14. 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).
Please note that the data you type will not be visible in the terminal since most terminals are in char mode by default. Once you hit enter on your keyboard, you will be able to see the data on the Smartphone/Tablet side using nRF Connect for Mobile.
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.
The default Maximum Transmission Unit (MTU) set in the nRF Connect SDK Bluetooth stack is 23 bytes. It means you can’t send more data than can fit in the ATT MTU in one notification push. If you want to send more data in a single go, you need to increase this value; longer ATT payloads can be achieved, increasing the ATT throughput. This was covered in Lesson 3 – Exercise 2. Also, more details are available here
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:
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.
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.
In the GitHub repository for this course, go to the base code for this exercise, found in l4/l4_e3
of whichever version directory you are using.
1. Include NUS in your application.
Add the following line to your prj.conf
file
CONFIG_BT_NUS=y
KconfigEnabling 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:
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>\nrf\subsys\bluetooth\services\nus.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:
BT_GATT_PERM_READ
to BT_GATT_PERM_READ_AUTHEN
(requires authentication & encryption).BT_GATT_PERM_READ | BT_GATT_PERM_WRITE
to BT_GATT_PERM_READ_AUTHEN | BT_GATT_PERM_WRITE_AUTHEN
(requires authentication & encryption).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:
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:
fifo_uart_rx_data
).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);
C6.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;
};
CNotice 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();
}
C8. 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,
};
CWe 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 -1;
}
C8.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);
}
CIn 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);
C9.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);
CNote 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);
}
}
CIn 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
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.
Your log should look like below:
*** Booting nRF Connect SDK ***
Starting Nordic UART service example
TerminalIf you don’t see that log on the terminal, since it only prints on bootup once, you can press the reset button on the board to see it.
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.
The message will be forwarded to the UART peripheral.
*** Booting nRF Connect SDK ***
Starting Nordic UART service example
Hello from phone!
Terminal14. 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).
Please note that the data you type will not be visible in the terminal since most terminals are in char mode by default. Once you hit enter on your keyboard, you will be able to see the data on the Smartphone/Tablet side using nRF Connect for Mobile.
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.
The default Maximum Transmission Unit (MTU) set in the nRF Connect SDK Bluetooth stack is 23 bytes. It means you can’t send more data than can fit in the ATT MTU in one notification push. If you want to send more data in a single go, you need to increase this value; longer ATT payloads can be achieved, increasing the ATT throughput. This was covered in Lesson 3 – Exercise 2. Also, more details are available here