Hardware Protocols Mastery: I2C, SPI, and UART
Goal: Build a complete mental model of the three foundational serial communication protocols used in embedded systems. You will understand not just how to use libraries, but the actual electrical signals, timing requirements, and protocol state machines that make data flow between chips. By the end of this guide, you will be able to read oscilloscope traces, debug bus failures, design mixed-protocol systems, implement bit-banged versions from scratch, and make informed architectural decisions about when to use each protocol. You will finish with a portfolio of projects including logic analyzers, sensor interfaces, display drivers, protocol bridges, and debugging tools that demonstrate deep protocol mastery.
Introduction: What This Guide Covers
Serial communication protocols are the nervous system of embedded systems. Every sensor, display, memory chip, and peripheral communicates with your microcontroller through one of these three protocols. Understanding them deeply separates engineers who can read datasheets and debug hardware from those who rely on library examples and hope for the best.
What you will build (by the end of this guide):
- A complete serial terminal with flow control and break detection
- Multi-sensor I2C networks reading temperature, humidity, and motion data
- High-speed SPI interfaces to displays and flash memory
- A protocol analyzer that captures and decodes all three protocols
- A protocol bridge that translates between I2C and SPI devices
- Bit-banged implementations that work on any GPIO pins
Scope (what is included):
- UART: asynchronous serial, baud rates, framing, RS-232 vs TTL
- I2C: two-wire synchronous, addressing, clock stretching, multi-master
- SPI: four-wire synchronous, modes, chip select, high-speed operation
- Level shifting, noise immunity, and bus topology design
- Hardware debugging with logic analyzers and oscilloscopes
- Arduino and Raspberry Pi implementations in C/C++ and Python
Out of scope (for this guide):
- CAN bus, LIN, and automotive protocols (separate guide)
- Wireless protocols (Bluetooth, WiFi, Zigbee)
- USB protocol internals (separate guide)
- High-speed differential signaling (LVDS, RS-485)
The Big Picture (Mental Model)
┌─────────────────────────────────────┐
│ MICROCONTROLLER │
│ │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ UART │ │ I2C │ │ SPI │ │
│ │Peripheral│ │Peripheral│ │Peripheral│
│ └────┬────┘ └────┬────┘ └───┬───┘ │
└───────┼───────────┼─────────┼──────┘
│ │ │
┌──────────────────┼───────────┼─────────┼──────────────┐
│ │ │ │ │
│ ┌─────────────┴───┐ ┌───┴────┐ ┌┴─────────┐ │
│ │ TX ─────────► │ │ SDA ◄──►│ │ MOSI ───►│ │
│ │ RX ◄───────── │ │ SCL ───►│ │ MISO ◄───│ │
│ │ (GND shared) │ │ │ │ SCK ───►│ │
│ └─────────────────┘ └─────────┘ │ CS ───►│ │
│ └──────────┘ │
│ │
│ Point-to-Point Multi-Drop Bus Multi-Device│
│ Full Duplex Half Duplex Full Duplex │
│ Asynchronous Synchronous Synchronous │
│ │
└───────────────────────────────────────────────────────┘
PHYSICAL LAYER
Protocol Comparison:
UART I2C SPI
──── ─── ───
Wires: 2 (TX/RX) 2 (SDA/SCL) 4+ (MOSI/MISO/SCK/CS)
Speed: ~1 Mbps max ~3.4 Mbps max ~50+ Mbps
Devices: 1:1 Multi-drop (127) 1:N (per CS line)
Duplex: Full Half Full
Clock: None (async) Master provides Master provides
Address: None 7/10-bit Chip Select line
Distance:Meters Short (capacitance) Short (<1m typ)
Key Terms You Will See Everywhere
- Baud rate: bits per second (not bytes per second) for UART
- Clock polarity (CPOL): idle state of the SPI clock (high or low)
- Clock phase (CPHA): which clock edge samples data in SPI
- Open-drain: output that can only pull low; needs external pull-up
- Bit-banging: implementing a protocol in software using GPIO
- Pull-up resistor: resistor that holds a line high when not driven
- Chip select: SPI line that activates a specific slave device
- Arbitration: mechanism for multiple masters to share a bus
- Clock stretching: slave holding clock low to pause the master
How to Use This Guide
- Read the Theory Primer once to build the mental models for all three protocols.
- Start with UART projects (1-5) as they are the simplest conceptually.
- Move to I2C projects (6-11) to understand addressing and bus management.
- Tackle SPI projects (12-16) for high-speed and display interfaces.
- Finish with cross-protocol projects (17-20) that integrate everything.
- Keep a lab notebook: log wiring, timing measurements, and every bug you fix.
- Use a logic analyzer from Project 4 onward; it transforms your debugging ability.
Prerequisites & Background Knowledge
Before starting these projects, you should have foundational understanding in these areas:
Essential Prerequisites (Must Have)
Electronics Fundamentals:
- Voltage, current, and Ohm’s law
- Reading basic schematics and datasheets
- Understanding of logic levels (HIGH/LOW, 3.3V vs 5V)
- Breadboarding and basic soldering
- Recommended Reading: “The Art of Electronics” by Horowitz & Hill - Ch. 1-2
Microcontroller Basics:
- GPIO input/output configuration
- Basic C programming for embedded systems
- Interrupt concepts
- Using a serial terminal (minicom, screen, PuTTY)
- Recommended Reading: “Making Embedded Systems” by Elecia White - Ch. 1-5
Digital Logic:
- Binary and hexadecimal number systems
- Bitwise operations (AND, OR, XOR, shift)
- Timing diagrams interpretation
- Recommended Reading: “Digital Design” by Mano & Ciletti - Ch. 1-3
Helpful But Not Required
Advanced topics you can learn during the projects:
- DMA for high-speed transfers (Projects 14, 16)
- State machines for protocol implementation (Projects 17-20)
- Oscilloscope and logic analyzer usage (Project 4)
- Level shifter design (Project 18)
Self-Assessment Questions
- Can you calculate the value of a pull-up resistor given a desired rise time?
- Can you explain why 5V signals can damage a 3.3V microcontroller input?
- Can you read a timing diagram and identify setup and hold times?
- Can you configure a GPIO pin as an open-drain output?
- Have you used a serial terminal to communicate with a device?
If you answered “no” to questions 1-3: spend a week with basic electronics and “Making Embedded Systems” Ch. 1-5 before starting.
Development Environment Setup
Required hardware:
- Arduino Uno/Nano or Raspberry Pi Pico (or both)
- Raspberry Pi (3B+, 4, or 5) for Linux-based projects
- USB data cable, breadboard, jumper wires
- Pull-up resistor assortment (1K, 2.2K, 4.7K, 10K)
- Level shifter module (3.3V to 5V bidirectional)
- I2C devices: temperature sensor (TMP102, BME280), OLED display, EEPROM
- SPI devices: SD card module, TFT display, flash memory
Recommended tools:
- Logic analyzer (Saleae Logic or cheap 8-channel USB analyzer)
- Multimeter with continuity beeper
- Oscilloscope (even a cheap DSO150 helps)
Software setup:
# Arduino CLI installation
$ curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
$ arduino-cli core install arduino:avr
$ arduino-cli lib install "Adafruit BusIO"
# Raspberry Pi setup
$ sudo apt update
$ sudo apt install -y python3-smbus python3-spidev i2c-tools
$ sudo raspi-config # Enable I2C and SPI interfaces
Time Investment
- Beginner projects (1-3): weekend each (4-8 hours)
- Intermediate projects (4-8): 1-2 weeks each
- Advanced projects (9-14): 2-3 weeks each
- Expert projects (15-20): 3-4 weeks each
- Total mastery path: 4-6 months
Important Reality Check
Protocol debugging is 80% of embedded work. You will:
- Wire things wrong (swap TX/RX, forget ground)
- Use wrong baud rates or SPI modes
- Fight with pull-up resistor values
- Miss timing requirements in datasheets
- See signals that look nothing like the diagrams
This is normal. A logic analyzer and patience are your best tools.
Big Picture / Mental Model
┌────────────────────────────────────────────────────────────────┐
│ SERIAL COMMUNICATION │
├────────────────────────────────────────────────────────────────┤
│ │
│ ASYNCHRONOUS SYNCHRONOUS │
│ (No shared clock) (Shared clock signal) │
│ │ │ │
│ ▼ ┌──────┴──────┐ │
│ ┌──────┐ ▼ ▼ │
│ │ UART │ ┌─────┐ ┌─────┐ │
│ └──────┘ │ I2C │ │ SPI │ │
│ └─────┘ └─────┘ │
│ │
│ Both sides must agree Master generates clock │
│ on timing in advance Slave follows master's pace │
│ │
└────────────────────────────────────────────────────────────────┘
Communication Topology:
UART (Point-to-Point) I2C (Multi-Drop Bus)
┌───────┐ ┌───────┐ ┌───────┐
│ Device├────┤Device │ │Master │
│ A │ │ B │ └───┬───┘
└───────┘ └───────┘ │ SDA/SCL Bus
┌────┼────┬────┐
┌─┴─┐┌─┴─┐┌─┴─┐┌─┴─┐
│S1 ││S2 ││S3 ││S4 │
│ ││ ││ ││ │
│0x48│0x50│0x68│0x77│
└───┘└───┘└───┘└───┘
SPI (Star Topology)
┌───────┐
│Master ├──────────┬──────────┬──────────┐
└───────┘ │ │ │
MOSI/MISO/SCK │ │
┌─────┴─────┐ │ │
│ CS0 │ │ │
┌─┴─┐ ┌─┴─┐┌─┴─┐ ┌─┴─┐
│S1 │ │S2 ││S3 │ │S4 │
│ │ │ ││ │ │ │
│CS0│ │CS1││CS2│ │CS3│
└───┘ └───┘└───┘ └───┘
Theory Primer (Read This Before Coding)
This section is the mini-book. Each chapter builds a mental model you will apply directly in the projects.
Chapter 1: UART - Asynchronous Serial Communication
Fundamentals
UART (Universal Asynchronous Receiver/Transmitter) is the simplest serial protocol to understand because it requires no shared clock. Two devices agree on a baud rate (bits per second) in advance, and each samples the incoming data at that rate. The protocol uses two lines: TX (transmit) and RX (receive), making it full-duplex. Because there is no clock signal, timing accuracy is critical - if the sender and receiver disagree on timing by more than about 3%, communication fails.
UART is everywhere: debug consoles, GPS modules, Bluetooth adapters, WiFi modules, and computer serial ports all use UART. Understanding UART deeply means you can debug any embedded system by accessing its serial console.
Deep Dive
A UART frame consists of:
- Idle state: Line held high (mark)
- Start bit: Line pulled low (space) for one bit period
- Data bits: 5-9 bits, LSB first
- Parity bit: Optional error detection (even, odd, or none)
- Stop bits: 1, 1.5, or 2 bit periods held high
UART Frame Structure (8N1 - 8 data bits, No parity, 1 stop bit):
Idle Start D0 D1 D2 D3 D4 D5 D6 D7 Stop Idle
│ │ │ │ │ │ │ │ │ │ │ │
────┴──────┴─────┴────┴────┴────┴────┴────┴────┴────┴────┴─────┴────
▼ ▼
─────┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌───────
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
└───┘ └──┘ └──────┘ └──┘ └──┘ └──────┘ └──┘ └───┘
Start 1 0 1 1 1 0 1 Stop
(LSB) (MSB)
Example: Transmitting byte 0x55 = 0b01010101 at 9600 baud
Bit period = 1/9600 = 104.17 microseconds
Timing Requirements:
┌─────────────────────────────────────────────────────────┐
│ Baud Rate │ Bit Period │ Max Clock Error │ Common Use │
├────────────┼─────────────┼─────────────────┼────────────┤
│ 9600 │ 104.17 µs │ ±3% │ Debug │
│ 115200 │ 8.68 µs │ ±3% │ Fast debug │
│ 921600 │ 1.09 µs │ ±2% │ High-speed │
│ 1000000 │ 1.00 µs │ ±2% │ Maximum │
└────────────┴─────────────┴─────────────────┴────────────┘
The receiver samples each bit in the middle of its period. When it detects the falling edge of the start bit, it waits half a bit period and then samples at regular intervals. If the timing drifts too much, the receiver samples the wrong part of each bit and gets garbage. This is why both sides must agree on the exact baud rate.
RS-232 vs TTL Levels:
RS-232 is a voltage standard, not a protocol. Classic RS-232 uses ±12V signaling where +12V is a logic 0 (space) and -12V is a logic 1 (mark). Modern TTL/CMOS UART uses 0V/3.3V or 0V/5V with inverted logic. Never connect RS-232 directly to a microcontroller - you will damage it.
Voltage Level Comparison:
RS-232 TTL/CMOS
┌────────────────┐ ┌────────────────┐
│ │ │ │
+12V │ SPACE (0) │ 5V/ │ MARK (1) │
│ │ 3.3V │ (Idle) │
├────────────────┤ ├────────────────┤
0V │ (Invalid) │ 0V │ SPACE (0) │
├────────────────┤ │ │
-12V │ MARK (1) │ └────────────────┘
│ (Idle) │
└────────────────┘
RS-232: Logic inverted, high voltage swing
TTL: Direct logic, microcontroller-friendly
Conversion: MAX232, MAX3232, or USB-serial adapters
Flow Control:
Hardware flow control uses additional lines (RTS/CTS) to prevent buffer overflow. RTS (Request To Send) signals “I’m ready to receive” and CTS (Clear To Send) signals “I’m ready to transmit”. Software flow control (XON/XOFF) sends special characters to pause and resume transmission.
How This Fits in Projects
- Project 1: Basic UART configuration and loopback testing
- Project 2: Building a serial terminal with command parsing
- Project 3: UART debugging with error detection
- Project 4: Logic analyzer capture of UART traffic
- Project 5: RS-232 to TTL level conversion
Definitions & Key Terms
- Baud rate: bits per second transmitted
- Frame: start bit + data + optional parity + stop bits
- Mark: idle state, logic 1, line high (TTL)
- Space: active state, logic 0, line low (TTL)
- Framing error: stop bit not detected at expected time
- Overrun error: new data arrived before old data was read
Check-Your-Understanding Questions
- Why does UART require both devices to use the same baud rate?
- What happens if you swap TX and RX connections?
- Why is the start bit necessary?
- How does the receiver know where each bit ends?
Check-Your-Understanding Answers
- Without a shared clock, timing must be pre-agreed; mismatched rates cause sampling errors.
- No communication occurs; TX must connect to RX and vice versa.
- The falling edge of the start bit synchronizes the receiver to the frame.
- It counts bit periods from the start bit, sampling in the middle of each.
Chapter 2: I2C - Two-Wire Synchronous Bus
Fundamentals
I2C (Inter-Integrated Circuit, pronounced “eye-squared-see” or “eye-two-see”) uses just two wires to connect multiple devices on a shared bus. SDA (Serial Data) carries bidirectional data, and SCL (Serial Clock) carries the clock from master to slaves. The protocol uses addressing so multiple slaves can share the same two wires, with the master selecting which slave to talk to.
I2C is half-duplex - data flows in one direction at a time. The master initiates all transactions, generates the clock, and selects slaves by address. This makes I2C simpler to wire than SPI (fewer connections) but slower and more complex in software.
Deep Dive
Electrical Characteristics:
Both SDA and SCL are open-drain, meaning devices can only pull the lines low. External pull-up resistors (typically 2.2K to 10K) pull the lines high when no device is driving them low. This allows bidirectional communication on a single wire and enables clock stretching.
I2C Bus Electrical Structure:
VCC (3.3V or 5V)
│ │
┌┴┐ ┌┴┐
│ │Rp │ │Rp (Pull-up resistors)
│ │ │ │ (1K - 10K typical)
└┬┘ └┬┘
│ │
┌──────┼────────┼───────────────────────┐
│ │ │ │ SDA
│ ┌──┴──┐ ┌──┴──┐ ┌──────┐ ┌──────┐│
│ │ ══ │ │ ══ │ │ ══ │ │ ══ ││
│ └──┬──┘ └──┬──┘ └──┬───┘ └──┬───┘│
│ │ │ │ │ │
│ ┌──┴──┐ ┌──┴──┐ ┌──┴───┐ ┌──┴───┐│
│ │ ══ │ │ ══ │ │ ══ │ │ ══ ││
│ └──┬──┘ └──┬──┘ └──┬───┘ └──┬───┘│
│ │ │ │ │ │ SCL
└──────┼────────┼────────┼─────────┼────┘
│ │ │ │
Master Slave1 Slave2 Slave3
(0x---) (0x48) (0x50) (0x68)
Open-Drain Outputs:
┌──────────────────────────────────────┐
│ VCC │
│ │ │
│ ─┴─ Pull-up │
│ │ │
│ ─────●───────► Bus line (SDA/SCL) │
│ │ │
│ ──┴── MOSFET (open drain) │
│ │ │
│ GND (when active) │
└──────────────────────────────────────┘
Device can pull LOW but not drive HIGH
Multiple devices can share same line
If any device pulls LOW, line goes LOW (wired-AND)
Protocol State Machine:
I2C Transaction State Machine:
┌─────────┐
│ IDLE │ ◄──────────────────────────────────┐
└────┬────┘ │
│ Start Condition (SDA↓ while SCL high) │
▼ │
┌─────────┐ │
│ ADDRESS │ ───► Send 7-bit address + R/W bit │
└────┬────┘ │
│ Wait for ACK from slave │
▼ │
┌─────────┐ ┌─────────┐ │
│ ACK? │─NO─►│ NACK │──► Error handling ─┤
└────┬────┘ └─────────┘ │
│ YES │
▼ │
┌─────────┐ │
│ DATA │ ◄────────┐ │
└────┬────┘ │ │
│ │ More bytes to transfer │
▼ │ │
┌─────────┐ ┌────┴────┐ │
│ ACK/NACK│─────┤ Continue│ │
└────┬────┘ └─────────┘ │
│ Done │
▼ │
┌─────────┐ │
│ STOP │ ──► Stop Condition (SDA↑ while ────┘
└─────────┘ SCL high)
I2C Start and Stop Conditions:
Start Condition Stop Condition
SDA ─────┐ SDA ┌─────
│ │
└─────────── ─────┘
SCL ─────────────── SCL ───────────────
HIGH HIGH
SDA falls while SCL high SDA rises while SCL high
(Unique - normally SDA only (Unique - normally SDA only
changes when SCL is low) changes when SCL is low)
7-Bit Addressing:
I2C uses 7-bit addresses, allowing up to 127 devices (address 0 is reserved for general call). The 8th bit indicates read (1) or write (0). Some devices have configurable address pins to allow multiple identical devices on the same bus.
I2C Address Byte:
Bit: 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┐
│A6 │A5 │A4 │A3 │A2 │A1 │A0 │R/W│
└───┴───┴───┴───┴───┴───┴───┴───┘
└───────────┬───────────┘ │
7-bit address │
└─ 0 = Write, 1 = Read
Common Device Addresses:
┌────────────────────┬─────────┬────────────────┐
│ Device │ Address │ Notes │
├────────────────────┼─────────┼────────────────┤
│ AT24C02 EEPROM │ 0x50-57 │ A0-A2 pins │
│ TMP102 Temp Sensor │ 0x48-4B │ ADD0 pin │
│ MPU6050 Accel/Gyro │ 0x68-69 │ AD0 pin │
│ SSD1306 OLED │ 0x3C-3D │ SA0 pin │
│ BMP280 Pressure │ 0x76-77 │ SDO pin │
│ DS3231 RTC │ 0x68 │ Fixed │
│ PCA9685 PWM Driver │ 0x40-7F │ A0-A5 pins │
└────────────────────┴─────────┴────────────────┘
Clock Stretching:
Slow slave devices can hold SCL low to pause the master while they process data. The master must check SCL before assuming the clock transition completed. Not all masters support clock stretching, which can cause bus lockups.
Speed Modes:
I2C Speed Modes:
┌─────────────────┬─────────────┬────────────────────┐
│ Mode │ Max Speed │ Pull-up Resistance │
├─────────────────┼─────────────┼────────────────────┤
│ Standard Mode │ 100 kHz │ 10K typical │
│ Fast Mode │ 400 kHz │ 4.7K typical │
│ Fast Mode Plus │ 1 MHz │ 2K typical │
│ High Speed Mode │ 3.4 MHz │ Special drivers │
└─────────────────┴─────────────┴────────────────────┘
Pull-up Value Calculation:
Rp(min) = (VCC - VOL(max)) / IOL
Rp(max) = tr / (0.8473 × Cb)
Where:
VOL = Low output voltage (0.4V typical)
IOL = Low output current (3mA typical)
tr = Rise time (1µs for 100kHz, 300ns for 400kHz)
Cb = Bus capacitance (typically 10-400pF)
How This Fits in Projects
- Project 6: I2C scanner to discover devices on a bus
- Project 7: Reading temperature sensors and accelerometers
- Project 8: Writing to EEPROMs for data storage
- Project 9: Driving OLED displays
- Project 10: Multi-master arbitration experiments
- Project 11: I2C bus debugging and recovery
Definitions & Key Terms
- Master: Device that initiates transactions and generates clock
- Slave: Device that responds to master requests
- ACK/NACK: Acknowledgment (0) or No Acknowledgment (1) after each byte
- Repeated Start: New start without stop, used for read-after-write
- Clock stretching: Slave holding SCL low to pause communication
- Arbitration: Multiple masters detecting and yielding to each other
Check-Your-Understanding Questions
- Why does I2C use open-drain outputs instead of push-pull?
- What happens if two devices have the same address on the bus?
- How does a master know if a slave received data correctly?
- Why must SDA only change when SCL is low (except for start/stop)?
Check-Your-Understanding Answers
- Open-drain allows multiple devices to share lines and enables clock stretching.
- Both respond simultaneously, causing data corruption or one device never being accessed.
- The slave sends ACK (pulls SDA low) after each byte; NACK indicates a problem.
- Changing SDA while SCL is high is reserved for start and stop conditions to mark transaction boundaries.
Chapter 3: SPI - High-Speed Four-Wire Interface
Fundamentals
SPI (Serial Peripheral Interface) is the speed champion of embedded protocols. Using separate lines for data in each direction (MOSI and MISO) plus a clock (SCK), SPI achieves full-duplex communication at speeds often exceeding 10 MHz. A chip select (CS) line for each slave device creates a star topology that scales without address conflicts.
SPI trades wires for speed and simplicity. Where I2C uses two wires for many devices, SPI uses four wires plus one CS per device. The protocol itself is trivial - just shift data in and out on clock edges - but getting the timing right requires understanding SPI modes.
Deep Dive
SPI Signals:
SPI Signal Connections:
MASTER SLAVE
┌──────────────┐ ┌──────────────┐
│ │ MOSI │ │
│ MOSI ├──────────────►│ MOSI (DI) │
│ │ │ │
│ MISO │◄──────────────┤ MISO (DO) │
│ │ MISO │ │
│ SCK ├──────────────►│ SCK │
│ │ SCK │ │
│ CS0 ├──────────────►│ CS (SS) │
│ │ CS │ │
└──────────────┘ └──────────────┘
MOSI = Master Out Slave In (data from master to slave)
MISO = Master In Slave Out (data from slave to master)
SCK = Serial Clock (always from master)
CS = Chip Select (active low, one per slave)
Multiple Slaves:
MASTER
┌──────────────┐
│ │
MOSI ─┤ ├─── CS0 ───────► Slave 0
MISO ─┤ ├─── CS1 ───────► Slave 1
SCK ─┤ ├─── CS2 ───────► Slave 2
│ ├─── CS3 ───────► Slave 3
└──────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Slave0 │ │ Slave1 │ │ Slave2 │
│ CS0 │ │ CS1 │ │ CS2 │
└────────┘ └────────┘ └────────┘
All slaves share MOSI, MISO, SCK
Only selected slave (CS low) responds
Unselected slaves tri-state their MISO
SPI Modes:
The four SPI modes define when data is sampled relative to the clock. Getting this wrong is the most common SPI bug.
SPI Modes - Clock Polarity and Phase:
Mode │ CPOL │ CPHA │ Clock Idle │ Sample Edge │ Shift Edge
─────┼──────┼──────┼────────────┼─────────────┼───────────
0 │ 0 │ 0 │ LOW │ Rising │ Falling
1 │ 0 │ 1 │ LOW │ Falling │ Rising
2 │ 1 │ 0 │ HIGH │ Falling │ Rising
3 │ 1 │ 1 │ HIGH │ Rising │ Falling
CPOL = Clock POLarity (idle state of clock)
CPHA = Clock PHAse (which edge samples data)
Mode 0 (CPOL=0, CPHA=0) - Most Common:
CS ─────┐ ┌─────
└─────────────────────────────────────────┘
SCK ─────────┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ──────
└───┘ └───┘ └───┘ └───┘ └───┘
MOSI ─────────┬───────┬───────┬───────┬───────┬─────────
(idle) │ D7 │ D6 │ D5 │ D4 │ ...
▲ ▲ ▲ ▲ ▲
Sample on rising edge
Mode 3 (CPOL=1, CPHA=1) - Also Common:
CS ─────┐ ┌─────
└─────────────────────────────────────────┘
SCK ─────┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌─────────
└───┘ └───┘ └───┘ └───┘ └───┘
MOSI ─────────┬───────┬───────┬───────┬───────┬─────────
(idle) │ D7 │ D6 │ D5 │ D4 │ ...
▲ ▲ ▲ ▲
Sample on rising edge
Bit Order:
Most SPI devices use MSB-first transmission, but some (like certain LED drivers) use LSB-first. Check the datasheet.
Full-Duplex Operation:
SPI is truly full-duplex - data shifts in both directions simultaneously. When the master sends a command byte, it also receives a response byte (often garbage during command phase). A typical read operation sends a command, then continues clocking while reading the response.
SPI Full-Duplex Data Exchange:
Master TX: CMD ADDR XX XX XX XX
──► ──► ──► ──► ──► ──►
Slave TX: XX XX D0 D1 D2 D3
◄── ◄── ◄── ◄── ◄── ◄──
Legend:
CMD = Command byte sent by master
ADDR = Address byte sent by master
XX = Don't care / dummy byte
D0-D3 = Data bytes from slave
Both directions transfer on every clock cycle!
Master sends 6 bytes, receives 6 bytes simultaneously.
Only the last 4 received bytes contain useful data.
Speed Considerations:
SPI can run at very high speeds (10-50+ MHz) but layout matters. Long wires, poor grounding, and impedance mismatches cause signal integrity problems at high speeds. For breadboard prototyping, limit speed to 1-4 MHz.
SPI Speed vs Wire Length:
Speed │ Max Practical Length │ Notes
──────────┼──────────────────────┼─────────────────────
1 MHz │ 1 meter │ Breadboard OK
4 MHz │ 30 cm │ Short jumpers
10 MHz │ 10 cm │ PCB recommended
25 MHz │ 5 cm │ Careful routing
50+ MHz │ PCB only │ Impedance matched
How This Fits in Projects
- Project 12: SPI configuration and mode testing
- Project 13: Driving TFT displays at high speed
- Project 14: SD card interface and FAT filesystem
- Project 15: Flash memory read/write operations
- Project 16: High-speed data acquisition
Definitions & Key Terms
- MOSI/MISO: Data lines (some devices use SDI/SDO or DI/DO)
- SCK/SCLK: Clock signal from master
- CS/SS: Chip select / slave select (active low)
- CPOL: Clock polarity (idle state)
- CPHA: Clock phase (sample edge)
- Bit order: MSB-first or LSB-first
Check-Your-Understanding Questions
- Why does SPI need a separate chip select for each slave?
- What happens if you use the wrong SPI mode?
- Why is SPI faster than I2C?
- What is the purpose of dummy bytes in SPI transactions?
Check-Your-Understanding Answers
- No addressing scheme exists; CS physically selects which device responds.
- Data is sampled on the wrong edge, resulting in bit errors or shifted data.
- Full-duplex, no addressing overhead, higher clock speeds, simpler protocol.
- Keep the clock running while waiting for slave data or inserting timing delays.
Concept Summary Table
| Concept | Protocol | What You Must Internalize |
|---|---|---|
| Asynchronous timing | UART | Both sides must agree on baud rate beforehand |
| Framing | UART | Start bit, data, optional parity, stop bits |
| Voltage levels | UART | RS-232 (±12V) vs TTL (0-5V) vs CMOS (0-3.3V) |
| Open-drain | I2C | Lines can only be pulled low; pull-ups required |
| Addressing | I2C | 7-bit address selects slave; collisions cause problems |
| Clock stretching | I2C | Slave can pause master by holding SCL low |
| SPI modes | SPI | CPOL and CPHA determine sampling edge |
| Chip select | SPI | Active-low signal enables specific slave |
| Full-duplex | SPI | Data flows both directions simultaneously |
| Pull-up values | I2C/SPI | Affects rise time, power consumption, speed |
| Level shifting | All | Voltage domain translation for mixed systems |
| Bit-banging | All | Software implementation on GPIO pins |
Deep Dive Reading by Concept
| Concept | Book | Chapters |
|---|---|---|
| UART fundamentals | “Making Embedded Systems” by Elecia White | Ch. 6, 9 |
| I2C protocol details | “The Book of I2C” by Randall Hyde | Ch. 1-5 |
| SPI protocol | “Exploring BeagleBone” by Derek Molloy | Ch. 8 |
| Signal integrity | “High-Speed Digital Design” by Johnson & Graham | Ch. 1-3 |
| Protocol debugging | “Debugging Embedded and Real-Time Systems” | Ch. 4-5 |
| Bit-banging | “The Definitive Guide to ARM Cortex-M0” | Ch. 12 |
| Level shifting | “The Art of Electronics” by Horowitz & Hill | Ch. 1.6 |
| Embedded C | “C Programming for Embedded Systems” | Ch. 8-10 |
Quick Start Guide (First 48 Hours)
If you are overwhelmed, follow this minimal path:
Day 1 (4 hours):
- Set up Arduino IDE or Raspberry Pi development environment
- Complete Project 1: UART loopback test
- Use a serial terminal to verify communication
- Document your baud rate calculation
Day 2 (4 hours):
- Wire up an I2C device (temperature sensor or OLED)
- Run an I2C scanner to find the device address
- Read a single register from the device
- Display the value on serial output
After 48 hours: You have working UART and I2C. Continue with projects sequentially.
Recommended Learning Paths
Path A: Beginner (No Prior Embedded Experience)
- Read Chapter 1 (UART) thoroughly
- Projects 1-3 (UART fundamentals)
- Read Chapter 2 (I2C) thoroughly
- Projects 6-8 (I2C basics)
- Read Chapter 3 (SPI) thoroughly
- Projects 12-13 (SPI basics)
- Project 17 (Protocol analyzer) as capstone
Path B: Experienced Embedded Developer
- Skim theory chapters for gaps
- Project 4 (UART debugging)
- Projects 10-11 (I2C advanced)
- Projects 14-16 (SPI high-speed)
- Projects 18-20 (Cross-protocol)
Path C: Hardware Focus (PCB Designer)
- Focus on electrical characteristics in each chapter
- Project 5 (Level shifting)
- Project 11 (I2C bus debugging)
- Project 16 (High-speed SPI)
- Project 19 (Bus topology design)
Projects
Project 1: UART Fundamentals - Loopback and Echo
What you will build: A UART echo system that receives characters and transmits them back, demonstrating bidirectional communication. You will verify timing with an oscilloscope or logic analyzer.
Why it teaches the concept: Loopback testing isolates the UART hardware from complex peripherals, letting you verify baud rate, framing, and signal integrity in the simplest possible configuration.
Core Challenges
- Calculate baud rate register values from clock frequency
- Configure frame format (8N1 vs other configurations)
- Handle receive buffer overflow conditions
- Measure actual baud rate with an oscilloscope
- Compare theoretical vs measured bit timing
Key Concepts
| Concept | Book Reference |
|---|---|
| UART framing | “Making Embedded Systems” Ch. 6 |
| Baud rate calculation | “The Definitive Guide to ARM Cortex-M0” Ch. 12 |
| Oscilloscope measurements | “The Art of Electronics” Ch. 13 |
Difficulty & Time Estimate
- Difficulty: Beginner
- Time: 4-6 hours
- Prerequisites: Basic C programming, ability to use a serial terminal
Real-World Outcome
When complete, you will have:
Terminal Output (minicom/screen):
$ screen /dev/ttyUSB0 9600
Connected to Arduino UART Echo
Type characters and they appear back:
> Hello World!
Hello World!
> Testing 123
Testing 123
Baud rate verification (logic analyzer):
Start bit duration: 104.2 µs (expected 104.17 µs)
Error: 0.03% - WITHIN TOLERANCE
Frame capture:
'H' = 0x48 = 01001000
[Idle]─┐ ┌─┐ ┌─────┐ ┌─┐ [Stop]
└──┘ └─┘ └─────┘ └──
Start 0 0 0 1 0 0 1 0
The Core Question You’re Answering
How does asynchronous serial communication work without a shared clock, and what determines whether receiver and transmitter will understand each other?
Concepts You Must Understand First
Before starting, answer these questions:
- What is a baud rate and how is it different from bits per second?
- For 8N1 framing, what is the relationship?
- Reference: “Making Embedded Systems” Ch. 6
- How does the receiver find the start of each byte?
- What makes the start bit detectable?
- Reference: Any UART tutorial
- What happens if sender and receiver use different baud rates?
- Calculate the maximum tolerable mismatch
- Reference: UART timing specification
Questions to Guide Your Design
Hardware Setup:
- How will you create a loopback connection?
- Do you need a level shifter for your USB-serial adapter?
- What test equipment will you use to verify timing?
Software Architecture:
- Will you use polling or interrupts for receive?
- How will you handle buffer overflow?
- How will you test different baud rates?
Thinking Exercise
Before writing code, trace through the transmission of the character ‘A’ (0x41 = 01000001):
- Draw the complete UART frame (start, 8 data bits LSB first, stop)
- Calculate the total frame duration at 9600 baud
- Identify when the receiver will sample each bit
- Predict what happens if the receiver’s clock is 5% faster
The Interview Questions They’ll Ask
- “How does a UART receiver synchronize to incoming data?”
- “What causes framing errors and how do you detect them?”
- “Why is UART limited in maximum baud rate?”
- “Explain the difference between hardware and software flow control.”
- “How would you debug a UART that works at 9600 but fails at 115200?”
Hints in Layers
Hint 1 (Conceptual): Start with the simplest loopback: wire TX directly to RX on the same device. This tests the UART peripheral without any external complications.
Hint 2 (Direction): Use a baud rate that divides evenly from your clock. If the error is too high, consider adjusting the clock or using a different baud rate.
Hint 3 (Technical): On Arduino, the hardware serial is connected to USB. Use SoftwareSerial or a separate hardware UART for loopback testing without USB interference.
Hint 4 (Debugging): If you see garbage characters, the baud rate is wrong. If you see nothing, check TX/RX connections and pull-ups. Use an LED on TX to verify transmission is occurring.
Common Pitfalls & Debugging
| Problem | Root Cause | Fix | Verification |
|---|---|---|---|
| No characters received | TX not connected to RX | Verify wiring | Scope shows TX activity |
| Garbage characters | Baud rate mismatch | Verify both sides match | Measure bit duration |
| Missing characters | Buffer overflow | Add flow control | Monitor buffer level |
| Extra characters | Noise on line | Add filtering, shorter wires | Clean signal on scope |
| Works once then stops | Interrupt not cleared | Check ISR code | Add debug output |
Learning Milestones
- Basic: Echo works at 9600 baud with correct characters
- Intermediate: Works at 115200, can measure timing with logic analyzer
- Advanced: Handles continuous high-speed data without loss, implements flow control
Project 2: Building a Serial Command Terminal
What you will build: A command-line interface over UART that accepts text commands, parses them, and executes actions. Commands like “LED ON”, “READ ADC”, and “SET BAUD 115200” will control the microcontroller.
Why it teaches the concept: A command terminal requires line buffering, string parsing, and bidirectional communication - all fundamental UART operations. It also teaches you to design robust protocols.
Core Challenges
- Implement line buffering with backspace handling
- Parse commands into tokens (command, arguments)
- Implement a command dispatch table
- Handle command errors gracefully
- Support command history (advanced)
Key Concepts
| Concept | Book Reference |
|---|---|
| Line buffering | “C Programming for Embedded Systems” Ch. 8 |
| State machines | “Making Embedded Systems” Ch. 5 |
| String parsing | “C Programming Language” by K&R Ch. 5 |
Difficulty & Time Estimate
- Difficulty: Beginner-Intermediate
- Time: 8-12 hours
- Prerequisites: Project 1, basic string handling in C
Real-World Outcome
Terminal Session:
> help
Available commands:
LED <ON|OFF> - Control onboard LED
READ <ADC|TEMP> - Read sensor values
BAUD <rate> - Change baud rate
ECHO <ON|OFF> - Enable/disable echo
INFO - System information
RESET - Reset device
> led on
LED: ON
> read adc
ADC Channel 0: 2.45V (raw: 752)
> baud 115200
Changing baud rate to 115200...
Reconnect at new rate.
> invalid command
ERROR: Unknown command 'invalid'
Type 'help' for available commands.
The Core Question You’re Answering
How do you build a reliable human-machine interface over a serial link, handling the complexities of line editing, command parsing, and error recovery?
Questions to Guide Your Design
Buffer Management:
- How large should your line buffer be?
- What happens when the buffer overflows?
- How do you handle backspace at position 0?
Command Parsing:
- How will you separate command from arguments?
- How will you handle quoted strings with spaces?
- What errors need to be detected and reported?
Thinking Exercise
Design a state machine for line input that handles:
- Normal character accumulation
- Backspace (delete previous character)
- Escape sequences (arrow keys)
- Carriage return (end of line)
Draw the state diagram before implementing.
The Interview Questions They’ll Ask
- “How would you implement command history with up/down arrows?”
- “What happens if the user sends binary data to your text parser?”
- “How would you add authentication to your command interface?”
- “Describe the buffer overflow vulnerabilities in command parsing.”
- “How would you test this interface automatically?”
Hints in Layers
Hint 1: Start with the simplest command - just echo back what was typed.
Hint 2: Use a circular buffer for input to handle variable-length lines.
Hint 3: Implement commands as function pointers in a table for clean dispatch.
Hint 4: Handle the case where a command takes longer than the UART buffer fill time.
Project 3: UART Error Detection and Diagnostics
What you will build: A diagnostic tool that detects and reports UART errors including framing errors, parity errors, overrun errors, and noise detection. It will display statistics and help identify wiring or timing problems.
Why it teaches the concept: Real-world serial communication fails in subtle ways. Understanding error conditions and their causes is essential for debugging.
Core Challenges
- Enable and monitor all UART error flags
- Create an error statistics structure
- Inject errors intentionally to verify detection
- Correlate errors with physical conditions
- Build an error recovery mechanism
Key Concepts
| Concept | Book Reference |
|---|---|
| Error detection | “Making Embedded Systems” Ch. 9 |
| Noise immunity | “The Art of Electronics” Ch. 9 |
| Statistics | “C Programming for Embedded Systems” Ch. 10 |
Difficulty & Time Estimate
- Difficulty: Intermediate
- Time: 8-12 hours
- Prerequisites: Projects 1-2
Real-World Outcome
UART Diagnostics Report
========================
Baud Rate: 115200
Frame: 8N1
Statistics (last 60 seconds):
Bytes received: 45,230
Bytes transmitted: 45,230
Framing errors: 0
Parity errors: 0
Overrun errors: 2
Noise errors: 15
Break detected: 0
Error rate: 0.037%
Signal quality: GOOD
Last error: Overrun at byte 23,456
Cause: Receive buffer full, ISR delayed
Fix: Increase buffer or priority
Noise analysis:
Errors cluster at 12Hz intervals
Possible cause: Switching power supply
The Core Question You’re Answering
How do you build robust serial communication that can detect, report, and recover from real-world transmission errors?
Questions to Guide Your Design
- What error conditions can your UART hardware detect?
- How do you distinguish between wiring problems and software problems?
- When should you discard data vs attempt recovery?
The Interview Questions They’ll Ask
- “What causes framing errors and how do you prevent them?”
- “How would you implement error correction over a noisy link?”
- “Describe the difference between overrun and overflow.”
- “How do you handle break conditions in your protocol?”
Hints in Layers
Hint 1: Read your microcontroller’s UART status register documentation carefully.
Hint 2: Create a test mode that intentionally causes each error type.
Hint 3: Correlate error timing with external events (motor starts, power supply load).
Hint 4: Consider adding a watchdog that resets the UART if errors exceed a threshold.
Project 4: Logic Analyzer Capture of UART Traffic
What you will build: A software logic analyzer running on a second microcontroller that captures UART traffic, decodes frames, and displays the data stream. This is your most important debugging tool.
Why it teaches the concept: Seeing actual waveforms transforms your understanding. You will debug protocol issues visually instead of guessing.
Core Challenges
- Sample GPIO at rates high enough to capture UART
- Detect start bits reliably
- Decode frames from timing measurements
- Store and display captured data
- Handle edge cases (noise, glitches)
Key Concepts
| Concept | Book Reference |
|---|---|
| Sampling theory | “Digital Signal Processing” basics |
| Edge detection | “Making Embedded Systems” Ch. 8 |
| Timing measurement | “The Definitive Guide to ARM Cortex-M0” Ch. 12 |
Difficulty & Time Estimate
- Difficulty: Intermediate-Advanced
- Time: 2-3 weeks
- Prerequisites: Projects 1-3, interrupt handling
Real-World Outcome
Logic Analyzer Capture
======================
Sample rate: 1 MHz
Capture depth: 4096 samples
Trigger: Falling edge (start bit)
Time (µs) │ Level │ Decoded
───────────┼───────┼─────────────
0 │ HIGH │ Idle
104 │ LOW │ Start bit
208 │ HIGH │ Bit 0 = 1
312 │ LOW │ Bit 1 = 0
416 │ HIGH │ Bit 2 = 1
520 │ LOW │ Bit 3 = 0
624 │ HIGH │ Bit 4 = 1
728 │ LOW │ Bit 5 = 0
832 │ HIGH │ Bit 6 = 1
936 │ LOW │ Bit 7 = 0
1040 │ HIGH │ Stop bit
Decoded byte: 0x55 = 'U' (valid frame)
Measured baud: 9615 (+0.16% error)
The Core Question You’re Answering
How do you build a tool that lets you see exactly what is happening on a serial bus, turning invisible electrical signals into understandable data?
Questions to Guide Your Design
- What sample rate is needed to reliably capture your target baud rate?
- How do you synchronize to the start bit without a hardware UART?
- How much memory do you need for useful capture depth?
The Interview Questions They’ll Ask
- “What is the Nyquist rate and why does it matter here?”
- “How would you trigger on a specific byte pattern?”
- “How do you handle captures longer than your memory?”
- “What are the limitations of software-based logic analysis?”
Hints in Layers
Hint 1: You need to sample at least 8x the baud rate for reliable decoding.
Hint 2: Use timer capture interrupts for precise edge timing.
Hint 3: Implement circular buffers with pre-trigger and post-trigger modes.
Hint 4: Start with oscilloscope-verified test signals before trusting your decoder.
Project 5: RS-232 to TTL Level Converter with Protection
What you will build: A bidirectional level shifter that safely converts RS-232 signals (±12V) to TTL/CMOS levels (0-3.3V or 0-5V) with ESD and overvoltage protection.
Why it teaches the concept: Understanding voltage domains is essential for interfacing with legacy equipment and protecting your microcontroller from damage.
Core Challenges
- Design the level conversion circuit
- Add ESD protection components
- Test with actual RS-232 equipment
- Verify signal integrity at different speeds
- Document operating limits
Key Concepts
| Concept | Book Reference |
|---|---|
| Level shifting | “The Art of Electronics” Ch. 1.6 |
| ESD protection | “The Art of Electronics” Ch. 12 |
| RS-232 standard | “Making Embedded Systems” Ch. 9 |
Difficulty & Time Estimate
- Difficulty: Intermediate
- Time: 8-12 hours
- Prerequisites: Basic electronics, Projects 1-3
Real-World Outcome
Level Converter Specification:
==============================
Input: RS-232 (±12V)
Output: CMOS 3.3V
Protection:
ESD: ±15kV contact, ±8kV air
Overvoltage: Survives ±25V input
Reverse polarity: Protected
Tested performance:
Baud rates: 300 - 115200
Rise time: < 1µs
Propagation delay: 0.2µs
Power: 5mA typical
Schematic saved, BOM created, tested with vintage terminal.
The Core Question You’re Answering
How do you safely interface between different voltage domains while preserving signal integrity and protecting sensitive components?
Questions to Guide Your Design
- Why does RS-232 use ±12V when TTL uses 0-5V?
- What is the difference between a level shifter and a transceiver?
- What happens if you connect RS-232 directly to a 3.3V GPIO?
The Interview Questions They’ll Ask
- “Explain how a MAX232 charge pump works.”
- “What is the slew rate specification for RS-232 and why?”
- “How would you design for higher speeds than RS-232 supports?”
- “What protection do you need for industrial environments?”
Hints in Layers
Hint 1: Use a dedicated IC like MAX232 or MAX3232 for charge pump voltage generation.
Hint 2: Add TVS diodes for ESD protection on all signal lines.
Hint 3: Test with a known-working serial port before connecting to valuable equipment.
Hint 4: Consider optoisolation for noisy industrial environments.
Project 6: I2C Bus Scanner and Device Discovery
What you will build: A tool that scans all 127 possible I2C addresses and reports which devices respond. It will also attempt to identify common devices by their typical addresses and behavior.
Why it teaches the concept: Before you can communicate with an I2C device, you must find it. This project teaches addressing, ACK/NACK detection, and bus debugging.
Core Challenges
- Implement I2C start, stop, and address transmission
- Detect ACK vs NACK responses
- Handle bus errors (stuck bus, no pull-ups)
- Identify common devices by address
- Verify with known devices
Key Concepts
| Concept | Book Reference |
|---|---|
| I2C addressing | “The Book of I2C” Ch. 2 |
| ACK/NACK | “The Book of I2C” Ch. 3 |
| Bus debugging | “Making Embedded Systems” Ch. 9 |
Difficulty & Time Estimate
- Difficulty: Beginner-Intermediate
- Time: 6-8 hours
- Prerequisites: Basic I2C understanding
Real-World Outcome
I2C Bus Scanner
===============
Bus: I2C1, SDA=GPIO2, SCL=GPIO3
Pull-ups: 4.7K detected
Speed: 100 kHz
Scanning 0x03-0x77...
0 1 2 3 4 5 6 7 8 9 A B C D E F
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3C -- -- --
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- --
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- 77
Found 5 devices:
0x3C: SSD1306 OLED display (probable)
0x48: TMP102/LM75 temperature sensor (probable)
0x50: AT24C02 EEPROM (probable)
0x68: DS3231/MPU6050 (RTC or accelerometer)
0x77: BMP280/BME280 pressure sensor (probable)
Bus health: GOOD
SDA rise time: 450ns (OK)
SCL rise time: 420ns (OK)
No stuck conditions detected
The Core Question You’re Answering
How do you discover what devices are present on an I2C bus and verify that the bus is functioning correctly?
Questions to Guide Your Design
- What addresses are reserved and should not be scanned?
- What happens if you scan an address with no device?
- How do you handle a device that NACKs its own address?
The Interview Questions They’ll Ask
- “Why are addresses 0x00-0x07 and 0x78-0x7F reserved?”
- “How would you detect if pull-up resistors are missing?”
- “What causes a ‘bus stuck’ condition and how do you recover?”
- “How would you scan a bus with clock stretching devices?”
Hints in Layers
Hint 1: Start by sending just a start condition and address byte, then stop.
Hint 2: An ACK means something pulled SDA low on the 9th clock.
Hint 3: Add a timeout for devices that stretch the clock too long.
Hint 4: Compare your results with i2cdetect on Linux for verification.
Project 7: I2C Temperature and Motion Sensor Network
What you will build: A multi-sensor I2C network reading temperature (TMP102 or BME280), humidity, and motion (MPU6050) data, with all devices sharing the same bus.
Why it teaches the concept: Real I2C systems have multiple devices. You will learn addressing, register-based protocols, and managing multiple devices.
Core Challenges
- Address multiple devices on the same bus
- Implement device-specific register protocols
- Handle different data formats (12-bit, 16-bit, signed)
- Create a unified sensor abstraction layer
- Sample at different rates per device
Key Concepts
| Concept | Book Reference |
|---|---|
| Register protocols | “The Book of I2C” Ch. 4 |
| Sensor datasheets | Device documentation |
| Data conversion | “Making Embedded Systems” Ch. 6 |
Difficulty & Time Estimate
- Difficulty: Intermediate
- Time: 1-2 weeks
- Prerequisites: Project 6, ability to read datasheets
Real-World Outcome
Sensor Network Monitor
======================
Update rate: 10 Hz
TMP102 @ 0x48:
Temperature: 23.45°C
Alert: OFF (threshold: 30°C)
BME280 @ 0x77:
Temperature: 23.3°C
Humidity: 45.2%
Pressure: 1013.25 hPa
MPU6050 @ 0x68:
Accel X: 0.02g
Accel Y: -0.01g
Accel Z: 1.00g
Gyro X: 0.5°/s
Gyro Y: -0.2°/s
Gyro Z: 0.1°/s
Device Temp: 24.1°C
Motion events: 3 detected (tap)
Last motion: 2 seconds ago
The Core Question You’re Answering
How do you build a system that communicates with multiple I2C devices, each with different register layouts and data formats, on a shared bus?
Questions to Guide Your Design
- How do you organize code for devices with different register maps?
- How do you handle devices with configurable addresses?
- What is the best data structure for heterogeneous sensor data?
The Interview Questions They’ll Ask
- “How do you handle sensors with different update rates on the same bus?”
- “Describe the register read sequence for a typical I2C sensor.”
- “How would you add a new sensor type without modifying existing code?”
- “What is the bus bandwidth limit with multiple sensors?”
Hints in Layers
Hint 1: Read each sensor’s datasheet register map carefully.
Hint 2: Use a common I2C read/write wrapper, with sensor-specific layers above.
Hint 3: Implement pointer-based register addressing (write pointer, then read data).
Hint 4: Add calibration data reading and compensation for sensors that require it (BME280).
Project 8: I2C EEPROM Data Logger
What you will build: A data logging system that stores timestamped sensor readings to an I2C EEPROM, with read-back and wear-leveling capabilities.
Why it teaches the concept: EEPROM has limited write cycles and page-based writing. You will learn byte-level vs page-level operations and data persistence strategies.
Core Challenges
- Implement EEPROM byte read/write
- Implement page write with boundary handling
- Design a wear-leveling strategy
- Create a log format with timestamps
- Implement log retrieval and clear operations
Key Concepts
| Concept | Book Reference |
|---|---|
| EEPROM protocols | AT24C02 datasheet |
| Wear leveling | “Making Embedded Systems” Ch. 11 |
| Data structures | “C Programming for Embedded Systems” Ch. 7 |
Difficulty & Time Estimate
- Difficulty: Intermediate
- Time: 1-2 weeks
- Prerequisites: Projects 6-7
Real-World Outcome
EEPROM Data Logger
==================
Device: AT24C256 (32KB)
Organization: 512 pages x 64 bytes
Status:
Total entries: 1,247
Oldest entry: 2024-01-15 08:23:45
Newest entry: 2024-01-16 14:52:12
Free space: 18,432 bytes (56%)
Write cycles (page 0): 4,521 / 100,000
Commands:
> log read 10
Entry 1247: 2024-01-16 14:52:12 T=23.4C H=45% P=1013hPa
Entry 1246: 2024-01-16 14:42:12 T=23.3C H=44% P=1013hPa
...
> log stats
Entries per hour: 6
Estimated time to full: 8.2 days
Estimated EEPROM life: 22 years at current rate
> log export csv
Exporting 1247 entries to serial...
timestamp,temperature,humidity,pressure
2024-01-15 08:23:45,22.1,48,1015
...
The Core Question You’re Answering
How do you persistently store data on I2C EEPROM while managing page boundaries, write timing, and limited write endurance?
Questions to Guide Your Design
- How do you handle writes that cross page boundaries?
- What happens if power fails during a write?
- How do you know where the next free location is after power-up?
The Interview Questions They’ll Ask
- “Explain the EEPROM page write cycle and timing requirements.”
- “How would you implement a circular buffer in EEPROM?”
- “What is wear leveling and why is it necessary?”
- “How do you ensure data integrity after unexpected power loss?”
Hints in Layers
Hint 1: EEPROM write takes 5-10ms - poll for completion or delay.
Hint 2: Split multi-byte writes at page boundaries into multiple writes.
Hint 3: Reserve a header page for metadata (entry count, next write location).
Hint 4: Add a simple checksum to detect corrupted entries.
Project 9: I2C OLED Display Driver
What you will build: A driver for the SSD1306 128x64 OLED display that can draw text, shapes, and bitmap graphics over I2C.
Why it teaches the concept: Displays require sending large amounts of data efficiently. You will learn command vs data modes, frame buffers, and display memory organization.
Core Challenges
- Initialize the SSD1306 with correct command sequence
- Understand display memory organization (pages and columns)
- Implement a frame buffer in microcontroller RAM
- Create text rendering with fonts
- Optimize refresh rate
Key Concepts
| Concept | Book Reference |
|---|---|
| Display protocols | SSD1306 datasheet |
| Graphics primitives | “Computer Graphics” basics |
| Memory management | “Making Embedded Systems” Ch. 4 |
Difficulty & Time Estimate
- Difficulty: Intermediate
- Time: 2-3 weeks
- Prerequisites: Projects 6-7
Real-World Outcome
OLED Display Driver
===================
Display: SSD1306 128x64
Interface: I2C @ 400kHz
Frame buffer: 1024 bytes
Performance:
Full refresh: 12ms (83 FPS max)
Partial update: 2ms per page
API:
display_init()
display_clear()
display_pixel(x, y, on)
display_line(x0, y0, x1, y1)
display_rect(x, y, w, h, filled)
display_circle(cx, cy, r, filled)
display_text(x, y, "Hello World")
display_bitmap(x, y, bitmap, w, h)
display_update() // Flush buffer to display
Demo running: Shows sensor data with graphics and scrolling text.
The Core Question You’re Answering
How do you efficiently send display data over I2C while managing frame buffers and understanding display controller architecture?
Questions to Guide Your Design
- Why does the SSD1306 use page-based memory organization?
- Should you update the entire display or just changed regions?
- How do you balance memory usage against refresh speed?
The Interview Questions They’ll Ask
- “Explain the command/data mode switching in SSD1306.”
- “How would you implement hardware scrolling?”
- “What is the maximum refresh rate achievable over I2C?”
- “How do you render proportional fonts efficiently?”
Hints in Layers
Hint 1: Read the SSD1306 initialization sequence from a working library first.
Hint 2: The display is organized as 8 horizontal pages of 128 columns each.
Hint 3: Use a local frame buffer and send the whole thing for simplicity first.
Hint 4: For text, pre-render fonts as bitmaps indexed by ASCII value.
Project 10: I2C Multi-Master Configuration
What you will build: A system with two I2C masters that can both initiate transactions without collision, using I2C arbitration.
Why it teaches the concept: Multi-master is the most complex I2C scenario. Understanding arbitration and bus contention is essential for robust systems.
Core Challenges
- Configure two microcontrollers as I2C masters
- Implement and observe arbitration
- Handle bus busy conditions
- Design a communication protocol between masters
- Measure and log collision events
Key Concepts
| Concept | Book Reference |
|---|---|
| I2C arbitration | “The Book of I2C” Ch. 5 |
| Multi-master | I2C specification v6 |
| Contention handling | “Making Embedded Systems” Ch. 9 |
Difficulty & Time Estimate
- Difficulty: Advanced
- Time: 2-3 weeks
- Prerequisites: Projects 6-9
Real-World Outcome
Multi-Master I2C Test
=====================
Master 1: Arduino Uno @ 0x10
Master 2: Raspberry Pi Pico @ 0x11
Slave: EEPROM @ 0x50
Test: Simultaneous write attempts
Cycle 1: Master 1 started first, Master 2 detected busy, retried
Cycle 2: Master 2 started first, Master 1 detected busy, retried
Cycle 3: Simultaneous start, Master 1 lost arbitration at bit 3
Master 1 retried after backoff
Statistics (1000 cycles):
Master 1 won: 487
Master 2 won: 498
Arbitration events: 15
Failed transactions: 0
Arbitration is working correctly!
Average retry delay: 2.3ms
The Core Question You’re Answering
How does I2C handle multiple masters trying to use the bus simultaneously, and how do you build reliable systems with this capability?
Questions to Guide Your Design
- What makes I2C arbitration non-destructive?
- How does a master know it lost arbitration?
- What is clock synchronization in multi-master?
The Interview Questions They’ll Ask
- “Explain the I2C arbitration mechanism step by step.”
- “What happens if two masters send to the same slave address?”
- “How would you implement a priority scheme with multi-master?”
- “Why is multi-master rarely used in practice?”
Hints in Layers
Hint 1: Both masters must monitor SDA during transmission.
Hint 2: A master loses arbitration when it transmits 1 but reads 0.
Hint 3: After losing, the master should wait for stop and retry.
Hint 4: Add random backoff to prevent repeated collisions.
Project 11: I2C Bus Debugging and Recovery
What you will build: A diagnostic tool that can detect and recover from stuck I2C bus conditions, identify problematic devices, and measure bus health.
Why it teaches the concept: I2C buses can lock up. Knowing how to diagnose and fix bus problems is essential for debugging real systems.
Core Challenges
- Detect stuck SDA or SCL conditions
- Implement bus recovery (9 clock pulses)
- Measure rise times and compare to specification
- Identify slow or non-compliant devices
- Generate a bus health report
Key Concepts
| Concept | Book Reference |
|---|---|
| Bus recovery | “The Book of I2C” Ch. 6 |
| Signal integrity | “High-Speed Digital Design” Ch. 2 |
| Debugging | “Making Embedded Systems” Ch. 9 |
Difficulty & Time Estimate
- Difficulty: Advanced
- Time: 2 weeks
- Prerequisites: Projects 6-10
Real-World Outcome
I2C Bus Diagnostic Report
=========================
Bus: I2C1, SDA=GPIO2, SCL=GPIO3
Test date: 2024-01-16 15:30:00
Electrical Analysis:
Pull-up detected: Yes
SDA rise time: 650ns (MARGINAL - spec 1000ns max for 100kHz)
SCL rise time: 480ns (OK)
Bus capacitance: ~180pF (estimated)
Recommended pull-up: 2.2K (currently ~4.7K detected)
Device Health:
0x48: OK (responds in 45µs)
0x50: OK (responds in 38µs)
0x68: SLOW (clock stretch 1.2ms detected)
0x77: OK (responds in 52µs)
Bus Stress Test:
1000 transactions @ 100kHz: 0 errors
1000 transactions @ 400kHz: 3 errors (0.3%)
Recommendation: Use 100kHz for reliability
Recovery Test:
Simulated stuck SDA: Recovered after 9 clocks
Simulated stuck SCL: Timeout (manual intervention needed)
The Core Question You’re Answering
How do you diagnose I2C bus problems and recover from stuck conditions without power cycling the system?
Questions to Guide Your Design
- What causes a bus to get stuck?
- Why do 9 clock pulses help recovery?
- How do you measure rise time without an oscilloscope?
The Interview Questions They’ll Ask
- “How do you detect if the I2C bus is stuck?”
- “Explain the 9-clock recovery procedure.”
- “What causes excessive rise times and how do you fix them?”
- “How would you isolate a misbehaving device on a shared bus?”
Hints in Layers
Hint 1: A stuck bus usually means a slave is holding SDA low.
Hint 2: Toggle SCL 9 times to shift out any partial byte in the slave.
Hint 3: Measure rise time by toggling and sampling with a timer.
Hint 4: Consider adding a bus switch IC for hardware isolation.
Project 12: SPI Mode Configuration and Testing
What you will build: A test framework that verifies communication in all four SPI modes, helping you determine the correct settings for any device.
Why it teaches the concept: Wrong SPI mode is the most common SPI bug. Systematic testing teaches you to identify and fix mode mismatches.
Core Challenges
- Implement all four SPI modes
- Create a test pattern that reveals mode errors
- Visualize timing with logic analyzer
- Document mode behavior differences
- Build a mode auto-detection feature
Key Concepts
| Concept | Book Reference |
|---|---|
| SPI modes | “Exploring BeagleBone” Ch. 8 |
| Clock polarity | Device datasheets |
| Timing diagrams | “The Art of Electronics” Ch. 10 |
Difficulty & Time Estimate
- Difficulty: Intermediate
- Time: 1 week
- Prerequisites: Basic SPI understanding
Real-World Outcome
SPI Mode Tester
===============
Master: Raspberry Pi Pico
Test device: 74HC595 shift register
Clock: 1 MHz
Mode 0 (CPOL=0, CPHA=0):
Sent: 0xAA 0x55
Received: 0xAA 0x55
Status: PASS
Mode 1 (CPOL=0, CPHA=1):
Sent: 0xAA 0x55
Received: 0x54 0xAA
Status: FAIL (data shifted)
Mode 2 (CPOL=1, CPHA=0):
Sent: 0xAA 0x55
Received: 0x55 0x2A
Status: FAIL (data inverted/shifted)
Mode 3 (CPOL=1, CPHA=1):
Sent: 0xAA 0x55
Received: 0xAA 0x55
Status: PASS
Conclusion: Device works in Mode 0 and Mode 3
Recommended: Mode 0 (most common)
The Core Question You’re Answering
How do clock polarity and phase affect SPI communication, and how do you determine the correct settings for any device?
Questions to Guide Your Design
- What test patterns reveal mode errors?
- How do you verify correct operation without a known-good device?
- Can you auto-detect the correct mode?
The Interview Questions They’ll Ask
- “Draw the timing diagram for all four SPI modes.”
- “Why might a device work in two different modes?”
- “How would you debug SPI communication that only fails intermittently?”
- “What is the relationship between mode and maximum speed?”
Hints in Layers
Hint 1: Use 0xAA and 0x55 - these create alternating bit patterns.
Hint 2: A shift register provides loopback capability.
Hint 3: Wrong mode typically shifts data by one bit.
Hint 4: Capture with logic analyzer to see actual clock edges.
Project 13: SPI TFT Display Driver
What you will build: A driver for an ILI9341 or ST7789 TFT display using SPI, capable of drawing graphics and images at high speed.
Why it teaches the concept: Displays require high bandwidth and efficient data transfer. You will push SPI to its limits and optimize for speed.
Core Challenges
- Initialize the display controller
- Implement color format conversion (RGB565)
- Draw primitives (pixels, lines, rectangles)
- Optimize for maximum frame rate
- Implement double buffering (if memory permits)
Key Concepts
| Concept | Book Reference |
|---|---|
| Display controllers | ILI9341/ST7789 datasheets |
| Color formats | “Computer Graphics” basics |
| DMA transfers | “Making Embedded Systems” Ch. 8 |
Difficulty & Time Estimate
- Difficulty: Intermediate-Advanced
- Time: 2-3 weeks
- Prerequisites: Project 12
Real-World Outcome
TFT Display Driver
==================
Display: ILI9341 320x240
Interface: SPI @ 40 MHz
Color: RGB565 (16-bit)
Performance:
Full screen clear: 15ms (67 FPS)
Pixel write: 0.5µs
Line draw: 2ms (diagonal 320px)
Image blit: 25ms (100x100 px)
With DMA:
Full screen clear: 8ms (125 FPS)
Image blit: 12ms (100x100 px)
Demo running: Animated graphics, smooth scrolling text,
and image display from SD card.
The Core Question You’re Answering
How do you drive a high-resolution display over SPI while maximizing frame rate and minimizing CPU usage?
Questions to Guide Your Design
- What is the maximum theoretical frame rate for your display/SPI combo?
- Should you use a frame buffer or direct drawing?
- How do you handle the command/data mode pin?
The Interview Questions They’ll Ask
- “Calculate the SPI bandwidth needed for 60 FPS on a 320x240 RGB565 display.”
- “How would you implement partial screen updates?”
- “What is the advantage of DMA for display updates?”
- “How do you synchronize display updates to avoid tearing?”
Hints in Layers
Hint 1: The ILI9341 initialization sequence is complex - start from a working example.
Hint 2: Set the drawing window before sending pixel data.
Hint 3: Use DMA for large transfers to free the CPU.
Hint 4: Consider hardware-accelerated scrolling for text applications.
Project 14: SPI SD Card Interface and FAT Filesystem
What you will build: A complete SD card interface using SPI mode, with FAT16/32 filesystem support for reading and writing files.
Why it teaches the concept: SD cards are complex SPI devices with initialization sequences, command protocols, and block-based access. FAT filesystem adds another layer of abstraction.
Core Challenges
- Implement SD card SPI initialization
- Send SD commands and parse responses
- Read and write 512-byte sectors
- Integrate FatFs or similar filesystem library
- Handle card detect and write protect
Key Concepts
| Concept | Book Reference |
|---|---|
| SD card protocol | SD Simplified Specification |
| Block devices | “Operating Systems” basics |
| FAT filesystem | “File Systems” documentation |
Difficulty & Time Estimate
- Difficulty: Advanced
- Time: 3-4 weeks
- Prerequisites: Projects 12-13
Real-World Outcome
SD Card Interface
=================
Card: 16GB SDHC Class 10
Mode: SPI @ 25 MHz
Filesystem: FAT32
Card Info:
Manufacturer: SanDisk
Capacity: 15,931,539,456 bytes
Sector size: 512 bytes
Total sectors: 31,116,288
Filesystem:
Cluster size: 32KB
Free space: 12.4 GB
Files: 247
Benchmark:
Sequential read: 1.8 MB/s
Sequential write: 0.9 MB/s
Random read: 0.3 MB/s
Random write: 0.1 MB/s
File operations:
f_open("data.txt", "w") - OK
f_write(buffer, 1024) - OK, 1024 bytes written
f_close() - OK
The Core Question You’re Answering
How do you interface with block storage devices over SPI and implement filesystem access for persistent data storage?
Questions to Guide Your Design
- What is the SD card initialization sequence in SPI mode?
- How do you handle different card types (SD, SDHC, SDXC)?
- What is the role of CRC in SD commands?
The Interview Questions They’ll Ask
- “Explain the SD card command structure.”
- “How do you detect if a card is SDHC vs SD?”
- “What causes write performance to be slower than read?”
- “How would you implement wear leveling on SD card?”
Hints in Layers
Hint 1: SD initialization must occur at slow speed (400 kHz) before switching to high speed.
Hint 2: Use CMD0 with chip select asserted to enter SPI mode.
Hint 3: FatFs is a well-tested library - use it instead of implementing FAT yourself.
Hint 4: Add retry logic for failed commands - SD cards can be temperamental.
Project 15: SPI Flash Memory Driver
What you will build: A driver for SPI NOR flash memory (like W25Q32) supporting read, write, erase, and protection operations.
Why it teaches the concept: Flash memory has different semantics than RAM - write requires erase, and operations are page-based. This project teaches flash-specific concepts.
Core Challenges
- Implement flash ID detection
- Read data at high speed
- Implement page program with alignment
- Implement sector and block erase
- Handle write protection and status registers
Key Concepts
| Concept | Book Reference |
|---|---|
| Flash technology | W25Q32 datasheet |
| NOR vs NAND | “Making Embedded Systems” Ch. 11 |
| Wear considerations | “Flash Memory” documentation |
Difficulty & Time Estimate
- Difficulty: Intermediate-Advanced
- Time: 2 weeks
- Prerequisites: Projects 12-13
Real-World Outcome
SPI Flash Driver
================
Device: W25Q32JV (32 Mbit)
Interface: SPI @ 50 MHz
Device Info:
Manufacturer: Winbond (0xEF)
Device ID: 0x4016
Capacity: 4,194,304 bytes (4 MB)
Page size: 256 bytes
Sector size: 4,096 bytes
Block size: 65,536 bytes
Performance:
Read (sequential): 6.2 MB/s
Page program: 200 µs typical
Sector erase: 50 ms typical
Bulk erase: 40 seconds
API:
flash_read(addr, buffer, len)
flash_write(addr, buffer, len) // handles alignment
flash_erase_sector(addr)
flash_erase_block(addr)
flash_erase_chip()
flash_get_status()
flash_set_protection(level)
The Core Question You’re Answering
How do you work with flash memory’s unique constraints including erase-before-write, page boundaries, and limited write cycles?
Questions to Guide Your Design
- Why must you erase before writing to flash?
- What happens if you write across a page boundary?
- How do you handle wear leveling for frequently updated data?
The Interview Questions They’ll Ask
- “Explain the difference between NOR and NAND flash.”
- “What is a write-erase cycle and why does it matter?”
- “How would you implement a configuration storage area with flash?”
- “What is the purpose of the write enable command?”
Hints in Layers
Hint 1: Always read the JEDEC ID to verify correct device communication.
Hint 2: Enable write enable (WREN) before every write or erase operation.
Hint 3: Poll the status register to detect operation completion.
Hint 4: For frequent updates, implement a log-structured approach to reduce wear.
Project 16: High-Speed SPI Data Acquisition
What you will build: A data acquisition system that reads ADC samples at maximum SPI speed and transfers them to a host for analysis.
Why it teaches the concept: High-speed SPI requires understanding of DMA, buffering, and data throughput optimization. This project pushes hardware limits.
Core Challenges
- Configure SPI for maximum speed
- Implement DMA for continuous transfer
- Design a ring buffer for data storage
- Stream data to host over USB or UART
- Measure and optimize throughput
Key Concepts
| Concept | Book Reference |
|---|---|
| DMA operation | “Making Embedded Systems” Ch. 8 |
| ADC sampling | ADC datasheet |
| Buffer management | “C Programming for Embedded Systems” Ch. 9 |
Difficulty & Time Estimate
- Difficulty: Advanced
- Time: 3-4 weeks
- Prerequisites: Projects 12-15
Real-World Outcome
High-Speed Data Acquisition
===========================
ADC: MCP3208 (12-bit, 8-channel)
Interface: SPI @ 2 MHz
Sample rate: 100 kSPS (single channel)
Configuration:
Channels: 1 (continuous)
Trigger: Free-running
Buffer: 16K samples ring buffer
Transfer: USB CDC @ 921600 baud
Performance:
Samples/second: 100,000
Throughput: 200 KB/s (16-bit samples)
CPU usage: 12% (DMA handles transfers)
Latency: 160 µs (buffer to host)
Streaming to host:
Format: Binary (2 bytes per sample)
Timestamp: Every 1000 samples
Host software: Python with matplotlib
Captured 1M samples, FFT shows 1kHz test signal at -3dB.
The Core Question You’re Answering
How do you achieve maximum throughput from an SPI ADC while minimizing CPU usage and maintaining continuous data flow?
Questions to Guide Your Design
- What limits the maximum sample rate?
- How do you avoid buffer overrun?
- What is the CPU-to-DMA handoff overhead?
The Interview Questions They’ll Ask
- “Calculate the maximum sample rate for a given SPI clock.”
- “How do you handle the gap between DMA transfers?”
- “What causes aliasing and how do you prevent it?”
- “How would you add real-time triggering to this system?”
Hints in Layers
Hint 1: Check the ADC’s maximum SPI clock and sample rate specifications.
Hint 2: Use ping-pong DMA buffers for continuous capture.
Hint 3: Decouple capture from transmission using a large ring buffer.
Hint 4: Add simple compression if bandwidth is limited.
Project 17: Multi-Protocol Analyzer
What you will build: A logic analyzer that captures and decodes UART, I2C, and SPI traffic simultaneously, displaying transactions in a unified view.
Why it teaches the concept: Understanding all three protocols deeply enough to decode them in software represents true mastery. This is the ultimate debugging tool.
Core Challenges
- Sample multiple channels simultaneously
- Implement protocol decoders for each type
- Handle different speeds on different channels
- Create a unified timeline display
- Support triggering on protocol events
Key Concepts
| Concept | Book Reference |
|---|---|
| Protocol decoding | “Debugging Embedded Systems” |
| State machines | “Making Embedded Systems” Ch. 5 |
| Real-time capture | “High-Speed Digital Design” Ch. 1 |
Difficulty & Time Estimate
- Difficulty: Expert
- Time: 4-6 weeks
- Prerequisites: Projects 4, 11, 12
Real-World Outcome
Multi-Protocol Analyzer
=======================
Sample rate: 10 MHz
Channels: 8 (GPIO0-7)
Buffer: 64K samples per channel
Channel Configuration:
CH0: UART TX (9600 baud)
CH1: UART RX (9600 baud)
CH2: I2C SDA
CH3: I2C SCL
CH4: SPI MOSI
CH5: SPI MISO
CH6: SPI SCK
CH7: SPI CS
Capture Timeline:
0.00ms: [I2C] Start, Addr=0x48 Write
0.05ms: [I2C] Data=0x00 (register pointer)
0.10ms: [I2C] Repeated Start, Addr=0x48 Read
0.15ms: [I2C] Data=0x1C (temperature high)
0.20ms: [I2C] Data=0x80 (temperature low)
0.25ms: [I2C] NACK, Stop
0.30ms: [SPI] CS↓, Mode 0
0.32ms: [SPI] TX=0x0B (read command)
0.34ms: [SPI] TX=0x00 (address high)
0.36ms: [SPI] TX=0x10 (address low)
0.38ms: [SPI] RX=0xAA (data)
0.40ms: [SPI] CS↑
0.50ms: [UART] TX='T' 'e' 'm' 'p' ':' '2' '5' 'C'
Export options: CSV, VCD, JSON
The Core Question You’re Answering
How do you build a universal protocol analysis tool that can capture, decode, and display multiple serial protocols simultaneously?
Questions to Guide Your Design
- What sample rate covers all target protocols?
- How do you decode protocols with different speeds?
- What is the trigger architecture?
The Interview Questions They’ll Ask
- “How do you synchronize decoding across multiple protocols?”
- “What is the minimum sample rate needed for each protocol?”
- “How would you add a new protocol decoder?”
- “How do you handle protocol errors in the decoded output?”
Hints in Layers
Hint 1: Sample all channels at the highest rate needed (typically SPI clock * 4).
Hint 2: Run each protocol decoder as a separate state machine.
Hint 3: Use timestamps to correlate events across protocols.
Hint 4: Consider PIO on RP2040 for parallel capture.
Project 18: Protocol Bridge (I2C to SPI Translator)
What you will build: A bridge device that translates I2C transactions to SPI and vice versa, allowing devices on different buses to communicate.
Why it teaches the concept: Building a bridge requires deep understanding of both protocols, including timing constraints and addressing models.
Core Challenges
- Design the address/command mapping scheme
- Handle different data rates between buses
- Implement buffering for asynchronous operation
- Handle error conditions on both sides
- Minimize latency through the bridge
Key Concepts
| Concept | Book Reference |
|---|---|
| Protocol translation | “Making Embedded Systems” Ch. 9 |
| Buffering strategies | “C Programming for Embedded Systems” Ch. 9 |
| Embedded architecture | “Making Embedded Systems” Ch. 3 |
Difficulty & Time Estimate
- Difficulty: Expert
- Time: 4-6 weeks
- Prerequisites: Projects 7, 8, 12-15
Real-World Outcome
I2C to SPI Bridge
=================
I2C Slave Address: 0x50
SPI Master: Mode 0, 1 MHz
Protocol Mapping:
I2C Write [addr][cmd][data] -> SPI [cmd][data]
I2C Read [addr][cmd] -> SPI [cmd], return [data]
Example Transaction:
Host sends I2C: Start, 0x50 Write, 0x03, 0xAA, 0xBB, Stop
Bridge sends SPI: CS↓, 0x03, 0xAA, 0xBB, CS↑
Host sends I2C: Start, 0x50 Write, 0x05, Repeated Start, 0x50 Read
Bridge sends SPI: CS↓, 0x05, (reads 2 bytes), CS↑
Bridge responds I2C: Data=0xCC, Data=0xDD, NACK, Stop
Statistics:
Transactions bridged: 12,456
Errors: 0
Average latency: 45µs
Maximum latency: 120µs
The Core Question You’re Answering
How do you translate between different serial protocols while preserving data integrity and minimizing latency?
Questions to Guide Your Design
- How do you map I2C addresses to SPI chip selects?
- What happens when SPI is faster than I2C?
- How do you handle transaction boundaries?
The Interview Questions They’ll Ask
- “What is the maximum throughput of your bridge?”
- “How do you handle clock stretching on the I2C side?”
- “What happens if the SPI device is slow to respond?”
- “How would you add support for I2C register-style reads?”
Hints in Layers
Hint 1: Act as an I2C slave and SPI master simultaneously.
Hint 2: Buffer the entire I2C write before starting the SPI transaction.
Hint 3: Use clock stretching to pause the I2C master during SPI operations.
Hint 4: Define a clear command set that maps to SPI operations.
Project 19: Bit-Banging Protocol Implementations
What you will build: Software implementations of I2C and SPI using GPIO pins only, without hardware peripherals.
Why it teaches the concept: Bit-banging forces you to understand every signal transition. It also lets you use any pins and add debugging visibility.
Core Challenges
- Generate precise timing using delays or timers
- Implement I2C open-drain behavior in software
- Handle clock stretching detection
- Match hardware peripheral performance
- Make the code portable across platforms
Key Concepts
| Concept | Book Reference |
|---|---|
| Bit-banging | “The Definitive Guide to ARM Cortex-M0” Ch. 12 |
| Timing accuracy | “Making Embedded Systems” Ch. 8 |
| GPIO manipulation | “Making Embedded Systems” Ch. 6 |
Difficulty & Time Estimate
- Difficulty: Advanced
- Time: 3-4 weeks
- Prerequisites: All previous projects
Real-World Outcome
Bit-Banged Protocols
====================
Platform: Any GPIO (tested on AVR, ARM, ESP32)
I2C Implementation:
Pins: Any GPIO (configured as open-drain)
Speed: Up to 100 kHz
Features: Start, Stop, Repeated Start, ACK/NACK
Clock stretch: Supported
Test: Scanned bus, found device at 0x48
Test: Read 2 bytes from TMP102: OK
SPI Implementation:
Pins: Any GPIO (MOSI, MISO, SCK, CS)
Modes: All 4 modes supported
Speed: Up to 500 kHz (software limited)
Features: MSB/LSB first configurable
Test: Read JEDEC ID from flash: 0xEF4016 (correct)
Comparison vs Hardware:
I2C 100 kHz: Software 98 kHz actual
SPI 1 MHz: Hardware 1 MHz, Software 480 kHz
CPU usage: Software ~10x higher
Use case: Extra I2C buses on MCUs with limited hardware.
The Core Question You’re Answering
How do you implement serial protocols purely in software, and what are the trade-offs compared to hardware peripherals?
Questions to Guide Your Design
- How do you achieve accurate timing without a timer interrupt?
- How do you implement open-drain in software?
- What is the maximum achievable speed?
The Interview Questions They’ll Ask
- “When would you choose bit-banging over hardware?”
- “How do you handle timing when interrupts can fire?”
- “What is the CPU cost of bit-banged I2C vs hardware?”
- “How would you make bit-banging work at higher speeds?”
Hints in Layers
Hint 1: Measure your delay loop or NOP cycles against an oscilloscope.
Hint 2: For I2C open-drain, set pin to input for high, output-low for low.
Hint 3: Disable interrupts during timing-critical sections.
Hint 4: Consider using inline assembly for the inner loops.
Project 20: Bus Topology Design and Level Shifting
What you will build: A multi-voltage embedded system with proper level shifting between 3.3V and 5V domains, demonstrating best practices for bus topology.
Why it teaches the concept: Real systems mix voltage domains. Understanding level shifting, signal integrity, and topology design is essential for production hardware.
Core Challenges
- Design level shifters for I2C (bidirectional)
- Design level shifters for SPI (unidirectional)
- Calculate and implement proper pull-ups
- Handle different ground references
- Verify signal integrity at the target speed
Key Concepts
| Concept | Book Reference |
|---|---|
| Level shifting | “The Art of Electronics” Ch. 1.6 |
| Signal integrity | “High-Speed Digital Design” Ch. 2-3 |
| Grounding | “The Art of Electronics” Ch. 11 |
Difficulty & Time Estimate
- Difficulty: Expert
- Time: 4-6 weeks
- Prerequisites: All previous projects
Real-World Outcome
Multi-Voltage Bus Design
========================
System Configuration:
MCU: 3.3V (STM32)
Sensors: 3.3V (BME280, MPU6050)
Display: 3.3V (SSD1306 OLED)
Legacy ADC: 5V (MCP3008)
Legacy UART: RS-232 (PC serial port)
Level Shifters:
I2C (3.3V MCU to 5V domain):
Type: MOSFET bidirectional
Parts: BSS138 x2 + pull-ups
Pull-ups: 4.7K on each side
Tested: 400 kHz OK, rise time 450ns
SPI (3.3V MCU to 5V ADC):
MOSI: Direct (5V tolerant input)
MISO: Resistor divider (10K/20K)
SCK: Direct
CS: Direct
Tested: 1 MHz OK
UART (3.3V MCU to RS-232):
Type: MAX3232 transceiver
Tested: 115200 baud OK
Bus Topology:
┌─────────────────────────────────────────────┐
│ │
│ 3.3V Domain 5V Domain │
│ ┌───────┐ ┌───────┐ │
│ │ MCU ├──LEVEL───┤Legacy │ │
│ │ 3.3V │ SHIFT │ ADC │ │
│ └───┬───┘ └───────┘ │
│ │ │
│ ┌───┴───┐ ┌───────┐ ┌───────┐ │
│ │BME280 │ │MPU6050│ │ OLED │ │
│ │ 3.3V │ │ 3.3V │ │ 3.3V │ │
│ └───────┘ └───────┘ └───────┘ │
│ │
└─────────────────────────────────────────────┘
Signal Integrity Report:
All signals within spec at 400 kHz I2C, 1 MHz SPI
Ground loops: None detected
EMI: Minimal (short traces)
The Core Question You’re Answering
How do you design a robust embedded system that operates across multiple voltage domains while maintaining signal integrity and reliability?
Questions to Guide Your Design
- What determines the choice of level shifter topology?
- How do you handle bidirectional signals like I2C SDA?
- What are the speed limitations of different level shifting methods?
The Interview Questions They’ll Ask
- “Explain how a MOSFET level shifter works for I2C.”
- “What are the pros and cons of resistor-divider level shifting?”
- “How do you handle I2C addresses when bridging voltage domains?”
- “What causes ground loops and how do you prevent them?”
Hints in Layers
Hint 1: For I2C, use the classic BSS138 MOSFET circuit.
Hint 2: Check if your 3.3V device has 5V-tolerant inputs before adding level shifters.
Hint 3: Keep level shifters close to the voltage domain boundary.
Hint 4: Use an oscilloscope to verify rise times after adding level shifters.
Project Comparison Table
| # | Project | Protocol | Difficulty | Time | Depth | Key Skill |
|---|---|---|---|---|---|---|
| 1 | UART Loopback | UART | Beginner | 4h | Fundamentals | Timing |
| 2 | Command Terminal | UART | Beg-Int | 8h | Protocol | Parsing |
| 3 | Error Detection | UART | Intermediate | 8h | Debugging | Diagnostics |
| 4 | Logic Analyzer | UART | Int-Adv | 2w | Deep | Capture |
| 5 | Level Converter | UART | Intermediate | 8h | Hardware | Electronics |
| 6 | I2C Scanner | I2C | Beg-Int | 6h | Fundamentals | Addressing |
| 7 | Sensor Network | I2C | Intermediate | 1w | Protocol | Datasheets |
| 8 | EEPROM Logger | I2C | Intermediate | 1w | Storage | Persistence |
| 9 | OLED Driver | I2C | Int-Adv | 2w | Graphics | Displays |
| 10 | Multi-Master | I2C | Advanced | 2w | Advanced | Arbitration |
| 11 | Bus Debugging | I2C | Advanced | 2w | Debugging | Recovery |
| 12 | SPI Modes | SPI | Intermediate | 1w | Fundamentals | Timing |
| 13 | TFT Display | SPI | Int-Adv | 2w | Graphics | Performance |
| 14 | SD Card | SPI | Advanced | 3w | Storage | Filesystem |
| 15 | Flash Memory | SPI | Int-Adv | 2w | Storage | Flash |
| 16 | High-Speed DAQ | SPI | Advanced | 3w | Performance | DMA |
| 17 | Multi-Protocol | All | Expert | 4w | Mastery | Integration |
| 18 | Protocol Bridge | I2C/SPI | Expert | 4w | Translation | Architecture |
| 19 | Bit-Banging | All | Advanced | 3w | Deep | Software |
| 20 | Bus Topology | All | Expert | 4w | Hardware | Design |
Summary
| Project | Path Recommendation | Expected Outcome |
|---|---|---|
| 1 | Start here | UART basics verified |
| 2 | After 1 | Interactive command interface |
| 3 | After 2 | Error detection and handling |
| 4 | After 3 | Build your own debug tool |
| 5 | After 1-3 | Voltage domain understanding |
| 6 | After UART section | I2C fundamentals |
| 7 | After 6 | Multi-device I2C |
| 8 | After 7 | Persistent storage |
| 9 | After 7 | Display interfaces |
| 10 | After 6-9 | Advanced I2C |
| 11 | After 10 | Bus troubleshooting |
| 12 | After I2C section | SPI fundamentals |
| 13 | After 12 | High-speed displays |
| 14 | After 12-13 | Filesystem access |
| 15 | After 12 | Flash storage |
| 16 | After 14-15 | Maximum throughput |
| 17 | After all protocol sections | Universal debug tool |
| 18 | After 17 | Protocol translation |
| 19 | After 17 | Deep understanding |
| 20 | Capstone | Production-ready design |
References
Primary Books
| Book | Author | Relevant Chapters |
|---|---|---|
| “The Book of I2C” | Randall Hyde | Ch. 1-6 (all I2C projects) |
| “Making Embedded Systems” (2nd Ed) | Elecia White | Ch. 5-9 (architecture, protocols) |
| “The Art of Electronics” (3rd Ed) | Horowitz & Hill | Ch. 1, 9, 11, 12, 13 |
| “High-Speed Digital Design” | Johnson & Graham | Ch. 1-3 (signal integrity) |
| “Exploring BeagleBone” | Derek Molloy | Ch. 8 (SPI examples) |
| “The Definitive Guide to ARM Cortex-M0” | Joseph Yiu | Ch. 12 (bit-banging) |
Datasheets (Essential Reading)
- UART: 16550 UART datasheet, FT232 datasheet
- I2C: I2C-bus specification v6 (NXP), TMP102/BME280/MPU6050/SSD1306 datasheets
- SPI: SPI Block Guide (Motorola), W25Q32/ILI9341/SD card specifications
Online Resources
- I2C specification: https://www.nxp.com/docs/en/user-guide/UM10204.pdf
- SD card specification: https://www.sdcard.org/downloads/pls/
- Arduino Wire/SPI library source code
- Raspberry Pi I2C/SPI documentation
Final Words
Serial protocols are the common language of embedded systems. Every sensor, every display, every memory chip speaks one of these three protocols. By completing these projects, you will not just know how to use libraries - you will understand what the libraries do, why they do it, and how to debug when things go wrong.
The progression from UART to I2C to SPI mirrors the evolution of embedded communication: from simple point-to-point to addressed buses to high-speed parallel transfers. Each protocol teaches lessons that apply to all digital communication.
When you finish, you will be able to:
- Read an oscilloscope trace and understand exactly what is happening
- Debug protocol problems that stump other engineers
- Design mixed-protocol systems with proper level shifting
- Implement protocols from scratch when needed
- Make informed architectural decisions about communication
The skills you build here transfer to every embedded project you will ever work on. Start with Project 1, build systematically, and soon these protocols will feel like old friends.
Last updated: 2025-01 Total projects: 20 | Estimated completion: 4-6 months | Protocols covered: UART, I2C, SPI