Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions embassy-usb/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- next-header -->
## Unreleased - ReleaseDate

- Add support for USB HID Boot Protocol Mode

## 0.5.1 - 2025-08-26

## 0.5.0 - 2025-07-16
Expand Down
101 changes: 90 additions & 11 deletions embassy-usb/src/class/hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ use crate::types::InterfaceNumber;
use crate::{Builder, Handler};

const USB_CLASS_HID: u8 = 0x03;
const USB_SUBCLASS_NONE: u8 = 0x00;
const USB_PROTOCOL_NONE: u8 = 0x00;

// HID
const HID_DESC_DESCTYPE_HID: u8 = 0x21;
Expand All @@ -31,6 +29,52 @@ const HID_REQ_SET_REPORT: u8 = 0x09;
const HID_REQ_GET_PROTOCOL: u8 = 0x03;
const HID_REQ_SET_PROTOCOL: u8 = 0x0b;

/// Get/Set Protocol mapping
/// See (7.2.5 and 7.2.6): <https://www.usb.org/sites/default/files/hid1_11.pdf>
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum HidProtocolMode {
/// Hid Boot Protocol Mode
Boot = 0,
/// Hid Report Protocol Mode
Report = 1,
}

impl From<u8> for HidProtocolMode {
fn from(mode: u8) -> HidProtocolMode {
if mode == HidProtocolMode::Boot as u8 {
HidProtocolMode::Boot
} else {
HidProtocolMode::Report
}
}
}

/// USB HID interface subclass values.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum HidSubclass {
/// No subclass, standard HID device.
No = 0,
/// Boot interface subclass, supports BIOS boot protocol.
Boot = 1,
}

/// USB HID protocol values.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum HidBootProtocol {
/// No boot protocol.
None = 0,
/// Keyboard boot protocol.
Keyboard = 1,
/// Mouse boot protocol.
Mouse = 2,
}

/// Configuration for the HID class.
pub struct Config<'d> {
/// HID report descriptor.
Expand All @@ -48,6 +92,12 @@ pub struct Config<'d> {

/// Max packet size for both the IN and OUT endpoints.
pub max_packet_size: u16,

/// The HID subclass of this interface
pub hid_subclass: HidSubclass,

/// The HID boot protocol of this interface
pub hid_boot_protocol: HidBootProtocol,
}

