Using the UART interface on the Nordic nRF51 devices

This tutorial shows how to use the UART interface on the Nordic nRF51 devices using the Nordic firmware package. We will create a basic application using the standard C library functions, such as printf() and scanf() and demonstrate how to interface this application with Visual Studio.

  1. Start Visual Studio and launch the VisualGDB Embedded Project Wizard:01-newprj
  2. Ensure that “New Project -> Embedded Binary” is selected on the first page:02-binary
  3. On the next page select the device you want to use. As this example does not use any radio functionality, proceed without selecting a softdevice.03-device
  4. Select the LEDBlink (BSP) sample, enable the FIFO-based UART driver and specify the board type printed on a sticker on your board below:04-ledblink
  5. Select a debug method. The nRF51-DK board shown in this example uses the on-board Segger J-Link with Segger software package:05-debug
  6. Press Finish to create your project. Then replace the main() function with the following code:
        uint32_t err_code;
        const app_uart_comm_params_t comm_params =
        {
            RX_PIN_NUMBER,
            TX_PIN_NUMBER,
            RTS_PIN_NUMBER,
            CTS_PIN_NUMBER,
            APP_UART_FLOW_CONTROL_ENABLED,
            false,
            UART_BAUDRATE_BAUDRATE_Baud38400
        };
     
        APP_UART_FIFO_INIT(&comm_params,
            UART_RX_BUF_SIZE,
            UART_TX_BUF_SIZE,
            uart_error_handle,
            APP_IRQ_PRIORITY_LOW,
            err_code);
     
        for (;;)
        {
            uint8_t ch;
            while (app_uart_get(&ch) != NRF_SUCCESS)
            {
            }
            
            while (app_uart_put(ch) != NRF_SUCCESS)
            {
            }
        }
  7. Then add the includes and definitions required by the new code:
    extern "C"
    {
    #include "app_uart.h"
    #include "app_error.h"
    }
     
    #define UART_TX_BUF_SIZE 256
    #define UART_RX_BUF_SIZE 1
     
    void uart_error_handle(app_uart_evt_t * p_event)
    {
        if (p_event->evt_type == APP_UART_COMMUNICATION_ERROR)
        {
            APP_ERROR_HANDLER(p_event->data.error_communication);
        }
        else if (p_event->evt_type == APP_UART_FIFO_ERROR)
        {
            APP_ERROR_HANDLER(p_event->data.error_code);
        }
    }
  8. If you try building your project now, you will get an error stating that app_fifo.h is missing. This happens because the file is contained a framework that we have not referenced yet:06-nofifo
  9. To fix this, open VisualGDB Project Properties, add the “NRF51 Libraries” framework and check the “fifo” and “uart” checkboxes:07-libs
  10. Press OK and build your project again. The build should now succeed:07a-build
  11. Open the Device Manager, connect the Nordic board to your computer and find the virtual COM port provided by the on-board Segger J-Link. In this example this is COM3:  08-jlinkuart
  12. Open VisualGDB Project Properties, go to the Raw Terminal page and specify the COM port there. Click “advanced settings” and specify the speed and the flow control settings matching the ones set to comm_params. In this example these are 38400 bits per second and hardware control flow:09-uart
  13. Press F5 to start debugging your project. Open the COMx window and type some text there. You will see how the text is being echoed back:10-echo
  14. Now we will demonstrate the use of the standard C library functions like printf(). Include the <stdio.h> file and replace the loop in main() with the following code:
        for (;;)
        {
            int value;
            printf("Enter a number: ");
            scanf("%d", &value);
            printf("\nYou entered %d (0x%x)\n", value, value);
        }
  15. Press F5 to build the updated program and start debugging it. Now you will be able to enter the numbers and see the formatted output:11-numbers
  16. Note that when you start debugging, VisualGDB will prompt whether you want to show an ARM semihosting console, but the console will be empty and all output will be shown in the COMx window:12-semihosting
  17. This happens because the Nordic library overrides the functions for sending/receiving characters, but does not override other functions used by the standard C library. To find out which functions are missing, select “no” in the semihosting prompt and then select “yes” when VisualGDB suggests downloading the Newlib source code:13-newlib
  18. You will see that the semihosting call happens inside the default implementation of _isatty() called from printf():14-stack
  19. Add the following code to main() to override the isatty() function:
    extern "C" int _isatty(int fd)
    {
        return 1;
    }

    Now the semihosting prompt won’t appear anymore.

  20. The code redirecting the output of printf() to the UART is located in the retarget.c file that is provided by Nordic. You can right-click on the _write() function and select “Explore Call Hierarchy” to see that it finally ends up calling app_fifo_put():15-calls
  21. Stepping into app_fifo_put() or opening it via the Call Tree view shows that the function is non-blocking, i.e. it will fail in case the FIFO is filled before its contents is transferred out. Note that FIFO_LENGTH is not a global variable like it may seem, but a macro using the p_fifo parameter. If you have enabled the Clang IntelliSense, you can quickly see it using the Preprocessor Lens and evaluate its value while debugging:16-fifolen
  22. You can easily reproduce the overflow by starting to print multiple lines in an infinite loop:17-overflow
  23. You can resolve this by unchecking the “uart” checkbox in the nRF51 Librares group in Embedded Frameworks configuration and providing your own blocking implementations of _read() and _write():
    extern "C" int _write(int file, const char * p_char, int len)
    {
        for (int i = 0; i < len; i++)
        {
            while (app_uart_put(p_char[i]) != NRF_SUCCESS)
            {
            }
        }
     
        return len;
    }
     
     
    extern "C" int _read(int file, char * p_char, int len)
    {
        while (app_uart_get((uint8_t *)p_char) == NRF_ERROR_NOT_FOUND)
        {
        }
     
        return 1;
    }
  24. If you run your program now, the lines will be printed correctly and no overflow will occur because the _write() function will not return until it manages to store the written character in the FIFO:18-blockingBeware that the blocking version of _write() may interfere with the radio functionality by causing delays that the radio firmware would not expect. Hence if you encounter buffer overruns while debugging radio-related firmware, it is recommended to increase the FIFO size instead.