usb_tx.h 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. // usb_tx.h -- marshal device->host HID reports onto the TinyUSB "usbd" task.
  2. //
  3. // THE PROBLEM this fixes (see ARCHITECTURE / the usbd-queue-deadlock notes): TinyUSB's tud_task() runs in a
  4. // dedicated high-priority "usbd" FreeRTOS task, but this firmware historically called hid.sendReport() (i.e.
  5. // tud_hid_*_report) straight from the LOW-priority loop() task. When the nRF52 USBD's single shared EasyDMA is
  6. // momentarily busy, that cross-task send takes TinyUSB's usbd_defer_func() path, which does a BLOCKING
  7. // osal_queue_send on the device event queue. Under a comms burst the queue saturates, the loop task blocks
  8. // there, stops feeding the ~8 s watchdog, and the MCU resets (the controller "disconnects").
  9. //
  10. // THE FIX: loop-context code never calls tud_* directly. It enqueues the report here (non-blocking, ISR/PRIMASK
  11. // safe), and the actual sendReport() happens from tud_sof_cb() -- which TinyUSB dispatches ON THE usbd TASK
  12. // once per USB frame (~1 ms). Every tud_* call is therefore back in the single context TinyUSB expects, so the
  13. // blocking-defer path can never stall the loop. Reports are keyed by their Adafruit_USBD_HID* (NOT the HID
  14. // instance index, which is mode-dependent and fragile -- see the "HID instance order vs dynamic mount" note).
  15. #pragma once
  16. #include <stdint.h>
  17. class Adafruit_USBD_HID;
  18. // Queue one HID report for `hid` to be sent from the usbd task. Safe from any context (loop or usbd). The
  19. // payload is copied, so the caller's buffer can be reused immediately (tud_hid_*_report copies into the
  20. // endpoint buffer too, so this preserves the original fire-and-forget semantics). Never blocks: if that
  21. // destination's ring is full the oldest queued report is dropped (same policy as the haptic relay ring) --
  22. // for a 250 Hz gamepad stream the newest state is what matters. `len` is clamped to the 64 B report ceiling.
  23. void usbTxHid(Adafruit_USBD_HID *hid, uint8_t rid, const void *data,
  24. uint16_t len);
  25. // Kept for call-site stability; no longer does anything (the drain runs from loop() via usbTxPump). Call once.
  26. void usbTxBegin(void);
  27. // Drain all pending sends (HID rings + registered non-HID drains). MUST be called from loop() once per
  28. // iteration -- sends run in loop() context, sequentially with the RF poll, so they never interrupt its
  29. // timing-critical RX windows (which is what off-loop drain contexts did, dropping controllers).
  30. void usbTxPump(void);
  31. // Drain pending HID reports (one ready report per destination per call). Internal -- called by usbTxPump().
  32. void usbTxDrain(void);
  33. // Priority-inversion guard: run a loop-context USB TX window (HID/vendor/CDC writes) at the usbd task's
  34. // priority so TinyUSB's dcd DMA claim+start can't be preempted mid-claim (the issue-72 livelock; see
  35. // usb_tx.cpp). Depth-counted; loop task only, always in a balanced pair. usbTxPump() boosts itself; wrap
  36. // direct Serial/usb_web writes that can run at flood rate (e.g. per-relay logging).
  37. void usbTxBoost(void);
  38. void usbTxUnboost(void);
  39. // Register a callback to run every USB frame ON THE usbd TASK (from tud_sof_cb, right after usbTxDrain()).
  40. // This is how senders that DON'T go through Adafruit_USBD_HID get their transmits off the loop task too:
  41. // - mode_xinput flushes its raw custom-class IN endpoints;
  42. // - webusb_config sends the status blob (its usb_web.flush() can otherwise block the loop on a full queue).
  43. // Call once at setup() (before usbTxBegin()). Safe no-op if usb isn't up yet. Capacity is small + fixed.
  44. typedef void (*usbTxDrainFn)(void);
  45. void usbTxRegisterDrain(usbTxDrainFn fn);