Embedded - UART Advanced

UART – Advanced Features

Overview / Overview

The most straightforward way to use the UART interface is to set up and handle the UART interface in a round robin operation. The problem with polled UARTs is that polling is inherently inefficient. How long does it take to transmit a character if our UART is configured for a baud rate of 115200 and 8N1? Each byte of data requires 10 symbols to send. This means we can transmit 11520 characters per second. This means that 1 character takes about 86 microseconds to send.

86 microseconds may not seem like a lot, but if our microprocessor is running at 50Mhz, that equates to 4341 clock cycles per character sent. That's quite a few clock cycles to wait in order to send the next character in a string.

We'll look at some hardware and software techniques to reduce the amount of time we spend waiting for data to be transmitted or received.

Interrupt / Interrupts

One of the most obvious ways to reduce the number of clock cycles waiting for the UART is to have the UART generate an interrupt. UART interrupts allow the main application to perform other tasks while the UART is inactive. UARTs usually generate receive and transmit interrupts. The UART generates a receive interrupt when new data arrives. For receive interrupts, the interaction between the application and the ISR is pretty straightforward. The interrupt service routine puts new data into SRAM, alerts the main application, and clears the interrupt. Since the interrupt occurs only when data is received, the processor is free to perform other operations.

Sending interrupts is a little different. Some microcontrollers' UARTs generate an interrupt when the transmit buffer is empty. This interrupt is to indicate to the application that the next byte can be sent over the UART. What we want to avoid is a situation where the application is not transmitting data and as a result the UART keeps generating interrupts that the transmitted data is empty. This situation can leave the application idle as the UART handler is continuously executed. If you are using a microcontroller that keeps generating Tx empty interrupts when there is no data in the Tx hardware FIFO, you can use the following procedure to eliminate idling in the main application:

1. When the UART's circular transmit buffer is empty, the UART handler disables its own transmit empty interrupt. This prevents continuous generation of interrupts.

2. When the application wants to send data, it adds the data to the UART FIFO, then re-enables the interrupt for sending data empty.

 Pipeline FIFOs

In some cases, interrupts alone cannot reduce wasted clock cycles. An example is printing out a long list of characters. We can immediately send the first character in the string to the UART scratch register, but we still have to send the characters in the string one by one, and wait for the next one after sending one. Interrupts help us determine when the next character can be sent, but they don't eliminate the fact that the application cannot continue running until the main program has sent all the characters.

One mechanism that can reduce the time waiting to send (or receive) characters is the UART FIFO. A UART with a hardware FIFO allows an application to write more than one character to the UART. The UART queues the written characters and sends them one by one until the FIFO is empty. We no longer need to wait for a single character to be transmitted. We can send a block of characters to the UART and it returns immediately.

The result of the FIFO is fewer interrupts. Transmit and receive interrupts only occur after a data block has been transmitted/received. Each interrupt results in a context switch and execution of the ISR. Reducing the number of interrupts reduces the clock cycles taken to react to UART interrupts.

A special case we need to consider when using FIFOs is when the UART generates a receive interrupt. A UART with a FIFO may only generate an interrupt 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 only generate an interrupt when the FIFO is full? What we don't want to happen is data getting stuck in the receive FIFO because the FIFO is not full. To avoid this, there is usually a receive timeout interrupt. This interrupt is triggered periodically when data is in the receive FIFO, but there is not enough data to trigger the FIFO full interrupt.

Circular Buffers( Ring buffer) ring buffer

Hardware FIFOs don't solve all our wait problems either. The size of the hardware FIFO is fixed. If the printed string exceeds the capacity of the hardware FIFO, the application will still be busy waiting. Since we cannot dynamically increase the size of the hardware FIFO, we will create a software data structure in memory to buffer the characters going to the UART. The most commonly used software structure is a circular buffer.

So why do we use circular buffers instead of linked lists? The linked list gives us the freedom to buffer characters until the SRAM runs out. The reason we don't use a linked list is memory utilization. For every byte stored in the linked list, we also have to allocate a next pointer, which consumes 4 bytes. That's about 80% of the memory overhead of storing characters in a linked list. Since our microprocessor (microprocessor) only has a few KB of SRAM, this is not an acceptable solution.

