AVR Embedded Tutorial - UART

From Free Pascal wiki

Deutsch (de) English (en)

UART polling

ATmega328p (Arduino Uno/Nano)

A terminal with the following settings is required for serial output:

Setting Value
Baud Rate 9600
Bits 8
Stopbits 1
Parity none

When pressing the Space Bar in the terminal program, a "Hello World!" message is returned.

Constants

 const
   CPU_Clock = 16000000;  // Arduino clock frequency, default 16MHz.
   Baud = 9600;           // baud rate
   Divider = CPU_Clock div(16 * baud) - 1;

Note: If the CPU clock is too slow for the baud rate, there are transmission errors. Example: With a clock of 1MHz, a baud rate of 9600 does not work. However, a 1MHz clock and 1200 baud will work.

Initialization

Configure the UART interface.

 procedure UARTInit;
   begin
     UBRR0  := divider;                         // Baud
     UCSR0A := 0;                               // Normal speed
     UCSR0B := (1 shl TXEN0) or (1 shl RXEN0);  // Receive and send
     UCSR0C := (%011 shl UCSZ0);                // 8-bit word, 1 stop bit
   end;

Receive characters

  function UARTReadChar : char;
   begin
     while UCSR0A and (1 shl RXC0) = 0 do
       ;                                        // Wait for a character to arrive
     Result := char(UDR0);                      // Read character
   end;

Send characters

 procedure UARTSendChar(c : char);
   begin
     while UCSR0A and (1 shl UDRE0) = 0 do
       ;                                        // Wait for the last character to be sent
     UDR0 := byte(c);                           // Send character
   end;

Send string

 procedure UARTSendString(s : ShortString);
   var
     i : integer;
   begin
     for i := 1 to Length(s) do 
       begin
         UARTSendChar(s[i]);  // send characters one by one
       end;
   end;

Main loop

Here you wait until the space character arrives, then transmit "Hello World!".

