Writing a buffered UART driver for the NXP LPC17xx

Tyler Drazich
July 11, 2016

Although UARTs have been around for a long time, there are still many uses for them on embedded devices due to their minimal hardware requirements. Whether the UART is used for inter-device communication or for a simple terminal interface, it can be a real challenge to create a driver that not only provides reliable high speed operation, but maintains a simple API. This post explains the steps and design considerations that were made while creating the UART driver for the DZX Embedded SDK on the NXP LPC17xx platforms.

Architecture

The driver was required to follow a common API to maintain support across multiple hardware platforms (different MCUs). Each hardware platform driver defines a UART structure that contains the variables necessary to implement the driver for each UART port. The UART data structure for the LPC17xx is shown below.

struct UART
{
    NODE            node;                           /**< A node to place the UART into a list of open UARTs */
    UINT32          number;                         /**< The port number of the UART */
    REG_UART*       REG;                            /**< A pointer to the peripheral registers for the UART */
    BUFFER          rxb;                            /**< A circular buffer for storing received data */
    BUFFER          txb;                            /**< A circular buffer for storing data to be transmitted */
    BYTE            rxmem[CFG_UARTRXBUFFERSIZE];    /**< Memory allocation for the receive buffer */
    BYTE            txmem[CFG_UARTTXBUFFERSIZE];    /**< Memory allocation for the transmit buffer */
};

Using a structure for the variables per port defers all memory allocation until the UART datatype is instantiated, which means that memory resources are only used when the port is used. The driver provides the following functions for opening and transferring data through a UART.

STATUS UART_Open(UART* uart, UINT32 number, UINT32 baud, UINT32 nbits, STOPBITS stopbits, PARITY parity, HANDSHAKE hs);
STATUS UART_Read(UART* uart, void* buf, UINT32 nbytes, UINT32* actual, UINT32 timeout);
STATUS UART_Write(UART* uart, const void* data, UINT32 nbytes, UINT32* actual, UINT32 timeout);

Buffering Data

With reliability being a major requirement, it was chosen to use a buffer for holding received data to ensure that no data is dropped, even if it happens to be received without the application making a call to UART_Read(). The driver would make use of a circular buffer. If you are unfamiliar with a circular buffer, you can read about it here: https://en.wikipedia.org/wiki/Circular_buffer.

It turns out that the real-time kernel provided within the DZX SDK includes a special implementation of a circular buffer that also supports blocking calling threads until a specified amount of data or free space is available. These circular buffers are exactly what is needed when creating a UART driver. You can view the Circular Buffers API provided by the kernel within our documentation.

When the UART is opened by the application, by calling UART_Open(), the driver initializes the underlying circular buffer and initializes the UART peripheral. After the peripheral has been initialized, the receive interrupt is enabled. When the receive interrupt occurs, the driver acquires the received data from the peripheral and passes it to the underlying receive buffer. The receive interrupt handler is shown below. Take note that the handler empties the peripheral FIFO in a tight loop and then passes all of that data to the circular buffer. This method was chosen because the buffer component within the kernel is more efficient with fewer writes of larger sets of data rather than a bunch of writes of a single data byte.

BYTE buf[UARTFIFOSIZE];                                            /* FIFO is 16-bytes on LPC17xx */
UINT32 num;


switch (uart->REG->IIR.BITS.IID) {                                 /* Determine the source of the interrupt */
    case 0x2:                                                      /* Receive Data Available Interrupt (RDA) */
    case 0x6:                                                      /* Character Time-out Indicator Interrupt (CTI) */
        num = 0;
        while (uart->REG->LSR.BITS.DR == 1) {                      /* Empty the receive FIFO */
            buf[num++] = uart->REG->RBR;
            if (num >= UARTFIFOSIZE) {
                break;
            }
        }
        BUFFER_Write(&uart->rxb, buf, num, NULL, 0);                /* Buffer the received data */
        break;
}

Now that all of the received data makes its way to the underlying buffer, the driver simply calls the kernel's BUFFER_Read() function when the application wants to read the UART.

Transmitting Data

For the sake of efficiency, the driver uses another circular buffer for transmitting data. A transmit buffer allows the application to make transmit calls without having to block and wait for the transfer to complete. The UART_Write() function will only attempt to block the caller if the transmit buffer happens to be full, but the function allows for a timeout to be specified.

To avoid potential race conditions, the driver passes all data to be transmitted to the underlying transmit buffer by first calling BUFFER_Write(). The buffer read/write API is thread safe itself, so threads can simply compete to get their data into the buffer. After the data is buffered, the driver enters a critical section and if the transmit interrupt is disabled and there is data in the buffer, the driver removes some data from the buffer and primes the peripheral for the next transfer and enables the transmit complete interrupt. The transmit interrupt handler then queries whether there is data remaining in the transmit buffer; if so, it removes some data from the buffer and primes the peripheral for another transfer. If the buffer happened to be empty, the interrupt handler simply disables the interrupt to signal the next UART_Write() operation to prime the peripheral for the next transfer.

Using the Driver

If an application thread wants to read and consume all of the received data from a UART, the code below could be used. The snippet below simply loops and continuously reads the UART. If the specified amount of data is received (sizeof(buf)), the call would return immediately, otherwise the call would take up to 10 kernel ticks to return. The data that is received is then sent back out the UART.

void APP_Thread(void* arg)
{
    static UART uart;
    STATUS status;
    BYTE buf[32];
    UINT32 num;


    status = UART_Open(&uart,                               /* Open the uart */
                       0,                                   /* Port number, UART 0 */
                       115200,                              /* Baud rate, in bps */
                       8,                                   /* Number of data bits */
                       STOPBITS_ONE,                        /* One stopbit */
                       PARITY_NONE,                         /* No parity */
                       HANDSHAKE_NONE);                     /* No hardware flow control */

    if (status == SUCCESS) {                                /* Port opened successfully? */

    }


    for (;;) {                                              /* Loop and receive forever */

          UART_Read(&uart,                                  /* A pointer to the uart to read from */
                    buf,                                    /* A pointer to a buffer to receive the data */
                    sizeof(buf),                            /* The maximum amount of data, in bytes, to be returned */
                    &num,                                   /* A pointer to a variable to retrieve the actual amount of data that is returned */
                    10);                                    /* Maximum amount of time, in kernel ticks, to block and wait */

          if (num > 0) {                                    /* Has any data been returned? */

              /* 'num' bytes have been received and are
                  located within the local variable 'buf' */

              UART_Write(&uart,                            /* Echo the received data */                                                 
                          buf,                             /* A pointer to the data to be transmitted */
                          num,                             /* The amount of data, in bytes, to be transmitted */
                          NULL,                            /* Pointer to variable to receive actual amount transferred (not used here) */
                          INFINITE);                       /* Wait indefinitely for space in the transmit buffer */
          }
    }
}

Conclusion

The completed UART driver provides a minimal API for any application to read and write data over a UART. This driver has been tested and shown to be reliable even with baud rates up to 1Mbit/s. The described driver is part of the entire MCU driver that is provided as part of the DZX SDK for each supported microcontroller platform. You can download and try the driver for yourself as part of the SDK evaluations.

Comments

No comments
Submit Comment