Skip to main content

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}