Skip to content

rp2040 host: fix SET_REPORT (and any OUT-data control xfer) sending DATA0 instead of DATA1#3644

Merged
HiFiPhile merged 1 commit into
hathach:masterfrom
bdolgov:fix/rp2040-hcd-set-report-pid
May 16, 2026
Merged

rp2040 host: fix SET_REPORT (and any OUT-data control xfer) sending DATA0 instead of DATA1#3644
HiFiPhile merged 1 commit into
hathach:masterfrom
bdolgov:fix/rp2040-hcd-set-report-pid

Conversation

@bdolgov
Copy link
Copy Markdown
Contributor

@bdolgov bdolgov commented May 15, 2026

(disclaimer: the fix and the explanation were prepared by Claude, but it fixed a problem on my device after upgrading to latest tinyusb)

Summary

In hcd_edpt_xfer() (src/portable/raspberrypi/rp2040/hcd_rp2040.c), ep->next_pid is reset to 1 only when the control-endpoint direction changes between stages. That works for IN-data control transfers (GET_REPORT, GET_DESCRIPTOR, …) where SETUP is OUT and DATA is IN — the direction flips and the data stage correctly goes out as DATA1. It does not work for class requests with an OUT data stage like SET_REPORT, where both SETUP and DATA are OUT (ep_addr == 0x00 for both): the ep_addr != ep->ep_addr check is false, ep->next_pid stays at 0 from hcd_edpt_open(), and bufctrl_prepare16() then sends the data stage as DATA0.

Per USB 2.0 §8.5.3, every stage after SETUP must start with DATA1. A DATA0 OUT in the data stage of a SET_REPORT, when the device expected DATA1, is a protocol violation. Strict devices respond by tearing down the connection.

Repro

  • Host: RP2040 / Pico W running TinyUSB host, current master.
  • Device: Elgato Stream Deck (module-15_32).
  • Steps: enumerate, GET_REPORT 0x06 (serial) and 0x08 (unit info) — both succeed. Then SET_REPORT 0x03 (set-key-color, 32 bytes, OUT data stage).

Failing log (annotated):

[C tinyusb] [0:1] Class Request: 21 09 03 03 00 00 20 00   ← SETUP: SET_REPORT, type=Feature, ID=3, len=32
[C tinyusb] [:1] on EP 00 with 8 bytes: OK                  ← SETUP stage OK (DATA0, HW-forced)
[C tinyusb] [:1] on EP 00 with 32 bytes: OK                 ← DATA stage "OK" but actually went as DATA0
[C tinyusb] [0:1] Control data:
[C tinyusb]   0000:  03 06 08 FF 00 00 00 00 00 00 00 00 00 00 00 00
[C tinyusb]   0010:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[C tinyusb] [0:0:0] USBH Device Removed                     ← device disconnects, no STATUS stage logged
[C tinyusb] [0:0:0] unplugged address = 1

GET_REPORT works because OUT→IN direction change resets next_pid to 1. SET_REPORT (and any other OUT-data class control request) does not.

Fix

Key off "endpoint 0" instead of "direction changed", matching the earlier (working) behavior of this code. The control transfer's data and status stages both always start with DATA1 regardless of whether the direction has changed since the previous stage, so reset ep->next_pid to 1 every time hcd_edpt_xfer is invoked on ep 0.

Interrupt and bulk endpoints take the ep->interrupt_num > 0 branch above and never reach this code, so they are unaffected.

Proof the regression is fixed

Same SET_REPORT call after the patch — DATA stage goes out as DATA1, STATUS stage completes, device stays attached:

[INFO ] [C tinyusb] HID Set Report: id = 3, type = 3, len = 32
[INFO ] [C tinyusb] [0:1] Class Request: 21 09 03 03 00 00 20 00
[INFO ] send_feature_report: enqueued SET_REPORT 0x03 on device 875643
[INFO ] [C tinyusb] [:1] on EP 00 with 8 bytes: OK
[INFO ] [C tinyusb] [:1] on EP 00 with 32 bytes: OK
[INFO ] [C tinyusb] [0:1] Control data:
[INFO ] [C tinyusb]   0000:  03 06 07 FF 00 00 00 00 00 00 00 00 00 00 00 00  |................|
[INFO ] [C tinyusb]   0010:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
[INFO ] [C tinyusb] [:1] on EP 80 with 0 bytes: OK
[INFO ] [C tinyusb] HID Set Report complete
[INFO ] usbdrv_rs_set_report_complete: device=1/0 report_id=0x03 len=32

…ATA0 instead of DATA1

hcd_edpt_xfer() previously reset ep->next_pid to 1 only when the control
endpoint direction changed between stages. That handled IN-data control
transfers (e.g. GET_REPORT, GET_DESCRIPTOR) where SETUP is OUT and DATA is
IN, but not OUT-data class requests like SET_REPORT, where SETUP and DATA
are both OUT and the direction-change check is false. ep->next_pid was
left at 0 from hcd_edpt_open(), so the DATA stage went on the wire as
DATA0 when the device expected DATA1. Strict devices (observed: Elgato
Stream Deck) treat this as a protocol violation and disconnect.

Key off "endpoint 0" instead of "direction changed", restoring the
previous behavior. Interrupt/bulk endpoints take the ep->interrupt_num > 0
branch above and never reach this code, so they are unaffected.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a host-mode bug in the RP2040 HCD where SET_REPORT (and other class control transfers with an OUT data stage) sent the DATA stage as DATA0 instead of DATA1, causing strict devices to disconnect. The previous logic only reset next_pid to 1 when the control endpoint direction flipped between stages, which never happens for OUT-data control transfers since both SETUP and DATA share ep_addr == 0x00.

Changes:

  • Replace the "direction changed" check with an "endpoint 0" check so next_pid is reset to 1 on every control transfer stage.
  • Add a detailed comment explaining the USB 2.0 §8.5.3 requirement and the SET_REPORT failure mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Collaborator

@HiFiPhile HiFiPhile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@HiFiPhile HiFiPhile merged commit 90c0884 into hathach:master May 16, 2026
296 checks passed
@bdolgov bdolgov deleted the fix/rp2040-hcd-set-report-pid branch May 16, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants