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}