Project 3: USB HID Keyboard Emulator

Build a USB device that enumerates as a keyboard and types scripted key sequences reliably.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 1-2 weeks
Main Programming Language C (TinyUSB)
Alternative Programming Languages Rust (Embassy), MicroPython
Coolness Level Level 4: “USB wizard”
Business Potential 3. Great for automation tools and test rigs
Prerequisites Clock setup, interrupts, basic USB knowledge
Key Topics USB enumeration, HID descriptors, endpoint timing

1. Learning Objectives

By completing this project, you will:

  1. Build a USB device descriptor stack (device, config, HID report).
  2. Implement HID report generation for key press and release events.
  3. Maintain USB timing by keeping a 48 MHz clock and low ISR latency.
  4. Debug enumeration problems using logs and descriptor validators.
  5. Create a scripted typing demo with repeatable output.

2. All Theory Needed (Per-Concept Breakdown)

2.1 USB Enumeration and Descriptors

Fundamentals

USB hosts decide what a device is by reading descriptors during enumeration. The device must respond to setup requests on endpoint 0 and provide a consistent descriptor tree (device, configuration, interface, HID, endpoints). If any descriptor is malformed, the host rejects the device.

Deep Dive into the concept

Enumeration is a strict handshake. After reset, the host requests the device descriptor, assigns an address, then requests configuration descriptors. The device must respond within tight timing windows. Descriptors are not just metadata; they are the contract with the host’s driver stack. For HID, you provide a report descriptor that defines the structure of your input reports (modifier bits, keycodes, etc.). The OS uses this to parse raw bytes into key events. If your report descriptor is inconsistent with the data you send, keys appear wrong or not at all. The device must also implement standard requests like GET_DESCRIPTOR, SET_CONFIGURATION, and class-specific HID requests.

How this fits on projects

This is the core of §3.2 and §5.10 Phase 1. Also used in: P10-usb-host-mode-keyboardmouse-reader.md.

Definitions & key terms

  • Descriptor -> structured metadata about a USB device
  • Endpoint 0 -> control endpoint for setup requests
  • HID report descriptor -> defines report format for input data

Mental model diagram (ASCII)

Host -> GET_DESCRIPTOR -> Device
Host -> SET_ADDRESS -> Device
Host -> SET_CONFIGURATION -> Device

How it works (step-by-step)

  1. Host resets device and reads device descriptor.
  2. Host sets address.
  3. Host reads configuration + HID descriptors.
  4. Host sets configuration, enabling endpoints.

Minimal concrete example

// HID report: modifier + keycode
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)

Common misconceptions

  • “Descriptors are optional” -> they are mandatory for enumeration.
  • “Any HID report works” -> host expects exact format.

Check-your-understanding questions

  1. Why is endpoint 0 special?
  2. What does the HID report descriptor define?

Check-your-understanding answers

  1. It handles control requests required for enumeration.
  2. The byte layout and meaning of HID reports.

Real-world applications

  • USB keyboards, mice, barcode scanners

Where you’ll apply it

References

  • USB 2.0 specification (Chapter 9)
  • HID Usage Tables

Key insights

  • USB is a strict contract; descriptors are the contract text.

Summary

Enumeration succeeds only if your descriptors are correct and timely.

Homework/Exercises to practice the concept

  1. Build a minimal device descriptor and explain each field.
  2. Use a USB descriptor parser on your firmware output.

Solutions to the homework/exercises

  1. Show VID/PID, class code, max packet size, etc.
  2. Parser confirms lengths and class IDs.

2.2 USB Clocking and ISR Latency

Fundamentals

USB requires a stable 48 MHz clock. If the clock drifts or if interrupts are blocked for too long, the device misses SOF frames or control requests, leading to disconnects.

Deep Dive into the concept

USB full-speed devices expect a 1 ms frame timing. The host sends SOF (start of frame) packets. Your firmware must service USB interrupts promptly. If you disable interrupts for too long (e.g., during a long memcpy or flash operation), the device will miss packets. On RP2040/RP2350, USB requires the dedicated USB clock domain; you must configure PLLs to produce an accurate 48 MHz. Any error here causes enumeration failure on stricter hosts.

How this fits on projects

This drives §3.3 and §5.10 Phase 2. Also used in: P10-usb-host-mode-keyboardmouse-reader.md.

Definitions & key terms

  • SOF -> start of frame packet
  • ISR latency -> time between interrupt event and handler
  • CLK_USB -> USB clock domain

Mental model diagram (ASCII)

XOSC -> PLL_USB -> 48 MHz -> USB controller

How it works (step-by-step)

  1. Configure PLL for 48 MHz.
  2. Enable USB controller.
  3. Keep ISRs short and deterministic.

