Monday, October 08, 2007

Implement an UART Driver (cc)

Writing an Interrupt-Driven UART Driver

Generally speaking, a driver's purpose is to provide an application or operating system with an interface to hardware. In this case, we will discuss designing and writing a driver for the UART.

The code examples presented here are snippets from my driver for the Atmel AVR microcontroller. The buffer data structure and functions mentioned are available as well. You may download the complete driver from the articles page.

  1. Design
  2. Receiver
  3. Transmitter
  4. Buffers
  5. Interface
    1. putchar()
    2. puts()
    3. getchar()
  6. Interrupt Handlers
    1. Receiver Interrupt
    2. Transmitter Interrupt
  7. Initialization

Design

An interrupt-driven device such as a UART can be represented as a three-level design on top of the hardware: the interrupt handlers, buffers, and application interface. For the UART, the interrupt handlers that we are most interested in are for the 'receiver has data' event and the 'transmitter is empty' (and thus can accept more data) event. The application interface may be the familiar 'getchar()' and 'putchar()' routines.

The following figure shows the three software layers, arranged from left to right by their 'closeness' to the hardware:

design

Receiver

The receiver is a physical shift register into which data arrives. A UART receiver generally has the capacity to store one byte of data at a time. In interrupt-driven mode, the UART will generate some sort of 'receiver has data' interrupt when data arrives. Because a UART is asynchronous and data can arrive at any moment, the application code running on the processor may not be ready to deal with the data. In fact, the application will not actually need the data until a routine like 'getchar()' is explicitly called. However, the received byte will be overwritten by the next byte if it is left in the receiver data register.

The receiver interrupt handler, therefore, will read the newly arrived byte (step '1' in the figure above) and copy it into the receive buffer (step '2'). Whenever it is called in application code, 'getchar()' will read the next available character from the receive buffer.

Transmitter

The UART transmit side copies a byte into a physical shift register, from which a hardware state machine actually shifts the data to the outside world. The transmitter register typically can hold one byte at a time and therefore some amount of time must pass before the next byte of data may be written. The transmitter is considered to be ready for the next byte when it is empty (that is, the data from the register has been shifted out). The UART typically provides an interrupt for 'transmitter is empty', allowing the transmitter to be dealt with only when it is ready.

In order to free the application code from having to wait for the UART transmitter to be ready, a transmit buffer is provided. Calls to routines like 'putchar()' simply copy characters to the transmit buffer. When the 'transmitter is empty' interrupt occurs, the transmit interrupt handler will read the next byte from the transmit buffer (step '1' in the figure above) and copy it into the UART transmitter register (step '2').

Buffers

The buffers placed between the UART interrupt handlers and application interface can be identical in implementation. A typical approach is a 'ring' buffer (similar to a circular queue). Please see "Character Ring Buffer" for an explanation of the buffer and example code. The buffer data type and interface used in that article is used in the examples here.

The transmit and receive buffers should be accessible both interrupt handlers and interface routines that need access to them. This can be accomplished by declaring them in global scope. Additionally, they (along with any global variables used by an interrupt handler) should be declared 'volatile' to prevent problems introduced by compiler optimization.

Interface

This section suggests implementations for the typical 'putchar()', 'puts()' and 'getchar()' functions. The code is written in a generic manner with processor-specific tasks such as interrupt configuration abstracted away as macros that are assumed to be defined.

putchar()

The 'putchar()' routine simply takes a byte and stores it in the transmit buffer. Typically, it returns the byte that was passed to it or an error code. Assuming that 0x00 (character NULL) is not expected to be transmitted, 0 may be a good choice of error code. -1 is also often used. Here is an example:

int uart_putchar( char c  )
{
   
if( buffer_put( &transmit_buffer, c ) ) {
        UCSR0B
|= (1<<UDRIE0); /* enable TX interrupt */
       
return -1 ;
   
}
   
return c;
}

puts()

It is usually helpful to provide a 'put string' function along with 'putchar()' unless the C stdio library functions puts() and printf() are going to be available. The 'puts()' routine can either copy the string into the transmit buffer as part of the buffer interface or it can simply call 'putchar()' repeatedly for each byte in the string. Assuming that we are using the version of 'putchar()' presented here, an implementation of 'puts()' can be written like this:

int uart_puts( const char * s )
{
   
int i = 0;
   
   
while( *s != '\0' )
       
if( uart_putchar( *s ++ ) == 0 )
           
break;
       
else
            i
++;
   
return i;
}

