Loading header...

Exercise 3 — UART Serial Communication

Every piece of embedded hardware you will ever debug will need to tell you what it is doing. UART (Universal Asynchronous Receiver-Transmitter) is the oldest, simplest, most universal communication interface in computing. It is on every microcontroller ever made. It is how Arduino's Serial.print() works. It is how a GPS module sends coordinates and how a GSM module accepts AT commands.

In this exercise you will configure UART from scratch — no libraries — and build a small command interpreter.

Prerequisites — What to Buy

This is the cheapest exercise in the series — if you already have an Arduino Uno and USB cable from Exercise 1, you need to buy nothing. The Arduino Uno has a dedicated USB-to-UART chip on board (usually a CH340G or ATmega16U2). When you plug the USB cable in, your PC sees it as a virtual serial port.

Components list

# Component Qty Approx. cost How to identify / what to ask for
1 Arduino Uno R3 1 ₹350–₹500 Carry over from Exercise 1. The USB-B port doubles as the programming port and the UART serial port.
2 USB cable (Type A to Type B) 1 ₹50–₹100 Carry over from Exercise 1. This is the same cable used to program the board — it also carries the serial data in this exercise.
3 Breadboard + jumper wires Carry over from Exercise 1. Not strictly required for this exercise but useful if you extend it.

Total new hardware cost: ₹0 if you completed Exercise 1.

Software you need (free)

Software Platform Purpose Where to get
Arduino IDE or AVR-GCC Windows / Linux / Mac Compile and upload the program arduino.cc/en/software
PuTTY Windows Serial terminal — type commands, see responses search "PuTTY download" — putty.org
screen or minicom Linux / Mac Serial terminal Pre-installed on Linux; brew install minicom on Mac
VS Code Serial Monitor (optional) Any More comfortable GUI serial terminal Extension: "Serial Monitor" by Microsoft

If you do NOT have an Arduino Uno

If you are working with a bare ATmega328P chip (not on a board), you need a separate USB-to-TTL serial adapter:

Component Cost What to ask for
CP2102 USB-to-UART module ₹80–₹150 "CP2102 USB serial module" — has TX, RX, GND, VCC pins. Also works as a 3.3 V / 5 V power supply for the MCU.
Alternatively: CH340G module ₹40–₹80 Same function, lower cost. Ask for "CH340 USB TTL serial converter".

Connect: PC-USB → adapter → TX to MCU RX, RX to MCU TX, GND to GND.

Where to buy

  • Online: The CH340G or CP2102 USB-TTL module is on Amazon India, Robu.in, Evelta.com for under ₹150. Search "CP2102 USB serial module 5V".
  • Local shop: Ask for "USB to TTL serial converter" or "USB serial adapter". Specify 5 V logic level (not 3.3 V only).

How UART Works — The Protocol

UART transmits one bit at a time on a single wire, at a fixed clock rate called the baud rate (bits per second). There is no shared clock wire — both sides agree on the rate beforehand.

sequenceDiagram participant TX as Transmitter (MCU TX pin) participant RX as Receiver (PC RX pin) Note over TX,RX: Line is IDLE — held HIGH (mark state) TX->>RX: Start bit — pull LOW for 1 bit period (1/9600 s = 104 µs) TX->>RX: Bit 0 (LSB) — 104 µs TX->>RX: Bit 1 — 104 µs TX->>RX: Bit 2 — 104 µs TX->>RX: Bit 3 — 104 µs TX->>RX: Bit 4 — 104 µs TX->>RX: Bit 5 — 104 µs TX->>RX: Bit 6 — 104 µs TX->>RX: Bit 7 (MSB) — 104 µs TX->>RX: Stop bit — pull HIGH for 1 bit period Note over TX,RX: Line returns to IDLE (HIGH) Note over TX,RX: Total frame = 10 bit periods = 1.04 ms per byte at 9600 baud

The UART Frame

