Skip to main content

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}