Project 3: USB MIDI Controller
Build a class-compliant USB MIDI controller with knobs and buttons that send MIDI CC and note events to any DAW.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (Pico SDK + TinyUSB) |
| Alternative Programming Languages | MicroPython |
| Coolness Level | Level 3: Creative Hardware |
| Business Potential | 3. The “Maker Product” Tier |
| Prerequisites | GPIO basics, ADC reading, serial logging |
| Key Topics | USB enumeration, MIDI protocol, input smoothing, debounce |
1. Learning Objectives
By completing this project, you will:
- Implement USB descriptors for a class-compliant MIDI device.
- Read and smooth analog inputs to reduce jitter.
- Encode MIDI Control Change and Note messages correctly.
- Build a mapping layer from physical controls to MIDI parameters.
- Validate the device on at least two operating systems.
2. All Theory Needed (Per-Concept Breakdown)
2.1 USB Enumeration and Descriptors
Fundamentals
USB devices must describe themselves to the host during enumeration. Descriptors are structured data that define device class, endpoints, and capabilities. Every USB device must support endpoint 0 for control transfers. Class-compliant MIDI devices follow the USB Audio/MIDI class specifications, allowing OS drivers to work without custom software.
Deep Dive into the concept
Enumeration begins when the device connects and pulls up D+ to signal full-speed. The host resets the device, assigns an address, and reads descriptors: device, configuration, interface, endpoint, and class-specific MIDI descriptors. If any descriptor fields are wrong (length, total size, endpoint type), the device may fail to enumerate or appear with the wrong class. For MIDI, you typically define one AudioControl interface and one MIDIStreaming interface. Endpoints are bulk or interrupt endpoints that carry MIDI event packets. TinyUSB abstracts much of this, but you must still provide correct descriptors and strings. The device needs stable IDs (VID/PID) and meaningful product strings for recognition. USB also has power constraints: bus-powered devices must declare max current. If you exceed it, some hubs may refuse power. This project forces you to understand how descriptors translate into OS-visible device types.
How this fits on projects
Descriptors define §3.2 requirements and are tested in §3.7 outcome with lsusb.
Definitions & key terms
- Descriptor -> structured data describing a USB device
- Endpoint 0 -> control endpoint for enumeration
- Class-compliant -> no custom driver needed
Mental model diagram (ASCII)
Host -> Get Descriptor -> Device responds with type/length/value
How it works (step-by-step)
- Device connects and signals presence.
- Host resets and assigns address.
- Host reads descriptors to determine class.
- Host loads class driver.
- Data transfers begin.
Minimal concrete example
const tusb_desc_device_t desc_device = {
.idVendor = 0x2E8A,
.idProduct = 0x000A,
.iProduct = 0x02,
};
Common misconceptions
- “USB is just serial” -> enumeration and descriptors are mandatory.
- “Any PID works” -> mismatched IDs can break class recognition.
Check-your-understanding questions
- What happens if the configuration descriptor length is wrong?
- Why must endpoint 0 always exist?
- Why is class compliance valuable?
Check-your-understanding answers
- The host may reject enumeration or mis-parse endpoints.
- It is required for control transfers like SET_ADDRESS.
- It enables plug-and-play without custom drivers.
Real-world applications
- MIDI controllers, keyboards, audio interfaces, HID devices.
Where you’ll apply it
- In this project: §3.2, §3.7.2, §5.10 Phase 2.
- Also used in: P08-usb-rubber-ducky-clone-security-tool.md.
References
- USB 2.0 specification (descriptor chapters)
- TinyUSB documentation
Key insights
Correct descriptors are the “identity card” of your device.
Summary
USB enumeration is the contract between device and host; your descriptors must be exact.
Homework/Exercises to practice the concept
- Modify product string and verify OS recognizes the new name.
- Toggle between MIDI-only and MIDI+CDC configurations.
Solutions to the homework/exercises
- The device name changes in MIDI device lists.
- Two interfaces appear; CDC provides a serial port alongside MIDI.
2.2 Analog Input Smoothing and Jitter Control
Fundamentals
Potentiometers and sensors produce noisy ADC readings. If you map raw values directly to MIDI, the output jitters. Smoothing uses filters (moving average, exponential) to reduce noise while maintaining responsiveness.
Deep Dive into the concept
ADC noise comes from quantization, electrical noise, and mechanical wobble. MIDI values are 7-bit (0-127), so a small ADC change can cause a noticeable jump. A moving average filter averages the last N samples, reducing noise but adding latency. An exponential moving average (EMA) applies a weight to new samples, balancing responsiveness and stability. Another technique is hysteresis: only send a new MIDI value if the change exceeds a threshold. For example, you might only send when delta >= 2. This prevents high-frequency updates that saturate the USB bus and overwhelm the DAW. You must also consider control rates: if you sample at 1 kHz but send MIDI at 20 Hz, you can aggressively smooth without losing musical responsiveness. The best approach is to decouple sampling from transmission: sample frequently, smooth continuously, and only send when the output changes meaningfully.
How this fits on projects
Smoothing determines §3.2 requirements and is validated in §3.7 outcome when knobs are stable.
Definitions & key terms
- EMA -> exponential moving average
- Hysteresis -> require change above threshold
- Quantization noise -> error from discrete steps
Mental model diagram (ASCII)
Raw ADC: 100 102 101 103 100
Smoothed: 101 101 101 102 101
How it works (step-by-step)
- Read ADC value.
- Update filter state (moving average or EMA).
- Map to 0-127 MIDI value.
- If change exceeds threshold, send MIDI event.
Minimal concrete example
avg = (avg * 7 + raw) / 8; // EMA
uint8_t midi = avg >> 5;
if (abs(midi - last) > 1) send_cc(midi);
Common misconceptions
- “More samples always equals smoother” -> too much smoothing adds lag.
- “Jitter is only electrical” -> mechanical noise also matters.
Check-your-understanding questions
- Why does a moving average add latency?
- How can hysteresis reduce USB bandwidth?
- Why map 12-bit ADC to 7-bit MIDI?
Check-your-understanding answers
- It depends on multiple past samples before changing output.
- It prevents sending tiny updates that don’t matter musically.
- MIDI CC values are 7-bit by standard.
Real-world applications
- MIDI controllers, joystick input, sensor smoothing in robotics.
Where you’ll apply it
- In this project: §3.2, §5.10 Phase 2.
- Also used in: P09-servo-robot-arm-motion-control.md, P12-digital-theremin-touchless-music.md.
References
- “Making Embedded Systems” Ch. 8 (filtering)
Key insights
Smooth inputs and throttle outputs; the music should feel stable, not jittery.
Summary
Filtering and hysteresis turn noisy analog signals into usable MIDI controls.
Homework/Exercises to practice the concept
- Compare EMA alpha values (0.1, 0.2, 0.5) and feel the response.
- Implement threshold-based MIDI send and measure message rate.
Solutions to the homework/exercises
- Lower alpha is smoother but slower; higher alpha is more responsive.
- Message rate drops significantly while perceived control remains stable.
2.3 MIDI Message Format and Mapping
Fundamentals
MIDI is a byte-oriented protocol that encodes musical events: Note On/Off, Control Change, Program Change, etc. Each event includes a status byte and data bytes. MIDI over USB uses 4-byte USB MIDI event packets.
Deep Dive into the concept
A MIDI Control Change message is 3 bytes: 0xBn (status with channel), controller number, and value (0-127). Note On is 0x9n, note, velocity. Note Off is 0x8n. USB MIDI wraps these into 4-byte packets with a cable number and code index. If you send malformed messages, DAWs may ignore them or behave unpredictably. Mapping physical controls to MIDI requires a consistent scheme: decide which knob maps to which CC number, how buttons map to notes, and whether to support channels. You also need to manage state: if a button is momentary, send Note On on press and Note Off on release. For toggles, you might send alternating notes or CC values. The mapping layer is where the hardware becomes musical; your design here defines the user experience.
How this fits on projects
MIDI encoding defines §3.2 requirements and is demonstrated in §3.7 serial logs.
Definitions & key terms
- Status byte -> identifies message type and channel
- Control Change (CC) -> continuous controller message
- Velocity -> intensity of note
Mental model diagram (ASCII)
MIDI CC: [0xB0][CC#][Value]
MIDI Note On: [0x90][Note][Velocity]
How it works (step-by-step)
- Read control input.
- Map to CC/Note number and value.
- Build MIDI event packet.
- Send via TinyUSB.
Minimal concrete example
uint8_t msg[3] = { 0xB0, 7, 64 }; // CC7 volume
Common misconceptions
- “MIDI is audio” -> it is control data, not sound.
- “USB MIDI uses the same wire protocol as DIN” -> USB wraps MIDI in packets.
Check-your-understanding questions
- What is the difference between Note On and Control Change?
- Why is MIDI value limited to 0-127?
- How does USB wrap MIDI messages?
Check-your-understanding answers
- Note On triggers a note; CC changes a continuous parameter.
- MIDI uses 7-bit data bytes by design.
- It encapsulates messages in 4-byte USB MIDI event packets.
Real-world applications
- DAW control surfaces, keyboards, drum pads, synth controllers.
Where you’ll apply it
- In this project: §3.2, §5.10 Phase 2.
- Also used in: P12-digital-theremin-touchless-music.md.
References
- MIDI 1.0 specification (message format)
Key insights
Correct MIDI encoding makes your hardware instantly usable in any DAW.
Summary
MIDI is a compact control protocol; mapping and correctness matter more than speed.
Homework/Exercises to practice the concept
- Create a table mapping 4 knobs to CC numbers and channels.
- Implement Note On/Off for a button and verify in a MIDI monitor.
Solutions to the homework/exercises
- Example: CC1=mod, CC7=volume, CC10=pan, CC74=filter.
- Press -> Note On, release -> Note Off on same note/channel.
3. Project Specification
3.1 What You Will Build
A USB MIDI controller with at least 2 knobs and 2 buttons. It enumerates as a class-compliant device and sends MIDI CC and note events to a DAW.
3.2 Functional Requirements
- USB MIDI enumeration: device appears as MIDI input on host.
- Analog inputs: read knobs via ADC and smooth values.
- Button inputs: debounce and send Note On/Off events.
- Mapping layer: configurable CC numbers and note values.
- Serial diagnostics: optional CDC log of messages.
3.3 Non-Functional Requirements
- Performance: <10 ms input-to-MIDI latency.
- Reliability: no USB disconnects under continuous use.
- Usability: consistent mapping and stable controls.
3.4 Example Usage / Output
[MIDI] CC#7 value=64
[MIDI] NOTE_ON 60 velocity=100
[MIDI] NOTE_OFF 60
3.5 Data Formats / Schemas / Protocols
- MIDI CC: status 0xB0-0xBF, controller, value
- MIDI Note On: status 0x90-0x9F, note, velocity
3.6 Edge Cases
- USB disconnect/reconnect while knobs are moving.
- Button bounce causing double Note On events.
- ADC noise causing rapid CC updates.
3.7 Real World Outcome
The Pico appears as a MIDI device and controls parameters in your DAW.
3.7.1 How to Run (Copy/Paste)
mkdir build && cd build
cmake ..
make -j4
picotool load -f pico_midi.uf2
lsusb | grep Pico
3.7.2 Golden Path Demo (Deterministic)
- Set knob to midpoint (ADC ~2048). Expected CC value = 64.
- Press Button 1 -> Note On 60 velocity 100.
- Release Button 1 -> Note Off 60.
3.7.3 Failure Demo (Bad Input)
- Scenario: ADC pin left floating.
- Expected result: log shows jitter, device clamps changes and prints
[WARN] input unstable.
3.7.4 If CLI: exact terminal transcript
$ aconnect -l
client 20: 'Pico MIDI' [type=kernel]
0 'Pico MIDI 1'
4. Solution Architecture
4.1 High-Level Design
Knobs/Buttons -> Filter/Debounce -> MIDI Mapping -> USB MIDI Endpoint -> Host
4.2 Key Components
| Component | Responsibility | Key Decisions | |———–|—————-|—————| | USB Stack | Enumeration + endpoints | TinyUSB MIDI class | | Input Scan | Read ADC + GPIO | 1 kHz sampling | | Filter | Smooth analog values | EMA + hysteresis | | Mapping | CC/Note mapping | Static table | | Logger | Optional CDC debug | Non-blocking prints |
4.3 Data Structures (No Full Code)
typedef struct {
uint8_t cc_number;
uint8_t last_value;
} knob_t;
4.4 Algorithm Overview
Key Algorithm: Input-to-MIDI Update
- Read inputs and update filters.
- Convert values to MIDI range.
- If value changed > threshold, send MIDI packet.
Complexity Analysis:
- Time: O(N) for N controls
- Space: O(N)
5. Implementation Guide
5.1 Development Environment Setup
# Pico SDK + TinyUSB
export PICO_SDK_PATH=/path/to/pico-sdk
5.2 Project Structure
usb-midi/
├── firmware/
│ ├── main.c
│ ├── usb_descriptors.c
│ ├── inputs.c
│ └── midi.c
└── README.md
5.3 The Core Question You’re Answering
“How do you turn physical controls into a standards-compliant USB device?”
5.4 Concepts You Must Understand First
- USB descriptors and enumeration
- MIDI message structure
- Input filtering and debounce
5.5 Questions to Guide Your Design
- Which CC numbers map to your knobs?
- How will you avoid MIDI spam from jitter?
- Will you support multiple channels?
5.6 Thinking Exercise
Design a mapping table for 4 knobs and 4 buttons for a synth.
5.7 The Interview Questions They’ll Ask
- What is a USB descriptor and why does it matter?
- How do you reduce analog jitter in firmware?
- How does MIDI differ from HID?
5.8 Hints in Layers
Hint 1: Start from TinyUSB MIDI example. Hint 2: Add one knob and send one CC. Hint 3: Add filtering and hysteresis. Hint 4: Add buttons and Note On/Off.
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | USB fundamentals | “Making Embedded Systems” | Ch. 9 | | Input filtering | “Making Embedded Systems” | Ch. 8 | | Protocol design | “Serial Port Complete” | Ch. 2 |
5.10 Implementation Phases
Phase 1: Foundation (2-3 days)
- Get USB MIDI enumeration working.
- Send a fixed MIDI note. Checkpoint: DAW receives a Note On.
Phase 2: Core Functionality (3-5 days)
- Read knobs and buttons; add filtering.
- Implement mapping table. Checkpoint: Controls send stable MIDI events.
Phase 3: Polish & UX (2-3 days)
- Add labels, consistent mapping, and serial diagnostics. Checkpoint: Device feels stable and musical.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | USB class | MIDI vs HID | MIDI | DAW compatibility | | Filtering | EMA vs moving avg | EMA | Simple, low latency | | Mapping | Hardcoded vs config | Hardcoded | Simpler for first version |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | Filter correctness | Step response smoothing | | Integration Tests | USB enumeration | lsusb/aconnect detection | | Edge Case Tests | Jitter and noise | Floating input detection |
6.2 Critical Test Cases
- Knob mid-point -> CC value 64.
- Button press/release -> Note On then Note Off.
- Jitter suppression -> no more than 5 CC messages per second when idle.
6.3 Test Data
ADC raw: 2048 -> MIDI 64
ADC raw: 4095 -> MIDI 127
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution | |———|———|———-| | Wrong descriptors | Device not enumerated | Start from TinyUSB example | | No filtering | CC spam | Add hysteresis threshold | | Button bounce | Double notes | Debounce in software |
7.2 Debugging Strategies
- Use a MIDI monitor on host to verify message format.
- Log raw ADC values to CDC serial for tuning.
7.3 Performance Traps
- Excessive USB writes can stall the main loop.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add an LED that blinks on MIDI activity.
- Add a shift button for alternate CC mappings.
8.2 Intermediate Extensions
- Add velocity sensitivity using pressure sensor.
- Support MIDI channel switching.
8.3 Advanced Extensions
- Add BLE MIDI for wireless control (Pico W).
- Implement configuration over USB CDC.
9. Real-World Connections
9.1 Industry Applications
- Music hardware: MIDI controllers and synthesizers.
- Industrial control: MIDI-style control surfaces.
9.2 Related Open Source Projects
- TinyUSB MIDI examples
- MIDI-OX / DAW MIDI monitors
9.3 Interview Relevance
- USB descriptors and protocol mapping are common firmware topics.
10. Resources
10.1 Essential Reading
- USB MIDI class specification
- “Making Embedded Systems” Ch. 9
10.2 Video Resources
- “MIDI Basics” tutorial series (general)
10.3 Tools & Documentation
- TinyUSB docs
- MIDI monitor (MIDI-OX, aconnect)
10.4 Related Projects in This Series
- P01-blinky-on-steroids-multi-pattern-led-controller.md for debounce
- P08-usb-rubber-ducky-clone-security-tool.md for USB HID comparison
11. Self-Assessment Checklist
11.1 Understanding
- I can explain USB enumeration and descriptors.
- I can encode MIDI CC and Note messages.
- I can filter analog inputs to avoid jitter.
11.2 Implementation
- Device enumerates on at least two OSes.
- Controls are stable and responsive.
- Logs confirm correct message format.
11.3 Growth
- I can describe mapping choices and trade-offs.
- I can extend the controller with more inputs.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Enumerates as MIDI device.
- Sends at least one CC and one Note.
Full Completion:
- Stable filtering and debounce.
- Documented mapping table.
Excellence (Going Above & Beyond):
- Configurable mappings and multiple MIDI channels.
13. Additional Content Rules
13.1 Determinism
Use fixed ADC reference and fixed filter parameters for demos. Log MIDI values with timestamps.
13.2 Outcome Completeness
- Success demo: §3.7.2
- Failure demo: §3.7.3
- CLI exit codes: host test utility returns
0success,2USB open failure,3malformed MIDI packet.
13.3 Cross-Linking
Concept references appear in §2.x and related projects in §10.4.
13.4 No Placeholder Text
All sections are fully specified for this project.