Configuring a UART interface for polled operation is a fairly straight forward process. The problem with a polled UART is the inherent inefficiency in polling. If our UART was configured for a baud rate of 115200 and 8N1, how long does it take to transmit a single character? Each byte of data requires 10 symbols to be sent. This means that we can transmit 11520 characters per second. This means that 1 character takes roughly 86 microseconds to send.
86 microseconds might not seem like a long time, but if our microprocessor is running at 50Mhz, that equates to 4341 clock cycles per character sent. That’s a fair number of clock cycles to busy wait in order to send the next character in a string of characters.
We are going to examine some hardware and software techniques that will reduce the amount of time that we have to wait for data to be transmitted or received.
One of the most obvious ways to reduce the number of clock cycles waiting for the UART is to have the UART generate interrupts. UART interrupts allow the main application to carry out other tasks when the UART in inactive. UARTs typically generate receive and transmit interrupts. A UART will generate a receive interrupt when new data has arrived. The interaction between the application and the ISR is pretty straight forward for receive interrupts. The interrupt service routine places the new data into SRAM, alerts the main application, and clears the interrupt. Since interrupts only occur when data is received, the processor is free to carry out other operations.
Transmit interrupts are a little bit different. Some microcontroller’s UARTs generate an interrupt when the transmit buffer is empty. This interrupt is meant as an indication that the application can send the next byte over the UART. What we want to avoid is the situation where the application is not transmitting data and, as a result, the UART constantly generates transmit empty interrupts. This situation would starve the application because the UART handler would be executed constantly. If the microcontroller you are using constantly generates Tx empty interrupts when no data is in the Tx hardware FIFO, you can use the following process to eliminate the starvation of the main application.
- The UART handler disables its own transmit empty interrupts when the UART’s transmit circular buffer is empty. This prevents a continuous flow of interrupts
- When the application wishes to send data, it adds the data to the UART FIFO and then re-enables transmit empty interrupts.
In some situations, interrupts alone are not going to reduce wasted clock cycles. An example would be printing out a long string of characters. We can immediately send the first character in the string to the UART holding register, but we will still have to busy wait for each additional character in the string. Interrupts help us determine when it is OK to send the next character, but they don’t eliminate the fact that the application cannot continue on until the main application has transmitted all the characters.
One mechanism that will decrease the amount of time waiting to send (or receive) characters is a UART FIFO. UARTs that have a hardware FIFO allow an application to write more than one character to the UART. The UART queues up the characters and will transmit each character until the FIFO is empty. We no longer have to wait for a single character to transmit. We can send a block of characters to the UART and return immediately.
FIFO’s result in fewer interrupts. Transmit and receive interrupts will occur only after a block of data has been transmitted/received. Each interrupt results in a context switch and execution of the ISR. Reducing the number of interrupts reduces the number of clock cycles spent reacting to the UART interrupts.
One special situation we need to consider when using FIFOs is when the UART generates a receive interrupt. A UART with a FIFO may generate an interrupt only when the receive FIFO is full ( or nearly full). So what happens when a single byte of data is received and the FIFO is configured to generate an interrupt only when the FIFO is full? What we don’t want to happen is for data be stuck in the receive FIFO because the FIFO is not full. In order to avoid this situation, there is normally a receive timeout interrupt. This interrupt is triggered periodically when data is in the receive FIFO but not enough data to trigger the FIFO full interrupt.
Hardware FIFOs also do not solve all of our waiting problems. Hardware FIFOs are of a fixed size. If a string is printed that exceeds the capacity of the hardware FIFO the application will still busy wait. Since we cannot dynamically increase the size of the hardware FIFO, we will create a software structure in SRAM to buffer the characters destined for the UART. The software structure most commonly used is a circular buffer.
So why are we going to use a circular buffer instead of a linked list? A linked list gives us the freedom to buffer characters until SRAM is exhausted. The reason we will not use a linked list is memory utilization. For each byte stored in a linked list we also have to allocate a next pointer which consumes 4 bytes. That’s roughly an 80% memory overhead for storing characters in a linked list. With only a few kilobytes of SRAM in our microprocessor, that’s not an acceptable solution.
A circular buffer on the other hand has very little overhead. The circular buffer must store some information on where to insert the next character, but by and large, a circular buffer is simply an array. The disadvantage of the circular buffer is that we cannot dynamically increase the size of the buffer easily. If the rate at which data is added to the circular buffer is greater than the rate that data is removed from the circular buffer, data will be overwritten and lost.
Producer Consumer Model
Our UART implementation will include hardware FIFOs, software circular buffers, interrupt handlers, and the main application. We will use a producer-consumer model to determine when and how to communicate data between the application and the interrupt service routine. Both the transmit and receive operations require their own circular buffer and each operation will be examined independently.
When receiving data, the UART interrupt service routine will act as the producer by inserting characters into the receive circular buffer. When either the receive or receive timeout interrupt is active, the ISR will remove entries from the hardware FIFO until the hardware FIFO is empty. Each data entry is placed into the receive circular buffer. When the hardware FIFO is empty, the ISR will clear the receive interrupt(s) and return.
The main application acts as the consumer of the receive FIFO. The application does not grab data directly from the UART. The application checks the receive circular buffer and removes entries from the circular buffer when it is not empty. If there is no data in the circular buffer, the application can choose to wait (block) until data arrives or continue on with other operations.
One design consideration we must address is the situation when the receive circular buffer is full and a receive interrupt occurs. This situation would arise when data is being received faster than the main application can consume it. This situation will always result in lost data. The question is, which data gets discarded? The oldest data or the newest data? In my experience, the oldest data gets discarded, but there is no correct answer to this question. The only real solution we have in this situation is to implement a form of flow control.
Application Receive Routine Flow Diagram
ISR Receive Flow Diagram
When transmitting data, the application produces data by inserting characters into the transmit circular buffer. When an transmit empty interrupt occurs, the UART ISR acts as the consumer and removes data from the transmit circular buffer.
When transmitting data using a circular buffer, what happens when the circular buffer is full? We could overwrite the oldest data but in most situations you should busy wait until the circular buffer is no longer full. Busy waiting is not optimal, but it is better than loosing data.
Another situation we have to address is how does data get transmitted if the transmit empty interrupts have been disabled. As discussed above, the transmit empty interrupt is disabled anytime the application has no data to send. This prevents the UART ISR from running constantly. If our application adds data to the transmit circular buffer and the transmit empty interrupt is disabled, the transmit empty interrupt is never triggered and the data in the circular buffer is never consumed by the UART.
When we transmit data, the application will examine the circular buffer. If the circular buffer is empty and the hardware FIFO is not full, we will place data directly into the hardware FIFO and re-enabled transmit empty interrupts. The result is that the UART transmits the characters in the hardware FIFO and will generate a new transmit interrupt when the hardware FIFO is empty. The UART ISR will then check the transmit circular buffer. If there is data in the circular buffer, the ISR consumes characters until either the hardware FIFO is full or the circular buffer is empty. The UART will continue to transmit characters and generate interrupts until the UART ISR detects that the transmit circular buffer is empty, at which point the UART ISR will disable transmit empty interrupts.
Application Transmit Routine Flow Diagram
ISR Transmit Routine Flow Diagram
When the application is removing (or adding) data from the circular buffer we must make sure that operations that modify the circular buffer are atomic. Atomic simply means that all of the operations involved in the modification of the circular buffer must happen without interruption. If we do not ensure that these operations are atomic, then we can have a race condition where the main application is attempting to modify the contents of a circular buffer when a UART interrupt occurs.
Since a sequence of instructions can only be interrupted by a higher priority interrupt, we do not need to worry about the UART ISR being interrupted by the application. On the other hand, we do need to prevent the application from being interrupted when removing/adding data to/from the circular buffer. We could use some form of a semaphore, but the easiest approach would be to have the main application temporarily disable interrupts prior to modifying the circular buffer and then re-enable them after the data has been removed.