Skip to main content

hsm_firmware/
channels.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//! Global communication primitives between firmware tasks.
17//!
18//! The firmware uses three async tasks (USB run loop, dispatch loop,
19//! animation loop) plus the touch and state tasks. They communicate
20//! through three lock-free primitives, all backed by
21//! [`CriticalSectionRawMutex`].
22//!
23//! # Why `CriticalSectionRawMutex` and not `NoopRawMutex`
24//!
25//! The embassy executor used here is single-threaded, so the "no-op" mutex
26//! would be sufficient in terms of actual synchronisation. Unfortunately
27//! `NoopRawMutex` is explicitly not `Sync` (so it cannot be placed in a
28//! `static`), because it cannot guarantee correctness when shared across
29//! threads. `CriticalSectionRawMutex` is `Sync` and pays only the cost of
30//! a brief critical section (interrupts off) per lock, which is fine on
31//! the RP2040.
32//!
33//! - [`EVENT_CHANNEL`] : fan-in queue of [`Event`]s. Every task that wants
34//!   to drive a state transition (dispatch_loop on PIN verified, touch_task
35//!   on press, timer_task on timeout) posts here. The state_task is the
36//!   sole consumer.
37//! - [`TOKEN_STATE`] : last-write-wins signal carrying the current
38//!   [`TokenState`]. The state_task publishes; the animation_task reads.
39//! - [`TOUCH_CONFIRMED`] : pulsed by the state_task each time the SM
40//!   transitions into [`TokenState::Signing`]. The dispatch_loop awaits it
41//!   to know its `Sign` request has been authorised by the user.
42
43use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
44use embassy_sync::channel::Channel;
45use embassy_sync::signal::Signal;
46
47use hsm_firmware_logic::{Event, TokenState};
48
49/// Capacity of the event channel. 16 is generous: in practice the channel
50/// holds 0 or 1 event most of the time. The producer tasks all use
51/// non-blocking `try_send` so an unexpected backlog drops events rather
52/// than stalling the firmware.
53pub(crate) const EVENT_QUEUE_DEPTH: usize = 16;
54
55/// Fan-in queue of state machine events.
56pub(crate) static EVENT_CHANNEL: Channel<CriticalSectionRawMutex, Event, EVENT_QUEUE_DEPTH> =
57    Channel::new();
58
59/// Last-write-wins signal of the current operating state. The animation
60/// task reads this on every frame; the state task republishes on every
61/// transition.
62pub(crate) static TOKEN_STATE: Signal<CriticalSectionRawMutex, TokenState> = Signal::new();
63
64/// Pulsed by the state task when the SM enters [`TokenState::Signing`].
65/// The dispatch loop blocks on it after firing
66/// [`hsm_firmware_logic::Event::SignRequested`] so it can resume signing
67/// only after the user has physically touched the button.
68pub(crate) static TOUCH_CONFIRMED: Signal<CriticalSectionRawMutex, ()> = Signal::new();
69
70/// Fire an event without blocking.
71///
72/// If the channel is somehow full, drops the event. State machine events
73/// are advisory: the SM is conservative on unknown transitions, so a
74/// dropped event at worst delays a transition by one cycle. We log it so
75/// it does not go unnoticed.
76pub(crate) fn post_event(event: Event)
77{
78    if EVENT_CHANNEL.try_send(event).is_err()
79    {
80        defmt::warn!("event channel full, dropping event");
81    }
82}