atecc608b/driver.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//! Top-level driver handle.
17//!
18//! The driver is split into two types that together model the chip's
19//! lifecycle explicitly:
20//!
21//! - [`Atecc`] owns the HAL and represents a chip that is asleep (or about
22//! to be: the chip's actual state is unknown until a channel is opened).
23//! It has no command API. Its sole role is to hand out [`AteccChannel`]
24//! instances.
25//! - [`AteccChannel`] is the awake-and-talking handle. All high-level
26//! commands (`Info`, `Sign`, `GenKey`, etc.) live as methods on this type
27//! in the [`crate::command`] modules. They share the same execution
28//! skeleton implemented here as [`AteccChannel::execute_command`] (for
29//! commands returning a data payload) and
30//! [`AteccChannel::execute_command_status`] (for commands that signal
31//! success with a single `0x00` status byte).
32//!
33//! # Lifecycle
34//!
35//! Every command goes through the same dance:
36//!
37//! 1. **Open a channel**: [`Atecc::open_channel`] wakes the chip and
38//! returns an [`AteccChannel`].
39//! 2. **Send** the command frame, prefixed with the command word address.
40//! 3. **Poll** for the response. The driver waits the nominal execution time
41//! for that opcode, then re-reads at fixed intervals until the chip
42//! responds or until the global timeout elapses.
43//! 4. **Verify** the response CRC and parse it into either a payload or a
44//! chip status byte.
45//! 5. **Close the channel**: [`AteccChannel::close`] sends the idle token
46//! so the watchdog does not fire on the next call. The borrow on
47//! [`Atecc`] is released.
48//!
49//! Within one channel the caller may run as many commands as needed (this
50//! is how multi-step workflows like Nonce + Sign or Nonce + `GenDig` + Write
51//! keep `TempKey` alive between steps). The channel must always be closed
52//! explicitly so the chip's state stays in sync with the driver's view.
53//!
54//! # Why a separate `AteccChannel` type
55//!
56//! The ATECC608B requires a wake pulse before any command. After commands
57//! are done, it must be put back to idle (or sleep), otherwise the chip's
58//! internal watchdog (~1.3 s) silently transitions it to sleep without
59//! informing the driver. A subsequent command would then see a NACK because
60//! the driver still believes the chip is awake.
61//!
62//! Modeling "awake" as a separate type tied to a lifetime forces the caller
63//! to acquire a channel for every command sequence and close it when done,
64//! which keeps the chip's state synchronized with the driver's view at
65//! every program point. The borrow checker enforces that you cannot
66//! "forget" to wake, and a `Drop` warning (see [`AteccChannel`]) catches
67//! the case where a channel is dropped without `close()`.
68
69use crate::error::{AteccError, ChipError};
70use crate::hal::AteccHal;
71use crate::opcodes::
72{
73 I2C_ADDRESS,
74 MAX_PACKET_SIZE,
75 MAX_RESPONSE_SIZE,
76 POLLING_MAX_MS,
77 POLLING_PERIOD_MS,
78 WORD_ADDRESS_COMMAND,
79};
80use crate::packet::
81{
82 build_command_frame,
83 parse_response_frame,
84 PacketBuildError,
85 PacketParseError,
86 ResponseFrame,
87};
88use crate::wake::{idle, sleep, wake};
89
90/// Smallest valid response frame: count + status + crc(2).
91const STATUS_RESPONSE_LEN: usize = 4;
92
93/// Driver handle owning the HAL.
94///
95/// Does not, by itself, expose any chip command. Use [`Self::open_channel`]
96/// to wake the chip and obtain an [`AteccChannel`] on which the command
97/// API lives.
98pub struct Atecc<H>
99where
100 H: AteccHal,
101{
102 hal: H,
103 device_addr: u8,
104}
105
106impl<H> Atecc<H>
107where
108 H: AteccHal,
109{
110 /// Build a new driver around an existing HAL, using the chip's default
111 /// I2C address ([`I2C_ADDRESS`]).
112 pub fn new(hal: H) -> Self
113 {
114 Self::with_address(hal, I2C_ADDRESS)
115 }
116
117 /// Build a new driver against a chip with a non-default I2C address.
118 pub(crate) fn with_address(hal: H, addr: u8) -> Self
119 {
120 Self
121 {
122 hal,
123 device_addr: addr,
124 }
125 }
126
127 /// Consume the driver and return the underlying HAL.
128 pub fn into_hal(self) -> H
129 {
130 self.hal
131 }
132
133 /// Open a communication channel with the chip.
134 ///
135 /// Performs the wake sequence and returns an [`AteccChannel`] that
136 /// exposes the typed command API. The channel borrows `self` mutably
137 /// for as long as it exists, which prevents accidentally opening two
138 /// channels concurrently.
139 ///
140 /// The caller is expected to close the channel via
141 /// [`AteccChannel::close`] when finished. Dropping a channel without
142 /// closing leaves the chip awake; its watchdog will eventually idle it,
143 /// but the next [`Self::open_channel`] call may observe a transient
144 /// state. A `defmt::warn!` is emitted in that case to flag the
145 /// protocol violation.
146 ///
147 /// # Errors
148 /// Forwards every variant from [`crate::wake::wake`].
149 pub async fn open_channel(&mut self) -> Result<AteccChannel<'_, H>, AteccError<H::Error>>
150 {
151 wake(&mut self.hal, self.device_addr).await?;
152 Ok(AteccChannel
153 {
154 driver: self,
155 closed: false,
156 })
157 }
158}
159
160/// An open communication channel with a woken chip.
161///
162/// Holds a mutable borrow of the parent [`Atecc`] for the duration of the
163/// channel. All high-level chip commands are exposed as methods on this
164/// type (see the [`crate::command`] modules: `info`, `random`, `sign`, etc.).
165///
166/// # Closing
167///
168/// The channel must be closed via [`Self::close`] when commands are done.
169/// Closing sends the idle token, which preserves volatile state (`TempKey`,
170/// RNG seed) and resets the chip's watchdog. After close, the parent
171/// [`Atecc`] becomes usable again for a new channel.
172///
173/// To put the chip into its lowest-power mode and clear volatile state
174/// instead of just idling, use [`Self::close_to_sleep`].
175///
176/// If the channel is dropped without `close` being called, the chip is
177/// left awake. Its watchdog will eventually time it out to sleep, and the
178/// next [`Atecc::open_channel`] call will wake it normally. The [`Drop`]
179/// impl emits a `defmt::warn!` to flag the protocol violation in
180/// development; nothing breaks, but it indicates a bug to fix.
181pub struct AteccChannel<'a, H>
182where
183 H: AteccHal,
184{
185 driver: &'a mut Atecc<H>,
186 closed: bool,
187}
188
189impl<H> AteccChannel<'_, H>
190where
191 H: AteccHal,
192{
193 /// Close the channel by sending the idle token to the chip and
194 /// consuming the channel handle.
195 ///
196 /// Idle preserves volatile chip state (`TempKey`, RNG seed) and resets
197 /// the chip's watchdog. To clear volatile state and put the chip into
198 /// its lowest-power mode instead, use [`Self::close_to_sleep`].
199 ///
200 /// # Errors
201 /// Forwards [`AteccError::Hal`] from the I2C layer.
202 pub async fn close(mut self) -> Result<(), AteccError<H::Error>>
203 {
204 idle(&mut self.driver.hal, self.driver.device_addr).await?;
205 self.closed = true;
206 Ok(())
207 }
208
209 /// Close the channel by sending the sleep token to the chip.
210 ///
211 /// Unlike [`Self::close`], sleep clears volatile state (`TempKey`, RNG
212 /// seed) and brings the chip to its low power consumption level. The
213 /// next [`Atecc::open_channel`] will re-wake the chip from a clean
214 /// state.
215 ///
216 /// # Errors
217 /// Forwards [`AteccError::Hal`] from the I2C layer.
218 pub async fn close_to_sleep(mut self) -> Result<(), AteccError<H::Error>>
219 {
220 sleep(&mut self.driver.hal, self.driver.device_addr).await?;
221 self.closed = true;
222 Ok(())
223 }
224
225 /// Force a fresh wake mid-channel.
226 ///
227 /// Useful after an HAL-level error suggests the chip's state has become
228 /// uncertain (a NACK during a command, for example). Equivalent to
229 /// closing and reopening the channel, but cheaper because it does not
230 /// idle first.
231 ///
232 /// # Errors
233 /// Forwards every variant from [`crate::wake::wake`].
234 pub async fn refresh(&mut self) -> Result<(), AteccError<H::Error>>
235 {
236 wake(&mut self.driver.hal, self.driver.device_addr).await
237 }
238
239 /// Execute one full command round-trip and return the data payload.
240 ///
241 /// Use this overload for commands that return data (Info, Random, Read,
242 /// `GenKey`, Sign, Verify, ECDH). The chip responds with `count >= 5`
243 /// bytes (count + payload + CRC).
244 ///
245 /// For commands that only signal success or failure via a 4-byte status
246 /// frame (Write, Lock, Counter set), use [`Self::execute_command_status`].
247 /// This method does not accept the `0x00` success byte because a
248 /// data-returning command never emits a bare `0x00`. If it appears
249 /// here, the frame is malformed.
250 ///
251 /// `data` is the command-specific payload, `expected_exec_ms` is the
252 /// typical execution time for that opcode (consult the `EXEC_TIME_*`
253 /// constants in [`crate::opcodes`]), and `response_buf` is filled with
254 /// the raw response frame (count byte and CRC included).
255 ///
256 /// On success returns a `&[u8]` slice over the payload section of
257 /// `response_buf` (excluding count and CRC).
258 ///
259 /// This method does not idle the chip on its own. Callers that have
260 /// finished their command sequence should drop the channel via
261 /// [`Self::close`]. Callers chaining multiple commands that share
262 /// volatile state (Nonce followed by Sign for example) keep the
263 /// channel open between calls.
264 ///
265 /// # Errors
266 /// Every variant of [`AteccError`] is reachable. See its documentation.
267 pub(crate) async fn execute_command<'r>
268 (
269 &mut self,
270 opcode: u8,
271 param1: u8,
272 param2: u16,
273 data: &[u8],
274 expected_exec_ms: u32,
275 response_buf: &'r mut [u8],
276 ) -> Result<&'r [u8], AteccError<H::Error>>
277 {
278 let response_len = self
279 .run_command(opcode, param1, param2, data, expected_exec_ms, response_buf)
280 .await?;
281 let response = &response_buf[..response_len];
282
283 match parse_response_frame(response).map_err(map_parse_error)?
284 {
285 ResponseFrame::Payload(_) =>
286 {
287 // Re-borrow the payload from response_buf with the caller's
288 // lifetime. The slice indexes are count byte (1) up to the
289 // two trailing CRC bytes.
290 Ok(&response_buf[1..response_len - 2])
291 }
292 ResponseFrame::Status(status_byte) =>
293 {
294 match ChipError::from_status_byte(status_byte)
295 {
296 Some(err) => Err(AteccError::Chip(err)),
297 None =>
298 {
299 // A bare 0x00 status here means the chip returned a
300 // 4-byte success frame for a data-returning command.
301 // That should not happen for the opcodes that use
302 // this method. Treat as malformed.
303 Err(AteccError::MalformedResponse)
304 }
305 }
306 }
307 }
308 }
309
310 /// Execute one full command round-trip and expect a status-only response.
311 ///
312 /// Use this overload for commands that signal completion with a 4-byte
313 /// status frame (Write, Lock, Counter set, Nonce mode 0x03). A status
314 /// byte of `0x00` is the success indicator. Any non-zero status maps to
315 /// an [`AteccError::Chip`] variant.
316 ///
317 /// A response longer than 4 bytes here means the chip returned data
318 /// when none was expected. This is treated as a malformed response.
319 ///
320 /// # Errors
321 /// Every variant of [`AteccError`] is reachable. See its documentation.
322 pub(crate) async fn execute_command_status
323 (
324 &mut self,
325 opcode: u8,
326 param1: u8,
327 param2: u16,
328 data: &[u8],
329 expected_exec_ms: u32,
330 ) -> Result<(), AteccError<H::Error>>
331 {
332 let mut response_buf = [0u8; STATUS_RESPONSE_LEN];
333 let response_len = self
334 .run_command
335 (
336 opcode,
337 param1,
338 param2,
339 data,
340 expected_exec_ms,
341 &mut response_buf,
342 )
343 .await?;
344
345 if response_len != STATUS_RESPONSE_LEN
346 {
347 return Err(AteccError::MalformedResponse);
348 }
349
350 match parse_response_frame(&response_buf[..response_len]).map_err(map_parse_error)?
351 {
352 ResponseFrame::Status(0x00) => Ok(()),
353 ResponseFrame::Status(status_byte) => Err(AteccError::Chip(
354 ChipError::from_status_byte(status_byte)
355 .unwrap_or(ChipError::Unknown(status_byte)),
356 )),
357 ResponseFrame::Payload(_) => Err(AteccError::MalformedResponse),
358 }
359 }
360
361 /// Send the command frame and poll for the raw response.
362 ///
363 /// Returns the total number of bytes written into `response_buf` (count
364 /// byte included). Parsing of the response frame is the caller's
365 /// responsibility, so this helper can be shared between the data-payload
366 /// and status-only entry points above.
367 ///
368 /// This helper does NOT idle the chip after the response: the channel
369 /// model means idling is the explicit job of [`Self::close`]. That
370 /// keeps multi-step workflows (Nonce + Sign, Nonce + `GenDig` + Write)
371 /// working naturally inside a single channel.
372 async fn run_command
373 (
374 &mut self,
375 opcode: u8,
376 param1: u8,
377 param2: u16,
378 data: &[u8],
379 expected_exec_ms: u32,
380 response_buf: &mut [u8],
381 ) -> Result<usize, AteccError<H::Error>>
382 {
383 // Build the command frame. We use a stack buffer sized to the
384 // protocol maximum so this works in pure no_std without an allocator.
385 let mut tx = [0u8; MAX_PACKET_SIZE];
386 // First byte sent on I2C is the command word address. The CRC of the
387 // frame does not cover it.
388 tx[0] = WORD_ADDRESS_COMMAND;
389 let frame_len = build_command_frame(opcode, param1, param2, data, &mut tx[1..])
390 .map_err(map_build_error)?;
391 let total_tx = 1 + frame_len;
392
393 self.driver.hal.i2c_write(self.driver.device_addr, &tx[..total_tx]).await?;
394
395 // Wait the nominal execution time before the first attempt.
396 self.driver.hal.delay_ms(expected_exec_ms).await;
397
398 self.poll_for_response(response_buf).await
399 }
400
401 /// Poll the chip's response register until a frame is available or the
402 /// global timeout elapses.
403 ///
404 /// The ATECC608B signals "I am ready" by responding to the read with the
405 /// frame proper. While it is still busy it NACKs the read, which
406 /// surfaces as an HAL error. We treat any HAL error during this phase as
407 /// "not yet ready" and retry after [`POLLING_PERIOD_MS`].
408 ///
409 /// On success returns the number of bytes written into `response_buf`.
410 async fn poll_for_response
411 (
412 &mut self,
413 response_buf: &mut [u8],
414 ) -> Result<usize, AteccError<H::Error>>
415 {
416 let max_buf_len = response_buf.len().min(MAX_RESPONSE_SIZE);
417 let mut elapsed_ms: u32 = 0;
418
419 loop
420 {
421 // First read the count byte alone. This tells us how big the
422 // rest of the frame is.
423 let mut count = [0u8; 1];
424 if let Ok(()) = self.driver.hal.i2c_read(self.driver.device_addr, &mut count).await
425 {
426 let total = count[0] as usize;
427 if total < 4 || total > max_buf_len
428 {
429 return Err(AteccError::MalformedResponse);
430 }
431
432 response_buf[0] = count[0];
433 self.driver.hal
434 .i2c_read(self.driver.device_addr, &mut response_buf[1..total])
435 .await?;
436
437 return Ok(total);
438 }
439 // Treated as "chip still busy". Back off and retry.
440 if elapsed_ms >= POLLING_MAX_MS
441 {
442 return Err(AteccError::Timeout);
443 }
444 self.driver.hal.delay_ms(POLLING_PERIOD_MS).await;
445 elapsed_ms = elapsed_ms.saturating_add(POLLING_PERIOD_MS);
446 }
447 }
448}
449
450impl<H> Drop for AteccChannel<'_, H>
451where
452 H: AteccHal,
453{
454 /// On drop without an explicit `close`, emit a `defmt::warn!` to flag
455 /// the protocol violation.
456 ///
457 /// The chip is left awake; its watchdog (~1.3 s) will eventually idle
458 /// it, but the next channel may observe a transient state for that
459 /// duration. The drop itself cannot send the idle token because Drop
460 /// is synchronous and the HAL is async; the warn-on-drop is the only
461 /// signal we can emit without a `block_on` of unknown safety.
462 fn drop(&mut self)
463 {
464 if !self.closed
465 {
466 #[cfg(feature = "defmt")]
467 defmt::warn!
468 (
469 "AteccChannel dropped without close(); chip left awake. \
470 Its watchdog (~1.3 s) will eventually idle it, but the \
471 next open_channel may observe a transient state. Fix \
472 the caller to call channel.close().await explicitly."
473 );
474 }
475 }
476}
477
478fn map_build_error<E: core::fmt::Debug>(err: PacketBuildError) -> AteccError<E>
479{
480 match err
481 {
482 PacketBuildError::DataTooLong { .. } | PacketBuildError::OutputBufferTooSmall { .. } =>
483 {
484 AteccError::BufferTooSmall
485 }
486 }
487}
488
489fn map_parse_error<E: core::fmt::Debug>(err: PacketParseError) -> AteccError<E>
490{
491 match err
492 {
493 PacketParseError::TooShort | PacketParseError::LengthMismatch { .. } =>
494 {
495 AteccError::MalformedResponse
496 }
497 PacketParseError::BadCrc => AteccError::BadCrc,
498 }
499}