Skip to main content

hsm_firmware/
main.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//! Mini-HSM firmware entry point.
17//!
18//! Boot sequence:
19//!
20//! 1. Initialise embassy-rp peripherals.
21//! 2. Configure GPIOs:
22//!    - GP15 input with pull-up : touch button (active-low).
23//!    - GP16 output : green status LED.
24//!    - GP17 output : yellow touch-awaiting LED.
25//! 3. Build the USB-HID stack and split it into reader/writer.
26//! 4. Build the ATECC HAL on I2C0 (SCL=GP5, SDA=GP4).
27//! 5. Spawn the auxiliary tasks: USB run, state machine, animation, touch.
28//! 6. Fire `Event::BootComplete` so the state machine leaves the boot state.
29//! 7. Enter the dispatch loop in the main task.
30//!
31//! # `static_cell` instead of `static mut`
32//!
33//! Two pieces of state need to live in `static` storage so the embassy
34//! USB stack can borrow them with a `'static` lifetime: the descriptor
35//! buffers and the HID class state. The idiomatic way to allocate these
36//! without `unsafe` is [`static_cell::StaticCell`], which lets the runtime
37//! check at boot that the storage is initialised exactly once. The price
38//! paid is a one-shot `init` call returning `&'static mut T`; the runtime
39//! cost is a single atomic flag flip.
40
41#![no_std]
42#![no_main]
43
44use defmt::info;
45use defmt_rtt as _;
46use embassy_executor::Spawner;
47use embassy_rp::gpio::{Input, Level, Output, Pull};
48use embassy_time::Instant;
49use embassy_usb::class::hid::State;
50use panic_probe as _;
51use static_cell::StaticCell;
52
53use atecc608b::Atecc;
54use hsm_crypto_service::{Clock, CryptoService};
55use hsm_firmware_logic::Event;
56
57mod animation;
58mod channels;
59mod hal_rp2040;
60mod state;
61mod tasks;
62mod touch;
63mod usb;
64
65use crate::channels::post_event;
66use crate::usb::{build_usb, UsbBuffers};
67
68/// Storage for the USB descriptor / control buffers. The contents are
69/// borrowed by the USB device for the rest of the program. Initialised
70/// once at boot via `init`.
71static USB_BUFFERS: StaticCell<UsbBuffers> = StaticCell::new();
72
73/// Storage for the HID class state. Kept separate from `USB_BUFFERS`
74/// because `State` is invariant over its lifetime parameter, which
75/// forces it to share a single named lifetime with the device borrow.
76static HID_STATE: StaticCell<State<'static>> = StaticCell::new();
77
78/// Clock backed by `embassy_time::Instant`. Provides milliseconds since
79/// boot, monotonic, used by the PIN session timeout.
80pub(crate) struct EmbassyClock;
81
82impl Clock for EmbassyClock
83{
84    fn now_ms(&self) -> u64
85    {
86        Instant::now().as_millis()
87    }
88}
89
90#[embassy_executor::main]
91async fn main(spawner: Spawner)
92{
93    let peripherals = embassy_rp::init(Default::default());
94
95    // GPIOs.
96    // GP15 : touch button. Active-low: switch to ground, internal pull-up
97    // to 3V3. Reads low when pressed. Wrapped in `Rp2040Button` so the
98    // touch task can interact with it through the `Button` trait, which
99    // also lets us test the task logic against a mock host-side.
100    let button = hal_rp2040::Rp2040Button::new(Input::new(peripherals.PIN_15, Pull::Up));
101    // GP16 : green status LED, active-high. Same wrapping rationale as
102    // the button.
103    let led_green = hal_rp2040::Rp2040Led::new(Output::new(peripherals.PIN_16, Level::Low));
104    // GP17 : yellow touch-awaiting LED, active-high.
105    let led_yellow = hal_rp2040::Rp2040Led::new(Output::new(peripherals.PIN_17, Level::Low));
106
107    info!("mini-hsm firmware booted");
108
109    // Initialise the static storage exactly once. `init` returns a
110    // `&'static mut T` borrowed from the cell.
111    let buffers = USB_BUFFERS.init(UsbBuffers::new());
112    let hid_state = HID_STATE.init(State::new());
113
114    let (usb_device, rx, tx) = build_usb(peripherals.USB, buffers, hid_state);
115
116    // Build the ATECC handle on I2C0 (SCL=GP5, SDA=GP4 per the project
117    // schematic). The Peri singletons are moved into the HAL, which
118    // re-borrows them on every transaction or wake pulse.
119    let hal = hal_rp2040::Rp2040Hal::new
120    (
121        peripherals.I2C0,
122        peripherals.PIN_5,
123        peripherals.PIN_4,
124    );
125    let atecc = Atecc::new(hal);
126
127    let service = CryptoService::new(atecc, EmbassyClock);
128
129    // Spawn the auxiliary tasks. `spawner.spawn` returns `()`; the
130    // `#[embassy_executor::task]` macro wraps the task body in a function
131    // returning `Result<SpawnToken, SpawnError>` which we expect here.
132    // `SpawnError` only fires when the task pool is full, which cannot
133    // happen at boot with the queue empty.
134    spawner.spawn
135    (
136        tasks::usb_run_task(usb_device).expect("failed to spawn USB run task"),
137    );
138    spawner.spawn
139    (
140        state::state_task().expect("failed to spawn state machine task"),
141    );
142    spawner.spawn
143    (
144        animation::animation_task(led_green, led_yellow)
145            .expect("failed to spawn animation task"),
146    );
147    spawner.spawn
148    (
149        touch::touch_task(button).expect("failed to spawn touch task"),
150    );
151
152    // Now that every auxiliary task is up, tell the state machine that
153    // boot is complete so it leaves the Booting state.
154    post_event(Event::BootComplete);
155
156    // Enter the dispatch loop. Never returns.
157    tasks::dispatch_loop(rx, tx, service).await
158}