Recall that a C string is a character array with a terminating 0x00 byte. This version of 'puts()' attempts to copy each byte in the string to the transmit buffer, returning the number of bytes that it was able to copy.

getchar()

There are two typical ways to implement 'getchar()': blocking and non-blocking. Occasionally, it is helpful to have both available. A blocking 'getchar()' will wait until there is data in the receive buffer, effectively halting the application program if no data is available. This is similar to console input in a shell application and is sometimes desirable. However, a blocking call may be inappropriate in, for example, a control system. A non-blocking 'getchar()' will instead return a status code in the event that no data was available. This allows the routine to be called without potentially halting application code.

A blocking 'getchar()' can be written like:

char getchar( void  )
{
   
while( buffer_size ( &receive_buffer ) == 0 ); /* wait for data, if none is available */
   
return buffer_get( &receive_buffer );
}

The caller, of course, can also check the buffer capacity before calling 'getchar()'. The non-blocking version can be written in several ways. A status code (such as 0x00 or -1) can be used, or 'getchar()' could take a pointer to the 'destination' character and return only status codes. A simple implementation taking a pointer and returning the status code can be:

char uart_getchar( char  *c )
{
   
if( buffer_size ( &receive_buffer ) > 0 ) {
       
*c = buffer_get ( &receive_buffer );
       
return 1;
   
}
   
return 0 ;
}

Interrupt Handlers

Implementation for interrupt handlers greatly depends on the processor that you are using. Suggested implementations of 'receiver has data' and 'transmitter is empty' are discussed here in a very generic manner. You should read your processor and toolchain documentation for the details. Additionally, processors differ in the way UART interrupts occur. Some processors provide individual interrupts for various UART events. Others provide one general 'UART event' interrupt, whose handler must first decide what event has occurred and then handle that event. This is normally done by checking a status register. In such cases, the 'read' of the status register usually clears the interrupt flag.

The 'receiver has data' interrupt handler usually just needs to read from the UART data register and write to the receive buffer. Similarly, the 'transmitter is empty' handler simply reads from the transmit buffer and writes to the UART data register. In the 'transmit' handler, it is often helpful to check the occupancy of the transmit buffer after performing the read and copy. When the transmit buffer is empty, the 'transmitter is empty' interrupt can be turned off as it is no longer needed until 'putchar()' is called again (and re-enables the interrupt). This prevents the 'transmit' interrupt from happening again when the last byte is shifted out but no new data is ready.

Receiver Interrupt

Here is an example of a receiver interrupt handler for the AVR:

ISR(USART_RX_vect) 
{
   
char temp;

   
/* receiver has data */
   
if( UCSR0A & (1<< RXC0) ) {
        temp
= UDR0;
        buffer_put
( &receive_buffer, temp );
   
}
}

Transmitter Interrupt

Here is an example of a transmitter interrupt handler for the AVR:

ISR(USART_UDRE_vect) 
{    
   
/* we have data to send */
   
if( buffer_size( &transmit_buffer.g ) > 0 ) {
        UDR0
= buffer_get( &transmit_buffer );
       
if( buffer_size( &transmit_buffer ) == 0 ) /* no more data to send */
            UCSR0B
&= ~(1<<UDRIE0); /* disable TX interrupt */
   
}
   
else
        UCSR0B
<= ~(1<<UDRIE0); /* disable TX interrupt */
}

This interrupt handler transmits a byte if the transmit buffer had any data. It then ensures that the "transmitter is empty" interrupt is disabled when there is no more data in the transmit buffer.

Initialization

One final piece of software that your driver should provide is an 'init' routine. This routine should ensure that the UART is ready to use. Typically it will:

  • initialize the transmit and receive buffers
  • enable the UART transmitter and receiver (and configure IO pins, if needed), configure the baud rate and other UART settings
  • enable the UART 'receiver has data' interrupt

The 'transmitter is empty' interrupt is typically not enabled during 'init' because it is not needed until 'putchar()' is called. Here is an example for the AVR:

 inline void uart_init( uint16_t brr )
{
   
/* set up buffers */
    buffer_init
( receive_buffer );
    buffer_init
( transmit_buffer );
   
   
/* set up UART */
    UBRR0L
= (uint8_t)brr ;
    UBRR0H
= brr>>8;
#ifdef UART_X2
    UCSR0A
|= (1<<U2X0);

No comments: