Project 9: The USB-to-UART Bridge (XIAO ESP32S3/C3)

Build a USB CDC-ACM device that bridges data between a PC and a UART-connected target.

Quick Reference

Attribute Value
Difficulty Intermediate
Time Estimate 1-2 weeks
Main Programming Language C (ESP-IDF)
Alternative Programming Languages None recommended (USB stack is C)
Coolness Level High
Business Potential Medium (debug adapters)
Prerequisites UART basics, USB overview, buffering concepts
Key Topics USB CDC-ACM, UART flow control, buffering

1. Learning Objectives

By completing this project, you will:

  1. Explain USB enumeration and CDC-ACM class behavior.
  2. Build a USB serial device recognized by a PC.
  3. Implement bidirectional bridging between USB and UART.
  4. Handle rate mismatch with buffers and flow control.
  5. Debug data loss and connection resets.

2. All Theory Needed (Per-Concept Breakdown)

2.1 Concept 1: USB Enumeration and CDC-ACM

Fundamentals

USB is a host-controlled bus. Devices do not initiate communication; the host enumerates and configures them. CDC-ACM is the USB class that emulates a serial port, commonly used for USB-to-UART adapters. During enumeration, the device presents descriptors that describe its class, endpoints, and capabilities. If these descriptors are incorrect, the host will not recognize the device as a serial port.

Deep Dive into the Concept

When a USB device is connected, the host detects a change in voltage on the data lines and begins enumeration. It resets the device, requests the device descriptor, then configuration descriptors. These descriptors include interface and endpoint definitions. For CDC-ACM, there are typically two interfaces: a control interface and a data interface. The control interface includes class-specific descriptors that identify the ACM capabilities, while the data interface defines bulk IN and bulk OUT endpoints for data transfer.

Enumeration also involves setting the device address and configuration. Only after configuration does the device begin normal operation. The host uses control transfers (endpoint 0) to set line coding, which includes baud rate, parity, and stop bits. For a USB-UART bridge, you must capture these settings and apply them to the UART side. If you ignore line coding, the UART may run at the wrong baud rate.

USB transfers are packetized and scheduled by the host. Full-speed USB has a maximum packet size of 64 bytes for bulk endpoints. This means data arrives in chunks, not streams. Your bridge must handle these packets, store them, and forward to UART. Similarly, data from UART must be packetized into USB IN transfers. If you try to push data faster than the host polls, you will either drop data or block.

The ESP32-S3 includes a native USB controller that can implement CDC-ACM. However, it has constraints: you must use the correct pins and avoid low-power states that disable USB. The ESP32-C3 has limited USB support and may require a different approach; for this project, focus on the ESP32-S3 for full USB CDC.

How this fits on projects

You will configure the USB stack, implement descriptors, and ensure the device enumerates as a serial port. This is the foundation of the bridge.

Definitions & Key Terms

  • Enumeration: Process where host configures the device.
  • Descriptor: Data structure describing device capabilities.
  • CDC-ACM: USB class for serial devices.
  • Endpoint: USB data channel (control, bulk, interrupt).
  • Line coding: USB control message describing serial settings.

Mental Model Diagram (ASCII)

Host -> Enumerate -> Device
Host <-> CDC-ACM endpoints <-> Device

How It Works (Step-by-Step)

  1. Device connects; host resets and requests descriptors.
  2. Device returns CDC-ACM descriptors.
  3. Host sets configuration and line coding.
  4. Bulk endpoints carry data in and out.

Minimal Concrete Example

// Pseudocode: apply line coding to UART
void cdc_line_coding_cb(uint32_t baud, uint8_t parity, uint8_t stopbits) {
    uart_set_baudrate(UART_NUM_0, baud);
}

Common Misconceptions

  • “USB is like UART.” USB is host-driven and packetized.
  • “Descriptors are optional.” Without correct descriptors, enumeration fails.
  • “Baud rate is irrelevant.” It controls the UART side of the bridge.

Check-Your-Understanding Questions

  1. Why is CDC-ACM split into control and data interfaces?
  2. What happens if descriptors are wrong?
  3. Why must line coding be applied to the UART?

Check-Your-Understanding Answers

  1. Control handles class-specific commands; data carries payloads.
  2. The host may not recognize or configure the device.
  3. The UART must match the host’s serial settings.

Real-World Applications

  • USB-to-serial adapters and debugging probes.
  • Embedded devices exposing a serial console over USB.

