Skip to content

fix #3596: big-endian host support for SETUP packet handling#3597

Merged
HiFiPhile merged 4 commits into
hathach:masterfrom
canokeys:big-endian-for-setup-packets
Apr 24, 2026
Merged

fix #3596: big-endian host support for SETUP packet handling#3597
HiFiPhile merged 4 commits into
hathach:masterfrom
canokeys:big-endian-for-setup-packets

Conversation

@dangfan
Copy link
Copy Markdown
Contributor

@dangfan dangfan commented Apr 13, 2026

Summary

This PR fixes two issues that prevented TinyUSB from working correctly on big-endian hosts (#3596 ).

Problem 1: SETUP packet multi-byte fields not converted

USB wire format is little-endian. dcd_event_setup_received() did a raw memcpy of the SETUP packet into tusb_control_request_t, leaving wValue, wIndex, and wLength in wire byte order on big-endian hosts.

Fix: Apply tu_le16toh() on each field inside dcd_event_setup_received(). This is a no-op on little-endian hosts.

Problem 2: Bit-field structure ordering is implementation-defined

C bit-field allocation order is implementation-defined (C11 §6.7.2.1p11). GCC on big-endian ARM allocates from MSB downward, opposite to the USB spec's LSB-first bit numbering. This caused bmRequestType_bit.{direction,type,recipient} and bmAttributes.{xfer,sync,usage} to parse incorrectly.

Fix: Following Linux kernel style (__LITTLE_ENDIAN_BITFIELD / __BIG_ENDIAN_BITFIELD), define TU_LITTLE_ENDIAN_BITFIELD and TU_BIG_ENDIAN_BITFIELD macros in tusb_compiler.h. Use explicit #ifdef blocks in tusb_types.h to select correct field order. An #error directive ensures new compiler/platform support must explicitly define the appropriate macro.

Changes

  • src/common/tusb_compiler.h: Add TU_LITTLE_ENDIAN_BITFIELD / TU_BIG_ENDIAN_BITFIELD macros (GCC and IAR)
  • src/common/tusb_types.h: Use explicit bit-field order selection for bmAttributes and bmRequestType_bit
  • src/device/dcd.h: Add tu_le16toh() conversion for SETUP packet fields

Testing

Verified on CIU98320B (big-endian ARM Cortex-M0, full-speed USB device) with HID keyboard demo:

  • Device enumeration: ✓
  • HID report descriptor transmission: ✓
  • Keystroke input to host: ✓

dangfan added 3 commits April 13, 2026 08:56
1. Add TU_LITTLE_ENDIAN_BITFIELD / TU_BIG_ENDIAN_BITFIELD macros in
tusb_compiler.h (GCC and IAR), following Linux kernel style.

2. Update bmAttributes (tusb_desc_endpoint_t) and bmRequestType_bit
(tusb_control_request_t) in tusb_types.h to use these macros with explicit
#error fallback if undefined.

3. Add tu_le16toh() conversion in dcd_event_setup_received() for
wValue/wIndex/wLength.

Tested on CIU98320B (big-endian ARM Cortex-M, full-speed HID keyboard).
@github-actions
Copy link
Copy Markdown

MemBrowse Memory Report

Top 1 targets by memory change (%) (out of 2156 targets) View Project Dashboard →

target .text .rodata .data .bss total % diff
raspberrypi_zero/mtp 60,160 → 60,184 (+24) 4,603 → 4,571 (-32) 64,763 → 64,755 (-8) -0.0%

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

This PR aims to make TinyUSB work correctly on big-endian CPUs by (1) converting multi-byte SETUP fields from USB wire (little-endian) to host byte order and (2) making USB bitfield parsing deterministic across endian/compilers.

Changes:

  • Convert SETUP packet wValue/wIndex/wLength to host endianness in dcd_event_setup_received().
  • Add a TU_BITFIELD_ORDER mechanism and use it to select correct bitfield layouts for bmRequestType_bit and endpoint bmAttributes.
  • Introduce new bitfield-order constants in tusb_compiler.h and set TU_BITFIELD_ORDER for some toolchains.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/device/dcd.h Converts SETUP multi-byte fields to host order before dispatching the event.
src/common/tusb_types.h Makes bmRequestType_bit and endpoint bmAttributes bitfields endian-order aware via TU_BITFIELD_ORDER.
src/common/tusb_compiler.h Adds bitfield-order constants and defines TU_BITFIELD_ORDER for some compiler branches.

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

Comment thread src/device/dcd.h
Comment on lines 221 to +226
(void) memcpy(&event.setup_received, setup, sizeof(tusb_control_request_t));
// USB wire format is little-endian. Convert multi-byte fields to host byte order
// so the stack always sees correct values regardless of CPU endianness.
event.setup_received.wValue = tu_le16toh(event.setup_received.wValue);
event.setup_received.wIndex = tu_le16toh(event.setup_received.wIndex);
event.setup_received.wLength = tu_le16toh(event.setup_received.wLength);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

dcd_event_setup_received() now converts wValue/wIndex/wLength to host endianness. There is at least one in-tree caller that still applies tu_le16toh() to these fields (src/device/usbd.c passes tu_le16toh(p_request->wIndex) into tud_descriptor_string_cb()), which will double-swap on big-endian and break string descriptor requests (e.g., langid 0x0409 becomes 0x0904). After this change, the device stack should treat tusb_control_request_t fields as host-endian everywhere and remove any remaining tu_le16toh() uses on setup fields accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines 169 to 177
// Endian conversion use well-known host to network (big endian) naming
// For TI ARM compiler, __BYTE_ORDER__ is not defined for MSP430 but still LE
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ || defined(__MSP430__)
#define TU_BYTE_ORDER TU_LITTLE_ENDIAN
#define TU_BITFIELD_ORDER TU_BITFIELD_LE
#else
#define TU_BYTE_ORDER TU_BIG_ENDIAN
#define TU_BITFIELD_ORDER TU_BITFIELD_BE
#endif
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

TU_BITFIELD_ORDER is only defined in the GCC/TI and IAR compiler branches, but not in the __CCRX__ branch (which is still supported and defines TU_ATTR_BIT_FIELD_ORDER_BEGIN). With the new #error checks in tusb_types.h, CCRX builds will now fail because TU_BITFIELD_ORDER will be undefined. Please define TU_BITFIELD_ORDER for __CCRX__ as well (likely TU_BITFIELD_LE if #pragma bit_order right is intended to enforce LSB-first allocation).

Copilot uses AI. Check for mistakes.
Comment on lines 66 to +71
#define TU_LITTLE_ENDIAN (0x12u)
#define TU_BIG_ENDIAN (0x21u)

#define TU_BITFIELD_LE (0x34u)
#define TU_BITFIELD_BE (0x43u)

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

PR description mentions introducing TU_LITTLE_ENDIAN_BITFIELD / TU_BIG_ENDIAN_BITFIELD macros, but the implementation introduces TU_BITFIELD_LE / TU_BITFIELD_BE and TU_BITFIELD_ORDER instead. Please either align the macro names with the PR description or update the PR description to match the implemented API to avoid confusing downstream ports/users.

Copilot uses AI. Check for mistakes.
@dangfan dangfan marked this pull request as draft April 16, 2026 14:55
@dangfan dangfan marked this pull request as ready for review April 17, 2026 10:43
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.

Thank you !

@HiFiPhile HiFiPhile merged commit 4c7fd70 into hathach:master Apr 24, 2026
290 of 291 checks passed
@hathach
Copy link
Copy Markdown
Owner

hathach commented Apr 25, 2026

Code review (endianness scan, requested post-merge)

Found 3 endian-related issues:

  1. The byte-swap in dcd_event_setup_received corrupts the audio20_control_request_t alias cast on big-endian hosts. After this PR, wValue/wIndex are stored in host byte order, so on BE the bytes that the audio examples reinterpret as bChannelNumber/bControlSelector (former wValue bytes) and bInterface/bEntityID (former wIndex bytes) are now swapped relative to wire data. UAC2 tud_audio_get_req_entity_cb/tud_audio_set_req_entity_cb would dispatch on the wrong entity/selector on BE.

    Conversion site:

    tinyusb/src/device/dcd.h

    Lines 219 to 230 in 4c7fd70

    event.rhport = rhport;
    event.event_id = DCD_EVENT_SETUP_RECEIVED;
    (void) memcpy(&event.setup_received, setup, sizeof(tusb_control_request_t));
    // USB wire format is little-endian. Convert multi-byte fields to host byte order
    // so the stack always sees correct values regardless of CPU endianness.
    event.setup_received.wValue = tu_le16toh(event.setup_received.wValue);
    event.setup_received.wIndex = tu_le16toh(event.setup_received.wIndex);
    event.setup_received.wLength = tu_le16toh(event.setup_received.wLength);
    dcd_event_handler(&event, in_isr);
    }
    // helper to send transfer complete event

    Affected cast (also in uac2_headset/src/main.c L437,L451 and uac2_speaker_fb/src/main.c L426,L440):

    // Invoked when audio class specific get request received for an entity
    bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request) {
    audio20_control_request_t const *request = (audio20_control_request_t const *)p_request;
    if (request->bEntityID == UAC2_ENTITY_CLOCK) {
    return tud_audio_clock_get_request(rhport, request);
    }
    if (request->bEntityID == UAC2_ENTITY_SPK_FEATURE_UNIT) {
    return tud_audio_feature_unit_get_request(rhport, request);
    } else {
    TU_LOG1("Get request not handled, entity = %d, selector = %d, request = %d\r\n",
    request->bEntityID, request->bControlSelector, request->bRequest);
    }
    return false;
    }
    // Invoked when audio class specific set request received for an entity
    bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request, uint8_t *buf) {
    audio20_control_request_t const *request = (audio20_control_request_t const *)p_request;
    if (request->bEntityID == UAC2_ENTITY_SPK_FEATURE_UNIT) {
    return tud_audio_feature_unit_set_request(rhport, request, buf);
    }
    if (request->bEntityID == UAC2_ENTITY_CLOCK) {
    return tud_audio_clock_set_request(rhport, request, buf);
    }
    TU_LOG1("Set request not handled, entity = %d, selector = %d, request = %d\r\n",
    request->bEntityID, request->bControlSelector, request->bRequest);
    return false;
    }

    Note: audio_device.c itself is fine — it uses TU_U16_LOW/HIGH(p_request->wIndex/wValue), which works on both LE and BE after this PR. The bug is isolated to the alias-cast pattern in the user-facing UAC2 examples.

  2. audio10_desc_as_iso_data_ep_t.bmAttributes was missed by the bitfield guards. The PR added TU_BITFIELD_ORDER guards to tusb_desc_endpoint_t.bmAttributes, but the functionally identical xfer:2 / sync:2 / usage:2 / :2 packing in audio.h was left unguarded. It is actively read at runtime to classify isochronous endpoints (is_data_ep = desc_ep_uac1->bmAttributes.sync != TUSB_ISO_EP_ATT_NO_SYNC), so on a BE compiler the multi-bit fields would be reversed within the byte and EP classification would break.

    Unguarded struct:

    /// Standard AS Isochronous Audio Data Endpoint Descriptor UAC1 (4.6.1.1)
    typedef struct TU_ATTR_PACKED {
    uint8_t bLength; ///< Size of this descriptor in bytes: 9.
    uint8_t bDescriptorType; ///< Descriptor Type. Value: TUSB_DESC_ENDPOINT.
    uint8_t bEndpointAddress;///< The address of the endpoint on the USB device described by this descriptor.
    struct TU_ATTR_PACKED {
    uint8_t xfer : 2; // Control, ISO, Bulk, Interrupt
    uint8_t sync : 2; // None, Asynchronous, Adaptive, Synchronous
    uint8_t usage : 2; // Data, Feedback, Implicit feedback
    uint8_t : 2;
    } bmAttributes;
    uint16_t wMaxPacketSize; ///< Maximum packet size this endpoint is capable of sending or receiving when this configuration is selected.
    uint8_t bInterval; ///< Interval for polling endpoint for data transfers.
    uint8_t bRefresh; ///< The rate at which the endpoint is refreshed.
    uint8_t bSynchAddress; ///< The address of the endpoint used to send synchronization information for the data endpoint.
    } audio10_desc_as_iso_data_ep_t;

    Active consumer:

    if (tud_audio_n_version(i) == 1) {
    // UAC1: Use bRefresh field to distinguish endpoint types
    audio10_desc_as_iso_data_ep_t const *desc_ep_uac1 = (audio10_desc_as_iso_data_ep_t const *) p_desc;
    is_data_ep = (desc_ep_uac1->bmAttributes.sync != TUSB_ISO_EP_ATT_NO_SYNC);
    is_feedback_ep = (desc_ep_uac1->bmAttributes.sync == TUSB_ISO_EP_ATT_NO_SYNC);
    } else {
    // UAC2: Use bmAttributes.usage to distinguish endpoint types
    is_data_ep = (desc_ep->bmAttributes.usage == 0 || desc_ep->bmAttributes.usage == 2);
    is_feedback_ep = (desc_ep->bmAttributes.usage == 1);

  3. audio20_control_request_t.bmRequestType_bit was missed by the bitfield guards. Same packing as tusb_control_request_t.bmRequestType_bit which was fixed, but the audio-class duplicate has no TU_BITFIELD_ORDER block. No in-tree consumer reads the bitfield via this alias today, but it is part of the public class header — anyone writing UAC2 callbacks against it gets reversed recipient/type/direction on BE.

    // 5.2.2 Control Request Layout
    typedef struct TU_ATTR_PACKED {
    union {
    struct TU_ATTR_PACKED {
    uint8_t recipient : 5;///< Recipient type tusb_request_recipient_t.
    uint8_t type : 2; ///< Request type tusb_request_type_t.
    uint8_t direction : 1;///< Direction type. tusb_dir_t
    } bmRequestType_bit;
    uint8_t bmRequestType;
    };
    uint8_t bRequest;///< Request type audio_cs_req_t
    uint8_t bChannelNumber;
    uint8_t bControlSelector;
    union {
    uint8_t bInterface;
    uint8_t bEndpoint;
    };
    uint8_t bEntityID;
    uint16_t wLength;
    } audio20_control_request_t;

Out of scope (latent BE issues, but pre-existing and not introduced by this PR): multi-bit SCSI bitfields in src/class/msc/msc.h (scsi_inquiry_resp_t, scsi_mode_sense6_t, scsi_start_stop_unit_t, scsi_sense_fixed_resp_t) and hub_characteristics_t in src/host/hub.h are also unguarded; and dcd_musb.c/dcd_sunxi_musb.c read raw _dcd.setup_packet.wValue/wLength (not the converted event copy) without tu_le16toh. None of this PR's changes made those worse, but they would all need the same treatment for true BE support.

No double-conversion issues found in src/class/* device drivers or the host stack — host side keeps setup packets in wire order with explicit tu_le16toh at read sites, which remains self-consistent.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Copilot AI added a commit that referenced this pull request Apr 25, 2026
…e field extraction

- Add TU_BITFIELD_ORDER guards for audio10_desc_as_iso_data_ep_t.bmAttributes in audio.h
- Add TU_BITFIELD_ORDER guards for audio20_control_request_t.bmRequestType_bit in audio.h
- Fix cdc_uac2/src/uac2_app.c: replace alias cast with TU_U16_LOW/HIGH field extraction
- Fix uac2_headset/src/main.c: replace alias cast with TU_U16_LOW/HIGH field extraction
- Fix uac2_speaker_fb/src/main.c: replace alias cast with TU_U16_LOW/HIGH field extraction

Addresses review comment: #3597 (comment)

Agent-Logs-Url: https://github.com/hathach/tinyusb/sessions/8b695271-74b3-4a26-b3d8-c48ecdf2e481

Co-authored-by: HiFiPhile <4375114+HiFiPhile@users.noreply.github.com>
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.

4 participants