hsm_firmware/hal_rp2040.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//! ATECC HAL implementation for the RP2040.
17//!
18//! Backs the [`atecc608b::AteccHal`] trait by `embassy_rp::i2c` for normal
19//! transactions and by a temporary 100 kHz I2C write to address `0x00`
20//! for the ATECC wake token.
21//!
22//! # Wake-token rationale
23//!
24//! The ATECC608B detects a host-driven wake when SDA is held low for at
25//! least `tWLO = 60 us`. There are two ways to produce that waveform:
26//!
27//! 1. **GPIO bit-bang**: reconfigure SDA as an output, drive it low for
28//! the required duration, release.
29//! 2. **I2C wake token at 100 kHz** (what CryptoAuthLib does in
30//! `lib/calib/calib_basic.c::calib_wakeup_i2c` and every
31//! `lib/hal/hal_i2c_*.c::hal_i2c_wake`): drop the bus to 100 kHz so
32//! that the address byte of a regular I2C write to a NACK address
33//! (`0x00`) takes long enough to satisfy `tWLO`, then issue that
34//! write and ignore the inevitable NACK.
35//!
36//! We use the second method. The first one ran into stability problems
37//! during bring-up: reconfiguring SDA between GPIO output and I2C
38//! alternate function created transient transitions that the dormant
39//! chip sometimes mis-interpreted as protocol noise, and led to
40//! intermittent `0x04` HAL errors on subsequent reads. The CryptoAuthLib
41//! method keeps the I2C controller in continuous control of the line
42//! and matches the reference implementation byte-for-byte. Restoring the
43//! bus to 400 kHz is automatic on the next [`Rp2040Hal::build_i2c`] call.
44//!
45//! The post-pulse wait (`tHTSU`, ~4.5 ms before the chip responds to
46//! I2C) is the **driver's** responsibility, not the HAL's:
47//! [`crate::tasks`] indirectly calls `atecc608b::wake::wake`, which
48//! performs `pulse_sda_low` followed by a `delay_us(WAKE_DELAY_US)` of
49//! its own. Keeping the delay in the driver lets us tune it without
50//! recompiling the firmware crate and avoids a duplicated source of
51//! truth.
52//!
53//! # Resource management
54//!
55//! Because the I2C peripheral and the SDA/SCL pins are needed at two
56//! different bus frequencies (100 kHz for the wake token, 400 kHz
57//! otherwise), this HAL owns the [`Peri`] singletons directly rather
58//! than holding a long-lived `I2c` instance. Each transaction creates a
59//! fresh [`I2c`] via [`Peri::reborrow`], performs the operation, and
60//! drops the controller. The peripherals are released for the next
61//! transaction or for the next wake token.
62
63use embassy_rp::gpio::{Input, Output};
64use embassy_rp::i2c::{self, Async, Config as I2cConfig, I2c, InterruptHandler};
65use embassy_rp::peripherals::{I2C0, PIN_4, PIN_5};
66use embassy_rp::{bind_interrupts, Peri};
67use embassy_time::{Duration, Timer};
68
69use atecc608b::AteccHal;
70use hsm_firmware_logic::{Button, Led};
71
72bind_interrupts!(pub(crate) struct Irqs
73{
74 I2C0_IRQ => InterruptHandler<I2C0>;
75});
76
77/// I2C bus frequency for normal command traffic. The ATECC608B supports
78/// up to 1 MHz; we run at the "fast mode" 400 kHz to match the typical
79/// layout constraints of breadboard / 2-layer PCB hardware. Lower if
80/// signal integrity is poor.
81pub(crate) const I2C_FREQ_HZ: u32 = 400_000;
82
83/// I2C bus frequency used **only** for the wake token. CryptoAuthLib
84/// drops to 100 kHz so a single byte time on the bus (~90 us address
85/// phase) exceeds the chip's tWLO of 60 us. At 400 kHz an address byte
86/// is too short to be seen as a wake token, hence the temporary slowdown.
87pub(crate) const WAKE_TOKEN_FREQ_HZ: u32 = 100_000;
88
89/// I2C address used to generate the wake token. CryptoAuthLib writes to
90/// address `0x00` (general call) so the dormant chip NACKs cleanly. Any
91/// address the chip does not respond to would do; sticking to `0x00`
92/// matches the reference implementation.
93pub(crate) const WAKE_TOKEN_ADDR: u8 = 0x00;
94
95/// Filler byte for the wake-token write. RP2040's I2C peripheral refuses
96/// zero-length writes; one filler byte is enough to make the controller
97/// happy, and the byte is never actually clocked out because the address
98/// is NACKed.
99pub(crate) const WAKE_TOKEN_FILLER: u8 = 0x00;
100
101/// Error type returned by the RP2040 HAL.
102#[derive(Debug, defmt::Format)]
103pub(crate) enum Rp2040HalError
104{
105 /// An I2C transfer failed (NACK, arbitration loss, abort, etc).
106 I2c(i2c::Error),
107}
108
109impl From<i2c::Error> for Rp2040HalError
110{
111 fn from(err: i2c::Error) -> Self
112 {
113 Rp2040HalError::I2c(err)
114 }
115}
116
117/// ATECC HAL bound to I2C0 on the RP2040.
118///
119/// Hard-wired to SCL=GP5, SDA=GP4 per the project schematic. To use a
120/// different pin pair, change the concrete `PIN_*` types in the struct
121/// fields and the `new` constructor signature.
122pub(crate) struct Rp2040Hal
123{
124 /// Owned I2C0 instance, used by [`Peri::reborrow`] on each
125 /// transaction.
126 i2c_peri: Peri<'static, I2C0>,
127 /// SCL pin (GP5).
128 scl: Peri<'static, PIN_5>,
129 /// SDA pin (GP4). The pin is always driven by the I2C controller,
130 /// at either 400 kHz (normal traffic) or 100 kHz (wake token). It
131 /// is never reconfigured to GPIO output.
132 sda: Peri<'static, PIN_4>,
133}
134
135impl Rp2040Hal
136{
137 /// Build the HAL from the three peripherals.
138 ///
139 /// The caller passes `peripherals.I2C0`, `peripherals.PIN_5` (SCL),
140 /// and `peripherals.PIN_4` (SDA).
141 #[must_use]
142 pub(crate) fn new
143 (
144 i2c_peri: Peri<'static, I2C0>,
145 scl: Peri<'static, PIN_5>,
146 sda: Peri<'static, PIN_4>,
147 ) -> Self
148 {
149 Self { i2c_peri, scl, sda }
150 }
151
152 /// Build a fresh `I2c` for one transaction. The instance is dropped
153 /// when this function returns (or when the caller drops the
154 /// returned `I2c`).
155 fn build_i2c(&mut self) -> I2c<'_, I2C0, Async>
156 {
157 let mut config = I2cConfig::default();
158 config.frequency = I2C_FREQ_HZ;
159 I2c::new_async
160 (
161 self.i2c_peri.reborrow(),
162 self.scl.reborrow(),
163 self.sda.reborrow(),
164 Irqs,
165 config,
166 )
167 }
168}
169
170impl AteccHal for Rp2040Hal
171{
172 type Error = Rp2040HalError;
173
174 async fn i2c_write(&mut self, addr: u8, data: &[u8]) -> Result<(), Self::Error>
175 {
176 let mut i2c = self.build_i2c();
177 i2c.write_async(addr, data.iter().copied()).await?;
178 Ok(())
179 }
180
181 async fn i2c_read(&mut self, addr: u8, buf: &mut [u8]) -> Result<(), Self::Error>
182 {
183 let mut i2c = self.build_i2c();
184 i2c.read_async(addr, buf).await?;
185 Ok(())
186 }
187
188 async fn pulse_sda_low(&mut self, _duration_us: u32) -> Result<(), Self::Error>
189 {
190 // Wake-token method, faithful to CryptoAuthLib
191 // (`lib/calib/calib_basic.c::calib_wakeup_i2c` and
192 // `lib/hal/hal_i2c_*.c::hal_i2c_wake`):
193 //
194 // 1. Drop the bus to 100 kHz so a single I2C byte time exceeds
195 // `tWLO` (60 us, the chip's minimum wake-pulse low time).
196 // 2. Issue a write to a deliberate-NACK address (here `0x00`,
197 // matching CryptoAuthLib). The address phase of that byte at
198 // 100 kHz holds SDA in a pattern that the dormant chip
199 // detects as a wake token. The chip NACKs because it is
200 // asleep and `0x00` is not its address anyway; we ignore
201 // the result.
202 // 3. The post-pulse `tHTSU` wait happens in the driver
203 // (`wake::wake` calls `delay_us(WAKE_DELAY_US)` right after
204 // this returns), so this method only generates the token.
205 //
206 // The `duration_us` argument is ignored: in CryptoAuthLib the
207 // pulse duration is derived from the bus baud rate, not from
208 // an external parameter. We keep the parameter in the trait to
209 // accommodate alternative HALs (e.g. a SoftI2C HAL that does
210 // need to bit-bang the line for a precise duration).
211 //
212 // RP2040 specific: `embassy_rp::i2c` refuses zero-length writes
213 // because its FIFO state machine requires at least one byte to
214 // queue, so we send one filler byte. The chip NACKs during the
215 // address phase, the filler is never clocked out anyway.
216 let mut config = I2cConfig::default();
217 config.frequency = WAKE_TOKEN_FREQ_HZ;
218 let mut i2c = I2c::new_async
219 (
220 self.i2c_peri.reborrow(),
221 self.scl.reborrow(),
222 self.sda.reborrow(),
223 Irqs,
224 config,
225 );
226 // The result is intentionally discarded: a NACK from address
227 // `0x00` is the expected outcome, and any other I2C error here
228 // is also moot since the only point of the call is the
229 // waveform it places on SDA.
230 let _ = i2c.write_async(WAKE_TOKEN_ADDR, [WAKE_TOKEN_FILLER]).await;
231 Ok(())
232 }
233
234 async fn delay_us(&mut self, duration_us: u32)
235 {
236 Timer::after(Duration::from_micros(u64::from(duration_us))).await;
237 }
238
239 async fn delay_ms(&mut self, duration_ms: u32)
240 {
241 Timer::after(Duration::from_millis(u64::from(duration_ms))).await;
242 }
243}
244
245// --- LED and button trait implementations --------------------------------
246//
247// Orphan rules forbid implementing the `Led` / `Button` traits from
248// `hsm_firmware_logic` directly on `embassy_rp::gpio::Output` /
249// `Input`: neither the trait nor the type is local to this crate.
250//
251// We bridge through newtype wrappers. `main.rs` constructs them from
252// the raw embassy types right after configuring the GPIO peripherals,
253// and the task spawners take the wrappers. The newtypes are zero-cost
254// at runtime (single-field structs around `Output<'static>` /
255// `Input<'static>` with no extra padding).
256//
257// Active-high LED wiring assumed: driving the pin high lights the LED.
258// Active-low button wiring assumed: the pin reads low when the user is
259// pressing the button (pull-up to 3V3, switch to ground).
260
261/// Newtype wrapper that bridges `embassy_rp::gpio::Output<'static>` to
262/// the [`Led`] trait defined in `hsm_firmware_logic`.
263pub(crate) struct Rp2040Led
264{
265 pin: Output<'static>,
266}
267
268impl Rp2040Led
269{
270 /// Wrap an existing `Output` as an LED. Constructed in `main.rs`
271 /// from the raw GPIO peripheral after `Output::new`.
272 pub(crate) fn new(pin: Output<'static>) -> Self
273 {
274 Self { pin }
275 }
276}
277
278impl Led for Rp2040Led
279{
280 fn on(&mut self)
281 {
282 self.pin.set_high();
283 }
284
285 fn off(&mut self)
286 {
287 self.pin.set_low();
288 }
289}
290
291/// Newtype wrapper that bridges `embassy_rp::gpio::Input<'static>` to
292/// the [`Button`] trait defined in `hsm_firmware_logic`.
293pub(crate) struct Rp2040Button
294{
295 pin: Input<'static>,
296}
297
298impl Rp2040Button
299{
300 /// Wrap an existing `Input` as a button.
301 pub(crate) fn new(pin: Input<'static>) -> Self
302 {
303 Self { pin }
304 }
305}
306
307impl Button for Rp2040Button
308{
309 fn is_pressed_raw(&self) -> bool
310 {
311 // Active-low: `is_low()` means the user is pressing.
312 self.pin.is_low()
313 }
314}