Where You’ll Apply It

  • See Section 3.2 Functional Requirements and Section 5.10 Phase 1.
  • Also used in: P01 Architectural Blink for register-level understanding.

References

  • USB CDC-ACM specification
  • ESP-IDF USB device docs

Key Insights

USB devices must be described correctly before any data can flow.

Summary

CDC-ACM emulates a serial port using USB descriptors and endpoints. Correct enumeration is the foundation of a reliable bridge.

Homework/Exercises to Practice the Concept

  1. List the descriptors required for a CDC-ACM device.
  2. Explain what happens during USB enumeration.

Solutions to the Homework/Exercises

  1. Device descriptor, configuration, interface, endpoint, CDC class-specific descriptors.
  2. Host resets device, requests descriptors, assigns address, sets configuration.

2.2 Concept 2: UART Flow Control and Buffering

Fundamentals

UART is a simple serial protocol with fixed baud rate. When bridging USB to UART, you must handle differences in speed and buffering. UART flow control (RTS/CTS) allows the receiver to signal when it can accept data. Without flow control, data can be lost if the receiver cannot keep up. Buffers smooth out bursts and allow rate matching between USB and UART.

Deep Dive into the Concept

UART transmits data at a fixed baud rate, e.g., 115200 bps. USB can deliver bursts at much higher rates. If you feed UART directly from USB without buffering, the UART will overflow, dropping bytes. A ring buffer decouples the two domains. When USB packets arrive, you enqueue data into the UART buffer. A UART TX task then drains the buffer at the UART rate. If the buffer fills, you can either drop data or use flow control to pause the sender.

Hardware flow control uses RTS (request to send) and CTS (clear to send) lines. When the UART receive buffer is near full, it deasserts RTS to tell the sender to pause. In a bridge, you can map flow control between USB and UART, but USB itself does not have hardware flow control. Instead, you implement backpressure by slowing USB IN transfers or by not reading data as fast. For CDC-ACM, you can use the serial state notification or just rely on buffering and drop policy.

Choosing buffer sizes is a tradeoff. Large buffers handle bursts but consume RAM. Small buffers reduce RAM usage but risk overflow. For a debugging console, occasional drops might be acceptable; for firmware flashing, drops are not. You can also implement a “high water mark” policy: when the buffer is near full, stop reading USB OUT packets until space is available. This provides backpressure to the host, which will throttle transfers.

The UART side must also handle different baud rates. When the host changes baud rate, you should apply it immediately. If you ignore it, the data will be garbled. For a bridge, you can also choose to support only a fixed baud rate and ignore line coding, but then the host’s configuration is misleading. A robust bridge respects line coding changes.

How this fits on projects

You will implement UART RX/TX buffers and optional flow control, ensuring no data loss during high-speed transfers.

Definitions & Key Terms

  • Baud rate: Symbol rate of UART.
  • RTS/CTS: Hardware flow control signals.
  • Ring buffer: Circular buffer for streaming data.
  • Backpressure: Slowing input when output is saturated.

Mental Model Diagram (ASCII)

USB OUT -> RX buffer -> UART TX
UART RX -> TX buffer -> USB IN

How It Works (Step-by-Step)

  1. USB packet arrives, enqueue in UART TX buffer.
  2. UART task sends bytes at configured baud.
  3. UART RX bytes are enqueued into USB IN buffer.
  4. USB IN endpoint sends packets to host.

Minimal Concrete Example

// Pseudocode
ring_push(uart_tx_buf, usb_data, len);

Common Misconceptions

  • “USB speed means UART speed.” USB is much faster.
  • “Small buffers are fine.” Bursts can overflow quickly.
  • “Flow control is optional.” It is essential for reliability.

Check-Your-Understanding Questions

  1. Why can USB overwhelm UART?
  2. How does RTS/CTS prevent overflow?
  3. What is a high water mark?

Check-Your-Understanding Answers

  1. USB can deliver data much faster than UART can transmit.
  2. It signals the sender to pause when buffers are full.
  3. A buffer threshold that triggers backpressure.

Real-World Applications

  • Serial debug adapters and flashing tools.
  • Industrial serial device gateways.

Where You’ll Apply It

  • See Section 4.2 Key Components and Section 6.2 Critical Test Cases.
  • Also used in: P10 Web Oscilloscope for streaming buffers.

References

  • UART flow control documentation
  • ESP-IDF UART driver docs