/// Report ID
Expand Down Expand Up @@ -109,10 +159,15 @@ fn build<'d, D: Driver<'d>>(
) -> (Option<D::EndpointOut>, D::EndpointIn, &'d AtomicUsize) {
let len = config.report_descriptor.len();

let mut func = builder.function(USB_CLASS_HID, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE);
let mut func = builder.function(USB_CLASS_HID, config.hid_subclass as u8, config.hid_boot_protocol as u8);
let mut iface = func.interface();
let if_num = iface.interface_number();
let mut alt = iface.alt_setting(USB_CLASS_HID, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE, None);
let mut alt = iface.alt_setting(
USB_CLASS_HID,
config.hid_subclass as u8,
config.hid_boot_protocol as u8,
None,
);

// HID descriptor
alt.descriptor(
Expand Down Expand Up @@ -389,6 +444,23 @@ pub trait RequestHandler {
OutResponse::Rejected
}

/// Gets the current hid protocol.
///
/// Returns `Report` protocol by default.
fn get_protocol(&self) -> HidProtocolMode {
HidProtocolMode::Report
}

/// Sets the current hid protocol to `protocol`.
///
/// Accepts only `Report` protocol by default.
fn set_protocol(&mut self, protocol: HidProtocolMode) -> OutResponse {
match protocol {
HidProtocolMode::Report => OutResponse::Accepted,
HidProtocolMode::Boot => OutResponse::Rejected,
}
}

/// Get the idle rate for `id`.
///
/// If `id` is `None`, get the idle rate for all reports. Returning `None`
Expand Down Expand Up @@ -482,11 +554,14 @@ impl<'d> Handler for Control<'d> {
_ => Some(OutResponse::Rejected),
},
HID_REQ_SET_PROTOCOL => {
if req.value == 1 {
Some(OutResponse::Accepted)
} else {
warn!("HID Boot Protocol is unsupported.");
Some(OutResponse::Rejected) // UNSUPPORTED: Boot Protocol
let hid_protocol = HidProtocolMode::from(req.value as u8);
match (self.request_handler.as_mut(), hid_protocol) {
(Some(request_handler), hid_protocol) => Some(request_handler.set_protocol(hid_protocol)),
(None, HidProtocolMode::Report) => Some(OutResponse::Accepted),
(None, HidProtocolMode::Boot) => {
info!("Received request to switch to Boot protocol mode, but it is disabled by default.");
Some(OutResponse::Rejected)
}
}
}
_ => Some(OutResponse::Rejected),
Expand Down Expand Up @@ -539,8 +614,12 @@ impl<'d> Handler for Control<'d> {
}
}
HID_REQ_GET_PROTOCOL => {
// UNSUPPORTED: Boot Protocol
buf[0] = 1;
if let Some(request_handler) = self.request_handler.as_mut() {
buf[0] = request_handler.get_protocol() as u8;
} else {
// Return `Report` protocol mode by default
buf[0] = HidProtocolMode::Report as u8;
}
Some(InResponse::Accepted(&buf[0..1]))
}
_ => Some(InResponse::Rejected),
Expand Down
58 changes: 46 additions & 12 deletions examples/nrf52840/src/bin/usb_hid_keyboard.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![no_std]
#![no_main]

use core::sync::atomic::{AtomicBool, Ordering};
use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};

use defmt::*;
use embassy_executor::Spawner;
Expand All @@ -13,7 +13,9 @@ use embassy_nrf::usb::Driver;
use embassy_nrf::{bind_interrupts, pac, peripherals, usb};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_usb::class::hid::{HidReaderWriter, ReportId, RequestHandler, State};
use embassy_usb::class::hid::{
HidBootProtocol, HidProtocolMode, HidReaderWriter, HidSubclass, ReportId, RequestHandler, State,
};
use embassy_usb::control::OutResponse;
use embassy_usb::{Builder, Config, Handler};
use usbd_hid::descriptor::{KeyboardReport, SerializedDescriptor};
Expand All @@ -26,6 +28,8 @@ bind_interrupts!(struct Irqs {

static SUSPENDED: AtomicBool = AtomicBool::new(false);

static HID_PROTOCOL_MODE: AtomicU8 = AtomicU8::new(HidProtocolMode::Boot as u8);

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_nrf::init(Default::default());
Expand All @@ -45,6 +49,10 @@ async fn main(_spawner: Spawner) {
config.max_power = 100;
config.max_packet_size_0 = 64;
config.supports_remote_wakeup = true;
config.composite_with_iads = false;
config.device_class = 0;
config.device_sub_class = 0;
config.device_protocol = 0;

// Create embassy-usb DeviceBuilder using the driver and config.
// It needs some buffers for building the descriptors.
Expand Down Expand Up @@ -74,6 +82,8 @@ async fn main(_spawner: Spawner) {
request_handler: None,
poll_ms: 60,
max_packet_size: 64,
hid_subclass: HidSubclass::Boot,
hid_boot_protocol: HidBootProtocol::Keyboard,
};
let hid = HidReaderWriter::<_, 1, 8>::new(&mut builder, &mut state, config);

Expand Down Expand Up @@ -106,6 +116,11 @@ async fn main(_spawner: Spawner) {
if SUSPENDED.load(Ordering::Acquire) {
info!("Triggering remote wakeup");
remote_wakeup.signal(());
} else if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 {
match writer.write(&[0, 0, 4, 0, 0, 0, 0, 0]).await {
Ok(()) => {}
Err(e) => warn!("Failed to send boot report: {:?}", e),
};
} else {
let report = KeyboardReport {
keycodes: [4, 0, 0, 0, 0, 0],
Expand All @@ -121,16 +136,23 @@ async fn main(_spawner: Spawner) {

button.wait_for_high().await;
info!("RELEASED");
let report = KeyboardReport {
keycodes: [0, 0, 0, 0, 0, 0],
leds: 0,
modifier: 0,
reserved: 0,
};
match writer.write_serialize(&report).await {
Ok(()) => {}
Err(e) => warn!("Failed to send report: {:?}", e),
};
if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 {
match writer.write(&[0, 0, 0, 0, 0, 0, 0, 0]).await {
Ok(()) => {}
Err(e) => warn!("Failed to send boot report: {:?}", e),
};
} else {
let report = KeyboardReport {
keycodes: [0, 0, 0, 0, 0, 0],
leds: 0,
modifier: 0,
reserved: 0,
};
match writer.write_serialize(&report).await {
Ok(()) => {}
Err(e) => warn!("Failed to send report: {:?}", e),
};
}
}
};

Expand All @@ -156,6 +178,18 @@ impl RequestHandler for MyRequestHandler {
OutResponse::Accepted
}

fn get_protocol(&self) -> HidProtocolMode {
let protocol = HidProtocolMode::from(HID_PROTOCOL_MODE.load(Ordering::Relaxed));
info!("The current HID protocol mode is: {}", protocol);
protocol
}

fn set_protocol(&mut self, protocol: HidProtocolMode) -> OutResponse {
info!("Switching to HID protocol mode: {}", protocol);
HID_PROTOCOL_MODE.store(protocol as u8, Ordering::Relaxed);
OutResponse::Accepted
}

fn set_idle_ms(&mut self, id: Option<ReportId>, dur: u32) {
info!("Set idle rate for {:?} to {:?}", id, dur);
}
Expand Down
56 changes: 45 additions & 11 deletions examples/nrf52840/src/bin/usb_hid_mouse.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#![no_std]
#![no_main]

use core::sync::atomic::{AtomicU8, Ordering};

use defmt::*;
use embassy_executor::Spawner;
use embassy_futures::join::join;
use embassy_nrf::usb::vbus_detect::HardwareVbusDetect;
use embassy_nrf::usb::Driver;
use embassy_nrf::{bind_interrupts, pac, peripherals, usb};
use embassy_time::Timer;
use embassy_usb::class::hid::{HidWriter, ReportId, RequestHandler, State};
use embassy_usb::class::hid::{
HidBootProtocol, HidProtocolMode, HidSubclass, HidWriter, ReportId, RequestHandler, State,
};
use embassy_usb::control::OutResponse;
use embassy_usb::{Builder, Config};
use usbd_hid::descriptor::{MouseReport, SerializedDescriptor};
Expand All @@ -19,6 +23,8 @@ bind_interrupts!(struct Irqs {
CLOCK_POWER => usb::vbus_detect::InterruptHandler;
});

static HID_PROTOCOL_MODE: AtomicU8 = AtomicU8::new(HidProtocolMode::Boot as u8);

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_nrf::init(Default::default());
Expand All @@ -37,6 +43,10 @@ async fn main(_spawner: Spawner) {
config.serial_number = Some("12345678");
config.max_power = 100;
config.max_packet_size_0 = 64;
config.composite_with_iads = false;
config.device_class = 0;
config.device_sub_class = 0;
config.device_protocol = 0;

// Create embassy-usb DeviceBuilder using the driver and config.
// It needs some buffers for building the descriptors.
Expand All @@ -63,6 +73,8 @@ async fn main(_spawner: Spawner) {
request_handler: Some(&mut request_handler),
poll_ms: 60,
max_packet_size: 8,
hid_subclass: HidSubclass::Boot,
hid_boot_protocol: HidBootProtocol::Mouse,
};

let mut writer = HidWriter::<_, 5>::new(&mut builder, &mut state, config);
Expand All @@ -80,16 +92,26 @@ async fn main(_spawner: Spawner) {
Timer::after_millis(500).await;

y = -y;
let report = MouseReport {
buttons: 0,
x: 0,
y,
wheel: 0,
pan: 0,
};
match writer.write_serialize(&report).await {
Ok(()) => {}
Err(e) => warn!("Failed to send report: {:?}", e),

if HID_PROTOCOL_MODE.load(Ordering::Relaxed) == HidProtocolMode::Boot as u8 {
let buttons = 0u8;
let x = 0i8;
match writer.write(&[buttons, x as u8, y as u8]).await {
Ok(()) => {}
Err(e) => warn!("Failed to send boot report: {:?}", e),
}
} else {
let report = MouseReport {
buttons: 0,
x: 0,
y,
wheel: 0,
pan: 0,
};
match writer.write_serialize(&report).await {
Ok(()) => {}
Err(e) => warn!("Failed to send report: {:?}", e),
}
}
}
};
Expand All @@ -112,6 +134,18 @@ impl RequestHandler for MyRequestHandler {
OutResponse::Accepted
}

fn get_protocol(&self) -> HidProtocolMode {
let protocol = HidProtocolMode::from(HID_PROTOCOL_MODE.load(Ordering::Relaxed));
info!("The current HID protocol mode is: {}", protocol);
protocol
}

fn set_protocol(&mut self, protocol: HidProtocolMode) -> OutResponse {
info!("Switching to HID protocol mode: {}", protocol);
HID_PROTOCOL_MODE.store(protocol as u8, Ordering::Relaxed);
OutResponse::Accepted
}

fn set_idle_ms(&mut self, id: Option<ReportId>, dur: u32) {
info!("Set idle rate for {:?} to {:?}", id, dur);
}
Expand Down
Loading