A microcontroller working alone is the exception. Almost every embedded system is made up of several circuits that must exchange information with one another, and the two ends of a link are always independent: different clocks, different power supplies, no shared memory. Information must therefore be encoded as a voltage variation on a wire, transmitted over time, then decoded at the other end. This lecture answers two questions: how bits are encoded in time, and how the receiver knows where each bit begins and ends, without any clock wire telling it so.
1Scope and structure of the lecture6 min
We will trace the underlying logic of asynchronous serial communication: why it replaced parallel communication almost everywhere, what a UART frame looks like, how a receiver with no clock wire manages to find the middle of every bit, what practically limits the baud rate, and how all of this theory translates into the concrete registers of the ATmega328P USART module.
- An interrupt routine must be short and communicate through volatile variables (Lecture 05)
- The prescaling formulas - frequency, divider, compare value - return unchanged, now applied to the baud rate divider (Lecture 06)
- Explain why serial communication replaced parallel, and what synchronous means as opposed to asynchronous
- Describe the structure of a UART frame and the role of each bit
- Explain the 16x oversampling mechanism and why the last bit of the frame is the most vulnerable to clock error
- Calculate the UBRR register value and the resulting error for a given baud rate
- Write UART initialization, transmission and reception, both by polling and by interrupt with a circular buffer
- Distinguish TTL, RS-232 and RS-485 by electrical levels and range of application
2Communication systems and types12 min
Every data exchange requires a mechanism by which information in one circuit's register reaches, unaltered, a register in another circuit - a communication system. The need arises from interfacing with external peripherals (sensors, displays), from communication between several microcontrollers, from linking to a computer, and from integration into IoT networks.
Serial versus parallel
In parallel communication, eight bits travel simultaneously on eight wires; in serial, one after another, on a single wire. Intuition says parallel should be eight times faster - reality went exactly the opposite way.
Synchronous versus asynchronous
Synchronous: the transmitter sends, on a separate wire, a clock signal that marks each bit - perfect synchronization by construction (SPI, I2C). Asynchronous: there is no clock wire - the receiver deduces the sampling instants on its own, with its own oscillator, starting from a reference transmitted right in the stream (the transition that marks the start of the frame). The cost: the two ends must have agreed on the speed beforehand, and the oscillators must be close enough in frequency. The gain: one fewer wire, and complete independence between the two devices.
Simplex, half duplex, full duplex
Simplex: a single direction, permanently. Half duplex: both directions, but not at the same time - the two ends share the same medium, in turn. Full duplex: simultaneously in both directions, through separate wires or channels. UART, with separate RxD and TxD, is full duplex.
Point to point versus bus
Point to point: exactly two ends, no addressing - everything sent is meant for the other end. A bus (multipoint) links several devices on the same wires and requires addressing plus arbitration. UART, in its basic form, is point to point; RS-485 (final section) extends it to a bus of up to 32 devices.
3The UART module and frame format13 min
The UART (Universal Asynchronous Receiver Transmitter) translates bidirectionally between the parallel world inside the microcontroller and the serial world on the wire. The TxD line is the output, RxD is the input; when connecting two devices, the two lines are crossed (TxD to RxD), plus a mandatory shared ground connection. On the ATmega328P: PD0 (RxD), PD1 (TxD).
Nothing is transmitted and nothing burns out - the error is hard to spot precisely because it produces no immediately visible symptom, just silence on both channels.
Four parameters must be identical at both ends: the number of data bits (usually 8, LSB first), the parity bit (none, even or odd), the baud rate (9600, 19200, 38400, 57600, 115200 being the usual values) and the number of stop bits (1 or 2). The notation 8N1 (eight data bits, no parity, one stop bit) is the default configuration of almost every terminal.
The frame format
The encoding is NRZ (Non Return to Zero): each bit is a constant level held for its entire duration, with no return to idle between bits - consequence: a long run of identical bits produces no transition at all, so the receiver cannot recover the clock from the signal and the rate must be agreed beforehand. At idle, the line stays at logic 1.
Data bits (5-9, LSB first).
Parity bit (optional).
Stop bit(s) (always 1): guarantees the line returns to idle before the next frame, so the next start edge is detectable.
The parity bit detects an odd number of bit errors, but does not correct them and does not detect an even number of errors - on short links it is usually disabled; on long cables, a checksum over the whole message is preferable anyway.
4Synchronization between transmitter and receiver12 min
The edges of a bit are the least reliable zone - that is where the transition happens, with rise time, possible ringing and jitter. At the middle of the bit, the level is stable and as far as possible from both neighboring transitions. The receiver therefore samples at the middle of the bit, not at the edge.
16x oversampling
The receiver runs on a clock 16 times faster than the baud rate, sampling the RxD line 16 times during each bit. On a falling edge from idle, it assumes the start of a start bit and starts a counter; samples 8, 9, 10 land, by construction, around the middle of the bit - the receiver reads all three and decides by majority vote. If the majority confirms 0, the start bit is validated; otherwise the edge was a spurious pulse, and the receiver goes back to waiting. The same procedure repeats, with no transition required, for every following bit.
16×115200 = 1.8432 MHz - the
oscillator must be able to supply this frequency through integer division, which is the source of
the main practical limitation (next section).Why clock error accumulates
The receiver resynchronizes only once per frame, at the start edge. If its clock has a relative
error e compared to the transmitter, the interval considered one bit is T_b(1+e) -
the error does not cancel out, it adds up: after n bits, the drift is n·e·T_b.
In an 8N1 frame, the middle of the stop bit falls at 9.5 bit durations after the start edge. For
the sample to still land inside the bit, the accumulated drift must stay under half a bit:
9.5·|e| < 0.5 ⇒ |e| < 5.3% - the ideal geometric limit. In practice, the
uncertainty in detecting the start edge (1/16 of a bit) and the fact that all three samples 8, 9,
10 must land inside the bit narrow the real margin to roughly ±4.6% - split between the two
ends, the practical rule is that each end's error should not exceed ±2.5%. The symptom of an
excessive clock error, on a serial terminal: an apparently random stream of characters instead of
the expected text.
5Calculating the baud rate13 min
The baud rate is obtained by integer division of the oscillator frequency, through a programmable counter - the UBRR register (USART Baud Rate Register).
UBRR = f_osc/(16·B) - 1, where B is the desired rate.Actual rate:
B_ef = f_osc / (16·(UBRR+1)).Relative error:
ε = (B_ef/B - 1)×100%.The result of the first formula is rarely an integer - rounding introduces an error. The smaller the divider (a high requested rate relative to the oscillator frequency), the larger a fraction of the divider one rounding unit represents - which is why high rates are much harder to generate exactly.
See the solution
9600 baud: UBRR = 16×10⁶/(16×9600) - 1 = 104.17-1 = 103.17 → 103.
B_ef = 16×10⁶/(16×104) = 9615.4. ε = +0.16% - completely negligible.
115200 baud: UBRR = 16×10⁶/(16×115200) - 1 = 8.68-1 = 7.68 → 8.
B_ef = 16×10⁶/(16×9) = 111 111. ε = -3.55% - above the practical limit of
±2.5%; if the other end also has an error of the same sign, reception becomes unreliable.
Double speed mode
The U2X0 bit in UCSR0A lowers the oversampling factor to 8: UBRR = f_osc/(8·B) -
1. The divider doubles, and rounding has a smaller relative effect. At 115200 baud, 16 MHz:
UBRR = 16.36 → 16, B_ef = 16×10⁶/(8×17) = 117 647, ε =
+2.1% - much better than -3.55%, exactly what the Arduino library does at 115200. The price:
with eight samples per bit instead of sixteen, the vote is taken on samples 4, 5, 6, the useful
window narrows, and noise immunity drops by roughly half.
14 745 600 = 115200×128 = 9600×1536 - an integer multiple of every standard rate. At
115200 baud: UBRR = 14 745 600/(16×115200) - 1 = 8-1 = 7, exactly an integer, zero
error. Arduino favors 16 MHz, more convenient for decimal timing, at the cost of the error discussed
above - a clear example of a design trade-off: no single frequency is simultaneously convenient for
timing and for exact standard rates.A program correct at 16 MHz, moved without changes to a board at 8 MHz, transmits at half the desired rate - at 8 MHz, 9600 baud requires UBRR=51, not 103. In portable code, the value is never written as a numeric constant, but computed at compile time, from the F_CPU macro.
6The ATmega328P USART module registers8 min
The USART0 module has three categories of registers: a data register, control and status registers, and the baud rate register.
A write to UDR0 loads the transmit buffer; a read returns the receive buffer - you cannot read back what was written. On reception, the buffer has two levels, forming a small FIFO.
| Register | Bit | Role |
|---|---|---|
| UCSR0A | RXC0 | reception complete (cleared by reading UDR0) |
| UCSR0A | UDRE0 | the transmit buffer is empty, ready for a new byte |
| UCSR0A | FE0 / DOR0 / UPE0 | frame error / overrun / parity error |
| UCSR0B | RXCIE0 / RXEN0 / TXEN0 | receive interrupt enable / receiver enable / transmitter enable |
| UCSR0C | UPM01:00 / USBS0 / UCSZ01:00 | parity / stop bits / frame size |
Three error flags, all referring to the byte at the head of the buffer: FE0 (frame error) - a 0 was read in the position of the stop bit, a typical sign of mismatched baud rate between ends; DOR0 (data overrun) - a new frame arrived complete while the buffer was still full, data lost through a software architecture error, not an electrical one; UPE0 (parity error) - the parity bit does not match, a sign of electrical noise or a differing configuration.
Reading the data register advances the buffer - if the error flags are read afterward, they already refer to the next byte, not the one just extracted.
7Application: transmitting and receiving a character11 min
Initialization requires three operations: the divider in UBRR0, enabling the transmitter/receiver in UCSR0B, the frame format in UCSR0C. UBRR0 is 12 bits wide, split into UBRR0H and UBRR0L - the upper part is written first.
#define F_CPU 16000000UL
#define BAUD 9600UL
#define UBRR_VAL ((F_CPU + 8UL*BAUD) / (16UL*BAUD) - 1UL) /* correct rounding */
void usart_init(void) {
UBRR0H = (uint8_t)(UBRR_VAL >> 8);
UBRR0L = (uint8_t)(UBRR_VAL & 0xFF);
UCSR0B = (1<<RXEN0) | (1<<TXEN0);
UCSR0C = (1<<UCSZ01) | (1<<UCSZ00); /* 8N1 */
}
Transmission and reception by polling
void usart_transmite(uint8_t data) {
while (!(UCSR0A & (1<<UDRE0))) { ; } /* wait for the transmit buffer to empty */
UDR0 = data;
}
uint8_t usart_date_disponibile(void) {
return (UCSR0A & (1<<RXC0)) != 0;
}
On transmit, the wait is bounded by the duration of one frame (about 1 ms at 9600 baud). On
reception, if the other end sends nothing, a blocking function stalls the entire program for an
unbounded time. The correct approach: test only availability
(usart_date_disponibile()) and read only when data exists, with the status register
read before UDR0 so the error flags refer to the byte about to be extracted.
The equivalent using the Arduino Serial class
void setup() { Serial.begin(9600); }
void loop() {
Serial.write('G');
if (Serial.available() > 0) { char c = Serial.read(); Serial.write(c); }
delay(500);
}
Serial.available() does not read RXC0, but the number of bytes in a 64-byte circular
buffer maintained by the receive interrupt routine. Serial.write() places the byte into
a software buffer and returns immediately - transmission happens in the background, through
interrupts. This is exactly the architecture discussed in the next section.
8Interrupt-driven reception and the circular buffer11 min
Polling is simple, but has a structural flaw: the program must pass through the check point more often than characters arrive. At 115200 baud, a frame lasts about 87 µs, i.e. about 1400 cycles at 16 MHz between two characters - any slower function (a blocking ADC conversion, a delay()) lets the second character that arrives in the meantime overwrite the hardware buffer, raising DOR0, and the data is lost without the program noticing, unless it explicitly tests the flag.
head (where writes happen) and tail (where reads happen), each advancing
cyclically. Empty when the two indices are equal, full when advancing head would equal tail. No data
shifting, no dynamic allocation, constant time - essential inside an interrupt routine.#define BUF_SIZE 64 /* must be a power of two */
#define BUF_MASK (BUF_SIZE - 1)
static volatile uint8_t buf[BUF_SIZE];
static volatile uint8_t head = 0, tail = 0;
ISR(USART_RX_vect) {
uint8_t status = UCSR0A; /* status BEFORE reading the data */
uint8_t data = UDR0;
if (status & ((1<<FE0)|(1<<DOR0)|(1<<UPE0))) return; /* bad frame, discard */
uint8_t next_head = (head + 1) & BUF_MASK;
if (next_head != tail) { buf[head] = data; head = next_head; }
/* otherwise the buffer is full, the byte is dropped in a controlled way */
}
uint8_t buffer_read(void) {
uint8_t data = buf[tail];
tail = (tail + 1) & BUF_MASK;
return data;
}
Enabling it only requires UCSR0B |= (1<<RXCIE0) plus sei().
head, the main program writes only tail - makes the writes atomic on
single-byte variables, with no need for a critical section (if the buffer were to exceed 256
positions, the indices would become 16-bit and access from the main loop would need protection). When
full, the code checks for room before writing and drops the byte in a controlled way - the
alternative, overwriting the oldest byte, would break message ordering.It does not parse the message, does not print anything, does not call timing functions, does not transmit. Any processing added into the routine reduces the available margin and brings back, by another route, exactly the problem the interrupt was meant to solve.
9From UART to USART5 min
USART (Universal Synchronous and Asynchronous Receiver Transmitter) adds, on top of the asynchronous mode described so far, a synchronous mode, with an extra clock line (XCK, on PD4 on the ATmega328P). One partner generates the clock (master), the other receives it (slave); bits are validated by its edges, and the receiver no longer needs oversampling, majority voting, or a precise clock of its own.
10The RS-232 and RS-485 standards10 min
UART says everything about the logic of transmission, but nothing about its electrical side - what voltage represents a logic 1, how long the cable can be. This is the subject of the RS-232 and RS-485 standards.
TTL levels are not RS-232
ATmega328P pins work with TTL/CMOS levels: about 0 V for 0, the supply voltage for 1. RS-232 uses negative voltages for logic 1 (mark, -3 to -15 V) and positive for 0 (space, +3 to +15 V) - a dead zone between -3 and +3 V gives noise immunity.
Two independent reasons, each sufficient on its own: the signal would be inverted (RS-232 polarity is opposite to the TTL convention), and the -12 V voltage applied to a microcontroller pin will almost certainly destroy it. A level converter (MAX232) performs the translation in both directions, generating the negative voltages itself from a single 5 V supply, through a charge pump.
RS-485 and the differential signal
RS-232 solves the amplitude problem, but remains unbalanced (referenced to ground) - over long
distances, the potential difference between grounds and induced electromagnetic interference add
directly to the signal. RS-485 eliminates both problems: information is carried by the voltage
difference between two twisted wires, A and B, driven in antiphase. The receiver reads only
U_A - U_B.
| RS-232 | RS-485 | |
|---|---|---|
| Topology | point to point | multipoint |
| Signal type | unbalanced | differential, twisted pair |
| Maximum distance | about 15 m | about 1200 m |
| Devices | 2 | up to 32 |
| Mode of operation | full duplex | half duplex (2 wires) |
The cable is a transmission line with a characteristic impedance (about 120 Ω); an edge reaching an unmatched end reflects and overlaps the signal, producing false transitions. The remedy: one 120 Ω resistor at each of the two physical ends of the bus - not at intermediate devices. An unterminated bus can appear to work fine at low speed and fail abruptly as speed increases, once the edge duration becomes comparable to the round-trip propagation time.
Important: both standards describe only the physical layer - the frame format remains that of UART (start, data, parity, stop). A microcontroller transmitting over RS-485 uses exactly the same USART module; only the interface circuit changes, plus one extra pin that controls enabling the transmitter.
11Common mistakes4 min
- I connected TxD to TxD - if it does not work, at least I did not burn anything. True that nothing burns out, but the error often goes unnoticed for a long time, precisely because the absence of any obvious symptom does not direct attention to the wiring. TxD is always connected to RxD at the other end, never to its own counterpart.
- I read UDR0 and only then checked the error bit - but I never find a frame error. A structural false negative - reading UDR0 advances the buffer, so flags read afterward already refer to the next byte, not the one just extracted. UCSR0A is always read before UDR0.
- Polling is enough for any application with UART. False at high rates or with slow main loops - any function that takes longer than the interval between two frames (87 µs at 115200 baud) causes data to be lost through overrun of the hardware buffer, with no warning if the DOR0 flag is not explicitly tested. For fast streams, use a short interrupt routine plus a circular buffer, separating arrival from processing.
12Summary and glossary5 min
Asynchronous serial communication solves the transfer of information between independent circuits with a minimum of hardware resources, giving up the clock wire in exchange for a prior agreement on speed and a resynchronization on every frame, at the start bit. The receiver oversamples 16 times per bit and decides by majority vote around the middle of each bit, tolerating a clock error of a few percent, provided it does not accumulate past half a bit by the last bit of the frame - the most vulnerable one. The integer divider imposed by the UBRR register makes some combinations of frequency and rate exact, others not; hence the 14.7456 MHz crystals and double speed mode. At the programming level, the USART module reduces to a data register, three control and status registers, and a divider register - reception by polling is sufficient for simple programs, but the correct architecture for fast streams combines a short interrupt with a circular buffer. Protocol and electrical level are distinct things: the same frames travel at TTL levels over a few centimeters, through RS-232 over fifteen meters, or through differential RS-485 over more than a kilometer.
13Review questions7 min
- Explain why serial communication replaced parallel communication in almost every modern application. What is propagation skew, and why does its effect worsen at high frequencies?
- What must the receiver in an asynchronous link know beforehand, and why can it not be deduced from the received signal, given NRZ encoding?
- Describe the 16x oversampling mechanism. Why are samples 8, 9, 10 used, and why by majority vote?
- A frame is 8E2. How many bit durations after the start edge does the middle of the last stop bit fall? Derive the maximum tolerated relative clock error, geometrically.
- An ATmega328P at 16 MHz must communicate at 57600 baud. Calculate UBRR0, the actual rate and the error, both normally and with U2X0 enabled.
- Distinguish the FE0, DOR0 and UPE0 flags. For each, state the likely cause.
- Argue why the size of a circular buffer is chosen as a power of two, and why the indices must be declared volatile.
14Further directions2 min
The next lecture stays within the serial communication family, but moves to synchronous protocols - SPI and I2C - where bringing the clock wire back into the picture eliminates the baud rate problem discussed here, in exchange for an extra wire and a different topology.
UART configuration, the UBRR calculation and the circular buffer from this lecture become real hardware in Laboratory 06.