USART
INTRODUCTION:
In lamest terms the USART (Universal Synchronous and Asynchronous serial Receiver and Transmitter) is a 0-5V version of the RS232 serial protocol. A lot of devices communicate over this protocol and several devices exist to boost the USART to RS232 levels so that you could talk to serial devices. RS232 was the protocol of choice before USB and many industrial and aviation hardware still contain RS232 ports.
HARDWARE:
Figure 1: ATmega8 USART
Hardware wise serial is simple. Most the time you will be using 2 pins and a ground, the RXD (Receive Data) and TXD (Transmit Data). The device that you are trying to talk to will also have a TX and a RX pin, in order to get the two devices talking you have to hook up the TX of each device to the RX pin of the other. If you think about if it's just like your telephone at home, you talk into the microphone (transmitter) and the person on the other end hears what you said on the Receiver (speaker). A clock pin also exits XCK, however, most devices do not need this line.
Figure 2: ATmega8 & ATmega168/328 to Xbee Hookup
Like any other device, it is important to tie the grounds of both devices together. Failure to do so will result in garbage data and can damage both devices.
Lastly remember that both the devices are rated at the same voltage or that the lower voltage device can take the voltage of the higher device on on its receive pin. The perfect example is the Xbee which is a 3.3V device, however, its serial data pins are 5V tolerant so an AVR running at any voltage up to 5V and still talk to the 3.3V Xbee. If the 3.3V device is not 5V tolerant you could use a Logic Level Converter (I love you Sparkfun).
Now a few cool things about the USART. If hook the RX and TX signal on the AVR you can test to see if your AVR is transmitting/receiving properly.
Using the MAX232 in order to convert from the TTL levels to RS232 levels in order to communicate with your PC. Or an FTDI FT232RL Serial to USB chip in order to use Serial over USB (more and more common now that PCs do not come with serial ports. The Arduino has one built in).
THEORY OF OPERATION:
The USART outputs serial data over the TX (transmit) pin and listens for data on the RX (Receive) pin using TTL voltage levels (0-5V). The protocol is fairly simple, if the settings on both devices match they should be able to talk to each other.
The AVR datasheet gives a good writeup of the protocol, so I'm not going to go into any detail about it. I however say that the most common setting is:
Baud rate: 9600
Data bits: 8
Parity: None
Stop Bit: 1
Flow Control: None
The difference between the registers on the ATmega8 and the ATmega168/328 is mostly a "0" at the end of most register names. I'm guessing that this is preparations for AVRs with multiple USART ports. In this section I am going to change up for format a bit and show both registers under the same table.
7 bit | 6 bit | 5 bit | 4 bit | 3 bit | 2 bit | 1 bit | 0 bit | AVR Type | |
---|---|---|---|---|---|---|---|---|---|
UCSRA | RXC | TXC | UDRE | FE | DOR | PE | U2X | MPCM | ATmega8 |
UCSR0A | RXCn | TXCn | UDREn | FEn | DORn | PEn | U2Xn | MPCMn | ATmega168/328 |
USART Control and Status Register A
7 bit | 6 bit | 5 bit | 4 bit | 3 bit | 2 bit | 1 bit | 0 bit | AVR Type | |
---|---|---|---|---|---|---|---|---|---|
UCSRB | RXCIE | TXCIE | UDRIE | RXEN | TXEN | UCSZ2 | RXB8 | TXB8 | ATmega8 |
UCSR0B | RXCIE0 | TXCIE0 | UDRIE0 | RXEN0 | TXEN0 | UCSZ02 | RXB80 | TXB80 | ATmega168/328 |
USART Control and Status Register B
7 bit | 6 bit | 5 bit | 4 bit | 3 bit | 2 bit | 1 bit | 0 bit | AVR Type | |
---|---|---|---|---|---|---|---|---|---|
UCSRC | URSEL* | UMSEL | UPM1 | UPM0 | USBS | UCSZ1 | UCSZ0 | UCPOL | ATmega8 |
UCSR0C | UMSELn1 | UMSELn0 | UPMn1 | UPMn0 | USBSn | UCSZn1 | UCSZn0 | UCPOLn | ATmega168/328 |
USART Control and Status Register C (* ATmega8 only)
UMSEL | MODE |
---|---|
0 | Asynchronous Operation |
1 | Synchronous Operation |
UMSEL Bit Settings (ATmega8 only)
UMSELn | UMSELn0 | MODE |
---|---|---|
0 | 0 | Asynchronous Operation |
0 | 1 | Synchronous Operation |
1 | 0 | Reserved |
1 | 1 | Master SPI Mode |
UMSEL Bits Settings (ATmega168/328 Only)
UPM1 (UPMn1) |
UPM0 (UPM00) |
PARITY MODE |
---|---|---|
0 | 0 | Disabled |
0 | 1 | Reserved |
1 | 0 | Enabled, Even Parity |
1 | 1 | Enabled, Odd Parity |
UPM (UPMn) Bit Settings
USBS (USBS0) | Stop Bit(s) |
---|---|
0 | 1 bit |
1 | 2 bit |
USBS (USBS0) Bit Settings
UCSZ2 (UCSZ02) | UCSZ1 (UCSZ01) | UCSZ0 (UCSZ00) | CHAR SIZE |
---|---|---|---|
0 | 0 | 0 | 5-bit |
0 | 0 | 1 | 6-bit |
1 | 0 | 0 | 7-bit |
0 | 1 | 1 | 8-bit |
1 | 0 | 0 | Reserved |
1 | 0 | 1 | Reserved |
1 | 1 | 0 | Reserved |
1 | 1 | 1 | 9-bit |
UCSZ (UCSZn) Bit Settings
UCPOL (UCPOLn) | TX Data Change | RX Data Sampled |
---|---|---|
0 | Rising XCK Edge | Falling XCK Edge |
1 | Falling XCK Edge | Rising XCK Edge |
UCPOL (UCPOL0) Bit Settings
NAME | FUNCTION | SIZE | AVR TYPE |
---|---|---|---|
UDR | Transmit / Receive Buffer | 8 bit | ATmega8 |
UDRn | Transmit / Receive Buffer | 8 bit | ATmega328 |
UBRRH | Baud Rate Register upper 4 bits | 8 bit (3:0 used) | ATmega8 |
UBRRnH | Baud Rate Register upper 4 bits | 8 bit (3:0 used) | ATmega328 |
UBRRL | Baud Rate Register Lower 8 bits | 8 bit | ATmega8 |
UBRRRnL | Baud Rate Register Lower 8 bits | 8 bit | ATmega328 |
Other Registers
So we have 3 Control and Status Registers (A, B & C). Register A mainly contains status data. Register B contains all interrupt settings and some none optional settings. Register C contains all the configuration settings.
Transmitting Speed:
For whatever reason the USART circuitry within the AVR really likes clock frequencies divisible by 1.8432MHz. If your clock source is not divisible by 1.8432 you will have a percentage of error which will result in unstable communications. The datasheet provides a large table called "Examples of Baud Rate Setting" under the "USART" section which shows you the Error rate for all baud rates at given System Clock Settings, As long as the value is less then 0.5% your communication should be solid. The chart also shows you the value that you want to write into the UBRR register.
Most programmers choose to use a a formula in code to figure this out, this way they don't have to reference the datasheet every time they need to change the speed. The formula for this is:
ubrr_value = (Clock_Speed[a.k.a. FOSC] / 16 / Baud_Rate) - 1
Just remember this value can be up to 12 bits in size, and has to be written across 2 different 8 bit registers, so make sure you make ubrr_value an integer. Then we need to split the register the value into the upper and lowest 8 bits into the UBRRL(UBRRnL) register and the highest 4 bits into the UBRRH (UBRRnH) register.
Special Note for ATmega8:
The C register (UCRSC) on the ATmega8 contains URSEL (USART Select) bit. In order to write data to the UBRRH register the URSEL bit needs to be LOW(0), and in order to write to write to register C (UCRSC) you need to set this bit (URSEL) to HIGH(1). This does not exist on the ATmega168/328.
Frame Formats:
Now that we know how fast to send the data, we have to figure out how to format our data so that the other device could understand the data that we are sending it (and so that we could understand the data that is being sent to us). All of our configuration options are located in Register C (UCRSC or UCSRnC).
The first thing we want to do is setup the character size which is controlled by the UCSZ1(UCSZn1) bit and UCSZ0(UCSZn0) bit which are located in Register C, and the oddball UCSZ2(UCSZn2) in register B (which is used for 9 bit data, this rare).
The next thing we want to setup is the parity which is set by the UPM1 (UPMn1) bit and the UPM0 (UPMn0) bit, the most common parity setting is NONE so we can simply leave the bits LOW (0).
Lastly the stop bit which is controlled by the USBS(USBS0) bit, again, 2 bit stop is rarely used so we usually leave this bit LOW(0) for 1bit.
Enabling Communication:
Now this is something really cool, you can enable the transmission and receive functionality independently. Both the RXEN(RXENn) bit (Receive Enable) and the TXEN(TXENn) bit (Transmitter Enable) are both located in the B Register (UCSRB or UCSRnB)
Transmit and Receive Buffer:
This is something that blows my mind, the Transmit and Receive share the same register the UDR(UDRn). Whats even more wild is that the Receive buffer can store 2 characters. While this sound a bit crazy it does work. We have to keep 2 things in mind in order to get flawless operation. The buffer has to be clear in order to transmit. So all we really have to do is check to see if the buffer is clear UDR(UDRn) and if it is we can transmit. If it is not clear, reading the data will will cause it clear (much like reading the ADC clears it).
Status and Fault Bits:
Now that we have our communication up and running we need to be able to monitor its status and any faults. This is done by Register A (UCSRA or UCSRnA)
The RXC(RXCn) bit (Receive Complete) will be set to HIGH(1) when data has been received and is available in the UDR (UDR0) buffer.
The TXC(TXCn) bit (Transmission Complete) is set to HIGH(1) when a transmission is completed and there is no other data to send.
The UDRE(UDREn) is set to HIGH(1) when the buffer (UDR/UDRn) is empty, and therefore ready to be written.
The FE(FE0) bit (Frame Error) is set HIGH(1) if the next bit in the receive buffer has a Frame Error. If the recieve buffer is read the FE(FE0) bit is cleared.
The DOR(DORn) bit (Data OverRun) is set HIGH(1) if the receive buffer is full (2 characters). The bit is cleared LOW(0) if the UDR(UDRn) is read.
The UPE(UPEn) bit (Parity Error) is set HIGH(1) if the next character in the receive buffer has a parity error. The bit is cleared LOW(0) if the UDR(UDRn) is read.
Interrupts:
Finally, the interrupts. There are 3 of them. Receive Complete, Data Register Empty and, Transmit Complete. All 3 of the bits required to set these interrupts are located in the B register (UCSRB or UCSRnB). Setting the RXCIE(RXCIEn) bit (Receive Complete Interrupt Enable) to HIGH(1) enables the Receive interrupt. Setting the TXCIE(TXCIEn) bit (Transmit Complete Interrupt Enable) to HIGH(1) enables the Transmit interrupt. Setting the UDRIE(UDRIEn) bit (USART Data Register Empty Interrupt Enable) enables the USART Data Register Empty Interrupt.
IMPORTANT: If you enable the receiver interrupt you MUST READ the UDR register in order to clear the interrupt flag. If you fail to read the UDR register in the interrupt routine the interrupt flag will not be cleared. This will cause the program to will jump back to the main routine, execute 1 line of code (Remember this back from Interrupt Tutorial) then jump back into the interrupt routine.
SOFTWARE:
Let's put some code down to all this theory.
ATmega8 Code:
// This code waits for a character and transmits the character back (No interrupts) #include <avr/io.h> #include <stdint.h> // needed for uint8_t #define FOSC 16000000 // Clock Speed #define BAUD 9600 #define MYUBRR FOSC/16/BAUD -1 char ReceivedChar; int main( void ) { /* Set baud rate */ UBRRH = (MYUBRR >> 8); UBRRL = MYUBRR; UCSRB |= (1 << RXEN) | (1 << TXEN); // Enable receiver and transmitter UCSRC |= (1 << URSEL) |(1 << UCSZ1) | (1 << UCSZ0); // Set frame: 8data, 1 stp while(1) { while ( !(UCSRA & (1 << RXC)) ) // Wait until data is received ReceivedChar = UDR; // Read the data from the RX buffer while ( !(UCSRA & (1 << UDRE)) ) // Wait until buffer is empty UDR = ReceivedChar; // Send the data to the TX buffer } }
ATmega168/328 Code:
// This code waits for a character and transmits the character back (No interrupts) #include <avr/io.h> #include <stdint.h> // needed for uint8_t #define FOSC 16000000 // Clock Speed #define BAUD 9600 #define MYUBRR FOSC/16/BAUD -1 char ReceivedChar; int main( void ) { /* Set baud rate */ UBRR0H = (MYUBRR >> 8); UBRR0L = MYUBRR; UCSR0B |= (1 << RXEN0) | (1 << TXEN0); // Enable receiver and transmitter UCSR0C |= (1 << UCSZ01) | (1 << UCSZ00); // Set frame: 8data, 1 stp while(1) { while ( !(UCSR0A & (1 << RXC0)) ) // Wait until data is received ReceivedChar = UDR0; // Read the data from the RX buffer while ( !(UCSR0A & (1 << UDRE0)) ) // Wait until buffer is empty UDR0 = ReceivedChar; // Send the data to the TX buffer } }
Now let's do the same but this time use interrupts.
ATmega8 Code:
// This code waits for a character and transmits the character back (with interrupts) #include <avr/io.h> #include <stdint.h> // needed for uint8_t #include <avr/interrupt.h> #define FOSC 16000000 // Clock Speed #define BAUD 9600 #define MYUBRR FOSC/16/BAUD -1 volatile char ReceivedChar; int main( void ) { /* Set baud rate */ UBRRH = (MYUBRR >> 8); UBRRL = MYUBRR; UCSRB |= (1 << RXEN) | (1 << TXEN); // Enable receiver and transmitter UCSRB |= (1 << RXCIE); // Enable the receiver interrupt UCSRC |= (1 << URSEL) |(1 << UCSZ1) | (1 << UCSZ0); // Set frame: 8data, 1 stp sei(); while(1) { ; // main loop } } ISR (USART_RXC_vect) { ReceivedChar = UDR; // Read data from the RX buffer UDR = ReceivedChar; // Write the data to the TX buffer }
ATmega168/328 Code:
// This code waits for a character and transmits the character back (with interrupts) #include <avr/io.h> #include <stdint.h> // needed for uint8_t #include <avr/interrupt.h> #define FOSC 16000000 // Clock Speed #define BAUD 9600 #define MYUBRR FOSC/16/BAUD -1 volatile char ReceivedChar; int main( void ) { /*Set baud rate */ UBRR0H = (MYUBRR >> 8); UBRR0L = MYUBRR; UCSR0B |= (1 << RXEN0) | (1 << TXEN0); // Enable receiver and transmitter UCSR0B |= (1 << RXCIE0); // Enable reciever interrupt UCSR0C |= (1 << UCSZ01) | (1 << UCSZ00); // Set frame: 8data, 1 stp sei(); // Lets not forget to enable interrupts =P while(1) { ; // Main loop } } ISR (USART_RX_vect) { ReceivedChar = UDR0; // Read data from the RX buffer UDR0 = ReceivedChar; // Write the data to the TX buffer }
There you go guys, the secrets of the USART are now yours. If you enslave mankind with this knowledge pay me back by putting me to work in the easiest pits.
Cheers
Q