flowchart TD classDef start fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef data fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef stop fill:#dcfce7,stroke:#16a34a,color:#14532d classDef idle fill:#f1f5f9,stroke:#475569,color:#1e293b IDLE1["IDLE (line held HIGH)\nNo transmission in progress"]:::idle START["Start Bit — always LOW\nOne bit-period (104 µs at 9600 baud)\nTells receiver: 'a byte is coming'\nReceiver starts its bit-sampling timer here"]:::start subgraph DATABITS ["Data Byte — 8 bits, LSB first"] D0["D0 (LSB — least significant bit)"]:::data D1["D1"]:::data D2["D2"]:::data D3["D3"]:::data D4["D4"]:::data D5["D5"]:::data D6["D6"]:::data D7["D7 (MSB — most significant bit)"]:::data D0 --> D1 --> D2 --> D3 --> D4 --> D5 --> D6 --> D7 end STOP["Stop Bit — always HIGH\nOne bit-period minimum\nMarks end of frame\nLine returns to idle"]:::stop IDLE2["IDLE or immediately next Start Bit\nif more data follows"]:::idle IDLE1 --> START --> D0 D7 --> STOP --> IDLE2

Baud Rate

The baud rate is the number of symbols (bits) per second. Common values: 9600, 19200, 38400, 57600, 115200 bps.

At 9600 baud:

  • 1 bit = 1/9600 s ≈ 104 µs
  • 1 byte (10 bits with start+stop) = 1.04 ms
  • 1 KB of data takes ≈ 1 second

At 115200 baud (most common today):

  • 1 bit = 8.68 µs
  • 1 byte = 86.8 µs
  • 1 KB ≈ 88 ms

AVR UART Hardware

The ATmega328P has one hardware UART (USART0). It handles start/stop bit generation, shifting bits in and out, and buffering — completely in hardware.

flowchart TD classDef reg fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef hw fill:#dcfce7,stroke:#16a34a,color:#14532d classDef pin fill:#ffedd5,stroke:#ea580c,color:#9a3412 classDef cpu fill:#f3e8ff,stroke:#9333ea,color:#581c87 CPU["CPU"]:::cpu subgraph USART0 ["USART0 Hardware"] UDR0["UDR0\n(USART Data Register)\nWrite here to transmit\nRead here to receive\n(two separate physical registers,\nsame address)"]:::reg UBRR0["UBRR0H:UBRR0L\n(Baud Rate Register)\nUBRR = (F_CPU / (16 × baud)) − 1\nAt 16 MHz, 9600 baud:\nUBRR = 103\nAt 16 MHz, 115200 baud:\nUBRR = 8"]:::reg UCSR0A["UCSR0A\nStatus register\nTXC0: TX complete flag\nRXC0: RX complete flag\nUDRE0: TX buffer empty\n(ready for next byte)"]:::reg UCSR0B["UCSR0B\nControl register\nRXEN0: enable receiver\nTXEN0: enable transmitter\nRXCIE0: RX interrupt enable"]:::reg UCSR0C["UCSR0C\nFrame format\nUSBS0: stop bits (0=1, 1=2)\nUCSZ01:UCSZ00: char size (11 = 8-bit)"]:::reg TX_SHIFT["TX Shift Register\n(shifts bits onto TX pin\nat baud rate — hardware)"]:::hw RX_SHIFT["RX Shift Register\n(samples RX pin at baud rate\nassembles bits — hardware)"]:::hw end TX_PIN["PD1 (TX)\n→ to PC RX"]:::pin RX_PIN["PD0 (RX)\n← from PC TX"]:::pin CPU --> UDR0 UDR0 --> TX_SHIFT --> TX_PIN RX_PIN --> RX_SHIFT --> UDR0 UDR0 --> CPU

Baud Rate Calculation

UBRR = (F_CPU / (16 × BAUD)) − 1

At 16 MHz, 9600 baud:   UBRR = (16,000,000 / 153,600) − 1 = 103  (0.16% error)
At 16 MHz, 115200 baud: UBRR = (16,000,000 / 1,843,200) − 1 = 7.68 ≈ 8  (3.7% error — acceptable)

UART Initialisation and Transmit/Receive Functions

// UART driver for ATmega328P @ 16 MHz, 9600 baud
// 8 data bits, no parity, 1 stop bit (8N1) — the universal default

#include <avr/io.h>

#define BAUD     9600UL
#define UBRR_VAL ((F_CPU / (16UL * BAUD)) - 1)

void uart_init(void) {
    UBRR0H = (uint8_t)(UBRR_VAL >> 8);
    UBRR0L = (uint8_t)(UBRR_VAL);
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);          // enable RX and TX
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);        // 8-bit data, 1 stop bit
}

void uart_send_byte(uint8_t data) {
    while (!(UCSR0A & (1 << UDRE0)));               // wait until TX buffer empty
    UDR0 = data;
}

uint8_t uart_recv_byte(void) {
    while (!(UCSR0A & (1 << RXC0)));                // wait until byte received
    return UDR0;
}

void uart_send_string(const char *s) {
    while (*s) uart_send_byte(*s++);
}

Program Flow — The Command Interpreter

flowchart TD classDef init fill:#e0e7ff,stroke:#4338ca,color:#312e81 classDef recv fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef cmd fill:#dcfce7,stroke:#16a34a,color:#14532d classDef resp fill:#ffedd5,stroke:#ea580c,color:#9a3412 classDef err fill:#fee2e2,stroke:#dc2626,color:#7f1d1d INIT["uart_init()\nSend greeting:\n'AVR Command Shell'\n'> '"]:::init RECV["uart_recv_byte()\nread one character\necho it back"]:::recv IS_CR["Character = '\\r'\n(Enter key)?"]:::cmd PARSE["Parse the accumulated\ncommand buffer"]:::cmd CMD_ON["Command 'on'\n→ PORTB |= (1<<5)\n→ reply 'LED on'"]:::resp CMD_OFF["Command 'off'\n→ PORTB &= ~(1<<5)\n→ reply 'LED off'"]:::resp CMD_HI["Command 'hi'\n→ reply 'Hello!'"]:::resp CMD_VER["Command 'ver'\n→ reply 'v1.0 ATmega328P'"]:::resp CMD_UNK["Unknown command\n→ reply 'Unknown: ...'"]:::err PROMPT["Send '\\r\\n> '\nClear buffer"]:::init INIT --> RECV RECV -->|"not Enter"| RECV RECV -->|"Enter"| IS_CR IS_CR --> PARSE PARSE --> CMD_ON & CMD_OFF & CMD_HI & CMD_VER & CMD_UNK CMD_ON & CMD_OFF & CMD_HI & CMD_VER & CMD_UNK --> PROMPT --> RECV

Building the Program — Snippet by Snippet


Snippet 1 — Register Definitions

UART registers sit above 0x5F (extended I/O), so they need sts/lds. Name everything with .equ so the code is self-documenting.

; ── UART registers (extended I/O — use lds/sts) ─────────
.equ    UCSR0A, 0xC0
.equ    UCSR0B, 0xC1
.equ    UCSR0C, 0xC2
.equ    UBRR0L, 0xC4
.equ    UBRR0H, 0xC5
.equ    UDR0,   0xC6

; Bit positions
.equ    RXC0,   7       ; UCSR0A: 1 = RX data ready to read
.equ    UDRE0,  5       ; UCSR0A: 1 = TX buffer empty (ready to send)
.equ    RXEN0,  4       ; UCSR0B: enable receiver
.equ    TXEN0,  3       ; UCSR0B: enable transmitter

; ── GPIO ────────────────────────────────────────────────
.equ    DDRB,   0x04
.equ    PORTB,  0x05
.equ    LED,    5       ; PB5 = Arduino pin 13 (built-in LED)

Snippet 2 — UART Initialisation

Three things to configure: baud rate (via UBRR), enable RX+TX (UCSR0B), and frame format — 8 data bits, no parity, 1 stop bit (UCSR0C).

main:
    LDI     R16, HIGH(RAMEND)   ; stack pointer setup
    OUT     SPH, R16
    LDI     R16, LOW(RAMEND)
    OUT     SPL, R16

    ; Baud rate: 9600 at 16 MHz  →  UBRR = 103 = 0x0067
    LDI     R16, 0x00
    STS     UBRR0H, R16
    LDI     R16, 103
    STS     UBRR0L, R16

    ; Enable receiver AND transmitter
    LDI     R16, (1<<RXEN0)|(1<<TXEN0)
    STS     UCSR0B, R16

    ; 8-bit frame: UCSZ01=1, UCSZ00=1
    LDI     R16, 0x06
    STS     UCSR0C, R16

    SBI     DDRB, LED           ; LED pin as output

Formula: UBRR = (F_CPU / (16 × baud)) − 1 = (16,000,000 / 153,600) − 1 = 103. This is a fixed number — no calculation at runtime, just a constant loaded into UBRR.


Snippet 3 — Transmit and Receive Subroutines

These two are the foundation of everything else. Every time you send or receive, you poll a flag in UCSR0A first.

; ── uart_tx — send one byte ─────────────────────────────
; In: r24 = byte to send    Clobbers: r18
uart_tx:
    LDS     R18, UCSR0A
    SBRS    R18, UDRE0          ; skip if UDR0 is empty (ready)
    RJMP    uart_tx             ; loop until ready
    STS     UDR0, R24
    RET

; ── uart_rx — receive one byte ──────────────────────────
; Out: r24 = byte received    Clobbers: r18
uart_rx:
    LDS     R18, UCSR0A
    SBRS    R18, RXC0           ; skip if data waiting
    RJMP    uart_rx             ; loop until data arrives
    LDS     R24, UDR0
    RET

SBRS = Skip if Bit in Register is Set. The bit we check (UDRE0 for TX, RXC0 for RX) is inside UCSR0A, which we first load into a general-purpose register with LDS. After SBRS skips the RJMP, execution falls through to the actual send/receive.


Snippet 4 — Print a Flash String

Rather than pushing each character individually, we write a subroutine that walks through a null-terminated string in Flash using LPM Z+.

; ── print_str — print null-terminated string from Flash ─
; In: Z = byte address of string (label << 1)
print_str:
    LPM     R24, Z+             ; load byte from Flash, advance Z
    TST     R24                 ; is it the null terminator?
    BREQ    ps_done
    RCALL   uart_tx
    RJMP    print_str
ps_done:
    RET

Usage: load Z with LDI ZH, hi(msg<<1) / LDI ZL, lo(msg<<1), then RCALL print_str. Each call walks until it finds the 0x00 byte you put at the end of the string with .byte 0.


Snippet 5 — Receive a Line into SRAM Buffer

We collect incoming characters into a 32-byte buffer in SRAM. X register (R27:R26) serves as the write pointer. We act on the buffer when Enter (\r) arrives.

; X register = buffer pointer (r27:r26)
; R19 = character count (so we know if buffer is empty)

    LDI     XH, HIGH(cmd_buf)   ; point X at the buffer in SRAM
    LDI     XL, LOW(cmd_buf)
    LDI     R19, 0              ; character count = 0

rx_loop:
    RCALL   uart_rx             ; wait for and receive one byte → r24

    CPI     R24, '\r'
    BREQ    process_cmd         ; Enter pressed — process what we have

    CPI     R24, 8              ; backspace (ASCII 8)?
    BREQ    do_backspace
    CPI     R24, 127            ; DEL key also acts as backspace
    BREQ    do_backspace

    CPI     R19, 31             ; buffer full? (keep one slot for null)
    BRGE    rx_loop             ; yes — ignore extra characters

    ST      X+, R24             ; store character in buffer, advance X
    INC     R19                 ; count++
    RCALL   uart_tx             ; echo character back to terminal
    RJMP    rx_loop

