Nordic BLE Serial

The nRF Serial Port Library is designed as a more sophisticated replacement for the app_uart module, as pointed out at the library’s page, there are many advantages of this module over app_uart:

  • API is more generic and robust: you can read or write any amount of bytes.
  • Multi-instance capability.
  • The module can work in three modes: POLLING, IRQ, and DMA.
  • Calls can be asynchronous and synchronus (with timeouts).
  • Independent RX/TX FIFOs with configurable sizes.
  • Configurable RX/TX transfer buffers (smallest transfer slice).
  • Event handler (not mandatory).
  • Sleep handler (not mandatory).

Note: the Serial module has been deprecated in SDK 17.0 and replaced by libUARTE module, which will be covered in the future.

1. nRF Serial

There are three working modes:

  • NRF_SERIAL_MODE_POLLING - Simple polling mode. API calls will be synchronous. There is no need to define queues or buffers. No events will be generated.
  • NRF_SERIAL_MODE_IRQ - Interrupt mode. API can be set to work in synchronous or asynchronous mode. Queues and buffers must be passed during initialization. Events will be generated if a non NULL handler is passed as the evt_handler parameter.
  • NRF_SERIAL_MODE_DMA - Similar to NRF_SERIAL_MODE_IRQ. Uses EasyDMA.

Note that Interrupt mode. API can be set to work in synchronous or asynchronous mode, further read you will see:

Warning: Do not use synchronous API (timeout_ms parameter > 0) in IRQ context. It may lead to a deadlock because the timeout interrupt cannot preempt the current IRQ context.

The two serial examples have mixed use of synchronous and asynchronous. It is my experiences that it is better to only use synchronous mode with Polling, especially when in BLE applications, and asynchronous mode with IRQ/DMA and implementing event handler.

1.1. Polling Mode

To use serial polling, use NRF_SERIAL_MODE_POLLING in the configuration, and set to use UART0, as it is the only one support “legacy mode”.

NRF_SERIAL_CONFIG_DEF(serial0_config, NRF_SERIAL_MODE_POLLING,
                     &serial0_queues, &serial0_buffs, NULL, NULL);

NRF_SERIAL_UART_DEF(serial0_uarte, 0);

Add/change APP_UART_ENABLED and APP_UART_DRIVER_INSTANCE in sdk_config.h.

// </h>
//==========================================================

// </e>

// <e> APP_UART_ENABLED - app_uart - UART driver
//==========================================================
#ifndef APP_UART_ENABLED
#define APP_UART_ENABLED 1
#endif
// <o> APP_UART_DRIVER_INSTANCE  - UART instance used

// <0=> 0

#ifndef APP_UART_DRIVER_INSTANCE
#define APP_UART_DRIVER_INSTANCE 1
#endif

// </e>

Now we can use nrf_serial_write and nrf_serial_read with timeout > 0.

1.2. Interrupt with DMA

More interesting is using serial interrupt with EasyDMA. The serial_uartes example is good start, to make it true asynchronous interrupt, we will need to implement a event handler.

static void serial_evt_handler(struct nrf_serial_s const * p_serial, nrf_serial_event_t event)
{   
    switch (event)
    {
    case NRF_SERIAL_EVENT_TX_DONE:
        break;

    case NRF_SERIAL_EVENT_RX_DATA:
        break;

    case NRF_SERIAL_EVENT_DRV_ERR:
        break;

    default:
        break;
    }
}

and add it to the configuration NRF_SERIAL_CONFIG_DEF.

1.3. Handle Serial Drvier Errors

If you get a NRF_SERIAL_EVENT_DRV_ERR, which may be caused by uart disconnection, incorrect baud-rate, etc, the error will prolong. The only way to recover is by calling nrf_serial_uninit() and reinitialize with nrf_serial_init() again, as in this example.

void uart_event_handle(struct nrf_serial_s const* p_serial, nrf_serial_event_t event){
    switch (event){
        case NRF_SERIAL_EVENT_DRV_ERR:
            NRF_LOG_INFO("Serial driver error handler");
            uint32_t error;
            if(NRF_UART0->EVENTS_ERROR !=0)
            {
                error = NRF_UART0->ERRORSRC;
                NRF_LOG_INFO("Serial driver err: %d", error);
                NRF_UART0->EVENTS_ERROR = 0;
            }

            nrf_serial_uninit(p_serial);

            uart_init(); // <-- The function you use to configure the uart driver.
            break;
    }
}

2. Add nRF Serial to BLE

To add Serial module to BLE examples is rather straight forward.

2.1. Add required files to .emProject

Under c_user_include_directories add:

../../../../../../components/libraries/serial;
../../../../../../components/libraries/queue;

Under nRF_Libraries add:

<file file_name="../../../../../../components/libraries/serial/nrf_serial.c" />
<file file_name="../../../../../../components/libraries/queue/nrf_queue.c" />

2.2. Changes in sdk_config.h

Enable queue, uart and serial, or add if not already there.

#define UART1_ENABLED 1

#define NRF_QUEUE_ENABLED 1

// </e>

// <q> NRF_SERIAL_ENABLED  - nrf_serial - Serial port interface

#ifndef NRF_SERIAL_ENABLED
#define NRF_SERIAL_ENABLED 1
#endif

2.3. Changes in main.c

This step is bascially a copy/paste every important parts from seral_uartes example, don’t foget to include nrf_serial.h and initilize it in main function.

A special note to NRF_SERIAL_BUFFERS_DEF, as addressed in this post:

how often the event handler is called depends on the size of the buffers set using NRF_SERIAL_BUFFERS_DEF. These buffers are used for transfers, and the size will determine the smallest slices of data that can be transfered in a single UART driver - legacy layer driver request. Increasing the buffer size should give you fever calls of the handler.

Another important thing is try not to use for-loop and nrf_delay to either nrf_serial_write or nrf_serial_read the serial port. This may cause buffer being overrun and missing events. Use app_timer in case of constant delay time.

3. Serial/UART Module Power Consumption

Serial/UART peripheral uses a lot of power, e.g., ~ 1mA in idle with EasyDMA mode. So it is critical to turn it off when not using it. Due to a bug in nRF52840 that persists within the SDK, you’ll need to first uninitialize and then power cycle the it.

err_code = nrf_serial_uninit(&serial0_uarte);
APP_ERROR_CHECK(err_code);
*(volatile uint32_t*)0x40002FFC = 0;
__DSB();
*(volatile uint32_t*)0x40002FFC = 1;
__DSB();

Details can be found in many posts such as here and here.

4. Other Q/As