Computing stuff tied to the physical world

Serial port interrupts

As a case study, let’s look into an interrupt-driven driver for a serial port. Serial ports are everywhere, and perhaps even the most common way to get data into and out of a µC. The hardware for this is called a UART, which stands for Universal Asynchronous Receiver / Transmitter (if they also handle synchronous communication, they’ll be called USARTs).

A UART receives a bit stream and reassembles it into a byte stream, and also can send out bytes, again as a stream of bits with proper “start” and “stop” bits, parity, etc. Other than sharing some hardware, the send and receive sides are essentially independent. Also part of the configuration is the bit rate, often 9600, 19200, 57600, or 115200 baud.

At 115,200 baud, new data bytes can arrive once every 86 µs and we need to fetch each byte before the next one has arrived (at least with single-buffered hardware, as in the LPC8xx). This is an excellent reason to use interrupts – incoming data can be collected into a larger buffer until we’re ready to process it.

For the sending side, interrupts are usually less important. Without interrupts, our code will wait and send each byte out before continuing with other tasks. With interrupts, we can quickly stash (at least part of) the outgoing data into a buffer, and continue with our work, while interrupts gradually drain the buffer and send each byte out.

There is a nice symmetry in all this: the receive interrupt fills a buffer, and we empty it when we’re interested in it. Conversely, we can fill a buffer when we have something to send, and the transmit interrupt plucks each byte out of the buffer as soon as it can be sent. Note that we need two separate buffers for this.

Circular buffers

As first step, we’ll need some sort of buffering mechanism. This can be done elegantly using a ring buffer – which is implemented using an array, where successive entries store the individual bytes, and then wrap around back to the first slot.

Here is a complete ringbuffer implementation in C++ (also on GitHub):

template < int SIZE >
class RingBuf {
  volatile uint8_t in, out, buf [SIZE];
  RingBuf () : in (0), out (0) {}

  bool isEmpty () const { return in == out; }
  bool isFull () const { return (in + 1 - out) % SIZE == 0; }

  void put (uint8_t data) {
    if (in >= SIZE)
      in = 0;
    buf[in++] = data;

  uint8_t get () {
    if (out >= SIZE)
      out = 0;
    return buf[out++];

In other words: a ringbuffer is an object with 4 methods (isEmpty, isFull, put, and get), it has a buffer area (called “buf”) and two indices into that buffer (called “in”, and “out”).

The template mechanism allows us to efficiently define a ring buffer with a specific size.

Serial driver API

To implement a complete UART driver on top of this is a little more involved. The code for this can be found here. First, we define two ringbuffers, of size 16 and 64, respectively:

  RingBuf<16> txBuf;
  RingBuf<64> rxBuf;

Next, we need to define four functions:

  1. uart0Init() – configures the UART and prepares it to work with interrupts
  2. UART0_IRQHandler() – the interrupt handler, filling rxBuf and emptying txBuf
  3. uart0SendChar() – wait until there is room in txBuf, then add a byte to send out
  4. uart0RecvChar() – get the next byte out of rxBuf, or return -1 if there isn’t any

Looking through the code, you should note that:

  • the assignments to LPC_USART0->INTENCLR and LPC_USART0->INTENSET are used to briefly disable and re-enable UART interrupts while we’re accessing the buffers – this is a far better approach than completely disabling all interrupts
  • it all looks deceptively simple – but make no mistake, there are some very subtle aspects to this code: the placement and order of these tests and calls is critical

Much of the elegance of such a design comes from having a very clean abstraction (ring buffers), and the fact that modern UART hardware has evolved to allow very simple use. A lot of edge cases, such as avoiding receive buffer overflows and disabling interrupts when txBuf empties, end up working well with very little code.

In a way, these software-based ring buffers form a holding tank for data which just came in or is about to go out. They allow the rest of the app to be much more relaxed in its timing.

Sample code

Here is an example app using this interrupt driver (also available on GitHub):

#include "sys.h"
#include "uart_irq.h"

int main () {


  // this string exceeds the 15-byte capacity of the output ring buffer
  printf("123456789 123456789 123456789 123456789 123456789\n");

  while (true) {
    printf("%u\n", (unsigned) tick.millis);

(clarification: serial.init() is a thin wrapper around the uart0Init() code)

The while loop sends out a short message once every 500 ms, and due to the interrupt-driven design, the printf will return immediately. So every 500 ms, one new message is sent out, and this will take place during the next call to tick.delay().

With a non-interrupt-based design, the printf would wait for each character to be sent out before returning, causing each while loop iteration to take slightly more than 500 ms.

This example doesn’t use the input buffer (which will simply fill up and never get read).

In conclusion

Interrupt-based drivers often follow this same basic design: a dedicated data structure (in this case: two ring buffers) “sits between” the low-level interrupt handler, which does as little as possible, and the high-level “user mode” API, which carefully disables/enables interrupts when dealing with that shared data structure.

In this serial port driver, uart0Init(), uart0SendChar(), and uart0RecvChar() fully define the high-level public API, for use in the rest of the application.

Our application code is not allowed to access the UART hardware directly – and as reward for adhering to this discipline, it need not worry about interrupts or race conditions.

Note also that rxBuf and txBuf are not part of the public API. Application code must go through those three API routines at all times. A somewhat cleaner API could in fact have been created by defining the entire serial driver as a C++ class with private data members.

[Back to article index]