program Project1;
 begin
   UARTInit;

   repeat
     if UARTReadChar = #32 then  // #32 = space character
       begin  
          UARTSendString('Hello World!'#13#10); 
       end;
   until 1 = 2;
 end.

ATtiny2313

The registers on the ATtiny2313 have different names than on the ATmega328p.

  • The 0 is omitted in the name.
  • UBRR0 must be broken down into UBRRH and UBRRL .

It will look like this:

 UBRRH := divider shr 8;
 UBRRL := byte(divider);

 UCSRB := (1 shl TXEN ) or (1 shl RXEN);
 UCSRC := (%011 shl UCSZ);

(The code has not been tested)

UART interrupt

With UART polling, a character may be lost. This can happen when a character arrives and you cannot pick it up in time. For this reason it is advisable to have the reception of characters controlled by an interrupt. At higher baud rates, this is mandatory.

A simple 16-character ring buffer is used.

The examples show this using an ATmega328p (Arduino Uno/Nano).

Receive buffer

Declaration

Declare a character buffer: here a 16 character buffer is created.

 const
   Crxlen = 16;    // Buffer size
 var
   RXBuf   : array[0..Crxlen - 1] of byte;
   RXread  : byte = 0;
   RXwrite : byte = 0;

Initialization

This is similar to polling, the difference is that RXCIE0 is used to indicate that an interrupt is triggered when a character is received.

 procedure UARTInit;
   begin
     UBRR0  := divider;
     UCSR0A := (0 shl U2X0);
     UCSR0B := (1 shl TXEN0) or (1 shl RXEN0) or (1 shl RXCIE0);
     UCSR0C := %011 shl UCSZ0;
   end;

Interrupt

When the interrupt is triggered, the character is copied from the UART into the character buffer.

 procedure UART_RX_Receiving; public name 'USART__RX_ISR'; interrupt;
   var
     b : byte;
   begin
     // Read characters
     b := UDR0;

     // Write characters to buffer
     if b <> 0 then 
       begin
         RxBuf[Rxwrite] := b;
         Inc(RXwrite);

         if RXwrite >= Crxlen then 
           begin
             RXwrite := 0;
           end;
       end;
   end;

Main loop

The main loop checks whether something is in the receive buffer, and if so, a character is read from it. In this example, the characters received are converted to uppercase letters and then output.

 begin
   // Initialize UART which is interrupt controlled
   UARTInit;

   // Enable interrupts
   asm
     sei
   end;

   // Main loop
   repeat

     // Is there a character in the buffer?
     while RXwrite <> Rxread do 
       begin
         // Disable interrupts
         asm 
           cli
         end;

         // Read character from buffer
         ch := RXBuf[rxread];
         Inc(RXread);

         if RXread >= Crxlen then 
           begin
             RXread := 0;
           end;

         // Enable interrupts
         asm 
           sei
         end;

         // Convert lowercase letters, into uppercase ones
         if ch in[$61..$7A] then 
           begin
             Dec(ch, 32);
           end;

         // Output character
         UARTSendChar(char(ch));
       end;
   until 1 = 2;
 end.

Send buffer

Declaration

Declare a send buffer: here a 16 character buffer is created.

 const
   CTXlen = 16;    // Buffer size
 var
   TXBuf   : array[0..CTXlen - 1] of byte;
   TXread  : byte = 0;
   TXwrite : byte = 0;

Initializarion

This is similar to the receive buffer. Important: the interrupt function must not be activated yet, because there is nothing to send yet.

 procedure UARTInit;
   begin
     UBRR0  := divider;
     UCSR0A := (0 shl U2X0);
     UCSR0B := (1 shl TXEN0) or (1 shl RXEN0);
     UCSR0C := %011 shl UCSZ0;
   end;

Interrupt

The interrupt is triggered as soon as a character has been sent to the UART.

As soon as the ring buffer is empty, the interrupt function is deactivated again.

The query of UDRE0 is not needed here, since the interrupt is only triggered when the buffer is empty.

 procedure UART_UDRE_Send; public name 'USART__UDRE_ISR'; interrupt;
   begin
     // Send character
     UDR0 := TXBuf[TXread];
    
     // Move ring buffer
     Inc(TXread);

     // When pointer is at the end, start again
     if TXread >= CTXlen then 
       begin
         TXread :=0;
       end;

     // If the ring buffer is empty, an interrupt is no longer necessary
     if TXread = TXwrite then 
       begin
         UCSR0B := UCSR0B and not (1 shl UDRIE0);
       end;
   end;

Send string

As soon as something is in the send buffer, the interrupt is not activated.

 procedure UARTSendString(s : ShortString);
   var
     i : byte;
   begin
     // Fill the ring buffer with data
     for i := 1 to Length(s) do 
       begin
         TXBuf[TXwrite] := byte(s[i]);
         Inc(TXwrite);
         
         if TXwrite >= CTXlen then 
           begin
             TXwrite := 0;
           end;
     end;

     // Enable send interrupt
     UCSR0B := UCSR0B or (1 shl UDRIE0);
   end;

Main loop

The string is output in the background. In the example, an LED flashes during output.

 var
   z : byte;
   s : ShortString;
 begin
   DDRB := DDRB or (1 shl 5);

   // Initialize UART that is interrupt controlled
   UARTInit;

   // Enable interrupts
   asm 
     sei
   end;

   // Main loop
   repeat
     UARTSendString('Hello World');
     UARTSendString('!'#13#10);

     PORTB := PORTB or (1 shl 5);
     mysleep(sl);

     Str(z : 3, s);
     UARTSendString(s + '.');
     Inc(z);

     PORTB := PORTB and not (1 shl 5);
     mysleep(sl);

   until 1 = 2;
 end.

UART emulation in software

It is also possible to emulate a UART interface using software.

This is practical if you have an AVR which has no/too few hardware UARTs.

Note: This code only works with a 16MHz CPU clock and 9600 baud. Simply adjusting baud and CPU_Clock is not enough because the formula still has an error.

Important: Interrupts must be blocked during the transmission.

Reading and writing at the same time does not work because there is no buffer.

Ideally, this can be used to output errors to a UART terminal.

Constants

In this example, Pins 2 and 3 from Port D are used.

 const
   CPU_Clock = 16000000; // Arduino clock frequency, default 16MHz.
   Baud = 9600;          // Baud rate

   TXpin = (1 shl 3);    // Pin 3 (receive)
   RXpin = (1 shl 2);    // Pin 2 (send)

   softDivider = CPU_Clock div(Baud * 22);  // bit counter

Set the receive pin to input

 begin
   DDRD := DDRD or TXpin;
   ...
   // loop
   ...
 end.

Note: If you want to use the same pins as for the hardware UART, pins 0 and 1 must be enabled on the Arduino.

 begin
   UCSR0B := 0;  // Release blocked pins 0 and 1
   ...
   // loop
   ...
 end.

Send characters

 procedure UARTSoftSendByte(c : byte);
   var
     i, data : int16;
     j : Int8;
   begin
     asm 
       cli  // Disable interrupts
     end;  

     Data := (c shl 1) or $FE00;
 
     for j := 0 to 9 do  // Output bits, including stop bit
       begin
         if(Data and 1) = 1 then 
           PORTD := PORTD or TXpin;      // HIGH
         else 
           PORTD := PORTD and not TXpin; // LOW
       
         Data := Data shr 1;

         for i := 0 to softDivider do    // One bit break
           ;  
       end;

     asm 
       sei
     end;  // Enable interrupts
   end;

Receive characters

Note: If no character is received, the loop hangs forever.

  function UARTSoftReadByte : byte;
   var
     i : Int16;
     j : Int8;
   begin
     asm 
       cli   // Disable interrupts
     end;

     Result := 0;

     while PIND and RXpin > 0 do  // Wait until the first falling edge
       ;  

     for i := 0 to softDivider div 2 do  // Half a bit break
       ;

     for j : = 0 to 7 do 
       begin
         for i := 0 to softDivider do    // One bit break
           ;
       
         Result := Result shr 1;

         if PIND and RXpin <> 0 then 
           Inc(Result, $80);
       end;

     for i := 0 to softDivider           // One bit break
       ;

     asm 
       sei   // Enable interrupts
     end;
   end;

See also