do_backspace:
    TST     R19
    BREQ    rx_loop             ; nothing to delete
    LD      R24, -X             ; decrement X, load (but we discard the byte)
    DEC     R19
    LDI     R24, 8
    RCALL   uart_tx             ; send BS to move cursor left
    LDI     R24, ' '
    RCALL   uart_tx             ; overwrite character with space
    LDI     R24, 8
    RCALL   uart_tx             ; move cursor left again
    RJMP    rx_loop

ST X+, R24 stores R24 at the address in X then increments X. LD R24, -X pre-decrements X then loads — this is how we implement backspace without keeping a separate pointer.


Snippet 6 — The Command Interpreter

After Enter arrives, we compare the buffer against known commands. Assembly has no strcmp — we write a byte-by-byte comparison subroutine.

process_cmd:
    LDI     R24, '\r'           ; move terminal to new line
    RCALL   uart_tx
    LDI     R24, '\n'
    RCALL   uart_tx

    ; Null-terminate the buffer
    LDI     R24, 0
    ST      X, R24

    ; Compare against "on"
    LDI     ZH, HIGH(str_on<<1)
    LDI     ZL, LOW(str_on<<1)
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    RCALL   str_eq
    BRNE    try_off
    SBI     PORTB, LED
    LDI     ZH, HIGH(msg_on<<1)
    LDI     ZL, LOW(msg_on<<1)
    RCALL   print_str
    RJMP    new_prompt

try_off:
    LDI     ZH, HIGH(str_off<<1)
    LDI     ZL, LOW(str_off<<1)
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    RCALL   str_eq
    BRNE    try_hi
    CBI     PORTB, LED
    LDI     ZH, HIGH(msg_off<<1)
    LDI     ZL, LOW(msg_off<<1)
    RCALL   print_str
    RJMP    new_prompt

try_hi:
    ; ... (pattern repeats for each command)

new_prompt:
    LDI     ZH, HIGH(msg_prompt<<1)
    LDI     ZL, LOW(msg_prompt<<1)
    RCALL   print_str
    LDI     XH, HIGH(cmd_buf)   ; reset buffer pointer
    LDI     XL, LOW(cmd_buf)
    LDI     R19, 0
    RJMP    rx_loop

; ── str_eq — compare Flash string (Z) against SRAM buffer (X) ──
; Out: Z flag set if equal, clear if not    Clobbers: r20, r21, r24
str_eq:
    LPM     R20, Z+             ; byte from Flash
    LD      R21, X+             ; byte from SRAM
    CP      R20, R21
    BRNE    not_equal
    TST     R20                 ; both were null → strings equal
    BRNE    str_eq
    RET                         ; Z flag set (equal)
not_equal:
    LDI     R24, 1              ; force Z flag clear
    TST     R24
    RET

Complete Program

; ═══════════════════════════════════════════════════════
;  uart_shell.S  —  UART Command Interpreter
;  ATmega328P @ 16 MHz, 9600 8N1
;  Commands: on, off, hi, ver
;  LED on PB5 (Arduino pin 13)
; ═══════════════════════════════════════════════════════

.equ    UCSR0A, 0xC0
.equ    UCSR0B, 0xC1
.equ    UCSR0C, 0xC2
.equ    UBRR0L, 0xC4
.equ    UBRR0H, 0xC5
.equ    UDR0,   0xC6
.equ    RXC0,   7
.equ    UDRE0,  5
.equ    RXEN0,  4
.equ    TXEN0,  3
.equ    DDRB,   0x04
.equ    PORTB,  0x05
.equ    LED,    5

.org 0x0000
    RJMP    main

.org 0x0034
main:
    LDI     R16, HIGH(RAMEND)
    OUT     SPH, R16
    LDI     R16, LOW(RAMEND)
    OUT     SPL, R16

    CLR     R16
    STS     UBRR0H, R16
    LDI     R16, 103
    STS     UBRR0L, R16
    LDI     R16, (1<<RXEN0)|(1<<TXEN0)
    STS     UCSR0B, R16
    LDI     R16, 0x06
    STS     UCSR0C, R16

    SBI     DDRB, LED

    LDI     ZH, HIGH(msg_banner<<1)
    LDI     ZL, LOW(msg_banner<<1)
    RCALL   print_str

    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    LDI     R19, 0