On the other hand, circular buffers have very little overhead. A circular buffer must store some information about where to insert the next character, but in general a circular buffer is just an array. The disadvantage of circular buffers is that we cannot easily increase the size of the buffer dynamically. If data is added to the circular buffer faster than it can be removed from the circular buffer, the data is overwritten and lost.

Producer Consumer Model production consumer model

Our UART implementation will include a hardware FIFO, a software circular buffer, an interrupt handler, and the main application. We will use the producer-consumer model to determine when and how to communicate data between the application and the interrupt service routine. Both send and receive operations require their own circular buffers and are checked independently.

  • Receive

When receiving data, the UART interrupt service routine acts as a producer, inserting characters into the receive circular buffer. When a receive or receive timeout interrupt is activated, the ISR will remove data 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 and return.

The main application acts as a consumer of the receive FIFO. The application does not grab data directly from the UART. When the circular buffer is not empty, the application process receives the data in the circular buffer, and then deletes the data entries in it. If there is no data in the circular buffer, the application can choose to wait (block) until data arrives, or continue with other operations.

One design issue we must consider is the condition of a receive interrupt when the receive circular buffer is full. This occurs when data is received faster than the main program can consume it. This situation always results in data loss. The question is, what data will be discarded? Oldest data or newest data? In my experience, the oldest data is discarded, but there is no right answer to this question. In this case, our only real solution is to implement a kind of flow control.

Application Receive Routine Flow Diagram flow diagram of the application receiving data

ISR Receive Flow Diagram Receive interrupt flow diagram

  • Transmit send

When sending data, the application provides the data by inserting characters into the send circular buffer. When a transmit data is empty interrupt occurs, the UART ISR acts as a consumer, fetching and deleting data from the transmit circular buffer.

When sending data using a circular buffer, what happens when the circular buffer is full? We can overwrite the oldest data, but in most cases you should keep waiting until the circular buffer is no longer full. Constantly waiting is not the best option, but it is better than losing data.

Another case we want to address is how data is sent if the send data is empty interrupt is disabled. As mentioned above, the Send Data Empty interrupt is disabled when the application has no data to send. This avoids constant calls of the UART ISR. If our application adds data to the transmit circular buffer, and the transmit data is empty interrupt is disabled, then the transmit data is empty interrupt will not be triggered and the data in the circular buffer will not be consumed by the UART.

When we send data, the application will check the circular buffer. If the circular buffer is empty and the hardware FIFO is not full, we will put the data directly into the hardware FIFO and re-enable the send empty interrupt. The result is that the UART transmits characters in the hardware FIFO, and a new transmit interrupt will be generated 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 the hardware FIFO is full or the circular buffer is empty. The UART will continue to send characters and generate interrupts until the UART ISR detects that the transmit circular buffer is empty, at which point the UART ISR will disable the transmit empty interrupt.

Application Transmit Routine Flow Diagram Application Transmit Routine Flow Diagram flow chart sending data

ISR Transmit Routine Flow Diagram Send data interrupt handler flowchart

Race condition

When the application removes (or adds) data from the circular buffer, we must ensure that the operation of modifying the circular buffer is atomic (atomic). Atomic operations mean that all operations involving modification of the circular buffer cannot be interrupted. If we do not ensure that these operations are atomic, then there will be a race condition. When the main program tries to modify the content of the circular buffer, a UART interrupt occurs and the content of the circular buffer is also modified.

Since an instruction sequence can only be interrupted by higher priority interrupts, we don't 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 while adding or removing data to the circular buffer. We could use some form of semaphore, but the easiest way is to have the main program temporarily disable interrupts before modifying the circular buffer, and then re-enable interrupts after modifying the data.

reference:

1. Advanced use of UART

UART – Advanced Features – ECE353: Introduction to Microprocessor Systems – UW–Madison

Guess you like

Origin blog.csdn.net/guoqx/article/details/131517322