HID Device Stack
This section documents XRUSB’s USB HID (Human Interface Device) device-class implementation and extension model, focusing on:
- The templated HID base class (
LibXR::USB::HID) and automatic descriptor generation - Optional Interrupt OUT endpoint support (Output Report over Interrupt OUT)
- Standard
GET_DESCRIPTORhandling for HID / Report descriptors - HID class requests (
GET_REPORT/SET_REPORT/GET_IDLE/SET_IDLE/GET_PROTOCOL/SET_PROTOCOL) and the control-transfer data stage - Input Report transmission (Interrupt IN) and transfer-complete callbacks
- Reference derived classes: mouse (
HIDMouse), keyboard (HIDKeyboard), and gamepad (HIDGamepadT)
Component Overview
LibXR::USB::HID<REPORT_DESC_LEN, TX_REPORT_LEN, RX_REPORT_LEN>
HID is a templated HID base class (inherits from DeviceClass). It fixes report/descriptor sizing at compile time via template parameters, making it straightforward to implement keyboards, mice, gamepads, and similar devices:
REPORT_DESC_LEN: Report Descriptor length (bytes)TX_REPORT_LEN: maximum Input Report length (Interrupt INwMaxPacketSize)RX_REPORT_LEN: Output Report length (Interrupt OUTwMaxPacketSize), default0(unused)
The base class provides:
- Endpoint allocation and configuration (Interrupt IN; optional Interrupt OUT)
- Automatic generation of a configuration-descriptor block containing:
- Interface Descriptor
- HID Descriptor (0x21)
- Endpoint Descriptor(s)
- Standard
GET_DESCRIPTORsupport for HID (0x21) and Report (0x22) descriptors - A framework for HID class requests and the control-transfer data stage (override in derived classes)
- An Input Report helper
SendInputReport()(copies into the endpoint buffer and starts the transfer) - Transfer-complete hooks for both directions (
OnDataInComplete/OnDataOutComplete)
Descriptors and Endpoint Layout
Interface
HID is a single-interface device class:
GetInterfaceNum()returns1HasIAD()returnsfalsebInterfaceClass = 0x03(HID)bNumEndpoints = 1(IN only) or2(IN + OUT)
Note: The current base implementation fills
bInterfaceSubClassandbInterfaceProtocolas0.
For strict HID Boot Keyboard/Mouse behavior, the interface descriptor is typically set as:
- Boot Keyboard:
bInterfaceSubClass=1,bInterfaceProtocol=1- Boot Mouse:
bInterfaceSubClass=1,bInterfaceProtocol=2You can customize this either by adjusting the base class descriptor population or by providing an upper-layer wrapper that modifies the interface descriptor fields.
HID Descriptor (0x21)
The base class generates a 9-byte HID class descriptor with key fields:
bcdHID = 0x0111(HID v1.11)bNumDescriptors = 1bReportDescriptorType = 0x22wReportDescriptorLength = REPORT_DESC_LEN
Endpoints
During Init(), the base class acquires endpoints from EndpointPool and configures them:
| Endpoint | Direction | Type | wMaxPacketSize | Purpose |
|---|---|---|---|---|
| IN endpoint | IN | INTERRUPT | TX_REPORT_LEN | Device → host Input Report |
| OUT endpoint (optional) | OUT | INTERRUPT | RX_REPORT_LEN | Host → device Output Report |
Polling intervals:
in_ep_interval_: written into IN endpoint descriptorbIntervalout_ep_interval_: written into OUT endpoint descriptorbInterval
Note:
bIntervalsemantics vary by speed (FS frame-based vs HS microframe encoding).
This stack treats the parameter as “milliseconds”, and the final behavior depends on the underlying controller/stack interpretation.
Initialization and Resource Release
Init (HID::Init(endpoint_pool, start_itf_num))
Key steps:
- Record
itf_num_ = start_itf_num, clear endpoint pointers, and resetinited_ - Acquire Interrupt IN endpoint from
EndpointPoolandConfigure({IN, INTERRUPT, TX_REPORT_LEN}) - If enabled, acquire Interrupt OUT endpoint and
Configure({OUT, INTERRUPT, RX_REPORT_LEN}) - Populate the configuration-descriptor block:
- Interface Descriptor
- HID Descriptor (0x21)
- Endpoint IN Descriptor
- (optional) Endpoint OUT Descriptor
- Publish the descriptor block via
SetData(RawData{...})for the device framework to stitch into the Configuration Descriptor - Register transfer-complete callbacks:
ep_in_->SetOnTransferCompleteCallback(on_data_in_complete_cb_)- (optional)
ep_out_->SetOnTransferCompleteCallback(on_data_out_complete_cb_)
- If OUT is enabled, start the first OUT receive:
ep_out_->Transfer(RX_REPORT_LEN)(then re-armed automatically) - Set
inited_ = true
Deinit (HID::Deinit(endpoint_pool))
- Set
inited_ = false - Close and release IN/OUT endpoints back to
EndpointPool - Null out endpoint pointers
Standard Request: GET_DESCRIPTOR (HID / Report)
HID overrides OnGetDescriptor() to handle standard GET_DESCRIPTOR:
- If
(wValue >> 8) == 0x21: return HID Descriptor (GetHIDDesc()) - If
(wValue >> 8) == 0x22: return Report Descriptor (GetReportDesc()) - Other types (e.g., Physical 0x23): return
ErrorCode::NOT_SUPPORT
Returned data is truncated to wLength.
Derived classes must implement:
virtual ConstRawData GetReportDesc() = 0;
to provide the Report Descriptor pointer and length.
HID Class Requests and Control-Transfer Data Stage
The base class implements common HID class requests in OnClassRequest():
GET_REPORT: uses the high byte ofwValueto selectINPUT/OUTPUT/FEATURE, then calls:OnGetInputReport(report_id, result)OnGetLastOutputReport(report_id, result)OnGetFeatureReport(report_id, result)
SET_REPORT: checkswLength != 0, then delegates to:OnSetReport(report_id, result)(typically setsresult.read_datato accept data in the control transfer)- the actual payload is delivered in
OnClassData()and finalized byOnSetReportData(in_isr, data)
GET_IDLE / SET_IDLE: maintains a singleidle_rate_(units of 4 ms); current implementation only supportsreport_id=0GET_PROTOCOL / SET_PROTOCOL: maintainsprotocol_(BOOT / REPORT)
Override points for device-specific logic:
- Report retrieval:
OnGetInputReport()/OnGetLastOutputReport()/OnGetFeatureReport()
- Report setting:
OnSetReport()(setup stage)OnSetReportData()(data stage)
- Custom extensions:
OnCustomClassRequest()/OnCustomClassData()
Data Path: IN/OUT Endpoints and Completion Callbacks
Sending an Input Report: SendInputReport()
SendInputReport(ConstRawData report) sends an Interrupt IN report to the host:
- Validate initialized state and IN endpoint presence
- Validate
report.addr_and0 < report.size_ <= TX_REPORT_LEN - Ensure the IN endpoint state is
IDLE; otherwise returnErrorCode::BUSY - Copy
reportinto the endpoint buffer (ep_in_->GetBuffer()) - Start the transfer via
ep_in_->Transfer(report.size_)
Common return codes (subject to stack definitions):
OK: transfer started successfullyBUSY: endpoint is still transmittingARG_ERR: invalid report sizeNO_BUFF: endpoint buffer insufficientFAILED: not initialized or endpoint invalid
Receiving Output Reports (optional): automatic re-arm
If the OUT endpoint is enabled, the default behavior is:
- After
Init(),ep_out_->Transfer(RX_REPORT_LEN)arms the OUT endpoint - Each OUT completion triggers
OnDataOutCompleteStatic():- Calls the virtual
OnDataOutComplete(in_isr, data) - Immediately re-arms via
ep_out_->Transfer(RX_REPORT_LEN)(continuous reception)
- Calls the virtual
Override:
virtual void OnDataOutComplete(bool in_isr, LibXR::ConstRawData& data);
to process Output Reports (e.g., keyboard LEDs).
IN transfer-complete hook
When an IN transfer completes, the stack calls:
virtual void OnDataInComplete(bool in_isr, LibXR::ConstRawData& data);
Typical uses:
- Dequeue/dispatch the next report in a software TX queue
- Throughput accounting and link monitoring
Reference Derived Classes
HIDMouse: Standard Boot Mouse
- Report Descriptor: standard Boot Mouse
- Input Report length: 4 bytes (Buttons + X + Y + Wheel)
- IN-only, default
bInterval=1 ms
Key APIs:
void Move(uint8_t buttons, int8_t x, int8_t y, int8_t wheel = 0);
void Release();
Move() builds a report and calls SendInputReport().
HIDKeyboard: Standard Boot Keyboard (with LEDs)
- Report Descriptor: standard Boot Keyboard
- Input Report length: 8 bytes (Modifier + Reserved + 6 KeyCodes)
- Optional OUT endpoint (
RX_REPORT_LEN=1) for LED state - Implements the
SET_REPORTcontrol-transfer path (compatible with hosts that deliver LEDs via control endpoint)
Key APIs:
- Send keys:
void PressKey(std::initializer_list<KeyCode> keys, uint8_t mods = Modifier::NONE);
void ReleaseAll();
- Read LED state (bitwise):
bool GetNumLock();
bool GetCapsLock();
bool GetScrollLock();
- LED change callback:
void SetOnLedChangeCallback(LibXR::Callback<bool, bool, bool> cb);
Callback parameters are (NumLock, CapsLock, ScrollLock) and may be triggered by either OUT endpoint reception or the SET_REPORT data stage.
HIDGamepadT: Templated 4-Axis + 8-Button Gamepad
- Input Report is fixed to 9 bytes:
4 × int16 axis + 1 × uint8 buttons - Report Descriptor is a compile-time constant of 50 bytes
- Logical range is templated:
LOG_MIN/LOG_MAX(default0..2047)
- Convenience sending APIs:
ErrorCode Send(int x, int y, int z, int rx, uint8_t buttons);
ErrorCode SendButtons(uint8_t buttons);
ErrorCode SendAxes(int x, int y, int z, int rx);
Provided aliases:
HIDGamepad:0..2047HIDGamepadBipolar:-2048..2047
Usage Examples
Note: The exact mechanism to register class instances depends on your upper-layer
usb_devimplementation. The examples below show construction and typical APIs.
Mouse
#include "hid_mouse.hpp" // adjust to your actual header
LibXR::USB::HIDMouse hid_mouse;
// usb_dev class list: {{&hid_mouse}}
// usb_dev.Init();
// usb_dev.Start();
hid_mouse.Move(LibXR::USB::HIDMouse::LEFT, 10, 0); // move right
hid_mouse.Release();
Keyboard (with LED callback)
#include "hid_keyboard.hpp" // adjust to your actual header
LibXR::USB::HIDKeyboard hid_kbd(true); // enable_out_endpoint=true
hid_kbd.SetOnLedChangeCallback(
LibXR::Callback<bool, bool, bool>(
[](bool in_isr, bool num, bool caps, bool scroll) {
(void)in_isr;
// Update on-board LEDs based on num/caps/scroll
}
)
);
// Send: Shift + A
hid_kbd.PressKey({LibXR::USB::HIDKeyboard::KeyCode::A},
LibXR::USB::HIDKeyboard::Modifier::LEFT_SHIFT);
hid_kbd.ReleaseAll();
Gamepad
#include "hid_gamepad.hpp" // adjust to your actual header
LibXR::USB::HIDGamepad gamepad;
gamepad.Send(1024, 1024, 1024, 1024, LibXR::USB::HIDGamepad::BTN1);
Extension Guidelines (Derived-Class Checklist)
When implementing a new HID device, you typically do the following:
- Provide a Report Descriptor by overriding
GetReportDesc() - Define your Input Report so it does not exceed
TX_REPORT_LEN - (Optional) Implement Output/Feature handling:
- If using control transfers: override
OnSetReport()/OnSetReportData() - If using an Interrupt OUT endpoint: construct with
enable_out_endpoint=trueand overrideOnDataOutComplete()
- If using control transfers: override
If you need continuous transmission (TX queue), implement “send next report” scheduling in OnDataInComplete().