rx_loop:
    RCALL   uart_rx

    CPI     R24, '\r'
    BREQ    process_cmd
    CPI     R24, 8
    BREQ    do_bs
    CPI     R24, 127
    BREQ    do_bs
    CPI     R19, 31
    BRGE    rx_loop
    ST      X+, R24
    INC     R19
    RCALL   uart_tx
    RJMP    rx_loop

do_bs:
    TST     R19
    BREQ    rx_loop
    LD      R24, -X
    DEC     R19
    LDI     R24, 8
    RCALL   uart_tx
    LDI     R24, ' '
    RCALL   uart_tx
    LDI     R24, 8
    RCALL   uart_tx
    RJMP    rx_loop

process_cmd:
    LDI     R24, '\r'
    RCALL   uart_tx
    LDI     R24, '\n'
    RCALL   uart_tx
    LDI     R24, 0
    ST      X, R24

    ; "on"
    LDI     ZH, HIGH(str_on<<1)
    LDI     ZL, LOW(str_on<<1)
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    RCALL   str_eq
    BRNE    try_off
    SBI     PORTB, LED
    LDI     ZH, HIGH(msg_on<<1)
    LDI     ZL, LOW(msg_on<<1)
    RCALL   print_str
    RJMP    new_prompt

try_off:
    LDI     ZH, HIGH(str_off<<1)
    LDI     ZL, LOW(str_off<<1)
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    RCALL   str_eq
    BRNE    try_hi
    CBI     PORTB, LED
    LDI     ZH, HIGH(msg_off<<1)
    LDI     ZL, LOW(msg_off<<1)
    RCALL   print_str
    RJMP    new_prompt

try_hi:
    LDI     ZH, HIGH(str_hi<<1)
    LDI     ZL, LOW(str_hi<<1)
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    RCALL   str_eq
    BRNE    try_ver
    LDI     ZH, HIGH(msg_hi<<1)
    LDI     ZL, LOW(msg_hi<<1)
    RCALL   print_str
    RJMP    new_prompt

try_ver:
    LDI     ZH, HIGH(str_ver<<1)
    LDI     ZL, LOW(str_ver<<1)
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    RCALL   str_eq
    BRNE    unknown
    LDI     ZH, HIGH(msg_ver<<1)
    LDI     ZL, LOW(msg_ver<<1)
    RCALL   print_str
    RJMP    new_prompt

unknown:
    TST     R19
    BREQ    new_prompt
    LDI     ZH, HIGH(msg_unknown<<1)
    LDI     ZL, LOW(msg_unknown<<1)
    RCALL   print_str

new_prompt:
    LDI     ZH, HIGH(msg_prompt<<1)
    LDI     ZL, LOW(msg_prompt<<1)
    RCALL   print_str
    LDI     XH, HIGH(cmd_buf)
    LDI     XL, LOW(cmd_buf)
    LDI     R19, 0
    RJMP    rx_loop

; ── uart_tx ─────────────────────────────────────────────
uart_tx:
    LDS     R18, UCSR0A
    SBRS    R18, UDRE0
    RJMP    uart_tx
    STS     UDR0, R24
    RET

; ── uart_rx ─────────────────────────────────────────────
uart_rx:
    LDS     R18, UCSR0A
    SBRS    R18, RXC0
    RJMP    uart_rx
    LDS     R24, UDR0
    RET

; ── print_str ───────────────────────────────────────────
print_str:
    LPM     R24, Z+
    TST     R24
    BREQ    ps_done
    RCALL   uart_tx
    RJMP    print_str
ps_done:
    RET

; ── str_eq: compare Flash (Z) vs SRAM (X) ───────────────
; Equal → Z flag set    Not equal → Z flag clear
str_eq:
    LPM     R20, Z+
    LD      R21, X+
    CP      R20, R21
    BRNE    se_no
    TST     R20
    BRNE    str_eq
    RET
se_no:
    LDI     R20, 1
    TST     R20
    RET

; ── Flash strings ───────────────────────────────────────
msg_banner:
    .ascii  "\r\n================================\r\n"
    .ascii  "  AVR Command Shell  v1.0\r\n"
    .ascii  "  Commands: on, off, hi, ver\r\n"
    .ascii  "================================\r\n> "
    .byte   0

str_on:   .ascii "on"  .byte 0
str_off:  .ascii "off" .byte 0
str_hi:   .ascii "hi"  .byte 0
str_ver:  .ascii "ver" .byte 0

msg_on:      .ascii "LED is ON\r\n"  .byte 0
msg_off:     .ascii "LED is OFF\r\n" .byte 0
msg_hi:      .ascii "Hello from ATmega328P!\r\n" .byte 0
msg_ver:     .ascii "Firmware v1.0 | AVR @ 16 MHz\r\n" .byte 0
msg_unknown: .ascii "Unknown command\r\n" .byte 0
msg_prompt:  .ascii "> " .byte 0

; ── SRAM buffer (32 bytes) ──────────────────────────────
.dseg
cmd_buf: .byte 32

Connecting to a Terminal

flowchart LR classDef pc fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef cable fill:#f1f5f9,stroke:#475569,color:#1e293b classDef mcu fill:#dcfce7,stroke:#16a34a,color:#14532d classDef note fill:#fef9c3,stroke:#ca8a04,color:#713f12 PC["PC / Laptop\nSerial Terminal:\n• PuTTY (Windows)\n• screen /dev/ttyUSB0 9600\n (Linux/Mac)\n• Arduino IDE Serial Monitor"]:::pc USB["USB Cable\n(on Arduino Uno)\nCH340 / CP2102 / FTDI\nconverts USB ↔ UART"]:::cable AVR["ATmega328P\nTX = PD1\nRX = PD0\n(shared with\nUSB-UART chip)"]:::mcu SETTINGS["Terminal settings:\nBaud: 9600\nData: 8 bits\nParity: None\nStop: 1\nFlow control: None\n(= 8N1)"]:::note PC --> USB --> AVR SETTINGS -.-> PC

[!TIP] On Arduino Uno, PD0/PD1 (RX/TX) are shared with the USB-UART chip. If you are uploading a sketch while something is connected to these pins, the upload will fail. Disconnect external devices from PD0/PD1 before flashing.


How Data Moves — Interrupt-Driven vs Polling

The program above uses polling — it sits in uart_rx() waiting for a byte. This blocks the CPU entirely. For real applications, you use interrupts:

flowchart TD classDef poll fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef irq fill:#dcfce7,stroke:#16a34a,color:#14532d subgraph POLLING ["Polling — Blocking"] P1["CPU enters uart_rx()"]:::poll P2["CPU loops checking RXC0 flag"]:::poll P3["Byte arrives → CPU reads UDR0"]:::poll P4["CPU returns — can do other work"]:::poll P5["❌ CPU does nothing useful\nwhile waiting for byte"]:::poll P1 --> P2 --> P3 --> P4 end subgraph INTERRUPT ["Interrupt-Driven — Non-blocking"] I1["CPU does main work loop"]:::irq I2["UART RX interrupt fires\nwhen byte arrives"]:::irq I3["CPU saves context\njumps to ISR (Interrupt Service Routine)"]:::irq I4["ISR reads UDR0\nstores in ring buffer"]:::irq I5["CPU resumes main work\nexactly where it left off"]:::irq I6["✅ CPU is never idle\nwaiting for UART"]:::irq I1 --> I2 --> I3 --> I4 --> I5 end

Interrupt-driven UART uses a ring buffer (circular queue) in SRAM. The ISR writes incoming bytes into the buffer; the main loop reads from it. This pattern is used in virtually every production embedded system.


Extension Challenges

Challenge A — Printf over UART
Redirect stdout to UART by implementing a custom uart_putchar() and linking it into avr-libc's FILE stream. Then printf("ADC value: %d\r\n", adc_result) works normally.