Key Insights

Bridging USB to UART is mostly a buffering and flow control problem.

Summary

UART and USB run at different speeds and require buffering and backpressure to prevent data loss. Flow control is the tool that keeps the bridge reliable.

Homework/Exercises to Practice the Concept

  1. Calculate how long it takes to send 1 KB over UART at 115200 bps.
  2. Decide a buffer size for a 64-byte USB packet burst.

Solutions to the Homework/Exercises

  1. 1 KB (1024 bytes) ~ 81,920 bits including framing; ~0.71 seconds.
  2. At least 64 bytes, ideally 256-512 bytes for bursts.

3. Project Specification

3.1 What You Will Build

A USB CDC-ACM device on XIAO ESP32-S3 that bridges data to a UART target. The PC should see a serial port, and data should pass in both directions without loss under normal conditions.

3.2 Functional Requirements

  1. USB Enumeration: Device appears as CDC-ACM serial port.
  2. UART Config: Apply line coding to UART (baud, parity).
  3. Bidirectional Bridge: USB OUT -> UART TX, UART RX -> USB IN.
  4. Buffering: Ring buffers for both directions.
  5. Logging: Report buffer overflow or errors.

3.3 Non-Functional Requirements

  • Reliability: No data loss for sustained 115200 bps UART.
  • Stability: Runs for 30 minutes without disconnect.
  • Usability: Works with common serial terminal tools.

3.4 Example Usage / Output

[USB] CDC-ACM enumerated
[UART] 115200 8N1
[BRIDGE] RX 64 bytes -> UART
[BRIDGE] RX 32 bytes <- UART

3.5 Data Formats / Schemas / Protocols

  • USB CDC-ACM bulk packets (64 bytes max at full speed)
  • UART byte stream

3.6 Edge Cases

  • Host disconnects while UART data pending.
  • UART target resets mid-transfer.
  • Buffer overflow due to bursty USB input.

3.7 Real World Outcome

A PC can open a serial port and communicate with a UART device through the XIAO.

3.7.1 How to Run (Copy/Paste)

idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

3.7.2 Golden Path Demo (Deterministic)

  • Fixed UART baud 115200.
  • Send a known string from PC and verify echo from target.

Expected log:

[BRIDGE] USB->UART: "hello"
[BRIDGE] UART->USB: "hello"

3.7.3 Failure Demo (Buffer Overflow)

E (1234) BRIDGE: UART TX buffer overflow, dropped 32 bytes

3.7.4 If CLI

No standalone CLI. Exit codes not applicable.

3.7.5 If Web App

Not applicable.

3.7.6 If API

No API is exposed. Error JSON shape not applicable.

3.7.7 If GUI / Desktop / Mobile

Not applicable.

3.7.8 If TUI

Not applicable.


4. Solution Architecture

4.1 High-Level Design

USB CDC -> RX buffer -> UART TX
UART RX -> TX buffer -> USB CDC

4.2 Key Components

| Component | Responsibility | Key Decisions | |———-|—————-|—————| | USB stack | Enumeration and CDC endpoints | Use ESP-IDF tinyusb | | UART driver | Configure baud and parity | Apply line coding | | Buffers | Decouple USB/UART | Ring buffers | | Bridge tasks | Move data between buffers | Separate RX/TX tasks |

4.3 Data Structures (No Full Code)

struct ringbuf {
    uint8_t data[512];
    size_t head;
    size_t tail;
};

4.4 Algorithm Overview

Key Algorithm: Data Bridge

  1. On USB OUT packet, enqueue to UART TX buffer.
  2. UART task sends bytes until buffer empty.
  3. UART RX bytes enqueue to USB IN buffer.
  4. USB task sends IN packets when host ready.

Complexity Analysis:

  • Time: O(n) per packet
  • Space: O(buffer size)

5. Implementation Guide

5.1 Development Environment Setup

idf.py set-target esp32s3
idf.py build

5.2 Project Structure

p09_usb_uart_bridge/
+-- main/
|   +-- usb_cdc.c
|   +-- uart_bridge.c
|   +-- ringbuf.c
+-- README.md

5.3 The Core Question You’re Answering

“How does USB become UART in real devices?”

5.4 Concepts You Must Understand First

  1. USB enumeration and CDC-ACM
  2. UART flow control and buffering
  3. Rate matching between interfaces