Minimal concrete example

// pseudo
setup_usb_pll(48e6); enable_usb();

Common misconceptions

  • “USB is tolerant” -> it is not; timing is strict.

Check-your-understanding questions

  1. What happens if you block interrupts for >1 ms?

Check-your-understanding answers

  1. The host may reset or drop the device.

Real-world applications

  • Reliable USB peripherals

Where you’ll apply it

  • In this project: §5.10 Phase 2

References

  • RP2040 datasheet: USB clocking

Key insights

  • USB reliability is a timing problem, not a code-size problem.

Summary

Stable 48 MHz and short ISRs are mandatory for USB success.

Homework/Exercises to practice the concept

  1. Measure PLL output on a pin.
  2. Add logging and measure ISR latency.

Solutions to the homework/exercises

  1. Use clock output mux and scope.
  2. Toggle a pin inside the ISR.

2.3 HID Reports and Key State Machines

Fundamentals

A HID keyboard is a state machine: keys go down, remain pressed, then are released. HID reports encode this state with modifier bits and keycodes. You must send a release report after a press to avoid “stuck keys.”

Deep Dive into the concept

HID keyboards typically use an 8-byte report: 1 byte for modifiers (Ctrl/Alt/Shift), 1 byte reserved, and 6 bytes for keycodes. When you “press” a key, you send a report with that keycode; to release, you send a report with all keycodes set to 0. The host interprets transitions. If you send repeated reports without releases, the OS will behave like the key is held down. A scripted typing engine must manage key down/up with delays and optional key repeat logic. Timing matters: too fast and the host may drop reports; too slow and typing looks laggy.

How this fits on projects

This powers §3.2 and §5.10 Phase 3. Also used in: P10-usb-host-mode-keyboardmouse-reader.md.

Definitions & key terms

  • HID report -> byte structure sent to host
  • Keycode -> HID usage for a specific key
  • Modifier -> Shift/Ctrl/Alt bits

Mental model diagram (ASCII)

Report: [mods][0][key1][key2][key3][key4][key5][key6]

How it works (step-by-step)

  1. Send key down report.
  2. Wait a short delay.
  3. Send all-zero report (release).

Minimal concrete example

hid_report[0] = MOD_LSHIFT;
hid_report[2] = HID_KEY_A; // 'A'

Common misconceptions

  • “One report per character” -> you need press + release.

Check-your-understanding questions

  1. Why do you need a release report?
  2. What happens if multiple keys are pressed?

Check-your-understanding answers

  1. To stop the OS from treating the key as held.
  2. You place multiple keycodes in the report slots.

Real-world applications

  • Macro keyboards, test automation tools

Where you’ll apply it

  • In this project: §3.2, §5.10 Phase 3

References

  • HID Usage Tables

Key insights

  • HID is a state protocol; you must manage state explicitly.

Summary

Correct HID reports are about key state transitions, not just characters.

Homework/Exercises to practice the concept

  1. Create a report sequence for “Hello”.
  2. Implement a key repeat delay.

Solutions to the homework/exercises

  1. Press/release each key with delays.
  2. Use a timer to insert repeat events.

3. Project Specification

3.1 What You Will Build

A USB HID keyboard device that enumerates on Windows/macOS/Linux and types a scripted phrase on demand.

3.2 Functional Requirements

  1. Enumerate as HID keyboard with valid descriptors.
  2. Send key press and release reports.
  3. Provide a serial log of enumeration stages (optional but recommended).
  4. Support a simple script string in firmware.

3.3 Non-Functional Requirements

  • Performance: no dropped reports at 10 ms polling.
  • Reliability: stable enumeration across 3 host OSes.
  • Usability: configuration via a single header file.

3.4 Example Usage / Output

USB HID Keyboard Ready
Typing: HELLO_FROM_RP2040

3.5 Data Formats / Schemas / Protocols

  • HID report: 8 bytes (modifier + keycodes)
  • USB descriptor tree per HID spec

3.6 Edge Cases

  • Host resets device during enumeration.
  • Script includes unsupported characters.
  • Long delay between reports.

3.7 Real World Outcome

Plugging the board into a PC shows a new keyboard device. A scripted phrase appears in a text editor with correct capitalization and punctuation.

3.7.1 How to Run (Copy/Paste)

mkdir -p build
cd build
cmake ..
make -j
picotool load hid_keyboard.uf2 -f

3.7.2 Golden Path Demo (Deterministic)

  1. Open a text editor on the host.
  2. Plug in the board.
  3. The phrase “HELLO_FROM_RP2040” appears exactly once.

3.7.3 If CLI: exact terminal transcript