Challenge B — ADC Reading Reporter
Read the internal temperature sensor (ATmega328P ADC channel 8) and send the temperature over UART every second. Format the output as CSV: "time_ms,temp_raw,temp_celsius\r\n" — importable directly into Excel or Python.

Challenge C — UART Bootloader Concept
The Arduino bootloader is a small program in the top 2KB of Flash that listens on UART at startup. If it receives the STK500 programming protocol, it writes new firmware to Flash using the SPM instruction. If no data arrives within 1 second, it jumps to your application. Study the optiboot source code — it is under 500 lines of C and teaches you Flash self-programming, watchdog timers, and the AVR reset vector mechanism.


Verification Checklist

Before you call the UART shell working, verify these items:

  • The PC terminal is set to 9600 baud, 8 data bits, no parity, 1 stop bit, and no flow control.
  • Reset prints the banner and prompt exactly once.
  • Typed characters echo back immediately.
  • Pressing Enter after on turns PB5 on and replies LED is ON.
  • Pressing Enter after off turns PB5 off and replies LED is OFF.
  • Backspace removes one buffered character without corrupting the prompt.
  • Unknown commands produce a controlled error message and return to the prompt.

Common failure symptoms:

  • Garbled text: baud rate, CPU clock, or terminal frame settings do not match.
  • Nothing prints: TX is not enabled, UDR0 is written before UDRE0 is set, or the wrong COM port is open.
  • Upload fails on Arduino Uno: external wiring is loading PD0 or PD1 during programming.
  • Command never completes: terminal sends \n only while firmware waits for \r; configure CR or handle both.

Explained Solution

The UART peripheral shifts one byte at a time using the frame format configured in UCSR0C. UBRR0 = 103 gives 9600 baud at a 16 MHz clock in normal asynchronous mode. The transmit routine waits for UDRE0 before writing UDR0, and the receive routine waits for RXC0 before reading UDR0. The shell stores characters in an SRAM buffer, handles backspace by moving the X pointer backward, null-terminates the buffer on Enter, and compares it against Flash-resident command strings. GPIO action is intentionally small: on sets PB5 and off clears PB5.

Summary

flowchart TD classDef key fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef imp fill:#dcfce7,stroke:#16a34a,color:#14532d subgraph PROTOCOL ["Protocol"] K1["Asynchronous — no clock wire\nBoth sides agree on baud rate beforehand\n9600 / 115200 baud most common"]:::key K2["Frame = 1 start bit + 8 data bits + 1 stop bit\n10 bit-periods per byte\nAt 115200 baud: one byte takes 86.8 µs"]:::key end subgraph REGISTERS ["AVR Registers"] K3["UBRR0 sets baud rate\nUCSR0B enables TX (TXEN0) and RX (RXEN0)\nUDR0 is the data register — write to TX, read from RX"]:::key K4["Poll UDRE0 flag before writing\nPoll RXC0 flag before reading\nor use RX interrupt for non-blocking receive"]:::key end K5["UART is how you debug every MCU project\nyou will ever build.\nIf something is not working, print it over serial."]:::imp PROTOCOL --> REGISTERS --> K5

Further Reading

Next Exercise: Exercise 4 — EEPROM: Persistent Memory →

Mind Map

mindmap root((UART Shell)) Core idea Async serial link No clock wire Shared baud rate Terminal talks to MCU Frame Idle high Start bit low Eight data bits LSB first Stop bit high 8N1 default Formulas bit time equals 1 over baud 9600 bit time about 104 us byte time equals 10 over baud UBRR equals FCPU over 16 baud minus 1 16 MHz 9600 gives 103 Registers UBRR0 sets baud UCSR0B enables RX TX UCSR0C sets frame UDR0 sends and receives UCSR0A holds flags Checks Terminal at 9600 8N1 Prompt appears Echo works Commands change PB5 Backspace stable Mistakes TX RX swapped Wrong COM port Bad clock assumption Waiting for wrong newline