hsm_firmware/usb.rs
1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! USB-HID device stack.
17//!
18//! Initialises an embassy-usb device that exposes one vendor-defined HID
19//! interface, per the project's USB protocol (see crate `hsm-usb-protocol`).
20//!
21//! The two endpoints are split into a [`embassy_usb::class::hid::HidReader`] (host -> token) and a
22//! [`embassy_usb::class::hid::HidWriter`] (token -> host). They are owned by separate async tasks
23//! in [`crate::tasks`] so reads and writes proceed independently.
24//!
25//! # Lifetime model
26//!
27//! [`embassy_usb::class::hid::State`] is invariant over its lifetime
28//! parameter, which means the `'d` lifetime tag of the [`HidReaderWriter`]
29//! must match exactly the lifetime tag of the `State` it borrows. We
30//! therefore take the `State` as a separate parameter to [`build_usb`]
31//! with its own borrow, and the device lifetime `'d` is derived from
32//! whichever borrow is shortest. In practice both borrows come from
33//! `static` storage at the call site, so `'d = 'static`.
34
35use embassy_rp::peripherals::USB;
36use embassy_rp::usb::{Driver, InterruptHandler};
37use embassy_rp::{bind_interrupts, Peri};
38use embassy_usb::class::hid::{
39 Config as HidConfig, HidBootProtocol, HidReaderWriter, HidSubclass, State,
40};
41use embassy_usb::{Builder, Config, UsbDevice};
42
43use hsm_usb_protocol::{HID_REPORT_DESCRIPTOR, HID_REPORT_SIZE, USB_PID, USB_VID};
44
45bind_interrupts!(pub(crate) struct Irqs
46{
47 USBCTRL_IRQ => InterruptHandler<USB>;
48});
49
50/// Size of the HID report in bytes (matches `HID_REPORT_SIZE`).
51pub(crate) const REPORT_SIZE: usize = HID_REPORT_SIZE;
52
53/// HID reader half: receives host -> token reports.
54pub(crate) type HidRx<'d> = embassy_usb::class::hid::HidReader<'d, Driver<'d, USB>, REPORT_SIZE>;
55
56/// HID writer half: sends token -> host reports.
57pub(crate) type HidTx<'d> = embassy_usb::class::hid::HidWriter<'d, Driver<'d, USB>, REPORT_SIZE>;
58
59/// embassy-usb device handle. Spawn its `run` future to keep the USB stack
60/// alive.
61pub(crate) type UsbStack<'d> = UsbDevice<'d, Driver<'d, USB>>;
62
63/// Descriptor and control-transfer buffers borrowed by the embassy-usb
64/// builder for the lifetime of the device.
65///
66/// Held in a separate struct so the borrow checker sees one stable address
67/// for each buffer. Storing them as fields of a single `static mut` value
68/// is the idiomatic embassy-rp pattern for systems without an allocator.
69pub(crate) struct UsbBuffers
70{
71 /// USB configuration descriptor buffer.
72 pub(crate) config_descriptor: [u8; 256],
73 /// USB BOS descriptor buffer.
74 pub(crate) bos_descriptor: [u8; 256],
75 /// USB MSOS descriptor buffer (unused, kept for the Builder API).
76 pub(crate) msos_descriptor: [u8; 256],
77 /// Control transfer scratch buffer.
78 pub(crate) control_buf: [u8; 64],
79}
80
81impl UsbBuffers
82{
83 /// Build an empty set of buffers.
84 #[must_use]
85 pub(crate) const fn new() -> Self
86 {
87 Self
88 {
89 config_descriptor: [0; 256],
90 bos_descriptor: [0; 256],
91 msos_descriptor: [0; 256],
92 control_buf: [0; 64],
93 }
94 }
95}
96
97impl Default for UsbBuffers
98{
99 fn default() -> Self
100 {
101 Self::new()
102 }
103}
104
105/// Build the USB device with one HID interface.
106///
107/// `buffers` holds the descriptor and control buffers; `hid_state` holds the
108/// HID class internal state. Both must outlive the returned device,
109/// reader, and writer, so the caller typically allocates them in `static`
110/// storage and passes mutable references with `'static` lifetime.
111///
112/// Returns the device (whose `run` future must be polled forever by a task)
113/// and the split HID reader/writer.
114pub(crate) fn build_usb<'d>
115(
116 usb: Peri<'d, USB>,
117 buffers: &'d mut UsbBuffers,
118 hid_state: &'d mut State<'d>,
119) -> (UsbStack<'d>, HidRx<'d>, HidTx<'d>)
120{
121 let driver = Driver::new(usb, Irqs);
122
123 let mut config = Config::new(USB_VID, USB_PID);
124 config.manufacturer = Some("Ethamin");
125 config.product = Some("mini-HSM");
126 config.serial_number = Some("0001");
127 config.max_power = 100;
128 // 64 bytes is the max for USB full-speed; a 128-byte HID report is sent
129 // as two 64-byte transactions automatically by the stack.
130 config.max_packet_size_0 = 64;
131
132 let mut builder = Builder::new
133 (
134 driver,
135 config,
136 &mut buffers.config_descriptor,
137 &mut buffers.bos_descriptor,
138 &mut buffers.msos_descriptor,
139 &mut buffers.control_buf,
140 );
141
142 let hid_config = HidConfig
143 {
144 report_descriptor: HID_REPORT_DESCRIPTOR,
145 request_handler: None,
146 poll_ms: 10,
147 max_packet_size: 64,
148 hid_subclass: HidSubclass::No,
149 hid_boot_protocol: HidBootProtocol::None,
150 };
151
152 let hid = HidReaderWriter::<_, REPORT_SIZE, REPORT_SIZE>::new
153 (
154 &mut builder,
155 hid_state,
156 hid_config,
157 );
158
159 let device = builder.build();
160 let (reader, writer) = hid.split();
161 (device, reader, writer)
162}