5.5 Questions to Guide Your Design

  1. How large should your buffers be for bursts?
  2. Will you support RTS/CTS hardware flow control?
  3. How will you handle host disconnects?

5.6 Thinking Exercise

If USB sends data at 12 Mbps but UART is 115200 bps, what must your firmware do?

5.7 The Interview Questions They’ll Ask

  1. What is CDC-ACM?
  2. Why do USB devices need descriptors?
  3. How do you prevent data loss in a bridge?

5.8 Hints in Layers

Hint 1: Start with the ESP-IDF USB CDC example.

Hint 2: Use ring buffers for both directions.

Hint 3: Respect line coding changes.

Hint 4: Log buffer occupancy for debugging.

5.9 Books That Will Help

| Topic | Book | Chapter | |——|——|———| | USB fundamentals | USB Complete | CDC chapters | | Serial protocols | Embedded Systems | UART basics |

5.10 Implementation Phases

Phase 1: USB Enumeration (2 days)

Goals:

  • Enumerate as CDC-ACM.
  • Confirm host sees serial device.

Tasks:

  1. Configure descriptors.
  2. Verify enumeration on PC.

Checkpoint: Serial port appears in OS.

Phase 2: UART Bridge (2 days)

Goals:

  • Bridge data between USB and UART.
  • Implement ring buffers.

Tasks:

  1. Implement buffer and tasks.
  2. Echo test with UART target.

Checkpoint: Data passes both directions.

Phase 3: Robustness (1-2 days)

Goals:

  • Handle disconnects and overflow.
  • Add optional flow control.

Tasks:

  1. Add overflow logging.
  2. Add RTS/CTS if needed.

Checkpoint: Bridge runs for 30 minutes without loss.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | USB stack | TinyUSB vs custom | TinyUSB | Maintained and stable | | Buffer size | 128 vs 512 bytes | 512 bytes | Handles bursts | | Flow control | None vs RTS/CTS | RTS/CTS if available | Prevents overflow |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | Ring buffer correctness | push/pop tests | | Integration Tests | Serial loopback | USB->UART->USB | | Edge Case Tests | Disconnect handling | Unplug USB mid-transfer |

6.2 Critical Test Cases

  1. Enumeration: Device appears as COM/tty port.
  2. Bidirectional: Data passes both directions.
  3. Overflow: Buffer overflow logs without crash.

6.3 Test Data

Test string: "The quick brown fox"

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Wrong descriptors | Device not recognized | Use CDC-ACM example | | No buffering | Data loss | Add ring buffers | | Ignoring line coding | Garbled UART | Apply baud settings |

7.2 Debugging Strategies

  • Use a logic analyzer to verify UART signals.
  • Use OS USB logs to confirm enumeration.

7.3 Performance Traps

  • Large USB bursts without backpressure cause buffer overflow.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add LED indicators for RX/TX.
  • Add a simple throughput counter.

8.2 Intermediate Extensions

  • Support configurable baud rate via USB control.
  • Add a small CLI to set UART pins.

8.3 Advanced Extensions

  • Implement multiple CDC interfaces.
  • Add USB DFU for firmware updates.

9. Real-World Connections

9.1 Industry Applications

  • Debug probes and programming adapters.
  • Industrial gateways between USB and UART devices.
  • TinyUSB - USB device stack.
  • CP2102 drivers - common USB-UART chips.

9.3 Interview Relevance

  • USB enumeration and buffering problems are common embedded interview topics.

10. Resources

10.1 Essential Reading

  • USB CDC-ACM specification
  • ESP-IDF USB device documentation

10.2 Video Resources

  • “USB Enumeration Explained” (lecture)
  • “Building a CDC Device” (tutorial)

10.3 Tools & Documentation

  • usbmon or USBPcap (host capture)
  • ESP-IDF UART driver docs

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain USB enumeration.
  • I can describe CDC-ACM endpoints.
  • I understand UART flow control.

11.2 Implementation

  • Device enumerates as a serial port.
  • Data passes in both directions without loss.
  • Buffer overflow handling is logged.

11.3 Growth

  • I can extend to multi-port CDC.
  • I can explain tradeoffs of buffer sizes.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • USB CDC device enumerates and passes data one direction.

Full Completion:

  • Bidirectional bridge with buffering and stable operation.

Excellence (Going Above & Beyond):

  • Hardware flow control support and throughput benchmarks.
  • DFU or multi-interface USB support.