$ screen /dev/tty.usbmodem14101 115200
[USB] device reset
[USB] enumerated, address=3
[USB] HID ready
[HID] typing: HELLO_FROM_RP2040

Failure Demo (Expected)

[USB] ERROR: invalid report descriptor length
[USB] Device stalled
# Exit code: 3 (descriptor error)

4. Solution Architecture

4.1 High-Level Design

USB Stack -> HID Report Engine -> Script Scheduler

4.2 Key Components

| Component | Responsibility | Key Decisions | |———-|—————–|—————| | Descriptors | Define device identity | Keep minimal HID keyboard | | HID engine | Build reports | Press/release sequence | | Script scheduler | Timing between keys | 10-20 ms delay |

4.3 Data Structures (No Full Code)

struct hid_key_event { uint8_t mods; uint8_t key; uint16_t delay_ms; };

4.4 Algorithm Overview

  1. Enumerate and wait for host ready.
  2. Iterate script events.
  3. Send press report then release report.

Complexity: O(N) events.


5. Implementation Guide

5.1 Development Environment Setup

brew install cmake ninja arm-none-eabi-gcc

5.2 Project Structure

usb-hid-keyboard/
├── CMakeLists.txt
├── src/
│   ├── main.c
│   ├── usb_descriptors.c
│   └── hid_report.c
└── README.md

5.3 The Core Question You’re Answering

“How does a USB host decide what a device is, and how do I convince it?”

5.4 Concepts You Must Understand First

  1. USB descriptors (see §2.1)
  2. USB clocking (see §2.2)
  3. HID report state machine (see §2.3)

5.5 Questions to Guide Your Design

  1. What is the minimal descriptor set for HID?
  2. How do you map ASCII characters to HID keycodes?
  3. How do you handle shift/caps for uppercase letters?

5.6 Thinking Exercise

Write a table mapping A–Z to HID keycodes and modifier flags.

5.7 The Interview Questions They’ll Ask

  1. What happens during enumeration?
  2. Why does the host need a report descriptor?
  3. How do you avoid stuck keys?

5.8 Hints in Layers

  • Hint 1: Start from TinyUSB HID examples.
  • Hint 2: Validate descriptors with a parser.
  • Hint 3: Log every setup request.
  • Hint 4: Test on multiple OSes.

5.9 Books That Will Help

| Topic | Book | Chapter | |——|——|———| | USB fundamentals | USB Complete | Ch. 1-6 | | HID devices | USB Complete | Ch. 11 |

5.10 Implementation Phases

Phase 1: Enumeration ([2-3 days])

  • Implement descriptors and ensure device enumerates.

Phase 2: HID Reports ([3-4 days])

  • Implement press/release reports and simple script.

Phase 3: Reliability ([2-3 days])

  • Test across OSes and handle suspend/resume.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———|———|—————-|———–| | Stack | TinyUSB vs custom | TinyUSB | Fast, reliable | | Polling interval | 1 ms vs 10 ms | 10 ms | Lower CPU load |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———|———|———-| | Enumeration tests | Verify descriptors | OS device manager check | | HID report tests | Correct key outputs | Script output in text editor | | Stress tests | Long runs | 10 minute typing loop |

6.2 Critical Test Cases

  1. Enumeration on Windows/macOS/Linux.
  2. Scripted phrase outputs correctly.
  3. No stuck keys after script ends.

6.3 Test Data

Script: HELLO_FROM_RP2040

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Descriptor length wrong | Device rejected | Recalculate lengths | | Missing release report | Keys stuck | Send zero report | | Clock not 48 MHz | Enumeration fails | Fix PLL settings |

7.2 Debugging Strategies

  • Use lsusb/Device Manager to inspect descriptors.
  • Add verbose logging to control requests.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a button to trigger typing.

8.2 Intermediate Extensions

  • Add media keys (volume/play).

8.3 Advanced Extensions

  • Composite USB device (keyboard + CDC serial).

9. Real-World Connections

9.1 Industry Applications

  • USB automation and hardware-in-the-loop testing
  • TinyUSB: full USB stack used by Pico SDK

9.3 Interview Relevance

  • USB enumeration and HID are common embedded interview topics.

10. Resources

10.1 Essential Reading

  • USB 2.0 spec (Chapter 9)
  • HID usage tables

10.2 Tools & Documentation

  • USB descriptor parser tools
  • Logic analyzer for USB (optional)

11. Self-Assessment Checklist

  • Device enumerates reliably on 3 OSes.
  • Scripted typing output is correct.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • HID keyboard enumerates and types a short phrase.

Full Completion:

  • Works across OSes with clean logs.

Excellence (Going Above & Beyond):

  • Composite device with keyboard + serial.