USB Media Controller
Custom STM32F4 composite USB device (CDC + HID consumer control) with FreeRTOS and TinyUSB, plus a C++/WinRT companion app.
- Year
- 2024
- Hardware
- STM32F407
OutcomeWorking desk controller: one USB cable carries a serial debug channel and media-key input at the same time.
The problem
I wanted a physical volume/media control for my desk that behaves like a real device, not a keyboard macro hack — and I wanted to learn USB properly rather than copy a HID example.
Constraints
- One device, two personalities. A serial port for debugging and media keys, on a single plug — which means a genuine composite USB device, not two devices on a hub.
- No dropped input. A rotary encoder at full speed generates interrupts far faster than a naive loop can service.
- Must just work on Windows with no driver install.
What I did
Built a composite USB device with TinyUSB on an STM32F4: CDC for serial, HID consumer control for media. Enumerating as a composite device means Windows binds both interfaces from the built-in HID/CDC class drivers — no INF file, no admin rights.
Handled input in hardware where possible. The rotary encoder is decoded on GPIO interrupts with a state-machine quadrature decoder, and buttons use hardware debouncing so the firmware never sees chatter.
Used DMA for UART so the serial channel doesn’t steal CPU from the USB stack.
Wrote a C++/WinRT companion app that talks to the CDC interface — reader/writer tasks, reconnection handling, and a settings surface.
Trade-offs
- HID reports instead of a custom driver. A custom HID report for volume and play/pause is understood by the OS out of the box; a vendor-specific interface would have needed a driver and an admin install. Less flexibility, zero installation friction.
- Interrupt-driven input over polling. Slightly more complex decoder, but it means the encoder never misses a detent at speed.
What I’d do differently
The first PCB revision had the encoder pull-ups misplaced. It worked — but only because the internal pull-ups masked it, and that’s precisely the kind of thing that bites in a volume build. Now I check every pull-up on the schematic against its datasheet requirement before layout.