I'm working with an Adafruit RP2040 Feather ThinkInk in Rust to drive some e-paper panels. The catch with this board is that there is zero hardware debug access: no SWD header, no footprints, and not even underside pads because the 24-pin FPC latch and display circuitry take up all that space. USB is my only way in or out, meaning defmt-rtt is out of the picture and I'm stuck using defmt-bbq and usbd-serial over USB CDC.
The issue is that I have two constraints that absolutely hate each other:
- usb_dev.poll() has to be called every few milliseconds, or the host drops the connection.
- A panel refresh completely blocks the CPU for anywhere from 2 to 20 seconds, with nothing pumping that poll loop.
On top of that, defmt-bbq drops buffered frames if the device isn't configured instead of queuing them, so anything logged before host attachment is lost.
My current workaround is to basically invert the whole process. I run the panel silently, recording timings and events into a fixed-size Report struct. Once the work is done, I bring the USB stack up, wait for UsbDeviceState::Configured, dump everything over serial at once, and then just park in a poll loop forever. You can see the code here: https://github.com/melastmohican/adafruit-feather-thinkink-discovery, specifically in src/usb_report.rs.
It's reliable and handles all eight of my examples without duplicating code, but the trade-off sucks. Because the serial port only enumerates after the run completes, there is zero live progress. If the panel hangs mid-refresh, I get absolutely nothing, which is exactly when I actually need the logs.
I haven't tried a few options yet:
- Pumping poll() from a TIMER interrupt.
- Passing a custom DelayNs implementation into the driver's busy-wait that services USB as a side effect (the driver takes a &mut DELAY there, so it should slot in).
Moving the whole thing to Embassy so the blocking refresh becomes a yield point.
Has anyone run into this kind of bottleneck before (long blocking work on a device where USB CDC is the only logging channel) and found a cleaner pattern? I'm especially curious if setting up the interrupt-driven poll is as straightforward as it sounds or if there's a hidden headache.