From f3d80b87a9a2bbaee7a1f76b3960d4f057892d83 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 18 Mar 2026 21:33:18 -0700 Subject: [PATCH 01/46] util: add regcpy Regcpy are utility functions for dealing with memory regions in mmio space. Signed-off-by: Chris Frantz --- util/regcpy/BUILD.bazel | 29 ++ util/regcpy/regcpy.rs | 774 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 803 insertions(+) create mode 100644 util/regcpy/BUILD.bazel create mode 100644 util/regcpy/regcpy.rs diff --git a/util/regcpy/BUILD.bazel b/util/regcpy/BUILD.bazel new file mode 100644 index 00000000..3d366e2e --- /dev/null +++ b/util/regcpy/BUILD.bazel @@ -0,0 +1,29 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "regcpy", + srcs = [ + "regcpy.rs", + ], + crate_name = "util_regcpy", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:aligned", + "@rust_crates//:zerocopy", + "@ureg", + ], +) + +rust_test( + name = "regcpy_test", + crate = ":regcpy", + edition = "2024", + rustc_flags = [ + "-C", + "debug-assertions", + ], +) diff --git a/util/regcpy/regcpy.rs b/util/regcpy/regcpy.rs new file mode 100644 index 00000000..f0f8b37d --- /dev/null +++ b/util/regcpy/regcpy.rs @@ -0,0 +1,774 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +#![cfg_attr(not(test), no_std)] + +// TODO: should this be upstreamed into ureg? + +use aligned::Aligned; +use aligned::A4; +use core::cmp::min; +use zerocopy::FromBytes; +use zerocopy::Unalign; + +#[inline(never)] +pub fn copy_to_reg_unaligned( + reg: &ureg::RegRef< + impl ureg::WritableReg + ureg::ResettableReg, + impl ureg::MmioMut + Copy, + >, + src: &[u8], +) { + let (words, rem_bytes): (&[Unalign], &[u8]) = FromBytes::ref_from_prefix(src).unwrap(); + + for word in words { + reg.write(|_| word.get()); + } + if let Some(last_word) = last_word(rem_bytes) { + reg.write(|_| last_word); + } +} + +#[inline(never)] +pub fn copy_to_reg( + reg: &ureg::RegRef< + impl ureg::WritableReg + ureg::ResettableReg, + impl ureg::MmioMut + Copy, + >, + src: &Aligned, +) { + // Convert to regular slice; optimizer should be smart enough to realize it's still aligned. + let (words, rem_bytes): (&[u32], &[u8]) = FromBytes::ref_from_prefix(src.as_ref()).unwrap(); + + for word in words { + reg.write(|_| *word); + } + if let Some(last_word) = last_word(rem_bytes) { + reg.write(|_| last_word); + } +} + +#[inline(never)] +pub fn copy_to_reg_array( + array: &ureg::Array< + LEN, + ureg::RegRef< + impl ureg::WritableReg + ureg::ResettableReg, + impl ureg::MmioMut + Copy, + >, + >, + src: &Aligned, +) { + // Convert to regular slice; optimizer should be smart enough to realize it's still aligned. + let (words, rem_bytes): (&[u32], &[u8]) = FromBytes::ref_from_prefix(src.as_ref()).unwrap(); + + let words_to_copy = min(LEN, words.len()); + + #[allow(clippy::needless_range_loop)] // optimizes better + for i in 0..words_to_copy { + array.at(i).write(|_| words[i]); + } + let Some(reg) = array.get(words.len()) else { + return; + }; + let Some(last_word) = last_word(rem_bytes) else { + return; + }; + reg.write(|_| last_word); +} + +#[inline(never)] +pub fn copy_from_reg( + dest: &mut Aligned, + reg: &ureg::RegRef, impl ureg::Mmio + Copy>, +) { + let (words, rem_bytes): (&mut [u32], &mut [u8]) = FromBytes::mut_from_prefix(dest).unwrap(); + let words_to_copy = min(LEN, words.len()); + + #[allow(clippy::needless_range_loop)] + for i in 0..words_to_copy { + words[i] = reg.read(); + } + + if words_to_copy < LEN { + set_rem_bytes(rem_bytes, || reg.read()); + } +} + +#[inline(never)] +pub fn copy_from_reg_array( + dest: &mut Aligned, + array: &ureg::Array< + LEN, + ureg::RegRef, impl ureg::Mmio + Copy>, + >, +) { + let (words, rem_bytes): (&mut [u32], &mut [u8]) = FromBytes::mut_from_prefix(dest).unwrap(); + let words_to_copy = min(LEN, words.len()); + + #[allow(clippy::needless_range_loop)] + for i in 0..words_to_copy { + words[i] = array.at(i).read(); + } + + if words_to_copy < LEN { + set_rem_bytes(rem_bytes, || array.at(words_to_copy).read()); + } +} + +#[inline(never)] +pub fn copy_from_reg_unaligned( + dest: &mut [u8], + reg: &ureg::RegRef, impl ureg::MmioMut + Copy>, +) { + let (words, rem_bytes): (&mut [Unalign], &mut [u8]) = + FromBytes::mut_from_prefix(dest).unwrap(); + + for word in words { + word.set(reg.read()) + } + set_rem_bytes(rem_bytes, || reg.read()); +} + +#[inline(always)] +fn last_word(rem_bytes: &[u8]) -> Option { + if rem_bytes.is_empty() { + None + } else { + Some(u32::from_le_bytes([ + rem_bytes[0], + rem_bytes.get(1).copied().unwrap_or_default(), + rem_bytes.get(2).copied().unwrap_or_default(), + 0, + ])) + } +} + +#[inline(always)] +fn set_rem_bytes(rem_bytes: &mut [u8], get_word: impl FnOnce() -> u32) { + if rem_bytes.is_empty() { + return; + } + let word = get_word(); + rem_bytes[0] = word as u8; + if rem_bytes.len() == 1 { + return; + } + rem_bytes[1] = (word >> 8) as u8; + if rem_bytes.len() == 2 { + return; + } + rem_bytes[2] = (word >> 16) as u8; +} + +#[cfg(test)] +mod test { + use super::*; + + use core::cell::RefCell; + use core::mem::transmute_copy; + use std::collections::HashMap; + use std::collections::VecDeque; + use std::rc::Rc; + + use ureg::Mmio; + use ureg::MmioMut; + use ureg::ReadWriteReg32; + use ureg::RegRef; + use ureg::UintType; + + fn uint_val(val: T) -> u64 { + // nosemgrep + unsafe { + // SAFETY: The underlying source type is the same as the destination type. + match T::TYPE { + ureg::UintType::U8 => core::mem::transmute_copy::(&val).into(), + ureg::UintType::U16 => core::mem::transmute_copy::(&val).into(), + ureg::UintType::U32 => core::mem::transmute_copy::(&val).into(), + ureg::UintType::U64 => core::mem::transmute_copy::(&val), + } + } + } + + #[derive(Clone, Default)] + struct FakeMmio { + fifos: Rc>>>, + write_log: Rc>>, + } + impl FakeMmio { + fn fifo_push(&self, addr: usize, val: u32) { + self.fifos + .borrow_mut() + .entry(addr) + .or_default() + .push_back(val); + } + fn take_log(&self) -> Vec<(usize, u64)> { + core::mem::take(&mut *self.write_log.borrow_mut()) + } + fn log(&self) -> Vec<(usize, u64)> { + self.write_log.borrow().clone() + } + } + impl Mmio for FakeMmio { + unsafe fn read_volatile(&self, src: *const T) -> T { + let addr = src as usize; + let Some(val) = self.fifos.borrow_mut().entry(addr).or_default().pop_front() else { + panic!("Unexpected read from addr 0x{addr:x}") + }; + if T::TYPE != UintType::U32 { + panic!("Read must be of type u32"); + } + // nosemgrep + unsafe { + // SAFETY: the type `T` is u32. + transmute_copy::(&val) + } + } + } + impl MmioMut for FakeMmio { + unsafe fn write_volatile(&self, dst: *mut T, src: T) { + self.write_log + .borrow_mut() + .push((dst as usize, uint_val(src))); + } + } + + #[test] + #[should_panic(expected = "Unexpected read from addr 0x40404040")] + pub fn test_fake_mmio_read_unexpected_addr() { + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, _>::new_with_mmio(0x4040_4040 as *mut _, &mmio) + }; + fifo_reg.read(); + } + + #[test] + #[should_panic(expected = "Read must be of type u32")] + pub fn test_fake_mmio_read_unexpected_type() { + let addr: usize = 0x4040; + let mmio = FakeMmio::default(); + mmio.fifo_push(addr, 42); + // nosemgrep + unsafe { + // SAFETY: the backend is FakeMmio. + mmio.read_volatile(addr as *const u64) + }; + } + + #[test] + pub fn test_fake_mmio_read() { + let addr = 0x4040_4040; + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, _>::new_with_mmio(addr as *mut _, &mmio) + }; + mmio.fifo_push(addr, 0xba5e_ba11); + mmio.fifo_push(addr, 0x1234_5678); + assert_eq!(fifo_reg.read(), 0xba5e_ba11); + assert_eq!(fifo_reg.read(), 0x1234_5678); + } + + #[test] + #[should_panic(expected = "Unexpected read from addr 0x40404040")] + pub fn test_fake_mmio_read_fifo_exhausted() { + let addr = 0x4040_4040; + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, _>::new_with_mmio(addr as *mut _, &mmio) + }; + mmio.fifo_push(addr, 0xba5e_ba11); + mmio.fifo_push(addr, 0x1234_5678); + assert_eq!(fifo_reg.read(), 0xba5e_ba11); + assert_eq!(fifo_reg.read(), 0x1234_5678); + fifo_reg.read(); + } + + #[test] + #[rustfmt::skip] + pub fn test_fake_mmio_write() { + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, _>::new_with_mmio(0x4040_4040 as *mut _, &mmio) + }; + // nosemgrep + let fifo_reg2 = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, _>::new_with_mmio(0x5050_5050 as *mut _, &mmio) + }; + + assert_eq!( + mmio.log(), + vec![], + ); + fifo_reg.write(|_| 0xba5e_ba11); + assert_eq!( + mmio.log(), + vec![ + (0x4040_4040, 0xba5e_ba11), + ], + ); + fifo_reg.write(|_| 0xabba_abba); + assert_eq!( + mmio.log(), + vec![ + (0x4040_4040, 0xba5e_ba11), + (0x4040_4040, 0xabba_abba), + ], + ); + fifo_reg2.write(|_| 0x1234_5678); + assert_eq!( + mmio.log(), + vec![ + (0x4040_4040, 0xba5e_ba11), + (0x4040_4040, 0xabba_abba), + (0x5050_5050, 0x1234_5678), + ], + ); + } + + #[test] + #[rustfmt::skip] + pub fn test_copy_to_reg_unaligned() { + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, &FakeMmio>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_to_reg_unaligned(&fifo_reg, &[]); + assert_eq!( + mmio.take_log(), + vec![], + ); + copy_to_reg_unaligned(&fifo_reg, &[0x12]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_0012)], + ); + + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_3412)], + ); + + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34, 0x56]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0056_3412)], + ); + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34, 0x56, 0x78]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x7856_3412)], + ); + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34, 0x56, 0x78, 0x9a]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0x0000_009a), + ], + ); + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0x0000_bc9a), + ], + ); + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0xf0de_bc9a), + ], + ); + copy_to_reg_unaligned(&fifo_reg, &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0xdd]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0xf0de_bc9a), + (0x4040_4040, 0x0000_00dd), + ], + ); + } + + #[test] + #[rustfmt::skip] + pub fn test_copy_to_reg() { + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, &FakeMmio>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_to_reg(&fifo_reg, &Aligned([])); + assert_eq!( + mmio.take_log(), + vec![], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_0012)], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_3412)], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34, 0x56])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0056_3412)], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34, 0x56, 0x78])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x7856_3412)], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0x0000_009a), + ], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0x0000_bc9a), + ], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0xf0de_bc9a), + ], + ); + copy_to_reg(&fifo_reg, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0xdd])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4040, 0xf0de_bc9a), + (0x4040_4040, 0x0000_00dd), + ], + ); + } + + #[test] + #[rustfmt::skip] + pub fn test_copy_to_reg_array() { + let mmio = FakeMmio::default(); + // nosemgrep + let reg_array = unsafe { + // SAFETY: the backend is FakeMmio. + ureg::Array::<3, RegRef, &FakeMmio>>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_to_reg_array(®_array, &Aligned([])); + assert_eq!( + mmio.take_log(), + vec![], + ); + + copy_to_reg_array(®_array, &Aligned([0x12])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_0012)], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_3412)], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34, 0x56])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0056_3412)], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34, 0x56, 0x78])); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x7856_3412)], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0x0000_009a), + ], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0x0000_bc9a), + ], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0xf0de_bc9a), + ], + ); + + copy_to_reg_array(®_array, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0xdd])); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0xf0de_bc9a), + (0x4040_4048, 0x0000_00dd), + ], + ); + } + + #[test] + #[rustfmt::skip] + pub fn test_copy_from_reg() { + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, &FakeMmio>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_from_reg::<10>(&mut Aligned([]), &fifo_reg); + assert_eq!( + mmio.take_log(), + vec![], + ); + + mmio.fifo_push(0x4040_4040, 0x0000_0012); + let mut result = Aligned::([0_u8; 1]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12])); + + mmio.fifo_push(0x4040_4040, 0x0000_3412); + let mut result = Aligned::([0_u8; 2]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34])); + + mmio.fifo_push(0x4040_4040, 0x0056_3412); + let mut result = Aligned::([0_u8; 3]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + let mut result = Aligned::([0_u8; 4]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4040, 0x0000_009A); + let mut result = Aligned::([0_u8; 5]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4040, 0x0000_BC9A); + let mut result = Aligned::([0_u8; 6]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4040, 0x00DE_BC9A); + let mut result = Aligned::([0_u8; 7]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4040, 0xF0DE_BC9A); + let mut result = Aligned::([0_u8; 8]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4040, 0xF0DE_BC9A); + mmio.fifo_push(0x4040_4040, 0x0000_00DD); + let mut result = Aligned::([0_u8; 9]); + copy_from_reg::<10>(&mut result, &fifo_reg); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0xDD])); + } + + #[test] + #[rustfmt::skip] + pub fn test_copy_from_reg_array() { + let mmio = FakeMmio::default(); + // nosemgrep + let reg_array = unsafe { + // SAFETY: the backend is FakeMmio. + ureg::Array::<3, RegRef, &FakeMmio>>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_from_reg_array(&mut Aligned([]), ®_array); + assert_eq!( + mmio.take_log(), + vec![], + ); + + mmio.fifo_push(0x4040_4040, 0x0000_0012); + let mut result = Aligned::([0_u8; 1]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12])); + + mmio.fifo_push(0x4040_4040, 0x0000_3412); + let mut result = Aligned::([0_u8; 2]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34])); + + mmio.fifo_push(0x4040_4040, 0x0056_3412); + let mut result = Aligned::([0_u8; 3]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + let mut result = Aligned::([0_u8; 4]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0x0000_009A); + let mut result = Aligned::([0_u8; 5]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0x0000_BC9A); + let mut result = Aligned::([0_u8; 6]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0x00DE_BC9A); + let mut result = Aligned::([0_u8; 7]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0xF0DE_BC9A); + let mut result = Aligned::([0_u8; 8]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0])); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0xF0DE_BC9A); + mmio.fifo_push(0x4040_4048, 0x0000_00DD); + let mut result = Aligned::([0_u8; 9]); + copy_from_reg_array(&mut result, ®_array); + assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0xDD])); + } + + #[test] + pub fn test_copy_from_reg_unaligned() { + let addr: usize = 0x4040_4040; + let mmio = FakeMmio::default(); + // nosemgrep + let fifo_reg = unsafe { + // SAFETY: the backend is FakeMmio. + RegRef::, &FakeMmio>::new_with_mmio(addr as *mut _, &mmio) + }; + copy_from_reg_unaligned(&mut [], &fifo_reg); + + mmio.fifo_push(addr, 0x7856_3412); + let mut result = [0_u8; 1]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12]); + + mmio.fifo_push(addr, 0x7856_3412); + let mut result = [0_u8; 2]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34]); + + mmio.fifo_push(addr, 0x7856_3412); + let mut result = [0_u8; 3]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34, 0x56]); + + mmio.fifo_push(addr, 0x7856_3412); + let mut result = [0_u8; 4]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78]); + + mmio.fifo_push(addr, 0x7856_3412); + mmio.fifo_push(addr, 0xf0de_bc9a); + let mut result = [0_u8; 5]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9a]); + + mmio.fifo_push(addr, 0x7856_3412); + mmio.fifo_push(addr, 0xf0de_bc9a); + let mut result = [0_u8; 6]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]); + + mmio.fifo_push(addr, 0x7856_3412); + mmio.fifo_push(addr, 0xf0de_bc9a); + let mut result = [0_u8; 7]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde]); + + mmio.fifo_push(addr, 0x7856_3412); + mmio.fifo_push(addr, 0xf0de_bc9a); + let mut result = [0_u8; 8]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]); + + mmio.fifo_push(addr, 0x7856_3412); + mmio.fifo_push(addr, 0xf0de_bc9a); + mmio.fifo_push(addr, 0x2c); + let mut result = [0_u8; 9]; + copy_from_reg_unaligned(&mut result, &fifo_reg); + assert_eq!( + result, + [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x2c] + ); + } +} From a6bc506d4ce4ddbadf24482b098c078dce1e8c23 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 22 Apr 2026 12:51:17 -0700 Subject: [PATCH 02/46] presubmit: fix the C/C++ include guard format Signed-off-by: Chris Frantz --- presubmit/cpp_include_guard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presubmit/cpp_include_guard.py b/presubmit/cpp_include_guard.py index 68fec6dc..9908b93b 100644 --- a/presubmit/cpp_include_guard.py +++ b/presubmit/cpp_include_guard.py @@ -17,7 +17,7 @@ def guard_name(path: Path) -> str: # The presubmit tool runs in the root of the project. # Compute the path relative to the project root. path = path.relative_to(os.getcwd()) - guard = f"{PROJECT}_{path}".replace("/", "_").replace(".", "_") + guard = f"{PROJECT}_{path}_".replace("/", "_").replace(".", "_") return guard.upper() From a1177e4f7d919cedde60330303b97e0d4ec0745a Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Tue, 21 Apr 2026 20:30:57 -0700 Subject: [PATCH 03/46] util: add a simple `ufmt` based console Signed-off-by: Chris Frantz --- MODULE.bazel.lock | 56 ++++++++++++- third_party/crates_io/Cargo.lock | 28 +++++++ third_party/crates_io/Cargo.toml | 1 + util/console/BUILD.bazel | 57 ++++++++++++++ util/console/dbg_print.c | 131 +++++++++++++++++++++++++++++++ util/console/dbg_print.h | 53 +++++++++++++ util/console/lib.rs | 69 ++++++++++++++++ util/console/pigweed.rs | 32 ++++++++ util/console/stdout.rs | 18 +++++ 9 files changed, 441 insertions(+), 4 deletions(-) create mode 100644 util/console/BUILD.bazel create mode 100644 util/console/dbg_print.c create mode 100644 util/console/dbg_print.h create mode 100644 util/console/lib.rs create mode 100644 util/console/pigweed.rs create mode 100644 util/console/stdout.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 34955a02..8c6b52f6 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -2588,8 +2588,8 @@ "bzlTransitiveDigest": "NWG/zr7TmqNjKiFdm1yVDA5cIKBVybQcHs2v6Jj3knc=", "usagesDigest": "fXPTszFzBe4VvGFoJKnLB/E/6WtlKiQ600VfVyI7MFk=", "recordedFileInputs": { - "@@//third_party/crates_io/Cargo.lock": "1e709fd194acdfdb8bd71d208a4f0be6a59c0acfe8042caa645a1ad5c2f5ce7e", - "@@//third_party/crates_io/Cargo.toml": "9a692cfbf8fab58e3d003e53550be0247aaf602cee26c7cea06caffb05280a48", + "@@//third_party/crates_io/Cargo.lock": "6fcf085c5320b3a12b047e82aa1c8d91516f833cf2cfc7733c39bc6f7eed39a1", + "@@//third_party/crates_io/Cargo.toml": "6692cc7e0d688a16fba5d7344f95204ba818953b07af82fc77853c7c2136528a", "@@caliptra_deps+//crates_io/embedded/Cargo.lock": "d6c0101f48da22f2bc2d339f358de79bad3dd03218c6db29a14099b9f7757691", "@@caliptra_deps+//crates_io/embedded/Cargo.toml": "8f9f4ed2721db13476b12fdac045dac2142b38f189a8abb5f4c446dc0c6ac3dd", "@@caliptra_deps+//crates_io/host/Cargo.lock": "ae555b01424917ec61d892c15d7a66af88c2f10be4e0e7b0bc2357b702883b89", @@ -2615,9 +2615,9 @@ "repoRuleId": "@@rules_rust+//crate_universe:extensions.bzl%_generate_repo", "attributes": { "contents": { - "BUILD.bazel": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files(\n [\n \"cargo-bazel.json\",\n \"crates.bzl\",\n \"defs.bzl\",\n ] + glob(\n allow_empty = True,\n include = [\"*.bazel\"],\n ),\n)\n\nfilegroup(\n name = \"srcs\",\n srcs = glob(\n allow_empty = True,\n include = [\n \"*.bazel\",\n \"*.bzl\",\n ],\n ),\n)\n\n# Workspace Member Dependencies\nalias(\n name = \"aes-0.8.4\",\n actual = \"@rust_crates__aes-0.8.4//:aes\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aes\",\n actual = \"@rust_crates__aes-0.8.4//:aes\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aes-gcm-0.10.3\",\n actual = \"@rust_crates__aes-gcm-0.10.3//:aes_gcm\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aes-gcm\",\n actual = \"@rust_crates__aes-gcm-0.10.3//:aes_gcm\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aligned-0.4.3\",\n actual = \"@rust_crates__aligned-0.4.3//:aligned\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aligned\",\n actual = \"@rust_crates__aligned-0.4.3//:aligned\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"anyhow-1.0.102\",\n actual = \"@rust_crates__anyhow-1.0.102//:anyhow\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"anyhow\",\n actual = \"@rust_crates__anyhow-1.0.102//:anyhow\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield-0.14.0\",\n actual = \"@rust_crates__bitfield-0.14.0//:bitfield\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield\",\n actual = \"@rust_crates__bitfield-0.14.0//:bitfield\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield-struct-0.11.0\",\n actual = \"@rust_crates__bitfield-struct-0.11.0//:bitfield_struct\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield-struct\",\n actual = \"@rust_crates__bitfield-struct-0.11.0//:bitfield_struct\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitflags-2.11.0\",\n actual = \"@rust_crates__bitflags-2.11.0//:bitflags\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitflags\",\n actual = \"@rust_crates__bitflags-2.11.0//:bitflags\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"byteorder-1.5.0\",\n actual = \"@rust_crates__byteorder-1.5.0//:byteorder\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"byteorder\",\n actual = \"@rust_crates__byteorder-1.5.0//:byteorder\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cfg-if-1.0.4\",\n actual = \"@rust_crates__cfg-if-1.0.4//:cfg_if\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cfg-if\",\n actual = \"@rust_crates__cfg-if-1.0.4//:cfg_if\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cipher-0.4.4\",\n actual = \"@rust_crates__cipher-0.4.4//:cipher\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cipher\",\n actual = \"@rust_crates__cipher-0.4.4//:cipher\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"clap-4.6.0\",\n actual = \"@rust_crates__clap-4.6.0//:clap\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"clap\",\n actual = \"@rust_crates__clap-4.6.0//:clap\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"compiler_builtins-0.1.160\",\n actual = \"@rust_crates__compiler_builtins-0.1.160//:compiler_builtins\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"compiler_builtins\",\n actual = \"@rust_crates__compiler_builtins-0.1.160//:compiler_builtins\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cortex-m-0.7.7\",\n actual = \"@rust_crates__cortex-m-0.7.7//:cortex_m\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cortex-m\",\n actual = \"@rust_crates__cortex-m-0.7.7//:cortex_m\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ctr-0.9.2\",\n actual = \"@rust_crates__ctr-0.9.2//:ctr\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ctr\",\n actual = \"@rust_crates__ctr-0.9.2//:ctr\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ecdsa-0.16.9\",\n actual = \"@rust_crates__ecdsa-0.16.9//:ecdsa\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ecdsa\",\n actual = \"@rust_crates__ecdsa-0.16.9//:ecdsa\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-1.0.0\",\n actual = \"@rust_crates__embedded-hal-1.0.0//:embedded_hal\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal\",\n actual = \"@rust_crates__embedded-hal-1.0.0//:embedded_hal\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-async-1.0.0\",\n actual = \"@rust_crates__embedded-hal-async-1.0.0//:embedded_hal_async\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-async\",\n actual = \"@rust_crates__embedded-hal-async-1.0.0//:embedded_hal_async\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-nb-1.0.0\",\n actual = \"@rust_crates__embedded-hal-nb-1.0.0//:embedded_hal_nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-nb\",\n actual = \"@rust_crates__embedded-hal-nb-1.0.0//:embedded_hal_nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-io-0.6.1\",\n actual = \"@rust_crates__embedded-io-0.6.1//:embedded_io\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-io\",\n actual = \"@rust_crates__embedded-io-0.6.1//:embedded_io\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"futures-0.3.32\",\n actual = \"@rust_crates__futures-0.3.32//:futures\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"futures\",\n actual = \"@rust_crates__futures-0.3.32//:futures\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"heapless-0.9.2\",\n actual = \"@rust_crates__heapless-0.9.2//:heapless\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"heapless\",\n actual = \"@rust_crates__heapless-0.9.2//:heapless\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hex-0.4.3\",\n actual = \"@rust_crates__hex-0.4.3//:hex\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hex\",\n actual = \"@rust_crates__hex-0.4.3//:hex\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hmac-0.12.1\",\n actual = \"@rust_crates__hmac-0.12.1//:hmac\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hmac\",\n actual = \"@rust_crates__hmac-0.12.1//:hmac\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"k256-0.13.4\",\n actual = \"@rust_crates__k256-0.13.4//:k256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"k256\",\n actual = \"@rust_crates__k256-0.13.4//:k256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"log-0.4.29\",\n actual = \"@rust_crates__log-0.4.29//:log\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"log\",\n actual = \"@rust_crates__log-0.4.29//:log\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"memoffset-0.9.1\",\n actual = \"@rust_crates__memoffset-0.9.1//:memoffset\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"memoffset\",\n actual = \"@rust_crates__memoffset-0.9.1//:memoffset\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"minijinja-2.19.0\",\n actual = \"@rust_crates__minijinja-2.19.0//:minijinja\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"minijinja\",\n actual = \"@rust_crates__minijinja-2.19.0//:minijinja\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nb-1.1.0\",\n actual = \"@rust_crates__nb-1.1.0//:nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nb\",\n actual = \"@rust_crates__nb-1.1.0//:nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nom-7.1.3\",\n actual = \"@rust_crates__nom-7.1.3//:nom\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nom\",\n actual = \"@rust_crates__nom-7.1.3//:nom\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"object-0.37.3\",\n actual = \"@rust_crates__object-0.37.3//:object\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"object\",\n actual = \"@rust_crates__object-0.37.3//:object\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p256-0.13.2\",\n actual = \"@rust_crates__p256-0.13.2//:p256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p256\",\n actual = \"@rust_crates__p256-0.13.2//:p256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p384-0.13.1\",\n actual = \"@rust_crates__p384-0.13.1//:p384\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p384\",\n actual = \"@rust_crates__p384-0.13.1//:p384\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"paste-1.0.15\",\n actual = \"@rust_crates__paste-1.0.15//:paste\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"paste\",\n actual = \"@rust_crates__paste-1.0.15//:paste\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"proc-macro2-1.0.106\",\n actual = \"@rust_crates__proc-macro2-1.0.106//:proc_macro2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"proc-macro2\",\n actual = \"@rust_crates__proc-macro2-1.0.106//:proc_macro2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"prost-0.13.5\",\n actual = \"@rust_crates__prost-0.13.5//:prost\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"prost\",\n actual = \"@rust_crates__prost-0.13.5//:prost\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"quote-1.0.45\",\n actual = \"@rust_crates__quote-1.0.45//:quote\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"quote\",\n actual = \"@rust_crates__quote-1.0.45//:quote\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rand_core-0.9.5\",\n actual = \"@rust_crates__rand_core-0.9.5//:rand_core\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rand_core\",\n actual = \"@rust_crates__rand_core-0.9.5//:rand_core\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-0.12.1\",\n actual = \"@rust_crates__riscv-0.12.1//:riscv\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv\",\n actual = \"@rust_crates__riscv-0.12.1//:riscv\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-rt-0.12.2\",\n actual = \"@rust_crates__riscv-rt-0.12.2//:riscv_rt\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-rt\",\n actual = \"@rust_crates__riscv-rt-0.12.2//:riscv_rt\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-semihosting-0.1.3\",\n actual = \"@rust_crates__riscv-semihosting-0.1.3//:riscv_semihosting\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-semihosting\",\n actual = \"@rust_crates__riscv-semihosting-0.1.3//:riscv_semihosting\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rustc-demangle-0.1.27\",\n actual = \"@rust_crates__rustc-demangle-0.1.27//:rustc_demangle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rustc-demangle\",\n actual = \"@rust_crates__rustc-demangle-0.1.27//:rustc_demangle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sec1-0.7.3\",\n actual = \"@rust_crates__sec1-0.7.3//:sec1\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sec1\",\n actual = \"@rust_crates__sec1-0.7.3//:sec1\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde-1.0.228\",\n actual = \"@rust_crates__serde-1.0.228//:serde\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde\",\n actual = \"@rust_crates__serde-1.0.228//:serde\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_derive-1.0.228\",\n actual = \"@rust_crates__serde_derive-1.0.228//:serde_derive\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_derive\",\n actual = \"@rust_crates__serde_derive-1.0.228//:serde_derive\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_json5-0.2.1\",\n actual = \"@rust_crates__serde_json5-0.2.1//:serde_json5\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_json5\",\n actual = \"@rust_crates__serde_json5-0.2.1//:serde_json5\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha2-0.10.9\",\n actual = \"@rust_crates__sha2-0.10.9//:sha2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha2\",\n actual = \"@rust_crates__sha2-0.10.9//:sha2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha3-0.10.8\",\n actual = \"@rust_crates__sha3-0.10.8//:sha3\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha3\",\n actual = \"@rust_crates__sha3-0.10.8//:sha3\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"smlang-0.8.0\",\n actual = \"@rust_crates__smlang-0.8.0//:smlang\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"smlang\",\n actual = \"@rust_crates__smlang-0.8.0//:smlang\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"subtle-2.6.1\",\n actual = \"@rust_crates__subtle-2.6.1//:subtle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"subtle\",\n actual = \"@rust_crates__subtle-2.6.1//:subtle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn-1.0.109\",\n actual = \"@rust_crates__syn-1.0.109//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn1-1.0.109\",\n actual = \"@rust_crates__syn-1.0.109//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn1\",\n actual = \"@rust_crates__syn-1.0.109//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn-2.0.117\",\n actual = \"@rust_crates__syn-2.0.117//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn\",\n actual = \"@rust_crates__syn-2.0.117//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"thiserror-2.0.18\",\n actual = \"@rust_crates__thiserror-2.0.18//:thiserror\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"thiserror\",\n actual = \"@rust_crates__thiserror-2.0.18//:thiserror\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tock-registers-0.9.0\",\n actual = \"@rust_crates__tock-registers-0.9.0//:tock_registers\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tock-registers\",\n actual = \"@rust_crates__tock-registers-0.9.0//:tock_registers\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio-1.51.1\",\n actual = \"@rust_crates__tokio-1.51.1//:tokio\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio\",\n actual = \"@rust_crates__tokio-1.51.1//:tokio\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio-util-0.7.18\",\n actual = \"@rust_crates__tokio-util-0.7.18//:tokio_util\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio-util\",\n actual = \"@rust_crates__tokio-util-0.7.18//:tokio_util\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zerocopy-0.8.48\",\n actual = \"@rust_crates__zerocopy-0.8.48//:zerocopy\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zerocopy\",\n actual = \"@rust_crates__zerocopy-0.8.48//:zerocopy\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zeroize-1.8.2\",\n actual = \"@rust_crates__zeroize-1.8.2//:zeroize\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zeroize\",\n actual = \"@rust_crates__zeroize-1.8.2//:zeroize\",\n tags = [\"manual\"],\n)\n", + "BUILD.bazel": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files(\n [\n \"cargo-bazel.json\",\n \"crates.bzl\",\n \"defs.bzl\",\n ] + glob(\n allow_empty = True,\n include = [\"*.bazel\"],\n ),\n)\n\nfilegroup(\n name = \"srcs\",\n srcs = glob(\n allow_empty = True,\n include = [\n \"*.bazel\",\n \"*.bzl\",\n ],\n ),\n)\n\n# Workspace Member Dependencies\nalias(\n name = \"aes-0.8.4\",\n actual = \"@rust_crates__aes-0.8.4//:aes\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aes\",\n actual = \"@rust_crates__aes-0.8.4//:aes\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aes-gcm-0.10.3\",\n actual = \"@rust_crates__aes-gcm-0.10.3//:aes_gcm\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aes-gcm\",\n actual = \"@rust_crates__aes-gcm-0.10.3//:aes_gcm\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aligned-0.4.3\",\n actual = \"@rust_crates__aligned-0.4.3//:aligned\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"aligned\",\n actual = \"@rust_crates__aligned-0.4.3//:aligned\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"anyhow-1.0.102\",\n actual = \"@rust_crates__anyhow-1.0.102//:anyhow\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"anyhow\",\n actual = \"@rust_crates__anyhow-1.0.102//:anyhow\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield-0.14.0\",\n actual = \"@rust_crates__bitfield-0.14.0//:bitfield\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield\",\n actual = \"@rust_crates__bitfield-0.14.0//:bitfield\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield-struct-0.11.0\",\n actual = \"@rust_crates__bitfield-struct-0.11.0//:bitfield_struct\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitfield-struct\",\n actual = \"@rust_crates__bitfield-struct-0.11.0//:bitfield_struct\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitflags-2.11.0\",\n actual = \"@rust_crates__bitflags-2.11.0//:bitflags\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"bitflags\",\n actual = \"@rust_crates__bitflags-2.11.0//:bitflags\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"byteorder-1.5.0\",\n actual = \"@rust_crates__byteorder-1.5.0//:byteorder\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"byteorder\",\n actual = \"@rust_crates__byteorder-1.5.0//:byteorder\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cfg-if-1.0.4\",\n actual = \"@rust_crates__cfg-if-1.0.4//:cfg_if\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cfg-if\",\n actual = \"@rust_crates__cfg-if-1.0.4//:cfg_if\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cipher-0.4.4\",\n actual = \"@rust_crates__cipher-0.4.4//:cipher\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cipher\",\n actual = \"@rust_crates__cipher-0.4.4//:cipher\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"clap-4.6.0\",\n actual = \"@rust_crates__clap-4.6.0//:clap\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"clap\",\n actual = \"@rust_crates__clap-4.6.0//:clap\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"compiler_builtins-0.1.160\",\n actual = \"@rust_crates__compiler_builtins-0.1.160//:compiler_builtins\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"compiler_builtins\",\n actual = \"@rust_crates__compiler_builtins-0.1.160//:compiler_builtins\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cortex-m-0.7.7\",\n actual = \"@rust_crates__cortex-m-0.7.7//:cortex_m\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"cortex-m\",\n actual = \"@rust_crates__cortex-m-0.7.7//:cortex_m\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ctr-0.9.2\",\n actual = \"@rust_crates__ctr-0.9.2//:ctr\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ctr\",\n actual = \"@rust_crates__ctr-0.9.2//:ctr\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ecdsa-0.16.9\",\n actual = \"@rust_crates__ecdsa-0.16.9//:ecdsa\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ecdsa\",\n actual = \"@rust_crates__ecdsa-0.16.9//:ecdsa\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-1.0.0\",\n actual = \"@rust_crates__embedded-hal-1.0.0//:embedded_hal\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal\",\n actual = \"@rust_crates__embedded-hal-1.0.0//:embedded_hal\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-async-1.0.0\",\n actual = \"@rust_crates__embedded-hal-async-1.0.0//:embedded_hal_async\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-async\",\n actual = \"@rust_crates__embedded-hal-async-1.0.0//:embedded_hal_async\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-nb-1.0.0\",\n actual = \"@rust_crates__embedded-hal-nb-1.0.0//:embedded_hal_nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-hal-nb\",\n actual = \"@rust_crates__embedded-hal-nb-1.0.0//:embedded_hal_nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-io-0.6.1\",\n actual = \"@rust_crates__embedded-io-0.6.1//:embedded_io\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"embedded-io\",\n actual = \"@rust_crates__embedded-io-0.6.1//:embedded_io\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"futures-0.3.32\",\n actual = \"@rust_crates__futures-0.3.32//:futures\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"futures\",\n actual = \"@rust_crates__futures-0.3.32//:futures\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"heapless-0.9.2\",\n actual = \"@rust_crates__heapless-0.9.2//:heapless\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"heapless\",\n actual = \"@rust_crates__heapless-0.9.2//:heapless\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hex-0.4.3\",\n actual = \"@rust_crates__hex-0.4.3//:hex\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hex\",\n actual = \"@rust_crates__hex-0.4.3//:hex\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hmac-0.12.1\",\n actual = \"@rust_crates__hmac-0.12.1//:hmac\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"hmac\",\n actual = \"@rust_crates__hmac-0.12.1//:hmac\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"k256-0.13.4\",\n actual = \"@rust_crates__k256-0.13.4//:k256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"k256\",\n actual = \"@rust_crates__k256-0.13.4//:k256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"log-0.4.29\",\n actual = \"@rust_crates__log-0.4.29//:log\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"log\",\n actual = \"@rust_crates__log-0.4.29//:log\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"memoffset-0.9.1\",\n actual = \"@rust_crates__memoffset-0.9.1//:memoffset\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"memoffset\",\n actual = \"@rust_crates__memoffset-0.9.1//:memoffset\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"minijinja-2.19.0\",\n actual = \"@rust_crates__minijinja-2.19.0//:minijinja\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"minijinja\",\n actual = \"@rust_crates__minijinja-2.19.0//:minijinja\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nb-1.1.0\",\n actual = \"@rust_crates__nb-1.1.0//:nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nb\",\n actual = \"@rust_crates__nb-1.1.0//:nb\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nom-7.1.3\",\n actual = \"@rust_crates__nom-7.1.3//:nom\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"nom\",\n actual = \"@rust_crates__nom-7.1.3//:nom\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"object-0.37.3\",\n actual = \"@rust_crates__object-0.37.3//:object\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"object\",\n actual = \"@rust_crates__object-0.37.3//:object\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p256-0.13.2\",\n actual = \"@rust_crates__p256-0.13.2//:p256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p256\",\n actual = \"@rust_crates__p256-0.13.2//:p256\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p384-0.13.1\",\n actual = \"@rust_crates__p384-0.13.1//:p384\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"p384\",\n actual = \"@rust_crates__p384-0.13.1//:p384\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"paste-1.0.15\",\n actual = \"@rust_crates__paste-1.0.15//:paste\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"paste\",\n actual = \"@rust_crates__paste-1.0.15//:paste\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"proc-macro2-1.0.106\",\n actual = \"@rust_crates__proc-macro2-1.0.106//:proc_macro2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"proc-macro2\",\n actual = \"@rust_crates__proc-macro2-1.0.106//:proc_macro2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"prost-0.13.5\",\n actual = \"@rust_crates__prost-0.13.5//:prost\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"prost\",\n actual = \"@rust_crates__prost-0.13.5//:prost\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"quote-1.0.45\",\n actual = \"@rust_crates__quote-1.0.45//:quote\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"quote\",\n actual = \"@rust_crates__quote-1.0.45//:quote\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rand_core-0.9.5\",\n actual = \"@rust_crates__rand_core-0.9.5//:rand_core\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rand_core\",\n actual = \"@rust_crates__rand_core-0.9.5//:rand_core\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-0.12.1\",\n actual = \"@rust_crates__riscv-0.12.1//:riscv\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv\",\n actual = \"@rust_crates__riscv-0.12.1//:riscv\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-rt-0.12.2\",\n actual = \"@rust_crates__riscv-rt-0.12.2//:riscv_rt\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-rt\",\n actual = \"@rust_crates__riscv-rt-0.12.2//:riscv_rt\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-semihosting-0.1.3\",\n actual = \"@rust_crates__riscv-semihosting-0.1.3//:riscv_semihosting\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"riscv-semihosting\",\n actual = \"@rust_crates__riscv-semihosting-0.1.3//:riscv_semihosting\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rustc-demangle-0.1.27\",\n actual = \"@rust_crates__rustc-demangle-0.1.27//:rustc_demangle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"rustc-demangle\",\n actual = \"@rust_crates__rustc-demangle-0.1.27//:rustc_demangle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sec1-0.7.3\",\n actual = \"@rust_crates__sec1-0.7.3//:sec1\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sec1\",\n actual = \"@rust_crates__sec1-0.7.3//:sec1\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde-1.0.228\",\n actual = \"@rust_crates__serde-1.0.228//:serde\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde\",\n actual = \"@rust_crates__serde-1.0.228//:serde\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_derive-1.0.228\",\n actual = \"@rust_crates__serde_derive-1.0.228//:serde_derive\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_derive\",\n actual = \"@rust_crates__serde_derive-1.0.228//:serde_derive\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_json5-0.2.1\",\n actual = \"@rust_crates__serde_json5-0.2.1//:serde_json5\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"serde_json5\",\n actual = \"@rust_crates__serde_json5-0.2.1//:serde_json5\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha2-0.10.9\",\n actual = \"@rust_crates__sha2-0.10.9//:sha2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha2\",\n actual = \"@rust_crates__sha2-0.10.9//:sha2\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha3-0.10.8\",\n actual = \"@rust_crates__sha3-0.10.8//:sha3\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"sha3\",\n actual = \"@rust_crates__sha3-0.10.8//:sha3\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"smlang-0.8.0\",\n actual = \"@rust_crates__smlang-0.8.0//:smlang\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"smlang\",\n actual = \"@rust_crates__smlang-0.8.0//:smlang\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"subtle-2.6.1\",\n actual = \"@rust_crates__subtle-2.6.1//:subtle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"subtle\",\n actual = \"@rust_crates__subtle-2.6.1//:subtle\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn-1.0.109\",\n actual = \"@rust_crates__syn-1.0.109//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn1-1.0.109\",\n actual = \"@rust_crates__syn-1.0.109//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn1\",\n actual = \"@rust_crates__syn-1.0.109//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn-2.0.117\",\n actual = \"@rust_crates__syn-2.0.117//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn\",\n actual = \"@rust_crates__syn-2.0.117//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"thiserror-2.0.18\",\n actual = \"@rust_crates__thiserror-2.0.18//:thiserror\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"thiserror\",\n actual = \"@rust_crates__thiserror-2.0.18//:thiserror\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tock-registers-0.9.0\",\n actual = \"@rust_crates__tock-registers-0.9.0//:tock_registers\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tock-registers\",\n actual = \"@rust_crates__tock-registers-0.9.0//:tock_registers\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio-1.51.1\",\n actual = \"@rust_crates__tokio-1.51.1//:tokio\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio\",\n actual = \"@rust_crates__tokio-1.51.1//:tokio\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio-util-0.7.18\",\n actual = \"@rust_crates__tokio-util-0.7.18//:tokio_util\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"tokio-util\",\n actual = \"@rust_crates__tokio-util-0.7.18//:tokio_util\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ufmt-0.2.0\",\n actual = \"@rust_crates__ufmt-0.2.0//:ufmt\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"ufmt\",\n actual = \"@rust_crates__ufmt-0.2.0//:ufmt\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zerocopy-0.8.48\",\n actual = \"@rust_crates__zerocopy-0.8.48//:zerocopy\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zerocopy\",\n actual = \"@rust_crates__zerocopy-0.8.48//:zerocopy\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zeroize-1.8.2\",\n actual = \"@rust_crates__zeroize-1.8.2//:zeroize\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"zeroize\",\n actual = \"@rust_crates__zeroize-1.8.2//:zeroize\",\n tags = [\"manual\"],\n)\n", "alias_rules.bzl": "\"\"\"Alias that transitions its target to `compilation_mode=opt`. Use `transition_alias=\"opt\"` to enable.\"\"\"\n\nload(\"@rules_cc//cc:defs.bzl\", \"CcInfo\")\nload(\"@rules_rust//rust:rust_common.bzl\", \"COMMON_PROVIDERS\")\n\ndef _transition_alias_impl(ctx):\n # `ctx.attr.actual` is a list of 1 item due to the transition\n providers = [ctx.attr.actual[0][provider] for provider in COMMON_PROVIDERS]\n if CcInfo in ctx.attr.actual[0]:\n providers.append(ctx.attr.actual[0][CcInfo])\n return providers\n\ndef _change_compilation_mode(compilation_mode):\n def _change_compilation_mode_impl(_settings, _attr):\n return {\n \"//command_line_option:compilation_mode\": compilation_mode,\n }\n\n return transition(\n implementation = _change_compilation_mode_impl,\n inputs = [],\n outputs = [\n \"//command_line_option:compilation_mode\",\n ],\n )\n\ndef _transition_alias_rule(compilation_mode):\n return rule(\n implementation = _transition_alias_impl,\n provides = COMMON_PROVIDERS,\n attrs = {\n \"actual\": attr.label(\n mandatory = True,\n doc = \"`rust_library()` target to transition to `compilation_mode=opt`.\",\n providers = COMMON_PROVIDERS,\n cfg = _change_compilation_mode(compilation_mode),\n ),\n \"_allowlist_function_transition\": attr.label(\n default = \"@bazel_tools//tools/allowlists/function_transition_allowlist\",\n ),\n },\n doc = \"Transitions a Rust library crate to the `compilation_mode=opt`.\",\n )\n\ntransition_alias_dbg = _transition_alias_rule(\"dbg\")\ntransition_alias_fastbuild = _transition_alias_rule(\"fastbuild\")\ntransition_alias_opt = _transition_alias_rule(\"opt\")\n", - "defs.bzl": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\"\"\"\n# `crates_repository` API\n\n- [aliases](#aliases)\n- [crate_deps](#crate_deps)\n- [all_crate_deps](#all_crate_deps)\n- [crate_repositories](#crate_repositories)\n\n\"\"\"\n\nload(\"@bazel_tools//tools/build_defs/repo:git.bzl\", \"new_git_repository\")\nload(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\")\nload(\"@bazel_tools//tools/build_defs/repo:utils.bzl\", \"maybe\")\nload(\"@bazel_skylib//lib:selects.bzl\", \"selects\")\nload(\"@rules_rust//crate_universe/private:local_crate_mirror.bzl\", \"local_crate_mirror\")\n\n###############################################################################\n# MACROS API\n###############################################################################\n\n# An identifier that represent common dependencies (unconditional).\n_COMMON_CONDITION = \"\"\n\ndef _flatten_dependency_maps(all_dependency_maps):\n \"\"\"Flatten a list of dependency maps into one dictionary.\n\n Dependency maps have the following structure:\n\n ```python\n DEPENDENCIES_MAP = {\n # The first key in the map is a Bazel package\n # name of the workspace this file is defined in.\n \"workspace_member_package\": {\n\n # Not all dependencies are supported for all platforms.\n # the condition key is the condition required to be true\n # on the host platform.\n \"condition\": {\n\n # An alias to a crate target. # The label of the crate target the\n # Aliases are only crate names. # package name refers to.\n \"package_name\": \"@full//:label\",\n }\n }\n }\n ```\n\n Args:\n all_dependency_maps (list): A list of dicts as described above\n\n Returns:\n dict: A dictionary as described above\n \"\"\"\n dependencies = {}\n\n for workspace_deps_map in all_dependency_maps:\n for pkg_name, conditional_deps_map in workspace_deps_map.items():\n if pkg_name not in dependencies:\n non_frozen_map = dict()\n for key, values in conditional_deps_map.items():\n non_frozen_map.update({key: dict(values.items())})\n dependencies.setdefault(pkg_name, non_frozen_map)\n continue\n\n for condition, deps_map in conditional_deps_map.items():\n # If the condition has not been recorded, do so and continue\n if condition not in dependencies[pkg_name]:\n dependencies[pkg_name].setdefault(condition, dict(deps_map.items()))\n continue\n\n # Alert on any miss-matched dependencies\n inconsistent_entries = []\n for crate_name, crate_label in deps_map.items():\n existing = dependencies[pkg_name][condition].get(crate_name)\n if existing and existing != crate_label:\n inconsistent_entries.append((crate_name, existing, crate_label))\n dependencies[pkg_name][condition].update({crate_name: crate_label})\n\n return dependencies\n\ndef crate_deps(deps, package_name = None):\n \"\"\"Finds the fully qualified label of the requested crates for the package where this macro is called.\n\n Args:\n deps (list): The desired list of crate targets.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()`.\n\n Returns:\n list: A list of labels to generated rust targets (str)\n \"\"\"\n\n if not deps:\n return []\n\n if package_name == None:\n package_name = native.package_name()\n\n # Join both sets of dependencies\n dependencies = _flatten_dependency_maps([\n _NORMAL_DEPENDENCIES,\n _NORMAL_DEV_DEPENDENCIES,\n _PROC_MACRO_DEPENDENCIES,\n _PROC_MACRO_DEV_DEPENDENCIES,\n _BUILD_DEPENDENCIES,\n _BUILD_PROC_MACRO_DEPENDENCIES,\n ]).pop(package_name, {})\n\n # Combine all conditional packages so we can easily index over a flat list\n # TODO: Perhaps this should actually return select statements and maintain\n # the conditionals of the dependencies\n flat_deps = {}\n for deps_set in dependencies.values():\n for crate_name, crate_label in deps_set.items():\n flat_deps.update({crate_name: crate_label})\n\n missing_crates = []\n crate_targets = []\n for crate_target in deps:\n if crate_target not in flat_deps:\n missing_crates.append(crate_target)\n else:\n crate_targets.append(flat_deps[crate_target])\n\n if missing_crates:\n fail(\"Could not find crates `{}` among dependencies of `{}`. Available dependencies were `{}`\".format(\n missing_crates,\n package_name,\n dependencies,\n ))\n\n return crate_targets\n\ndef all_crate_deps(\n normal = False, \n normal_dev = False, \n proc_macro = False, \n proc_macro_dev = False,\n build = False,\n build_proc_macro = False,\n package_name = None):\n \"\"\"Finds the fully qualified label of all requested direct crate dependencies \\\n for the package where this macro is called.\n\n If no parameters are set, all normal dependencies are returned. Setting any one flag will\n otherwise impact the contents of the returned list.\n\n Args:\n normal (bool, optional): If True, normal dependencies are included in the\n output list.\n normal_dev (bool, optional): If True, normal dev dependencies will be\n included in the output list..\n proc_macro (bool, optional): If True, proc_macro dependencies are included\n in the output list.\n proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n included in the output list.\n build (bool, optional): If True, build dependencies are included\n in the output list.\n build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n included in the output list.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()` when unset.\n\n Returns:\n list: A list of labels to generated rust targets (str)\n \"\"\"\n\n if package_name == None:\n package_name = native.package_name()\n\n # Determine the relevant maps to use\n all_dependency_maps = []\n if normal:\n all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n if normal_dev:\n all_dependency_maps.append(_NORMAL_DEV_DEPENDENCIES)\n if proc_macro:\n all_dependency_maps.append(_PROC_MACRO_DEPENDENCIES)\n if proc_macro_dev:\n all_dependency_maps.append(_PROC_MACRO_DEV_DEPENDENCIES)\n if build:\n all_dependency_maps.append(_BUILD_DEPENDENCIES)\n if build_proc_macro:\n all_dependency_maps.append(_BUILD_PROC_MACRO_DEPENDENCIES)\n\n # Default to always using normal dependencies\n if not all_dependency_maps:\n all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n\n dependencies = _flatten_dependency_maps(all_dependency_maps).pop(package_name, None)\n\n if not dependencies:\n if dependencies == None:\n fail(\"Tried to get all_crate_deps for package \" + package_name + \" but that package had no Cargo.toml file\")\n else:\n return []\n\n crate_deps = list(dependencies.pop(_COMMON_CONDITION, {}).values())\n for condition, deps in dependencies.items():\n crate_deps += selects.with_or({\n tuple(_CONDITIONS[condition]): deps.values(),\n \"//conditions:default\": [],\n })\n\n return crate_deps\n\ndef aliases(\n normal = False,\n normal_dev = False,\n proc_macro = False,\n proc_macro_dev = False,\n build = False,\n build_proc_macro = False,\n package_name = None):\n \"\"\"Produces a map of Crate alias names to their original label\n\n If no dependency kinds are specified, `normal` and `proc_macro` are used by default.\n Setting any one flag will otherwise determine the contents of the returned dict.\n\n Args:\n normal (bool, optional): If True, normal dependencies are included in the\n output list.\n normal_dev (bool, optional): If True, normal dev dependencies will be\n included in the output list..\n proc_macro (bool, optional): If True, proc_macro dependencies are included\n in the output list.\n proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n included in the output list.\n build (bool, optional): If True, build dependencies are included\n in the output list.\n build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n included in the output list.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()` when unset.\n\n Returns:\n dict: The aliases of all associated packages\n \"\"\"\n if package_name == None:\n package_name = native.package_name()\n\n # Determine the relevant maps to use\n all_aliases_maps = []\n if normal:\n all_aliases_maps.append(_NORMAL_ALIASES)\n if normal_dev:\n all_aliases_maps.append(_NORMAL_DEV_ALIASES)\n if proc_macro:\n all_aliases_maps.append(_PROC_MACRO_ALIASES)\n if proc_macro_dev:\n all_aliases_maps.append(_PROC_MACRO_DEV_ALIASES)\n if build:\n all_aliases_maps.append(_BUILD_ALIASES)\n if build_proc_macro:\n all_aliases_maps.append(_BUILD_PROC_MACRO_ALIASES)\n\n # Default to always using normal aliases\n if not all_aliases_maps:\n all_aliases_maps.append(_NORMAL_ALIASES)\n all_aliases_maps.append(_PROC_MACRO_ALIASES)\n\n aliases = _flatten_dependency_maps(all_aliases_maps).pop(package_name, None)\n\n if not aliases:\n return dict()\n\n common_items = aliases.pop(_COMMON_CONDITION, {}).items()\n\n # If there are only common items in the dictionary, immediately return them\n if not len(aliases.keys()) == 1:\n return dict(common_items)\n\n # Build a single select statement where each conditional has accounted for the\n # common set of aliases.\n crate_aliases = {\"//conditions:default\": dict(common_items)}\n for condition, deps in aliases.items():\n condition_triples = _CONDITIONS[condition]\n for triple in condition_triples:\n if triple in crate_aliases:\n crate_aliases[triple].update(deps)\n else:\n crate_aliases.update({triple: dict(deps.items() + common_items)})\n\n return select(crate_aliases)\n\n###############################################################################\n# WORKSPACE MEMBER DEPS AND ALIASES\n###############################################################################\n\n_NORMAL_DEPENDENCIES = {\n \"third_party/crates_io\": {\n _COMMON_CONDITION: {\n \"aes\": Label(\"@rust_crates//:aes-0.8.4\"),\n \"aes-gcm\": Label(\"@rust_crates//:aes-gcm-0.10.3\"),\n \"aligned\": Label(\"@rust_crates//:aligned-0.4.3\"),\n \"anyhow\": Label(\"@rust_crates//:anyhow-1.0.102\"),\n \"bitfield\": Label(\"@rust_crates//:bitfield-0.14.0\"),\n \"bitflags\": Label(\"@rust_crates//:bitflags-2.11.0\"),\n \"byteorder\": Label(\"@rust_crates//:byteorder-1.5.0\"),\n \"cfg-if\": Label(\"@rust_crates//:cfg-if-1.0.4\"),\n \"cipher\": Label(\"@rust_crates//:cipher-0.4.4\"),\n \"clap\": Label(\"@rust_crates//:clap-4.6.0\"),\n \"compiler_builtins\": Label(\"@rust_crates//:compiler_builtins-0.1.160\"),\n \"cortex-m\": Label(\"@rust_crates//:cortex-m-0.7.7\"),\n \"ctr\": Label(\"@rust_crates//:ctr-0.9.2\"),\n \"ecdsa\": Label(\"@rust_crates//:ecdsa-0.16.9\"),\n \"embedded-hal\": Label(\"@rust_crates//:embedded-hal-1.0.0\"),\n \"embedded-hal-async\": Label(\"@rust_crates//:embedded-hal-async-1.0.0\"),\n \"embedded-hal-nb\": Label(\"@rust_crates//:embedded-hal-nb-1.0.0\"),\n \"embedded-io\": Label(\"@rust_crates//:embedded-io-0.6.1\"),\n \"futures\": Label(\"@rust_crates//:futures-0.3.32\"),\n \"heapless\": Label(\"@rust_crates//:heapless-0.9.2\"),\n \"hex\": Label(\"@rust_crates//:hex-0.4.3\"),\n \"hmac\": Label(\"@rust_crates//:hmac-0.12.1\"),\n \"k256\": Label(\"@rust_crates//:k256-0.13.4\"),\n \"log\": Label(\"@rust_crates//:log-0.4.29\"),\n \"memoffset\": Label(\"@rust_crates//:memoffset-0.9.1\"),\n \"minijinja\": Label(\"@rust_crates//:minijinja-2.19.0\"),\n \"nb\": Label(\"@rust_crates//:nb-1.1.0\"),\n \"nom\": Label(\"@rust_crates//:nom-7.1.3\"),\n \"object\": Label(\"@rust_crates//:object-0.37.3\"),\n \"p256\": Label(\"@rust_crates//:p256-0.13.2\"),\n \"p384\": Label(\"@rust_crates//:p384-0.13.1\"),\n \"proc-macro2\": Label(\"@rust_crates//:proc-macro2-1.0.106\"),\n \"prost\": Label(\"@rust_crates//:prost-0.13.5\"),\n \"quote\": Label(\"@rust_crates//:quote-1.0.45\"),\n \"rand_core\": Label(\"@rust_crates//:rand_core-0.9.5\"),\n \"riscv\": Label(\"@rust_crates//:riscv-0.12.1\"),\n \"riscv-rt\": Label(\"@rust_crates//:riscv-rt-0.12.2\"),\n \"riscv-semihosting\": Label(\"@rust_crates//:riscv-semihosting-0.1.3\"),\n \"rustc-demangle\": Label(\"@rust_crates//:rustc-demangle-0.1.27\"),\n \"sec1\": Label(\"@rust_crates//:sec1-0.7.3\"),\n \"serde\": Label(\"@rust_crates//:serde-1.0.228\"),\n \"serde_json5\": Label(\"@rust_crates//:serde_json5-0.2.1\"),\n \"sha2\": Label(\"@rust_crates//:sha2-0.10.9\"),\n \"sha3\": Label(\"@rust_crates//:sha3-0.10.8\"),\n \"smlang\": Label(\"@rust_crates//:smlang-0.8.0\"),\n \"subtle\": Label(\"@rust_crates//:subtle-2.6.1\"),\n \"syn1\": Label(\"@rust_crates//:syn-1.0.109\"),\n \"syn\": Label(\"@rust_crates//:syn-2.0.117\"),\n \"thiserror\": Label(\"@rust_crates//:thiserror-2.0.18\"),\n \"tock-registers\": Label(\"@rust_crates//:tock-registers-0.9.0\"),\n \"tokio\": Label(\"@rust_crates//:tokio-1.51.1\"),\n \"tokio-util\": Label(\"@rust_crates//:tokio-util-0.7.18\"),\n \"zerocopy\": Label(\"@rust_crates//:zerocopy-0.8.48\"),\n \"zeroize\": Label(\"@rust_crates//:zeroize-1.8.2\"),\n },\n },\n}\n\n\n_NORMAL_ALIASES = {\n \"third_party/crates_io\": {\n _COMMON_CONDITION: {\n Label(\"@rust_crates//:syn-1.0.109\"): \"syn1\",\n },\n },\n}\n\n\n_NORMAL_DEV_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_NORMAL_DEV_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_PROC_MACRO_DEPENDENCIES = {\n \"third_party/crates_io\": {\n _COMMON_CONDITION: {\n \"bitfield-struct\": Label(\"@rust_crates//:bitfield-struct-0.11.0\"),\n \"paste\": Label(\"@rust_crates//:paste-1.0.15\"),\n \"serde_derive\": Label(\"@rust_crates//:serde_derive-1.0.228\"),\n },\n },\n}\n\n\n_PROC_MACRO_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_PROC_MACRO_DEV_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_PROC_MACRO_DEV_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_PROC_MACRO_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_PROC_MACRO_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_CONDITIONS = {\n \"aarch64-apple-darwin\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\"],\n \"aarch64-linux-android\": [],\n \"aarch64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\": [],\n \"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\",\"@rules_rust//rust/platform:x86_64-apple-darwin\"],\n \"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"linux\\\"))\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"cfg(all(target_arch = \\\"aarch64\\\", target_vendor = \\\"apple\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\"],\n \"cfg(all(target_arch = \\\"loongarch64\\\", target_os = \\\"linux\\\"))\": [],\n \"cfg(any())\": [],\n \"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(any(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\", target_pointer_width = \\\"64\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(any(unix, target_os = \\\"hermit\\\", target_os = \\\"wasi\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(any(unix, target_os = \\\"wasi\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(target_arch = \\\"aarch64\\\")\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"cfg(target_os = \\\"hermit\\\")\": [],\n \"cfg(target_os = \\\"redox\\\")\": [],\n \"cfg(target_os = \\\"wasi\\\")\": [],\n \"cfg(unix)\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(windows)\": [],\n \"riscv32imc-unknown-none-elf\": [\"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\"],\n \"x86_64-apple-darwin\": [\"@rules_rust//rust/platform:x86_64-apple-darwin\"],\n \"x86_64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n}\n\n###############################################################################\n\ndef crate_repositories():\n \"\"\"A macro for defining repositories for all generated crates.\n\n Returns:\n A list of repos visible to the module through the module extension.\n \"\"\"\n maybe(\n http_archive,\n name = \"rust_crates__adler2-2.0.1\",\n sha256 = \"320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/adler2/2.0.1/download\"],\n strip_prefix = \"adler2-2.0.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.adler2-2.0.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aead-0.5.2\",\n sha256 = \"d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aead/0.5.2/download\"],\n strip_prefix = \"aead-0.5.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aead-0.5.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aes-0.8.4\",\n sha256 = \"b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aes/0.8.4/download\"],\n strip_prefix = \"aes-0.8.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aes-0.8.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aes-gcm-0.10.3\",\n sha256 = \"831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aes-gcm/0.10.3/download\"],\n strip_prefix = \"aes-gcm-0.10.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aes-gcm-0.10.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aligned-0.4.3\",\n sha256 = \"ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aligned/0.4.3/download\"],\n strip_prefix = \"aligned-0.4.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aligned-0.4.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstream-1.0.0\",\n sha256 = \"824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstream/1.0.0/download\"],\n strip_prefix = \"anstream-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstream-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-1.0.14\",\n sha256 = \"940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle/1.0.14/download\"],\n strip_prefix = \"anstyle-1.0.14\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-1.0.14.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-parse-1.0.0\",\n sha256 = \"52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle-parse/1.0.0/download\"],\n strip_prefix = \"anstyle-parse-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-parse-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-query-1.1.5\",\n sha256 = \"40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle-query/1.1.5/download\"],\n strip_prefix = \"anstyle-query-1.1.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-query-1.1.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-wincon-3.0.11\",\n sha256 = \"291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle-wincon/3.0.11/download\"],\n strip_prefix = \"anstyle-wincon-3.0.11\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-wincon-3.0.11.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anyhow-1.0.102\",\n sha256 = \"7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anyhow/1.0.102/download\"],\n strip_prefix = \"anyhow-1.0.102\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anyhow-1.0.102.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__as-slice-0.2.1\",\n sha256 = \"516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/as-slice/0.2.1/download\"],\n strip_prefix = \"as-slice-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.as-slice-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__autocfg-1.5.0\",\n sha256 = \"c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/autocfg/1.5.0/download\"],\n strip_prefix = \"autocfg-1.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.autocfg-1.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bare-metal-0.2.5\",\n sha256 = \"5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bare-metal/0.2.5/download\"],\n strip_prefix = \"bare-metal-0.2.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bare-metal-0.2.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__base16ct-0.2.0\",\n sha256 = \"4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/base16ct/0.2.0/download\"],\n strip_prefix = \"base16ct-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.base16ct-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitfield-0.13.2\",\n sha256 = \"46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitfield/0.13.2/download\"],\n strip_prefix = \"bitfield-0.13.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitfield-0.13.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitfield-0.14.0\",\n sha256 = \"2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitfield/0.14.0/download\"],\n strip_prefix = \"bitfield-0.14.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitfield-0.14.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitfield-struct-0.11.0\",\n sha256 = \"d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitfield-struct/0.11.0/download\"],\n strip_prefix = \"bitfield-struct-0.11.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitfield-struct-0.11.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitflags-2.11.0\",\n sha256 = \"843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitflags/2.11.0/download\"],\n strip_prefix = \"bitflags-2.11.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitflags-2.11.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__block-buffer-0.10.4\",\n sha256 = \"3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/block-buffer/0.10.4/download\"],\n strip_prefix = \"block-buffer-0.10.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.block-buffer-0.10.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__byteorder-1.5.0\",\n sha256 = \"1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/byteorder/1.5.0/download\"],\n strip_prefix = \"byteorder-1.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.byteorder-1.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bytes-1.11.1\",\n sha256 = \"1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bytes/1.11.1/download\"],\n strip_prefix = \"bytes-1.11.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bytes-1.11.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cfg-if-1.0.4\",\n sha256 = \"9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cfg-if/1.0.4/download\"],\n strip_prefix = \"cfg-if-1.0.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cfg-if-1.0.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cipher-0.4.4\",\n sha256 = \"773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cipher/0.4.4/download\"],\n strip_prefix = \"cipher-0.4.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cipher-0.4.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap-4.6.0\",\n sha256 = \"b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap/4.6.0/download\"],\n strip_prefix = \"clap-4.6.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap-4.6.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap_builder-4.6.0\",\n sha256 = \"714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap_builder/4.6.0/download\"],\n strip_prefix = \"clap_builder-4.6.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap_builder-4.6.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap_derive-4.6.0\",\n sha256 = \"1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap_derive/4.6.0/download\"],\n strip_prefix = \"clap_derive-4.6.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap_derive-4.6.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap_lex-1.1.0\",\n sha256 = \"c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap_lex/1.1.0/download\"],\n strip_prefix = \"clap_lex-1.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap_lex-1.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__colorchoice-1.0.5\",\n sha256 = \"1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/colorchoice/1.0.5/download\"],\n strip_prefix = \"colorchoice-1.0.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.colorchoice-1.0.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__compiler_builtins-0.1.160\",\n sha256 = \"6376049cfa92c0aa8b9ac95fae22184b981c658208d4ed8a1dc553cd83612895\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/compiler_builtins/0.1.160/download\"],\n strip_prefix = \"compiler_builtins-0.1.160\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.compiler_builtins-0.1.160.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__const-oid-0.9.6\",\n sha256 = \"c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/const-oid/0.9.6/download\"],\n strip_prefix = \"const-oid-0.9.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.const-oid-0.9.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cortex-m-0.7.7\",\n sha256 = \"8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cortex-m/0.7.7/download\"],\n strip_prefix = \"cortex-m-0.7.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cortex-m-0.7.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cpufeatures-0.2.17\",\n sha256 = \"59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cpufeatures/0.2.17/download\"],\n strip_prefix = \"cpufeatures-0.2.17\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cpufeatures-0.2.17.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__crc32fast-1.5.0\",\n sha256 = \"9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/crc32fast/1.5.0/download\"],\n strip_prefix = \"crc32fast-1.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.crc32fast-1.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__critical-section-1.2.0\",\n sha256 = \"790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/critical-section/1.2.0/download\"],\n strip_prefix = \"critical-section-1.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.critical-section-1.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__crypto-bigint-0.5.5\",\n sha256 = \"0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/crypto-bigint/0.5.5/download\"],\n strip_prefix = \"crypto-bigint-0.5.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.crypto-bigint-0.5.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__crypto-common-0.1.7\",\n sha256 = \"78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/crypto-common/0.1.7/download\"],\n strip_prefix = \"crypto-common-0.1.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.crypto-common-0.1.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ctr-0.9.2\",\n sha256 = \"0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ctr/0.9.2/download\"],\n strip_prefix = \"ctr-0.9.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ctr-0.9.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__der-0.7.10\",\n sha256 = \"e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/der/0.7.10/download\"],\n strip_prefix = \"der-0.7.10\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.der-0.7.10.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__digest-0.10.7\",\n sha256 = \"9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/digest/0.10.7/download\"],\n strip_prefix = \"digest-0.10.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.digest-0.10.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ecdsa-0.16.9\",\n sha256 = \"ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ecdsa/0.16.9/download\"],\n strip_prefix = \"ecdsa-0.16.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ecdsa-0.16.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__either-1.15.0\",\n sha256 = \"48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/either/1.15.0/download\"],\n strip_prefix = \"either-1.15.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.either-1.15.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__elliptic-curve-0.13.8\",\n sha256 = \"b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/elliptic-curve/0.13.8/download\"],\n strip_prefix = \"elliptic-curve-0.13.8\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.elliptic-curve-0.13.8.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-0.2.7\",\n sha256 = \"35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal/0.2.7/download\"],\n strip_prefix = \"embedded-hal-0.2.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-0.2.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-1.0.0\",\n sha256 = \"361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal/1.0.0/download\"],\n strip_prefix = \"embedded-hal-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-async-1.0.0\",\n sha256 = \"0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal-async/1.0.0/download\"],\n strip_prefix = \"embedded-hal-async-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-async-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-nb-1.0.0\",\n sha256 = \"fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal-nb/1.0.0/download\"],\n strip_prefix = \"embedded-hal-nb-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-nb-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-io-0.6.1\",\n sha256 = \"edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-io/0.6.1/download\"],\n strip_prefix = \"embedded-io-0.6.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-io-0.6.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__equivalent-1.0.2\",\n sha256 = \"877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/equivalent/1.0.2/download\"],\n strip_prefix = \"equivalent-1.0.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.equivalent-1.0.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__errno-0.3.14\",\n sha256 = \"39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/errno/0.3.14/download\"],\n strip_prefix = \"errno-0.3.14\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.errno-0.3.14.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ff-0.13.1\",\n sha256 = \"c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ff/0.13.1/download\"],\n strip_prefix = \"ff-0.13.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ff-0.13.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__flate2-1.1.9\",\n sha256 = \"843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/flate2/1.1.9/download\"],\n strip_prefix = \"flate2-1.1.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.flate2-1.1.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__foldhash-0.1.5\",\n sha256 = \"d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/foldhash/0.1.5/download\"],\n strip_prefix = \"foldhash-0.1.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.foldhash-0.1.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-0.3.32\",\n sha256 = \"8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures/0.3.32/download\"],\n strip_prefix = \"futures-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-channel-0.3.32\",\n sha256 = \"07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-channel/0.3.32/download\"],\n strip_prefix = \"futures-channel-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-channel-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-core-0.3.32\",\n sha256 = \"7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-core/0.3.32/download\"],\n strip_prefix = \"futures-core-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-core-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-executor-0.3.32\",\n sha256 = \"baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-executor/0.3.32/download\"],\n strip_prefix = \"futures-executor-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-executor-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-io-0.3.32\",\n sha256 = \"cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-io/0.3.32/download\"],\n strip_prefix = \"futures-io-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-io-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-macro-0.3.32\",\n sha256 = \"e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-macro/0.3.32/download\"],\n strip_prefix = \"futures-macro-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-macro-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-sink-0.3.32\",\n sha256 = \"c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-sink/0.3.32/download\"],\n strip_prefix = \"futures-sink-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-sink-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-task-0.3.32\",\n sha256 = \"037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-task/0.3.32/download\"],\n strip_prefix = \"futures-task-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-task-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-util-0.3.32\",\n sha256 = \"389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-util/0.3.32/download\"],\n strip_prefix = \"futures-util-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-util-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__generic-array-0.14.7\",\n sha256 = \"85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/generic-array/0.14.7/download\"],\n strip_prefix = \"generic-array-0.14.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.generic-array-0.14.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ghash-0.5.1\",\n sha256 = \"f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ghash/0.5.1/download\"],\n strip_prefix = \"ghash-0.5.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ghash-0.5.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__group-0.13.0\",\n sha256 = \"f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/group/0.13.0/download\"],\n strip_prefix = \"group-0.13.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.group-0.13.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hash32-0.3.1\",\n sha256 = \"47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hash32/0.3.1/download\"],\n strip_prefix = \"hash32-0.3.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hash32-0.3.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hashbrown-0.15.5\",\n sha256 = \"9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hashbrown/0.15.5/download\"],\n strip_prefix = \"hashbrown-0.15.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hashbrown-0.15.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hashbrown-0.17.0\",\n sha256 = \"4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hashbrown/0.17.0/download\"],\n strip_prefix = \"hashbrown-0.17.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hashbrown-0.17.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__heapless-0.9.2\",\n sha256 = \"2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/heapless/0.9.2/download\"],\n strip_prefix = \"heapless-0.9.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.heapless-0.9.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__heck-0.5.0\",\n sha256 = \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/heck/0.5.0/download\"],\n strip_prefix = \"heck-0.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.heck-0.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hex-0.4.3\",\n sha256 = \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hex/0.4.3/download\"],\n strip_prefix = \"hex-0.4.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hex-0.4.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hmac-0.12.1\",\n sha256 = \"6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hmac/0.12.1/download\"],\n strip_prefix = \"hmac-0.12.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hmac-0.12.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__indexmap-2.14.0\",\n sha256 = \"d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/indexmap/2.14.0/download\"],\n strip_prefix = \"indexmap-2.14.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.indexmap-2.14.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__inout-0.1.4\",\n sha256 = \"879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/inout/0.1.4/download\"],\n strip_prefix = \"inout-0.1.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.inout-0.1.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__is_terminal_polyfill-1.70.2\",\n sha256 = \"a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/is_terminal_polyfill/1.70.2/download\"],\n strip_prefix = \"is_terminal_polyfill-1.70.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.is_terminal_polyfill-1.70.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__itertools-0.14.0\",\n sha256 = \"2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/itertools/0.14.0/download\"],\n strip_prefix = \"itertools-0.14.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.itertools-0.14.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__k256-0.13.4\",\n sha256 = \"f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/k256/0.13.4/download\"],\n strip_prefix = \"k256-0.13.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.k256-0.13.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__keccak-0.1.6\",\n sha256 = \"cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/keccak/0.1.6/download\"],\n strip_prefix = \"keccak-0.1.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.keccak-0.1.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__libc-0.2.184\",\n sha256 = \"48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/libc/0.2.184/download\"],\n strip_prefix = \"libc-0.2.184\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.libc-0.2.184.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__linux-raw-sys-0.12.1\",\n sha256 = \"32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/linux-raw-sys/0.12.1/download\"],\n strip_prefix = \"linux-raw-sys-0.12.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.linux-raw-sys-0.12.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__lock_api-0.4.14\",\n sha256 = \"224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/lock_api/0.4.14/download\"],\n strip_prefix = \"lock_api-0.4.14\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.lock_api-0.4.14.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__log-0.4.29\",\n sha256 = \"5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/log/0.4.29/download\"],\n strip_prefix = \"log-0.4.29\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.log-0.4.29.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__memchr-2.8.0\",\n sha256 = \"f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memchr/2.8.0/download\"],\n strip_prefix = \"memchr-2.8.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.memchr-2.8.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__memo-map-0.3.3\",\n sha256 = \"38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memo-map/0.3.3/download\"],\n strip_prefix = \"memo-map-0.3.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.memo-map-0.3.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__memoffset-0.9.1\",\n sha256 = \"488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memoffset/0.9.1/download\"],\n strip_prefix = \"memoffset-0.9.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.memoffset-0.9.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__minijinja-2.19.0\",\n sha256 = \"805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/minijinja/2.19.0/download\"],\n strip_prefix = \"minijinja-2.19.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.minijinja-2.19.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__minimal-lexical-0.2.1\",\n sha256 = \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/minimal-lexical/0.2.1/download\"],\n strip_prefix = \"minimal-lexical-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.minimal-lexical-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__miniz_oxide-0.8.9\",\n sha256 = \"1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/miniz_oxide/0.8.9/download\"],\n strip_prefix = \"miniz_oxide-0.8.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.miniz_oxide-0.8.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__mio-1.2.0\",\n sha256 = \"50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/mio/1.2.0/download\"],\n strip_prefix = \"mio-1.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.mio-1.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__nb-0.1.3\",\n sha256 = \"801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/nb/0.1.3/download\"],\n strip_prefix = \"nb-0.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.nb-0.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__nb-1.1.0\",\n sha256 = \"8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/nb/1.1.0/download\"],\n strip_prefix = \"nb-1.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.nb-1.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__nom-7.1.3\",\n sha256 = \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/nom/7.1.3/download\"],\n strip_prefix = \"nom-7.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.nom-7.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__object-0.37.3\",\n sha256 = \"ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/object/0.37.3/download\"],\n strip_prefix = \"object-0.37.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.object-0.37.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__once_cell_polyfill-1.70.2\",\n sha256 = \"384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/once_cell_polyfill/1.70.2/download\"],\n strip_prefix = \"once_cell_polyfill-1.70.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.once_cell_polyfill-1.70.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__opaque-debug-0.3.1\",\n sha256 = \"c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/opaque-debug/0.3.1/download\"],\n strip_prefix = \"opaque-debug-0.3.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.opaque-debug-0.3.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__p256-0.13.2\",\n sha256 = \"c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/p256/0.13.2/download\"],\n strip_prefix = \"p256-0.13.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.p256-0.13.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__p384-0.13.1\",\n sha256 = \"fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/p384/0.13.1/download\"],\n strip_prefix = \"p384-0.13.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.p384-0.13.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__parking_lot-0.12.5\",\n sha256 = \"93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/parking_lot/0.12.5/download\"],\n strip_prefix = \"parking_lot-0.12.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.parking_lot-0.12.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__parking_lot_core-0.9.12\",\n sha256 = \"2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/parking_lot_core/0.9.12/download\"],\n strip_prefix = \"parking_lot_core-0.9.12\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.parking_lot_core-0.9.12.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__paste-1.0.15\",\n sha256 = \"57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/paste/1.0.15/download\"],\n strip_prefix = \"paste-1.0.15\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.paste-1.0.15.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest-2.8.6\",\n sha256 = \"e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest/2.8.6/download\"],\n strip_prefix = \"pest-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest_derive-2.8.6\",\n sha256 = \"11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest_derive/2.8.6/download\"],\n strip_prefix = \"pest_derive-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest_derive-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest_generator-2.8.6\",\n sha256 = \"8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest_generator/2.8.6/download\"],\n strip_prefix = \"pest_generator-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest_generator-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest_meta-2.8.6\",\n sha256 = \"89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest_meta/2.8.6/download\"],\n strip_prefix = \"pest_meta-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest_meta-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pin-project-lite-0.2.17\",\n sha256 = \"a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pin-project-lite/0.2.17/download\"],\n strip_prefix = \"pin-project-lite-0.2.17\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pin-project-lite-0.2.17.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__polyval-0.6.2\",\n sha256 = \"9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/polyval/0.6.2/download\"],\n strip_prefix = \"polyval-0.6.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.polyval-0.6.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__primeorder-0.13.6\",\n sha256 = \"353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/primeorder/0.13.6/download\"],\n strip_prefix = \"primeorder-0.13.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.primeorder-0.13.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__proc-macro2-1.0.106\",\n sha256 = \"8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/proc-macro2/1.0.106/download\"],\n strip_prefix = \"proc-macro2-1.0.106\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.proc-macro2-1.0.106.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__prost-0.13.5\",\n sha256 = \"2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/prost/0.13.5/download\"],\n strip_prefix = \"prost-0.13.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.prost-0.13.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__prost-derive-0.13.5\",\n sha256 = \"8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/prost-derive/0.13.5/download\"],\n strip_prefix = \"prost-derive-0.13.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.prost-derive-0.13.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__quote-1.0.45\",\n sha256 = \"41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/quote/1.0.45/download\"],\n strip_prefix = \"quote-1.0.45\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.quote-1.0.45.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rand_core-0.6.4\",\n sha256 = \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rand_core/0.6.4/download\"],\n strip_prefix = \"rand_core-0.6.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rand_core-0.6.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rand_core-0.9.5\",\n sha256 = \"76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rand_core/0.9.5/download\"],\n strip_prefix = \"rand_core-0.9.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rand_core-0.9.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__redox_syscall-0.5.18\",\n sha256 = \"ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/redox_syscall/0.5.18/download\"],\n strip_prefix = \"redox_syscall-0.5.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.redox_syscall-0.5.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rfc6979-0.4.0\",\n sha256 = \"f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rfc6979/0.4.0/download\"],\n strip_prefix = \"rfc6979-0.4.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rfc6979-0.4.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-0.11.1\",\n sha256 = \"2f5c1b8bf41ea746266cdee443d1d1e9125c86ce1447e1a2615abd34330d33a9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv/0.11.1/download\"],\n strip_prefix = \"riscv-0.11.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-0.11.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-0.12.1\",\n sha256 = \"5ea8ff73d3720bdd0a97925f0bf79ad2744b6da8ff36be3840c48ac81191d7a7\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv/0.12.1/download\"],\n strip_prefix = \"riscv-0.12.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-0.12.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-0.13.0\",\n sha256 = \"afa3cdbeccae4359f6839a00e8b77e5736caa200ba216caf38d24e4c16e2b586\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv/0.13.0/download\"],\n strip_prefix = \"riscv-0.13.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-0.13.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-macros-0.1.0\",\n sha256 = \"f265be5d634272320a7de94cea15c22a3bfdd4eb42eb43edc528415f066a1f25\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-macros/0.1.0/download\"],\n strip_prefix = \"riscv-macros-0.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-macros-0.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-macros-0.2.0\",\n sha256 = \"e8c4aa1ea1af6dcc83a61be12e8189f9b293c3ba5a487778a4cd89fb060fdbbc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-macros/0.2.0/download\"],\n strip_prefix = \"riscv-macros-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-macros-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-pac-0.2.0\",\n sha256 = \"8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-pac/0.2.0/download\"],\n strip_prefix = \"riscv-pac-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-pac-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-rt-0.12.2\",\n sha256 = \"c0d35e32cf1383183e8885d8a9aa4402a087fd094dc34c2cb6df6687d0229dfe\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-rt/0.12.2/download\"],\n strip_prefix = \"riscv-rt-0.12.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-rt-0.12.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-rt-macros-0.2.2\",\n sha256 = \"30f19a85fe107b65031e0ba8ec60c34c2494069fe910d6c297f5e7cb5a6f76d0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-rt-macros/0.2.2/download\"],\n strip_prefix = \"riscv-rt-macros-0.2.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-rt-macros-0.2.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-semihosting-0.1.3\",\n sha256 = \"1086dd4bcc13de1cb14b93849411e3466de7e5907d2d8eb269032536e93facc6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-semihosting/0.1.3/download\"],\n strip_prefix = \"riscv-semihosting-0.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-semihosting-0.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rustc-demangle-0.1.27\",\n sha256 = \"b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustc-demangle/0.1.27/download\"],\n strip_prefix = \"rustc-demangle-0.1.27\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rustc-demangle-0.1.27.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rustc_version-0.2.3\",\n sha256 = \"138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustc_version/0.2.3/download\"],\n strip_prefix = \"rustc_version-0.2.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rustc_version-0.2.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rustix-1.1.4\",\n sha256 = \"b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustix/1.1.4/download\"],\n strip_prefix = \"rustix-1.1.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rustix-1.1.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ruzstd-0.8.2\",\n sha256 = \"e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ruzstd/0.8.2/download\"],\n strip_prefix = \"ruzstd-0.8.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ruzstd-0.8.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__scopeguard-1.2.0\",\n sha256 = \"94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/scopeguard/1.2.0/download\"],\n strip_prefix = \"scopeguard-1.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.scopeguard-1.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__sec1-0.7.3\",\n sha256 = \"d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/sec1/0.7.3/download\"],\n strip_prefix = \"sec1-0.7.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.sec1-0.7.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__semver-0.9.0\",\n sha256 = \"1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/semver/0.9.0/download\"],\n strip_prefix = \"semver-0.9.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.semver-0.9.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__semver-parser-0.7.0\",\n sha256 = \"388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/semver-parser/0.7.0/download\"],\n strip_prefix = \"semver-parser-0.7.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.semver-parser-0.7.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde-1.0.228\",\n sha256 = \"9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde/1.0.228/download\"],\n strip_prefix = \"serde-1.0.228\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde-1.0.228.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde_core-1.0.228\",\n sha256 = \"41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde_core/1.0.228/download\"],\n strip_prefix = \"serde_core-1.0.228\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde_core-1.0.228.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde_derive-1.0.228\",\n sha256 = \"d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde_derive/1.0.228/download\"],\n strip_prefix = \"serde_derive-1.0.228\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde_derive-1.0.228.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde_json5-0.2.1\",\n sha256 = \"5d34d03f54462862f2a42918391c9526337f53171eaa4d8894562be7f252edd3\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde_json5/0.2.1/download\"],\n strip_prefix = \"serde_json5-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde_json5-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__sha2-0.10.9\",\n sha256 = \"a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/sha2/0.10.9/download\"],\n strip_prefix = \"sha2-0.10.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.sha2-0.10.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__sha3-0.10.8\",\n sha256 = \"75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/sha3/0.10.8/download\"],\n strip_prefix = \"sha3-0.10.8\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.sha3-0.10.8.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__signal-hook-registry-1.4.8\",\n sha256 = \"c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/signal-hook-registry/1.4.8/download\"],\n strip_prefix = \"signal-hook-registry-1.4.8\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.signal-hook-registry-1.4.8.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__signature-2.2.0\",\n sha256 = \"77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/signature/2.2.0/download\"],\n strip_prefix = \"signature-2.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.signature-2.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__simd-adler32-0.3.9\",\n sha256 = \"703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/simd-adler32/0.3.9/download\"],\n strip_prefix = \"simd-adler32-0.3.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.simd-adler32-0.3.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__slab-0.4.12\",\n sha256 = \"0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/slab/0.4.12/download\"],\n strip_prefix = \"slab-0.4.12\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.slab-0.4.12.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__smallvec-1.15.1\",\n sha256 = \"67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/smallvec/1.15.1/download\"],\n strip_prefix = \"smallvec-1.15.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.smallvec-1.15.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__smlang-0.8.0\",\n sha256 = \"1de84f9f80bbe6272174e2bfdb8cf7ce4815b218038a42161c2f21c1d872c215\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/smlang/0.8.0/download\"],\n strip_prefix = \"smlang-0.8.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.smlang-0.8.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__smlang-macros-0.8.0\",\n sha256 = \"231b4425dcc43afc7e18c34e7c6738cd252d42d91d909c948df14107c9ae79f1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/smlang-macros/0.8.0/download\"],\n strip_prefix = \"smlang-macros-0.8.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.smlang-macros-0.8.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__socket2-0.6.3\",\n sha256 = \"3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/socket2/0.6.3/download\"],\n strip_prefix = \"socket2-0.6.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.socket2-0.6.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__stable_deref_trait-1.2.1\",\n sha256 = \"6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/stable_deref_trait/1.2.1/download\"],\n strip_prefix = \"stable_deref_trait-1.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.stable_deref_trait-1.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__string_morph-0.1.0\",\n sha256 = \"183aaf7fa637cc7b5f54c45b8f7cb6e8d73831f9f75a56b6defa5bf8c51d1699\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/string_morph/0.1.0/download\"],\n strip_prefix = \"string_morph-0.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.string_morph-0.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__strsim-0.11.1\",\n sha256 = \"7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/strsim/0.11.1/download\"],\n strip_prefix = \"strsim-0.11.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.strsim-0.11.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__subtle-2.6.1\",\n sha256 = \"13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/subtle/2.6.1/download\"],\n strip_prefix = \"subtle-2.6.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.subtle-2.6.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__syn-1.0.109\",\n sha256 = \"72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/syn/1.0.109/download\"],\n strip_prefix = \"syn-1.0.109\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.syn-1.0.109.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__syn-2.0.117\",\n sha256 = \"e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/syn/2.0.117/download\"],\n strip_prefix = \"syn-2.0.117\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.syn-2.0.117.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__terminal_size-0.4.4\",\n sha256 = \"230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/terminal_size/0.4.4/download\"],\n strip_prefix = \"terminal_size-0.4.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.terminal_size-0.4.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__thiserror-2.0.18\",\n sha256 = \"4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/thiserror/2.0.18/download\"],\n strip_prefix = \"thiserror-2.0.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.thiserror-2.0.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__thiserror-impl-2.0.18\",\n sha256 = \"ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/thiserror-impl/2.0.18/download\"],\n strip_prefix = \"thiserror-impl-2.0.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.thiserror-impl-2.0.18.bazel\"),\n )\n\n maybe(\n new_git_repository,\n name = \"rust_crates__tock-registers-0.9.0\",\n commit = \"9554639b17501a9f5940cef7a1770a0823e790c3\",\n init_submodules = True,\n remote = \"https://github.com/tock/tock.git\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tock-registers-0.9.0.bazel\"),\n strip_prefix = \"libraries/tock-register-interface\",\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__tokio-1.51.1\",\n sha256 = \"f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/tokio/1.51.1/download\"],\n strip_prefix = \"tokio-1.51.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tokio-1.51.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__tokio-macros-2.7.0\",\n sha256 = \"385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/tokio-macros/2.7.0/download\"],\n strip_prefix = \"tokio-macros-2.7.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tokio-macros-2.7.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__tokio-util-0.7.18\",\n sha256 = \"9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/tokio-util/0.7.18/download\"],\n strip_prefix = \"tokio-util-0.7.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tokio-util-0.7.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__twox-hash-2.1.2\",\n sha256 = \"9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/twox-hash/2.1.2/download\"],\n strip_prefix = \"twox-hash-2.1.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.twox-hash-2.1.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__typenum-1.19.0\",\n sha256 = \"562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/typenum/1.19.0/download\"],\n strip_prefix = \"typenum-1.19.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.typenum-1.19.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ucd-trie-0.1.7\",\n sha256 = \"2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ucd-trie/0.1.7/download\"],\n strip_prefix = \"ucd-trie-0.1.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ucd-trie-0.1.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__unicode-ident-1.0.24\",\n sha256 = \"e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/unicode-ident/1.0.24/download\"],\n strip_prefix = \"unicode-ident-1.0.24\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.unicode-ident-1.0.24.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__universal-hash-0.5.1\",\n sha256 = \"fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/universal-hash/0.5.1/download\"],\n strip_prefix = \"universal-hash-0.5.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.universal-hash-0.5.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__utf8parse-0.2.2\",\n sha256 = \"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/utf8parse/0.2.2/download\"],\n strip_prefix = \"utf8parse-0.2.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.utf8parse-0.2.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__vcell-0.1.3\",\n sha256 = \"77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/vcell/0.1.3/download\"],\n strip_prefix = \"vcell-0.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.vcell-0.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__version_check-0.9.5\",\n sha256 = \"0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/version_check/0.9.5/download\"],\n strip_prefix = \"version_check-0.9.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.version_check-0.9.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__void-1.0.2\",\n sha256 = \"6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/void/1.0.2/download\"],\n strip_prefix = \"void-1.0.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.void-1.0.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__volatile-register-0.2.2\",\n sha256 = \"de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/volatile-register/0.2.2/download\"],\n strip_prefix = \"volatile-register-0.2.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.volatile-register-0.2.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__wasi-0.11.1-wasi-snapshot-preview1\",\n sha256 = \"ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/wasi/0.11.1+wasi-snapshot-preview1/download\"],\n strip_prefix = \"wasi-0.11.1+wasi-snapshot-preview1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.wasi-0.11.1+wasi-snapshot-preview1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__windows-link-0.2.1\",\n sha256 = \"f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/windows-link/0.2.1/download\"],\n strip_prefix = \"windows-link-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.windows-link-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__windows-sys-0.61.2\",\n sha256 = \"ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/windows-sys/0.61.2/download\"],\n strip_prefix = \"windows-sys-0.61.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.windows-sys-0.61.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zerocopy-0.8.48\",\n sha256 = \"eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zerocopy/0.8.48/download\"],\n strip_prefix = \"zerocopy-0.8.48\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zerocopy-0.8.48.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zerocopy-derive-0.8.48\",\n sha256 = \"70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zerocopy-derive/0.8.48/download\"],\n strip_prefix = \"zerocopy-derive-0.8.48\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zerocopy-derive-0.8.48.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zeroize-1.8.2\",\n sha256 = \"b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zeroize/1.8.2/download\"],\n strip_prefix = \"zeroize-1.8.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zeroize-1.8.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zeroize_derive-1.4.3\",\n sha256 = \"85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zeroize_derive/1.4.3/download\"],\n strip_prefix = \"zeroize_derive-1.4.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zeroize_derive-1.4.3.bazel\"),\n )\n\n return [\n struct(repo=\"rust_crates__aes-0.8.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__aes-gcm-0.10.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__aligned-0.4.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__anyhow-1.0.102\", is_dev_dep = False),\n struct(repo=\"rust_crates__bitfield-0.14.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__bitfield-struct-0.11.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__bitflags-2.11.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__byteorder-1.5.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__cfg-if-1.0.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__cipher-0.4.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__clap-4.6.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__compiler_builtins-0.1.160\", is_dev_dep = False),\n struct(repo=\"rust_crates__cortex-m-0.7.7\", is_dev_dep = False),\n struct(repo=\"rust_crates__ctr-0.9.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__ecdsa-0.16.9\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-hal-1.0.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-hal-async-1.0.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-hal-nb-1.0.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-io-0.6.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__futures-0.3.32\", is_dev_dep = False),\n struct(repo=\"rust_crates__heapless-0.9.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__hex-0.4.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__hmac-0.12.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__k256-0.13.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__log-0.4.29\", is_dev_dep = False),\n struct(repo=\"rust_crates__memoffset-0.9.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__minijinja-2.19.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__nb-1.1.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__nom-7.1.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__object-0.37.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__p256-0.13.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__p384-0.13.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__paste-1.0.15\", is_dev_dep = False),\n struct(repo=\"rust_crates__proc-macro2-1.0.106\", is_dev_dep = False),\n struct(repo=\"rust_crates__prost-0.13.5\", is_dev_dep = False),\n struct(repo=\"rust_crates__quote-1.0.45\", is_dev_dep = False),\n struct(repo=\"rust_crates__rand_core-0.9.5\", is_dev_dep = False),\n struct(repo=\"rust_crates__riscv-0.12.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__riscv-rt-0.12.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__riscv-semihosting-0.1.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__rustc-demangle-0.1.27\", is_dev_dep = False),\n struct(repo=\"rust_crates__sec1-0.7.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__serde-1.0.228\", is_dev_dep = False),\n struct(repo=\"rust_crates__serde_derive-1.0.228\", is_dev_dep = False),\n struct(repo=\"rust_crates__serde_json5-0.2.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__sha2-0.10.9\", is_dev_dep = False),\n struct(repo=\"rust_crates__sha3-0.10.8\", is_dev_dep = False),\n struct(repo=\"rust_crates__smlang-0.8.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__subtle-2.6.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__syn-1.0.109\", is_dev_dep = False),\n struct(repo=\"rust_crates__syn-2.0.117\", is_dev_dep = False),\n struct(repo=\"rust_crates__thiserror-2.0.18\", is_dev_dep = False),\n struct(repo=\"rust_crates__tock-registers-0.9.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__tokio-1.51.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__tokio-util-0.7.18\", is_dev_dep = False),\n struct(repo=\"rust_crates__zerocopy-0.8.48\", is_dev_dep = False),\n struct(repo=\"rust_crates__zeroize-1.8.2\", is_dev_dep = False),\n ]\n" + "defs.bzl": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\"\"\"\n# `crates_repository` API\n\n- [aliases](#aliases)\n- [crate_deps](#crate_deps)\n- [all_crate_deps](#all_crate_deps)\n- [crate_repositories](#crate_repositories)\n\n\"\"\"\n\nload(\"@bazel_tools//tools/build_defs/repo:git.bzl\", \"new_git_repository\")\nload(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\")\nload(\"@bazel_tools//tools/build_defs/repo:utils.bzl\", \"maybe\")\nload(\"@bazel_skylib//lib:selects.bzl\", \"selects\")\nload(\"@rules_rust//crate_universe/private:local_crate_mirror.bzl\", \"local_crate_mirror\")\n\n###############################################################################\n# MACROS API\n###############################################################################\n\n# An identifier that represent common dependencies (unconditional).\n_COMMON_CONDITION = \"\"\n\ndef _flatten_dependency_maps(all_dependency_maps):\n \"\"\"Flatten a list of dependency maps into one dictionary.\n\n Dependency maps have the following structure:\n\n ```python\n DEPENDENCIES_MAP = {\n # The first key in the map is a Bazel package\n # name of the workspace this file is defined in.\n \"workspace_member_package\": {\n\n # Not all dependencies are supported for all platforms.\n # the condition key is the condition required to be true\n # on the host platform.\n \"condition\": {\n\n # An alias to a crate target. # The label of the crate target the\n # Aliases are only crate names. # package name refers to.\n \"package_name\": \"@full//:label\",\n }\n }\n }\n ```\n\n Args:\n all_dependency_maps (list): A list of dicts as described above\n\n Returns:\n dict: A dictionary as described above\n \"\"\"\n dependencies = {}\n\n for workspace_deps_map in all_dependency_maps:\n for pkg_name, conditional_deps_map in workspace_deps_map.items():\n if pkg_name not in dependencies:\n non_frozen_map = dict()\n for key, values in conditional_deps_map.items():\n non_frozen_map.update({key: dict(values.items())})\n dependencies.setdefault(pkg_name, non_frozen_map)\n continue\n\n for condition, deps_map in conditional_deps_map.items():\n # If the condition has not been recorded, do so and continue\n if condition not in dependencies[pkg_name]:\n dependencies[pkg_name].setdefault(condition, dict(deps_map.items()))\n continue\n\n # Alert on any miss-matched dependencies\n inconsistent_entries = []\n for crate_name, crate_label in deps_map.items():\n existing = dependencies[pkg_name][condition].get(crate_name)\n if existing and existing != crate_label:\n inconsistent_entries.append((crate_name, existing, crate_label))\n dependencies[pkg_name][condition].update({crate_name: crate_label})\n\n return dependencies\n\ndef crate_deps(deps, package_name = None):\n \"\"\"Finds the fully qualified label of the requested crates for the package where this macro is called.\n\n Args:\n deps (list): The desired list of crate targets.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()`.\n\n Returns:\n list: A list of labels to generated rust targets (str)\n \"\"\"\n\n if not deps:\n return []\n\n if package_name == None:\n package_name = native.package_name()\n\n # Join both sets of dependencies\n dependencies = _flatten_dependency_maps([\n _NORMAL_DEPENDENCIES,\n _NORMAL_DEV_DEPENDENCIES,\n _PROC_MACRO_DEPENDENCIES,\n _PROC_MACRO_DEV_DEPENDENCIES,\n _BUILD_DEPENDENCIES,\n _BUILD_PROC_MACRO_DEPENDENCIES,\n ]).pop(package_name, {})\n\n # Combine all conditional packages so we can easily index over a flat list\n # TODO: Perhaps this should actually return select statements and maintain\n # the conditionals of the dependencies\n flat_deps = {}\n for deps_set in dependencies.values():\n for crate_name, crate_label in deps_set.items():\n flat_deps.update({crate_name: crate_label})\n\n missing_crates = []\n crate_targets = []\n for crate_target in deps:\n if crate_target not in flat_deps:\n missing_crates.append(crate_target)\n else:\n crate_targets.append(flat_deps[crate_target])\n\n if missing_crates:\n fail(\"Could not find crates `{}` among dependencies of `{}`. Available dependencies were `{}`\".format(\n missing_crates,\n package_name,\n dependencies,\n ))\n\n return crate_targets\n\ndef all_crate_deps(\n normal = False, \n normal_dev = False, \n proc_macro = False, \n proc_macro_dev = False,\n build = False,\n build_proc_macro = False,\n package_name = None):\n \"\"\"Finds the fully qualified label of all requested direct crate dependencies \\\n for the package where this macro is called.\n\n If no parameters are set, all normal dependencies are returned. Setting any one flag will\n otherwise impact the contents of the returned list.\n\n Args:\n normal (bool, optional): If True, normal dependencies are included in the\n output list.\n normal_dev (bool, optional): If True, normal dev dependencies will be\n included in the output list..\n proc_macro (bool, optional): If True, proc_macro dependencies are included\n in the output list.\n proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n included in the output list.\n build (bool, optional): If True, build dependencies are included\n in the output list.\n build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n included in the output list.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()` when unset.\n\n Returns:\n list: A list of labels to generated rust targets (str)\n \"\"\"\n\n if package_name == None:\n package_name = native.package_name()\n\n # Determine the relevant maps to use\n all_dependency_maps = []\n if normal:\n all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n if normal_dev:\n all_dependency_maps.append(_NORMAL_DEV_DEPENDENCIES)\n if proc_macro:\n all_dependency_maps.append(_PROC_MACRO_DEPENDENCIES)\n if proc_macro_dev:\n all_dependency_maps.append(_PROC_MACRO_DEV_DEPENDENCIES)\n if build:\n all_dependency_maps.append(_BUILD_DEPENDENCIES)\n if build_proc_macro:\n all_dependency_maps.append(_BUILD_PROC_MACRO_DEPENDENCIES)\n\n # Default to always using normal dependencies\n if not all_dependency_maps:\n all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n\n dependencies = _flatten_dependency_maps(all_dependency_maps).pop(package_name, None)\n\n if not dependencies:\n if dependencies == None:\n fail(\"Tried to get all_crate_deps for package \" + package_name + \" but that package had no Cargo.toml file\")\n else:\n return []\n\n crate_deps = list(dependencies.pop(_COMMON_CONDITION, {}).values())\n for condition, deps in dependencies.items():\n crate_deps += selects.with_or({\n tuple(_CONDITIONS[condition]): deps.values(),\n \"//conditions:default\": [],\n })\n\n return crate_deps\n\ndef aliases(\n normal = False,\n normal_dev = False,\n proc_macro = False,\n proc_macro_dev = False,\n build = False,\n build_proc_macro = False,\n package_name = None):\n \"\"\"Produces a map of Crate alias names to their original label\n\n If no dependency kinds are specified, `normal` and `proc_macro` are used by default.\n Setting any one flag will otherwise determine the contents of the returned dict.\n\n Args:\n normal (bool, optional): If True, normal dependencies are included in the\n output list.\n normal_dev (bool, optional): If True, normal dev dependencies will be\n included in the output list..\n proc_macro (bool, optional): If True, proc_macro dependencies are included\n in the output list.\n proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n included in the output list.\n build (bool, optional): If True, build dependencies are included\n in the output list.\n build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n included in the output list.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()` when unset.\n\n Returns:\n dict: The aliases of all associated packages\n \"\"\"\n if package_name == None:\n package_name = native.package_name()\n\n # Determine the relevant maps to use\n all_aliases_maps = []\n if normal:\n all_aliases_maps.append(_NORMAL_ALIASES)\n if normal_dev:\n all_aliases_maps.append(_NORMAL_DEV_ALIASES)\n if proc_macro:\n all_aliases_maps.append(_PROC_MACRO_ALIASES)\n if proc_macro_dev:\n all_aliases_maps.append(_PROC_MACRO_DEV_ALIASES)\n if build:\n all_aliases_maps.append(_BUILD_ALIASES)\n if build_proc_macro:\n all_aliases_maps.append(_BUILD_PROC_MACRO_ALIASES)\n\n # Default to always using normal aliases\n if not all_aliases_maps:\n all_aliases_maps.append(_NORMAL_ALIASES)\n all_aliases_maps.append(_PROC_MACRO_ALIASES)\n\n aliases = _flatten_dependency_maps(all_aliases_maps).pop(package_name, None)\n\n if not aliases:\n return dict()\n\n common_items = aliases.pop(_COMMON_CONDITION, {}).items()\n\n # If there are only common items in the dictionary, immediately return them\n if not len(aliases.keys()) == 1:\n return dict(common_items)\n\n # Build a single select statement where each conditional has accounted for the\n # common set of aliases.\n crate_aliases = {\"//conditions:default\": dict(common_items)}\n for condition, deps in aliases.items():\n condition_triples = _CONDITIONS[condition]\n for triple in condition_triples:\n if triple in crate_aliases:\n crate_aliases[triple].update(deps)\n else:\n crate_aliases.update({triple: dict(deps.items() + common_items)})\n\n return select(crate_aliases)\n\n###############################################################################\n# WORKSPACE MEMBER DEPS AND ALIASES\n###############################################################################\n\n_NORMAL_DEPENDENCIES = {\n \"third_party/crates_io\": {\n _COMMON_CONDITION: {\n \"aes\": Label(\"@rust_crates//:aes-0.8.4\"),\n \"aes-gcm\": Label(\"@rust_crates//:aes-gcm-0.10.3\"),\n \"aligned\": Label(\"@rust_crates//:aligned-0.4.3\"),\n \"anyhow\": Label(\"@rust_crates//:anyhow-1.0.102\"),\n \"bitfield\": Label(\"@rust_crates//:bitfield-0.14.0\"),\n \"bitflags\": Label(\"@rust_crates//:bitflags-2.11.0\"),\n \"byteorder\": Label(\"@rust_crates//:byteorder-1.5.0\"),\n \"cfg-if\": Label(\"@rust_crates//:cfg-if-1.0.4\"),\n \"cipher\": Label(\"@rust_crates//:cipher-0.4.4\"),\n \"clap\": Label(\"@rust_crates//:clap-4.6.0\"),\n \"compiler_builtins\": Label(\"@rust_crates//:compiler_builtins-0.1.160\"),\n \"cortex-m\": Label(\"@rust_crates//:cortex-m-0.7.7\"),\n \"ctr\": Label(\"@rust_crates//:ctr-0.9.2\"),\n \"ecdsa\": Label(\"@rust_crates//:ecdsa-0.16.9\"),\n \"embedded-hal\": Label(\"@rust_crates//:embedded-hal-1.0.0\"),\n \"embedded-hal-async\": Label(\"@rust_crates//:embedded-hal-async-1.0.0\"),\n \"embedded-hal-nb\": Label(\"@rust_crates//:embedded-hal-nb-1.0.0\"),\n \"embedded-io\": Label(\"@rust_crates//:embedded-io-0.6.1\"),\n \"futures\": Label(\"@rust_crates//:futures-0.3.32\"),\n \"heapless\": Label(\"@rust_crates//:heapless-0.9.2\"),\n \"hex\": Label(\"@rust_crates//:hex-0.4.3\"),\n \"hmac\": Label(\"@rust_crates//:hmac-0.12.1\"),\n \"k256\": Label(\"@rust_crates//:k256-0.13.4\"),\n \"log\": Label(\"@rust_crates//:log-0.4.29\"),\n \"memoffset\": Label(\"@rust_crates//:memoffset-0.9.1\"),\n \"minijinja\": Label(\"@rust_crates//:minijinja-2.19.0\"),\n \"nb\": Label(\"@rust_crates//:nb-1.1.0\"),\n \"nom\": Label(\"@rust_crates//:nom-7.1.3\"),\n \"object\": Label(\"@rust_crates//:object-0.37.3\"),\n \"p256\": Label(\"@rust_crates//:p256-0.13.2\"),\n \"p384\": Label(\"@rust_crates//:p384-0.13.1\"),\n \"proc-macro2\": Label(\"@rust_crates//:proc-macro2-1.0.106\"),\n \"prost\": Label(\"@rust_crates//:prost-0.13.5\"),\n \"quote\": Label(\"@rust_crates//:quote-1.0.45\"),\n \"rand_core\": Label(\"@rust_crates//:rand_core-0.9.5\"),\n \"riscv\": Label(\"@rust_crates//:riscv-0.12.1\"),\n \"riscv-rt\": Label(\"@rust_crates//:riscv-rt-0.12.2\"),\n \"riscv-semihosting\": Label(\"@rust_crates//:riscv-semihosting-0.1.3\"),\n \"rustc-demangle\": Label(\"@rust_crates//:rustc-demangle-0.1.27\"),\n \"sec1\": Label(\"@rust_crates//:sec1-0.7.3\"),\n \"serde\": Label(\"@rust_crates//:serde-1.0.228\"),\n \"serde_json5\": Label(\"@rust_crates//:serde_json5-0.2.1\"),\n \"sha2\": Label(\"@rust_crates//:sha2-0.10.9\"),\n \"sha3\": Label(\"@rust_crates//:sha3-0.10.8\"),\n \"smlang\": Label(\"@rust_crates//:smlang-0.8.0\"),\n \"subtle\": Label(\"@rust_crates//:subtle-2.6.1\"),\n \"syn1\": Label(\"@rust_crates//:syn-1.0.109\"),\n \"syn\": Label(\"@rust_crates//:syn-2.0.117\"),\n \"thiserror\": Label(\"@rust_crates//:thiserror-2.0.18\"),\n \"tock-registers\": Label(\"@rust_crates//:tock-registers-0.9.0\"),\n \"tokio\": Label(\"@rust_crates//:tokio-1.51.1\"),\n \"tokio-util\": Label(\"@rust_crates//:tokio-util-0.7.18\"),\n \"ufmt\": Label(\"@rust_crates//:ufmt-0.2.0\"),\n \"zerocopy\": Label(\"@rust_crates//:zerocopy-0.8.48\"),\n \"zeroize\": Label(\"@rust_crates//:zeroize-1.8.2\"),\n },\n },\n}\n\n\n_NORMAL_ALIASES = {\n \"third_party/crates_io\": {\n _COMMON_CONDITION: {\n Label(\"@rust_crates//:syn-1.0.109\"): \"syn1\",\n },\n },\n}\n\n\n_NORMAL_DEV_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_NORMAL_DEV_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_PROC_MACRO_DEPENDENCIES = {\n \"third_party/crates_io\": {\n _COMMON_CONDITION: {\n \"bitfield-struct\": Label(\"@rust_crates//:bitfield-struct-0.11.0\"),\n \"paste\": Label(\"@rust_crates//:paste-1.0.15\"),\n \"serde_derive\": Label(\"@rust_crates//:serde_derive-1.0.228\"),\n },\n },\n}\n\n\n_PROC_MACRO_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_PROC_MACRO_DEV_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_PROC_MACRO_DEV_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_PROC_MACRO_DEPENDENCIES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_BUILD_PROC_MACRO_ALIASES = {\n \"third_party/crates_io\": {\n },\n}\n\n\n_CONDITIONS = {\n \"aarch64-apple-darwin\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\"],\n \"aarch64-linux-android\": [],\n \"aarch64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\": [],\n \"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\",\"@rules_rust//rust/platform:x86_64-apple-darwin\"],\n \"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"linux\\\"))\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"cfg(all(target_arch = \\\"aarch64\\\", target_vendor = \\\"apple\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\"],\n \"cfg(all(target_arch = \\\"loongarch64\\\", target_os = \\\"linux\\\"))\": [],\n \"cfg(any())\": [],\n \"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(any(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\", target_pointer_width = \\\"64\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(any(unix, target_os = \\\"hermit\\\", target_os = \\\"wasi\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(any(unix, target_os = \\\"wasi\\\"))\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(target_arch = \\\"aarch64\\\")\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"cfg(target_os = \\\"hermit\\\")\": [],\n \"cfg(target_os = \\\"redox\\\")\": [],\n \"cfg(target_os = \\\"wasi\\\")\": [],\n \"cfg(unix)\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\",\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\",\"@rules_rust//rust/platform:x86_64-apple-darwin\",\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"cfg(windows)\": [],\n \"riscv32imc-unknown-none-elf\": [\"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\"],\n \"x86_64-apple-darwin\": [\"@rules_rust//rust/platform:x86_64-apple-darwin\"],\n \"x86_64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n}\n\n###############################################################################\n\ndef crate_repositories():\n \"\"\"A macro for defining repositories for all generated crates.\n\n Returns:\n A list of repos visible to the module through the module extension.\n \"\"\"\n maybe(\n http_archive,\n name = \"rust_crates__adler2-2.0.1\",\n sha256 = \"320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/adler2/2.0.1/download\"],\n strip_prefix = \"adler2-2.0.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.adler2-2.0.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aead-0.5.2\",\n sha256 = \"d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aead/0.5.2/download\"],\n strip_prefix = \"aead-0.5.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aead-0.5.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aes-0.8.4\",\n sha256 = \"b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aes/0.8.4/download\"],\n strip_prefix = \"aes-0.8.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aes-0.8.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aes-gcm-0.10.3\",\n sha256 = \"831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aes-gcm/0.10.3/download\"],\n strip_prefix = \"aes-gcm-0.10.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aes-gcm-0.10.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__aligned-0.4.3\",\n sha256 = \"ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aligned/0.4.3/download\"],\n strip_prefix = \"aligned-0.4.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.aligned-0.4.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstream-1.0.0\",\n sha256 = \"824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstream/1.0.0/download\"],\n strip_prefix = \"anstream-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstream-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-1.0.14\",\n sha256 = \"940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle/1.0.14/download\"],\n strip_prefix = \"anstyle-1.0.14\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-1.0.14.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-parse-1.0.0\",\n sha256 = \"52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle-parse/1.0.0/download\"],\n strip_prefix = \"anstyle-parse-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-parse-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-query-1.1.5\",\n sha256 = \"40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle-query/1.1.5/download\"],\n strip_prefix = \"anstyle-query-1.1.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-query-1.1.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anstyle-wincon-3.0.11\",\n sha256 = \"291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anstyle-wincon/3.0.11/download\"],\n strip_prefix = \"anstyle-wincon-3.0.11\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anstyle-wincon-3.0.11.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__anyhow-1.0.102\",\n sha256 = \"7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/anyhow/1.0.102/download\"],\n strip_prefix = \"anyhow-1.0.102\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.anyhow-1.0.102.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__as-slice-0.2.1\",\n sha256 = \"516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/as-slice/0.2.1/download\"],\n strip_prefix = \"as-slice-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.as-slice-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__autocfg-1.5.0\",\n sha256 = \"c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/autocfg/1.5.0/download\"],\n strip_prefix = \"autocfg-1.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.autocfg-1.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bare-metal-0.2.5\",\n sha256 = \"5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bare-metal/0.2.5/download\"],\n strip_prefix = \"bare-metal-0.2.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bare-metal-0.2.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__base16ct-0.2.0\",\n sha256 = \"4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/base16ct/0.2.0/download\"],\n strip_prefix = \"base16ct-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.base16ct-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitfield-0.13.2\",\n sha256 = \"46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitfield/0.13.2/download\"],\n strip_prefix = \"bitfield-0.13.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitfield-0.13.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitfield-0.14.0\",\n sha256 = \"2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitfield/0.14.0/download\"],\n strip_prefix = \"bitfield-0.14.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitfield-0.14.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitfield-struct-0.11.0\",\n sha256 = \"d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitfield-struct/0.11.0/download\"],\n strip_prefix = \"bitfield-struct-0.11.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitfield-struct-0.11.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bitflags-2.11.0\",\n sha256 = \"843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bitflags/2.11.0/download\"],\n strip_prefix = \"bitflags-2.11.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bitflags-2.11.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__block-buffer-0.10.4\",\n sha256 = \"3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/block-buffer/0.10.4/download\"],\n strip_prefix = \"block-buffer-0.10.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.block-buffer-0.10.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__byteorder-1.5.0\",\n sha256 = \"1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/byteorder/1.5.0/download\"],\n strip_prefix = \"byteorder-1.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.byteorder-1.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__bytes-1.11.1\",\n sha256 = \"1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/bytes/1.11.1/download\"],\n strip_prefix = \"bytes-1.11.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.bytes-1.11.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cfg-if-1.0.4\",\n sha256 = \"9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cfg-if/1.0.4/download\"],\n strip_prefix = \"cfg-if-1.0.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cfg-if-1.0.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cipher-0.4.4\",\n sha256 = \"773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cipher/0.4.4/download\"],\n strip_prefix = \"cipher-0.4.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cipher-0.4.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap-4.6.0\",\n sha256 = \"b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap/4.6.0/download\"],\n strip_prefix = \"clap-4.6.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap-4.6.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap_builder-4.6.0\",\n sha256 = \"714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap_builder/4.6.0/download\"],\n strip_prefix = \"clap_builder-4.6.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap_builder-4.6.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap_derive-4.6.0\",\n sha256 = \"1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap_derive/4.6.0/download\"],\n strip_prefix = \"clap_derive-4.6.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap_derive-4.6.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__clap_lex-1.1.0\",\n sha256 = \"c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/clap_lex/1.1.0/download\"],\n strip_prefix = \"clap_lex-1.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.clap_lex-1.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__colorchoice-1.0.5\",\n sha256 = \"1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/colorchoice/1.0.5/download\"],\n strip_prefix = \"colorchoice-1.0.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.colorchoice-1.0.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__compiler_builtins-0.1.160\",\n sha256 = \"6376049cfa92c0aa8b9ac95fae22184b981c658208d4ed8a1dc553cd83612895\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/compiler_builtins/0.1.160/download\"],\n strip_prefix = \"compiler_builtins-0.1.160\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.compiler_builtins-0.1.160.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__const-oid-0.9.6\",\n sha256 = \"c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/const-oid/0.9.6/download\"],\n strip_prefix = \"const-oid-0.9.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.const-oid-0.9.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cortex-m-0.7.7\",\n sha256 = \"8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cortex-m/0.7.7/download\"],\n strip_prefix = \"cortex-m-0.7.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cortex-m-0.7.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__cpufeatures-0.2.17\",\n sha256 = \"59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/cpufeatures/0.2.17/download\"],\n strip_prefix = \"cpufeatures-0.2.17\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.cpufeatures-0.2.17.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__crc32fast-1.5.0\",\n sha256 = \"9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/crc32fast/1.5.0/download\"],\n strip_prefix = \"crc32fast-1.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.crc32fast-1.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__critical-section-1.2.0\",\n sha256 = \"790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/critical-section/1.2.0/download\"],\n strip_prefix = \"critical-section-1.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.critical-section-1.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__crypto-bigint-0.5.5\",\n sha256 = \"0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/crypto-bigint/0.5.5/download\"],\n strip_prefix = \"crypto-bigint-0.5.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.crypto-bigint-0.5.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__crypto-common-0.1.7\",\n sha256 = \"78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/crypto-common/0.1.7/download\"],\n strip_prefix = \"crypto-common-0.1.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.crypto-common-0.1.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ctr-0.9.2\",\n sha256 = \"0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ctr/0.9.2/download\"],\n strip_prefix = \"ctr-0.9.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ctr-0.9.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__der-0.7.10\",\n sha256 = \"e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/der/0.7.10/download\"],\n strip_prefix = \"der-0.7.10\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.der-0.7.10.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__digest-0.10.7\",\n sha256 = \"9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/digest/0.10.7/download\"],\n strip_prefix = \"digest-0.10.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.digest-0.10.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ecdsa-0.16.9\",\n sha256 = \"ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ecdsa/0.16.9/download\"],\n strip_prefix = \"ecdsa-0.16.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ecdsa-0.16.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__either-1.15.0\",\n sha256 = \"48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/either/1.15.0/download\"],\n strip_prefix = \"either-1.15.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.either-1.15.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__elliptic-curve-0.13.8\",\n sha256 = \"b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/elliptic-curve/0.13.8/download\"],\n strip_prefix = \"elliptic-curve-0.13.8\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.elliptic-curve-0.13.8.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-0.2.7\",\n sha256 = \"35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal/0.2.7/download\"],\n strip_prefix = \"embedded-hal-0.2.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-0.2.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-1.0.0\",\n sha256 = \"361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal/1.0.0/download\"],\n strip_prefix = \"embedded-hal-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-async-1.0.0\",\n sha256 = \"0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal-async/1.0.0/download\"],\n strip_prefix = \"embedded-hal-async-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-async-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-hal-nb-1.0.0\",\n sha256 = \"fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-hal-nb/1.0.0/download\"],\n strip_prefix = \"embedded-hal-nb-1.0.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-hal-nb-1.0.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__embedded-io-0.6.1\",\n sha256 = \"edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/embedded-io/0.6.1/download\"],\n strip_prefix = \"embedded-io-0.6.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.embedded-io-0.6.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__equivalent-1.0.2\",\n sha256 = \"877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/equivalent/1.0.2/download\"],\n strip_prefix = \"equivalent-1.0.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.equivalent-1.0.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__errno-0.3.14\",\n sha256 = \"39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/errno/0.3.14/download\"],\n strip_prefix = \"errno-0.3.14\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.errno-0.3.14.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ff-0.13.1\",\n sha256 = \"c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ff/0.13.1/download\"],\n strip_prefix = \"ff-0.13.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ff-0.13.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__flate2-1.1.9\",\n sha256 = \"843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/flate2/1.1.9/download\"],\n strip_prefix = \"flate2-1.1.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.flate2-1.1.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__foldhash-0.1.5\",\n sha256 = \"d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/foldhash/0.1.5/download\"],\n strip_prefix = \"foldhash-0.1.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.foldhash-0.1.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-0.3.32\",\n sha256 = \"8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures/0.3.32/download\"],\n strip_prefix = \"futures-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-channel-0.3.32\",\n sha256 = \"07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-channel/0.3.32/download\"],\n strip_prefix = \"futures-channel-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-channel-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-core-0.3.32\",\n sha256 = \"7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-core/0.3.32/download\"],\n strip_prefix = \"futures-core-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-core-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-executor-0.3.32\",\n sha256 = \"baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-executor/0.3.32/download\"],\n strip_prefix = \"futures-executor-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-executor-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-io-0.3.32\",\n sha256 = \"cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-io/0.3.32/download\"],\n strip_prefix = \"futures-io-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-io-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-macro-0.3.32\",\n sha256 = \"e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-macro/0.3.32/download\"],\n strip_prefix = \"futures-macro-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-macro-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-sink-0.3.32\",\n sha256 = \"c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-sink/0.3.32/download\"],\n strip_prefix = \"futures-sink-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-sink-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-task-0.3.32\",\n sha256 = \"037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-task/0.3.32/download\"],\n strip_prefix = \"futures-task-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-task-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__futures-util-0.3.32\",\n sha256 = \"389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/futures-util/0.3.32/download\"],\n strip_prefix = \"futures-util-0.3.32\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.futures-util-0.3.32.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__generic-array-0.14.7\",\n sha256 = \"85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/generic-array/0.14.7/download\"],\n strip_prefix = \"generic-array-0.14.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.generic-array-0.14.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ghash-0.5.1\",\n sha256 = \"f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ghash/0.5.1/download\"],\n strip_prefix = \"ghash-0.5.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ghash-0.5.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__group-0.13.0\",\n sha256 = \"f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/group/0.13.0/download\"],\n strip_prefix = \"group-0.13.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.group-0.13.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hash32-0.3.1\",\n sha256 = \"47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hash32/0.3.1/download\"],\n strip_prefix = \"hash32-0.3.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hash32-0.3.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hashbrown-0.15.5\",\n sha256 = \"9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hashbrown/0.15.5/download\"],\n strip_prefix = \"hashbrown-0.15.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hashbrown-0.15.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hashbrown-0.17.0\",\n sha256 = \"4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hashbrown/0.17.0/download\"],\n strip_prefix = \"hashbrown-0.17.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hashbrown-0.17.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__heapless-0.9.2\",\n sha256 = \"2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/heapless/0.9.2/download\"],\n strip_prefix = \"heapless-0.9.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.heapless-0.9.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__heck-0.5.0\",\n sha256 = \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/heck/0.5.0/download\"],\n strip_prefix = \"heck-0.5.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.heck-0.5.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hex-0.4.3\",\n sha256 = \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hex/0.4.3/download\"],\n strip_prefix = \"hex-0.4.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hex-0.4.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__hmac-0.12.1\",\n sha256 = \"6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/hmac/0.12.1/download\"],\n strip_prefix = \"hmac-0.12.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.hmac-0.12.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__indexmap-2.14.0\",\n sha256 = \"d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/indexmap/2.14.0/download\"],\n strip_prefix = \"indexmap-2.14.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.indexmap-2.14.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__inout-0.1.4\",\n sha256 = \"879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/inout/0.1.4/download\"],\n strip_prefix = \"inout-0.1.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.inout-0.1.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__is_terminal_polyfill-1.70.2\",\n sha256 = \"a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/is_terminal_polyfill/1.70.2/download\"],\n strip_prefix = \"is_terminal_polyfill-1.70.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.is_terminal_polyfill-1.70.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__itertools-0.14.0\",\n sha256 = \"2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/itertools/0.14.0/download\"],\n strip_prefix = \"itertools-0.14.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.itertools-0.14.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__k256-0.13.4\",\n sha256 = \"f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/k256/0.13.4/download\"],\n strip_prefix = \"k256-0.13.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.k256-0.13.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__keccak-0.1.6\",\n sha256 = \"cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/keccak/0.1.6/download\"],\n strip_prefix = \"keccak-0.1.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.keccak-0.1.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__libc-0.2.184\",\n sha256 = \"48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/libc/0.2.184/download\"],\n strip_prefix = \"libc-0.2.184\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.libc-0.2.184.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__linux-raw-sys-0.12.1\",\n sha256 = \"32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/linux-raw-sys/0.12.1/download\"],\n strip_prefix = \"linux-raw-sys-0.12.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.linux-raw-sys-0.12.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__lock_api-0.4.14\",\n sha256 = \"224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/lock_api/0.4.14/download\"],\n strip_prefix = \"lock_api-0.4.14\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.lock_api-0.4.14.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__log-0.4.29\",\n sha256 = \"5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/log/0.4.29/download\"],\n strip_prefix = \"log-0.4.29\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.log-0.4.29.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__memchr-2.8.0\",\n sha256 = \"f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memchr/2.8.0/download\"],\n strip_prefix = \"memchr-2.8.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.memchr-2.8.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__memo-map-0.3.3\",\n sha256 = \"38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memo-map/0.3.3/download\"],\n strip_prefix = \"memo-map-0.3.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.memo-map-0.3.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__memoffset-0.9.1\",\n sha256 = \"488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memoffset/0.9.1/download\"],\n strip_prefix = \"memoffset-0.9.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.memoffset-0.9.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__minijinja-2.19.0\",\n sha256 = \"805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/minijinja/2.19.0/download\"],\n strip_prefix = \"minijinja-2.19.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.minijinja-2.19.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__minimal-lexical-0.2.1\",\n sha256 = \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/minimal-lexical/0.2.1/download\"],\n strip_prefix = \"minimal-lexical-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.minimal-lexical-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__miniz_oxide-0.8.9\",\n sha256 = \"1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/miniz_oxide/0.8.9/download\"],\n strip_prefix = \"miniz_oxide-0.8.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.miniz_oxide-0.8.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__mio-1.2.0\",\n sha256 = \"50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/mio/1.2.0/download\"],\n strip_prefix = \"mio-1.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.mio-1.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__nb-0.1.3\",\n sha256 = \"801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/nb/0.1.3/download\"],\n strip_prefix = \"nb-0.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.nb-0.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__nb-1.1.0\",\n sha256 = \"8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/nb/1.1.0/download\"],\n strip_prefix = \"nb-1.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.nb-1.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__nom-7.1.3\",\n sha256 = \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/nom/7.1.3/download\"],\n strip_prefix = \"nom-7.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.nom-7.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__object-0.37.3\",\n sha256 = \"ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/object/0.37.3/download\"],\n strip_prefix = \"object-0.37.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.object-0.37.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__once_cell_polyfill-1.70.2\",\n sha256 = \"384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/once_cell_polyfill/1.70.2/download\"],\n strip_prefix = \"once_cell_polyfill-1.70.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.once_cell_polyfill-1.70.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__opaque-debug-0.3.1\",\n sha256 = \"c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/opaque-debug/0.3.1/download\"],\n strip_prefix = \"opaque-debug-0.3.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.opaque-debug-0.3.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__p256-0.13.2\",\n sha256 = \"c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/p256/0.13.2/download\"],\n strip_prefix = \"p256-0.13.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.p256-0.13.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__p384-0.13.1\",\n sha256 = \"fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/p384/0.13.1/download\"],\n strip_prefix = \"p384-0.13.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.p384-0.13.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__parking_lot-0.12.5\",\n sha256 = \"93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/parking_lot/0.12.5/download\"],\n strip_prefix = \"parking_lot-0.12.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.parking_lot-0.12.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__parking_lot_core-0.9.12\",\n sha256 = \"2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/parking_lot_core/0.9.12/download\"],\n strip_prefix = \"parking_lot_core-0.9.12\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.parking_lot_core-0.9.12.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__paste-1.0.15\",\n sha256 = \"57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/paste/1.0.15/download\"],\n strip_prefix = \"paste-1.0.15\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.paste-1.0.15.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest-2.8.6\",\n sha256 = \"e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest/2.8.6/download\"],\n strip_prefix = \"pest-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest_derive-2.8.6\",\n sha256 = \"11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest_derive/2.8.6/download\"],\n strip_prefix = \"pest_derive-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest_derive-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest_generator-2.8.6\",\n sha256 = \"8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest_generator/2.8.6/download\"],\n strip_prefix = \"pest_generator-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest_generator-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pest_meta-2.8.6\",\n sha256 = \"89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pest_meta/2.8.6/download\"],\n strip_prefix = \"pest_meta-2.8.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pest_meta-2.8.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__pin-project-lite-0.2.17\",\n sha256 = \"a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/pin-project-lite/0.2.17/download\"],\n strip_prefix = \"pin-project-lite-0.2.17\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.pin-project-lite-0.2.17.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__polyval-0.6.2\",\n sha256 = \"9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/polyval/0.6.2/download\"],\n strip_prefix = \"polyval-0.6.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.polyval-0.6.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__primeorder-0.13.6\",\n sha256 = \"353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/primeorder/0.13.6/download\"],\n strip_prefix = \"primeorder-0.13.6\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.primeorder-0.13.6.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__proc-macro2-1.0.106\",\n sha256 = \"8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/proc-macro2/1.0.106/download\"],\n strip_prefix = \"proc-macro2-1.0.106\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.proc-macro2-1.0.106.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__prost-0.13.5\",\n sha256 = \"2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/prost/0.13.5/download\"],\n strip_prefix = \"prost-0.13.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.prost-0.13.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__prost-derive-0.13.5\",\n sha256 = \"8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/prost-derive/0.13.5/download\"],\n strip_prefix = \"prost-derive-0.13.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.prost-derive-0.13.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__quote-1.0.45\",\n sha256 = \"41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/quote/1.0.45/download\"],\n strip_prefix = \"quote-1.0.45\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.quote-1.0.45.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rand_core-0.6.4\",\n sha256 = \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rand_core/0.6.4/download\"],\n strip_prefix = \"rand_core-0.6.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rand_core-0.6.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rand_core-0.9.5\",\n sha256 = \"76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rand_core/0.9.5/download\"],\n strip_prefix = \"rand_core-0.9.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rand_core-0.9.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__redox_syscall-0.5.18\",\n sha256 = \"ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/redox_syscall/0.5.18/download\"],\n strip_prefix = \"redox_syscall-0.5.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.redox_syscall-0.5.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rfc6979-0.4.0\",\n sha256 = \"f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rfc6979/0.4.0/download\"],\n strip_prefix = \"rfc6979-0.4.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rfc6979-0.4.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-0.11.1\",\n sha256 = \"2f5c1b8bf41ea746266cdee443d1d1e9125c86ce1447e1a2615abd34330d33a9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv/0.11.1/download\"],\n strip_prefix = \"riscv-0.11.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-0.11.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-0.12.1\",\n sha256 = \"5ea8ff73d3720bdd0a97925f0bf79ad2744b6da8ff36be3840c48ac81191d7a7\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv/0.12.1/download\"],\n strip_prefix = \"riscv-0.12.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-0.12.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-0.13.0\",\n sha256 = \"afa3cdbeccae4359f6839a00e8b77e5736caa200ba216caf38d24e4c16e2b586\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv/0.13.0/download\"],\n strip_prefix = \"riscv-0.13.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-0.13.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-macros-0.1.0\",\n sha256 = \"f265be5d634272320a7de94cea15c22a3bfdd4eb42eb43edc528415f066a1f25\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-macros/0.1.0/download\"],\n strip_prefix = \"riscv-macros-0.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-macros-0.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-macros-0.2.0\",\n sha256 = \"e8c4aa1ea1af6dcc83a61be12e8189f9b293c3ba5a487778a4cd89fb060fdbbc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-macros/0.2.0/download\"],\n strip_prefix = \"riscv-macros-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-macros-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-pac-0.2.0\",\n sha256 = \"8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-pac/0.2.0/download\"],\n strip_prefix = \"riscv-pac-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-pac-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-rt-0.12.2\",\n sha256 = \"c0d35e32cf1383183e8885d8a9aa4402a087fd094dc34c2cb6df6687d0229dfe\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-rt/0.12.2/download\"],\n strip_prefix = \"riscv-rt-0.12.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-rt-0.12.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-rt-macros-0.2.2\",\n sha256 = \"30f19a85fe107b65031e0ba8ec60c34c2494069fe910d6c297f5e7cb5a6f76d0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-rt-macros/0.2.2/download\"],\n strip_prefix = \"riscv-rt-macros-0.2.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-rt-macros-0.2.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__riscv-semihosting-0.1.3\",\n sha256 = \"1086dd4bcc13de1cb14b93849411e3466de7e5907d2d8eb269032536e93facc6\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/riscv-semihosting/0.1.3/download\"],\n strip_prefix = \"riscv-semihosting-0.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.riscv-semihosting-0.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rustc-demangle-0.1.27\",\n sha256 = \"b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustc-demangle/0.1.27/download\"],\n strip_prefix = \"rustc-demangle-0.1.27\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rustc-demangle-0.1.27.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rustc_version-0.2.3\",\n sha256 = \"138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustc_version/0.2.3/download\"],\n strip_prefix = \"rustc_version-0.2.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rustc_version-0.2.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__rustix-1.1.4\",\n sha256 = \"b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustix/1.1.4/download\"],\n strip_prefix = \"rustix-1.1.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.rustix-1.1.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ruzstd-0.8.2\",\n sha256 = \"e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ruzstd/0.8.2/download\"],\n strip_prefix = \"ruzstd-0.8.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ruzstd-0.8.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__scopeguard-1.2.0\",\n sha256 = \"94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/scopeguard/1.2.0/download\"],\n strip_prefix = \"scopeguard-1.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.scopeguard-1.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__sec1-0.7.3\",\n sha256 = \"d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/sec1/0.7.3/download\"],\n strip_prefix = \"sec1-0.7.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.sec1-0.7.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__semver-0.9.0\",\n sha256 = \"1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/semver/0.9.0/download\"],\n strip_prefix = \"semver-0.9.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.semver-0.9.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__semver-parser-0.7.0\",\n sha256 = \"388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/semver-parser/0.7.0/download\"],\n strip_prefix = \"semver-parser-0.7.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.semver-parser-0.7.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde-1.0.228\",\n sha256 = \"9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde/1.0.228/download\"],\n strip_prefix = \"serde-1.0.228\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde-1.0.228.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde_core-1.0.228\",\n sha256 = \"41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde_core/1.0.228/download\"],\n strip_prefix = \"serde_core-1.0.228\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde_core-1.0.228.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde_derive-1.0.228\",\n sha256 = \"d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde_derive/1.0.228/download\"],\n strip_prefix = \"serde_derive-1.0.228\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde_derive-1.0.228.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__serde_json5-0.2.1\",\n sha256 = \"5d34d03f54462862f2a42918391c9526337f53171eaa4d8894562be7f252edd3\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/serde_json5/0.2.1/download\"],\n strip_prefix = \"serde_json5-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.serde_json5-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__sha2-0.10.9\",\n sha256 = \"a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/sha2/0.10.9/download\"],\n strip_prefix = \"sha2-0.10.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.sha2-0.10.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__sha3-0.10.8\",\n sha256 = \"75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/sha3/0.10.8/download\"],\n strip_prefix = \"sha3-0.10.8\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.sha3-0.10.8.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__signal-hook-registry-1.4.8\",\n sha256 = \"c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/signal-hook-registry/1.4.8/download\"],\n strip_prefix = \"signal-hook-registry-1.4.8\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.signal-hook-registry-1.4.8.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__signature-2.2.0\",\n sha256 = \"77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/signature/2.2.0/download\"],\n strip_prefix = \"signature-2.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.signature-2.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__simd-adler32-0.3.9\",\n sha256 = \"703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/simd-adler32/0.3.9/download\"],\n strip_prefix = \"simd-adler32-0.3.9\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.simd-adler32-0.3.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__slab-0.4.12\",\n sha256 = \"0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/slab/0.4.12/download\"],\n strip_prefix = \"slab-0.4.12\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.slab-0.4.12.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__smallvec-1.15.1\",\n sha256 = \"67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/smallvec/1.15.1/download\"],\n strip_prefix = \"smallvec-1.15.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.smallvec-1.15.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__smlang-0.8.0\",\n sha256 = \"1de84f9f80bbe6272174e2bfdb8cf7ce4815b218038a42161c2f21c1d872c215\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/smlang/0.8.0/download\"],\n strip_prefix = \"smlang-0.8.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.smlang-0.8.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__smlang-macros-0.8.0\",\n sha256 = \"231b4425dcc43afc7e18c34e7c6738cd252d42d91d909c948df14107c9ae79f1\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/smlang-macros/0.8.0/download\"],\n strip_prefix = \"smlang-macros-0.8.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.smlang-macros-0.8.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__socket2-0.6.3\",\n sha256 = \"3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/socket2/0.6.3/download\"],\n strip_prefix = \"socket2-0.6.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.socket2-0.6.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__stable_deref_trait-1.2.1\",\n sha256 = \"6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/stable_deref_trait/1.2.1/download\"],\n strip_prefix = \"stable_deref_trait-1.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.stable_deref_trait-1.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__string_morph-0.1.0\",\n sha256 = \"183aaf7fa637cc7b5f54c45b8f7cb6e8d73831f9f75a56b6defa5bf8c51d1699\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/string_morph/0.1.0/download\"],\n strip_prefix = \"string_morph-0.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.string_morph-0.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__strsim-0.11.1\",\n sha256 = \"7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/strsim/0.11.1/download\"],\n strip_prefix = \"strsim-0.11.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.strsim-0.11.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__subtle-2.6.1\",\n sha256 = \"13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/subtle/2.6.1/download\"],\n strip_prefix = \"subtle-2.6.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.subtle-2.6.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__syn-1.0.109\",\n sha256 = \"72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/syn/1.0.109/download\"],\n strip_prefix = \"syn-1.0.109\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.syn-1.0.109.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__syn-2.0.117\",\n sha256 = \"e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/syn/2.0.117/download\"],\n strip_prefix = \"syn-2.0.117\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.syn-2.0.117.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__terminal_size-0.4.4\",\n sha256 = \"230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/terminal_size/0.4.4/download\"],\n strip_prefix = \"terminal_size-0.4.4\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.terminal_size-0.4.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__thiserror-2.0.18\",\n sha256 = \"4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/thiserror/2.0.18/download\"],\n strip_prefix = \"thiserror-2.0.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.thiserror-2.0.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__thiserror-impl-2.0.18\",\n sha256 = \"ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/thiserror-impl/2.0.18/download\"],\n strip_prefix = \"thiserror-impl-2.0.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.thiserror-impl-2.0.18.bazel\"),\n )\n\n maybe(\n new_git_repository,\n name = \"rust_crates__tock-registers-0.9.0\",\n commit = \"9554639b17501a9f5940cef7a1770a0823e790c3\",\n init_submodules = True,\n remote = \"https://github.com/tock/tock.git\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tock-registers-0.9.0.bazel\"),\n strip_prefix = \"libraries/tock-register-interface\",\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__tokio-1.51.1\",\n sha256 = \"f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/tokio/1.51.1/download\"],\n strip_prefix = \"tokio-1.51.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tokio-1.51.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__tokio-macros-2.7.0\",\n sha256 = \"385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/tokio-macros/2.7.0/download\"],\n strip_prefix = \"tokio-macros-2.7.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tokio-macros-2.7.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__tokio-util-0.7.18\",\n sha256 = \"9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/tokio-util/0.7.18/download\"],\n strip_prefix = \"tokio-util-0.7.18\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.tokio-util-0.7.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__twox-hash-2.1.2\",\n sha256 = \"9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/twox-hash/2.1.2/download\"],\n strip_prefix = \"twox-hash-2.1.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.twox-hash-2.1.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__typenum-1.19.0\",\n sha256 = \"562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/typenum/1.19.0/download\"],\n strip_prefix = \"typenum-1.19.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.typenum-1.19.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ucd-trie-0.1.7\",\n sha256 = \"2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ucd-trie/0.1.7/download\"],\n strip_prefix = \"ucd-trie-0.1.7\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ucd-trie-0.1.7.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ufmt-0.2.0\",\n sha256 = \"1a64846ec02b57e9108d6469d98d1648782ad6bb150a95a9baac26900bbeab9d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ufmt/0.2.0/download\"],\n strip_prefix = \"ufmt-0.2.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ufmt-0.2.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ufmt-macros-0.3.0\",\n sha256 = \"d337d3be617449165cb4633c8dece429afd83f84051024079f97ad32a9663716\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ufmt-macros/0.3.0/download\"],\n strip_prefix = \"ufmt-macros-0.3.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ufmt-macros-0.3.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__ufmt-write-0.1.0\",\n sha256 = \"e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/ufmt-write/0.1.0/download\"],\n strip_prefix = \"ufmt-write-0.1.0\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.ufmt-write-0.1.0.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__unicode-ident-1.0.24\",\n sha256 = \"e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/unicode-ident/1.0.24/download\"],\n strip_prefix = \"unicode-ident-1.0.24\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.unicode-ident-1.0.24.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__universal-hash-0.5.1\",\n sha256 = \"fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/universal-hash/0.5.1/download\"],\n strip_prefix = \"universal-hash-0.5.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.universal-hash-0.5.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__utf8parse-0.2.2\",\n sha256 = \"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/utf8parse/0.2.2/download\"],\n strip_prefix = \"utf8parse-0.2.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.utf8parse-0.2.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__vcell-0.1.3\",\n sha256 = \"77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/vcell/0.1.3/download\"],\n strip_prefix = \"vcell-0.1.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.vcell-0.1.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__version_check-0.9.5\",\n sha256 = \"0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/version_check/0.9.5/download\"],\n strip_prefix = \"version_check-0.9.5\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.version_check-0.9.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__void-1.0.2\",\n sha256 = \"6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/void/1.0.2/download\"],\n strip_prefix = \"void-1.0.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.void-1.0.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__volatile-register-0.2.2\",\n sha256 = \"de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/volatile-register/0.2.2/download\"],\n strip_prefix = \"volatile-register-0.2.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.volatile-register-0.2.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__wasi-0.11.1-wasi-snapshot-preview1\",\n sha256 = \"ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/wasi/0.11.1+wasi-snapshot-preview1/download\"],\n strip_prefix = \"wasi-0.11.1+wasi-snapshot-preview1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.wasi-0.11.1+wasi-snapshot-preview1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__windows-link-0.2.1\",\n sha256 = \"f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/windows-link/0.2.1/download\"],\n strip_prefix = \"windows-link-0.2.1\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.windows-link-0.2.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__windows-sys-0.61.2\",\n sha256 = \"ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/windows-sys/0.61.2/download\"],\n strip_prefix = \"windows-sys-0.61.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.windows-sys-0.61.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zerocopy-0.8.48\",\n sha256 = \"eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zerocopy/0.8.48/download\"],\n strip_prefix = \"zerocopy-0.8.48\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zerocopy-0.8.48.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zerocopy-derive-0.8.48\",\n sha256 = \"70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zerocopy-derive/0.8.48/download\"],\n strip_prefix = \"zerocopy-derive-0.8.48\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zerocopy-derive-0.8.48.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zeroize-1.8.2\",\n sha256 = \"b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zeroize/1.8.2/download\"],\n strip_prefix = \"zeroize-1.8.2\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zeroize-1.8.2.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"rust_crates__zeroize_derive-1.4.3\",\n sha256 = \"85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/zeroize_derive/1.4.3/download\"],\n strip_prefix = \"zeroize_derive-1.4.3\",\n build_file = Label(\"@rust_crates//rust_crates:BUILD.zeroize_derive-1.4.3.bazel\"),\n )\n\n return [\n struct(repo=\"rust_crates__aes-0.8.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__aes-gcm-0.10.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__aligned-0.4.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__anyhow-1.0.102\", is_dev_dep = False),\n struct(repo=\"rust_crates__bitfield-0.14.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__bitfield-struct-0.11.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__bitflags-2.11.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__byteorder-1.5.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__cfg-if-1.0.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__cipher-0.4.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__clap-4.6.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__compiler_builtins-0.1.160\", is_dev_dep = False),\n struct(repo=\"rust_crates__cortex-m-0.7.7\", is_dev_dep = False),\n struct(repo=\"rust_crates__ctr-0.9.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__ecdsa-0.16.9\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-hal-1.0.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-hal-async-1.0.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-hal-nb-1.0.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__embedded-io-0.6.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__futures-0.3.32\", is_dev_dep = False),\n struct(repo=\"rust_crates__heapless-0.9.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__hex-0.4.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__hmac-0.12.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__k256-0.13.4\", is_dev_dep = False),\n struct(repo=\"rust_crates__log-0.4.29\", is_dev_dep = False),\n struct(repo=\"rust_crates__memoffset-0.9.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__minijinja-2.19.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__nb-1.1.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__nom-7.1.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__object-0.37.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__p256-0.13.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__p384-0.13.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__paste-1.0.15\", is_dev_dep = False),\n struct(repo=\"rust_crates__proc-macro2-1.0.106\", is_dev_dep = False),\n struct(repo=\"rust_crates__prost-0.13.5\", is_dev_dep = False),\n struct(repo=\"rust_crates__quote-1.0.45\", is_dev_dep = False),\n struct(repo=\"rust_crates__rand_core-0.9.5\", is_dev_dep = False),\n struct(repo=\"rust_crates__riscv-0.12.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__riscv-rt-0.12.2\", is_dev_dep = False),\n struct(repo=\"rust_crates__riscv-semihosting-0.1.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__rustc-demangle-0.1.27\", is_dev_dep = False),\n struct(repo=\"rust_crates__sec1-0.7.3\", is_dev_dep = False),\n struct(repo=\"rust_crates__serde-1.0.228\", is_dev_dep = False),\n struct(repo=\"rust_crates__serde_derive-1.0.228\", is_dev_dep = False),\n struct(repo=\"rust_crates__serde_json5-0.2.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__sha2-0.10.9\", is_dev_dep = False),\n struct(repo=\"rust_crates__sha3-0.10.8\", is_dev_dep = False),\n struct(repo=\"rust_crates__smlang-0.8.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__subtle-2.6.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__syn-1.0.109\", is_dev_dep = False),\n struct(repo=\"rust_crates__syn-2.0.117\", is_dev_dep = False),\n struct(repo=\"rust_crates__thiserror-2.0.18\", is_dev_dep = False),\n struct(repo=\"rust_crates__tock-registers-0.9.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__tokio-1.51.1\", is_dev_dep = False),\n struct(repo=\"rust_crates__tokio-util-0.7.18\", is_dev_dep = False),\n struct(repo=\"rust_crates__ufmt-0.2.0\", is_dev_dep = False),\n struct(repo=\"rust_crates__zerocopy-0.8.48\", is_dev_dep = False),\n struct(repo=\"rust_crates__zeroize-1.8.2\", is_dev_dep = False),\n ]\n" } } }, @@ -5211,6 +5211,54 @@ "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"ucd_trie\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"std\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=ucd-trie\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\": [],\n \"@rules_rust//rust/platform:x86_64-apple-darwin\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.1.7\",\n)\n" } }, + "rust_crates__ufmt-0.2.0": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "1a64846ec02b57e9108d6469d98d1648782ad6bb150a95a9baac26900bbeab9d", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/ufmt/0.2.0/download" + ], + "strip_prefix": "ufmt-0.2.0", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"ufmt\",\n deps = [\n \"@rust_crates__ufmt-write-0.1.0//:ufmt_write\",\n ],\n proc_macro_deps = [\n \"@rust_crates__ufmt-macros-0.3.0//:ufmt_macros\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=ufmt\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\": [],\n \"@rules_rust//rust/platform:x86_64-apple-darwin\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.2.0\",\n)\n" + } + }, + "rust_crates__ufmt-macros-0.3.0": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "d337d3be617449165cb4633c8dece429afd83f84051024079f97ad32a9663716", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/ufmt-macros/0.3.0/download" + ], + "strip_prefix": "ufmt-macros-0.3.0", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n name = \"ufmt_macros\",\n deps = [\n \"@rust_crates__proc-macro2-1.0.106//:proc_macro2\",\n \"@rust_crates__quote-1.0.45//:quote\",\n \"@rust_crates__syn-1.0.109//:syn\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=ufmt-macros\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\": [],\n \"@rules_rust//rust/platform:x86_64-apple-darwin\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.3.0\",\n)\n" + } + }, + "rust_crates__ufmt-write-0.1.0": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/ufmt-write/0.1.0/download" + ], + "strip_prefix": "ufmt-write-0.1.0", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"ufmt_write\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2018\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=ufmt-write\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\": [],\n \"@rules_rust//rust/platform:x86_64-apple-darwin\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.1.0\",\n)\n" + } + }, "rust_crates__unicode-ident-1.0.24": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { diff --git a/third_party/crates_io/Cargo.lock b/third_party/crates_io/Cargo.lock index 609665a8..90b7ab7b 100644 --- a/third_party/crates_io/Cargo.lock +++ b/third_party/crates_io/Cargo.lock @@ -1187,6 +1187,7 @@ dependencies = [ "tock-registers", "tokio", "tokio-util", + "ufmt", "zerocopy", "zeroize", ] @@ -1533,6 +1534,33 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "ufmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a64846ec02b57e9108d6469d98d1648782ad6bb150a95a9baac26900bbeab9d" +dependencies = [ + "ufmt-macros", + "ufmt-write", +] + +[[package]] +name = "ufmt-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d337d3be617449165cb4633c8dece429afd83f84051024079f97ad32a9663716" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ufmt-write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/third_party/crates_io/Cargo.toml b/third_party/crates_io/Cargo.toml index 4984d1a2..ef76dc05 100644 --- a/third_party/crates_io/Cargo.toml +++ b/third_party/crates_io/Cargo.toml @@ -34,6 +34,7 @@ smlang = { version = "0.8.0", default-features = false } syn = { version = "2.0.104", features = ["full", "extra-traits"] } syn1 = { package = "syn", version = "1.0.109", features = ["full", "extra-traits"] } tock-registers = "0.9.0" +ufmt = "0.2.0" zerocopy = { version = "0.8.48", default-features = false, features = ["derive"] } zeroize = { version = "1.8", default-features = false, features = ["derive"] } diff --git a/util/console/BUILD.bazel b/util/console/BUILD.bazel new file mode 100644 index 00000000..f9d475f4 --- /dev/null +++ b/util/console/BUILD.bazel @@ -0,0 +1,57 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_static_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "console", + srcs = [ + "lib.rs", + ], + edition = "2024", + deps = [ + ":dbg_print", + "@rust_crates//:ufmt", + ] + select({ + "@platforms//os:none": [ + ":pigweed", + ], + "//conditions:default": [":stdout"], + }), +) + +rust_doc( + name = "console_doc", + crate = ":console", +) + +rust_static_library( + name = "stdout", + srcs = [ + "stdout.rs", + ], + crate_name = "console_stdout", + edition = "2024", +) + +rust_static_library( + name = "pigweed", + srcs = [ + "pigweed.rs", + ], + crate_name = "console_pigweed", + edition = "2024", + rustc_flags = [ + "-C", + "panic=abort", + ], +) + +cc_library( + name = "dbg_print", + srcs = ["dbg_print.c"], + hdrs = ["dbg_print.h"], + alwayslink = 1, +) diff --git a/util/console/dbg_print.c b/util/console/dbg_print.c new file mode 100644 index 00000000..c492577b --- /dev/null +++ b/util/console/dbg_print.c @@ -0,0 +1,131 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#include "util/console/dbg_print.h" + +#include +#include +#include +#include + +extern void system_lowlevel_console_write(const char *buf, size_t len); + +static const char kHexTable[17] = "0123456789abcdef"; + +static size_t print_integer(char *dest, unsigned value, bool is_signed) { + char buf[12]; + char *b = buf + sizeof(buf); + size_t len = 0; + if (is_signed && (int)value < 0) { + *dest++ = ('-'); + len++; + value = (unsigned)(-(int)value); + } + *--b = '\0'; + do { + *--b = '0' + value % 10; + value /= 10; + } while (value); + while (*b) { + *dest++ = (*b++); + len++; + } + return len; +} + +void dbg_printf(const char *format, ...) { + char buffer[256]; + char *buf = buffer; + + va_list args; + va_start(args, format); + + for (; *format != '\0'; ++format) { + if (*format != '%') { + *buf++ = *format; + continue; + } + + ++format; // Skip over the '%'. + switch (*format) { + case '%': + *buf++ = *format; + break; + case 'c': { + int ch = va_arg(args, int); + *buf++ = (char)ch; + break; + } + case 'C': { + uint32_t val = va_arg(args, uint32_t); + for (size_t i = 0; i < sizeof(uint32_t); ++i, val >>= 8) { + uint8_t ch = (uint8_t)val; + if (ch >= 32 && ch < 127) { + *buf++ = (char)ch; + } else { + *buf++ = ('\\'); + *buf++ = ('x'); + *buf++ = (kHexTable[ch >> 4]); + *buf++ = (kHexTable[ch & 15]); + } + } + break; + } + case 's': { + // Print a null-terminated string. + const char *str = va_arg(args, const char *); + while (*str != '\0') { + *buf++ = (*str++); + } + break; + } + case 'd': + // `print_integer` will handle the sign bit of the value. + buf += print_integer(buf, va_arg(args, unsigned), true); + break; + case 'u': + buf += print_integer(buf, va_arg(args, unsigned), false); + break; + case 'p': + case 'x': { + // Print an `unsigned int` in hexadecimal. + unsigned int v = va_arg(args, unsigned int); + for (size_t i = 0; i < sizeof(v) * 2; ++i) { + int shift = sizeof(v) * 8 - 4; + *buf++ = (kHexTable[v >> shift]); + v <<= 4; + } + break; + } + default: + // For an invalid format specifier, back up one char and allow + // the output via the normal mechanism. + *buf++ = ('%'); + --format; + } + } + va_end(args); + system_lowlevel_console_write(buffer, buf - buffer); +} + +void dbg_hexdump(const void *data, size_t len) { + const uint8_t *p = (const uint8_t *)data; + size_t j = 0; + + while (j < len) { + // hexbuf is initialized as 48 spaces followed by a nul byte. + char hexbuf[] = " "; + // ascii is initialized as 17 nul bytes. + char ascii[17] = { + 0, + }; + dbg_printf("%p: ", p); + for (size_t i = 0; i < 16 && j < len; ++p, ++i, ++j) { + uint8_t val = *p; + hexbuf[i * 3 + 0] = kHexTable[val >> 4]; + hexbuf[i * 3 + 1] = kHexTable[val & 15]; + ascii[i] = (val >= 32 && val < 127) ? (char)val : '.'; + } + dbg_printf("%s %s\r\n", hexbuf, ascii); + } +} diff --git a/util/console/dbg_print.h b/util/console/dbg_print.h new file mode 100644 index 00000000..57c540bb --- /dev/null +++ b/util/console/dbg_print.h @@ -0,0 +1,53 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#ifndef OPENPROT_UTIL_CONSOLE_DBG_PRINT_H_ +#define OPENPROT_UTIL_CONSOLE_DBG_PRINT_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * An intentionally pared-down implementation of `printf()` that writes + * to UART0. + * + * This function only supports the format specifiers required by the + * ROM: + * - %c prints a single character. + * - %C prints a 'FourCC' style uint32_t (ASCII bytes in little-endian order). + * - %d prints a signed int in decimal. + * - %u prints an unsigned int in decimal. + * - %s prints a nul-terminated string. + * - %p prints pointer in hexadecimal. + * - %x prints an `unsigned int` in hexadecimal using lowercase characters. + * + * No modifiers are supported and the leading zeros in hexidecimal + * values are always printed. + * + * Note: unfortunately `uint32_t` is not necessarily an alias for + * `unsigned int`. An explicit cast is therefore necessary when printing + * `uint32_t` values using the `%x` format specifier in order to satisfy + * the `printf` format checker (`-Wformat`). + * + * @param format The format specifier. + * @param ... The values to interpolate in the format. + * @return The result of the operation. + */ +void dbg_printf(const char *format, ...) __attribute__((format(printf, 1, 2))); + +/** + * Hexdump a region of memory. + * + * @param data The memory to dump. + * @param len The length of the region to dump. + */ +void dbg_hexdump(const void *data, size_t len); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // OPENPROT_UTIL_CONSOLE_DBG_PRINT_H_ diff --git a/util/console/lib.rs b/util/console/lib.rs new file mode 100644 index 00000000..69f75087 --- /dev/null +++ b/util/console/lib.rs @@ -0,0 +1,69 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Console +//! +//! This crate provides basic console functionality. + +#![no_std] + +use core::convert::Infallible; + +pub use ufmt; +use ufmt::uWrite; +pub use ufmt::{uwrite, uwriteln}; + +pub struct Console; + +unsafe extern "C" { + fn system_lowlevel_console_write(ptr: *const u8, length: usize); +} + +impl uWrite for Console { + type Error = Infallible; + + fn write_str(&mut self, s: &str) -> Result<(), Infallible> { + // ok: unsafe-usage + unsafe { + // SAFETY: The pointer can never be null. + system_lowlevel_console_write(s.as_ptr(), s.len()) + }; + Ok(()) + } +} + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => {{ + use $crate::ufmt; + $crate::uwrite!(&mut $crate::Console, $($arg)*).unwrap(); + }}; +} + +#[macro_export] +macro_rules! println { + ($($arg:tt)*) => {{ + use $crate::ufmt; + $crate::ufmt::uwriteln!(&mut $crate::Console, $($arg)*).unwrap(); + }}; +} + +#[macro_export] +macro_rules! trace { + ($($arg:tt)*) => { + if cfg!(feature = "trace") { + use $crate::ufmt; + $crate::uwrite!(&mut $crate::Console, $($arg)*).unwrap(); + } + }; +} + +#[macro_export] +macro_rules! traceln { + ($($arg:tt)*) => { + if cfg!(feature = "trace") { + use $crate::ufmt; + $crate::uwriteln!(&mut $crate::Console, $($arg)*).unwrap(); + } + }; +} diff --git a/util/console/pigweed.rs b/util/console/pigweed.rs new file mode 100644 index 00000000..3af23146 --- /dev/null +++ b/util/console/pigweed.rs @@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +use core::arch::naked_asm; + +/// This is a low-level console output function that works with firmware +/// code. This function is a wrapper for the pigweed `DebugLog` syscall. +/// We make the syscall directly because this is a static library and we +/// don't want to create duplicate symbols for the syscall crate. +/// +/// # Safety +/// +/// Callers must supply a valid ptr and length. +#[unsafe(no_mangle)] +#[unsafe(naked)] +pub unsafe extern "C" fn system_lowlevel_console_write(ptr: *const u8, length: usize) { + // This bit of assembly code is the same as: + // let _ = syscall::debug_log(bytes); + naked_asm!(" + li t0, {id} + ecall + ret + ", + id = const 0xF002_u32, + ); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/util/console/stdout.rs b/util/console/stdout.rs new file mode 100644 index 00000000..44106ba1 --- /dev/null +++ b/util/console/stdout.rs @@ -0,0 +1,18 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use std::io::Write; + +/// This is a low-level console output function that works with host-based +/// code. This simply outputs to stdout. +/// +/// # Safety +/// +/// Callers must supply a valid ptr and length. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn system_lowlevel_console_write(ptr: *const u8, length: usize) { + // SAFETY: ptr and length must be valid. + let bytes = unsafe { core::slice::from_raw_parts(ptr, length) }; + let _ = std::io::stdout().write_all(bytes); + let _ = std::io::stdout().flush(); +} From 48d6de8dc99f6bccf3440477142e9a97e1e9ca05 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 18 Mar 2026 21:35:28 -0700 Subject: [PATCH 04/46] hal: add usb hal and stack Signed-off-by: Chris Frantz --- hal/blocking/usb/BUILD.bazel | 27 ++ hal/blocking/usb/descriptor.rs | 760 +++++++++++++++++++++++++++++++++ hal/blocking/usb/driver.rs | 88 ++++ hal/blocking/usb/lib.rs | 412 ++++++++++++++++++ protocol/usb/stack/BUILD.bazel | 26 ++ protocol/usb/stack/lib.rs | 441 +++++++++++++++++++ 6 files changed, 1754 insertions(+) create mode 100644 hal/blocking/usb/BUILD.bazel create mode 100644 hal/blocking/usb/descriptor.rs create mode 100644 hal/blocking/usb/driver.rs create mode 100644 hal/blocking/usb/lib.rs create mode 100644 protocol/usb/stack/BUILD.bazel create mode 100644 protocol/usb/stack/lib.rs diff --git a/hal/blocking/usb/BUILD.bazel b/hal/blocking/usb/BUILD.bazel new file mode 100644 index 00000000..e9989f4c --- /dev/null +++ b/hal/blocking/usb/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "hal_usb", + srcs = [ + "descriptor.rs", + "driver.rs", + "lib.rs", + ], + crate_name = "hal_usb", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:aligned", + "@rust_crates//:ufmt", + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "hal_usb_test", + crate = ":hal_usb", + rustc_flags = [ + "-C", + "debug-assertions", + ], +) diff --git a/hal/blocking/usb/descriptor.rs b/hal/blocking/usb/descriptor.rs new file mode 100644 index 00000000..e0f06c30 --- /dev/null +++ b/hal/blocking/usb/descriptor.rs @@ -0,0 +1,760 @@ +use aligned::Aligned; +use aligned::A4; +use ufmt::uWrite; + +pub const USB_CLASS_AUDIO: u8 = 0x01; +pub const USB_CLASS_COMMUNIATIONS: u8 = 0x02; +pub const USB_CLASS_HID: u8 = 0x03; +pub const USB_CLASS_PHYSICAL: u8 = 0x05; +pub const USB_CLASS_IMAGE: u8 = 0x06; +pub const USB_CLASS_PRINTER: u8 = 0x07; +pub const USB_CLASS_MASS_STORAGE: u8 = 0x08; +pub const USB_CLASS_HUB: u8 = 0x09; +pub const USB_CLASS_CDC_DATA: u8 = 0x0a; +pub const USB_CLASS_SMART_CARD: u8 = 0x0b; +pub const USB_CLASS_CONTENT_SECURITY: u8 = 0x0d; +pub const USB_CLASS_VIDEO: u8 = 0x0e; +pub const USB_CLASS_PERSONAL_HEALTHCARE: u8 = 0x0f; +pub const USB_CLASS_AUDIO_VIDEO: u8 = 0x10; +pub const USB_CLASS_BILLBOARD: u8 = 0x11; +pub const USB_CLASS_USB_TYPEC_BRIDGE: u8 = 0x12; +pub const USB_CLASS_BULK_DISPLAY: u8 = 0x13; +pub const USB_CLASS_MCTP: u8 = 0x14; +pub const USB_CLASS_I3C: u8 = 0x3c; +pub const USB_CLASS_DIAGNOSTIC_DEVICE: u8 = 0xdc; +pub const USB_CLASS_WIRELESS_CONTROLLER: u8 = 0xe0; +pub const USB_CLASS_MISC: u8 = 0xef; +pub const USB_CLASS_APPLICATION_SPECIFIC: u8 = 0xfe; +pub const USB_CLASS_VENDOR: u8 = 0xff; + +pub const USB_SUBCLASS_APPLICATION_SPECIFIC_DFU: u8 = 0x01; + +pub const USB_PROTOCOL_APPLICATION_SPECIFIC_DFU_RUNTIME_MODE: u8 = 0x01; +pub const USB_PROTOCOL_APPLICATION_SPECIFIC_DFU_DFU_MODE: u8 = 0x02; + +use crate::DescriptorType; +use crate::Direction; + +#[derive(Clone, Copy, Eq, PartialEq)] +#[repr(transparent)] +pub struct StringHandle(pub u8); +impl StringHandle { + pub const NONE: Self = StringHandle(0); +} + +pub struct DeviceDescriptor { + pub device_class: DeviceClass, + pub device_sub_class: u8, + pub device_protocol: u8, + pub max_packet_size: u8, + pub vendor_id: u16, + pub product_id: u16, + pub device_release_num: u16, + pub manufacturer: StringHandle, + pub product: StringHandle, + pub serial_num: StringHandle, +} +impl DeviceDescriptor { + const SIZE: usize = 18; + + #[allow(dead_code)] + pub(crate) const fn total_size(&self) -> usize { + Self::SIZE + } + + #[allow(clippy::identity_op)] + pub const fn serialize(&self) -> [u8; Self::SIZE] { + let mut buf = [0u8; Self::SIZE]; + + // sizeof descriptor + buf[0] = 18; + // bDescriptorType = Device + buf[1] = 1; + // USB version 2.0 + buf[2] = 0x00; + buf[3] = 0x02; + + buf[4] = self.device_class.0; + buf[5] = self.device_sub_class; + buf[6] = self.device_protocol; + buf[7] = self.max_packet_size; + + buf[8] = ((self.vendor_id & 0x00ff) >> 0) as u8; + buf[9] = ((self.vendor_id & 0xff00) >> 8) as u8; + + buf[10] = ((self.product_id & 0x00ff) >> 0) as u8; + buf[11] = ((self.product_id & 0xff00) >> 8) as u8; + + buf[12] = ((self.device_release_num & 0x00ff) >> 0) as u8; + buf[13] = ((self.device_release_num & 0xff00) >> 8) as u8; + + buf[14] = self.manufacturer.0; + buf[15] = self.product.0; + buf[16] = self.serial_num.0; + + // num configurations + buf[17] = 1; + buf + } +} + +pub struct ConfigDescriptor { + pub configuration_value: u8, + // in 2 mA units + pub max_power: u8, + pub self_powered: bool, + pub remote_wakeup: bool, + pub interfaces: &'static [InterfaceDescriptor], +} + +impl ConfigDescriptor { + const SIZE: usize = 9; + + pub const fn total_size(&self) -> usize { + let mut result = Self::SIZE; + let mut i = 0; + while i < self.interfaces.len() { + result += self.interfaces[i].total_size(); + i += 1; + } + result + } + + #[allow(clippy::identity_op)] + pub const fn serialize(&self) -> [u8; RESULT_SIZE] { + assert!(self.total_size() == RESULT_SIZE); + let mut buf = [0u8; RESULT_SIZE]; + + // alternates don't count towards total + // as interfaces numbers are per spec monotonically increasing, we can use that as the count + let mut uniq_interface_count = 0; + let mut i = 0; + while i < self.interfaces.len() { + if self.interfaces[i].interface_number + 1 > uniq_interface_count { + uniq_interface_count = self.interfaces[i].interface_number + 1 + } + i += 1; + } + + // sizeof descriptor + buf[0] = 9; + // bDescriptorType = Configuration + buf[1] = 2; + buf[2] = ((RESULT_SIZE & 0x00ff) >> 0) as u8; + buf[3] = ((RESULT_SIZE & 0xff00) >> 8) as u8; + buf[4] = uniq_interface_count; + buf[5] = self.configuration_value; + // iConfiguration + buf[6] = 0; + buf[7] = (1 << 7) | // must be 1 (USB 1.0 bus powered) + if self.self_powered { 1 << 6 } else { 0 } | + if self.remote_wakeup { 1 << 5 } else { 0 }; + buf[8] = self.max_power; + + let mut offset = 9; + + let mut i = 0; + while i < self.interfaces.len() { + let (iface_buf, iface_buf_len) = self.interfaces[i].serialize::(); + let mut iface_offset = 0; + while iface_offset < iface_buf_len { + buf[offset] = iface_buf[iface_offset]; + iface_offset += 1; + offset += 1; + } + i += 1; + } + buf + } +} + +pub struct InterfaceDescriptor { + pub name: StringHandle, + pub alternate_setting: u8, + pub interface_number: u8, + pub interface_class: u8, + pub interface_sub_class: u8, + pub interface_protocol: u8, + pub func_descs: &'static [FunctionalDescriptor], + pub endpoints: &'static [EndpointDescriptor], +} +impl InterfaceDescriptor { + const SIZE: usize = 9; + + const fn total_size(&self) -> usize { + let mut result = Self::SIZE; + let mut i = 0; + while i < self.func_descs.len() { + result += self.func_descs[i].total_size(); + i += 1; + } + let mut i = 0; + while i < self.endpoints.len() { + result += self.endpoints[i].total_size(); + i += 1; + } + result + } + pub const fn serialize(&self) -> ([u8; RESULT_SIZE], usize) { + assert!(RESULT_SIZE >= self.total_size()); + + let mut buf = [0u8; RESULT_SIZE]; + + // sizeof descriptor + buf[0] = 9; + // bDescriptorType = Interface + buf[1] = 4; + buf[2] = self.interface_number; + buf[3] = self.alternate_setting; + buf[4] = self.endpoints.len() as u8; + buf[5] = self.interface_class; + buf[6] = self.interface_sub_class; + buf[7] = self.interface_protocol; + // iInterface: Index of string descriptor describing this interface + buf[8] = self.name.0; + let mut offset = 9; + + let mut i = 0; + while i < self.func_descs.len() { + self.func_descs[i].serialize(&mut buf, offset); + offset += self.func_descs[i].total_size(); + i += 1; + } + + let mut i = 0; + while i < self.endpoints.len() { + let ep_buf = self.endpoints[i].serialize(i as u8); + let mut ep_offset = 0; + while ep_offset < ep_buf.len() { + buf[offset] = ep_buf[ep_offset]; + offset += 1; + ep_offset += 1; + } + i += 1; + } + (buf, offset) + } +} + +pub struct EndpointDescriptor { + pub direction: Direction, + pub endpoint_num: u8, + pub transfer_type: TransferType, + pub max_packet_size: u16, + pub interval: u8, +} +impl EndpointDescriptor { + const SIZE: usize = 7; + + const fn total_size(&self) -> usize { + Self::SIZE + } + + #[allow(clippy::identity_op)] + const fn serialize(&self, _index: u8) -> [u8; Self::SIZE] { + let mut buf = [0u8; Self::SIZE]; + + // sizeof descriptor + buf[0] = Self::SIZE as u8; + // bDescriptorType = endpoint + buf[1] = 5; + buf[2] = self.endpoint_num & 0x7 + | match self.direction { + Direction::HostToDevice => 0, + Direction::DeviceToHost => 1 << 7, + }; + buf[3] = match self.transfer_type { + TransferType::Control => 0, + TransferType::Isochronous(sync_type, usage_type) => { + 1 | match sync_type { + SynchronizationType::None => 0 << 2, + SynchronizationType::Asynchronous => 1 << 2, + SynchronizationType::Adaptive => 2 << 2, + SynchronizationType::Synchronous => 3 << 3, + } | match usage_type { + UsageType::DataEndpoint => 0 << 4, + UsageType::FeedbackEndpoint => 1 << 4, + UsageType::ExplicitFeedbackDataEndpoint => 2 << 4, + } + } + TransferType::Bulk => 2, + TransferType::Interrupt => 3, + }; + + buf[4] = ((self.max_packet_size & 0x00ff) >> 0) as u8; + buf[5] = ((self.max_packet_size & 0xff00) >> 8) as u8; + buf[6] = self.interval; + buf + } +} + +pub struct StringDescriptor0 { + pub langs: &'static [u16], +} +impl StringDescriptor0 { + pub const fn total_size(&self) -> usize { + 2 + core::mem::size_of_val(self.langs) + } + + #[allow(clippy::identity_op)] + pub const fn serialize(&self) -> [u8; RESULT_SIZE] { + assert!(RESULT_SIZE == self.total_size()); + assert!(self.total_size() <= (u8::MAX as usize)); + + let mut buf = [0u8; RESULT_SIZE]; + // sizeof descriptor + buf[0] = self.total_size() as u8; + // bDescriptorType = String + buf[1] = 3; + + let mut offset = 2; + let mut i = 0; + while i < self.langs.len() { + let bytes = self.langs[i].to_le_bytes(); + buf[offset + 0] = bytes[0]; + buf[offset + 1] = bytes[1]; + i += 1; + offset += 2; + } + buf + } +} + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub enum TransferType { + Control, + Isochronous(SynchronizationType, UsageType), + Bulk, + Interrupt, +} +impl TransferType { + #[allow(dead_code)] + fn as_eptyp(self) -> u32 { + match self { + TransferType::Control => 0, + TransferType::Isochronous(_, _) => 1, + TransferType::Bulk => 2, + TransferType::Interrupt => 3, + } + } +} + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub enum SynchronizationType { + None = 0, + Asynchronous = 1, + Adaptive = 2, + Synchronous = 3, +} + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code, clippy::enum_variant_names)] +pub enum UsageType { + DataEndpoint, + FeedbackEndpoint, + ExplicitFeedbackDataEndpoint, +} + +pub struct DeviceClass(pub u8); +impl DeviceClass { + pub const SPECIFIED_BY_INTERFACE: Self = Self(0x00); + pub const COMMUNICATIONS_AND_CDC: Self = Self(0x02); + pub const HUB: Self = Self(0x09); + pub const BILLBOARD: Self = Self(0x11); + pub const DIAGNOSTIC_DEVICE: Self = Self(0x3c); + pub const MISCELLANEOUS: Self = Self(0xef); + pub const VENDOR_SPECIFIED: Self = Self(0xff); +} +impl From for u8 { + fn from(val: DeviceClass) -> Self { + val.0 + } +} + +pub struct StringDescriptor(Aligned); + +impl StringDescriptor { + pub const fn const_from_ascii(s: &str) -> Self { + assert!(BYTE_LEN <= (u8::MAX as usize)); + assert!(s.len() * 2 + 2 == BYTE_LEN); + let mut result = [0u8; BYTE_LEN]; + result[0] = BYTE_LEN as u8; + result[1] = 0x03; // DescriptorType string + + let s = s.as_bytes(); + let mut i = 0; + while i < s.len() { + if s[i] >= 0x80 { + panic!("ascii characters only"); + } + result[2 + i * 2] = s[i]; + i += 1; + } + StringDescriptor(Aligned(result)) + } + pub const fn as_ref(&self) -> StringDescriptorRef<'_> { + StringDescriptorRef(&self.0) + } +} + +#[derive(Clone, Copy)] +pub struct StringDescriptorRef<'a>(pub &'a Aligned); +impl<'a> StringDescriptorRef<'a> { + pub const fn as_bytes(self) -> &'a Aligned { + self.0 + } +} + +#[macro_export] +macro_rules! string_descriptor { + ($s:expr) => { + $crate::StringDescriptor::<{ $s.len() * 2 + 2 }>::const_from_ascii($s) + }; +} + +#[derive(Debug)] +pub enum DescriptorErr { + Overflow, + Encoding, +} + +#[inline(always)] +pub fn hex_utf16_descriptor(dest: &mut [u8], src: &[u8]) -> Result { + const { assert!(cfg!(target_endian = "little")) }; + const HEX_CHARS: [u8; 16] = *b"0123456789abcdef"; + let total_len = src.len() * 4 + 2; + if dest.len() < total_len || total_len > 255 { + return Err(DescriptorErr::Overflow); + } + dest[0] = total_len as u8; + dest[1] = DescriptorType::STRING.0; + + let mut i = 2; + for src_byte in src.iter() { + dest[i] = HEX_CHARS[usize::from(*src_byte >> 4)]; + dest[i + 1] = 0; + dest[i + 2] = HEX_CHARS[usize::from(*src_byte & 0xf)]; + dest[i + 3] = 0; + i += 4; + } + Ok(total_len) +} + +#[inline(always)] +pub fn hex_utf16_descriptor_aligned<'a>( + dest: &'a mut Aligned, + src: &[u8], +) -> Result, DescriptorErr> { + let len = hex_utf16_descriptor(dest, src)?; + Ok(StringDescriptorRef(&dest[..len])) +} + +pub struct StringDescriptorWritter<'a> { + buf: &'a mut Aligned, + index: usize, +} +impl<'a> StringDescriptorWritter<'a> { + pub fn new(buf: &'a mut Aligned) -> Result { + if buf.len() < 2 || buf.len() > 2 + 255 { + return Err(DescriptorErr::Overflow); + } + *buf.get_mut(1).unwrap() = DescriptorType::STRING.0; + Ok(StringDescriptorWritter { buf, index: 2 }) + } + pub fn finalize(self) -> Result, DescriptorErr> { + *self.buf.get_mut(0).ok_or(DescriptorErr::Overflow)? = + u8::try_from(self.index).map_err(|_| DescriptorErr::Overflow)?; + + if self.index > self.buf.len() { + return Err(DescriptorErr::Overflow); + } + Ok(StringDescriptorRef(&self.buf[..self.index])) + } +} +impl uWrite for StringDescriptorWritter<'_> { + type Error = core::fmt::Error; + + fn write_str(&mut self, s: &str) -> Result<(), Self::Error> { + let bytes = s.as_bytes(); + let remaining_buf = self.buf.get_mut(self.index..).ok_or(core::fmt::Error)?; + + if remaining_buf.len() < bytes.len() * 2 { + return Err(core::fmt::Error); + } + + for &b in bytes { + if b >= 0x80 { + return Err(core::fmt::Error); + } + *self.buf.get_mut(self.index).ok_or(core::fmt::Error)? = b; + *self.buf.get_mut(self.index + 1).ok_or(core::fmt::Error)? = 0; + self.index += 2; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test_string_descriptor_writter { + use aligned::Aligned; + use aligned::A4; + use core::ops::Deref; + use ufmt::uwrite; + + use crate::StringDescriptorWritter; + + #[test] + fn works() { + let mut buf = Aligned::([0u8; 30]); + let mut writter = StringDescriptorWritter::new(&mut buf).unwrap(); + uwrite!(writter, "Hello").unwrap(); + uwrite!(writter, " ").unwrap(); + uwrite!(writter, "World").unwrap(); + let result = writter.finalize().unwrap(); + assert_eq!( + result.as_bytes().deref(), + &[ + 24, 3, b'H', 0, b'e', 0, b'l', 0, b'l', 0, b'o', 0, b' ', 0, b'W', 0, b'o', 0, + b'r', 0, b'l', 0, b'd', 0 + ] + ); + } + + #[test] + fn too_small_buffer() { + let mut buf = Aligned::([0u8; 1]); + assert!(StringDescriptorWritter::new(&mut buf).is_err()); + } + + #[test] + fn too_big_buffer() { + let mut buf = Aligned::([0u8; 258]); + assert!(StringDescriptorWritter::new(&mut buf).is_err()); + } + + #[test] + fn too_small_to_fit() { + let mut buf = Aligned::([0u8; 12]); + let mut writter = StringDescriptorWritter::new(&mut buf).unwrap(); + uwrite!(writter, "Hello").unwrap(); + assert!(uwrite!(writter, " ").is_err()); + } + + #[test] + fn non_ascii_char() { + let mut buf = Aligned::([0u8; 20]); + let mut writter = StringDescriptorWritter::new(&mut buf).unwrap(); + assert!(uwrite!(writter, "Héllö").is_err()); + } +} + +pub struct DfuFunctionalDescriptor { + /// New firmware can be received from the host + pub can_download: bool, + /// Current firmware can be sent back to the host + pub can_upload: bool, + /// Device can still communicate with the host after the manifestation phase. + pub manifestation_tolerant: bool, + /// Device will detach from the USB bus autonomously after receiving + /// DFU_DETACH; the host does not need to explicitly issue a bus reset. + pub will_detach: bool, + /// Timeout the device will wait to be reset by host after receiving DFU_DETACH. + pub detach_timeout_ms: u16, + /// The number of bytes the device can receive per control request. + pub transfer_size: u16, +} +impl DfuFunctionalDescriptor { + pub const fn total_size(&self) -> usize { + 9 + } + pub const fn serialize(&self, dest: &mut [u8], offset: usize) { + const fn bit(index: u8, val: bool) -> u8 { + (if val { 1 } else { 0 }) << index + } + const fn copy_u16(dest: &mut [u8], index: usize, val: u16) { + let bytes = val.to_le_bytes(); + dest[index] = bytes[0]; + dest[index + 1] = bytes[1]; + } + // sizeof descriptor + dest[offset] = 9; + // bDescriptorType = DFU Functional + dest[offset + 1] = 0x21; + // bmAttributes + dest[offset + 2] = bit(0, self.can_download) + | bit(1, self.can_upload) + | bit(2, self.manifestation_tolerant) + | bit(3, self.will_detach); + copy_u16(dest, offset + 3, self.detach_timeout_ms); + copy_u16(dest, offset + 5, self.transfer_size); + // bcdDFUVersion + copy_u16(dest, offset + 7, 0x0100); + } +} + +// This should be a trait, but traits can't be used from const functions :( +pub enum FunctionalDescriptor { + Dfu(DfuFunctionalDescriptor), +} + +impl FunctionalDescriptor { + pub const fn total_size(&self) -> usize { + match self { + Self::Dfu(dfu) => dfu.total_size(), + } + } + #[allow(clippy::identity_op)] + pub const fn serialize(&self, dest: &mut [u8], offset: usize) { + assert!(offset + self.total_size() <= dest.len()); + match self { + Self::Dfu(dfu) => dfu.serialize(dest, offset), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + const INTERFACE_NAME_HANDLE: StringHandle = StringHandle(5); + + const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[InterfaceDescriptor { + name: INTERFACE_NAME_HANDLE, + interface_number: 0, + alternate_setting: 0, + interface_class: 0xff, + interface_sub_class: 0xff, + interface_protocol: 0xff, + func_descs: &[], + endpoints: &[ + EndpointDescriptor { + direction: Direction::DeviceToHost, + endpoint_num: 1, + transfer_type: TransferType::Bulk, + max_packet_size: 64, + interval: 0, + }, + EndpointDescriptor { + direction: Direction::HostToDevice, + endpoint_num: 2, + transfer_type: TransferType::Bulk, + max_packet_size: 64, + interval: 0, + }, + ], + }], + }; + const CONFIG_DESC_RAW: [u8; CONFIG_DESC.total_size()] = CONFIG_DESC.serialize(); + + #[test] + fn test_config_desc() { + assert_eq!( + &CONFIG_DESC_RAW, + &[ + 0x09, 0x02, 0x20, 0x00, 0x01, 0x01, 0x00, 0x80, 0xfa, 0x09, 0x04, 0x00, 0x00, 0x02, + 0xff, 0xff, 0xff, 0x05, 0x07, 0x05, 0x81, 0x02, 0x40, 0x00, 0x00, 0x07, 0x05, 0x02, + 0x02, 0x40, 0x00, 0x00 + ] + ) + } + + #[test] + fn test_config_desc_dfu() { + const CONFIG_DESC_DFU: ConfigDescriptor = ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[InterfaceDescriptor { + name: INTERFACE_NAME_HANDLE, + interface_number: 0, + alternate_setting: 0, + interface_class: 0xfe, + interface_sub_class: 0x01, + interface_protocol: 0x02, + func_descs: &[FunctionalDescriptor::Dfu(DfuFunctionalDescriptor { + can_download: true, + can_upload: false, + manifestation_tolerant: true, + will_detach: true, + transfer_size: 2048, + detach_timeout_ms: 8000, + })], + endpoints: &[], + }], + }; + const CONFIG_DESC_BYTES: [u8; CONFIG_DESC_DFU.total_size()] = CONFIG_DESC_DFU.serialize(); + + assert_eq!( + &CONFIG_DESC_BYTES, + &[ + 0x09, 0x02, 0x1b, 0x00, 0x01, 0x01, 0x00, 0x80, 0xfa, 0x09, 0x04, 0x00, 0x00, 0x00, + 0xfe, 0x01, 0x02, 0x05, 0x09, 0x21, 0x0d, 0x40, 0x1f, 0x00, 0x08, 0x00, 0x01 + ] + ) + } + + #[test] + fn test_string_descriptor() { + use core::ops::Deref; + const USB_VENDOR: StringDescriptorRef = string_descriptor!("Mutask").as_ref(); + assert_eq!( + USB_VENDOR.as_bytes().deref(), + &[14, 3, b'M', 0, b'u', 0, b't', 0, b'a', 0, b's', 0, b'k', 0,] + ); + } + + #[test] + pub fn test_hex_utf16_descriptor() { + let mut buf = [0_u8; 80]; + let len = hex_utf16_descriptor(&mut buf, &[0xab, 0x1c, 0xd2, 0xe3, 0x4f, 0x56, 0x78, 0x90]) + .unwrap(); + assert_eq!( + [ + 34, 3, b'a', 0, b'b', 0, b'1', 0, b'c', 0, b'd', 0, b'2', 0, b'e', 0, b'3', 0, + b'4', 0, b'f', 0, b'5', 0, b'6', 0, b'7', 0, b'8', 0, b'9', 0, b'0', 0 + ], + &buf[..len] + ); + + // empty string; tight fit + let mut buf = [0_u8; 2]; + let len = hex_utf16_descriptor(&mut buf, b"").unwrap(); + assert_eq!(&[2, 3], &buf[..len]); + + // 1 byte; tight fit + let mut buf = [0_u8; 6]; + let len = hex_utf16_descriptor(&mut buf, &[0xca]).unwrap(); + assert_eq!(&[6, 3, b'c', 0, b'a', 0], &buf[..len]); + + // 2 bytes; tight fit + let mut buf = [0_u8; 10]; + let len = hex_utf16_descriptor(&mut buf, &[0xca, 0xfe]).unwrap(); + assert_eq!(&[10, 3, b'c', 0, b'a', 0, b'f', 0, b'e', 0], &buf[..len]); + + // too small to fit descriptor + let mut buf = [0_u8; 1]; + hex_utf16_descriptor(&mut buf, b"").unwrap_err(); + + // too small to fit 1 byte hex string + let mut buf = [0_u8; 5]; + hex_utf16_descriptor(&mut buf, b"H").unwrap_err(); + + // too small to fit 2 byte hex string + let mut buf = [0_u8; 9]; + hex_utf16_descriptor(&mut buf, b"Hi").unwrap_err(); + + // length too big to fit in length field + let mut buf = [0_u8; 258]; + hex_utf16_descriptor(&mut buf, &[0x42_u8; 64]).unwrap_err(); + } +} diff --git a/hal/blocking/usb/driver.rs b/hal/blocking/usb/driver.rs new file mode 100644 index 00000000..9a529441 --- /dev/null +++ b/hal/blocking/usb/driver.rs @@ -0,0 +1,88 @@ +use aligned::Aligned; +use aligned::A4; +use core::mem::MaybeUninit; + +use crate::SetupPacket; + +/// A trait implemented by drivers for USB peripheral controllers. +pub trait UsbDriver { + const MAX_PACKET_SIZE: usize; + type Packet<'a>: UsbPacket + where + Self: 'a; + + /// Store data in a peripheral buffer that will be transferred to the host + /// when it requests data from the IN endpoint at `endpoint_idx`. If + /// zlp=true and `data.len()` is a multiple of MAX_PACKET_SIZE, + /// send a zero-length packet after sending all the data. + /// + /// The return value is the number of bytes that were copied into the + /// peripheral buffer. It will be either a multiple of MAX_PACKET_SIZE, or `data.len()`. + /// + /// This function may fault or panic if endpoint_idx is invalid, or the hardware is misbehaving. + fn transfer_in(&mut self, endpoint_idx: u8, data: &Aligned, zlp: bool) -> usize; + + /// Stalls an input endpoint. Note: the driver will automatically unstall all endpoints upon a USB reset or a new SETUP packet. + fn stall_in(&mut self, endpoint_idx: u8, stalled: bool); + + /// Stalls an output endpoint. Note: the driver will automatically unstall all endpoints upon a USB reset or a new SETUP packet. + fn stall_out(&mut self, endpoint_idx: u8, stalled: bool); + + /// Sets the address the peripheral responds to. The USB stack must call + /// this function in response to a SET_ADDRESS control request on endpoint 0. + fn set_address(&mut self, address: u8); + + /// Polls the driver for an event. When a USB interrupt occurs, the USB + /// stack should call this function repeatedly until it returns None and + /// process the returned events. + fn poll(&mut self) -> Option>>; +} + +pub trait UsbPacket { + /// The endpoint the packet was received on. + fn endpoint_index(&self) -> usize; + + /// The length of the packet in bytes + fn len(&self) -> usize; + + /// Copy the packet data from the peripheral buffer into SRAM. Will fault if + /// `self.len()` > `dest.len()`. + fn copy_to_uninit(self, dest: &mut [MaybeUninit]) -> &Aligned; + + /// Copy the packet data from the peripheral buffer into SRAM. + fn copy_to(self, dest: &mut [u32]) -> &Aligned; + + fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +pub enum UsbEvent { + /// A SETUP packet has been received from the host. It can be read with TPacket::copy_to()... + SetupPacket { + pkt: SetupPacket, + endpoint: u8, + }, + + /// An OUT packet has been received from the host. It can be read with TPacket::copy_to()... + DataOutPacket(TPacket), + + /// A packet has been sent by the peripheral and an ACK has been received + /// from the host. This will have freed up some buffer space, so if the USB + /// stack has more data to send on this endpoint, it should attempt to + /// buffer it now with `UsbDriver::transfer_in()`. + PacketSent { + endpoint: u32, + }, + + VBus, + VBusLost, + LinkDown, + LinkUp, + UsbReset, + Suspend, + Resume, + + // TODO: Put these into the global error namespace... + ErrorUnexpectedBufId, +} diff --git a/hal/blocking/usb/lib.rs b/hal/blocking/usb/lib.rs new file mode 100644 index 00000000..02a6721c --- /dev/null +++ b/hal/blocking/usb/lib.rs @@ -0,0 +1,412 @@ +#![cfg_attr(not(test), no_std)] + +mod descriptor; +pub mod driver; + +use ufmt::derive::uDebug; + +pub use descriptor::*; + +// Big endian is dead; code in this file assumes little-endian +const _: () = assert!(cfg!(target_endian = "little")); + +#[derive(Clone, Copy, Eq, PartialEq)] +#[repr(transparent)] +pub struct Request(u16); +#[allow(clippy::identity_op)] +impl Request { + pub const fn new( + direction: Direction, + ty: RequestType, + recipient: Recipient, + request: u8, + ) -> Self { + Self( + ((direction as u16) << 7) + | ((ty as u16) << 5) + | ((recipient as u16) << 0) + | ((request as u16) << 8), + ) + } + pub fn direction(&self) -> Direction { + Direction::try_from((u32::from(self.0) >> 7) & 0x1).unwrap() + } + pub fn request_type(&self) -> RequestType { + RequestType::try_from(u32::from((self.0 >> 5) & 0x3)).unwrap() + } + pub fn recipient(&self) -> Recipient { + Recipient::try_from(u32::from((self.0 >> 0) & 0x1f)).unwrap() + } + pub fn request(&self) -> u8 { + u8::try_from((self.0 >> 8) & 0xff).unwrap() + } +} +impl ufmt::uDebug for Request { + fn fmt( + &self, + f: &mut ufmt::Formatter<'_, W>, + ) -> Result<(), W::Error> { + f.debug_struct("usb::Request")? + .field("request_type", &self.request_type())? + .field("direction", &self.direction())? + .field("recipient", &self.recipient())? + .field("request", &self.request())? + .finish() + } +} +impl Request { + pub const DEVICE_GET_STATUS: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Device, + 0x00, + ); + pub const DEVICE_CLEAR_FEATURE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Device, + 0x01, + ); + pub const DEVICE_SET_FEATURE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Device, + 0x03, + ); + pub const DEVICE_SET_ADDRESS: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Device, + 0x05, + ); + pub const DEVICE_GET_DESCRIPTOR: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Device, + 0x06, + ); + pub const DEVICE_SET_DESCRIPTOR: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Device, + 0x07, + ); + pub const DEVICE_GET_CONFIGURATION: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Device, + 0x08, + ); + pub const DEVICE_SET_CONFIGURATION: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Device, + 0x09, + ); + pub const INTERFACE_GET_STATUS: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Interface, + 0x00, + ); + pub const INTERFACE_CLEAR_FEATURE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Interface, + 0x01, + ); + pub const INTERFACE_SET_FEATURE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Interface, + 0x03, + ); + pub const INTERFACE_GET_INTERFACE: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Interface, + 0x0a, + ); + pub const INTERFACE_SET_INTERFACE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Interface, + 0x0b, + ); + pub const ENDPOINT_GET_STATUS: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Endpoint, + 0x00, + ); + pub const ENDPOINT_CLEAR_FEATURE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Endpoint, + 0x01, + ); + pub const ENDPOINT_SET_FEATURE: Self = Self::new( + Direction::HostToDevice, + RequestType::Standard, + Recipient::Endpoint, + 0x03, + ); + pub const ENDPOINT_SYNCH_FRAME: Self = Self::new( + Direction::DeviceToHost, + RequestType::Standard, + Recipient::Endpoint, + 0x12, + ); +} +impl From for u16 { + fn from(val: Request) -> Self { + val.0 + } +} +#[cfg(test)] +mod request_tests { + use super::*; + #[test] + fn test_constants() { + assert_eq!(u16::from(Request::DEVICE_GET_STATUS), 0x0080); + assert_eq!(u16::from(Request::DEVICE_CLEAR_FEATURE), 0x0100); + assert_eq!(u16::from(Request::DEVICE_SET_FEATURE), 0x0300); + assert_eq!(u16::from(Request::DEVICE_SET_ADDRESS), 0x0500); + assert_eq!(u16::from(Request::DEVICE_GET_DESCRIPTOR), 0x0680); + assert_eq!(u16::from(Request::DEVICE_SET_DESCRIPTOR), 0x0700); + assert_eq!(u16::from(Request::DEVICE_GET_CONFIGURATION), 0x0880); + assert_eq!(u16::from(Request::DEVICE_SET_CONFIGURATION), 0x0900); + assert_eq!(u16::from(Request::INTERFACE_GET_STATUS), 0x0081); + assert_eq!(u16::from(Request::INTERFACE_CLEAR_FEATURE), 0x0101); + assert_eq!(u16::from(Request::INTERFACE_SET_FEATURE), 0x0301); + assert_eq!(u16::from(Request::INTERFACE_GET_INTERFACE), 0x0a81); + assert_eq!(u16::from(Request::INTERFACE_SET_INTERFACE), 0x0b01); + assert_eq!(u16::from(Request::ENDPOINT_GET_STATUS), 0x0082); + assert_eq!(u16::from(Request::ENDPOINT_CLEAR_FEATURE), 0x0102); + assert_eq!(u16::from(Request::ENDPOINT_SET_FEATURE), 0x0302); + assert_eq!(u16::from(Request::ENDPOINT_SYNCH_FRAME), 0x1282); + } +} + +#[derive(Clone, Copy, Eq, PartialEq, uDebug)] +pub struct DescriptorInfo { + pub index: u8, + pub ty: DescriptorType, + pub lang: u16, +} +impl From<&SetupPacket> for DescriptorInfo { + fn from(pkt: &SetupPacket) -> Self { + DescriptorInfo { + index: u8::try_from(pkt.value() & 0xff).unwrap(), + ty: DescriptorType::from(u8::try_from((pkt.value() >> 8) & 0xff).unwrap()), + lang: pkt.index(), + } + } +} +#[derive(Clone, Copy)] +#[repr(C)] +pub struct SetupPacket { + buf: [u32; 2], +} +impl SetupPacket { + pub fn new(buf: [u32; 2]) -> SetupPacket { + SetupPacket { buf } + } + pub fn request(&self) -> Request { + Request(u16::try_from(self.buf[0] & 0xffff).unwrap()) + } + pub fn value(&self) -> u16 { + u16::try_from((self.buf[0] >> 16) & 0xffff).unwrap() + } + #[allow(clippy::identity_op)] + pub fn index(&self) -> u16 { + u16::try_from((self.buf[1] >> 0) & 0xffff).unwrap() + } + pub fn length(&self) -> u16 { + u16::try_from((self.buf[1] >> 16) & 0xffff).unwrap() + } +} +impl ufmt::uDebug for SetupPacket { + fn fmt( + &self, + f: &mut ufmt::Formatter<'_, W>, + ) -> Result<(), W::Error> { + f.debug_struct("usb::SetupPacket")? + .field("request", &self.request())? + .field("value", &self.value())? + .field("index", &self.index())? + .field("length", &self.length())? + .finish() + } +} + +#[derive(Clone, Copy, Eq, PartialEq, uDebug)] +pub enum Direction { + HostToDevice = 0, + DeviceToHost = 1, +} +impl From for u32 { + fn from(val: Direction) -> u32 { + val as u32 + } +} +impl TryFrom for Direction { + type Error = (); + #[inline(always)] + fn try_from(val: u32) -> Result { + match val { + 0 => Ok(Self::HostToDevice), + 1 => Ok(Self::DeviceToHost), + _ => Err(()), + } + } +} +#[derive(Clone, Copy, Eq, PartialEq, uDebug)] +pub enum RequestType { + Standard = 0, + Class = 1, + Vendor = 2, + Reserved = 3, +} +impl TryFrom for RequestType { + type Error = (); + #[inline(always)] + fn try_from(val: u32) -> Result { + match val { + 0 => Ok(Self::Standard), + 1 => Ok(Self::Class), + 2 => Ok(Self::Vendor), + 3 => Ok(Self::Reserved), + _ => Err(()), + } + } +} +impl From for u32 { + fn from(val: RequestType) -> Self { + val as u32 + } +} +#[derive(Clone, Copy, Eq, PartialEq, uDebug)] +pub enum Recipient { + Device = 0, + Interface = 1, + Endpoint = 2, + Other = 3, + Reserved4 = 4, + Reserved5 = 5, + Reserved6 = 6, + Reserved7 = 7, + Reserved8 = 8, + Reserved9 = 9, + Reserved10 = 10, + Reserved11 = 11, + Reserved12 = 12, + Reserved13 = 13, + Reserved14 = 14, + Reserved15 = 15, + Reserved16 = 16, + Reserved17 = 17, + Reserved18 = 18, + Reserved19 = 19, + Reserved20 = 20, + Reserved21 = 21, + Reserved22 = 22, + Reserved23 = 23, + Reserved24 = 24, + Reserved25 = 25, + Reserved26 = 26, + Reserved27 = 27, + Reserved28 = 28, + Reserved29 = 29, + Reserved30 = 30, + Reserved31 = 31, +} +impl TryFrom for Recipient { + type Error = (); + #[inline(always)] + fn try_from(val: u32) -> Result { + // TODO: Evaluate whether the optimizer is smart enough for this, and use + // transmute if it's not. + match val { + 0 => Ok(Self::Device), + 1 => Ok(Self::Interface), + 2 => Ok(Self::Endpoint), + 3 => Ok(Self::Other), + 4 => Ok(Self::Reserved4), + 5 => Ok(Self::Reserved5), + 6 => Ok(Self::Reserved6), + 7 => Ok(Self::Reserved7), + 8 => Ok(Self::Reserved8), + 9 => Ok(Self::Reserved9), + 10 => Ok(Self::Reserved10), + 11 => Ok(Self::Reserved11), + 12 => Ok(Self::Reserved12), + 13 => Ok(Self::Reserved13), + 14 => Ok(Self::Reserved14), + 15 => Ok(Self::Reserved15), + 16 => Ok(Self::Reserved16), + 17 => Ok(Self::Reserved17), + 18 => Ok(Self::Reserved18), + 19 => Ok(Self::Reserved19), + 20 => Ok(Self::Reserved20), + 21 => Ok(Self::Reserved21), + 22 => Ok(Self::Reserved22), + 23 => Ok(Self::Reserved23), + 24 => Ok(Self::Reserved24), + 25 => Ok(Self::Reserved25), + 26 => Ok(Self::Reserved26), + 27 => Ok(Self::Reserved27), + 28 => Ok(Self::Reserved28), + 29 => Ok(Self::Reserved29), + 30 => Ok(Self::Reserved30), + 31 => Ok(Self::Reserved31), + _ => Err(()), + } + } +} +impl From for u32 { + fn from(val: Recipient) -> Self { + val as u32 + } +} +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct DescriptorType(u8); +impl DescriptorType { + pub const DEVICE: Self = Self(1); + pub const CONFIGURATION: Self = Self(2); + pub const STRING: Self = Self(3); + pub const INTERFACE: Self = Self(4); + pub const ENDPOINT: Self = Self(5); + pub const DEVICE_QUALIFIER: Self = Self(6); +} +impl From for DescriptorType { + fn from(val: u8) -> Self { + DescriptorType(val) + } +} +impl From for u8 { + fn from(val: DescriptorType) -> Self { + val.0 + } +} +impl From for u32 { + fn from(val: DescriptorType) -> Self { + u32::from(val.0) + } +} +impl ufmt::uDebug for DescriptorType { + fn fmt( + &self, + f: &mut ufmt::Formatter<'_, W>, + ) -> Result<(), W::Error> { + match *self { + Self::DEVICE => f.write_str("DEVICE"), + Self::CONFIGURATION => f.write_str("CONFIGURATION"), + Self::STRING => f.write_str("STRING"), + Self::INTERFACE => f.write_str("INTERFACE"), + Self::ENDPOINT => f.write_str("ENDPOINT"), + Self::DEVICE_QUALIFIER => f.write_str("DEVICE_QUALIFIER"), + other => ufmt::uwrite!(f, "{}", other.0), + } + } +} diff --git a/protocol/usb/stack/BUILD.bazel b/protocol/usb/stack/BUILD.bazel new file mode 100644 index 00000000..275a7805 --- /dev/null +++ b/protocol/usb/stack/BUILD.bazel @@ -0,0 +1,26 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "stack", + srcs = [ + "lib.rs", + ], + crate_name = "usb_stack", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/usb:hal_usb", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "stack_test", + crate = ":stack", + rustc_flags = [ + "-C", + "debug-assertions", + ], +) diff --git a/protocol/usb/stack/lib.rs b/protocol/usb/stack/lib.rs new file mode 100644 index 00000000..6b23a19b --- /dev/null +++ b/protocol/usb/stack/lib.rs @@ -0,0 +1,441 @@ +#![no_std] + +use aligned::Aligned; +use aligned::A4; +use core::mem::size_of; +use hal_usb::driver::UsbDriver; +use hal_usb::driver::UsbEvent; +use hal_usb::driver::UsbPacket; +use hal_usb::DescriptorInfo; +use hal_usb::DescriptorType; +use hal_usb::Request; +use hal_usb::SetupPacket; +use hal_usb::StringDescriptorRef; +use hal_usb::StringHandle; +use zerocopy::IntoBytes; + +use pw_status::Error; + +pub trait DescriptorSource { + const DEVICE_DESC_BYTES: &'static Aligned; + const CONFIG_DESC_BYTES: &'static Aligned; + const STRING_DESC_0_BYTES: &'static Aligned; + + fn get_string(&self, handle: StringHandle, lang: u16) -> Option>; +} + +pub const EMPTY: &Aligned = &Aligned([]); + +pub struct SimpleEp0 { + new_address: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum UsbActionRun { + NoOp, + HasMoreData, + Done, +} + +pub enum UsbAction<'a> { + None, + TransferIn { + endpoint: u8, + data: &'a Aligned, + /// If zlp=true and `data.len()` is a multiple of MAX_PACKET_SIZE, + /// send a zero-length packet after sending all the data. + zlp: bool, + }, + StallInAndOut { + endpoint: u8, + }, + SetAddress { + new_address: u8, + }, +} +impl<'a> UsbAction<'a> { + #[inline(always)] + #[track_caller] + pub fn control_transfer_in_or_stall( + endpoint: u8, + pkt: &SetupPacket, + data: &'a Aligned, + ) -> Self { + if data.len() > pkt.length().into() { + Self::StallInAndOut { endpoint } + } else { + Self::TransferIn { + endpoint, + data, + // Per USB Specs 5.5.3, we need to send ZLP for control transfers + // if the response is less than requested. + zlp: data.is_empty() || data.len() < pkt.length().into(), + } + } + } + pub fn merge(&mut self, new_action: UsbAction<'a>) { + match new_action { + UsbAction::None => {} + _ => *self = new_action, + } + } + + pub fn run(&mut self, driver: &mut TDriver) -> UsbActionRun { + match self { + Self::None => return UsbActionRun::NoOp, + Self::TransferIn { + endpoint, + data, + zlp, + } => { + let bytes_transferred = driver.transfer_in(*endpoint, data, *zlp); + // Note: bytes_transferred is guaranteed to be a multiple of + // UsbDriver::MAX_PACKET_SIZE, which is guaranteed to be a + // multiple of 4. + if bytes_transferred < data.len() && (bytes_transferred & 3) == 0 { + // We're not done yet... + *data = &data[bytes_transferred..]; + return UsbActionRun::HasMoreData; + } + } + Self::SetAddress { new_address } => driver.set_address(*new_address), + Self::StallInAndOut { endpoint } => { + driver.stall_in(*endpoint, true); + driver.stall_out(*endpoint, true); + } + } + *self = UsbAction::None; + UsbActionRun::Done + } +} + +impl SimpleEp0 { + pub fn new() -> Self { + Self { new_address: None } + } + /// A helper function to process a driver UsbEvent. + /// + /// This function returns the action that should be performed on the driver + /// (we can't take the driver as a parameter because the UsbPacket in the + /// event may capture the driver's lifetime). + pub fn handle_event<'a>( + &mut self, + ev: UsbEvent, + descriptor_source: &'a impl DescriptorSource, + ) -> UsbAction<'a> { + match ev { + UsbEvent::SetupPacket { endpoint, pkt } => { + if endpoint == 0 { + return self.handle_setup(pkt, descriptor_source); + } + } + UsbEvent::PacketSent { endpoint } => { + if endpoint == 0 { + return self.handle_packet_sent(); + } + } + _ => {} + } + UsbAction::None + } + + /// Process a SETUP transfer, and return the action that should be performed. + fn handle_setup<'a, TDescriptorSource: DescriptorSource>( + &mut self, + setup_pkt: SetupPacket, + descriptor_source: &'a TDescriptorSource, + ) -> UsbAction<'a> { + match setup_pkt.request() { + Request::DEVICE_GET_DESCRIPTOR => { + let descriptor = DescriptorInfo::from(&setup_pkt); + #[rustfmt::skip] + let mut response: Option<&Aligned> = match descriptor { + DescriptorInfo { ty: DescriptorType::DEVICE, index: 0, .. } => { + Some(TDescriptorSource::DEVICE_DESC_BYTES) + } + DescriptorInfo { ty: DescriptorType::CONFIGURATION, index: 0, .. } => { + Some(TDescriptorSource::CONFIG_DESC_BYTES) + } + DescriptorInfo { ty: DescriptorType::STRING, index: 0, .. } => { + Some(TDescriptorSource::STRING_DESC_0_BYTES) + } + DescriptorInfo { ty: DescriptorType::STRING, index, .. } => { + descriptor_source + .get_string(StringHandle(index), setup_pkt.index()) + .map(|desc| desc.as_bytes()) + } + _ => None, + }; + if let Some(response) = &mut response { + if response.len() > setup_pkt.length().into() { + *response = &(*response)[..setup_pkt.length().into()]; + } + UsbAction::control_transfer_in_or_stall(0, &setup_pkt, response) + } else { + UsbAction::StallInAndOut { endpoint: 0 } + } + } + Request::DEVICE_SET_ADDRESS => { + self.new_address = Some(setup_pkt.value() as u8); + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + } + } + Request::DEVICE_SET_CONFIGURATION => { + if setup_pkt.value() == 1 { + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + } + } else { + UsbAction::StallInAndOut { endpoint: 0 } + } + } + _ => UsbAction::StallInAndOut { endpoint: 0 }, + } + } + fn handle_packet_sent(&mut self) -> UsbAction<'static> { + if let Some(new_address) = self.new_address.take() { + // Now that the transfer is complete it's safe to change the address.. + return UsbAction::SetAddress { new_address }; + } + UsbAction::None + } +} + +impl Default for SimpleEp0 { + fn default() -> Self { + Self::new() + } +} + +/// A helper struct to handle multi-packet USB transfers. +/// +/// It accumulates incoming USB packets into an internal buffer until a short +/// packet or a zero-length packet (ZLP) is received, indicating the end of a transfer. +/// +/// `N` is the number of **words** (`u32`s) in the internal buffer and NOT bytes. +#[derive(Debug, PartialEq, Eq)] +pub struct Transfer { + buffer: [u32; N], + word_offset: usize, +} + +impl Transfer { + // TODO: ckungler - This could be a const generic if we need to support + // other packet sizes. + pub const MAX_PACKET_SIZE: usize = 64; + + pub fn new() -> Self { + Self { + buffer: [0; N], + word_offset: 0, + } + } + + /// Splices a USB packet into the buffer. + /// + /// Returns `Ok(Some(slice))` if the transfer is complete, `Ok(None)` otherwise. + pub fn splice(&mut self, packet: impl UsbPacket) -> Result>, Error> { + const { + assert!(Self::MAX_PACKET_SIZE % size_of::() == 0); + } + let packet_len = packet.len(); + let dest = { + let start = self.word_offset; + let end = start + packet_len.div_ceil(size_of::()); + self.buffer.get_mut(start..end).ok_or(Error::OutOfRange)? + }; + packet.copy_to(dest); + if packet_len < Self::MAX_PACKET_SIZE { + let result = &self + .buffer + .as_bytes() + .get(..self.word_offset * size_of::() + packet_len) + .ok_or(Error::OutOfRange)?; + self.word_offset = 0; + // This is safe because `self.buffer` is `[u32]` which has alignment of 4. + Ok(Some(unsafe { + core::mem::transmute::<&[u8], &Aligned>(result) + })) + } else { + self.word_offset += Self::MAX_PACKET_SIZE / size_of::(); + Ok(None) + } + } +} + +impl Default for Transfer { + fn default() -> Self { + Self::new() + } +} + +pub mod testing { + use aligned::Aligned; + use aligned::A4; + use hal_usb::driver::UsbPacket; + use zerocopy::IntoBytes; + + #[derive(Debug)] + pub struct FakeUsbPacket<'a> { + pub data: &'a [u8], + pub ep: usize, + } + + impl UsbPacket for FakeUsbPacket<'_> { + fn endpoint_index(&self) -> usize { + self.ep + } + + fn len(&self) -> usize { + self.data.len() + } + + fn copy_to_uninit(self, _dest: &mut [core::mem::MaybeUninit]) -> &Aligned { + unimplemented!() + } + + fn copy_to(self, dest: &mut [u32]) -> &Aligned { + let dest_bytes = dest.as_mut_bytes(); + let copy_len = self.data.len().min(dest_bytes.len()); + dest_bytes[..copy_len].copy_from_slice(&self.data[..copy_len]); + // This is safe because `dest` is a `&mut [u32]`, which is guaranteed to be 4-byte + // aligned. `dest_bytes` is a byte slice view of the same memory, so it's also + // 4-byte aligned. The subslice `&dest_bytes[..copy_len]` maintains this alignment. + unsafe { core::mem::transmute::<&[u8], &Aligned>(&dest_bytes[..copy_len]) } + } + } +} + +#[cfg(test)] +mod splice_tests { + use super::testing::FakeUsbPacket; + use super::*; + + const MAX_PACKET_SIZE: usize = Transfer::<0>::MAX_PACKET_SIZE; + + #[test] + fn test_splice_single_short_packet() { + let packet_data = [1, 2, 3, 4]; + let packet = FakeUsbPacket { + data: &packet_data, + ep: 0, + }; + + let mut transfer = Transfer::<32>::new(); + let result = transfer.splice(packet).unwrap(); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_ref(), &packet_data[..]); + } + + #[test] + fn test_splice_single_full_packet_then_zlp() { + let packet_data = [42; MAX_PACKET_SIZE]; + let packet = FakeUsbPacket { + data: &packet_data, + ep: 0, + }; + + let mut transfer = Transfer::<32>::new(); + let result = transfer.splice(packet).unwrap(); + + assert!(result.is_none()); + + let zlp = FakeUsbPacket { data: &[], ep: 0 }; + let result = transfer.splice(zlp).unwrap(); + assert!(result.is_some()); + assert_eq!(result.unwrap().as_ref(), &packet_data[..]); + } + + #[test] + fn test_splice_multiple_packets() { + let packet1_data = [1; MAX_PACKET_SIZE]; + let packet2_data = [2; MAX_PACKET_SIZE]; + let packet3_data = [3; 32]; + + // Packet 1 + let packet1 = FakeUsbPacket { + data: &packet1_data, + ep: 0, + }; + let mut transfer = Transfer::<64>::new(); + let result = transfer.splice(packet1).unwrap(); + assert!(result.is_none()); + + // Packet 2 + let packet2 = FakeUsbPacket { + data: &packet2_data, + ep: 0, + }; + let result = transfer.splice(packet2).unwrap(); + assert!(result.is_none()); + + // Packet 3 (short packet) + let packet3 = FakeUsbPacket { + data: &packet3_data, + ep: 0, + }; + let result = transfer.splice(packet3).unwrap(); + assert!(result.is_some()); + + let mut expected_data = [0u8; 2 * MAX_PACKET_SIZE + 32]; + expected_data[..MAX_PACKET_SIZE].copy_from_slice(&packet1_data); + expected_data[MAX_PACKET_SIZE..2 * MAX_PACKET_SIZE].copy_from_slice(&packet2_data); + expected_data[2 * MAX_PACKET_SIZE..].copy_from_slice(&packet3_data); + + assert_eq!(result.unwrap().as_ref(), &expected_data[..]); + } + + #[test] + fn test_splice_buffer_overflow() { + let packet_data = [1; 1]; + let packet = FakeUsbPacket { + data: &packet_data, + ep: 0, + }; + + let mut transfer = Transfer::<16>::new(); + transfer + .splice(FakeUsbPacket { + data: &[0; MAX_PACKET_SIZE], + ep: 0, + }) + .unwrap(); + let result = transfer.splice(packet); + assert_eq!(result.err(), Some(Error::OutOfRange)); + } + + #[test] + fn test_full_capacity_with_full_packets_then_partial_packet() { + const PARTIAL_SIZE: usize = 16; + const FULL1_DATA: &[u8] = &[0xaa; MAX_PACKET_SIZE]; + const FULL2_DATA: &[u8] = &[0xbb; MAX_PACKET_SIZE]; + const PARTIAL_DATA: &[u8] = &[0xcc; PARTIAL_SIZE]; + const RECEIVE_BUFFER_WORDS: usize = + (FULL1_DATA.len() + FULL2_DATA.len() + PARTIAL_DATA.len()) / size_of::(); + let full1 = FakeUsbPacket { + data: FULL1_DATA, + ep: 0, + }; + let full2 = FakeUsbPacket { + data: FULL2_DATA, + ep: 0, + }; + let partial = FakeUsbPacket { + data: PARTIAL_DATA, + ep: 0, + }; + let mut transfer = Transfer::::new(); + assert!(transfer.splice(full1).unwrap().is_none()); + assert!(transfer.splice(full2).unwrap().is_none()); + let buffer = transfer.splice(partial).unwrap().unwrap().as_ref(); + assert_eq!(&buffer[..MAX_PACKET_SIZE], FULL1_DATA); + assert_eq!(&buffer[MAX_PACKET_SIZE..2 * MAX_PACKET_SIZE], FULL2_DATA); + assert_eq!(&buffer[2 * MAX_PACKET_SIZE..], PARTIAL_DATA); + } +} From d49b42a3d388a4cefdc1a2e4e1dbc594f8ada026 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 18 Mar 2026 21:36:41 -0700 Subject: [PATCH 05/46] drivers: add the earlgrey usb driver Signed-off-by: Chris Frantz --- target/earlgrey/drivers/BUILD.bazel | 21 + target/earlgrey/drivers/usb_driver.rs | 1092 +++++++++++++++++++++++++ 2 files changed, 1113 insertions(+) create mode 100644 target/earlgrey/drivers/BUILD.bazel create mode 100644 target/earlgrey/drivers/usb_driver.rs diff --git a/target/earlgrey/drivers/BUILD.bazel b/target/earlgrey/drivers/BUILD.bazel new file mode 100644 index 00000000..558425ee --- /dev/null +++ b/target/earlgrey/drivers/BUILD.bazel @@ -0,0 +1,21 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "usb_driver", + srcs = ["usb_driver.rs"], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/usb:hal_usb", + "//target/earlgrey/registers:usbdev", + "//util/console", + "//util/regcpy", + "@rust_crates//:aligned", + "@rust_crates//:ufmt", + "@rust_crates//:zerocopy", + "@ureg", + ], +) diff --git a/target/earlgrey/drivers/usb_driver.rs b/target/earlgrey/drivers/usb_driver.rs new file mode 100644 index 00000000..dcb3b0c3 --- /dev/null +++ b/target/earlgrey/drivers/usb_driver.rs @@ -0,0 +1,1092 @@ +#![no_std] + +use aligned::A4; +use aligned::Aligned; +use core::cmp::min; +use util_regcpy::copy_to_reg_array; +use console::traceln; +use hal_usb::SetupPacket; +use hal_usb::driver::UsbDriver; +use hal_usb::driver::UsbEvent; +use hal_usb::driver::UsbPacket; +use zerocopy::IntoBytes; + +const MAX_PACKET_SIZE: usize = 64; +const BUFFER_SLOT_SIZE_WORDS: usize = MAX_PACKET_SIZE / 4; +const BUFFER_SLOT_COUNT: usize = 32; + +const EMPTY_A4: &Aligned = &Aligned([]); + +use buf_pool::BufId; +use buf_pool::BufPool; +use buf_pool::BuffPoolAllocator; +use core::cmp; +use ureg::RealMmio; + +use crate::transmit_queue::TransmitQueues; + +pub struct PacketHandle { + // A reference to 16 words of packet data in the peripheral MMIO memory. + data: ureg::Array<16, ureg::RegRef, TMmio>>, + // The length of the packet in bytes + packet_len: u16, + ep: u8, +} + +impl UsbPacket for PacketHandle { + fn endpoint_index(&self) -> usize { + self.ep.into() + } + fn len(&self) -> usize { + self.packet_len.into() + } + + fn copy_to_uninit(self, dest: &mut [core::mem::MaybeUninit]) -> &Aligned { + #![allow(clippy::needless_range_loop)] + + // TODO: Are we sure we want to silently truncate if dest isn't big enough? + let word_len = min(min(dest.len(), MAX_PACKET_SIZE / 4), self.len().div_ceil(4)); + for i in 0..word_len { + dest[i].write(self.data.at(i).read()); + } + //let result = unsafe { mutask_subtle::slice_assume_init(&dest[..word_len]) }; + + // This is feature(maybe_uninit_slice). + let result = &dest[..word_len]; + let result = unsafe { &*(result as *const [core::mem::MaybeUninit] as *const [u32]) }; + + // TODO: add a Aligned::try_from() function to the aligned crate and use it here with unwrap. + unsafe { + core::mem::transmute::<&[u8], &Aligned>( + &result.as_bytes()[..min(self.len(), word_len * 4)], + ) + } + } + + fn copy_to(self, dest: &mut [u32]) -> &Aligned { + #![allow(clippy::needless_range_loop)] + + // TODO: Are we sure we want to silently truncate if dest isn't big enough? + let word_len = min(min(dest.len(), MAX_PACKET_SIZE / 4), self.len().div_ceil(4)); + for i in 0..word_len { + dest[i] = self.data.at(i).read(); + } + // TODO: add a Aligned::try_from() function to the aligned crate and use it here with unwrap. + unsafe { + core::mem::transmute::<&[u8], &Aligned>( + &dest.as_bytes()[..min(self.len(), dest.as_bytes().len())], + ) + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct NextInPacket { + buf_id: u8, + len: u8, +} +impl NextInPacket { + const NONE: Self = Self { + buf_id: 0xff, + len: 0xff, + }; +} + +#[derive(Clone, Copy)] +pub struct EpIn { + pub num: u8, + pub buf_pool_size: u32, +} + +#[derive(Clone, Copy)] +pub struct EpOut { + pub num: u8, + /// If true, hardware will NAK OUT transfers on this endpoint after the first + /// until software re-enables by setting `rxenable_out` + pub set_nak: bool, +} + +const NB_EP: usize = 12; + +pub struct Usb { + mmio: usbdev::Usbdev, + + // buffer pool for SETUP transfers from host, technically common, but only used for EP0. + buf_pool_setup: BufPool, + // buffer pool for OUT transfers from host, common for all EPs. + buf_pool_out: BufPool, + + // buffer pools for IN transfers. + buf_pools_in: [BufPool; NB_EP], + + transmit_queues: TransmitQueues, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct UsbConfig { + buf_pool_setup: BufPool, + buf_pool_out: BufPool, + buf_pools_in: [BufPool; NB_EP], + in_mask: u32, + out_mask: u32, + set_nak_mask: u32, +} +impl UsbConfig { + /// Construct a USB config. This should typically be done within a const + /// block so any errors become compile-time panics. + /// + /// # Panic + /// + /// This function will panic if the supplied endpoints are invalid. + #[inline(always)] + pub const fn new(eps_in: &[EpIn], eps_out: &[EpOut]) -> Self { + let mut buf_pool_allocator = BuffPoolAllocator::new(); + let buf_pool_setup = buf_pool_allocator.new_bufpool(4).unwrap(); + let buf_pool_out = buf_pool_allocator.new_bufpool(12).unwrap(); + + let mut buf_pools_in = [BufPool::EMPTY; NB_EP]; + let mut i = 0; + while i < eps_in.len() { + let ep = &eps_in[i]; + if ep.num == 0 || ep.num as usize > NB_EP { + panic!("Invalid endpoint number"); + } + let Some(new_pool) = buf_pool_allocator.new_bufpool(ep.buf_pool_size) else { + panic!("Bufpool allocation overflow"); + }; + buf_pools_in[ep.num as usize] = new_pool; + i += 1; + } + let Some(new_pool) = buf_pool_allocator.remainder_bufpool() else { + panic!("Bufpool allocation overflow"); + }; + buf_pools_in[0] = new_pool; + + let in_mask: u32 = { + let mut v = 1; // always enable EP0 + let mut i = 0; + while i < eps_in.len() { + v |= 1 << eps_in[i].num; + i += 1; + } + v + }; + + let out_mask: u32 = { + let mut v = 1; // always enable EP0 + let mut i = 0; + while i < eps_out.len() { + v |= 1 << eps_out[i].num; + i += 1; + } + v + }; + + let set_nak_mask: u32 = { + let mut v = 0u32; + let mut i = 0; + while i < eps_out.len() { + if eps_out[i].set_nak { + v |= 1 << eps_out[i].num; + } + i += 1; + } + + // Writes to `rxenable_out` are not atomic - therefore we must + // guarantee that `set_nak_out` is only set for up to a single + // endpoint (github.com/lowRISC/opentitan/issues/27434) + assert!( + v.count_ones() <= 1, + "set_nak_out can be enabled on at most one endpoint" + ); + v + }; + + Self { + buf_pool_setup, + buf_pool_out, + buf_pools_in, + in_mask, + out_mask, + set_nak_mask, + } + } +} + +impl Usb { + pub fn new(mmio: usbdev::Usbdev, config: UsbConfig) -> Self { + let mut result = Self { + mmio, + buf_pool_setup: config.buf_pool_setup, + buf_pool_out: config.buf_pool_out, + buf_pools_in: config.buf_pools_in, + transmit_queues: TransmitQueues::new(), + }; + result.init(&config); + result + } + fn init(&mut self, config: &UsbConfig) { + self.fill_setup_buffer_fifo(); + self.fill_out_buffer_fifo(); + + let regs = self.mmio.regs_mut(); + + regs.ep_in_enable0().write(|_| config.in_mask.into()); + regs.ep_out_enable0().write(|_| config.out_mask.into()); + regs.rxenable_out0().write(|_| config.out_mask.into()); + regs.set_nak_out0().write(|_| config.set_nak_mask.into()); + + regs.rxenable_setup0().write(|w| w.setup0(true)); + regs.intr_enable().write(|w| { + w.pkt_received(true) + .pkt_sent(true) + .disconnected(true) + .host_lost(true) + .link_reset(true) + .link_suspend(true) + .link_resume(true) + .av_out_empty(true) + .rx_full(true) + .av_overflow(true) + .link_in_err(false) + .rx_crc_err(false) + .rx_pid_err(false) + .rx_bitstuff_err(false) + .frame(false) + .powered(true) + .link_out_err(false) + .av_setup_empty(true) + }); + regs.usbctrl().modify(|w| w.enable(true)); + + let stat = regs.usbstat().read(); + traceln!( + "Usb out_depth={} setup_depth={}", + stat.av_out_depth(), + stat.av_setup_depth() + ); + } + + fn reset_in(&mut self) { + let regs = self.mmio.regs_mut(); + for (i, pool) in &mut self.buf_pools_in.iter_mut().enumerate() { + pool.reset(); + let configin = regs.configin().at(i); + if configin.read().pend() { + // link reset will cancel any pending transactions. Since we're + // resetting the pool/queue state there's nothing else to do but + // clear the notification. + configin.write(|w| w.pend_clear()); + } + } + self.transmit_queues.reset(); + } + + fn fill_setup_buffer_fifo(&mut self) { + // Setup buffers for incoming SETUP packets from host. + while !self.mmio.regs().usbstat().read().av_setup_full() { + let Some(buf_id) = self.buf_pool_setup.take() else { + break; + }; + self.mmio + .regs_mut() + .avsetupbuffer() + .write(|w| w.buffer(buf_id.into())); + } + } + fn fill_out_buffer_fifo(&mut self) { + // Setup buffers for incoming OUT packets from host. + while !self.mmio.regs().usbstat().read().av_out_full() { + let Some(buf_id) = self.buf_pool_out.take() else { + break; + }; + self.mmio + .regs_mut() + .avoutbuffer() + .write(|w| w.buffer(buf_id.into())); + } + } + + /// Flush packets bufferred to transmit on `endpoint` starting with `buf_id`. + fn clear_ep_tx_queue(&mut self, endpoint: u32, mut buf_id: BufId) { + let buf_pool_in = &mut self.buf_pools_in[usize::try_from(endpoint).unwrap()]; + buf_pool_in.put(buf_id); + while let Some(pkt) = self.transmit_queues.deque_next_packet(endpoint, buf_id) { + buf_id = BufId(pkt.buf_id.into()); + buf_pool_in.put(buf_id); + } + } + + /// Resume accepting OUT transfers on this endpoint. + /// + /// This should be called after processing an OUT transfer on a given endpoint + /// that was configured with `EpOut { set_nak: true }`. The `set_nak` option causes + /// the hardware to automatically NAK subsequent OUT transactions until this function + /// is called to re-enable reception. + pub fn set_rxenable(&mut self, ep_num: u8) { + self.mmio + .regs_mut() + .rxenable_out0() + .modify(|w| bit_setval(u32::from(w), ep_num.into(), true).into()); + } +} + +#[inline(always)] +fn bit_setval(bits: u32, index: usize, value: bool) -> u32 { + let mask = 1 << index; + if value { bits | mask } else { bits & !mask } +} + +impl UsbDriver for Usb { + const MAX_PACKET_SIZE: usize = 64; + type Packet<'a> = PacketHandle>; + + #[inline(always)] + fn stall_in(&mut self, endpoint_idx: u8, stalled: bool) { + self.mmio + .regs_mut() + .in_stall0() + .modify(|w| bit_setval(u32::from(w), endpoint_idx.into(), stalled).into()); + } + #[inline(always)] + fn stall_out(&mut self, endpoint_idx: u8, stalled: bool) { + self.mmio + .regs_mut() + .out_stall0() + .modify(|w| bit_setval(u32::from(w), endpoint_idx.into(), stalled).into()); + } + + /// Store data in peripheral buffer that will be transferred when the host requests it. + #[inline(never)] + fn transfer_in(&mut self, endpoint: u8, mut data: &Aligned, zlp: bool) -> usize { + let mut bytes_queued = 0; + let zlp = zlp && (data.len() % MAX_PACKET_SIZE) == 0; + loop { + if data.is_empty() && !zlp { + break; + } + let regs = self.mmio.regs_mut(); + let Some(configin_reg) = regs.configin().get(endpoint.into()) else { + // Fault? + return 0; + }; + + let pkt = &data[..cmp::min(MAX_PACKET_SIZE, data.len())]; + if pkt.len() == MAX_PACKET_SIZE { + data = &data[pkt.len()..]; + } else { + data = EMPTY_A4; + } + + let buf_pool = self.buf_pools_in.get_mut(usize::from(endpoint)).unwrap(); + + // Check to see if we have enough buffers to send both + // the last data packet and a ZLP if necessary. If not, leave last + // data packet unsent so caller knows to retry transfer + if zlp && pkt.len() == MAX_PACKET_SIZE && buf_pool.len() < 2 { + traceln!("Couldn't find buf in pool for last IN + ZLP"); + break; + } + + let Some(buf_id) = buf_pool.take() else { + traceln!("Couldn't find buf in pool for next IN"); + break; + }; + + let Some(buffer) = regs + .buffer() + .get_sub_array::(buf_id.offset()) + else { + // Shouldn't fail to get buffer offset + unreachable!(); + }; + copy_to_reg_array(&buffer, pkt); + + match self.transmit_queues.queue( + endpoint.into(), + NextInPacket { + buf_id: u32::from(buf_id) as u8, + len: pkt.len() as u8, + }, + ) { + TransmitQueueAction::None => {} + TransmitQueueAction::SendNow => { + if configin_reg.read().rdy() { + traceln!("WARN: Packet already queued in hardware"); + } + configin_reg.write(|w| { + w.buffer(buf_id.into()) + .rdy(true) + .size(u32::try_from(pkt.len()).unwrap()) + }); + } + } + bytes_queued += pkt.len(); + + if pkt.is_empty() { + break; + } + } + bytes_queued + } + fn set_address(&mut self, address: u8) { + self.mmio + .regs_mut() + .usbctrl() + .modify(|w| w.device_address(address.into())); + } + + #[inline(never)] + fn poll(&mut self) -> Option>>> { + let intr = self.mmio.regs_mut().intr_state().read(); + + // TODO: use count_leading_zeros() to iterate over the pending interrupts + if intr.pkt_received() { + let fifo_entry = self.mmio.regs_mut().rxfifo().read(); + + if fifo_entry.setup() { + self.fill_setup_buffer_fifo(); + + if let Some(configin_reg) = self + .mmio + .regs_mut() + .configin() + .get(fifo_entry.ep() as usize) + { + let configin = configin_reg.read(); + if configin.pend() { + // Previous transmission was cancelled by an incoming setup packet + configin_reg.write(|w| w.pend_clear()); + self.clear_ep_tx_queue(fifo_entry.ep(), BufId(configin.buffer())); + } + } + } else { + self.fill_out_buffer_fifo(); + } + + let offset = usize::try_from(fifo_entry.buffer()).unwrap() * BUFFER_SLOT_SIZE_WORDS; + let Some(pkt_buffer) = self + .mmio + .regs() + .into_buffer() + .get_sub_array::(offset) + else { + return Some(UsbEvent::ErrorUnexpectedBufId); + }; + + if fifo_entry.setup() { + let buf_id = BufId(fifo_entry.buffer()); + + // Return the buffer back to the pool, but don't call + // self.fill_setup_buffer_fifo() yet, as the caller to poll() + // may look at this data from the returned event, and we don't want + // the peripheral to change it while they're reading the data + // (because the returned Event is exclusively holding self, it won't be possible + // to call fill_setup_buffer_fifo() until after they lose the event). + self.buf_pool_setup.put(buf_id); + + let ep = u8::try_from(fifo_entry.ep()).unwrap(); + let pkt_handle = PacketHandle { + data: pkt_buffer, + // These unwraps will optimize out + ep, + packet_len: u16::try_from(fifo_entry.size()).unwrap(), + }; + let mut pkt_words = [0_u32; 2]; + pkt_handle.copy_to(&mut pkt_words); + return Some(UsbEvent::SetupPacket { + endpoint: ep, + pkt: SetupPacket::new(pkt_words), + }); + } else { + let buf_id = BufId(fifo_entry.buffer()); + self.buf_pool_out.put(buf_id); + return Some(UsbEvent::DataOutPacket(PacketHandle { + data: pkt_buffer, + // These unwraps will optimize out + ep: u8::try_from(fifo_entry.ep()).unwrap(), + packet_len: u16::try_from(fifo_entry.size()).unwrap(), + })); + } + } + if intr.pkt_sent() { + let regs = self.mmio.regs_mut(); + loop { + let endpoint_bits: u32 = regs.in_sent0().read().into(); + if endpoint_bits == 0 { + break; + } + let endpoint_id = endpoint_bits.trailing_zeros(); + + // Ensure we don't get interrupted about this packet again (w1c) + regs.in_sent0().write(|_| (1 << endpoint_id).into()); + + let Some(configin_reg) = regs.configin().get(usize::try_from(endpoint_id).unwrap()) + else { + // TODO: Log weird hardware behavior? + continue; + }; + let configin = configin_reg.read(); + + if configin.rdy() { + // TODO: Log weird hardware behavior... + continue; + } + + let buf_pool = self + .buf_pools_in + .get_mut(usize::try_from(endpoint_id).unwrap()) + .unwrap(); + let sent_buf_id = BufId(configin.buffer()); + buf_pool.put(sent_buf_id); + + if let Some(next_pkt) = self + .transmit_queues + .deque_next_packet(endpoint_id, sent_buf_id) + { + // We have more packets for this endpoint already in the + // peripheral SRAM; let's tell the hardware to prep the next + // one for sending. + configin_reg.write(|w| { + w.buffer(next_pkt.buf_id.into()) + .size(next_pkt.len.into()) + .rdy(true) + }); + } + return Some(UsbEvent::PacketSent { + endpoint: endpoint_id, + }); + } + } + if intr.host_lost() { + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.host_lost_clear()); + return Some(UsbEvent::LinkDown); + } + if intr.powered() { + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.powered_clear()); + return Some(UsbEvent::VBus); + } + if intr.disconnected() { + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.disconnected_clear()); + return Some(UsbEvent::VBusLost); + } + if intr.link_reset() { + self.reset_in(); + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.link_reset_clear()); + return Some(UsbEvent::UsbReset); + } + if intr.av_overflow() { + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.av_overflow_clear()); + traceln!("av_overflow"); + } + if intr.link_suspend() { + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.link_suspend_clear()); + } + if intr.link_resume() { + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.link_resume_clear()); + } + if intr.av_out_empty() { + traceln!("av_out_empty"); + self.fill_out_buffer_fifo(); + } + if intr.av_setup_empty() { + traceln!("av_setup_empty"); + self.fill_setup_buffer_fifo(); + } + if intr.rx_full() { + traceln!("rx_full"); + } + + None + } +} + +#[derive(Eq, PartialEq, Debug)] +pub enum TransmitQueueAction { + None, + SendNow, +} + +pub mod transmit_queue { + use super::*; + + pub struct TransmitQueues { + slots: [NextInPacket; BUFFER_SLOT_COUNT], + + /// Indexed by endpoint num, this is the slot index of the last packet + /// queued for transmission on that endpoint. + last_pkt_idx: [Option; NB_EP], + } + impl TransmitQueues { + pub const fn new() -> Self { + Self { + slots: [NextInPacket::NONE; BUFFER_SLOT_COUNT], + last_pkt_idx: [None; NB_EP], + } + } + pub fn reset(&mut self) { + *self = Self::new() + } + + #[must_use] + #[inline(always)] + pub fn queue(&mut self, ep_id: u32, pkt: NextInPacket) -> TransmitQueueAction { + let ep_id = usize::try_from(ep_id).unwrap(); + let last_pkt_idx = &mut self.last_pkt_idx[ep_id]; + let result = if let Some(last_pkt_idx) = *last_pkt_idx + && let Some(entry) = self.slots.get_mut(usize::from(last_pkt_idx)) + { + *entry = pkt; + TransmitQueueAction::None + } else { + if let Some(entry) = self.slots.get_mut(usize::from(pkt.buf_id)) { + *entry = NextInPacket::NONE; + } + TransmitQueueAction::SendNow + }; + *last_pkt_idx = Some(pkt.buf_id); + result + } + + #[inline(always)] + pub fn deque_next_packet( + &mut self, + ep_id: u32, + sent_buf_id: BufId, + ) -> Option { + let ep_id = usize::try_from(ep_id).unwrap(); + let sent_buf_id = usize::from(sent_buf_id); + let next_pkt = &mut self.slots[sent_buf_id]; + if usize::from(next_pkt.buf_id) >= BUFFER_SLOT_COUNT { + self.last_pkt_idx[ep_id] = None; + return None; + } + Some(core::mem::replace(next_pkt, NextInPacket::NONE)) + } + } + impl Default for TransmitQueues { + fn default() -> Self { + Self::new() + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn test_transmit_queues() { + let mut queues = TransmitQueues::new(); + assert_eq!( + queues.queue(0, NextInPacket { buf_id: 4, len: 64 }), + TransmitQueueAction::SendNow + ); + assert_eq!( + queues.queue(0, NextInPacket { buf_id: 5, len: 64 }), + TransmitQueueAction::None + ); + assert_eq!( + queues.queue( + 1, + NextInPacket { + buf_id: 10, + len: 64 + } + ), + TransmitQueueAction::SendNow + ); + assert_eq!( + queues.queue( + 1, + NextInPacket { + buf_id: 11, + len: 64 + } + ), + TransmitQueueAction::None + ); + assert_eq!( + queues.queue(0, NextInPacket { buf_id: 6, len: 0 }), + TransmitQueueAction::None + ); + assert_eq!( + queues.queue(1, NextInPacket { buf_id: 12, len: 3 }), + TransmitQueueAction::None + ); + + assert_eq!( + queues.deque_next_packet(1, BufId(10)), + Some(NextInPacket { + buf_id: 11, + len: 64 + }) + ); + assert_eq!( + queues.deque_next_packet(0, BufId(4)), + Some(NextInPacket { buf_id: 5, len: 64 }) + ); + assert_eq!( + queues.deque_next_packet(0, BufId(5)), + Some(NextInPacket { buf_id: 6, len: 0 }) + ); + assert_eq!(queues.deque_next_packet(0, BufId(6)), None); + + assert_eq!( + queues.queue( + 1, + NextInPacket { + buf_id: 10, + len: 33 + } + ), + TransmitQueueAction::None + ); + assert_eq!( + queues.queue(0, NextInPacket { buf_id: 4, len: 64 }), + TransmitQueueAction::SendNow + ); + assert_eq!( + queues.queue(0, NextInPacket { buf_id: 5, len: 9 }), + TransmitQueueAction::None + ); + + assert_eq!( + queues.deque_next_packet(1, BufId(11)), + Some(NextInPacket { buf_id: 12, len: 3 }) + ); + assert_eq!( + queues.deque_next_packet(1, BufId(12)), + Some(NextInPacket { + buf_id: 10, + len: 33 + }) + ); + assert_eq!( + queues.deque_next_packet(0, BufId(4)), + Some(NextInPacket { buf_id: 5, len: 9 }) + ); + assert_eq!(queues.deque_next_packet(0, BufId(5)), None); + assert_eq!(queues.deque_next_packet(1, BufId(10)), None); + + // Make sure we cleaned up after ourselves... + assert!(queues.slots.iter().all(|s| *s == NextInPacket::NONE)); + assert!(queues.last_pkt_idx.iter().all(|i| i.is_none())); + } + } +} + +pub mod buf_pool { + use super::*; + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[repr(transparent)] + pub struct BufId(pub u32); + impl BufId { + pub const fn offset(&self) -> usize { + self.0 as usize * BUFFER_SLOT_SIZE_WORDS + } + } + impl From for u32 { + fn from(value: BufId) -> Self { + value.0 + } + } + impl From for usize { + fn from(value: BufId) -> Self { + usize::try_from(value.0).unwrap() + } + } + impl From for BufId { + fn from(value: u32) -> Self { + Self(value) + } + } + + pub struct BuffPoolAllocator { + allocated_buf_ids: u32, + } + impl Default for BuffPoolAllocator { + fn default() -> Self { + Self::new() + } + } + impl BuffPoolAllocator { + pub const fn new() -> Self { + Self { + allocated_buf_ids: 0, + } + } + pub const fn new_bufpool(&mut self, len: u32) -> Option { + let start_id = self.allocated_buf_ids.trailing_ones(); + if len == 0 || start_id + len > 32 { + return None; + } + let mask = (((1_u64 << len) - 1) << start_id) as u32; + self.allocated_buf_ids |= mask; + Some(BufPool { + init_value: mask, + available_bufs: mask, + }) + } + pub const fn remainder_bufpool(mut self) -> Option { + let left = self.allocated_buf_ids.leading_zeros(); + self.new_bufpool(left) + } + } + + #[cfg(test)] + mod test_buff_pool_allocator { + use super::*; + + #[test] + fn test_next() { + let mut allocator = BuffPoolAllocator::new(); + assert_eq!( + allocator.new_bufpool(1), + Some(BufPool { + available_bufs: 0b01, + init_value: 0b01 + }) + ); + assert_eq!( + allocator.new_bufpool(1), + Some(BufPool { + available_bufs: 0b10, + init_value: 0b10, + }) + ); + assert_eq!( + allocator.new_bufpool(2), + Some(BufPool { + available_bufs: 0b1100, + init_value: 0b1100, + }) + ); + assert_eq!(allocator.new_bufpool(0), None); + assert_eq!(allocator.new_bufpool(30), None); + assert_eq!( + allocator.new_bufpool(28), + Some(BufPool { + available_bufs: (0xffff_ffffu64 << 4) as u32, + init_value: (0xffff_ffffu64 << 4) as u32, + }) + ); + assert_eq!(allocator.new_bufpool(1), None); + } + #[test] + fn test_remainder() { + let mut allocator = BuffPoolAllocator::new(); + assert_eq!( + allocator.new_bufpool(1), + Some(BufPool { + available_bufs: 0b01, + init_value: 0b01, + }) + ); + assert_eq!( + allocator.new_bufpool(1), + Some(BufPool { + available_bufs: 0b10, + init_value: 0b10, + }) + ); + assert_eq!( + allocator.new_bufpool(2), + Some(BufPool { + available_bufs: 0b1100, + init_value: 0b1100, + }) + ); + assert_eq!( + allocator.remainder_bufpool(), + Some(BufPool { + available_bufs: (0xffff_ffffu64 << 4) as u32, + init_value: (0xffff_ffffu64 << 4) as u32, + }) + ); + } + } + + #[derive(PartialEq, Eq, Debug, Clone, Copy)] + pub struct BufPool { + // bitset of bufs that are currently available for taking with take(). + available_bufs: u32, + init_value: u32, + } + impl BufPool { + pub const EMPTY: Self = Self { + available_bufs: 0, + init_value: 0, + }; + + #[cfg(test)] + pub const fn new(start_id: usize, len: usize) -> Self { + assert!(start_id < 32); + assert!(len > 0); + assert!(start_id + len <= 32); + let available_bufs = (((1_u64 << len) - 1) << start_id) as u32; + Self { + available_bufs, + init_value: available_bufs, + } + } + pub fn reset(&mut self) { + self.available_bufs = self.init_value; + } + pub fn take(&mut self) -> Option { + if self.is_empty() { + return None; + } + let buf_id = self.available_bufs.trailing_zeros(); + let mask = 1 << buf_id; + debug_assert!((self.available_bufs & mask) != 0); + self.available_bufs &= !mask; + Some(BufId(buf_id)) + } + pub fn put(&mut self, buf_id: BufId) { + let mask = 1 << u32::from(buf_id); + debug_assert!((self.available_bufs & mask) == 0); + self.available_bufs |= mask; + } + + pub fn len(&self) -> usize { + usize::try_from(self.available_bufs.count_ones()).unwrap() + } + + pub fn is_empty(&self) -> bool { + self.available_bufs == 0 + } + } + + #[cfg(test)] + mod test { + use super::*; + + #[test] + fn test_full_size() { + let mut pool = BufPool::new(0, 32); + assert_eq!(pool.available_bufs, 0xffff_ffff); + for i in 0..32 { + assert_eq!(Some(BufId::from(i)), pool.take()); + } + assert_eq!(None, pool.take()); + pool.put(5.into()); + pool.put(7.into()); + pool.put(3.into()); + assert_eq!(Some(3.into()), pool.take()); + assert_eq!(Some(5.into()), pool.take()); + assert_eq!(Some(7.into()), pool.take()); + assert_eq!(None, pool.take()); + assert_eq!(None, pool.take()); + } + + #[test] + fn test_5_bits() { + let mut pool = BufPool::new(4, 5); + assert_eq!(pool.available_bufs, 0x0000_01f0); + assert_eq!(Some(BufId::from(4)), pool.take()); + assert_eq!(Some(BufId::from(5)), pool.take()); + assert_eq!(Some(BufId::from(6)), pool.take()); + assert_eq!(Some(BufId::from(7)), pool.take()); + assert_eq!(Some(BufId::from(8)), pool.take()); + assert_eq!(None, pool.take()); + + pool.put(5.into()); + pool.put(6.into()); + assert_eq!(Some(5.into()), pool.take()); + assert_eq!(Some(6.into()), pool.take()); + assert_eq!(None, pool.take()); + } + + #[test] + fn test_config() { + assert_eq!( + UsbConfig::new( + &[ + EpIn { + num: 1, + buf_pool_size: 3, + }, + EpIn { + num: 3, + buf_pool_size: 5, + }, + ], + &[ + EpOut { + num: 2, + set_nak: true + }, + EpOut { + num: 4, + set_nak: false + }, + ] + ), + UsbConfig { + buf_pool_setup: BufPool::new(0, 4), + buf_pool_out: BufPool::new(4, 12), + buf_pools_in: [ + BufPool::new(24, 8), + BufPool::new(16, 3), + BufPool::EMPTY, + BufPool::new(19, 5), + BufPool::EMPTY, + BufPool::EMPTY, + BufPool::EMPTY, + BufPool::EMPTY, + BufPool::EMPTY, + BufPool::EMPTY, + BufPool::EMPTY, + BufPool::EMPTY, + ], + in_mask: 0b01011, + out_mask: 0b10101, + set_nak_mask: 0b100, + }, + ); + } + + #[test] + #[should_panic] + fn test_config_too_many_set_nak() { + UsbConfig::new( + &[EpIn { + num: 1, + buf_pool_size: 3, + }], + &[ + EpOut { + num: 2, + set_nak: true, + }, + EpOut { + num: 4, + set_nak: true, + }, + ], + ); + } + } +} From 2831062957db4a467d0c68edd76b772144179f56 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 18 Mar 2026 21:38:05 -0700 Subject: [PATCH 06/46] tests: add a bare-bones usb test Signed-off-by: Chris Frantz --- target/earlgrey/tests/usbdev/BUILD.bazel | 130 +++++++++++++ target/earlgrey/tests/usbdev/system.json5 | 77 ++++++++ target/earlgrey/tests/usbdev/target.rs | 29 +++ target/earlgrey/tests/usbdev/test_usb.rs | 218 ++++++++++++++++++++++ 4 files changed, 454 insertions(+) create mode 100644 target/earlgrey/tests/usbdev/BUILD.bazel create mode 100644 target/earlgrey/tests/usbdev/system.json5 create mode 100644 target/earlgrey/tests/usbdev/target.rs create mode 100644 target/earlgrey/tests/usbdev/test_usb.rs diff --git a/target/earlgrey/tests/usbdev/BUILD.bazel b/target/earlgrey/tests/usbdev/BUILD.bazel new file mode 100644 index 00000000..5f9d7bd8 --- /dev/null +++ b/target/earlgrey/tests/usbdev/BUILD.bazel @@ -0,0 +1,130 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "test_usb", + srcs = [ + "test_usb.rs", + ], + codegen_crate_name = "test_usb_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/usb:hal_usb", + "//protocol/usb/stack", + "//target/earlgrey/drivers:usb_driver", + "//target/earlgrey/registers:pinmux", + "//target/earlgrey/registers:top_earlgrey", + "//target/earlgrey/registers:usbdev", + "//util/console", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + ], +) + +system_image( + name = "usb", + apps = [ + ":test_usb", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "usb_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":usb", +) diff --git a/target/earlgrey/tests/usbdev/system.json5 b/target/earlgrey/tests/usbdev/system.json5 new file mode 100644 index 00000000..87cfd0ad --- /dev/null +++ b/target/earlgrey/tests/usbdev/system.json5 @@ -0,0 +1,77 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "test_usb", + flash_size_bytes: 16384, + processes: [{ + name: "test_usb_process", + ram_size_bytes: 4096, + objects: [ + { + name: "usbdev_interrupts", + type: "interrupt", + irqs: [ + { name: "usbdev_pkt_received", number: 135 }, + { name: "usbdev_pkt_sent", number: 136 }, + { name: "usbdev_disconnected", number: 137 }, + { name: "usbdev_host_lost", number: 138 }, + + { name: "usbdev_link_reset", number: 139 }, + { name: "usbdev_link_suspend", number: 140 }, + { name: "usbdev_link_resume", number: 141 }, + { name: "usbdev_av_out_empty", number: 142 }, + + { name: "usbdev_rx_full", number: 143 }, + { name: "usbdev_av_overflow", number: 144 }, + //{ name: "usbdev_link_in_err", number: 145 }, + { name: "usbdev_rx_crc_err", number: 146 }, + + { name: "usbdev_rx_pid_err", number: 147 }, + { name: "usbdev_rx_bitstuff_err", number: 148 }, + { name: "usbdev_frame", number: 149 }, + //{ name: "usbdev_powered", number: 150 }, + + //{ name: "usbdev_link_out_err", number: 151 }, + { name: "usbdev_av_setup_empty", number: 152 }, + ], + }, + ], + memory_mappings: [ + { + name: "usbdev", + type: "device", + start_address: 0x40320000, + size_bytes: 0x1000, + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000, + } + + ], + threads: [ + { + name: "usb_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/usbdev/target.rs b/target/earlgrey/tests/usbdev/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/tests/usbdev/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/usbdev/test_usb.rs b/target/earlgrey/tests/usbdev/test_usb.rs new file mode 100644 index 00000000..f7f95dae --- /dev/null +++ b/target/earlgrey/tests/usbdev/test_usb.rs @@ -0,0 +1,218 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use test_usb_codegen::{handle, signals}; +use userspace::time::Instant; +use userspace::{entry, syscall}; +use pw_status::Error; + +use aligned::{Aligned, A4}; +use hal_usb::driver::{UsbDriver, UsbEvent, UsbPacket}; +use hal_usb::{Direction, StringDescriptorRef, USB_CLASS_VENDOR}; +use usb_driver::{EpIn, EpOut, UsbConfig}; +use usb_stack::{ + //UsbActionRun, + DescriptorSource, + UsbAction, +}; + +const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); +const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); +const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); +const USB_TEST_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); + +static DEVICE_DESC: hal_usb::DeviceDescriptor = hal_usb::DeviceDescriptor { + device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, + device_sub_class: 0x00, + device_protocol: 0x00, + max_packet_size: 64, + vendor_id: 0x18d1, // Google, Inc. + product_id: 0x503a, // STWG USB Fullspeed IP. + device_release_num: 0x0100, + manufacturer: USB_VENDOR_HANDLE, + product: USB_PRODUCT_HANDLE, + serial_num: USB_SERIAL_HANDLE, +}; +const CONFIG_DESC: hal_usb::ConfigDescriptor = hal_usb::ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[hal_usb::InterfaceDescriptor { + name: USB_TEST_HANDLE, + interface_number: 1, + alternate_setting: 0, + interface_class: USB_CLASS_VENDOR, + interface_sub_class: 0xFF, + interface_protocol: 1, + func_descs: &[], + endpoints: &[ + hal_usb::EndpointDescriptor { + direction: Direction::DeviceToHost, + endpoint_num: 1, + interval: 0, + max_packet_size: 64, + transfer_type: hal_usb::TransferType::Bulk, + }, + hal_usb::EndpointDescriptor { + direction: Direction::HostToDevice, + endpoint_num: 2, + interval: 0, + max_packet_size: 64, + transfer_type: hal_usb::TransferType::Bulk, + }, + ], + }], +}; + +const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { + langs: &[ + // English - United States + 0x0409, + ], +}; + +const VENDOR_ID: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Google Inc.").as_ref(); +const PRODUCT_ID_DEFAULT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("Earlgrey").as_ref(); +const USB_TEST: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("USB Test Interface").as_ref(); + +struct MyDescriptors<'a> { + serial_desc_bytes: StringDescriptorRef<'a>, + product_desc_bytes: StringDescriptorRef<'a>, +} + +impl DescriptorSource for MyDescriptors<'_> { + const DEVICE_DESC_BYTES: &'static Aligned = &Aligned(DEVICE_DESC.serialize()); + const CONFIG_DESC_BYTES: &'static Aligned = + &Aligned(CONFIG_DESC.serialize::<{ CONFIG_DESC.total_size() }>()); + const STRING_DESC_0_BYTES: &'static Aligned = + &Aligned(STRING_DESC_0.serialize::<{ STRING_DESC_0.total_size() }>()); + + fn get_string( + &self, + handle: hal_usb::StringHandle, + _lang: u16, + ) -> Option> { + match handle { + USB_VENDOR_HANDLE => Some(VENDOR_ID), + USB_PRODUCT_HANDLE => Some(self.product_desc_bytes), + USB_SERIAL_HANDLE => Some(self.serial_desc_bytes), + USB_TEST_HANDLE => Some(USB_TEST), + _ => None, + } + } +} + +const CONTROL_EP_OUT_NUM: u8 = 0; + +fn handle_usb() -> Result<(), Error> { + let mut serial_num_buffer = Aligned::([0_u8; 130]); + // TODO + //let mut product_desc_buffer = Aligned::([0_u8; 100]); + let descriptors = MyDescriptors { + serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned(&mut serial_num_buffer, b"12345") + .unwrap(), + product_desc_bytes: PRODUCT_ID_DEFAULT, + }; + const USB_EP_IN: EpIn = EpIn { + num: 1, + buf_pool_size: 8, + }; + const USB_EP_OUT: EpOut = EpOut { + num: 2, + set_nak: true, + }; + + const USB_CONFIG: UsbConfig = UsbConfig::new(&[USB_EP_IN], &[USB_EP_OUT]); + let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); + let mut ep0 = usb_stack::SimpleEp0::new(); + let mut ep0_action: UsbAction<'_> = UsbAction::None; + + loop { + let wait_return = syscall::object_wait( + handle::USBDEV_INTERRUPTS, + signals::USBDEV_PKT_RECEIVED + | signals::USBDEV_PKT_SENT + | signals::USBDEV_DISCONNECTED + | signals::USBDEV_HOST_LOST + | signals::USBDEV_LINK_RESET + | signals::USBDEV_LINK_SUSPEND + | signals::USBDEV_LINK_RESUME + | signals::USBDEV_AV_OUT_EMPTY + | signals::USBDEV_RX_FULL + | signals::USBDEV_AV_OVERFLOW + //| signals::USBDEV_LINK_IN_ERR + | signals::USBDEV_RX_CRC_ERR + | signals::USBDEV_RX_PID_ERR + | signals::USBDEV_RX_BITSTUFF_ERR + | signals::USBDEV_FRAME + //| signals::USBDEV_POWERED + //| signals::USBDEV_LINK_OUT_ERR + | signals::USBDEV_AV_SETUP_EMPTY, + Instant::MAX, + )?; + + if wait_return.user_data != 0 { + pw_log::error!("Incorrect WaitReturn values"); + return Err(Error::Unknown); + } + while let Some(event) = usb.poll() { + match event { + UsbEvent::SetupPacket { pkt, endpoint } => { + if endpoint == 0 { + console::println!("SETUP: {:?}", pkt); + ep0_action = ep0.handle_event(event, &descriptors); + } else { + console::println!("Setup on bad EP {:?}", endpoint); + } + } + + UsbEvent::DataOutPacket(pkt) => match u8::try_from(pkt.endpoint_index()).unwrap() { + CONTROL_EP_OUT_NUM => { + console::println!("OUT on control ep"); + } + ep => { + console::println!("Unhandled OUT on EP {} len={}", ep, pkt.len()); + } + }, + UsbEvent::UsbReset => { + console::println!("USB reset"); + } + _ => { + ep0_action.merge(ep0.handle_event(event, &descriptors)); + } + } + ep0_action.run(&mut usb); + } + } +} + +fn usb_setup_pinmux() { + use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn}; + let mut pinmux = unsafe { pinmux::PinmuxAon::new() }; + + pinmux + .regs_mut() + .mio_periph_insel() + .at(PinmuxPeripheralIn::UsbdevSense as usize) + .modify(|_| (PinmuxInsel::ConstantOne as u32).into()); +} + +#[entry] +fn entry() -> ! { + // Since this is written as a test, shut down with the return status from `main()`. + usb_setup_pinmux(); + let ret = handle_usb(); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} From 7ceb265ab57ecd3ac318a4f8d72a4b36b0f75c95 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Tue, 21 Apr 2026 21:09:21 -0700 Subject: [PATCH 07/46] usb: WIP stack cleanups Signed-off-by: Chris Frantz --- hal/blocking/usb/descriptor.rs | 28 +++++++++++++ hal/blocking/usb/driver.rs | 10 ++--- protocol/usb/stack/lib.rs | 50 ++++++++++++++++++++++-- target/earlgrey/drivers/usb_driver.rs | 46 ++++++++++++---------- target/earlgrey/tests/usbdev/test_usb.rs | 2 + 5 files changed, 107 insertions(+), 29 deletions(-) diff --git a/hal/blocking/usb/descriptor.rs b/hal/blocking/usb/descriptor.rs index e0f06c30..4e214ddf 100644 --- a/hal/blocking/usb/descriptor.rs +++ b/hal/blocking/usb/descriptor.rs @@ -595,15 +595,42 @@ impl DfuFunctionalDescriptor { } } +pub struct RawFunctionalDescriptor { + pub descriptor_type: u8, + pub content: &'static [u8], +} +impl RawFunctionalDescriptor { + pub const fn total_size(&self) -> usize { + self.content.len() + 2 + } + pub const fn serialize(&self, dest: &mut [u8], offset: usize) { + dest[offset] = self.total_size() as u8; + dest[offset + 1] = self.descriptor_type; + let mut i = 0; + while i < self.content.len() { + dest[offset + 2 + i] = self.content[i]; + i += 1; + } + } +} + // This should be a trait, but traits can't be used from const functions :( pub enum FunctionalDescriptor { Dfu(DfuFunctionalDescriptor), + Raw(RawFunctionalDescriptor), } impl FunctionalDescriptor { + pub const fn raw(descriptor_type: u8, content: &'static [u8]) -> Self { + Self::Raw(RawFunctionalDescriptor { + descriptor_type, + content, + }) + } pub const fn total_size(&self) -> usize { match self { Self::Dfu(dfu) => dfu.total_size(), + Self::Raw(raw) => raw.total_size(), } } #[allow(clippy::identity_op)] @@ -611,6 +638,7 @@ impl FunctionalDescriptor { assert!(offset + self.total_size() <= dest.len()); match self { Self::Dfu(dfu) => dfu.serialize(dest, offset), + Self::Raw(raw) => raw.serialize(dest, offset), } } } diff --git a/hal/blocking/usb/driver.rs b/hal/blocking/usb/driver.rs index 9a529441..43161841 100644 --- a/hal/blocking/usb/driver.rs +++ b/hal/blocking/usb/driver.rs @@ -23,10 +23,10 @@ pub trait UsbDriver { fn transfer_in(&mut self, endpoint_idx: u8, data: &Aligned, zlp: bool) -> usize; /// Stalls an input endpoint. Note: the driver will automatically unstall all endpoints upon a USB reset or a new SETUP packet. - fn stall_in(&mut self, endpoint_idx: u8, stalled: bool); + fn stall(&mut self, endpoint_num: u8, stalled: bool); - /// Stalls an output endpoint. Note: the driver will automatically unstall all endpoints upon a USB reset or a new SETUP packet. - fn stall_out(&mut self, endpoint_idx: u8, stalled: bool); + /// Returns whether the endpoint is stalled. + fn is_stalled(&mut self, endpoint_num: u8) -> bool; /// Sets the address the peripheral responds to. The USB stack must call /// this function in response to a SET_ADDRESS control request on endpoint 0. @@ -47,10 +47,10 @@ pub trait UsbPacket { /// Copy the packet data from the peripheral buffer into SRAM. Will fault if /// `self.len()` > `dest.len()`. - fn copy_to_uninit(self, dest: &mut [MaybeUninit]) -> &Aligned; + fn copy_to_uninit(self, dest: &mut [MaybeUninit]) -> &[u8]; /// Copy the packet data from the peripheral buffer into SRAM. - fn copy_to(self, dest: &mut [u32]) -> &Aligned; + fn copy_to(self, dest: &mut [u32]) -> &[u8]; fn is_empty(&self) -> bool { self.len() == 0 diff --git a/protocol/usb/stack/lib.rs b/protocol/usb/stack/lib.rs index 6b23a19b..e590db66 100644 --- a/protocol/usb/stack/lib.rs +++ b/protocol/usb/stack/lib.rs @@ -20,8 +20,12 @@ pub trait DescriptorSource { const DEVICE_DESC_BYTES: &'static Aligned; const CONFIG_DESC_BYTES: &'static Aligned; const STRING_DESC_0_BYTES: &'static Aligned; + const DEVICE_STATUS: Aligned; fn get_string(&self, handle: StringHandle, lang: u16) -> Option>; + fn get_device_status(&self) -> &Aligned { + &Self::DEVICE_STATUS + } } pub const EMPTY: &Aligned = &Aligned([]); @@ -52,8 +56,18 @@ pub enum UsbAction<'a> { SetAddress { new_address: u8, }, + GetEndpointStatus { + endpoint: u8, + }, + SetEndpointStatus { + endpoint: u8, + stall: bool, + }, } impl<'a> UsbAction<'a> { + const EP_CLEAR: Aligned = Aligned([0u8, 0]); + const EP_HALTED: Aligned = Aligned([1u8, 0]); + #[inline(always)] #[track_caller] pub fn control_transfer_in_or_stall( @@ -100,8 +114,20 @@ impl<'a> UsbAction<'a> { } Self::SetAddress { new_address } => driver.set_address(*new_address), Self::StallInAndOut { endpoint } => { - driver.stall_in(*endpoint, true); - driver.stall_out(*endpoint, true); + driver.stall((*endpoint) & 0x7f, true); + driver.stall((*endpoint) | 0x80, true); + } + Self::GetEndpointStatus { endpoint } => { + let data = if driver.is_stalled(*endpoint) { + &Self::EP_HALTED + } else { + &Self::EP_CLEAR + }; + let _ = driver.transfer_in(0, data, true); + } + Self::SetEndpointStatus { endpoint, stall } => { + driver.stall(*endpoint, *stall); + let _ = driver.transfer_in(0, EMPTY, true); } } *self = UsbAction::None; @@ -175,6 +201,22 @@ impl SimpleEp0 { UsbAction::StallInAndOut { endpoint: 0 } } } + Request::DEVICE_GET_STATUS => UsbAction::TransferIn { + endpoint: 0, + data: descriptor_source.get_device_status(), + zlp: true, + }, + Request::ENDPOINT_GET_STATUS => UsbAction::GetEndpointStatus { + endpoint: setup_pkt.index() as u8, + }, + Request::ENDPOINT_SET_FEATURE => UsbAction::SetEndpointStatus { + endpoint: setup_pkt.index() as u8, + stall: true, + }, + Request::ENDPOINT_CLEAR_FEATURE => UsbAction::SetEndpointStatus { + endpoint: setup_pkt.index() as u8, + stall: false, + }, Request::DEVICE_SET_ADDRESS => { self.new_address = Some(setup_pkt.value() as u8); UsbAction::TransferIn { @@ -295,11 +337,11 @@ pub mod testing { self.data.len() } - fn copy_to_uninit(self, _dest: &mut [core::mem::MaybeUninit]) -> &Aligned { + fn copy_to_uninit(self, _dest: &mut [core::mem::MaybeUninit]) -> &[u8] { unimplemented!() } - fn copy_to(self, dest: &mut [u32]) -> &Aligned { + fn copy_to(self, dest: &mut [u32]) -> &[u8] { let dest_bytes = dest.as_mut_bytes(); let copy_len = self.data.len().min(dest_bytes.len()); dest_bytes[..copy_len].copy_from_slice(&self.data[..copy_len]); diff --git a/target/earlgrey/drivers/usb_driver.rs b/target/earlgrey/drivers/usb_driver.rs index dcb3b0c3..0402471d 100644 --- a/target/earlgrey/drivers/usb_driver.rs +++ b/target/earlgrey/drivers/usb_driver.rs @@ -41,7 +41,7 @@ impl UsbPacket for PacketHandle { self.packet_len.into() } - fn copy_to_uninit(self, dest: &mut [core::mem::MaybeUninit]) -> &Aligned { + fn copy_to_uninit(self, dest: &mut [core::mem::MaybeUninit]) -> &[u8] { #![allow(clippy::needless_range_loop)] // TODO: Are we sure we want to silently truncate if dest isn't big enough? @@ -55,15 +55,10 @@ impl UsbPacket for PacketHandle { let result = &dest[..word_len]; let result = unsafe { &*(result as *const [core::mem::MaybeUninit] as *const [u32]) }; - // TODO: add a Aligned::try_from() function to the aligned crate and use it here with unwrap. - unsafe { - core::mem::transmute::<&[u8], &Aligned>( - &result.as_bytes()[..min(self.len(), word_len * 4)], - ) - } + &result.as_bytes()[..min(self.len(), word_len * 4)] } - fn copy_to(self, dest: &mut [u32]) -> &Aligned { + fn copy_to(self, dest: &mut [u32]) -> &[u8] { #![allow(clippy::needless_range_loop)] // TODO: Are we sure we want to silently truncate if dest isn't big enough? @@ -71,12 +66,7 @@ impl UsbPacket for PacketHandle { for i in 0..word_len { dest[i] = self.data.at(i).read(); } - // TODO: add a Aligned::try_from() function to the aligned crate and use it here with unwrap. - unsafe { - core::mem::transmute::<&[u8], &Aligned>( - &dest.as_bytes()[..min(self.len(), dest.as_bytes().len())], - ) - } + &dest.as_bytes()[..min(self.len(), dest.as_bytes().len())] } } @@ -342,18 +332,34 @@ impl UsbDriver for Usb { type Packet<'a> = PacketHandle>; #[inline(always)] - fn stall_in(&mut self, endpoint_idx: u8, stalled: bool) { + fn stall(&mut self, endpoint_num: u8, stalled: bool) { + if endpoint_num & 0x80 != 0 { self.mmio .regs_mut() .in_stall0() - .modify(|w| bit_setval(u32::from(w), endpoint_idx.into(), stalled).into()); - } - #[inline(always)] - fn stall_out(&mut self, endpoint_idx: u8, stalled: bool) { + .modify(|w| bit_setval(u32::from(w), (endpoint_num & 0x0f).into(), stalled).into()); + } else { self.mmio .regs_mut() .out_stall0() - .modify(|w| bit_setval(u32::from(w), endpoint_idx.into(), stalled).into()); + .modify(|w| bit_setval(u32::from(w), (endpoint_num & 0x0f).into(), stalled).into()); + } + } + + #[inline(always)] + fn is_stalled(&mut self, endpoint_num: u8) -> bool { + if endpoint_num & 0x80 != 0 { + u32::from(self.mmio + .regs() + .in_stall0() + .read()) & (1 << (endpoint_num & 0x0f)) != 0 + + } else { + u32::from(self.mmio + .regs() + .out_stall0() + .read()) & (1 << (endpoint_num & 0x0f)) != 0 + } } /// Store data in peripheral buffer that will be transferred when the host requests it. diff --git a/target/earlgrey/tests/usbdev/test_usb.rs b/target/earlgrey/tests/usbdev/test_usb.rs index f7f95dae..29ba26ca 100644 --- a/target/earlgrey/tests/usbdev/test_usb.rs +++ b/target/earlgrey/tests/usbdev/test_usb.rs @@ -91,6 +91,8 @@ impl DescriptorSource for MyDescriptors<'_> { &Aligned(CONFIG_DESC.serialize::<{ CONFIG_DESC.total_size() }>()); const STRING_DESC_0_BYTES: &'static Aligned = &Aligned(STRING_DESC_0.serialize::<{ STRING_DESC_0.total_size() }>()); + // We advertise that we are self-powered and do not support remote wakeup. + const DEVICE_STATUS: Aligned = Aligned([1u8, 0]); fn get_string( &self, From aa0422912f9fd97cb5bbb08dc1598c0831c371de Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 14:04:17 -0700 Subject: [PATCH 08/46] doc: add rustdoc documentation to USB HAL and stack Signed-off-by: Chris Frantz AI-assistant: Gemini --- hal/blocking/usb/descriptor.rs | 121 ++++++++++++++++++++++++++++++++- hal/blocking/usb/driver.rs | 82 +++++++++++++++------- hal/blocking/usb/lib.rs | 70 +++++++++++++++++++ protocol/usb/stack/lib.rs | 55 ++++++++++++--- 4 files changed, 294 insertions(+), 34 deletions(-) diff --git a/hal/blocking/usb/descriptor.rs b/hal/blocking/usb/descriptor.rs index 4e214ddf..e885b0fe 100644 --- a/hal/blocking/usb/descriptor.rs +++ b/hal/blocking/usb/descriptor.rs @@ -1,57 +1,103 @@ +//! USB descriptor structures and serialization. +//! +//! This module provides the tools to define and serialize standard USB +//! descriptors, including Device, Configuration, Interface, and Endpoint +//! descriptors. + use aligned::Aligned; use aligned::A4; use ufmt::uWrite; +/// USB Audio class code. pub const USB_CLASS_AUDIO: u8 = 0x01; +/// USB Communications and CDC Control class code. pub const USB_CLASS_COMMUNIATIONS: u8 = 0x02; +/// USB HID (Human Interface Device) class code. pub const USB_CLASS_HID: u8 = 0x03; +/// USB Physical class code. pub const USB_CLASS_PHYSICAL: u8 = 0x05; +/// USB Image class code. pub const USB_CLASS_IMAGE: u8 = 0x06; +/// USB Printer class code. pub const USB_CLASS_PRINTER: u8 = 0x07; +/// USB Mass Storage class code. pub const USB_CLASS_MASS_STORAGE: u8 = 0x08; +/// USB Hub class code. pub const USB_CLASS_HUB: u8 = 0x09; +/// USB CDC-Data class code. pub const USB_CLASS_CDC_DATA: u8 = 0x0a; +/// USB Smart Card class code. pub const USB_CLASS_SMART_CARD: u8 = 0x0b; +/// USB Content Security class code. pub const USB_CLASS_CONTENT_SECURITY: u8 = 0x0d; +/// USB Video class code. pub const USB_CLASS_VIDEO: u8 = 0x0e; +/// USB Personal Healthcare class code. pub const USB_CLASS_PERSONAL_HEALTHCARE: u8 = 0x0f; +/// USB Audio/Video class code. pub const USB_CLASS_AUDIO_VIDEO: u8 = 0x10; +/// USB Billboard class code. pub const USB_CLASS_BILLBOARD: u8 = 0x11; +/// USB Type-C Bridge class code. pub const USB_CLASS_USB_TYPEC_BRIDGE: u8 = 0x12; +/// USB Bulk Display class code. pub const USB_CLASS_BULK_DISPLAY: u8 = 0x13; +/// USB MCTP class code. pub const USB_CLASS_MCTP: u8 = 0x14; +/// USB I3C class code. pub const USB_CLASS_I3C: u8 = 0x3c; +/// USB Diagnostic Device class code. pub const USB_CLASS_DIAGNOSTIC_DEVICE: u8 = 0xdc; +/// USB Wireless Controller class code. pub const USB_CLASS_WIRELESS_CONTROLLER: u8 = 0xe0; +/// USB Miscellaneous class code. pub const USB_CLASS_MISC: u8 = 0xef; +/// USB Application Specific class code. pub const USB_CLASS_APPLICATION_SPECIFIC: u8 = 0xfe; +/// USB Vendor Specific class code. pub const USB_CLASS_VENDOR: u8 = 0xff; +/// DFU (Device Firmware Upgrade) subclass code. pub const USB_SUBCLASS_APPLICATION_SPECIFIC_DFU: u8 = 0x01; +/// DFU Runtime Mode protocol code. pub const USB_PROTOCOL_APPLICATION_SPECIFIC_DFU_RUNTIME_MODE: u8 = 0x01; +/// DFU Mode protocol code. pub const USB_PROTOCOL_APPLICATION_SPECIFIC_DFU_DFU_MODE: u8 = 0x02; use crate::DescriptorType; use crate::Direction; +/// A handle to a USB string descriptor. #[derive(Clone, Copy, Eq, PartialEq)] #[repr(transparent)] pub struct StringHandle(pub u8); impl StringHandle { + /// Indicates that no string descriptor is provided. pub const NONE: Self = StringHandle(0); } +/// A standard USB device descriptor. pub struct DeviceDescriptor { + /// The class of the device. pub device_class: DeviceClass, + /// The subclass of the device. pub device_sub_class: u8, + /// The protocol used by the device. pub device_protocol: u8, + /// Maximum packet size for Endpoint 0. pub max_packet_size: u8, + /// Vendor ID assigned by the USB-IF. pub vendor_id: u16, + /// Product ID assigned by the manufacturer. pub product_id: u16, + /// Device release number (in binary-coded decimal). pub device_release_num: u16, + /// Handle for the manufacturer string descriptor. pub manufacturer: StringHandle, + /// Handle for the product string descriptor. pub product: StringHandle, + /// Handle for the serial number string descriptor. pub serial_num: StringHandle, } impl DeviceDescriptor { @@ -62,6 +108,7 @@ impl DeviceDescriptor { Self::SIZE } + /// Serializes the device descriptor into a byte array. #[allow(clippy::identity_op)] pub const fn serialize(&self) -> [u8; Self::SIZE] { let mut buf = [0u8; Self::SIZE]; @@ -98,18 +145,25 @@ impl DeviceDescriptor { } } +/// A standard USB configuration descriptor. pub struct ConfigDescriptor { + /// The configuration value for this configuration. pub configuration_value: u8, - // in 2 mA units + /// Maximum power consumption in 2 mA units. pub max_power: u8, + /// Indicates if the device is self-powered. pub self_powered: bool, + /// Indicates if the device supports remote wakeup. pub remote_wakeup: bool, + /// List of interfaces included in this configuration. pub interfaces: &'static [InterfaceDescriptor], } impl ConfigDescriptor { const SIZE: usize = 9; + /// Returns the total size of the configuration descriptor, including + /// all interfaces and endpoints. pub const fn total_size(&self) -> usize { let mut result = Self::SIZE; let mut i = 0; @@ -120,6 +174,7 @@ impl ConfigDescriptor { result } + /// Serializes the configuration descriptor and its children into a byte array. #[allow(clippy::identity_op)] pub const fn serialize(&self) -> [u8; RESULT_SIZE] { assert!(self.total_size() == RESULT_SIZE); @@ -168,14 +223,23 @@ impl ConfigDescriptor { } } +/// A standard USB interface descriptor. pub struct InterfaceDescriptor { + /// Handle for the interface name string descriptor. pub name: StringHandle, + /// The alternate setting for this interface. pub alternate_setting: u8, + /// The interface number. pub interface_number: u8, + /// The interface class. pub interface_class: u8, + /// The interface subclass. pub interface_sub_class: u8, + /// The interface protocol. pub interface_protocol: u8, + /// List of class-specific functional descriptors. pub func_descs: &'static [FunctionalDescriptor], + /// List of endpoints used by this interface. pub endpoints: &'static [EndpointDescriptor], } impl InterfaceDescriptor { @@ -195,6 +259,7 @@ impl InterfaceDescriptor { } result } + /// Serializes the interface descriptor and its children. pub const fn serialize(&self) -> ([u8; RESULT_SIZE], usize) { assert!(RESULT_SIZE >= self.total_size()); @@ -236,11 +301,17 @@ impl InterfaceDescriptor { } } +/// A standard USB endpoint descriptor. pub struct EndpointDescriptor { + /// The data direction of the endpoint. pub direction: Direction, + /// The endpoint number (0-15). pub endpoint_num: u8, + /// The transfer type of the endpoint. pub transfer_type: TransferType, + /// Maximum packet size for this endpoint. pub max_packet_size: u16, + /// Polling interval (for interrupt and isochronous endpoints). pub interval: u8, } impl EndpointDescriptor { @@ -288,14 +359,18 @@ impl EndpointDescriptor { } } +/// A standard USB String Descriptor 0 (listing supported languages). pub struct StringDescriptor0 { + /// List of supported LANGIDs. pub langs: &'static [u16], } impl StringDescriptor0 { + /// Returns the total size of the descriptor. pub const fn total_size(&self) -> usize { 2 + core::mem::size_of_val(self.langs) } + /// Serializes the language list into a byte array. #[allow(clippy::identity_op)] pub const fn serialize(&self) -> [u8; RESULT_SIZE] { assert!(RESULT_SIZE == self.total_size()); @@ -320,12 +395,17 @@ impl StringDescriptor0 { } } +/// USB transfer type. #[derive(Clone, Copy, Debug)] #[allow(dead_code)] pub enum TransferType { + /// Control transfer. Control, + /// Isochronous transfer. Isochronous(SynchronizationType, UsageType), + /// Bulk transfer. Bulk, + /// Interrupt transfer. Interrupt, } impl TransferType { @@ -340,6 +420,7 @@ impl TransferType { } } +/// Isochronous synchronization type. #[derive(Clone, Copy, Debug)] #[allow(dead_code)] pub enum SynchronizationType { @@ -349,6 +430,7 @@ pub enum SynchronizationType { Synchronous = 3, } +/// Isochronous usage type. #[derive(Clone, Copy, Debug)] #[allow(dead_code, clippy::enum_variant_names)] pub enum UsageType { @@ -357,14 +439,22 @@ pub enum UsageType { ExplicitFeedbackDataEndpoint, } +/// USB device class code. pub struct DeviceClass(pub u8); impl DeviceClass { + /// Class is specified at the interface level. pub const SPECIFIED_BY_INTERFACE: Self = Self(0x00); + /// CDC (Communication Device Class). pub const COMMUNICATIONS_AND_CDC: Self = Self(0x02); + /// Hub device. pub const HUB: Self = Self(0x09); + /// Billboard device. pub const BILLBOARD: Self = Self(0x11); + /// Diagnostic device. pub const DIAGNOSTIC_DEVICE: Self = Self(0x3c); + /// Miscellaneous device. pub const MISCELLANEOUS: Self = Self(0xef); + /// Vendor-specified device class. pub const VENDOR_SPECIFIED: Self = Self(0xff); } impl From for u8 { @@ -373,9 +463,11 @@ impl From for u8 { } } +/// A statically-allocated USB string descriptor. pub struct StringDescriptor(Aligned); impl StringDescriptor { + /// Creates a string descriptor from an ASCII string at compile-time. pub const fn const_from_ascii(s: &str) -> Self { assert!(BYTE_LEN <= (u8::MAX as usize)); assert!(s.len() * 2 + 2 == BYTE_LEN); @@ -394,19 +486,23 @@ impl StringDescriptor { } StringDescriptor(Aligned(result)) } + /// Returns a reference to the string descriptor. pub const fn as_ref(&self) -> StringDescriptorRef<'_> { StringDescriptorRef(&self.0) } } +/// A reference to an aligned USB string descriptor. #[derive(Clone, Copy)] pub struct StringDescriptorRef<'a>(pub &'a Aligned); impl<'a> StringDescriptorRef<'a> { + /// Returns the descriptor as a byte slice. pub const fn as_bytes(self) -> &'a Aligned { self.0 } } +/// Macro for easily creating static string descriptors. #[macro_export] macro_rules! string_descriptor { ($s:expr) => { @@ -414,12 +510,16 @@ macro_rules! string_descriptor { }; } +/// Descriptor generation error. #[derive(Debug)] pub enum DescriptorErr { + /// Buffer is too small. Overflow, + /// Invalid encoding. Encoding, } +/// Generates a UTF-16 hex-encoded string descriptor from a byte slice. #[inline(always)] pub fn hex_utf16_descriptor(dest: &mut [u8], src: &[u8]) -> Result { const { assert!(cfg!(target_endian = "little")) }; @@ -442,6 +542,7 @@ pub fn hex_utf16_descriptor(dest: &mut [u8], src: &[u8]) -> Result( dest: &'a mut Aligned, @@ -451,11 +552,13 @@ pub fn hex_utf16_descriptor_aligned<'a>( Ok(StringDescriptorRef(&dest[..len])) } +/// Utility for dynamically writing content into a USB string descriptor. pub struct StringDescriptorWritter<'a> { buf: &'a mut Aligned, index: usize, } impl<'a> StringDescriptorWritter<'a> { + /// Creates a new writer using the provided buffer. pub fn new(buf: &'a mut Aligned) -> Result { if buf.len() < 2 || buf.len() > 2 + 255 { return Err(DescriptorErr::Overflow); @@ -463,6 +566,7 @@ impl<'a> StringDescriptorWritter<'a> { *buf.get_mut(1).unwrap() = DescriptorType::STRING.0; Ok(StringDescriptorWritter { buf, index: 2 }) } + /// Finalizes the descriptor and returns a reference to it. pub fn finalize(self) -> Result, DescriptorErr> { *self.buf.get_mut(0).ok_or(DescriptorErr::Overflow)? = u8::try_from(self.index).map_err(|_| DescriptorErr::Overflow)?; @@ -551,6 +655,7 @@ mod test_string_descriptor_writter { } } +/// A DFU functional descriptor. pub struct DfuFunctionalDescriptor { /// New firmware can be received from the host pub can_download: bool, @@ -567,9 +672,11 @@ pub struct DfuFunctionalDescriptor { pub transfer_size: u16, } impl DfuFunctionalDescriptor { + /// Returns the total size of the descriptor. pub const fn total_size(&self) -> usize { 9 } + /// Serializes the DFU functional descriptor. pub const fn serialize(&self, dest: &mut [u8], offset: usize) { const fn bit(index: u8, val: bool) -> u8 { (if val { 1 } else { 0 }) << index @@ -595,14 +702,19 @@ impl DfuFunctionalDescriptor { } } +/// A raw class-specific functional descriptor. pub struct RawFunctionalDescriptor { + /// The type of the descriptor. pub descriptor_type: u8, + /// The raw content of the descriptor. pub content: &'static [u8], } impl RawFunctionalDescriptor { + /// Returns the total size of the descriptor. pub const fn total_size(&self) -> usize { self.content.len() + 2 } + /// Serializes the raw functional descriptor. pub const fn serialize(&self, dest: &mut [u8], offset: usize) { dest[offset] = self.total_size() as u8; dest[offset + 1] = self.descriptor_type; @@ -614,25 +726,30 @@ impl RawFunctionalDescriptor { } } -// This should be a trait, but traits can't be used from const functions :( +/// Represents a class-specific functional descriptor. pub enum FunctionalDescriptor { + /// DFU functional descriptor. Dfu(DfuFunctionalDescriptor), + /// Raw class-specific functional descriptor. Raw(RawFunctionalDescriptor), } impl FunctionalDescriptor { + /// Creates a raw functional descriptor. pub const fn raw(descriptor_type: u8, content: &'static [u8]) -> Self { Self::Raw(RawFunctionalDescriptor { descriptor_type, content, }) } + /// Returns the total size of the descriptor. pub const fn total_size(&self) -> usize { match self { Self::Dfu(dfu) => dfu.total_size(), Self::Raw(raw) => raw.total_size(), } } + /// Serializes the functional descriptor. #[allow(clippy::identity_op)] pub const fn serialize(&self, dest: &mut [u8], offset: usize) { assert!(offset + self.total_size() <= dest.len()); diff --git a/hal/blocking/usb/driver.rs b/hal/blocking/usb/driver.rs index 43161841..dae27dba 100644 --- a/hal/blocking/usb/driver.rs +++ b/hal/blocking/usb/driver.rs @@ -1,3 +1,8 @@ +//! USB peripheral driver traits and events. +//! +//! This module defines the interface between the USB protocol stack and the +//! hardware-specific peripheral driver. + use aligned::Aligned; use aligned::A4; use core::mem::MaybeUninit; @@ -6,83 +11,112 @@ use crate::SetupPacket; /// A trait implemented by drivers for USB peripheral controllers. pub trait UsbDriver { + /// The maximum packet size supported by the hardware (typically 64 bytes). const MAX_PACKET_SIZE: usize; + /// The type of packet returned by the driver. type Packet<'a>: UsbPacket where Self: 'a; /// Store data in a peripheral buffer that will be transferred to the host - /// when it requests data from the IN endpoint at `endpoint_idx`. If - /// zlp=true and `data.len()` is a multiple of MAX_PACKET_SIZE, - /// send a zero-length packet after sending all the data. + /// when it requests data from the IN endpoint at `endpoint_idx`. + /// + /// If `zlp` is true and `data.len()` is a multiple of `MAX_PACKET_SIZE`, + /// the driver should send a zero-length packet (ZLP) after all data has + /// been acknowledged. /// /// The return value is the number of bytes that were copied into the - /// peripheral buffer. It will be either a multiple of MAX_PACKET_SIZE, or `data.len()`. + /// peripheral buffer. It will be either a multiple of `MAX_PACKET_SIZE`, + /// or `data.len()`. /// - /// This function may fault or panic if endpoint_idx is invalid, or the hardware is misbehaving. + /// This function may fault or panic if `endpoint_idx` is invalid, or if the + /// hardware is in an invalid state. fn transfer_in(&mut self, endpoint_idx: u8, data: &Aligned, zlp: bool) -> usize; - /// Stalls an input endpoint. Note: the driver will automatically unstall all endpoints upon a USB reset or a new SETUP packet. + /// Stalls or unstalls an endpoint. + /// + /// Note: the driver will automatically unstall all endpoints upon a USB + /// reset or upon receiving a new SETUP packet on Endpoint 0. fn stall(&mut self, endpoint_num: u8, stalled: bool); - /// Returns whether the endpoint is stalled. + /// Returns whether the specified endpoint is currently stalled. fn is_stalled(&mut self, endpoint_num: u8) -> bool; - /// Sets the address the peripheral responds to. The USB stack must call - /// this function in response to a SET_ADDRESS control request on endpoint 0. + /// Sets the address the peripheral responds to. + /// + /// The USB stack must call this function in response to a `SET_ADDRESS` + /// control request on Endpoint 0. fn set_address(&mut self, address: u8); - /// Polls the driver for an event. When a USB interrupt occurs, the USB - /// stack should call this function repeatedly until it returns None and - /// process the returned events. + /// Polls the driver for a USB event. + /// + /// When a USB interrupt occurs, the USB stack should call this function + /// repeatedly until it returns `None`. fn poll(&mut self) -> Option>>; } +/// A trait representing a received USB packet. pub trait UsbPacket { - /// The endpoint the packet was received on. + /// Returns the index of the endpoint the packet was received on. fn endpoint_index(&self) -> usize; - /// The length of the packet in bytes + /// Returns the length of the packet data in bytes. fn len(&self) -> usize; - /// Copy the packet data from the peripheral buffer into SRAM. Will fault if - /// `self.len()` > `dest.len()`. + /// Copies the packet data from the peripheral buffer into system memory. + /// + /// This method allows copying into uninitialized memory. + /// Will fault if `self.len() > dest.len() * 4`. fn copy_to_uninit(self, dest: &mut [MaybeUninit]) -> &[u8]; - /// Copy the packet data from the peripheral buffer into SRAM. + /// Copies the packet data from the peripheral buffer into system memory. + /// + /// Will fault if `self.len() > dest.len() * 4`. fn copy_to(self, dest: &mut [u32]) -> &[u8]; + /// Returns `true` if the packet is empty (zero-length). fn is_empty(&self) -> bool { self.len() == 0 } } +/// Events that can be reported by a USB driver. pub enum UsbEvent { - /// A SETUP packet has been received from the host. It can be read with TPacket::copy_to()... + /// A SETUP packet has been received from the host. SetupPacket { + /// The decoded SETUP packet. pkt: SetupPacket, + /// The endpoint index (always 0 for standard SETUP packets). endpoint: u8, }, - /// An OUT packet has been received from the host. It can be read with TPacket::copy_to()... + /// An OUT packet has been received from the host. DataOutPacket(TPacket), - /// A packet has been sent by the peripheral and an ACK has been received - /// from the host. This will have freed up some buffer space, so if the USB - /// stack has more data to send on this endpoint, it should attempt to - /// buffer it now with `UsbDriver::transfer_in()`. + /// A packet has been sent by the peripheral and acknowledged by the host. + /// + /// This indicates that buffer space is now available on the specified + /// endpoint for further `transfer_in` calls. PacketSent { + /// The index of the endpoint that sent the packet. endpoint: u32, }, + /// VBus presence detected. VBus, + /// VBus presence lost. VBusLost, + /// USB link is down. LinkDown, + /// USB link is up. LinkUp, + /// USB bus reset received. UsbReset, + /// USB bus suspend received. Suspend, + /// USB bus resume received. Resume, - // TODO: Put these into the global error namespace... + /// Unexpected buffer ID error. ErrorUnexpectedBufId, } diff --git a/hal/blocking/usb/lib.rs b/hal/blocking/usb/lib.rs index 02a6721c..5396ab42 100644 --- a/hal/blocking/usb/lib.rs +++ b/hal/blocking/usb/lib.rs @@ -1,5 +1,11 @@ #![cfg_attr(not(test), no_std)] +//! USB Hardware Abstraction Layer (HAL) for blocking I/O. +//! +//! This module provides the foundational types and traits for implementing +//! USB device drivers and protocol stacks. It includes definitions for +//! standard USB requests, setup packets, and descriptors. + mod descriptor; pub mod driver; @@ -10,11 +16,16 @@ pub use descriptor::*; // Big endian is dead; code in this file assumes little-endian const _: () = assert!(cfg!(target_endian = "little")); +/// Represents a USB control request. +/// +/// A request combines the `bmRequestType` and `bRequest` fields from a +/// USB SETUP packet into a single type-safe representation. #[derive(Clone, Copy, Eq, PartialEq)] #[repr(transparent)] pub struct Request(u16); #[allow(clippy::identity_op)] impl Request { + /// Creates a new USB request. pub const fn new( direction: Direction, ty: RequestType, @@ -28,15 +39,19 @@ impl Request { | ((request as u16) << 8), ) } + /// Returns the direction of the request (Host-to-Device or Device-to-Host). pub fn direction(&self) -> Direction { Direction::try_from((u32::from(self.0) >> 7) & 0x1).unwrap() } + /// Returns the type of the request (Standard, Class, Vendor, or Reserved). pub fn request_type(&self) -> RequestType { RequestType::try_from(u32::from((self.0 >> 5) & 0x3)).unwrap() } + /// Returns the recipient of the request (Device, Interface, Endpoint, or Other). pub fn recipient(&self) -> Recipient { Recipient::try_from(u32::from((self.0 >> 0) & 0x1f)).unwrap() } + /// Returns the specific request code. pub fn request(&self) -> u8 { u8::try_from((self.0 >> 8) & 0xff).unwrap() } @@ -55,102 +70,119 @@ impl ufmt::uDebug for Request { } } impl Request { + /// Standard DEVICE request to get the current status. pub const DEVICE_GET_STATUS: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, Recipient::Device, 0x00, ); + /// Standard DEVICE request to clear a feature. pub const DEVICE_CLEAR_FEATURE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Device, 0x01, ); + /// Standard DEVICE request to set a feature. pub const DEVICE_SET_FEATURE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Device, 0x03, ); + /// Standard DEVICE request to set the device address. pub const DEVICE_SET_ADDRESS: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Device, 0x05, ); + /// Standard DEVICE request to get a descriptor. pub const DEVICE_GET_DESCRIPTOR: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, Recipient::Device, 0x06, ); + /// Standard DEVICE request to set a descriptor. pub const DEVICE_SET_DESCRIPTOR: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Device, 0x07, ); + /// Standard DEVICE request to get the current configuration. pub const DEVICE_GET_CONFIGURATION: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, Recipient::Device, 0x08, ); + /// Standard DEVICE request to set the current configuration. pub const DEVICE_SET_CONFIGURATION: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Device, 0x09, ); + /// Standard INTERFACE request to get the current status. pub const INTERFACE_GET_STATUS: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, Recipient::Interface, 0x00, ); + /// Standard INTERFACE request to clear a feature. pub const INTERFACE_CLEAR_FEATURE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Interface, 0x01, ); + /// Standard INTERFACE request to set a feature. pub const INTERFACE_SET_FEATURE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Interface, 0x03, ); + /// Standard INTERFACE request to get the current interface setting. pub const INTERFACE_GET_INTERFACE: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, Recipient::Interface, 0x0a, ); + /// Standard INTERFACE request to set the interface setting. pub const INTERFACE_SET_INTERFACE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Interface, 0x0b, ); + /// Standard ENDPOINT request to get the current status. pub const ENDPOINT_GET_STATUS: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, Recipient::Endpoint, 0x00, ); + /// Standard ENDPOINT request to clear a feature. pub const ENDPOINT_CLEAR_FEATURE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Endpoint, 0x01, ); + /// Standard ENDPOINT request to set a feature. pub const ENDPOINT_SET_FEATURE: Self = Self::new( Direction::HostToDevice, RequestType::Standard, Recipient::Endpoint, 0x03, ); + /// Standard ENDPOINT request to synchronize frames. pub const ENDPOINT_SYNCH_FRAME: Self = Self::new( Direction::DeviceToHost, RequestType::Standard, @@ -188,10 +220,14 @@ mod request_tests { } } +/// Information about a USB descriptor request. #[derive(Clone, Copy, Eq, PartialEq, uDebug)] pub struct DescriptorInfo { + /// The index of the descriptor. pub index: u8, + /// The type of the descriptor. pub ty: DescriptorType, + /// The language ID (for string descriptors). pub lang: u16, } impl From<&SetupPacket> for DescriptorInfo { @@ -203,25 +239,36 @@ impl From<&SetupPacket> for DescriptorInfo { } } } + +/// Represents a standard USB SETUP packet. +/// +/// A SETUP packet is always 8 bytes long and is used for all control transfers +/// on Endpoint 0. #[derive(Clone, Copy)] #[repr(C)] pub struct SetupPacket { buf: [u32; 2], } impl SetupPacket { + /// Creates a new `SetupPacket` from two 32-bit words. pub fn new(buf: [u32; 2]) -> SetupPacket { SetupPacket { buf } } + /// Returns the control request information. pub fn request(&self) -> Request { Request(u16::try_from(self.buf[0] & 0xffff).unwrap()) } + /// Returns the `wValue` field of the SETUP packet. pub fn value(&self) -> u16 { u16::try_from((self.buf[0] >> 16) & 0xffff).unwrap() } + /// Returns the `wIndex` field of the SETUP packet. #[allow(clippy::identity_op)] pub fn index(&self) -> u16 { u16::try_from((self.buf[1] >> 0) & 0xffff).unwrap() } + /// Returns the `wLength` field of the SETUP packet, which indicates + /// the number of bytes to transfer in the data stage. pub fn length(&self) -> u16 { u16::try_from((self.buf[1] >> 16) & 0xffff).unwrap() } @@ -240,9 +287,12 @@ impl ufmt::uDebug for SetupPacket { } } +/// USB data transfer direction. #[derive(Clone, Copy, Eq, PartialEq, uDebug)] pub enum Direction { + /// Host to Device (OUT). HostToDevice = 0, + /// Device to Host (IN). DeviceToHost = 1, } impl From for u32 { @@ -261,11 +311,17 @@ impl TryFrom for Direction { } } } + +/// The type of a USB control request. #[derive(Clone, Copy, Eq, PartialEq, uDebug)] pub enum RequestType { + /// Standard USB request. Standard = 0, + /// Class-specific request. Class = 1, + /// Vendor-specific request. Vendor = 2, + /// Reserved for future use. Reserved = 3, } impl TryFrom for RequestType { @@ -286,11 +342,17 @@ impl From for u32 { val as u32 } } + +/// The intended recipient of a USB control request. #[derive(Clone, Copy, Eq, PartialEq, uDebug)] pub enum Recipient { + /// The device itself. Device = 0, + /// A specific interface on the device. Interface = 1, + /// A specific endpoint on the device. Endpoint = 2, + /// Other recipients (e.g., class-specific). Other = 3, Reserved4 = 4, Reserved5 = 5, @@ -369,14 +431,22 @@ impl From for u32 { val as u32 } } + +/// Standard USB descriptor types. #[derive(Clone, Copy, Eq, PartialEq)] pub struct DescriptorType(u8); impl DescriptorType { + /// Device descriptor. pub const DEVICE: Self = Self(1); + /// Configuration descriptor. pub const CONFIGURATION: Self = Self(2); + /// String descriptor. pub const STRING: Self = Self(3); + /// Interface descriptor. pub const INTERFACE: Self = Self(4); + /// Endpoint descriptor. pub const ENDPOINT: Self = Self(5); + /// Device qualifier descriptor. pub const DEVICE_QUALIFIER: Self = Self(6); } impl From for DescriptorType { diff --git a/protocol/usb/stack/lib.rs b/protocol/usb/stack/lib.rs index e590db66..6e7603af 100644 --- a/protocol/usb/stack/lib.rs +++ b/protocol/usb/stack/lib.rs @@ -1,3 +1,9 @@ +//! Generic USB protocol stack. +//! +//! This module provides the core logic for a USB device stack, including +//! Endpoint 0 control request handling, descriptor management, and +//! multi-packet transfer accumulation. + #![no_std] use aligned::Aligned; @@ -16,51 +22,81 @@ use zerocopy::IntoBytes; use pw_status::Error; +/// A trait for providing USB descriptors to the stack. +/// +/// Applications must implement this trait to define the device's identity +/// and capabilities. pub trait DescriptorSource { + /// Device descriptor bytes. const DEVICE_DESC_BYTES: &'static Aligned; + /// Configuration descriptor bytes (including interfaces and endpoints). const CONFIG_DESC_BYTES: &'static Aligned; + /// String descriptor 0 bytes (supported languages). const STRING_DESC_0_BYTES: &'static Aligned; + /// Device status bytes (2 bytes, usually [0, 0]). const DEVICE_STATUS: Aligned; + /// Returns a string descriptor by handle and language ID. fn get_string(&self, handle: StringHandle, lang: u16) -> Option>; + /// Returns the device status bytes. fn get_device_status(&self) -> &Aligned { &Self::DEVICE_STATUS } } +/// An empty aligned buffer. pub const EMPTY: &Aligned = &Aligned([]); +/// A simple implementation of USB Endpoint 0 (control endpoint). pub struct SimpleEp0 { new_address: Option, } +/// Indicates the result of running a USB action. #[derive(Copy, Clone, PartialEq, Eq)] pub enum UsbActionRun { + /// No operation was performed. NoOp, + /// The action has more data to transfer. HasMoreData, + /// The action is complete. Done, } +/// Actions to be performed on a USB driver. pub enum UsbAction<'a> { + /// No action. None, + /// Perform an IN transfer on the specified endpoint. TransferIn { + /// The endpoint index. endpoint: u8, + /// The data to transfer. data: &'a Aligned, - /// If zlp=true and `data.len()` is a multiple of MAX_PACKET_SIZE, - /// send a zero-length packet after sending all the data. + /// If true, send a zero-length packet (ZLP) if the data length + /// is a multiple of the maximum packet size. zlp: bool, }, + /// Stall both IN and OUT directions on the specified endpoint. StallInAndOut { + /// The endpoint index. endpoint: u8, }, + /// Set the device address. SetAddress { + /// The new device address. new_address: u8, }, + /// Get the status of an endpoint. GetEndpointStatus { + /// The endpoint index. endpoint: u8, }, + /// Set the stall status of an endpoint. SetEndpointStatus { + /// The endpoint index. endpoint: u8, + /// Whether to stall or unstall the endpoint. stall: bool, }, } @@ -68,6 +104,8 @@ impl<'a> UsbAction<'a> { const EP_CLEAR: Aligned = Aligned([0u8, 0]); const EP_HALTED: Aligned = Aligned([1u8, 0]); + /// Helper to create a TransferIn action for a control transfer, + /// or a StallInAndOut if the requested length is too small. #[inline(always)] #[track_caller] pub fn control_transfer_in_or_stall( @@ -87,6 +125,7 @@ impl<'a> UsbAction<'a> { } } } + /// Merges another action into this one. pub fn merge(&mut self, new_action: UsbAction<'a>) { match new_action { UsbAction::None => {} @@ -94,6 +133,7 @@ impl<'a> UsbAction<'a> { } } + /// Executes the action on the provided driver. pub fn run(&mut self, driver: &mut TDriver) -> UsbActionRun { match self { Self::None => return UsbActionRun::NoOp, @@ -136,14 +176,13 @@ impl<'a> UsbAction<'a> { } impl SimpleEp0 { + /// Creates a new `SimpleEp0` handler. pub fn new() -> Self { Self { new_address: None } } /// A helper function to process a driver UsbEvent. /// - /// This function returns the action that should be performed on the driver - /// (we can't take the driver as a parameter because the UsbPacket in the - /// event may capture the driver's lifetime). + /// This function returns the action that should be performed on the driver. pub fn handle_event<'a>( &mut self, ev: UsbEvent, @@ -165,7 +204,7 @@ impl SimpleEp0 { UsbAction::None } - /// Process a SETUP transfer, and return the action that should be performed. + /// Process a SETUP transfer and return the resulting action. fn handle_setup<'a, TDescriptorSource: DescriptorSource>( &mut self, setup_pkt: SetupPacket, @@ -267,10 +306,10 @@ pub struct Transfer { } impl Transfer { - // TODO: ckungler - This could be a const generic if we need to support - // other packet sizes. + /// Maximum packet size supported (fixed at 64 bytes). pub const MAX_PACKET_SIZE: usize = 64; + /// Creates a new `Transfer` buffer. pub fn new() -> Self { Self { buffer: [0; N], From 84ee0fdb0463bb49e51a403d51e363ee271d0305 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 15:20:25 -0700 Subject: [PATCH 09/46] feat: add support for unaligned USB data transfers Signed-off-by: Chris Frantz AI-assistant: Gemini --- hal/blocking/usb/driver.rs | 10 ++ protocol/usb/stack/lib.rs | 79 ++++++++++ target/earlgrey/drivers/usb_driver.rs | 117 ++++++++------- util/regcpy/regcpy.rs | 198 ++++++++++++++++++++++++++ 4 files changed, 355 insertions(+), 49 deletions(-) diff --git a/hal/blocking/usb/driver.rs b/hal/blocking/usb/driver.rs index dae27dba..c5c53c71 100644 --- a/hal/blocking/usb/driver.rs +++ b/hal/blocking/usb/driver.rs @@ -33,6 +33,11 @@ pub trait UsbDriver { /// hardware is in an invalid state. fn transfer_in(&mut self, endpoint_idx: u8, data: &Aligned, zlp: bool) -> usize; + /// Store data in a peripheral buffer that will be transferred to the host. + /// + /// This version accepts an unaligned data buffer. + fn transfer_in_unaligned(&mut self, endpoint_idx: u8, data: &[u8], zlp: bool) -> usize; + /// Stalls or unstalls an endpoint. /// /// Note: the driver will automatically unstall all endpoints upon a USB @@ -74,6 +79,11 @@ pub trait UsbPacket { /// Will fault if `self.len() > dest.len() * 4`. fn copy_to(self, dest: &mut [u32]) -> &[u8]; + /// Copies the packet data from the peripheral buffer into system memory. + /// + /// This version accepts an unaligned destination buffer. + fn copy_to_unaligned(self, dest: &mut [u8]) -> &[u8]; + /// Returns `true` if the packet is empty (zero-length). fn is_empty(&self) -> bool { self.len() == 0 diff --git a/protocol/usb/stack/lib.rs b/protocol/usb/stack/lib.rs index 6e7603af..992b0942 100644 --- a/protocol/usb/stack/lib.rs +++ b/protocol/usb/stack/lib.rs @@ -77,6 +77,16 @@ pub enum UsbAction<'a> { /// is a multiple of the maximum packet size. zlp: bool, }, + /// Perform an IN transfer on the specified endpoint using unaligned data. + TransferInUnaligned { + /// The endpoint index. + endpoint: u8, + /// The data to transfer. + data: &'a [u8], + /// If true, send a zero-length packet (ZLP) if the data length + /// is a multiple of the maximum packet size. + zlp: bool, + }, /// Stall both IN and OUT directions on the specified endpoint. StallInAndOut { /// The endpoint index. @@ -100,6 +110,57 @@ pub enum UsbAction<'a> { stall: bool, }, } + +impl PartialEq for UsbAction<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::None, Self::None) => true, + ( + Self::TransferIn { + endpoint: e1, + data: d1, + zlp: z1, + }, + Self::TransferIn { + endpoint: e2, + data: d2, + zlp: z2, + }, + ) => e1 == e2 && core::ptr::eq(*d1, *d2) && z1 == z2, + ( + Self::TransferInUnaligned { + endpoint: e1, + data: d1, + zlp: z1, + }, + Self::TransferInUnaligned { + endpoint: e2, + data: d2, + zlp: z2, + }, + ) => e1 == e2 && core::ptr::eq(*d1, *d2) && z1 == z2, + (Self::StallInAndOut { endpoint: e1 }, Self::StallInAndOut { endpoint: e2 }) => e1 == e2, + (Self::SetAddress { new_address: a1 }, Self::SetAddress { new_address: a2 }) => a1 == a2, + ( + Self::GetEndpointStatus { endpoint: e1 }, + Self::GetEndpointStatus { endpoint: e2 }, + ) => e1 == e2, + ( + Self::SetEndpointStatus { + endpoint: e1, + stall: s1, + }, + Self::SetEndpointStatus { + endpoint: e2, + stall: s2, + }, + ) => e1 == e2 && s1 == s2, + _ => false, + } + } +} +impl Eq for UsbAction<'_> {} + impl<'a> UsbAction<'a> { const EP_CLEAR: Aligned = Aligned([0u8, 0]); const EP_HALTED: Aligned = Aligned([1u8, 0]); @@ -152,6 +213,18 @@ impl<'a> UsbAction<'a> { return UsbActionRun::HasMoreData; } } + Self::TransferInUnaligned { + endpoint, + data, + zlp, + } => { + let bytes_transferred = driver.transfer_in_unaligned(*endpoint, data, *zlp); + if bytes_transferred < data.len() { + // We're not done yet... + *data = &data[bytes_transferred..]; + return UsbActionRun::HasMoreData; + } + } Self::SetAddress { new_address } => driver.set_address(*new_address), Self::StallInAndOut { endpoint } => { driver.stall((*endpoint) & 0x7f, true); @@ -389,6 +462,12 @@ pub mod testing { // 4-byte aligned. The subslice `&dest_bytes[..copy_len]` maintains this alignment. unsafe { core::mem::transmute::<&[u8], &Aligned>(&dest_bytes[..copy_len]) } } + + fn copy_to_unaligned(self, dest: &mut [u8]) -> &[u8] { + let copy_len = self.data.len().min(dest.len()); + dest[..copy_len].copy_from_slice(&self.data[..copy_len]); + &dest[..copy_len] + } } } diff --git a/target/earlgrey/drivers/usb_driver.rs b/target/earlgrey/drivers/usb_driver.rs index 0402471d..3bd7551f 100644 --- a/target/earlgrey/drivers/usb_driver.rs +++ b/target/earlgrey/drivers/usb_driver.rs @@ -3,7 +3,7 @@ use aligned::A4; use aligned::Aligned; use core::cmp::min; -use util_regcpy::copy_to_reg_array; +use util_regcpy::{copy_to_reg_array, copy_to_reg_array_unaligned, copy_from_reg_array_unaligned}; use console::traceln; use hal_usb::SetupPacket; use hal_usb::driver::UsbDriver; @@ -15,7 +15,6 @@ const MAX_PACKET_SIZE: usize = 64; const BUFFER_SLOT_SIZE_WORDS: usize = MAX_PACKET_SIZE / 4; const BUFFER_SLOT_COUNT: usize = 32; -const EMPTY_A4: &Aligned = &Aligned([]); use buf_pool::BufId; use buf_pool::BufPool; @@ -68,6 +67,11 @@ impl UsbPacket for PacketHandle { } &dest.as_bytes()[..min(self.len(), dest.as_bytes().len())] } + + fn copy_to_unaligned(self, dest: &mut [u8]) -> &[u8] { + copy_from_reg_array_unaligned(dest, &self.data); + &dest[..min(self.len(), dest.len())] + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -319,52 +323,9 @@ impl Usb { .rxenable_out0() .modify(|w| bit_setval(u32::from(w), ep_num.into(), true).into()); } -} - -#[inline(always)] -fn bit_setval(bits: u32, index: usize, value: bool) -> u32 { - let mask = 1 << index; - if value { bits | mask } else { bits & !mask } -} - -impl UsbDriver for Usb { - const MAX_PACKET_SIZE: usize = 64; - type Packet<'a> = PacketHandle>; - #[inline(always)] - fn stall(&mut self, endpoint_num: u8, stalled: bool) { - if endpoint_num & 0x80 != 0 { - self.mmio - .regs_mut() - .in_stall0() - .modify(|w| bit_setval(u32::from(w), (endpoint_num & 0x0f).into(), stalled).into()); - } else { - self.mmio - .regs_mut() - .out_stall0() - .modify(|w| bit_setval(u32::from(w), (endpoint_num & 0x0f).into(), stalled).into()); - } - } - - #[inline(always)] - fn is_stalled(&mut self, endpoint_num: u8) -> bool { - if endpoint_num & 0x80 != 0 { - u32::from(self.mmio - .regs() - .in_stall0() - .read()) & (1 << (endpoint_num & 0x0f)) != 0 - - } else { - u32::from(self.mmio - .regs() - .out_stall0() - .read()) & (1 << (endpoint_num & 0x0f)) != 0 - } - } - - /// Store data in peripheral buffer that will be transferred when the host requests it. - #[inline(never)] - fn transfer_in(&mut self, endpoint: u8, mut data: &Aligned, zlp: bool) -> usize { + /// Common internal implementation for transfer_in and transfer_in_unaligned. + fn transfer_in_internal(&mut self, endpoint: u8, mut data: &[u8], zlp: bool, aligned: bool) -> usize { let mut bytes_queued = 0; let zlp = zlp && (data.len() % MAX_PACKET_SIZE) == 0; loop { @@ -381,7 +342,7 @@ impl UsbDriver for Usb { if pkt.len() == MAX_PACKET_SIZE { data = &data[pkt.len()..]; } else { - data = EMPTY_A4; + data = &[]; } let buf_pool = self.buf_pools_in.get_mut(usize::from(endpoint)).unwrap(); @@ -406,7 +367,11 @@ impl UsbDriver for Usb { // Shouldn't fail to get buffer offset unreachable!(); }; - copy_to_reg_array(&buffer, pkt); + if aligned { + copy_to_reg_array(&buffer, unsafe { core::mem::transmute(pkt) }); + } else { + copy_to_reg_array_unaligned(&buffer, pkt); + } match self.transmit_queues.queue( endpoint.into(), @@ -435,6 +400,60 @@ impl UsbDriver for Usb { } bytes_queued } +} + +#[inline(always)] +fn bit_setval(bits: u32, index: usize, value: bool) -> u32 { + let mask = 1 << index; + if value { bits | mask } else { bits & !mask } +} + +impl UsbDriver for Usb { + const MAX_PACKET_SIZE: usize = 64; + type Packet<'a> = PacketHandle>; + + #[inline(always)] + fn stall(&mut self, endpoint_num: u8, stalled: bool) { + if endpoint_num & 0x80 != 0 { + self.mmio + .regs_mut() + .in_stall0() + .modify(|w| bit_setval(u32::from(w), (endpoint_num & 0x0f).into(), stalled).into()); + } else { + self.mmio + .regs_mut() + .out_stall0() + .modify(|w| bit_setval(u32::from(w), (endpoint_num & 0x0f).into(), stalled).into()); + } + } + + #[inline(always)] + fn is_stalled(&mut self, endpoint_num: u8) -> bool { + if endpoint_num & 0x80 != 0 { + u32::from(self.mmio + .regs() + .in_stall0() + .read()) & (1 << (endpoint_num & 0x0f)) != 0 + + } else { + u32::from(self.mmio + .regs() + .out_stall0() + .read()) & (1 << (endpoint_num & 0x0f)) != 0 + } + } + + /// Store data in peripheral buffer that will be transferred when the host requests it. + #[inline(never)] + fn transfer_in(&mut self, endpoint: u8, data: &Aligned, zlp: bool) -> usize { + self.transfer_in_internal(endpoint, data.as_ref(), zlp, true) + } + + #[inline(never)] + fn transfer_in_unaligned(&mut self, endpoint_idx: u8, data: &[u8], zlp: bool) -> usize { + self.transfer_in_internal(endpoint_idx, data, zlp, false) + } + fn set_address(&mut self, address: u8) { self.mmio .regs_mut() diff --git a/util/regcpy/regcpy.rs b/util/regcpy/regcpy.rs index f0f8b37d..417d2633 100644 --- a/util/regcpy/regcpy.rs +++ b/util/regcpy/regcpy.rs @@ -76,6 +76,34 @@ pub fn copy_to_reg_array( reg.write(|_| last_word); } +#[inline(never)] +pub fn copy_to_reg_array_unaligned( + array: &ureg::Array< + LEN, + ureg::RegRef< + impl ureg::WritableReg + ureg::ResettableReg, + impl ureg::MmioMut + Copy, + >, + >, + src: &[u8], +) { + let (words, rem_bytes): (&[Unalign], &[u8]) = FromBytes::ref_from_prefix(src).unwrap(); + + let words_to_copy = min(LEN, words.len()); + + #[allow(clippy::needless_range_loop)] // optimizes better + for i in 0..words_to_copy { + array.at(i).write(|_| words[i].get()); + } + let Some(reg) = array.get(words.len()) else { + return; + }; + let Some(last_word) = last_word(rem_bytes) else { + return; + }; + reg.write(|_| last_word); +} + #[inline(never)] pub fn copy_from_reg( dest: &mut Aligned, @@ -115,6 +143,28 @@ pub fn copy_from_reg_array( } } +#[inline(never)] +pub fn copy_from_reg_array_unaligned( + dest: &mut [u8], + array: &ureg::Array< + LEN, + ureg::RegRef, impl ureg::Mmio + Copy>, + >, +) { + let (words, rem_bytes): (&mut [Unalign], &mut [u8]) = + FromBytes::mut_from_prefix(dest).unwrap(); + let words_to_copy = min(LEN, words.len()); + + #[allow(clippy::needless_range_loop)] + for i in 0..words_to_copy { + words[i].set(array.at(i).read()); + } + + if words_to_copy < LEN { + set_rem_bytes(rem_bytes, || array.at(words_to_copy).read()); + } +} + #[inline(never)] pub fn copy_from_reg_unaligned( dest: &mut [u8], @@ -564,6 +614,85 @@ mod test { ); } + #[test] + #[rustfmt::skip] + pub fn test_copy_to_reg_array_unaligned() { + let mmio = FakeMmio::default(); + let reg_array = unsafe { + ureg::Array::<3, RegRef, &FakeMmio>>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_to_reg_array_unaligned(®_array, &[]); + assert_eq!( + mmio.take_log(), + vec![], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_0012)], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0000_3412)], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34, 0x56]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x0056_3412)], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34, 0x56, 0x78]); + assert_eq!( + mmio.take_log(), + vec![(0x4040_4040, 0x7856_3412)], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34, 0x56, 0x78, 0x9a]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0x0000_009a), + ], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0x0000_bc9a), + ], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0xf0de_bc9a), + ], + ); + + copy_to_reg_array_unaligned(®_array, &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0xdd]); + assert_eq!( + mmio.take_log(), + vec![ + (0x4040_4040, 0x7856_3412), + (0x4040_4044, 0xf0de_bc9a), + (0x4040_4048, 0x0000_00dd), + ], + ); + } + #[test] #[rustfmt::skip] pub fn test_copy_from_reg() { @@ -706,6 +835,75 @@ mod test { assert_eq!(&result, &Aligned([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0xDD])); } + #[test] + #[rustfmt::skip] + pub fn test_copy_from_reg_array_unaligned() { + let mmio = FakeMmio::default(); + let reg_array = unsafe { + ureg::Array::<3, RegRef, &FakeMmio>>::new_with_mmio( + 0x4040_4040 as *mut _, + &mmio, + ) + }; + + copy_from_reg_array_unaligned(&mut [], ®_array); + assert_eq!( + mmio.take_log(), + vec![], + ); + + mmio.fifo_push(0x4040_4040, 0x0000_0012); + let mut result = [0_u8; 1]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12]); + + mmio.fifo_push(0x4040_4040, 0x0000_3412); + let mut result = [0_u8; 2]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34]); + + mmio.fifo_push(0x4040_4040, 0x0056_3412); + let mut result = [0_u8; 3]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56]); + + mmio.fifo_push(0x4040_4040, 0x78563412); + let mut result = [0_u8; 4]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78]); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0x0000_009A); + let mut result = [0_u8; 5]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9A]); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0x0000_BC9A); + let mut result = [0_u8; 6]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0x00DE_BC9A); + let mut result = [0_u8; 7]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE]); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0xF0DE_BC9A); + let mut result = [0_u8; 8]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]); + + mmio.fifo_push(0x4040_4040, 0x78563412); + mmio.fifo_push(0x4040_4044, 0xF0DE_BC9A); + mmio.fifo_push(0x4040_4048, 0x0000_00DD); + let mut result = [0_u8; 9]; + copy_from_reg_array_unaligned(&mut result, ®_array); + assert_eq!(result, [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0xDD]); + } + #[test] pub fn test_copy_from_reg_unaligned() { let addr: usize = 0x4040_4040; From f940c2f282c248ee7045f4da8d306c2d4b07285f Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 15:27:38 -0700 Subject: [PATCH 10/46] doc: add safety comments for internal USB transfer methods Signed-off-by: Chris Frantz AI-assistant: Gemini --- target/earlgrey/drivers/usb_driver.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/target/earlgrey/drivers/usb_driver.rs b/target/earlgrey/drivers/usb_driver.rs index 3bd7551f..4084b798 100644 --- a/target/earlgrey/drivers/usb_driver.rs +++ b/target/earlgrey/drivers/usb_driver.rs @@ -325,7 +325,13 @@ impl Usb { } /// Common internal implementation for transfer_in and transfer_in_unaligned. - fn transfer_in_internal(&mut self, endpoint: u8, mut data: &[u8], zlp: bool, aligned: bool) -> usize { + /// + /// # Safety + /// + /// If `aligned` is true, `data` MUST be 4-byte aligned. Failure to ensure + /// alignment will result in undefined behavior when the data is transmuted + /// to an aligned reference. + unsafe fn transfer_in_internal(&mut self, endpoint: u8, mut data: &[u8], zlp: bool, aligned: bool) -> usize { let mut bytes_queued = 0; let zlp = zlp && (data.len() % MAX_PACKET_SIZE) == 0; loop { @@ -368,6 +374,8 @@ impl Usb { unreachable!(); }; if aligned { + // SAFETY: The caller of transfer_in_internal has guaranteed that 'data' + // is 4-byte aligned when the 'aligned' flag is set. copy_to_reg_array(&buffer, unsafe { core::mem::transmute(pkt) }); } else { copy_to_reg_array_unaligned(&buffer, pkt); @@ -446,12 +454,15 @@ impl UsbDriver for Usb { /// Store data in peripheral buffer that will be transferred when the host requests it. #[inline(never)] fn transfer_in(&mut self, endpoint: u8, data: &Aligned, zlp: bool) -> usize { - self.transfer_in_internal(endpoint, data.as_ref(), zlp, true) + // SAFETY: Aligned is guaranteed to be 4-byte aligned. + unsafe { self.transfer_in_internal(endpoint, data.as_ref(), zlp, true) } } #[inline(never)] fn transfer_in_unaligned(&mut self, endpoint_idx: u8, data: &[u8], zlp: bool) -> usize { - self.transfer_in_internal(endpoint_idx, data, zlp, false) + // SAFETY: The 'aligned' flag is set to false, so transfer_in_internal + // will use the unaligned copy helper. + unsafe { self.transfer_in_internal(endpoint_idx, data, zlp, false) } } fn set_address(&mut self, address: u8) { From 81e01946a844affa75ff24ea77da0c1ab88007a4 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 22 Apr 2026 13:58:58 -0700 Subject: [PATCH 11/46] usb_driver --- target/earlgrey/drivers/usb_driver.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/target/earlgrey/drivers/usb_driver.rs b/target/earlgrey/drivers/usb_driver.rs index 4084b798..26d04580 100644 --- a/target/earlgrey/drivers/usb_driver.rs +++ b/target/earlgrey/drivers/usb_driver.rs @@ -1,3 +1,6 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + #![no_std] use aligned::A4; @@ -376,7 +379,7 @@ impl Usb { if aligned { // SAFETY: The caller of transfer_in_internal has guaranteed that 'data' // is 4-byte aligned when the 'aligned' flag is set. - copy_to_reg_array(&buffer, unsafe { core::mem::transmute(pkt) }); + copy_to_reg_array(&buffer, unsafe { core::mem::transmute::<&[u8], &aligned::Aligned>(pkt) }); } else { copy_to_reg_array_unaligned(&buffer, pkt); } From ddc1773b1fdb5d111729fe46b12304dfae7f2bf4 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 14:38:31 -0700 Subject: [PATCH 12/46] feat: modularize USB stack and separate CDC-ACM logic Signed-off-by: Chris Frantz AI-assistant: Gemini --- protocol/usb/stack/lib.rs | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/protocol/usb/stack/lib.rs b/protocol/usb/stack/lib.rs index 992b0942..856e8225 100644 --- a/protocol/usb/stack/lib.rs +++ b/protocol/usb/stack/lib.rs @@ -52,6 +52,18 @@ pub struct SimpleEp0 { new_address: Option, } +/// A trait for modular USB class implementations. +pub trait UsbClass { + /// Attempt to handle a USB event. + /// + /// If the event is handled by this class, it returns `Ok(UsbAction)`. + /// Otherwise, it returns the original event in `Err`. + fn handle_event<'a, P: UsbPacket>( + &'a mut self, + event: UsbEvent

, + ) -> Result, UsbEvent

>; +} + /// Indicates the result of running a USB action. #[derive(Copy, Clone, PartialEq, Eq)] pub enum UsbActionRun { @@ -256,25 +268,23 @@ impl SimpleEp0 { /// A helper function to process a driver UsbEvent. /// /// This function returns the action that should be performed on the driver. - pub fn handle_event<'a>( + pub fn handle_event<'a, P: UsbPacket>( &mut self, - ev: UsbEvent, + ev: UsbEvent

, descriptor_source: &'a impl DescriptorSource, - ) -> UsbAction<'a> { + ) -> Result, UsbEvent

> { match ev { - UsbEvent::SetupPacket { endpoint, pkt } => { - if endpoint == 0 { - return self.handle_setup(pkt, descriptor_source); - } - } - UsbEvent::PacketSent { endpoint } => { - if endpoint == 0 { - return self.handle_packet_sent(); + UsbEvent::SetupPacket { endpoint, pkt } if endpoint == 0 => { + use hal_usb::RequestType; + if pkt.request().request_type() == RequestType::Standard { + Ok(self.handle_setup(pkt, descriptor_source)) + } else { + Err(ev) } } - _ => {} + UsbEvent::PacketSent { endpoint } if endpoint == 0 => Ok(self.handle_packet_sent()), + _ => Err(ev), } - UsbAction::None } /// Process a SETUP transfer and return the resulting action. From d2051e6fe51a0c9b2dff83e410e52fe0bbbb121e Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 15:50:05 -0700 Subject: [PATCH 13/46] feat: implement efficient single-threaded RingBuffer Replaces the initial ProducerConsumerQueue with a more idiomatic RingBuffer implementation. Improvements include: - Corrected wrap-around length calculation. - Idiomatic push/pop API returning Result/Option. - Efficient slice-based push_slice with Result return. - Compile-time assertion for minimum buffer size (N > 1). - Comprehensive rustdoc for all public methods. Signed-off-by: Chris Frantz AI-assistant: Gemini --- util/ringbuffer/BUILD.bazel | 27 ++++++ util/ringbuffer/lib.rs | 179 ++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 util/ringbuffer/BUILD.bazel create mode 100644 util/ringbuffer/lib.rs diff --git a/util/ringbuffer/BUILD.bazel b/util/ringbuffer/BUILD.bazel new file mode 100644 index 00000000..7efd7865 --- /dev/null +++ b/util/ringbuffer/BUILD.bazel @@ -0,0 +1,27 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "ringbuffer", + srcs = [ + "lib.rs", + ], + crate_name = "util_ringbuffer", + edition = "2024", + deps = [ + ], +) + +rust_test( + name = "ringbuffer_test", + crate = ":ringbuffer", +) + +rust_doc( + name = "ringbuffer_doc", + crate = ":ringbuffer", +) diff --git a/util/ringbuffer/lib.rs b/util/ringbuffer/lib.rs new file mode 100644 index 00000000..73c2dd84 --- /dev/null +++ b/util/ringbuffer/lib.rs @@ -0,0 +1,179 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(not(test), no_std)] + +/// A simple single-threaded ring buffer of type `T` with a backing array of size `N`. +/// +/// The effective capacity of this queue is `N - 1`. +/// +/// If producer == consumer, the queue is empty. +/// If (producer + 1) % N == consumer, the queue is full. +pub struct RingBuffer { + producer: usize, + consumer: usize, + data: [T; N], +} + +/// Creates a default empty `RingBuffer`. +/// +/// # Panics +/// +/// Panics at compile time if `N <= 1`. +impl Default for RingBuffer { + fn default() -> Self { + // The queue requires at least 2 slots to store 1 item (N-1 capacity). + // Using a const block to ensure this is checked at compile time. + const { assert!(N > 1, "RingBuffer size N must be greater than 1") }; + Self { + producer: 0, + consumer: 0, + data: [T::default(); N], + } + } +} + + +impl RingBuffer { + /// Pushes an item into the buffer. + /// + /// Returns `Ok(())` if the item was successfully added, or `Err(item)` if the buffer is full. + pub fn push(&mut self, item: T) -> Result<(), T> { + let next = (self.producer + 1) % N; + if next != self.consumer { + self.data[self.producer] = item; + self.producer = next; + Ok(()) + } else { + Err(item) + } + } + + /// Removes and returns the first item from the buffer. + /// + /// Returns `Some(item)` if the buffer is not empty, or `None` if it is. + pub fn pop(&mut self) -> Option { + if self.consumer != self.producer { + let item = self.data[self.consumer]; + self.consumer = (self.consumer + 1) % N; + Some(item) + } else { + None + } + } + + /// Returns the number of items currently in the buffer. + pub fn len(&self) -> usize { + (self.producer + N - self.consumer) % N + } + + /// Returns `true` if the buffer is empty. + pub fn is_empty(&self) -> bool { + self.producer == self.consumer + } + + /// Returns `true` if the buffer is full. + pub fn is_full(&self) -> bool { + (self.producer + 1) % N == self.consumer + } + + /// Pushes as many items from the slice into the buffer as possible. + /// + /// Returns `Ok(())` if all items were added, or `Err(&[T])` containing the + /// remaining unbuffered items if the buffer became full. + pub fn push_slice<'a>(&mut self, s: &'a [T]) -> Result<(), &'a [T]> { + let free = (self.consumer + N - self.producer - 1) % N; + let n = core::cmp::min(s.len(), free); + + let mut remaining = n; + let mut src_idx = 0; + + let chunk1 = core::cmp::min(remaining, N - self.producer); + if chunk1 > 0 { + self.data[self.producer..self.producer + chunk1].copy_from_slice(&s[src_idx..src_idx + chunk1]); + self.producer = (self.producer + chunk1) % N; + remaining -= chunk1; + src_idx += chunk1; + } + + if remaining > 0 { + self.data[0..remaining].copy_from_slice(&s[src_idx..src_idx + remaining]); + self.producer = remaining; + } + + if n < s.len() { + Err(&s[n..]) + } else { + Ok(()) + } + } + + /// Returns a slice containing the contiguous part of the buffered data. + /// + /// If the data wraps around the end of the backing array, this only returns + /// the first part. Callers should use `consume()` and `as_slice()` again to + /// retrieve the wrapped portion. + pub fn as_slice(&self) -> &[T] { + if self.consumer <= self.producer { + // The available slice is contiguous in the array. + &self.data[self.consumer..self.producer] + } else { + // Slices can't wrap around the end of the array, so give just the chunk we can give. + &self.data[self.consumer..] + } + } + + /// Advances the consumer pointer by `n` items, effectively removing them from the buffer. + /// + /// If `n` is greater than the current length, only the available items are consumed. + pub fn consume(&mut self, n: usize) { + let n = core::cmp::min(n, self.len()); + self.consumer = (self.consumer + n) % N; + } +} + +#[cfg(test)] +#[allow(clippy::bool_assert_comparison)] +mod tests { + use super::*; + + #[test] + fn test_push_pop() { + let mut q = RingBuffer::::default(); + + assert_eq!(q.push(b'b'), Ok(())); + assert_eq!(q.push(b'y'), Ok(())); + assert_eq!(q.push(b'e'), Ok(())); + assert_eq!(q.push(b'x'), Err(b'x')); // Overflow + assert_eq!(q.len(), 3); + assert_eq!(q.is_empty(), false); + assert_eq!(q.is_full(), true); + + assert_eq!(q.pop(), Some(b'b')); + assert_eq!(q.pop(), Some(b'y')); + assert_eq!(q.pop(), Some(b'e')); + assert_eq!(q.pop(), None); // No more items. + assert_eq!(q.len(), 0); + assert_eq!(q.is_empty(), true); + assert_eq!(q.is_full(), false); + } + + #[test] + fn test_as_slice() { + let mut q = RingBuffer::::default(); + assert_eq!(q.push_slice(b"Hello"), Ok(())); + + assert_eq!(q.as_slice(), b"Hello"); + q.consume(5); + assert_eq!(q.is_empty(), true); + + // This part will be wrapped around the end of the array, + // so we need to perform two `as_slice` ops to get the whole thing. + assert_eq!(q.push_slice(b"World"), Ok(())); + assert_eq!(q.as_slice(), b"Wor"); + q.consume(3); + assert_eq!(q.as_slice(), b"ld"); + q.consume(2); + assert_eq!(q.is_empty(), true); + } +} From 63f2a24bf3311dd4f0fd65d220b51ef7e68ace8d Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Fri, 27 Mar 2026 08:37:40 -0700 Subject: [PATCH 14/46] wip: usb-serial test Signed-off-by: Chris Frantz --- target/earlgrey/tests/usbserial/BUILD.bazel | 141 ++++++ target/earlgrey/tests/usbserial/system.json5 | 77 +++ target/earlgrey/tests/usbserial/target.rs | 29 ++ target/earlgrey/tests/usbserial/test_usb.rs | 502 +++++++++++++++++++ 4 files changed, 749 insertions(+) create mode 100644 target/earlgrey/tests/usbserial/BUILD.bazel create mode 100644 target/earlgrey/tests/usbserial/system.json5 create mode 100644 target/earlgrey/tests/usbserial/target.rs create mode 100644 target/earlgrey/tests/usbserial/test_usb.rs diff --git a/target/earlgrey/tests/usbserial/BUILD.bazel b/target/earlgrey/tests/usbserial/BUILD.bazel new file mode 100644 index 00000000..22359f98 --- /dev/null +++ b/target/earlgrey/tests/usbserial/BUILD.bazel @@ -0,0 +1,141 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:app_package.bzl", "app_package") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_binary( + name = "test_usb", + srcs = [ + "test_usb.rs", + ], + crate_features = ["trace"], + edition = "2024", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + ":app_test_usb", + "//hal/blocking/usb:hal_usb", + "//protocol/usb/stack", + "//target/earlgrey/drivers:usb_driver", + "//target/earlgrey/registers:pinmux", + "//target/earlgrey/registers:top_earlgrey", + "//target/earlgrey/registers:usbdev", + "//util/console", + "//util/error", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + "@rust_crates//:zerocopy", + "@rust_crates//:ufmt", + ], +) + +app_package( + name = "app_test_usb", + app_name = "test_usb", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], +) + +system_image( + name = "usb", + apps = [ + ":test_usb", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "usb_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":usb", +) diff --git a/target/earlgrey/tests/usbserial/system.json5 b/target/earlgrey/tests/usbserial/system.json5 new file mode 100644 index 00000000..e6dccb91 --- /dev/null +++ b/target/earlgrey/tests/usbserial/system.json5 @@ -0,0 +1,77 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "test_usb", + flash_size_bytes: 16384, + ram_size_bytes: 4096, + process: { + name: "test_uart_listener process", + objects: [ + { + name: "usbdev_interrupts", + type: "interrupt", + irqs: [ + { name: "usbdev_pkt_received", number: 135 }, + { name: "usbdev_pkt_sent", number: 136 }, + { name: "usbdev_disconnected", number: 137 }, + { name: "usbdev_host_lost", number: 138 }, + + { name: "usbdev_link_reset", number: 139 }, + { name: "usbdev_link_suspend", number: 140 }, + { name: "usbdev_link_resume", number: 141 }, + { name: "usbdev_av_out_empty", number: 142 }, + + { name: "usbdev_rx_full", number: 143 }, + { name: "usbdev_av_overflow", number: 144 }, + //{ name: "usbdev_link_in_err", number: 145 }, + { name: "usbdev_rx_crc_err", number: 146 }, + + { name: "usbdev_rx_pid_err", number: 147 }, + { name: "usbdev_rx_bitstuff_err", number: 148 }, + { name: "usbdev_frame", number: 149 }, + //{ name: "usbdev_powered", number: 150 }, + + //{ name: "usbdev_link_out_err", number: 151 }, + { name: "usbdev_av_setup_empty", number: 152 }, + ], + }, + ], + memory_mappings: [ + { + name: "usbdev", + type: "device", + start_address: 0x40320000, + size_bytes: 0x1000, + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000, + } + + ], + threads: [ + { + name: "usb thread", + stack_size_bytes: 2048, + }, + ], + }, + }, + ], +} diff --git a/target/earlgrey/tests/usbserial/target.rs b/target/earlgrey/tests/usbserial/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/tests/usbserial/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/usbserial/test_usb.rs b/target/earlgrey/tests/usbserial/test_usb.rs new file mode 100644 index 00000000..6020a9f4 --- /dev/null +++ b/target/earlgrey/tests/usbserial/test_usb.rs @@ -0,0 +1,502 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +#![allow(dead_code)] + +use app_test_usb::{handle, signals}; +use ufmt::derive::uDebug; +use util_error::{ErrorCode, KERNEL_ERROR_UNKNOWN}; +//use userspace::syscall::Signals; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use aligned::{Aligned, A4}; +use hal_usb::driver::{UsbDriver, UsbEvent, UsbPacket}; +use hal_usb::{Direction, Recipient, Request, RequestType, SetupPacket, StringDescriptorRef}; + +use usb_driver::{EpIn, EpOut, UsbConfig}; +use usb_stack::{ + //UsbActionRun, + DescriptorSource, + UsbAction, + EMPTY, +}; + +const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); +const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); +const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); +const USB_CDC_COMM_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); +const USB_CDC_DATA_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(5); + +const USB_CLASS_CDC: u8 = 0x02; + +const USB_CLASS_CDC_DATA: u8 = 0x0a; +const CDC_SUBCLASS_ACM: u8 = 0x02; +const CDC_PROTOCOL_NONE: u8 = 0x00; + +const CS_INTERFACE: u8 = 0x24; +const CDC_TYPE_HEADER: u8 = 0x00; +const CDC_TYPE_ACM: u8 = 0x02; +const CDC_TYPE_UNION: u8 = 0x06; + +const REQ_SEND_ENCAPSULATED_COMMAND: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0x00, +); +const REQ_GET_ENCAPSULATED_COMMAND: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 0x01, +); +const REQ_SET_LINE_CODING: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0x20, +); +const REQ_GET_LINE_CODING: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 0x21, +); +const REQ_SET_CONTROL_LINE_STATE: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0x22, +); + +#[derive(Copy, Clone, PartialEq, Eq, Default, uDebug)] +#[repr(u8)] +pub enum StopBits { + #[default] + One = 0, + OnePointFive = 1, + Two = 2, +} + +impl From for StopBits { + fn from(x: u8) -> Self { + match x { + 0 => StopBits::One, + 1 => StopBits::OnePointFive, + 2 => StopBits::Two, + _ => StopBits::One, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Default, uDebug)] +#[repr(u8)] +pub enum Parity { + #[default] + None = 0, + Odd = 1, + Even = 2, + Mark = 3, + Space = 4, +} + +impl From for Parity { + fn from(x: u8) -> Self { + match x { + 0 => Parity::None, + 1 => Parity::Odd, + 2 => Parity::Even, + 3 => Parity::Mark, + 4 => Parity::Space, + _ => Parity::None, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, uDebug)] +#[repr(C)] +pub struct LineCoding { + pub data_rate: u32, + pub stop_bits: StopBits, + pub parity: Parity, + pub data_bits: u8, +} + +impl TryFrom<&[u8]> for LineCoding { + type Error = (); + fn try_from(data: &[u8]) -> Result { + if data.len() >= 7 { + Ok(LineCoding { + data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()), + stop_bits: StopBits::from(data[4]), + parity: Parity::from(data[5]), + data_bits: data[6], + }) + } else { + Err(()) + } + } +} + +impl Default for LineCoding { + fn default() -> Self { + LineCoding { + data_rate: 9600, + stop_bits: StopBits::default(), + parity: Parity::default(), + data_bits: 8, + } + } +} + +impl LineCoding { + pub fn as_bytes(&self) -> &[u8] { + let x = unsafe { core::mem::transmute::<&LineCoding, &[u8; 7]>(self) }; + &*x + } +} + +#[derive(Default)] +struct CdcAcmControl { + pub line_coding: LineCoding, + pub dtr: bool, + pub rts: bool, + pub comm_if: u8, + pub expecting_out: bool, +} + +impl CdcAcmControl { + fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> UsbAction<'a> { + if !(pkt.request().recipient() == Recipient::Interface + && (pkt.index() as u8) == self.comm_if) + { + return UsbAction::None; + } + match pkt.request() { + REQ_SEND_ENCAPSULATED_COMMAND => { + console::println!("CdcAcm: SEND_ENCAPSULATED_COMMAND"); + // We don't support encapsulated commands. Lie and accept it. + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + } + } + REQ_GET_ENCAPSULATED_COMMAND => { + console::println!("CdcAcm: GET_ENCAPSULATED_COMMAND"); + // We don'st support this, to reject. + UsbAction::StallInAndOut { endpoint: 0 } + } + REQ_SET_LINE_CODING => { + console::println!("CdcAcm: SET_LINE_CODING"); + self.expecting_out = true; + UsbAction::None + } + REQ_GET_LINE_CODING => { + console::println!("CdcAcm: GET_LINE_CODING"); + let data = unsafe { + // SAFETY: LineCoding has an alignment of 4. + core::mem::transmute::<&[u8], &Aligned>(self.line_coding.as_bytes()) + }; + UsbAction::TransferIn { + endpoint: 0, + data, + zlp: true, + } + } + REQ_SET_CONTROL_LINE_STATE => { + let dtr = (pkt.value() & 1) != 0; + let rts = (pkt.value() & 2) != 0; + console::println!("CdcAcm: SET_CONTROL_LINE_STATE: dtr={} rts={}", dtr, rts); + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + } + } + _ => UsbAction::StallInAndOut { endpoint: 0 }, + } + } + + fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> { + if !(pkt.endpoint_index() == 0 && self.expecting_out) { + return UsbAction::None; + } + let mut data = [0u32; 2]; + let buf = pkt.copy_to(&mut data); + match LineCoding::try_from(buf) { + Ok(x) => { + console::println!("line_coding = {:?}", x); + self.line_coding = x; + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + } + } + Err(_) => UsbAction::StallInAndOut { endpoint: 0 }, + } + } +} + +static DEVICE_DESC: hal_usb::DeviceDescriptor = hal_usb::DeviceDescriptor { + device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, + device_sub_class: 0x00, + device_protocol: 0x00, + max_packet_size: 64, + vendor_id: 0x18d1, + product_id: 0x023b, + device_release_num: 0x0100, + manufacturer: USB_VENDOR_HANDLE, + product: USB_PRODUCT_HANDLE, + serial_num: USB_SERIAL_HANDLE, +}; +const CONFIG_DESC: hal_usb::ConfigDescriptor = hal_usb::ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[ + hal_usb::InterfaceDescriptor { + name: USB_CDC_COMM_HANDLE, + interface_number: 0, + alternate_setting: 0, + interface_class: USB_CLASS_CDC, + interface_sub_class: CDC_SUBCLASS_ACM, + interface_protocol: CDC_PROTOCOL_NONE, + func_descs: &[ + hal_usb::FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_HEADER, 0x10, 0x01]), + hal_usb::FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_ACM, 0x02]), + hal_usb::FunctionalDescriptor::raw( + CS_INTERFACE, + &[ + CDC_TYPE_UNION, + 0, // comm_if + 1, // data_if + ], + ), + ], + endpoints: &[hal_usb::EndpointDescriptor { + direction: Direction::DeviceToHost, + endpoint_num: 1, + interval: 255, + max_packet_size: 8, + transfer_type: hal_usb::TransferType::Interrupt, + }], + }, + hal_usb::InterfaceDescriptor { + name: USB_CDC_DATA_HANDLE, + interface_number: 1, + alternate_setting: 0, + interface_class: USB_CLASS_CDC_DATA, + interface_sub_class: 0, + interface_protocol: CDC_PROTOCOL_NONE, + func_descs: &[], + endpoints: &[ + hal_usb::EndpointDescriptor { + direction: Direction::HostToDevice, + endpoint_num: 2, + interval: 0, + max_packet_size: 64, + transfer_type: hal_usb::TransferType::Bulk, + }, + hal_usb::EndpointDescriptor { + direction: Direction::DeviceToHost, + endpoint_num: 3, + interval: 0, + max_packet_size: 64, + transfer_type: hal_usb::TransferType::Bulk, + }, + ], + }, + ], +}; + +const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { + langs: &[ + // English - United States + 0x0409, + ], +}; + +const VENDOR_ID: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Google Inc.").as_ref(); +const PRODUCT_ID_DEFAULT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("Earlgrey").as_ref(); +const USB_COMM: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("CDC Comm Interface").as_ref(); +const USB_DATA: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("CDC Data Interface").as_ref(); + +struct MyDescriptors<'a> { + serial_desc_bytes: StringDescriptorRef<'a>, + product_desc_bytes: StringDescriptorRef<'a>, +} + +impl DescriptorSource for MyDescriptors<'_> { + const DEVICE_DESC_BYTES: &'static Aligned = &Aligned(DEVICE_DESC.serialize()); + const CONFIG_DESC_BYTES: &'static Aligned = + &Aligned(CONFIG_DESC.serialize::<{ CONFIG_DESC.total_size() }>()); + const STRING_DESC_0_BYTES: &'static Aligned = + &Aligned(STRING_DESC_0.serialize::<{ STRING_DESC_0.total_size() }>()); + const DEVICE_STATUS: Aligned = Aligned([1u8, 0]); + + fn get_string( + &self, + handle: hal_usb::StringHandle, + _lang: u16, + ) -> Option> { + match handle { + USB_VENDOR_HANDLE => Some(VENDOR_ID), + USB_PRODUCT_HANDLE => Some(self.product_desc_bytes), + USB_SERIAL_HANDLE => Some(self.serial_desc_bytes), + USB_CDC_COMM_HANDLE => Some(USB_COMM), + USB_CDC_DATA_HANDLE => Some(USB_DATA), + _ => None, + } + } +} + +const CONTROL_EP_OUT_NUM: u8 = 0; + +fn handle_usb() -> Result<(), ErrorCode> { + let mut serial_num_buffer = Aligned::([0_u8; 130]); + // TODO + //let mut product_desc_buffer = Aligned::([0_u8; 100]); + let descriptors = MyDescriptors { + serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned(&mut serial_num_buffer, b"12345") + .unwrap(), + product_desc_bytes: PRODUCT_ID_DEFAULT, + }; + const USB_EP_ACM_INT_IN: EpIn = EpIn { + num: 1, + buf_pool_size: 1, + }; + const USB_EP_ACM_OUT: EpOut = EpOut { + num: 2, + set_nak: false, + }; + const USB_EP_ACM_IN: EpIn = EpIn { + num: 3, + buf_pool_size: 1, + }; + + const USB_CONFIG: UsbConfig = + UsbConfig::new(&[USB_EP_ACM_INT_IN, USB_EP_ACM_IN], &[USB_EP_ACM_OUT]); + let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); + let mut ep0 = usb_stack::SimpleEp0::new(); + let mut cdc_acm = CdcAcmControl::default(); + let mut ep3_action = UsbAction::None; + + loop { + let wait_return = syscall::object_wait( + handle::USBDEV_INTERRUPTS, + signals::USBDEV_PKT_RECEIVED + | signals::USBDEV_PKT_SENT + | signals::USBDEV_DISCONNECTED + | signals::USBDEV_HOST_LOST + | signals::USBDEV_LINK_RESET + | signals::USBDEV_LINK_SUSPEND + | signals::USBDEV_LINK_RESUME + | signals::USBDEV_AV_OUT_EMPTY + | signals::USBDEV_RX_FULL + | signals::USBDEV_AV_OVERFLOW + //| signals::USBDEV_LINK_IN_ERR + | signals::USBDEV_RX_CRC_ERR + | signals::USBDEV_RX_PID_ERR + | signals::USBDEV_RX_BITSTUFF_ERR + | signals::USBDEV_FRAME + //| signals::USBDEV_POWERED + //| signals::USBDEV_LINK_OUT_ERR + | signals::USBDEV_AV_SETUP_EMPTY, + Instant::MAX, + )?; + + if wait_return.user_data != 0 { + pw_log::error!("Incorrect WaitReturn values"); + return Err(KERNEL_ERROR_UNKNOWN); + } + + let mut buffer = [0u32; 16]; + while let Some(event) = usb.poll() { + let mut ep0_action = match event { + UsbEvent::SetupPacket { pkt, endpoint } => { + if endpoint == 0 { + console::println!("SETUP: {:?}", pkt); + if pkt.request().recipient() == Recipient::Interface { + cdc_acm.handle_setup(pkt) + } else { + ep0.handle_event(event, &descriptors) + } + } else { + console::println!("Setup on bad EP {:?}", endpoint); + UsbAction::None + } + } + + UsbEvent::tataOutPacket(pkt) => match u8::try_from(pkt.endpoint_index()).unwrap() { + CONTROL_EP_OUT_NUM => { + console::println!("OUT on control ep"); + cdc_acm.handle_control_out(pkt) + } + 2 => { + let x = pkt.copy_to(&mut buffer); + let x = unsafe { core::str::from_utf8_unchecked(x) }; + console::println!("ACM data: {}", x); + ep3_action = UsbAction::TransferIn { + endpoint: 3, + data: unsafe { core::mem::transmute(x) }, + zlp: true, + }; + UsbAction::None + } + ep => { + console::println!("Unhandled OUT on EP {} len={}", ep, pkt.len()); + UsbAction::None + } + }, + UsbEvent::UsbReset => { + console::println!("USB reset"); + UsbAction::None + } + _ => ep0.handle_event(event, &descriptors), + }; + ep0_action.run(&mut usb); + ep3_action.run(&mut usb); + } + } +} + +fn usb_setup_pinmux() { + use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn}; + let mut pinmux = unsafe { pinmux::PinmuxAon::new() }; + + pinmux + .regs_mut() + .mio_periph_insel() + .at(PinmuxPeripheralIn::UsbdevSense as usize) + .modify(|_| (PinmuxInsel::ConstantOne as u32).into()); +} + +#[entry] +fn entry() -> ! { + // Since this is written as a test, shut down with the return status from `main()`. + usb_setup_pinmux(); + let ret = match handle_usb() { + Ok(()) => Ok(()), + Err(e) => { + pw_log::error!("Error {:x}", e.0.get()); + Err(pw_status::Error::Unknown) + } + }; + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} From 3a155604173637923882e1fb156d3955d23ec21b Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 14:38:31 -0700 Subject: [PATCH 15/46] feat: modularize USB stack and separate CDC-ACM logic Signed-off-by: Chris Frantz AI-assistant: Gemini --- protocol/usb/cdc_acm/BUILD.bazel | 14 + protocol/usb/cdc_acm/lib.rs | 249 ++++++++++++++++++ target/earlgrey/tests/usbserial/BUILD.bazel | 3 +- target/earlgrey/tests/usbserial/test_usb.rs | 271 +------------------- 4 files changed, 274 insertions(+), 263 deletions(-) create mode 100644 protocol/usb/cdc_acm/BUILD.bazel create mode 100644 protocol/usb/cdc_acm/lib.rs diff --git a/protocol/usb/cdc_acm/BUILD.bazel b/protocol/usb/cdc_acm/BUILD.bazel new file mode 100644 index 00000000..88a1a8d6 --- /dev/null +++ b/protocol/usb/cdc_acm/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "cdc_acm", + srcs = ["lib.rs"], + crate_name = "protocol_usb_cdc_acm", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/usb:hal_usb", + "//protocol/usb/stack", + "@rust_crates//:aligned", + ], +) diff --git a/protocol/usb/cdc_acm/lib.rs b/protocol/usb/cdc_acm/lib.rs new file mode 100644 index 00000000..9d85c796 --- /dev/null +++ b/protocol/usb/cdc_acm/lib.rs @@ -0,0 +1,249 @@ +//! CDC-ACM (Serial) USB class implementation. + +#![no_std] + +use aligned::{Aligned, A4}; +use hal_usb::driver::{UsbEvent, UsbPacket}; +use hal_usb::{Direction, Recipient, Request, RequestType, SetupPacket}; +use usb_stack::{UsbAction, UsbClass, EMPTY}; + +/// CDC-ACM specific requests. +pub const REQ_SEND_ENCAPSULATED_COMMAND: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0x00, +); +pub const REQ_GET_ENCAPSULATED_COMMAND: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 0x01, +); +pub const REQ_SET_LINE_CODING: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0x20, +); +pub const REQ_GET_LINE_CODING: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 0x21, +); +pub const REQ_SET_CONTROL_LINE_STATE: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0x22, +); + +#[derive(Copy, Clone, PartialEq, Eq, Default)] +#[repr(u8)] +pub enum StopBits { + #[default] + One = 0, + OnePointFive = 1, + Two = 2, +} + +impl From for StopBits { + fn from(x: u8) -> Self { + match x { + 0 => StopBits::One, + 1 => StopBits::OnePointFive, + 2 => StopBits::Two, + _ => StopBits::One, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Default)] +#[repr(u8)] +pub enum Parity { + #[default] + None = 0, + Odd = 1, + Even = 2, + Mark = 3, + Space = 4, +} + +impl From for Parity { + fn from(x: u8) -> Self { + match x { + 0 => Parity::None, + 1 => Parity::Odd, + 2 => Parity::Even, + 3 => Parity::Mark, + 4 => Parity::Space, + _ => Parity::None, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +#[repr(C)] +pub struct LineCoding { + pub data_rate: u32, + pub stop_bits: StopBits, + pub parity: Parity, + pub data_bits: u8, +} + +impl TryFrom<&[u8]> for LineCoding { + type Error = (); + fn try_from(data: &[u8]) -> Result { + if data.len() >= 7 { + Ok(LineCoding { + data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()), + stop_bits: StopBits::from(data[4]), + parity: Parity::from(data[5]), + data_bits: data[6], + }) + } else { + Err(()) + } + } +} + +impl Default for LineCoding { + fn default() -> Self { + LineCoding { + data_rate: 9600, + stop_bits: StopBits::default(), + parity: Parity::default(), + data_bits: 8, + } + } +} + +impl LineCoding { + pub fn as_bytes(&self) -> &[u8] { + let x = unsafe { core::mem::transmute::<&LineCoding, &[u8; 7]>(self) }; + &*x + } +} + +/// CDC-ACM class handler. +#[allow(dead_code)] +pub struct CdcAcm { + comm_if: u8, + data_if: u8, + out_ep: u8, + in_ep: u8, + line_coding: LineCoding, + expecting_control_out: bool, + rx_buffer: Aligned, +} + +impl CdcAcm { + pub fn new(comm_if: u8, data_if: u8, out_ep: u8, in_ep: u8) -> Self { + Self { + comm_if, + data_if, + out_ep, + in_ep, + line_coding: LineCoding::default(), + expecting_control_out: false, + rx_buffer: Aligned([0u32; 16]), + } + } + + fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> (UsbAction<'a>, bool) { + if !(pkt.request().recipient() == Recipient::Interface + && (pkt.index() as u8) == self.comm_if) + { + return (UsbAction::None, false); + } + + match pkt.request() { + REQ_SEND_ENCAPSULATED_COMMAND => ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + false, + ), + REQ_GET_ENCAPSULATED_COMMAND => (UsbAction::StallInAndOut { endpoint: 0 }, false), + REQ_SET_LINE_CODING => { + self.expecting_control_out = true; + (UsbAction::None, true) + } + REQ_GET_LINE_CODING => { + let data = unsafe { + core::mem::transmute::<&[u8], &Aligned>(self.line_coding.as_bytes()) + }; + ( + UsbAction::TransferIn { + endpoint: 0, + data, + zlp: true, + }, + false, + ) + } + REQ_SET_CONTROL_LINE_STATE => ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + false, + ), + _ => (UsbAction::StallInAndOut { endpoint: 0 }, false), + } + } + + fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> { + let mut data = [0u32; 2]; + let buf = pkt.copy_to(&mut data); + self.expecting_control_out = false; + match LineCoding::try_from(buf) { + Ok(x) => { + self.line_coding = x; + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + } + } + Err(_) => UsbAction::StallInAndOut { endpoint: 0 }, + } + } +} + +impl UsbClass for CdcAcm { + fn handle_event<'a, P: UsbPacket>( + &'a mut self, + event: UsbEvent

, + ) -> Result, UsbEvent

> { + match event { + UsbEvent::SetupPacket { pkt, endpoint } if endpoint == 0 => { + let (action, claimed) = self.handle_setup(pkt); + if action != UsbAction::None || claimed { + Ok(action) + } else { + Err(UsbEvent::SetupPacket { pkt, endpoint }) + } + } + UsbEvent::DataOutPacket(pkt) => { + if pkt.endpoint_index() == 0 && self.expecting_control_out { + Ok(self.handle_control_out(pkt)) + } else if pkt.endpoint_index() == self.out_ep as usize { + let buf = pkt.copy_to(self.rx_buffer.as_mut()); + Ok(UsbAction::TransferIn { + endpoint: self.in_ep, + data: unsafe { core::mem::transmute(buf) }, + zlp: true, + }) + } else { + Err(UsbEvent::DataOutPacket(pkt)) + } + } + _ => Err(event), + } + } +} diff --git a/target/earlgrey/tests/usbserial/BUILD.bazel b/target/earlgrey/tests/usbserial/BUILD.bazel index 22359f98..ed8226bd 100644 --- a/target/earlgrey/tests/usbserial/BUILD.bazel +++ b/target/earlgrey/tests/usbserial/BUILD.bazel @@ -22,6 +22,7 @@ rust_binary( deps = [ ":app_test_usb", "//hal/blocking/usb:hal_usb", + "//protocol/usb/cdc_acm", "//protocol/usb/stack", "//target/earlgrey/drivers:usb_driver", "//target/earlgrey/registers:pinmux", @@ -33,8 +34,8 @@ rust_binary( "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", "@rust_crates//:aligned", - "@rust_crates//:zerocopy", "@rust_crates//:ufmt", + "@rust_crates//:zerocopy", ], ) diff --git a/target/earlgrey/tests/usbserial/test_usb.rs b/target/earlgrey/tests/usbserial/test_usb.rs index 6020a9f4..36def0e8 100644 --- a/target/earlgrey/tests/usbserial/test_usb.rs +++ b/target/earlgrey/tests/usbserial/test_usb.rs @@ -6,23 +6,21 @@ #![allow(dead_code)] use app_test_usb::{handle, signals}; -use ufmt::derive::uDebug; use util_error::{ErrorCode, KERNEL_ERROR_UNKNOWN}; -//use userspace::syscall::Signals; use userspace::time::Instant; use userspace::{entry, syscall}; use aligned::{Aligned, A4}; -use hal_usb::driver::{UsbDriver, UsbEvent, UsbPacket}; -use hal_usb::{Direction, Recipient, Request, RequestType, SetupPacket, StringDescriptorRef}; +use hal_usb::driver::UsbDriver; +use hal_usb::{Direction, StringDescriptorRef}; use usb_driver::{EpIn, EpOut, UsbConfig}; use usb_stack::{ - //UsbActionRun, DescriptorSource, UsbAction, - EMPTY, + UsbClass, }; +use protocol_usb_cdc_acm::CdcAcm; const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); @@ -31,7 +29,6 @@ const USB_CDC_COMM_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); const USB_CDC_DATA_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(5); const USB_CLASS_CDC: u8 = 0x02; - const USB_CLASS_CDC_DATA: u8 = 0x0a; const CDC_SUBCLASS_ACM: u8 = 0x02; const CDC_PROTOCOL_NONE: u8 = 0x00; @@ -41,207 +38,6 @@ const CDC_TYPE_HEADER: u8 = 0x00; const CDC_TYPE_ACM: u8 = 0x02; const CDC_TYPE_UNION: u8 = 0x06; -const REQ_SEND_ENCAPSULATED_COMMAND: Request = Request::new( - Direction::HostToDevice, - RequestType::Class, - Recipient::Interface, - 0x00, -); -const REQ_GET_ENCAPSULATED_COMMAND: Request = Request::new( - Direction::DeviceToHost, - RequestType::Class, - Recipient::Interface, - 0x01, -); -const REQ_SET_LINE_CODING: Request = Request::new( - Direction::HostToDevice, - RequestType::Class, - Recipient::Interface, - 0x20, -); -const REQ_GET_LINE_CODING: Request = Request::new( - Direction::DeviceToHost, - RequestType::Class, - Recipient::Interface, - 0x21, -); -const REQ_SET_CONTROL_LINE_STATE: Request = Request::new( - Direction::HostToDevice, - RequestType::Class, - Recipient::Interface, - 0x22, -); - -#[derive(Copy, Clone, PartialEq, Eq, Default, uDebug)] -#[repr(u8)] -pub enum StopBits { - #[default] - One = 0, - OnePointFive = 1, - Two = 2, -} - -impl From for StopBits { - fn from(x: u8) -> Self { - match x { - 0 => StopBits::One, - 1 => StopBits::OnePointFive, - 2 => StopBits::Two, - _ => StopBits::One, - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Default, uDebug)] -#[repr(u8)] -pub enum Parity { - #[default] - None = 0, - Odd = 1, - Even = 2, - Mark = 3, - Space = 4, -} - -impl From for Parity { - fn from(x: u8) -> Self { - match x { - 0 => Parity::None, - 1 => Parity::Odd, - 2 => Parity::Even, - 3 => Parity::Mark, - 4 => Parity::Space, - _ => Parity::None, - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, uDebug)] -#[repr(C)] -pub struct LineCoding { - pub data_rate: u32, - pub stop_bits: StopBits, - pub parity: Parity, - pub data_bits: u8, -} - -impl TryFrom<&[u8]> for LineCoding { - type Error = (); - fn try_from(data: &[u8]) -> Result { - if data.len() >= 7 { - Ok(LineCoding { - data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()), - stop_bits: StopBits::from(data[4]), - parity: Parity::from(data[5]), - data_bits: data[6], - }) - } else { - Err(()) - } - } -} - -impl Default for LineCoding { - fn default() -> Self { - LineCoding { - data_rate: 9600, - stop_bits: StopBits::default(), - parity: Parity::default(), - data_bits: 8, - } - } -} - -impl LineCoding { - pub fn as_bytes(&self) -> &[u8] { - let x = unsafe { core::mem::transmute::<&LineCoding, &[u8; 7]>(self) }; - &*x - } -} - -#[derive(Default)] -struct CdcAcmControl { - pub line_coding: LineCoding, - pub dtr: bool, - pub rts: bool, - pub comm_if: u8, - pub expecting_out: bool, -} - -impl CdcAcmControl { - fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> UsbAction<'a> { - if !(pkt.request().recipient() == Recipient::Interface - && (pkt.index() as u8) == self.comm_if) - { - return UsbAction::None; - } - match pkt.request() { - REQ_SEND_ENCAPSULATED_COMMAND => { - console::println!("CdcAcm: SEND_ENCAPSULATED_COMMAND"); - // We don't support encapsulated commands. Lie and accept it. - UsbAction::TransferIn { - endpoint: 0, - data: EMPTY, - zlp: true, - } - } - REQ_GET_ENCAPSULATED_COMMAND => { - console::println!("CdcAcm: GET_ENCAPSULATED_COMMAND"); - // We don'st support this, to reject. - UsbAction::StallInAndOut { endpoint: 0 } - } - REQ_SET_LINE_CODING => { - console::println!("CdcAcm: SET_LINE_CODING"); - self.expecting_out = true; - UsbAction::None - } - REQ_GET_LINE_CODING => { - console::println!("CdcAcm: GET_LINE_CODING"); - let data = unsafe { - // SAFETY: LineCoding has an alignment of 4. - core::mem::transmute::<&[u8], &Aligned>(self.line_coding.as_bytes()) - }; - UsbAction::TransferIn { - endpoint: 0, - data, - zlp: true, - } - } - REQ_SET_CONTROL_LINE_STATE => { - let dtr = (pkt.value() & 1) != 0; - let rts = (pkt.value() & 2) != 0; - console::println!("CdcAcm: SET_CONTROL_LINE_STATE: dtr={} rts={}", dtr, rts); - UsbAction::TransferIn { - endpoint: 0, - data: EMPTY, - zlp: true, - } - } - _ => UsbAction::StallInAndOut { endpoint: 0 }, - } - } - - fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> { - if !(pkt.endpoint_index() == 0 && self.expecting_out) { - return UsbAction::None; - } - let mut data = [0u32; 2]; - let buf = pkt.copy_to(&mut data); - match LineCoding::try_from(buf) { - Ok(x) => { - console::println!("line_coding = {:?}", x); - self.line_coding = x; - UsbAction::TransferIn { - endpoint: 0, - data: EMPTY, - zlp: true, - } - } - Err(_) => UsbAction::StallInAndOut { endpoint: 0 }, - } - } -} - static DEVICE_DESC: hal_usb::DeviceDescriptor = hal_usb::DeviceDescriptor { device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, device_sub_class: 0x00, @@ -359,12 +155,8 @@ impl DescriptorSource for MyDescriptors<'_> { } } -const CONTROL_EP_OUT_NUM: u8 = 0; - fn handle_usb() -> Result<(), ErrorCode> { let mut serial_num_buffer = Aligned::([0_u8; 130]); - // TODO - //let mut product_desc_buffer = Aligned::([0_u8; 100]); let descriptors = MyDescriptors { serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned(&mut serial_num_buffer, b"12345") .unwrap(), @@ -387,8 +179,7 @@ fn handle_usb() -> Result<(), ErrorCode> { UsbConfig::new(&[USB_EP_ACM_INT_IN, USB_EP_ACM_IN], &[USB_EP_ACM_OUT]); let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); let mut ep0 = usb_stack::SimpleEp0::new(); - let mut cdc_acm = CdcAcmControl::default(); - let mut ep3_action = UsbAction::None; + let mut cdc_acm = CdcAcm::new(0, 1, 2, 3); loop { let wait_return = syscall::object_wait( @@ -403,13 +194,10 @@ fn handle_usb() -> Result<(), ErrorCode> { | signals::USBDEV_AV_OUT_EMPTY | signals::USBDEV_RX_FULL | signals::USBDEV_AV_OVERFLOW - //| signals::USBDEV_LINK_IN_ERR | signals::USBDEV_RX_CRC_ERR | signals::USBDEV_RX_PID_ERR | signals::USBDEV_RX_BITSTUFF_ERR | signals::USBDEV_FRAME - //| signals::USBDEV_POWERED - //| signals::USBDEV_LINK_OUT_ERR | signals::USBDEV_AV_SETUP_EMPTY, Instant::MAX, )?; @@ -419,52 +207,12 @@ fn handle_usb() -> Result<(), ErrorCode> { return Err(KERNEL_ERROR_UNKNOWN); } - let mut buffer = [0u32; 16]; while let Some(event) = usb.poll() { - let mut ep0_action = match event { - UsbEvent::SetupPacket { pkt, endpoint } => { - if endpoint == 0 { - console::println!("SETUP: {:?}", pkt); - if pkt.request().recipient() == Recipient::Interface { - cdc_acm.handle_setup(pkt) - } else { - ep0.handle_event(event, &descriptors) - } - } else { - console::println!("Setup on bad EP {:?}", endpoint); - UsbAction::None - } - } - - UsbEvent::tataOutPacket(pkt) => match u8::try_from(pkt.endpoint_index()).unwrap() { - CONTROL_EP_OUT_NUM => { - console::println!("OUT on control ep"); - cdc_acm.handle_control_out(pkt) - } - 2 => { - let x = pkt.copy_to(&mut buffer); - let x = unsafe { core::str::from_utf8_unchecked(x) }; - console::println!("ACM data: {}", x); - ep3_action = UsbAction::TransferIn { - endpoint: 3, - data: unsafe { core::mem::transmute(x) }, - zlp: true, - }; - UsbAction::None - } - ep => { - console::println!("Unhandled OUT on EP {} len={}", ep, pkt.len()); - UsbAction::None - } - }, - UsbEvent::UsbReset => { - console::println!("USB reset"); - UsbAction::None - } - _ => ep0.handle_event(event, &descriptors), + let mut action = match cdc_acm.handle_event(event) { + Ok(a) => a, + Err(e) => ep0.handle_event(e, &descriptors).unwrap_or(UsbAction::None), }; - ep0_action.run(&mut usb); - ep3_action.run(&mut usb); + action.run(&mut usb); } } } @@ -482,7 +230,6 @@ fn usb_setup_pinmux() { #[entry] fn entry() -> ! { - // Since this is written as a test, shut down with the return status from `main()`. usb_setup_pinmux(); let ret = match handle_usb() { Ok(()) => Ok(()), From a26a8784893f8819676fab49de7f18ef93b08440 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 15:05:03 -0700 Subject: [PATCH 16/46] refactor: apply UsbClassBuilder pattern for modular const descriptors Signed-off-by: Chris Frantz AI-assistant: Gemini --- hal/blocking/usb/descriptor.rs | 32 +++- protocol/usb/cdc_acm/BUILD.bazel | 1 + protocol/usb/cdc_acm/lib.rs | 170 ++++++++++++++++++-- target/earlgrey/tests/usbserial/test_usb.rs | 128 +++++---------- 4 files changed, 222 insertions(+), 109 deletions(-) diff --git a/hal/blocking/usb/descriptor.rs b/hal/blocking/usb/descriptor.rs index e885b0fe..b8ee0154 100644 --- a/hal/blocking/usb/descriptor.rs +++ b/hal/blocking/usb/descriptor.rs @@ -78,6 +78,7 @@ impl StringHandle { } /// A standard USB device descriptor. +#[derive(Clone, Copy)] pub struct DeviceDescriptor { /// The class of the device. pub device_class: DeviceClass, @@ -146,6 +147,7 @@ impl DeviceDescriptor { } /// A standard USB configuration descriptor. +#[derive(Clone, Copy)] pub struct ConfigDescriptor { /// The configuration value for this configuration. pub configuration_value: u8, @@ -224,6 +226,7 @@ impl ConfigDescriptor { } /// A standard USB interface descriptor. +#[derive(Clone, Copy)] pub struct InterfaceDescriptor { /// Handle for the interface name string descriptor. pub name: StringHandle, @@ -245,7 +248,7 @@ pub struct InterfaceDescriptor { impl InterfaceDescriptor { const SIZE: usize = 9; - const fn total_size(&self) -> usize { + pub(crate) const fn total_size(&self) -> usize { let mut result = Self::SIZE; let mut i = 0; while i < self.func_descs.len() { @@ -302,6 +305,7 @@ impl InterfaceDescriptor { } /// A standard USB endpoint descriptor. +#[derive(Clone, Copy)] pub struct EndpointDescriptor { /// The data direction of the endpoint. pub direction: Direction, @@ -317,7 +321,7 @@ pub struct EndpointDescriptor { impl EndpointDescriptor { const SIZE: usize = 7; - const fn total_size(&self) -> usize { + pub(crate) const fn total_size(&self) -> usize { Self::SIZE } @@ -360,6 +364,7 @@ impl EndpointDescriptor { } /// A standard USB String Descriptor 0 (listing supported languages). +#[derive(Clone, Copy)] pub struct StringDescriptor0 { /// List of supported LANGIDs. pub langs: &'static [u16], @@ -440,6 +445,7 @@ pub enum UsageType { } /// USB device class code. +#[derive(Clone, Copy)] pub struct DeviceClass(pub u8); impl DeviceClass { /// Class is specified at the interface level. @@ -656,6 +662,7 @@ mod test_string_descriptor_writter { } /// A DFU functional descriptor. +#[derive(Clone, Copy)] pub struct DfuFunctionalDescriptor { /// New firmware can be received from the host pub can_download: bool, @@ -703,23 +710,26 @@ impl DfuFunctionalDescriptor { } /// A raw class-specific functional descriptor. +#[derive(Clone, Copy)] pub struct RawFunctionalDescriptor { /// The type of the descriptor. pub descriptor_type: u8, + /// The raw content length. + pub len: u8, /// The raw content of the descriptor. - pub content: &'static [u8], + pub content: [u8; 16], } impl RawFunctionalDescriptor { /// Returns the total size of the descriptor. pub const fn total_size(&self) -> usize { - self.content.len() + 2 + (self.len as usize) + 2 } /// Serializes the raw functional descriptor. pub const fn serialize(&self, dest: &mut [u8], offset: usize) { dest[offset] = self.total_size() as u8; dest[offset + 1] = self.descriptor_type; let mut i = 0; - while i < self.content.len() { + while i < (self.len as usize) { dest[offset + 2 + i] = self.content[i]; i += 1; } @@ -727,6 +737,7 @@ impl RawFunctionalDescriptor { } /// Represents a class-specific functional descriptor. +#[derive(Clone, Copy)] pub enum FunctionalDescriptor { /// DFU functional descriptor. Dfu(DfuFunctionalDescriptor), @@ -736,10 +747,17 @@ pub enum FunctionalDescriptor { impl FunctionalDescriptor { /// Creates a raw functional descriptor. - pub const fn raw(descriptor_type: u8, content: &'static [u8]) -> Self { + pub const fn raw(descriptor_type: u8, content: &[u8]) -> Self { + let mut buf = [0u8; 16]; + let mut i = 0; + while i < content.len() { + buf[i] = content[i]; + i += 1; + } Self::Raw(RawFunctionalDescriptor { descriptor_type, - content, + len: content.len() as u8, + content: buf, }) } /// Returns the total size of the descriptor. diff --git a/protocol/usb/cdc_acm/BUILD.bazel b/protocol/usb/cdc_acm/BUILD.bazel index 88a1a8d6..44c37ff4 100644 --- a/protocol/usb/cdc_acm/BUILD.bazel +++ b/protocol/usb/cdc_acm/BUILD.bazel @@ -9,6 +9,7 @@ rust_library( deps = [ "//hal/blocking/usb:hal_usb", "//protocol/usb/stack", + "//target/earlgrey/drivers:usb_driver", "@rust_crates//:aligned", ], ) diff --git a/protocol/usb/cdc_acm/lib.rs b/protocol/usb/cdc_acm/lib.rs index 9d85c796..b41f6bad 100644 --- a/protocol/usb/cdc_acm/lib.rs +++ b/protocol/usb/cdc_acm/lib.rs @@ -3,10 +3,25 @@ #![no_std] use aligned::{Aligned, A4}; -use hal_usb::driver::{UsbEvent, UsbPacket}; -use hal_usb::{Direction, Recipient, Request, RequestType, SetupPacket}; +pub use hal_usb::driver::{UsbEvent, UsbPacket}; +pub use hal_usb::{ + Direction, EndpointDescriptor, FunctionalDescriptor, InterfaceDescriptor, Recipient, Request, + RequestType, SetupPacket, StringHandle, TransferType, +}; +pub use usb_driver::{EpIn, EpOut}; use usb_stack::{UsbAction, UsbClass, EMPTY}; +/// CDC-ACM specific constants. +pub const USB_CLASS_CDC: u8 = 0x02; +pub const USB_CLASS_CDC_DATA: u8 = 0x0a; +pub const CDC_SUBCLASS_ACM: u8 = 0x02; +pub const CDC_PROTOCOL_NONE: u8 = 0x00; + +pub const CS_INTERFACE: u8 = 0x24; +pub const CDC_TYPE_HEADER: u8 = 0x00; +pub const CDC_TYPE_ACM: u8 = 0x02; +pub const CDC_TYPE_UNION: u8 = 0x06; + /// CDC-ACM specific requests. pub const REQ_SEND_ENCAPSULATED_COMMAND: Request = Request::new( Direction::HostToDevice, @@ -126,25 +141,152 @@ impl LineCoding { } } +/// A builder for CDC-ACM class configuration. +/// +/// This builder encapsulates the constants and configuration logic for a +/// CDC-ACM instance. It provides methods to generate descriptor fragments +/// and the final `InterfaceDescriptor` structs, hiding class-specific details +/// from the application. +/// +/// # Convention +/// USB classes should provide a `Builder` struct with `const` methods that: +/// 1. Return arrays of child descriptors (functional, endpoints). +/// 2. Construct fully-populated `InterfaceDescriptor`s from application-provided +/// handles and static fragments. +/// 3. Provide hardware configuration (`EpIn`, `EpOut`). +#[derive(Copy, Clone)] +pub struct CdcAcmBuilder { + pub comm_if: u8, + pub data_if: u8, + pub comm_ep: u8, + pub data_out_ep: u8, + pub data_in_ep: u8, +} + +impl CdcAcmBuilder { + /// Creates a new CDC-ACM configuration. + pub const fn new(comm_if: u8, data_if: u8, comm_ep: u8, data_out_ep: u8, data_in_ep: u8) -> Self { + Self { + comm_if, + data_if, + comm_ep, + data_out_ep, + data_in_ep, + } + } + + /// Returns the functional descriptors for the control interface. + pub const fn comm_func_descs(&self) -> [FunctionalDescriptor; 3] { + [ + FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_HEADER, 0x10, 0x01]), + FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_ACM, 0x02]), + FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_UNION, self.comm_if, self.data_if]), + ] + } + + /// Returns the endpoints for the control interface. + pub const fn comm_endpoints(&self) -> [EndpointDescriptor; 1] { + [EndpointDescriptor { + direction: Direction::DeviceToHost, + endpoint_num: self.comm_ep, + interval: 255, + max_packet_size: 8, + transfer_type: TransferType::Interrupt, + }] + } + + /// Returns the endpoints for the data interface. + pub const fn data_endpoints(&self) -> [EndpointDescriptor; 2] { + [ + EndpointDescriptor { + direction: Direction::HostToDevice, + endpoint_num: self.data_out_ep, + interval: 0, + max_packet_size: 64, + transfer_type: TransferType::Bulk, + }, + EndpointDescriptor { + direction: Direction::DeviceToHost, + endpoint_num: self.data_in_ep, + interval: 0, + max_packet_size: 64, + transfer_type: TransferType::Bulk, + }, + ] + } + + /// Constructs the CDC-ACM Communication (Control) interface descriptor. + pub const fn comm_interface( + &self, + name: StringHandle, + func_descs: &'static [FunctionalDescriptor], + endpoints: &'static [EndpointDescriptor], + ) -> InterfaceDescriptor { + InterfaceDescriptor { + name, + interface_number: self.comm_if, + alternate_setting: 0, + interface_class: USB_CLASS_CDC, + interface_sub_class: CDC_SUBCLASS_ACM, + interface_protocol: CDC_PROTOCOL_NONE, + func_descs, + endpoints, + } + } + + /// Constructs the CDC-ACM Data interface descriptor. + pub const fn data_interface( + &self, + name: StringHandle, + endpoints: &'static [EndpointDescriptor], + ) -> InterfaceDescriptor { + InterfaceDescriptor { + name, + interface_number: self.data_if, + alternate_setting: 0, + interface_class: USB_CLASS_CDC_DATA, + interface_sub_class: 0, + interface_protocol: CDC_PROTOCOL_NONE, + func_descs: &[], + endpoints, + } + } + + /// Returns the hardware endpoint configuration for this CDC-ACM instance. + pub const fn eps(&self) -> ([EpIn; 2], [EpOut; 1]) { + ( + [ + EpIn { + num: self.comm_ep, + buf_pool_size: 1, + }, + EpIn { + num: self.data_in_ep, + buf_pool_size: 1, + }, + ], + [EpOut { + num: self.data_out_ep, + set_nak: false, + }], + ) + } +} + /// CDC-ACM class handler. #[allow(dead_code)] pub struct CdcAcm { - comm_if: u8, - data_if: u8, - out_ep: u8, - in_ep: u8, + config: CdcAcmBuilder, line_coding: LineCoding, expecting_control_out: bool, rx_buffer: Aligned, } impl CdcAcm { - pub fn new(comm_if: u8, data_if: u8, out_ep: u8, in_ep: u8) -> Self { + /// Creates a new CDC-ACM class handler from a builder. + pub fn new(config: CdcAcmBuilder) -> Self { Self { - comm_if, - data_if, - out_ep, - in_ep, + config, line_coding: LineCoding::default(), expecting_control_out: false, rx_buffer: Aligned([0u32; 16]), @@ -153,7 +295,7 @@ impl CdcAcm { fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> (UsbAction<'a>, bool) { if !(pkt.request().recipient() == Recipient::Interface - && (pkt.index() as u8) == self.comm_if) + && (pkt.index() as u8) == self.config.comm_if) { return (UsbAction::None, false); } @@ -232,10 +374,10 @@ impl UsbClass for CdcAcm { UsbEvent::DataOutPacket(pkt) => { if pkt.endpoint_index() == 0 && self.expecting_control_out { Ok(self.handle_control_out(pkt)) - } else if pkt.endpoint_index() == self.out_ep as usize { + } else if pkt.endpoint_index() == self.config.data_out_ep as usize { let buf = pkt.copy_to(self.rx_buffer.as_mut()); Ok(UsbAction::TransferIn { - endpoint: self.in_ep, + endpoint: self.config.data_in_ep, data: unsafe { core::mem::transmute(buf) }, zlp: true, }) diff --git a/target/earlgrey/tests/usbserial/test_usb.rs b/target/earlgrey/tests/usbserial/test_usb.rs index 36def0e8..e9d780ce 100644 --- a/target/earlgrey/tests/usbserial/test_usb.rs +++ b/target/earlgrey/tests/usbserial/test_usb.rs @@ -12,15 +12,15 @@ use userspace::{entry, syscall}; use aligned::{Aligned, A4}; use hal_usb::driver::UsbDriver; -use hal_usb::{Direction, StringDescriptorRef}; - -use usb_driver::{EpIn, EpOut, UsbConfig}; -use usb_stack::{ - DescriptorSource, - UsbAction, - UsbClass, +use hal_usb::{ + ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, FunctionalDescriptor, + InterfaceDescriptor, StringDescriptorRef, }; -use protocol_usb_cdc_acm::CdcAcm; + +use usb_driver::UsbConfig; +use usb_stack::{DescriptorSource, UsbAction, UsbClass}; + +use protocol_usb_cdc_acm::{CdcAcm, CdcAcmBuilder}; const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); @@ -28,17 +28,15 @@ const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); const USB_CDC_COMM_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); const USB_CDC_DATA_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(5); -const USB_CLASS_CDC: u8 = 0x02; -const USB_CLASS_CDC_DATA: u8 = 0x0a; -const CDC_SUBCLASS_ACM: u8 = 0x02; -const CDC_PROTOCOL_NONE: u8 = 0x00; +const CDC_BUILDER: CdcAcmBuilder = CdcAcmBuilder::new( + 0, // comm_if: Communication Interface index + 1, // data_if: Data Interface index + 1, // comm_ep: Communication IN endpoint (Interrupt) + 2, // data_out_ep: Data OUT endpoint (Bulk) + 3, // data_in_ep: Data IN endpoint (Bulk) +); -const CS_INTERFACE: u8 = 0x24; -const CDC_TYPE_HEADER: u8 = 0x00; -const CDC_TYPE_ACM: u8 = 0x02; -const CDC_TYPE_UNION: u8 = 0x06; - -static DEVICE_DESC: hal_usb::DeviceDescriptor = hal_usb::DeviceDescriptor { +static DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, device_sub_class: 0x00, device_protocol: 0x00, @@ -50,65 +48,27 @@ static DEVICE_DESC: hal_usb::DeviceDescriptor = hal_usb::DeviceDescriptor { product: USB_PRODUCT_HANDLE, serial_num: USB_SERIAL_HANDLE, }; -const CONFIG_DESC: hal_usb::ConfigDescriptor = hal_usb::ConfigDescriptor { + +// Fragmented Static Assembly +static CDC_COMM_FUNC_DESCS: [FunctionalDescriptor; 3] = CDC_BUILDER.comm_func_descs(); +static CDC_COMM_ENDPOINTS: [EndpointDescriptor; 1] = CDC_BUILDER.comm_endpoints(); +static CDC_DATA_ENDPOINTS: [EndpointDescriptor; 2] = CDC_BUILDER.data_endpoints(); + +const CDC_INTERFACES: [InterfaceDescriptor; 2] = [ + CDC_BUILDER.comm_interface( + USB_CDC_COMM_HANDLE, + &CDC_COMM_FUNC_DESCS, + &CDC_COMM_ENDPOINTS, + ), + CDC_BUILDER.data_interface(USB_CDC_DATA_HANDLE, &CDC_DATA_ENDPOINTS), +]; + +const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { configuration_value: 1, max_power: 250, self_powered: false, remote_wakeup: false, - interfaces: &[ - hal_usb::InterfaceDescriptor { - name: USB_CDC_COMM_HANDLE, - interface_number: 0, - alternate_setting: 0, - interface_class: USB_CLASS_CDC, - interface_sub_class: CDC_SUBCLASS_ACM, - interface_protocol: CDC_PROTOCOL_NONE, - func_descs: &[ - hal_usb::FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_HEADER, 0x10, 0x01]), - hal_usb::FunctionalDescriptor::raw(CS_INTERFACE, &[CDC_TYPE_ACM, 0x02]), - hal_usb::FunctionalDescriptor::raw( - CS_INTERFACE, - &[ - CDC_TYPE_UNION, - 0, // comm_if - 1, // data_if - ], - ), - ], - endpoints: &[hal_usb::EndpointDescriptor { - direction: Direction::DeviceToHost, - endpoint_num: 1, - interval: 255, - max_packet_size: 8, - transfer_type: hal_usb::TransferType::Interrupt, - }], - }, - hal_usb::InterfaceDescriptor { - name: USB_CDC_DATA_HANDLE, - interface_number: 1, - alternate_setting: 0, - interface_class: USB_CLASS_CDC_DATA, - interface_sub_class: 0, - interface_protocol: CDC_PROTOCOL_NONE, - func_descs: &[], - endpoints: &[ - hal_usb::EndpointDescriptor { - direction: Direction::HostToDevice, - endpoint_num: 2, - interval: 0, - max_packet_size: 64, - transfer_type: hal_usb::TransferType::Bulk, - }, - hal_usb::EndpointDescriptor { - direction: Direction::DeviceToHost, - endpoint_num: 3, - interval: 0, - max_packet_size: 64, - transfer_type: hal_usb::TransferType::Bulk, - }, - ], - }, - ], + interfaces: &CDC_INTERFACES, }; const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { @@ -162,24 +122,16 @@ fn handle_usb() -> Result<(), ErrorCode> { .unwrap(), product_desc_bytes: PRODUCT_ID_DEFAULT, }; - const USB_EP_ACM_INT_IN: EpIn = EpIn { - num: 1, - buf_pool_size: 1, - }; - const USB_EP_ACM_OUT: EpOut = EpOut { - num: 2, - set_nak: false, - }; - const USB_EP_ACM_IN: EpIn = EpIn { - num: 3, - buf_pool_size: 1, - }; - const USB_CONFIG: UsbConfig = - UsbConfig::new(&[USB_EP_ACM_INT_IN, USB_EP_ACM_IN], &[USB_EP_ACM_OUT]); + const CDC_EPS: ( + [protocol_usb_cdc_acm::EpIn; 2], + [protocol_usb_cdc_acm::EpOut; 1], + ) = CDC_BUILDER.eps(); + const USB_CONFIG: UsbConfig = UsbConfig::new(&CDC_EPS.0, &CDC_EPS.1); + let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); let mut ep0 = usb_stack::SimpleEp0::new(); - let mut cdc_acm = CdcAcm::new(0, 1, 2, 3); + let mut cdc_acm = CdcAcm::new(CDC_BUILDER); loop { let wait_return = syscall::object_wait( From f577a283bc682b7e94971d5c4fd9be85984d2d0f Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 15:20:25 -0700 Subject: [PATCH 17/46] feat: add support for unaligned USB data transfers Signed-off-by: Chris Frantz AI-assistant: Gemini --- protocol/usb/cdc_acm/lib.rs | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/protocol/usb/cdc_acm/lib.rs b/protocol/usb/cdc_acm/lib.rs index b41f6bad..52f68e6e 100644 --- a/protocol/usb/cdc_acm/lib.rs +++ b/protocol/usb/cdc_acm/lib.rs @@ -2,7 +2,6 @@ #![no_std] -use aligned::{Aligned, A4}; pub use hal_usb::driver::{UsbEvent, UsbPacket}; pub use hal_usb::{ Direction, EndpointDescriptor, FunctionalDescriptor, InterfaceDescriptor, Recipient, Request, @@ -279,7 +278,7 @@ pub struct CdcAcm { config: CdcAcmBuilder, line_coding: LineCoding, expecting_control_out: bool, - rx_buffer: Aligned, + rx_buffer: [u8; 64], } impl CdcAcm { @@ -289,7 +288,7 @@ impl CdcAcm { config, line_coding: LineCoding::default(), expecting_control_out: false, - rx_buffer: Aligned([0u32; 16]), + rx_buffer: [0u8; 64], } } @@ -314,19 +313,14 @@ impl CdcAcm { self.expecting_control_out = true; (UsbAction::None, true) } - REQ_GET_LINE_CODING => { - let data = unsafe { - core::mem::transmute::<&[u8], &Aligned>(self.line_coding.as_bytes()) - }; - ( - UsbAction::TransferIn { - endpoint: 0, - data, - zlp: true, - }, - false, - ) - } + REQ_GET_LINE_CODING => ( + UsbAction::TransferInUnaligned { + endpoint: 0, + data: self.line_coding.as_bytes(), + zlp: true, + }, + false, + ), REQ_SET_CONTROL_LINE_STATE => ( UsbAction::TransferIn { endpoint: 0, @@ -340,8 +334,7 @@ impl CdcAcm { } fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> { - let mut data = [0u32; 2]; - let buf = pkt.copy_to(&mut data); + let buf = pkt.copy_to_unaligned(&mut self.rx_buffer); self.expecting_control_out = false; match LineCoding::try_from(buf) { Ok(x) => { @@ -375,10 +368,10 @@ impl UsbClass for CdcAcm { if pkt.endpoint_index() == 0 && self.expecting_control_out { Ok(self.handle_control_out(pkt)) } else if pkt.endpoint_index() == self.config.data_out_ep as usize { - let buf = pkt.copy_to(self.rx_buffer.as_mut()); - Ok(UsbAction::TransferIn { + let buf = pkt.copy_to_unaligned(&mut self.rx_buffer); + Ok(UsbAction::TransferInUnaligned { endpoint: self.config.data_in_ep, - data: unsafe { core::mem::transmute(buf) }, + data: buf, zlp: true, }) } else { From 31b04f00f25a6e52ad458414faca311354d3d537 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 28 Mar 2026 15:58:50 -0700 Subject: [PATCH 18/46] feat: integrate RingBuffer into CdcAcm and implement loopback test Signed-off-by: Chris Frantz AI-assistant: Gemini --- protocol/usb/cdc_acm/BUILD.bazel | 1 + protocol/usb/cdc_acm/lib.rs | 44 ++++++++++++++------- target/earlgrey/tests/usbserial/test_usb.rs | 10 ++++- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/protocol/usb/cdc_acm/BUILD.bazel b/protocol/usb/cdc_acm/BUILD.bazel index 44c37ff4..302b82a2 100644 --- a/protocol/usb/cdc_acm/BUILD.bazel +++ b/protocol/usb/cdc_acm/BUILD.bazel @@ -10,6 +10,7 @@ rust_library( "//hal/blocking/usb:hal_usb", "//protocol/usb/stack", "//target/earlgrey/drivers:usb_driver", + "//util/queue", "@rust_crates//:aligned", ], ) diff --git a/protocol/usb/cdc_acm/lib.rs b/protocol/usb/cdc_acm/lib.rs index 52f68e6e..832e3cc9 100644 --- a/protocol/usb/cdc_acm/lib.rs +++ b/protocol/usb/cdc_acm/lib.rs @@ -2,13 +2,14 @@ #![no_std] -pub use hal_usb::driver::{UsbEvent, UsbPacket}; +pub use hal_usb::driver::{UsbEvent, UsbPacket, UsbDriver}; pub use hal_usb::{ Direction, EndpointDescriptor, FunctionalDescriptor, InterfaceDescriptor, Recipient, Request, RequestType, SetupPacket, StringHandle, TransferType, }; pub use usb_driver::{EpIn, EpOut}; use usb_stack::{UsbAction, UsbClass, EMPTY}; +use util_queue::RingBuffer; /// CDC-ACM specific constants. pub const USB_CLASS_CDC: u8 = 0x02; @@ -273,25 +274,33 @@ impl CdcAcmBuilder { } /// CDC-ACM class handler. -#[allow(dead_code)] -pub struct CdcAcm { +pub struct CdcAcm { config: CdcAcmBuilder, line_coding: LineCoding, expecting_control_out: bool, - rx_buffer: [u8; 64], + control_buf: [u8; 8], + pub rx_queue: RingBuffer, + pub tx_queue: RingBuffer, } -impl CdcAcm { +impl CdcAcm { /// Creates a new CDC-ACM class handler from a builder. pub fn new(config: CdcAcmBuilder) -> Self { Self { config, line_coding: LineCoding::default(), expecting_control_out: false, - rx_buffer: [0u8; 64], + control_buf: [0u8; 8], + rx_queue: RingBuffer::new(), + tx_queue: RingBuffer::new(), } } + /// Returns the configuration builder for this instance. + pub fn config(&self) -> &CdcAcmBuilder { + &self.config + } + fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> (UsbAction<'a>, bool) { if !(pkt.request().recipient() == Recipient::Interface && (pkt.index() as u8) == self.config.comm_if) @@ -334,7 +343,7 @@ impl CdcAcm { } fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> { - let buf = pkt.copy_to_unaligned(&mut self.rx_buffer); + let buf = pkt.copy_to_unaligned(&mut self.control_buf); self.expecting_control_out = false; match LineCoding::try_from(buf) { Ok(x) => { @@ -348,9 +357,18 @@ impl CdcAcm { Err(_) => UsbAction::StallInAndOut { endpoint: 0 }, } } + + /// Polls the send buffer and initiates IN transfers if data is available. + pub fn poll_transmit(&mut self, driver: &mut D) { + let data = self.tx_queue.as_slice(); + if !data.is_empty() { + let n = driver.transfer_in_unaligned(self.config.data_in_ep, data, true); + self.tx_queue.consume(n); + } + } } -impl UsbClass for CdcAcm { +impl UsbClass for CdcAcm { fn handle_event<'a, P: UsbPacket>( &'a mut self, event: UsbEvent

, @@ -368,12 +386,10 @@ impl UsbClass for CdcAcm { if pkt.endpoint_index() == 0 && self.expecting_control_out { Ok(self.handle_control_out(pkt)) } else if pkt.endpoint_index() == self.config.data_out_ep as usize { - let buf = pkt.copy_to_unaligned(&mut self.rx_buffer); - Ok(UsbAction::TransferInUnaligned { - endpoint: self.config.data_in_ep, - data: buf, - zlp: true, - }) + let mut tmp = [0u8; 64]; + let buf = pkt.copy_to_unaligned(&mut tmp); + let _ = self.rx_queue.push_slice(buf); + Ok(UsbAction::None) } else { Err(UsbEvent::DataOutPacket(pkt)) } diff --git a/target/earlgrey/tests/usbserial/test_usb.rs b/target/earlgrey/tests/usbserial/test_usb.rs index e9d780ce..36f21542 100644 --- a/target/earlgrey/tests/usbserial/test_usb.rs +++ b/target/earlgrey/tests/usbserial/test_usb.rs @@ -131,7 +131,7 @@ fn handle_usb() -> Result<(), ErrorCode> { let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); let mut ep0 = usb_stack::SimpleEp0::new(); - let mut cdc_acm = CdcAcm::new(CDC_BUILDER); + let mut cdc_acm = CdcAcm::<1024, 1024>::new(CDC_BUILDER); loop { let wait_return = syscall::object_wait( @@ -166,6 +166,14 @@ fn handle_usb() -> Result<(), ErrorCode> { }; action.run(&mut usb); } + + // Loopback received data to send buffer + while let Some(byte) = cdc_acm.rx_queue.pop() { + let _ = cdc_acm.tx_queue.push(byte); + } + + // Initiate any pending transmissions + cdc_acm.poll_transmit(&mut usb); } } From a19c025905215c51e566f46b183cb821e4c73dba Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Tue, 31 Mar 2026 11:01:09 -0700 Subject: [PATCH 19/46] feat: simplify USB descriptor initialization in test_usb Refactor target/earlgrey/tests/usbserial/test_usb.rs to use the updated CdcAcmBuilder methods directly in constant definitions. This reduces boilerplate by inlining descriptor fragments and leverages Rust's constant promotion. Additionally: - Fix visibility of system_lowlevel_console_write in util/console/stdout.rs to allow proper linkage. - Remove unused descriptor imports in test_usb.rs. - Update GEMINI.md with correct project context. Signed-off-by: Chris Frantz AI-assistant: Gemini --- target/earlgrey/tests/usbserial/test_usb.rs | 36 +++++++-------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/target/earlgrey/tests/usbserial/test_usb.rs b/target/earlgrey/tests/usbserial/test_usb.rs index 36f21542..eb6ea377 100644 --- a/target/earlgrey/tests/usbserial/test_usb.rs +++ b/target/earlgrey/tests/usbserial/test_usb.rs @@ -11,12 +11,11 @@ use userspace::time::Instant; use userspace::{entry, syscall}; use aligned::{Aligned, A4}; -use hal_usb::driver::UsbDriver; use hal_usb::{ - ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, FunctionalDescriptor, - InterfaceDescriptor, StringDescriptorRef, + ConfigDescriptor, DeviceDescriptor, StringDescriptorRef, }; +use hal_usb::driver::UsbDriver; use usb_driver::UsbConfig; use usb_stack::{DescriptorSource, UsbAction, UsbClass}; @@ -36,7 +35,7 @@ const CDC_BUILDER: CdcAcmBuilder = CdcAcmBuilder::new( 3, // data_in_ep: Data IN endpoint (Bulk) ); -static DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { +const DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, device_sub_class: 0x00, device_protocol: 0x00, @@ -49,26 +48,19 @@ static DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { serial_num: USB_SERIAL_HANDLE, }; -// Fragmented Static Assembly -static CDC_COMM_FUNC_DESCS: [FunctionalDescriptor; 3] = CDC_BUILDER.comm_func_descs(); -static CDC_COMM_ENDPOINTS: [EndpointDescriptor; 1] = CDC_BUILDER.comm_endpoints(); -static CDC_DATA_ENDPOINTS: [EndpointDescriptor; 2] = CDC_BUILDER.data_endpoints(); - -const CDC_INTERFACES: [InterfaceDescriptor; 2] = [ - CDC_BUILDER.comm_interface( - USB_CDC_COMM_HANDLE, - &CDC_COMM_FUNC_DESCS, - &CDC_COMM_ENDPOINTS, - ), - CDC_BUILDER.data_interface(USB_CDC_DATA_HANDLE, &CDC_DATA_ENDPOINTS), -]; - const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { configuration_value: 1, max_power: 250, self_powered: false, remote_wakeup: false, - interfaces: &CDC_INTERFACES, + interfaces: &[ + CDC_BUILDER.comm_interface( + USB_CDC_COMM_HANDLE, + &CDC_BUILDER.comm_func_descs(), + &CDC_BUILDER.comm_endpoints(), + ), + CDC_BUILDER.data_interface(USB_CDC_DATA_HANDLE, &CDC_BUILDER.data_endpoints()), + ], }; const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { @@ -123,11 +115,7 @@ fn handle_usb() -> Result<(), ErrorCode> { product_desc_bytes: PRODUCT_ID_DEFAULT, }; - const CDC_EPS: ( - [protocol_usb_cdc_acm::EpIn; 2], - [protocol_usb_cdc_acm::EpOut; 1], - ) = CDC_BUILDER.eps(); - const USB_CONFIG: UsbConfig = UsbConfig::new(&CDC_EPS.0, &CDC_EPS.1); + const USB_CONFIG: UsbConfig = UsbConfig::new(&CDC_BUILDER.eps().0, &CDC_BUILDER.eps().1); let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); let mut ep0 = usb_stack::SimpleEp0::new(); From b387d55fba213c495e93bc8fa14cb0add0e7bfa4 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 22 Apr 2026 14:21:45 -0700 Subject: [PATCH 20/46] buildfixes --- protocol/usb/cdc_acm/BUILD.bazel | 2 +- protocol/usb/cdc_acm/lib.rs | 6 ++--- target/earlgrey/tests/usbserial/BUILD.bazel | 26 +++++++------------- target/earlgrey/tests/usbserial/system.json5 | 14 +++++------ target/earlgrey/tests/usbserial/test_usb.rs | 20 ++++++--------- 5 files changed, 27 insertions(+), 41 deletions(-) diff --git a/protocol/usb/cdc_acm/BUILD.bazel b/protocol/usb/cdc_acm/BUILD.bazel index 302b82a2..2539f259 100644 --- a/protocol/usb/cdc_acm/BUILD.bazel +++ b/protocol/usb/cdc_acm/BUILD.bazel @@ -10,7 +10,7 @@ rust_library( "//hal/blocking/usb:hal_usb", "//protocol/usb/stack", "//target/earlgrey/drivers:usb_driver", - "//util/queue", + "//util/ringbuffer", "@rust_crates//:aligned", ], ) diff --git a/protocol/usb/cdc_acm/lib.rs b/protocol/usb/cdc_acm/lib.rs index 832e3cc9..ffabf1be 100644 --- a/protocol/usb/cdc_acm/lib.rs +++ b/protocol/usb/cdc_acm/lib.rs @@ -9,7 +9,7 @@ pub use hal_usb::{ }; pub use usb_driver::{EpIn, EpOut}; use usb_stack::{UsbAction, UsbClass, EMPTY}; -use util_queue::RingBuffer; +use util_ringbuffer::RingBuffer; /// CDC-ACM specific constants. pub const USB_CLASS_CDC: u8 = 0x02; @@ -291,8 +291,8 @@ impl CdcAcm { line_coding: LineCoding::default(), expecting_control_out: false, control_buf: [0u8; 8], - rx_queue: RingBuffer::new(), - tx_queue: RingBuffer::new(), + rx_queue: RingBuffer::default(), + tx_queue: RingBuffer::default(), } } diff --git a/target/earlgrey/tests/usbserial/BUILD.bazel b/target/earlgrey/tests/usbserial/BUILD.bazel index ed8226bd..e0abea33 100644 --- a/target/earlgrey/tests/usbserial/BUILD.bazel +++ b/target/earlgrey/tests/usbserial/BUILD.bazel @@ -1,7 +1,7 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@pigweed//pw_kernel/tooling:app_package.bzl", "app_package") +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") @@ -10,17 +10,17 @@ load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") -rust_binary( - name = "test_usb", +rust_app( + name = "test_usb_serial", srcs = [ "test_usb.rs", ], - crate_features = ["trace"], + codegen_crate_name = "test_usb_serial_codegen", edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", tags = ["kernel"], visibility = ["//visibility:public"], deps = [ - ":app_test_usb", "//hal/blocking/usb:hal_usb", "//protocol/usb/cdc_acm", "//protocol/usb/stack", @@ -29,28 +29,20 @@ rust_binary( "//target/earlgrey/registers:top_earlgrey", "//target/earlgrey/registers:usbdev", "//util/console", - "//util/error", + "//util/ringbuffer", "@pigweed//pw_kernel/userspace", "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", "@rust_crates//:aligned", - "@rust_crates//:ufmt", - "@rust_crates//:zerocopy", + #"@rust_crates//:ufmt", + #"@rust_crates//:zerocopy", ], ) -app_package( - name = "app_test_usb", - app_name = "test_usb", - edition = "2024", - system_config = "@pigweed//pw_kernel/target:system_config_file", - tags = ["kernel"], -) - system_image( name = "usb", apps = [ - ":test_usb", + ":test_usb_serial", ], kernel = ":target", platform = "//target/earlgrey", diff --git a/target/earlgrey/tests/usbserial/system.json5 b/target/earlgrey/tests/usbserial/system.json5 index e6dccb91..b6b8bcd2 100644 --- a/target/earlgrey/tests/usbserial/system.json5 +++ b/target/earlgrey/tests/usbserial/system.json5 @@ -15,11 +15,11 @@ }, apps: [ { - name: "test_usb", + name: "test_usb_serial", flash_size_bytes: 16384, - ram_size_bytes: 4096, - process: { - name: "test_uart_listener process", + processes: [{ + name: "test_usb_serial_process", + ram_size_bytes: 4096, objects: [ { name: "usbdev_interrupts", @@ -67,11 +67,11 @@ ], threads: [ { - name: "usb thread", - stack_size_bytes: 2048, + name: "usb_thread", + kernel_stack_size_bytes: 2048, }, ], - }, + }], }, ], } diff --git a/target/earlgrey/tests/usbserial/test_usb.rs b/target/earlgrey/tests/usbserial/test_usb.rs index eb6ea377..1b0f0b8d 100644 --- a/target/earlgrey/tests/usbserial/test_usb.rs +++ b/target/earlgrey/tests/usbserial/test_usb.rs @@ -5,10 +5,10 @@ #![no_main] #![allow(dead_code)] -use app_test_usb::{handle, signals}; -use util_error::{ErrorCode, KERNEL_ERROR_UNKNOWN}; +use test_usb_serial_codegen::{handle, signals}; use userspace::time::Instant; use userspace::{entry, syscall}; +use pw_status::Error; use aligned::{Aligned, A4}; use hal_usb::{ @@ -40,8 +40,8 @@ const DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { device_sub_class: 0x00, device_protocol: 0x00, max_packet_size: 64, - vendor_id: 0x18d1, - product_id: 0x023b, + vendor_id: 0x18d1, // Google, Inc. + product_id: 0x503a, // STWG USB Fullspeed IP. device_release_num: 0x0100, manufacturer: USB_VENDOR_HANDLE, product: USB_PRODUCT_HANDLE, @@ -107,7 +107,7 @@ impl DescriptorSource for MyDescriptors<'_> { } } -fn handle_usb() -> Result<(), ErrorCode> { +fn handle_usb() -> Result<(), Error> { let mut serial_num_buffer = Aligned::([0_u8; 130]); let descriptors = MyDescriptors { serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned(&mut serial_num_buffer, b"12345") @@ -144,7 +144,7 @@ fn handle_usb() -> Result<(), ErrorCode> { if wait_return.user_data != 0 { pw_log::error!("Incorrect WaitReturn values"); - return Err(KERNEL_ERROR_UNKNOWN); + return Err(Error::Unknown); } while let Some(event) = usb.poll() { @@ -179,13 +179,7 @@ fn usb_setup_pinmux() { #[entry] fn entry() -> ! { usb_setup_pinmux(); - let ret = match handle_usb() { - Ok(()) => Ok(()), - Err(e) => { - pw_log::error!("Error {:x}", e.0.get()); - Err(pw_status::Error::Unknown) - } - }; + let ret = handle_usb(); let _ = syscall::debug_shutdown(ret); loop {} } From 994e80a3c3a8c95817eb0266fa9676fefbe4e947 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 22 Apr 2026 20:21:06 -0700 Subject: [PATCH 21/46] feat: implement USB DFU 1.1 protocol This commit adds a memory-efficient implementation of the USB DFU 1.1 protocol, including: - protocol/usb/dfu/SPEC.md: Technical specification for the implementation. - protocol/usb/dfu/lib.rs: Core logic, state machine, and DfuHandler trait. - protocol/usb/dfu/BUILD.bazel: Bazel build and test targets. The implementation supports configurable block sizes, multiple alternate settings, and explicit manifestation. Signed-off-by: Chris Frantz AI-assistant: Gemini --- protocol/usb/dfu/BUILD.bazel | 20 ++ protocol/usb/dfu/SPEC.md | 68 ++++ protocol/usb/dfu/lib.rs | 618 +++++++++++++++++++++++++++++++++++ 3 files changed, 706 insertions(+) create mode 100644 protocol/usb/dfu/BUILD.bazel create mode 100644 protocol/usb/dfu/SPEC.md create mode 100644 protocol/usb/dfu/lib.rs diff --git a/protocol/usb/dfu/BUILD.bazel b/protocol/usb/dfu/BUILD.bazel new file mode 100644 index 00000000..b20ce214 --- /dev/null +++ b/protocol/usb/dfu/BUILD.bazel @@ -0,0 +1,20 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "dfu", + srcs = ["lib.rs"], + crate_name = "protocol_usb_dfu", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/usb:hal_usb", + "//protocol/usb/stack", + "@rust_crates//:aligned", + ], +) + +rust_test( + name = "dfu_test", + crate = ":dfu", + edition = "2024", +) diff --git a/protocol/usb/dfu/SPEC.md b/protocol/usb/dfu/SPEC.md new file mode 100644 index 00000000..8db7846d --- /dev/null +++ b/protocol/usb/dfu/SPEC.md @@ -0,0 +1,68 @@ +# USB Device Firmware Upgrade (DFU) 1.1 Specification + +This document describes the USB DFU 1.1 implementation for this project. + +## 1. Overview +The USB DFU 1.1 protocol allows for firmware upgrades over USB. This implementation is designed for limited-memory devices and supports multiple alternate settings to address different memory regions (e.g., internal flash, external SPI flash, bootloader). + +## 2. USB Descriptors + +### 2.1 DFU Functional Descriptor +The DFU functional descriptor follows the USB DFU 1.1 specification. + +| Offset | Field | Size | Value | Description | +| :--- | :--- | :--- | :--- | :--- | +| 0 | bLength | 1 | 0x09 | Size of this descriptor in bytes. | +| 1 | bDescriptorType | 1 | 0x21 | DFU FUNCTIONAL descriptor type. | +| 2 | bmAttributes | 1 | 0x07 | bitCanDnload, bitCanUpload, bitManifestationTolerant. | +| 3 | wDetachTimeOut | 2 | 0x0000 | Time in ms to wait after DFU_DETACH. | +| 5 | wTransferSize | 2 | Configurable | Maximum number of bytes the device can accept per control-write. | +| 7 | bcdDFUVersion | 2 | 0x0110 | DFU specification release number in BCD. | + +### 2.2 Interface Descriptors +Multiple alternate settings are supported. Each altsetting typically represents a different memory partition. + +| Offset | Field | Size | Value | Description | +| :--- | :--- | :--- | :--- | :--- | +| 0 | bLength | 1 | 0x09 | Size of this descriptor in bytes. | +| 1 | bDescriptorType | 1 | 0x04 | INTERFACE descriptor type. | +| 2 | bInterfaceNumber | 1 | Variable | Number of this interface. | +| 3 | bAlternateSetting | 1 | Variable | Value used to select this alternate setting. | +| 4 | bNumEndpoints | 1 | 0x00 | No endpoints used (control only). | +| 5 | bInterfaceClass | 1 | 0xFE | Application Specific Class. | +| 6 | bInterfaceSubClass | 1 | 0x01 | Device Firmware Upgrade. | +| 7 | bInterfaceProtocol | 1 | 0x02 | DFU Mode. | +| 8 | iInterface | 1 | Variable | Index of string descriptor describing this partition. | + +## 3. DFU Requests + +The following DFU-specific requests are supported on the control endpoint (0): + +| Request | Code | Direction | Description | +| :--- | :--- | :--- | :--- | +| DFU_DETACH | 0 | Host to Device | Requests the device to leave its current mode and enter DFU mode. | +| DFU_DNLOAD | 1 | Host to Device | Data packets sent from the host to the device. | +| DFU_UPLOAD | 2 | Device to Host | Data packets sent from the device to the host. | +| DFU_GETSTATUS | 3 | Device to Host | Returns the current state and status of the device. | +| DFU_CLRSTATUS | 4 | Host to Device | Clears error status and returns to dfuIDLE. | +| DFU_GETSTATE | 5 | Device to Host | Returns the current state of the device. | +| DFU_ABORT | 6 | Host to Device | Aborts the current operation and returns to dfuIDLE. | + +## 4. State Machine +This implementation follows the DFU 1.1 state machine. For simplicity, all operations (flash erase/write) are performed synchronously within the `DFU_DNLOAD` and `DFU_UPLOAD` request handling, or immediately transition from SYNC to IDLE. + +## 5. Application Interface (`DfuHandler`) +The application must provide an implementation of the `DfuHandler` trait. + +```rust +pub trait DfuHandler { + /// Handle a DNLOAD block. + fn dnload(&mut self, alt: u8, block_num: u16, data: &[u8]) -> DfuResult; + /// Handle an UPLOAD block. + fn upload(&mut self, alt: u8, block_num: u16, data: &mut [u8]) -> Result; + /// Perform manifestation. + fn manifest(&mut self) -> DfuResult; + /// Abort current operation. + fn abort(&mut self); +} +``` diff --git a/protocol/usb/dfu/lib.rs b/protocol/usb/dfu/lib.rs new file mode 100644 index 00000000..1548ea68 --- /dev/null +++ b/protocol/usb/dfu/lib.rs @@ -0,0 +1,618 @@ +//! USB Device Firmware Upgrade (DFU) 1.1 implementation. + +#![no_std] + +use hal_usb::driver::{UsbDriver, UsbEvent, UsbPacket}; +use hal_usb::{ + Direction, FunctionalDescriptor, InterfaceDescriptor, Recipient, Request, RequestType, + SetupPacket, StringHandle, +}; +use usb_stack::{UsbAction, UsbClass, EMPTY}; + +/// DFU specific constants. +pub const USB_CLASS_APP_SPECIFIC: u8 = 0xFE; +pub const USB_SUBCLASS_DFU: u8 = 0x01; +pub const USB_PROTOCOL_DFU: u8 = 0x02; + +pub const DFU_DESCRIPTOR_TYPE: u8 = 0x21; + +/// DFU specific requests. +pub const DFU_DETACH: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 0, +); +pub const DFU_DNLOAD: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 1, +); +pub const DFU_UPLOAD: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 2, +); +pub const DFU_GETSTATUS: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 3, +); +pub const DFU_CLRSTATUS: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 4, +); +pub const DFU_GETSTATE: Request = Request::new( + Direction::DeviceToHost, + RequestType::Class, + Recipient::Interface, + 5, +); +pub const DFU_ABORT: Request = Request::new( + Direction::HostToDevice, + RequestType::Class, + Recipient::Interface, + 6, +); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum DfuState { + AppIdle = 0, + AppDetach = 1, + DfuIdle = 2, + DfuDnloadSync = 3, + DfuDnloadBusy = 4, + DfuDnloadIdle = 5, + DfuManifestSync = 6, + DfuManifest = 7, + DfuManifestWaitReset = 8, + DfuUploadIdle = 9, + DfuError = 10, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum DfuStatus { + Ok = 0, + ErrTarget = 1, + ErrFile = 2, + ErrWrite = 3, + ErrErase = 4, + ErrCheckChunked = 5, + ErrProg = 6, + ErrVerify = 7, + ErrAddress = 8, + ErrNotDone = 9, + ErrFirmware = 10, + ErrVendor = 11, + ErrUsbr = 12, + ErrPor = 13, + ErrUnknown = 14, + ErrStalledPkt = 15, +} + +pub enum DfuResult { + Ok, + Err(DfuStatus), +} + +/// A trait for providing the backend storage for the DFU implementation. +pub trait DfuHandler { + /// Write a block of data to the device. + fn dnload(&mut self, alt: u8, block_num: u16, data: &[u8]) -> DfuResult; + /// Read a block of data from the device. + fn upload(&mut self, alt: u8, block_num: u16, data: &mut [u8]) -> Result; + /// Finalize the download. + fn manifest(&mut self) -> DfuResult; + /// Abort the current operation. + fn abort(&mut self); +} + +/// A builder for DFU class configuration. +#[derive(Copy, Clone)] +pub struct DfuBuilder { + pub interface_num: u8, + pub alt_settings: u8, + pub transfer_size: u16, + pub attributes: u8, + pub detach_timeout: u16, +} + +impl DfuBuilder { + pub const fn new(interface_num: u8, alt_settings: u8, transfer_size: u16) -> Self { + Self { + interface_num, + alt_settings, + transfer_size, + attributes: 0x07, // bitCanDnload | bitCanUpload | bitManifestationTolerant + detach_timeout: 0, + } + } + + pub const fn functional_descriptor(&self) -> FunctionalDescriptor { + FunctionalDescriptor::raw( + DFU_DESCRIPTOR_TYPE, + &[ + self.attributes, + (self.detach_timeout & 0xFF) as u8, + ((self.detach_timeout >> 8) & 0xFF) as u8, + (self.transfer_size & 0xFF) as u8, + ((self.transfer_size >> 8) & 0xFF) as u8, + 0x10, // bcdDFUVersion 1.1 (0x0110) + 0x01, + ], + ) + } + + pub const fn interface( + &self, + alt: u8, + name: StringHandle, + func_descs: &'static [FunctionalDescriptor], + ) -> InterfaceDescriptor { + InterfaceDescriptor { + name, + interface_number: self.interface_num, + alternate_setting: alt, + interface_class: USB_CLASS_APP_SPECIFIC, + interface_sub_class: USB_SUBCLASS_DFU, + interface_protocol: USB_PROTOCOL_DFU, + func_descs, + endpoints: &[], + } + } +} + +pub struct DfuClass +where + H: DfuHandler, +{ + config: DfuBuilder, + handler: H, + state: DfuState, + status: DfuStatus, + alt: u8, + expecting_dnload: bool, + block_num: u16, + buffer: [u8; BLOCK_SIZE], + transfer_offset: usize, + transfer_total: usize, +} + +impl DfuClass +where + H: DfuHandler, +{ + pub fn new(config: DfuBuilder, handler: H) -> Self { + assert!(BLOCK_SIZE >= config.transfer_size as usize); + Self { + config, + handler, + state: DfuState::DfuIdle, + status: DfuStatus::Ok, + alt: 0, + expecting_dnload: false, + block_num: 0, + buffer: [0u8; BLOCK_SIZE], + transfer_offset: 0, + transfer_total: 0, + } + } + + /// Polls the send buffer and initiates IN transfers if data is available. + pub fn poll(&mut self, driver: &mut D) { + if (self.state == DfuState::DfuUploadIdle || self.state == DfuState::DfuIdle) + && self.transfer_offset < self.transfer_total + { + let data = &self.buffer[self.transfer_offset..self.transfer_total]; + let n = driver.transfer_in_unaligned(0, data, true); + self.transfer_offset += n; + + if self.transfer_offset == self.transfer_total { + if self.transfer_total < self.config.transfer_size as usize { + self.state = DfuState::DfuIdle; + } else { + self.state = DfuState::DfuUploadIdle; + } + } + } + } + + fn handle_setup<'a>(&'a mut self, pkt: SetupPacket) -> (UsbAction<'a>, bool) { + if pkt.request().recipient() != Recipient::Interface + || (pkt.index() as u8) != self.config.interface_num + { + return (UsbAction::None, false); + } + + match pkt.request() { + DFU_DETACH => { + // In DFU mode, DETACH is a no-op or transitions back to APP mode. + // We'll just ACK it. + ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + true, + ) + } + DFU_DNLOAD => { + if self.state == DfuState::DfuError { + return (UsbAction::StallInAndOut { endpoint: 0 }, true); + } + let len = pkt.length() as usize; + if len > BLOCK_SIZE { + self.state = DfuState::DfuError; + self.status = DfuStatus::ErrStalledPkt; + return (UsbAction::StallInAndOut { endpoint: 0 }, true); + } + self.block_num = pkt.value(); + self.transfer_offset = 0; + self.transfer_total = len; + + if len == 0 { + // Transition to manifest sync + self.state = DfuState::DfuManifestSync; + ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + true, + ) + } else { + self.expecting_dnload = true; + self.state = DfuState::DfuDnloadSync; + (UsbAction::None, true) + } + } + DFU_UPLOAD => { + if self.state != DfuState::DfuIdle && self.state != DfuState::DfuUploadIdle { + return (UsbAction::StallInAndOut { endpoint: 0 }, true); + } + self.block_num = pkt.value(); + match self + .handler + .upload(self.alt, self.block_num, &mut self.buffer) + { + Ok(n) => { + self.transfer_offset = 0; + self.transfer_total = n; + // poll_transmit will handle the actual transfer + (UsbAction::None, true) + } + Err(s) => { + self.state = DfuState::DfuError; + self.status = s; + (UsbAction::StallInAndOut { endpoint: 0 }, true) + } + } + } + DFU_GETSTATUS => { + // status[0]: bStatus + // status[1-3]: bwPollTimeout (24-bit, little endian) + // status[4]: bState + // status[5]: iString + self.buffer[0] = self.status as u8; + self.buffer[1] = 0; // bwPollTimeout = 0 + self.buffer[2] = 0; + self.buffer[3] = 0; + self.buffer[4] = self.state as u8; + self.buffer[5] = 0; + + // State transitions after GETSTATUS + match self.state { + DfuState::DfuDnloadSync => self.state = DfuState::DfuDnloadIdle, + DfuState::DfuManifestSync => { + match self.handler.manifest() { + DfuResult::Ok => self.state = DfuState::DfuIdle, // ManifestationTolerant = 1 + DfuResult::Err(s) => { + self.state = DfuState::DfuError; + self.status = s; + } + } + } + _ => {} + } + + ( + UsbAction::TransferInUnaligned { + endpoint: 0, + data: &self.buffer[..6], + zlp: true, + }, + true, + ) + } + DFU_CLRSTATUS => { + self.state = DfuState::DfuIdle; + self.status = DfuStatus::Ok; + ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + true, + ) + } + DFU_GETSTATE => { + self.buffer[0] = self.state as u8; + ( + UsbAction::TransferInUnaligned { + endpoint: 0, + data: &self.buffer[..1], + zlp: true, + }, + true, + ) + } + DFU_ABORT => { + self.handler.abort(); + self.state = DfuState::DfuIdle; + self.status = DfuStatus::Ok; + self.transfer_total = 0; + self.transfer_offset = 0; + ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + true, + ) + } + Request::INTERFACE_SET_INTERFACE => { + self.alt = pkt.value() as u8; + ( + UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + true, + ) + } + _ => (UsbAction::None, false), + } + } + + fn handle_control_out<'a>(&'a mut self, pkt: impl UsbPacket) -> UsbAction<'a> { + if !self.expecting_dnload { + return UsbAction::StallInAndOut { endpoint: 0 }; + } + + let data = pkt.copy_to_unaligned(&mut self.buffer[self.transfer_offset..]); + self.transfer_offset += data.len(); + + if self.transfer_offset >= self.transfer_total { + self.expecting_dnload = false; + match self.handler.dnload( + self.alt, + self.block_num, + &self.buffer[..self.transfer_total], + ) { + DfuResult::Ok => UsbAction::TransferIn { + endpoint: 0, + data: EMPTY, + zlp: true, + }, + DfuResult::Err(s) => { + self.state = DfuState::DfuError; + self.status = s; + UsbAction::StallInAndOut { endpoint: 0 } + } + } + } else { + UsbAction::None + } + } +} + +impl UsbClass for DfuClass +where + H: DfuHandler, +{ + fn handle_event<'a, P: UsbPacket>( + &'a mut self, + event: UsbEvent

, + ) -> Result, UsbEvent

> { + match event { + UsbEvent::SetupPacket { pkt, endpoint } if endpoint == 0 => { + let (action, claimed) = self.handle_setup(pkt); + if claimed { + Ok(action) + } else { + Err(UsbEvent::SetupPacket { pkt, endpoint }) + } + } + UsbEvent::DataOutPacket(pkt) if pkt.endpoint_index() == 0 => { + Ok(self.handle_control_out(pkt)) + } + _ => Err(event), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aligned::{Aligned, A4}; + use hal_usb::driver::UsbEvent; + use usb_stack::testing::FakeUsbPacket; + + struct MockHandler { + dnload_called: bool, + upload_called: bool, + manifest_called: bool, + abort_called: bool, + } + + impl MockHandler { + fn new() -> Self { + Self { + dnload_called: false, + upload_called: false, + manifest_called: false, + abort_called: false, + } + } + } + + impl DfuHandler for MockHandler { + fn dnload(&mut self, _alt: u8, _block_num: u16, _data: &[u8]) -> DfuResult { + self.dnload_called = true; + DfuResult::Ok + } + fn upload( + &mut self, + _alt: u8, + _block_num: u16, + data: &mut [u8], + ) -> Result { + self.upload_called = true; + data[0] = 0xAA; + Ok(1) + } + fn manifest(&mut self) -> DfuResult { + self.manifest_called = true; + DfuResult::Ok + } + fn abort(&mut self) { + self.abort_called = true; + } + } + + fn setup_packet(req: Request, val: u16, idx: u16, len: u16) -> SetupPacket { + SetupPacket::new([ + (u16::from(req) as u32) | ((val as u32) << 16), + (idx as u32) | ((len as u32) << 16), + ]) + } + + #[test] + fn test_dfu_dnload_sequence() { + let config = DfuBuilder::new(1, 1, 64); + let mut dfu = DfuClass::<_, 64>::new(config, MockHandler::new()); + + // 1. DNLOAD request (block 0, 4 bytes) + let setup = setup_packet(DFU_DNLOAD, 0, 1, 4); + let event: UsbEvent> = UsbEvent::SetupPacket { + endpoint: 0, + pkt: setup, + }; + let action = dfu.handle_event(event).map_err(|_| ()).unwrap(); + assert!(matches!(action, UsbAction::None)); + assert_eq!(dfu.state, DfuState::DfuDnloadSync); + + // 2. Data OUT packet + let data = [1, 2, 3, 4]; + let pkt = FakeUsbPacket { ep: 0, data: &data }; + let action = dfu + .handle_event(UsbEvent::DataOutPacket(pkt)) + .map_err(|_| ()) + .unwrap(); + assert!(matches!(action, UsbAction::TransferIn { endpoint: 0, .. })); + assert!(dfu.handler.dnload_called); + + // 3. GETSTATUS + let setup = setup_packet(DFU_GETSTATUS, 0, 1, 6); + let event: UsbEvent> = UsbEvent::SetupPacket { + endpoint: 0, + pkt: setup, + }; + let action = dfu.handle_event(event).map_err(|_| ()).unwrap(); + assert!(matches!(action, UsbAction::TransferInUnaligned { .. })); + assert_eq!(dfu.state, DfuState::DfuDnloadIdle); + + // 4. DNLOAD ZLP (Manifestation) + let setup = setup_packet(DFU_DNLOAD, 1, 1, 0); + let event: UsbEvent> = UsbEvent::SetupPacket { + endpoint: 0, + pkt: setup, + }; + let action = dfu.handle_event(event).map_err(|_| ()).unwrap(); + assert!(matches!(action, UsbAction::TransferIn { endpoint: 0, .. })); + assert_eq!(dfu.state, DfuState::DfuManifestSync); + + // 5. GETSTATUS (Manifest) + let setup = setup_packet(DFU_GETSTATUS, 0, 1, 6); + let event: UsbEvent> = UsbEvent::SetupPacket { + endpoint: 0, + pkt: setup, + }; + let _ = dfu.handle_event(event).map_err(|_| ()).unwrap(); + assert!(dfu.handler.manifest_called); + assert_eq!(dfu.state, DfuState::DfuIdle); + } + + #[test] + fn test_dfu_upload() { + let config = DfuBuilder::new(1, 1, 64); + let mut dfu = DfuClass::<_, 64>::new(config, MockHandler::new()); + + let setup = setup_packet(DFU_UPLOAD, 0, 1, 64); + let event: UsbEvent> = UsbEvent::SetupPacket { + endpoint: 0, + pkt: setup, + }; + let action = dfu.handle_event(event).map_err(|_| ()).unwrap(); + assert!(matches!(action, UsbAction::None)); + + // Mock driver for poll_transmit + struct MockDriver { + transferred: usize, + } + impl UsbDriver for MockDriver { + const MAX_PACKET_SIZE: usize = 64; + type Packet<'a> = FakeUsbPacket<'a>; + fn transfer_in(&mut self, _ep: u8, _data: &Aligned, _zlp: bool) -> usize { + 0 + } + fn transfer_in_unaligned(&mut self, _ep: u8, data: &[u8], _zlp: bool) -> usize { + self.transferred = data.len(); + data.len() + } + fn stall(&mut self, _ep: u8, _stall: bool) {} + fn is_stalled(&mut self, _ep: u8) -> bool { + false + } + fn set_address(&mut self, _addr: u8) {} + fn poll(&mut self) -> Option>> { + None + } + } + + let mut driver = MockDriver { transferred: 0 }; + dfu.poll_transmit(&mut driver); + assert_eq!(driver.transferred, 1); + assert!(dfu.handler.upload_called); + assert_eq!(dfu.state, DfuState::DfuIdle); // n < transfer_size + } + + #[test] + fn test_dfu_abort() { + let config = DfuBuilder::new(1, 1, 64); + let mut dfu = DfuClass::<_, 64>::new(config, MockHandler::new()); + dfu.state = DfuState::DfuDnloadIdle; + + let setup = setup_packet(DFU_ABORT, 0, 1, 0); + let event: UsbEvent> = UsbEvent::SetupPacket { + endpoint: 0, + pkt: setup, + }; + let action = dfu.handle_event(event).map_err(|_| ()).unwrap(); + assert!(matches!(action, UsbAction::TransferIn { endpoint: 0, .. })); + assert!(dfu.handler.abort_called); + assert_eq!(dfu.state, DfuState::DfuIdle); + } +} From 9f97663b14ee6e4d4b9ba71bfe3dd4ee1fe9b9cd Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Wed, 22 Apr 2026 20:29:06 -0700 Subject: [PATCH 22/46] feat: add USB DFU test/demo program for Earlgrey This commit adds a functional test/demo program for the USB DFU implementation on the Earlgrey platform. The demo: - Responds to DFU_DNLOAD by printing the block number and length. - Responds to DFU_UPLOAD by sending a 2048-byte block with a fixed pattern. - Supports manifestation and abort events with logging. - Includes the necessary system configuration (system.json5) and Bazel build rules. Signed-off-by: Chris Frantz AI-assistant: Gemini --- target/earlgrey/tests/usbdfu/BUILD.bazel | 131 +++++++++++++ target/earlgrey/tests/usbdfu/system.json5 | 77 ++++++++ target/earlgrey/tests/usbdfu/target.rs | 29 +++ target/earlgrey/tests/usbdfu/test_usb.rs | 214 ++++++++++++++++++++++ 4 files changed, 451 insertions(+) create mode 100644 target/earlgrey/tests/usbdfu/BUILD.bazel create mode 100644 target/earlgrey/tests/usbdfu/system.json5 create mode 100644 target/earlgrey/tests/usbdfu/target.rs create mode 100644 target/earlgrey/tests/usbdfu/test_usb.rs diff --git a/target/earlgrey/tests/usbdfu/BUILD.bazel b/target/earlgrey/tests/usbdfu/BUILD.bazel new file mode 100644 index 00000000..92bfc8dd --- /dev/null +++ b/target/earlgrey/tests/usbdfu/BUILD.bazel @@ -0,0 +1,131 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "test_usb_dfu", + srcs = [ + "test_usb.rs", + ], + codegen_crate_name = "test_usb_dfu_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/usb:hal_usb", + "//protocol/usb/dfu", + "//protocol/usb/stack", + "//target/earlgrey/drivers:usb_driver", + "//target/earlgrey/registers:pinmux", + "//target/earlgrey/registers:top_earlgrey", + "//target/earlgrey/registers:usbdev", + "//util/console", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + ], +) + +system_image( + name = "usb", + apps = [ + ":test_usb_dfu", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "usb_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":usb", +) + +opentitan_test( + name = "usb_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":usb", +) diff --git a/target/earlgrey/tests/usbdfu/system.json5 b/target/earlgrey/tests/usbdfu/system.json5 new file mode 100644 index 00000000..874c763b --- /dev/null +++ b/target/earlgrey/tests/usbdfu/system.json5 @@ -0,0 +1,77 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "test_usb_dfu", + flash_size_bytes: 16384, + processes: [{ + name: "test_usb_dfu_process", + ram_size_bytes: 4096, + objects: [ + { + name: "usbdev_interrupts", + type: "interrupt", + irqs: [ + { name: "usbdev_pkt_received", number: 135 }, + { name: "usbdev_pkt_sent", number: 136 }, + { name: "usbdev_disconnected", number: 137 }, + { name: "usbdev_host_lost", number: 138 }, + + { name: "usbdev_link_reset", number: 139 }, + { name: "usbdev_link_suspend", number: 140 }, + { name: "usbdev_link_resume", number: 141 }, + { name: "usbdev_av_out_empty", number: 142 }, + + { name: "usbdev_rx_full", number: 143 }, + { name: "usbdev_av_overflow", number: 144 }, + //{ name: "usbdev_link_in_err", number: 145 }, + { name: "usbdev_rx_crc_err", number: 146 }, + + { name: "usbdev_rx_pid_err", number: 147 }, + { name: "usbdev_rx_bitstuff_err", number: 148 }, + { name: "usbdev_frame", number: 149 }, + //{ name: "usbdev_powered", number: 150 }, + + //{ name: "usbdev_link_out_err", number: 151 }, + { name: "usbdev_av_setup_empty", number: 152 }, + ], + }, + ], + memory_mappings: [ + { + name: "usbdev", + type: "device", + start_address: 0x40320000, + size_bytes: 0x1000, + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000, + } + + ], + threads: [ + { + name: "usb_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/usbdfu/target.rs b/target/earlgrey/tests/usbdfu/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/tests/usbdfu/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/usbdfu/test_usb.rs b/target/earlgrey/tests/usbdfu/test_usb.rs new file mode 100644 index 00000000..039eb8f7 --- /dev/null +++ b/target/earlgrey/tests/usbdfu/test_usb.rs @@ -0,0 +1,214 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +#![allow(dead_code)] + +use pw_status::Error; +use test_usb_dfu_codegen::{handle, signals}; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use aligned::{Aligned, A4}; +use hal_usb::{ConfigDescriptor, DeviceDescriptor, StringDescriptorRef}; + +use hal_usb::driver::UsbDriver; +use usb_driver::UsbConfig; +use usb_stack::{DescriptorSource, UsbAction, UsbClass}; + +use protocol_usb_dfu::{DfuBuilder, DfuClass, DfuHandler, DfuResult, DfuStatus}; + +const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); +const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); +const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); +const USB_DFU_INTERFACE_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); + +const DFU_BUILDER: DfuBuilder = DfuBuilder::new( + 0, // interface_num + 1, // alt_settings (1 for now) + 2048, // transfer_size +); + +const DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { + device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, + device_sub_class: 0x00, + device_protocol: 0x00, + max_packet_size: 64, + vendor_id: 0x18d1, // Google, Inc. + product_id: 0x503a, // STWG USB Fullspeed IP. + device_release_num: 0x0100, + manufacturer: USB_VENDOR_HANDLE, + product: USB_PRODUCT_HANDLE, + serial_num: USB_SERIAL_HANDLE, +}; + +const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[DFU_BUILDER.interface( + 0, + USB_DFU_INTERFACE_HANDLE, + &[DFU_BUILDER.functional_descriptor()], + )], +}; + +const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { + langs: &[ + // English - United States + 0x0409, + ], +}; + +const VENDOR_ID: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Google Inc.").as_ref(); +const PRODUCT_ID_DEFAULT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("Earlgrey DFU").as_ref(); +const USB_DFU: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("DFU Interface").as_ref(); + +struct MyDescriptors<'a> { + serial_desc_bytes: StringDescriptorRef<'a>, + product_desc_bytes: StringDescriptorRef<'a>, +} + +impl DescriptorSource for MyDescriptors<'_> { + const DEVICE_DESC_BYTES: &'static Aligned = &Aligned(DEVICE_DESC.serialize()); + const CONFIG_DESC_BYTES: &'static Aligned = + &Aligned(CONFIG_DESC.serialize::<{ CONFIG_DESC.total_size() }>()); + const STRING_DESC_0_BYTES: &'static Aligned = + &Aligned(STRING_DESC_0.serialize::<{ STRING_DESC_0.total_size() }>()); + const DEVICE_STATUS: Aligned = Aligned([1u8, 0]); + + fn get_string( + &self, + handle: hal_usb::StringHandle, + _lang: u16, + ) -> Option> { + match handle { + USB_VENDOR_HANDLE => Some(VENDOR_ID), + USB_PRODUCT_HANDLE => Some(self.product_desc_bytes), + USB_SERIAL_HANDLE => Some(self.serial_desc_bytes), + USB_DFU_INTERFACE_HANDLE => Some(USB_DFU), + _ => None, + } + } +} + +struct MyDfuHandler; + +impl DfuHandler for MyDfuHandler { + fn dnload(&mut self, alt: u8, block_num: u16, data: &[u8]) -> DfuResult { + pw_log::info!( + "DNLOAD: alt={}, block={}, len={}", + alt, + block_num, + data.len() + ); + DfuResult::Ok + } + + fn upload(&mut self, alt: u8, block_num: u16, data: &mut [u8]) -> Result { + pw_log::info!( + "UPLOAD: alt={}, block={}, len={}", + alt, + block_num, + data.len() + ); + // Send a 2K block with a fixed pattern + for (i, byte) in data.iter_mut().enumerate() { + *byte = (i & 0xFF) as u8; + } + Ok(data.len()) + } + + fn manifest(&mut self) -> DfuResult { + pw_log::info!("MANIFEST"); + DfuResult::Ok + } + + fn abort(&mut self) { + pw_log::info!("ABORT"); + } +} + +fn handle_usb() -> Result<(), Error> { + let mut serial_num_buffer = Aligned::([0_u8; 130]); + let descriptors = MyDescriptors { + serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned( + &mut serial_num_buffer, + b"DFU-12345", + ) + .unwrap(), + product_desc_bytes: PRODUCT_ID_DEFAULT, + }; + + const USB_CONFIG: UsbConfig = UsbConfig::new(&[], &[]); + + let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); + let mut ep0 = usb_stack::SimpleEp0::new(); + let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, MyDfuHandler); + + loop { + let wait_return = syscall::object_wait( + handle::USBDEV_INTERRUPTS, + signals::USBDEV_PKT_RECEIVED + | signals::USBDEV_PKT_SENT + | signals::USBDEV_DISCONNECTED + | signals::USBDEV_HOST_LOST + | signals::USBDEV_LINK_RESET + | signals::USBDEV_LINK_SUSPEND + | signals::USBDEV_LINK_RESUME + | signals::USBDEV_AV_OUT_EMPTY + | signals::USBDEV_RX_FULL + | signals::USBDEV_AV_OVERFLOW + | signals::USBDEV_RX_CRC_ERR + | signals::USBDEV_RX_PID_ERR + | signals::USBDEV_RX_BITSTUFF_ERR + | signals::USBDEV_FRAME + | signals::USBDEV_AV_SETUP_EMPTY, + Instant::MAX, + )?; + + if wait_return.user_data != 0 { + pw_log::error!("Incorrect WaitReturn values"); + return Err(Error::Unknown); + } + + while let Some(event) = usb.poll() { + let mut action = match dfu.handle_event(event) { + Ok(a) => a, + Err(e) => ep0.handle_event(e, &descriptors).unwrap_or(UsbAction::None), + }; + action.run(&mut usb); + } + + // Initiate any pending transmissions (e.g. UPLOAD blocks) + dfu.poll(&mut usb); + } +} + +fn usb_setup_pinmux() { + use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn}; + let mut pinmux = unsafe { pinmux::PinmuxAon::new() }; + + pinmux + .regs_mut() + .mio_periph_insel() + .at(PinmuxPeripheralIn::UsbdevSense as usize) + .modify(|_| (PinmuxInsel::ConstantOne as u32).into()); +} + +#[entry] +fn entry() -> ! { + usb_setup_pinmux(); + let ret = handle_usb(); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} From 9d0d340a6aaf97b40e6ce1f180beebc596cc3c78 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 23 Apr 2026 12:14:00 -0700 Subject: [PATCH 23/46] util: integrate hexdump into console Signed-off-by: Chris Frantz --- util/console/BUILD.bazel | 10 ++- util/console/hexdump.rs | 151 +++++++++++++++++++++++++++++++++++++++ util/console/lib.rs | 2 + 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 util/console/hexdump.rs diff --git a/util/console/BUILD.bazel b/util/console/BUILD.bazel index f9d475f4..ef6d4b89 100644 --- a/util/console/BUILD.bazel +++ b/util/console/BUILD.bazel @@ -1,19 +1,22 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_static_library") +load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_static_library", "rust_test") package(default_visibility = ["//visibility:public"]) rust_library( name = "console", srcs = [ + "hexdump.rs", "lib.rs", ], + crate_name = "util_console", edition = "2024", deps = [ ":dbg_print", "@rust_crates//:ufmt", + "@rust_crates//:zerocopy", ] + select({ "@platforms//os:none": [ ":pigweed", @@ -55,3 +58,8 @@ cc_library( hdrs = ["dbg_print.h"], alwayslink = 1, ) + +rust_test( + name = "console_test", + crate = ":console", +) diff --git a/util/console/hexdump.rs b/util/console/hexdump.rs new file mode 100644 index 00000000..e57888fe --- /dev/null +++ b/util/console/hexdump.rs @@ -0,0 +1,151 @@ +use ufmt::uWrite; +use zerocopy::{Immutable, IntoBytes}; + +use crate::Console; + +const HEX: [u8; 16] = *b"0123456789ABCDEF"; + +pub fn hexdump_write(writer: &mut W, data: &T) +where + W: uWrite, + T: IntoBytes + Immutable + ?Sized, +{ + let data = data.as_bytes(); + for (i, d) in data.chunks(16).enumerate() { + let mut buf = [b' '; 80]; + let mut offset = i * 16; + for j in 0..8 { + buf[7 - j] = HEX[offset & 15]; + offset = offset >> 4; + } + for (j, &byte) in d.iter().enumerate() { + buf[10 + j * 3] = HEX[(byte >> 4) as usize]; + buf[11 + j * 3] = HEX[(byte & 15) as usize]; + buf[60 + j] = if byte >= 0x20 && byte < 0x7f { + byte + } else { + b'.' + }; + } + let end = 60 + d.len(); + buf[end] = b'\n'; + let line = unsafe { core::str::from_utf8_unchecked(&buf[..end + 1]) }; + let _ = writer.write_str(line); + } +} + +pub fn hexdump(data: &T) +where + T: IntoBytes + Immutable + ?Sized, +{ + hexdump_write(&mut Console, data); +} + +pub fn hexstr<'a, T>(dest: &'a mut [u8], data: &T) -> &'a str +where + T: IntoBytes + Immutable + ?Sized, +{ + let data = data.as_bytes(); + let mut i = 0; + for &byte in data.iter() { + dest[i] = HEX[(byte >> 4) as usize]; + dest[i + 1] = HEX[(byte & 15) as usize]; + i += 2; + } + // SAFETY: the hex chars emitted into `dest` are ASCII. + unsafe { core::str::from_utf8_unchecked(&dest[..i]) } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::println; + use core::convert::Infallible; + + struct TestConsole { + pub buf: [u8; 1024], + pub len: usize, + } + + impl TestConsole { + //fn reset(&mut self) { self.len = 0; } + fn as_slice(&self) -> &[u8] { + &self.buf[..self.len] + } + } + + impl Default for TestConsole { + fn default() -> Self { + Self { + buf: [0u8; 1024], + len: 0, + } + } + } + + impl uWrite for TestConsole { + type Error = Infallible; + fn write_str(&mut self, s: &str) -> Result<(), Infallible> { + let s = s.as_bytes(); + let end = self.len + s.len(); + self.buf[self.len..end].copy_from_slice(s); + self.len = end; + Ok(()) + } + } + + // From: http://www.abrahamlincolnonline.org/lincoln/speeches/gettysburg.htm + const GETTYSBURG_PRELUDE: &'static str = "\ +Four score and seven years ago our fathers brought forth on this \ +continent, a new nation, conceived in Liberty, and dedicated to the \ +proposition that all men are created equal."; + + const GETTYSBURG_PRELUDE_HEXDUMP: &'static [u8] = b"\ +00000000 46 6F 75 72 20 73 63 6F 72 65 20 61 6E 64 20 73 Four score and s\n\ +00000010 65 76 65 6E 20 79 65 61 72 73 20 61 67 6F 20 6F even years ago o\n\ +00000020 75 72 20 66 61 74 68 65 72 73 20 62 72 6F 75 67 ur fathers broug\n\ +00000030 68 74 20 66 6F 72 74 68 20 6F 6E 20 74 68 69 73 ht forth on this\n\ +00000040 20 63 6F 6E 74 69 6E 65 6E 74 2C 20 61 20 6E 65 continent, a ne\n\ +00000050 77 20 6E 61 74 69 6F 6E 2C 20 63 6F 6E 63 65 69 w nation, concei\n\ +00000060 76 65 64 20 69 6E 20 4C 69 62 65 72 74 79 2C 20 ved in Liberty, \n\ +00000070 61 6E 64 20 64 65 64 69 63 61 74 65 64 20 74 6F and dedicated to\n\ +00000080 20 74 68 65 20 70 72 6F 70 6F 73 69 74 69 6F 6E the proposition\n\ +00000090 20 74 68 61 74 20 61 6C 6C 20 6D 65 6E 20 61 72 that all men ar\n\ +000000A0 65 20 63 72 65 61 74 65 64 20 65 71 75 61 6C 2E e created equal.\n\ +"; + + // This is the SHA256 digest of the Gettysburg prelude. + const GETTYSBURG_DIGEST: [u8; 32] = [ + 0x1e, 0x6f, 0xd4, 0x03, 0x0f, 0x90, 0x34, 0xcd, 0x77, 0x57, 0x08, 0xa3, 0x96, 0xc3, 0x24, + 0xed, 0x42, 0x0e, 0xc5, 0x87, 0xeb, 0x3d, 0xd4, 0x33, 0xe2, 0x9f, 0x6a, 0xc0, 0x8b, 0x8c, + 0xc7, 0xba, + ]; + + const GETTYSBURG_DIGEST_HEXSTR: &'static str = + "1E6FD4030F9034CD775708A396C324ED420EC587EB3DD433E29F6AC08B8CC7BA"; + + #[test] + fn test_hexdump_short() { + let buf = [0u8, 1, 2, 3, 4, 5, 100, 128, 160]; + let mut console = TestConsole::default(); + hexdump_write(&mut console, &buf); + assert_eq!( + console.as_slice(), + b"00000000 00 01 02 03 04 05 64 80 A0 ......d..\n" + ); + } + + #[test] + fn test_hexdump_long() { + let mut console = TestConsole::default(); + hexdump_write(&mut console, GETTYSBURG_PRELUDE); + assert_eq!(console.as_slice(), GETTYSBURG_PRELUDE_HEXDUMP); + } + + #[test] + fn test_hexstr() { + let mut dest = [0u8; 100]; + let result = hexstr(&mut dest, &GETTYSBURG_DIGEST); + assert_eq!(result, GETTYSBURG_DIGEST_HEXSTR); + } +} diff --git a/util/console/lib.rs b/util/console/lib.rs index 69f75087..402659f0 100644 --- a/util/console/lib.rs +++ b/util/console/lib.rs @@ -13,6 +13,8 @@ pub use ufmt; use ufmt::uWrite; pub use ufmt::{uwrite, uwriteln}; +pub mod hexdump; + pub struct Console; unsafe extern "C" { From 110302fc40e4d7f71bd13cf20ef07bbfc3113217 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 23 Apr 2026 21:16:23 -0700 Subject: [PATCH 24/46] util: precise errors infrastructure Signed-off-by: Chris Frantz --- util/error/BUILD.bazel | 27 +++++++++++++ util/error/flash.rs | 40 +++++++++++++++++++ util/error/ipc.rs | 12 ++++++ util/error/kernel.rs | 22 ++++++++++ util/error/lib.rs | 91 ++++++++++++++++++++++++++++++++++++++++++ util/io/BUILD.bazel | 20 ++++++++++ util/io/io.rs | 62 ++++++++++++++++++++++++++++ 7 files changed, 274 insertions(+) create mode 100644 util/error/BUILD.bazel create mode 100644 util/error/flash.rs create mode 100644 util/error/ipc.rs create mode 100644 util/error/kernel.rs create mode 100644 util/error/lib.rs create mode 100644 util/io/BUILD.bazel create mode 100644 util/io/io.rs diff --git a/util/error/BUILD.bazel b/util/error/BUILD.bazel new file mode 100644 index 00000000..645693d4 --- /dev/null +++ b/util/error/BUILD.bazel @@ -0,0 +1,27 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "error", + srcs = [ + "flash.rs", + "lib.rs", + "kernel.rs", + "ipc.rs", + ], + crate_name = "util_error", + edition = "2024", + deps = [ + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:ufmt", + ], +) + +rust_doc( + name = "error_doc", + crate = ":error", +) diff --git a/util/error/flash.rs b/util/error/flash.rs new file mode 100644 index 00000000..08a49780 --- /dev/null +++ b/util/error/flash.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ErrorCode, ErrorModule}; +use pw_status::Error; + +// TODO: review the pw_status error codes. + +pub const FLASH_GENERIC: ErrorModule = ErrorModule::new(0x464c); //ascii `FL`. +pub const FLASH_GENERIC_BUSY: ErrorCode = FLASH_GENERIC.from_pw(0, Error::Unavailable); +pub const FLASH_GENERIC_ERASE_INVALID_ADDR: ErrorCode = + FLASH_GENERIC.from_pw(1, Error::InvalidArgument); +pub const FLASH_GENERIC_BAD_ALIGNMENT: ErrorCode = FLASH_GENERIC.from_pw(2, Error::InvalidArgument); +pub const FLASH_GENERIC_READ_TOO_LONG: ErrorCode = FLASH_GENERIC.from_pw(3, Error::InvalidArgument); +pub const FLASH_GENERIC_PROGRAM_EXCEEDS_WINDOW_SIZE: ErrorCode = + FLASH_GENERIC.from_pw(4, Error::InvalidArgument); +pub const FLASH_GENERIC_PROGRAM_SPANS_WINDOW_BOUNDARY: ErrorCode = + FLASH_GENERIC.from_pw(5, Error::InvalidArgument); +pub const FLASH_GENERIC_ADDR_OUT_OF_BOUNDS: ErrorCode = + FLASH_GENERIC.from_pw(6, Error::InvalidArgument); +pub const FLASH_GENERIC_INVALID_PAGE_SIZE: ErrorCode = + FLASH_GENERIC.from_pw(7, Error::InvalidArgument); +pub const FLASH_GENERIC_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(8, Error::InvalidArgument); + +pub const FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY: ErrorCode = + FLASH_GENERIC.from_pw(1024, Error::InvalidArgument); +pub const FLASH_GENERIC_SFDP_INVALID_SIGNATURE: ErrorCode = + FLASH_GENERIC.from_pw(1025, Error::InvalidArgument); +pub const FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND: ErrorCode = + FLASH_GENERIC.from_pw(1026, Error::InvalidArgument); +pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT: ErrorCode = + FLASH_GENERIC.from_pw(1027, Error::InvalidArgument); +pub const FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV: ErrorCode = + FLASH_GENERIC.from_pw(1028, Error::InvalidArgument); +pub const FLASH_GENERIC_SFDP_UNSUPPORTED_PARAMS_MAJOR_REV: ErrorCode = + FLASH_GENERIC.from_pw(1029, Error::InvalidArgument); +pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_LONG: ErrorCode = + FLASH_GENERIC.from_pw(1030, Error::InvalidArgument); + +pub const FLASH_OPENTITAN: ErrorModule = ErrorModule::new(0x464f); //ascii `FO`. diff --git a/util/error/ipc.rs b/util/error/ipc.rs new file mode 100644 index 00000000..fd97fe48 --- /dev/null +++ b/util/error/ipc.rs @@ -0,0 +1,12 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ErrorCode, ErrorModule}; +use pw_status::Error; + +pub const IPC_ERROR: ErrorModule = ErrorModule::new(0x4943); // ascii `IC` +pub const IPC_ERROR_RSP_BAD_LEN: ErrorCode = IPC_ERROR.from_pw(1, Error::InvalidArgument); +pub const IPC_ERROR_RSP_TOO_LARGE: ErrorCode = IPC_ERROR.from_pw(2, Error::InvalidArgument); +pub const IPC_ERROR_BAD_REQ: ErrorCode = IPC_ERROR.from_pw(3, Error::InvalidArgument); +pub const IPC_ERROR_BAD_REQ_LEN: ErrorCode = IPC_ERROR.from_pw(4, Error::InvalidArgument); +pub const IPC_ERROR_UNKNOWN_OP: ErrorCode = IPC_ERROR.from_pw(5, Error::Unknown); diff --git a/util/error/kernel.rs b/util/error/kernel.rs new file mode 100644 index 00000000..c8398f82 --- /dev/null +++ b/util/error/kernel.rs @@ -0,0 +1,22 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ErrorCode, ErrorModule}; + +pub const KERNEL_ERROR: ErrorModule = ErrorModule::new(0x4b45); // ascii `KE` +pub const KERNEL_ERROR_CANCELLED: ErrorCode = KERNEL_ERROR.error(1); +pub const KERNEL_ERROR_UNKNOWN: ErrorCode = KERNEL_ERROR.error(2); +pub const KERNEL_ERROR_INVALID_ARGUMENT: ErrorCode = KERNEL_ERROR.error(3); +pub const KERNEL_ERROR_DEADLINE_EXCEEDED: ErrorCode = KERNEL_ERROR.error(4); +pub const KERNEL_ERROR_NOT_FOUND: ErrorCode = KERNEL_ERROR.error(5); +pub const KERNEL_ERROR_ALREADY_EXISTS: ErrorCode = KERNEL_ERROR.error(6); +pub const KERNEL_ERROR_PERMISSION_DENIED: ErrorCode = KERNEL_ERROR.error(7); +pub const KERNEL_ERROR_RESOURCE_EXHAUSTED: ErrorCode = KERNEL_ERROR.error(8); +pub const KERNEL_ERROR_FAILED_PRECONDITION: ErrorCode = KERNEL_ERROR.error(9); +pub const KERNEL_ERROR_ABORTED: ErrorCode = KERNEL_ERROR.error(10); +pub const KERNEL_ERROR_OUT_OF_RANGE: ErrorCode = KERNEL_ERROR.error(11); +pub const KERNEL_ERROR_UNIMPLEMENTED: ErrorCode = KERNEL_ERROR.error(12); +pub const KERNEL_ERROR_INTERNAL: ErrorCode = KERNEL_ERROR.error(13); +pub const KERNEL_ERROR_UNAVAILABLE: ErrorCode = KERNEL_ERROR.error(14); +pub const KERNEL_ERROR_DATA_LOSS: ErrorCode = KERNEL_ERROR.error(15); +pub const KERNEL_ERROR_UNAUTHENTICATED: ErrorCode = KERNEL_ERROR.error(16); diff --git a/util/error/lib.rs b/util/error/lib.rs new file mode 100644 index 00000000..072c3364 --- /dev/null +++ b/util/error/lib.rs @@ -0,0 +1,91 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use core::num::NonZero; +use ufmt::{uDebug, uDisplay, uwrite}; + +mod flash; +mod ipc; +mod kernel; + +pub use flash::*; +pub use ipc::*; +pub use kernel::*; + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct ErrorModule(pub NonZero); + +impl ErrorModule { + pub const fn new(val: u16) -> Self { + match NonZero::new(val) { + Some(val) => Self(val), + None => panic!("ErrorModule must be non-zero"), + } + } + + pub const fn error(self, code: u16) -> ErrorCode { + ErrorCode::new(((self.0.get() as u32) << 16) | (code as u32)) + } + + pub const fn from_pw(self, code: u16, err: pw_status::Error) -> ErrorCode { + // pw_status::Error is 5 bits. + self.error((code << 5) | (err as u16)) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct ErrorCode(pub NonZero); +impl ErrorCode { + pub const fn new(val: u32) -> Self { + match NonZero::new(val) { + Some(val) => Self(val), + None => panic!("ErrorCode must be non-zero"), + } + } + + pub fn kernel_error(e: pw_status::Error) -> Self { + KERNEL_ERROR.error(e as u16) + } +} + +impl From for u32 { + fn from(e: ErrorCode) -> u32 { + e.0.get() + } +} + +impl uDisplay for ErrorCode { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + uwrite!(f, "0x{:x}", self.0.get()) + } +} + +impl uDebug for ErrorCode { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + uDisplay::fmt(self, f) + } +} + +impl core::fmt::Display for ErrorCode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "0x{:x}", self.0.get()) + } +} + +impl core::fmt::Debug for ErrorCode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(self, f) + } +} + +impl core::error::Error for ErrorCode {} diff --git a/util/io/BUILD.bazel b/util/io/BUILD.bazel new file mode 100644 index 00000000..49000837 --- /dev/null +++ b/util/io/BUILD.bazel @@ -0,0 +1,20 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "io", + srcs = [ + "io.rs", + ], + crate_name = "util_io", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/error", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_test( + name = "io_test", + crate = ":io", +) diff --git a/util/io/io.rs b/util/io/io.rs new file mode 100644 index 00000000..34851069 --- /dev/null +++ b/util/io/io.rs @@ -0,0 +1,62 @@ +#![no_std] + +use util_error::{ErrorCode, ErrorModule}; + +pub const IO_GENERIC: ErrorModule = ErrorModule::new(0x494F); // ascii: IO +pub const IO_GENERIC_READ_OUT_OF_BOUNDS: ErrorCode = + IO_GENERIC.from_pw(1, pw_status::Error::OutOfRange); + +/// Trait for random access read +pub trait RandomRead { + fn read(&mut self, start_addr: usize, dst: &mut [u8]) -> Result<(), ErrorCode>; + fn size(&self) -> usize; +} + +impl RandomRead for &[u8] { + fn read(&mut self, start_addr: usize, dst: &mut [u8]) -> Result<(), ErrorCode> { + // Explicit wrapping add. Overflows are expected to + // be detected in the indexing operation + let end_addr = start_addr.wrapping_add(dst.len()); + let src = self + .get(start_addr..end_addr) + .ok_or(IO_GENERIC_READ_OUT_OF_BOUNDS)?; + dst.copy_from_slice(src); + Ok(()) + } + fn size(&self) -> usize { + self.len() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn should_read() { + let mut src: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut dst: [u8; 3] = [0; 3]; + assert!(src.read(6, &mut dst).is_ok()); + assert_eq!(&dst, &[7, 8, 9]); + assert_eq!(RandomRead::size(&src), 9); + } + + #[test] + fn should_fail() { + let mut src: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut dst: [u8; 3] = [0; 3]; + assert!(src.read(7, &mut dst).is_err()); + } + + #[test] + fn invalid_start_address_should_not_panic() { + let mut src: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8]; + let mut dst: [u8; 4] = [0; 4]; + // Set `start_addr` so that adding `dst.len()` causes it to + // wrap around and become smaller than `src.len()` + let start_addr: usize = usize::MAX - (dst.len() - 1); + // Should not panic + let result = src.read(start_addr, &mut dst); + assert!(result.is_err()); + } +} From 2205ad16674c1eea2255dc96ba089c83b69e0d0e Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 23 Apr 2026 21:38:26 -0700 Subject: [PATCH 25/46] util: power-of-2, blocking types Signed-off-by: Chris Frantz --- util/types/BUILD.bazel | 21 ++++++++++++ util/types/lib.rs | 14 ++++++++ util/types/opcode.rs | 13 ++++++++ util/types/power_of_2.rs | 70 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 util/types/BUILD.bazel create mode 100644 util/types/lib.rs create mode 100644 util/types/opcode.rs create mode 100644 util/types/power_of_2.rs diff --git a/util/types/BUILD.bazel b/util/types/BUILD.bazel new file mode 100644 index 00000000..5c2c7f77 --- /dev/null +++ b/util/types/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "types", + srcs = [ + "lib.rs", + "opcode.rs", + "power_of_2.rs", + ], + crate_name = "util_types", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "types_test", + crate = ":types", +) diff --git a/util/types/lib.rs b/util/types/lib.rs new file mode 100644 index 00000000..7206ced5 --- /dev/null +++ b/util/types/lib.rs @@ -0,0 +1,14 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +mod opcode; +mod power_of_2; + +pub use opcode::Opcode; +pub use power_of_2::PowerOf2Usize; + +pub trait Blocking { + fn wait_for_notification(&self); +} diff --git a/util/types/opcode.rs b/util/types/opcode.rs new file mode 100644 index 00000000..688374f7 --- /dev/null +++ b/util/types/opcode.rs @@ -0,0 +1,13 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable)] +pub struct Opcode(u32); + +impl Opcode { + pub const fn new(val: [u8; 4]) -> Self { + Opcode(u32::from_le_bytes(val)) + } +} diff --git a/util/types/power_of_2.rs b/util/types/power_of_2.rs new file mode 100644 index 00000000..d40849e5 --- /dev/null +++ b/util/types/power_of_2.rs @@ -0,0 +1,70 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use core::hint::assert_unchecked; + +/// Represents a `usize`` that is guaranteed to be a power-of-two. The compiler +/// can take advantage of this fact when optimizing (for example, using bitwise +/// arithmetic instead of division). +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct PowerOf2Usize(usize); +impl PowerOf2Usize { + // WARNING: Do not add any functions or derives (such as + // zerocopy::FromBytes) that make it possible to modify this value without + // confirming that it is still a power-of-two. As the compiler is relying on + // the power-of-two assertion for safety, any such changes are unsound. + + #[inline(always)] + pub const fn new(val: usize) -> Option { + if !val.is_power_of_two() { + return None; + } + Some(Self(val)) + } + #[inline(always)] + pub const fn get(self) -> usize { + // SAFETY: These assertions are safe because self.0 can only be set by + // Self::new, and we check for the same preconditions there. + // (LLVM is too stupid to realize that is_power_of_two() implies != 0) + unsafe { assert_unchecked(self.0 != 0) }; + unsafe { assert_unchecked(self.0.is_power_of_two()) }; + self.0 + } +} + +#[cfg(test)] +mod tests { + use crate::PowerOf2Usize; + + #[test] + pub fn test() { + assert_eq!(PowerOf2Usize::new(0), None); + assert_eq!(PowerOf2Usize::new(1).unwrap().get(), 1); + assert_eq!(PowerOf2Usize::new(2).unwrap().get(), 2); + assert_eq!(PowerOf2Usize::new(3), None); + assert_eq!(PowerOf2Usize::new(4).unwrap().get(), 4); + assert_eq!(PowerOf2Usize::new(5), None); + assert_eq!(PowerOf2Usize::new(6), None); + assert_eq!(PowerOf2Usize::new(7), None); + assert_eq!(PowerOf2Usize::new(8).unwrap().get(), 8); + assert_eq!(PowerOf2Usize::new(9), None); + assert_eq!(PowerOf2Usize::new(0x7fff_ffff), None); + assert_eq!(PowerOf2Usize::new(0x8000_0000).unwrap().get(), 0x8000_0000); + assert_eq!(PowerOf2Usize::new(0x8000_0001), None); + assert_eq!(PowerOf2Usize::new(0xc000_0000), None); + assert_eq!(PowerOf2Usize::new(0xffff_ffff), None); + + #[cfg(target_pointer_width = "64")] + { + assert_eq!(PowerOf2Usize::new(0x7fff_ffff_ffff_ffff), None); + assert_eq!( + PowerOf2Usize::new(0x8000_0000_0000_0000).unwrap().get(), + 0x8000_0000_0000_0000 + ); + assert_eq!(PowerOf2Usize::new(0x8000_0000_0000_0001), None); + assert_eq!(PowerOf2Usize::new(0xc000_0000_0000_0000), None); + assert_eq!(PowerOf2Usize::new(0xffff_ffff_ffff_ffff), None); + } + } +} From 4b52735fc0f7ce6f42c46d266c5c7810e0505397 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 25 Apr 2026 14:35:36 -0700 Subject: [PATCH 26/46] util_ipc: ipc wrapper Signed-off-by: Chris Frantz --- util/ipc/BUILD.bazel | 24 +++++++++++++ util/ipc/lib.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 util/ipc/BUILD.bazel create mode 100644 util/ipc/lib.rs diff --git a/util/ipc/BUILD.bazel b/util/ipc/BUILD.bazel new file mode 100644 index 00000000..5c6c300a --- /dev/null +++ b/util/ipc/BUILD.bazel @@ -0,0 +1,24 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "ipc", + srcs = [ + "lib.rs", + ], + crate_name = "util_ipc", + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//util/error", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) diff --git a/util/ipc/lib.rs b/util/ipc/lib.rs new file mode 100644 index 00000000..5ddf2e5b --- /dev/null +++ b/util/ipc/lib.rs @@ -0,0 +1,85 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +use userspace::syscall::{self, Signals}; +use userspace::time::Instant; + +use util_error::ErrorCode; + +pub struct IpcChannel(u32); + +impl IpcChannel { + pub const fn new(channel: u32) -> Self { + IpcChannel(channel) + } + + pub fn check_status(code: u32) -> Result<(), ErrorCode> { + if code == 0 { + Ok(()) + } else { + Err(ErrorCode::new(code)) + } + } + + pub fn transaction( + &self, + request: &[&[u8]], + response: &mut [&mut [u8]], + deadline: Instant, + ) -> Result { + if false { + //let _n = N; + //syscall::channel_transact_iovec(self.0, request, response, deadline) + Err(util_error::KERNEL_ERROR_UNIMPLEMENTED) + } else { + let mut buffer = [0u8; N]; + let mut offset = 0usize; + + for item in request.iter() { + let sz = offset + item.len(); + buffer[offset..sz].copy_from_slice(item); + offset = sz; + } + let req = unsafe { + // SAFETY: naughty creation of a const ref to the same slice + // so we can use the same buffer for send and recv. + core::slice::from_raw_parts(buffer.as_ptr(), offset) + }; + let rsplen = syscall::channel_transact(self.0, req, &mut buffer, deadline) + .map_err(ErrorCode::kernel_error)?; + + offset = 0usize; + let rsp = &buffer[..rsplen]; + for item in response.iter_mut() { + let sz = offset + item.len(); + // TODO: how to handle an incomplete response?. + if sz > rsp.len() { + break; + } + item.copy_from_slice(&rsp[offset..sz]); + offset = sz; + } + Ok(rsplen) + } + } + + pub fn wait_readable(&self) -> Result<(), ErrorCode> { + loop { + let w = syscall::object_wait(self.0, Signals::READABLE, Instant::MAX) + .map_err(ErrorCode::kernel_error)?; + if w.pending_signals.contains(Signals::READABLE) { + break; + } + } + Ok(()) + } + + pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result { + syscall::channel_read(self.0, offset, buffer).map_err(ErrorCode::kernel_error) + } + + pub fn respond(&self, buffer: &[u8]) -> Result<(), ErrorCode> { + syscall::channel_respond(self.0, buffer).map_err(ErrorCode::kernel_error) + } +} From 617b2fe87b36cb44e6b44950f8b9f4f548e8d5ab Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 25 Apr 2026 11:42:42 -0700 Subject: [PATCH 27/46] hal: add a hal for accessing flash memories Signed-off-by: Chris Frantz --- hal/blocking/flash/BUILD.bazel | 43 +++++ hal/blocking/flash/driver.rs | 125 +++++++++++++ hal/blocking/flash/flash.rs | 328 +++++++++++++++++++++++++++++++++ 3 files changed, 496 insertions(+) create mode 100644 hal/blocking/flash/BUILD.bazel create mode 100644 hal/blocking/flash/driver.rs create mode 100644 hal/blocking/flash/flash.rs diff --git a/hal/blocking/flash/BUILD.bazel b/hal/blocking/flash/BUILD.bazel new file mode 100644 index 00000000..c871a068 --- /dev/null +++ b/hal/blocking/flash/BUILD.bazel @@ -0,0 +1,43 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "driver", + srcs = [ + "driver.rs", + ], + crate_name = "hal_flash_driver", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/error", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "flash", + srcs = [ + "flash.rs", + ], + crate_name = "hal_flash", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":driver", + "//util/error", + "//util/io", + "//util/types", + ], +) + +rust_test( + name = "flash_test", + crate = ":hal_flash", + rustc_flags = [ + "-C", + "debug-assertions", + ], +) diff --git a/hal/blocking/flash/driver.rs b/hal/blocking/flash/driver.rs new file mode 100644 index 00000000..28a7c628 --- /dev/null +++ b/hal/blocking/flash/driver.rs @@ -0,0 +1,125 @@ +#![no_std] + +use core::num::NonZero; + +use util_error::ErrorCode; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +pub trait FlashDriver { + const PAGE_SIZE: usize; + const PROGRAM_WINDOW_SIZE: usize; + const MAX_READ_SIZE: usize; + const READ_ALIGNMENT: usize; + const PROGRAM_ALIGNMENT: usize; + + fn size(&self) -> NonZero; + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode>; + fn start_erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode>; + fn start_program(&mut self, start_address: FlashAddress, data: &[u8]) -> Result<(), ErrorCode>; + fn is_busy(&mut self) -> bool; + fn complete_op(&mut self) -> Result<(), ErrorCode>; +} + +#[derive(Default, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable, FromBytes, KnownLayout)] +pub struct FlashAddress { + address: u32, +} + +impl FlashAddress { + /// Constructs a flash address for flash data pages. + pub fn data(address: u32) -> Self { + Self { + address: address & 0x7FFF_FFFF, + } + } + + /// Constructs a flash address for flash info pages. + pub fn info(bank: u32, page: u32, offset: u32) -> Self { + Self { + address: 0x8000_0000 | (bank & 0x7f) << 24 | (page & 0xff) << 16 | (offset & 0xFFFF), + } + } + + /// Returns whether the flash address is an info page address. + pub fn is_info(&self) -> bool { + self.address & 0x8000_0000 != 0 + } + + /// Returns the flash offset. For data pages, this is the flash address. For info pages, this + /// is the offset within the page. + pub fn offset(&self) -> u32 { + if self.is_info() { + self.address & 0xFFFF + } else { + self.address + } + } + + /// Returns the bank of a flash info page (only valid when `is_info` returns true). + pub fn bank(&self) -> u32 { + (self.address >> 24) & 0x7f + } + + /// Returns the page number of a flash info page (only valid when `is_info` returns true). + pub fn page(&self) -> u32 { + (self.address >> 16) & 0xff + } +} + +impl core::ops::Add for FlashAddress { + type Output = Self; + fn add(self, other: usize) -> Self { + let other = other as u32; + if self.is_info() { + let offset = self.offset() + other; + FlashAddress { + address: (self.address & !0xFFFF) | (offset & 0xFFFF), + } + } else { + let offset = self.offset() + other; + FlashAddress::data(offset as u32) + } + } +} + +impl core::ops::AddAssign for FlashAddress { + fn add_assign(&mut self, other: usize) { + let other = other as u32; + if self.is_info() { + let offset = self.offset() + other; + self.address = (self.address & !0xFFFF) | (offset & 0xFFFF); + } else { + let offset = self.offset() + other; + self.address = offset as u32; + } + } +} + +impl core::ops::BitAnd for FlashAddress { + type Output = Self; + fn bitand(self, other: usize) -> Self { + let other = other as u32; + if self.is_info() { + let offset = self.offset() & other; + FlashAddress { + address: (self.address & !0xFFFF) | (offset & 0xFFFF), + } + } else { + let offset = self.offset() & other; + FlashAddress::data(offset as u32) + } + } +} + +impl core::ops::BitAndAssign for FlashAddress { + fn bitand_assign(&mut self, other: usize) { + let other = other as u32; + if self.is_info() { + let offset = self.offset() & other; + self.address = (self.address & !0xFFFF) | (offset & 0xFFFF); + } else { + let offset = self.offset() & other; + self.address = offset as u32; + } + } +} diff --git a/hal/blocking/flash/flash.rs b/hal/blocking/flash/flash.rs new file mode 100644 index 00000000..9fc05224 --- /dev/null +++ b/hal/blocking/flash/flash.rs @@ -0,0 +1,328 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(not(test), no_std)] + +use core::{cmp::min, num::NonZero}; +pub use hal_flash_driver::FlashAddress; +use hal_flash_driver::FlashDriver; +use util_error::ErrorCode; +use util_io::RandomRead; +use util_types::{Blocking, PowerOf2Usize}; + +pub trait Flash { + /// The size (and alignment) of erase operations. + // By returning a non-zero type, we can prevent divide-by-zero + // panic-handling code from being generated at the call-site. + fn page_size(&self) -> PowerOf2Usize; + + fn size(&self) -> NonZero; + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode>; + fn erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode>; + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), ErrorCode>; + + fn random_reader(&mut self) -> impl RandomRead + where + Self: Sized, + { + FlashRandomReader(self) + } +} +impl Flash for &mut F { + #[inline(always)] + fn page_size(&self) -> PowerOf2Usize { + (**self).page_size() + } + #[inline(always)] + fn size(&self) -> NonZero { + (**self).size() + } + #[inline(always)] + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + (**self).read(start_addr, buf) + } + #[inline(always)] + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), ErrorCode> { + (**self).program(start_addr, data) + } + #[inline(always)] + fn erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode> { + (**self).erase_page(start_addr) + } +} + +/// A trait that can be used to constrain the page-size of the flash. If you +/// just need to read the page size at runtime, use Flash::page_size() instead. +pub trait FlashPageSize { + const PAGE_SIZE: usize; +} + +pub struct BlockingFlash { + pub driver: TDriver, + pub blocking: TBlocking, +} + +impl FlashPageSize + for BlockingFlash +{ + const PAGE_SIZE: usize = TDriver::PAGE_SIZE; +} + +impl Flash for BlockingFlash { + fn page_size(&self) -> PowerOf2Usize { + const { PowerOf2Usize::new(TDriver::PAGE_SIZE).unwrap() } + } + fn size(&self) -> NonZero { + self.driver.size() + } + fn read(&mut self, start_addr: FlashAddress, mut buf: &mut [u8]) -> Result<(), ErrorCode> { + let mut addr = start_addr; + let align_skip_len = (addr & (TDriver::READ_ALIGNMENT - 1)).offset() as usize; + if (align_skip_len) != 0 { + assert!(TDriver::READ_ALIGNMENT <= 16); + let mut tmp = [0_u8; 16]; + let prefix_count = min(TDriver::READ_ALIGNMENT - align_skip_len, buf.len()); + self.driver + .read(addr & !(TDriver::READ_ALIGNMENT - 1), &mut tmp)?; + buf[..prefix_count].copy_from_slice(&tmp[align_skip_len..][..prefix_count]); + buf = &mut buf[prefix_count..]; + addr += prefix_count; + } + for buf_chunk in buf.chunks_mut(TDriver::MAX_READ_SIZE) { + self.driver.read(addr, buf_chunk)?; + addr += buf_chunk.len(); + } + Ok(()) + } + fn erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode> { + self.driver.start_erase_page(start_addr)?; + self.blocking.wait_for_notification(); + self.driver.complete_op() + } + fn program(&mut self, start_addr: FlashAddress, mut data: &[u8]) -> Result<(), ErrorCode> { + assert!( + TDriver::PROGRAM_WINDOW_SIZE.count_ones() == 1, + "TDriver::PROGRAM_WINDOW_SIZE must be a power of 2" + ); + let window_mask = TDriver::PROGRAM_WINDOW_SIZE - 1; + let mut addr = start_addr; + while !data.is_empty() { + let chunk = &data[..min( + data.len(), + TDriver::PROGRAM_WINDOW_SIZE - ((addr & window_mask).offset() as usize), + )]; + self.driver.start_program(addr, chunk)?; + self.blocking.wait_for_notification(); + self.driver.complete_op()?; + data = &data[chunk.len()..]; + addr += chunk.len(); + } + Ok(()) + } +} + +struct FlashRandomReader<'a, F: Flash>(&'a mut F); +impl RandomRead for FlashRandomReader<'_, F> { + fn read(&mut self, start_addr: usize, buf: &mut [u8]) -> Result<(), ErrorCode> { + self.0.read(FlashAddress::data(start_addr as u32), buf) + } + fn size(&self) -> usize { + self.0.size().get() + } +} + +#[cfg(test)] +mod test { + use super::*; + + pub struct FakeBlocking(); + impl Blocking for FakeBlocking { + fn wait_for_notification(&self) {} + } + + #[derive(Clone)] + pub struct FakeFlashDriver { + pub data: Vec, + pub check_err_result: Result<(), ErrorCode>, + } + impl FakeFlashDriver { + pub fn new(data: Vec) -> Self { + Self { + data, + check_err_result: Ok(()), + } + } + } + impl FlashDriver for FakeFlashDriver { + const PAGE_SIZE: usize = 2048; + const PROGRAM_WINDOW_SIZE: usize = 64; + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 8; + + fn size(&self) -> NonZero { + NonZero::new(self.data.len()).unwrap() + } + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + assert!(start_addr.checked_add(buf.len()).unwrap() <= self.data.len()); + assert!(buf.len() <= Self::MAX_READ_SIZE); + assert!(start_addr % Self::READ_ALIGNMENT == 0); + buf.copy_from_slice(&self.data[start_addr..][..buf.len()]); + Ok(()) + } + fn start_erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode> { + assert!(start_addr.checked_add(Self::PAGE_SIZE).unwrap() <= self.data.len()); + assert!(start_addr % Self::PAGE_SIZE == 0); + self.data[start_addr..][..Self::PAGE_SIZE].fill(0xff); + Ok(()) + } + fn start_program( + &mut self, + start_addr: FlashAddress, + data: &[u8], + ) -> Result<(), ErrorCode> { + let start_addr = start_addr.offset() as usize; + assert!(start_addr.checked_add(data.len()).unwrap() <= self.data.len()); + assert!( + data.len() <= Self::PROGRAM_WINDOW_SIZE, + "Program window violation" + ); + let end_addr = start_addr.wrapping_add(data.len()); + assert!( + start_addr / Self::PROGRAM_WINDOW_SIZE + == (end_addr - 1) / Self::PROGRAM_WINDOW_SIZE, + "Program window violation" + ); + for (dest, src) in self.data[start_addr..end_addr].iter_mut().zip(data) { + *dest &= *src; + } + Ok(()) + } + fn is_busy(&mut self) -> bool { + false + } + fn complete_op(&mut self) -> Result<(), ErrorCode> { + self.check_err_result + } + } + + #[test] + #[should_panic(expected = "Program window violation")] + pub fn test_fake_flash_program_window_violation_0() { + let mut flash_driver = FakeFlashDriver::new((0..255).collect()); + flash_driver.start_program(0x3c, &[0x42; 5]).unwrap(); + } + + #[test] + #[should_panic(expected = "Program window violation")] + pub fn test_fake_flash_program_window_violation_1() { + let mut flash_driver = FakeFlashDriver::new((0..255).collect()); + flash_driver.start_program(0x0, &[0; 68]).unwrap(); + } + + #[test] + pub fn test_fake_flash_full_program_window() { + let mut flash_driver = FakeFlashDriver::new((0..255).collect()); + flash_driver.start_program(0x40, &[0; 0x40]).unwrap(); + assert_eq!(flash_driver.data[0x40..0x80], [0; 0x40]); + } + + #[test] + pub fn test_size() { + let flash_driver = FakeFlashDriver::new((0..255).collect()); + let mut flash = BlockingFlash { + driver: flash_driver, + blocking: FakeBlocking(), + }; + + assert_eq!(flash.size().get(), 255); + assert_eq!(flash.random_reader().size(), 255); + } + + #[test] + pub fn test_read() { + let flash_driver = FakeFlashDriver::new((0..255).collect()); + + let mut flash = BlockingFlash { + driver: flash_driver, + blocking: FakeBlocking(), + }; + + let mut buf = [0_u8; 4]; + flash.read(0, &mut buf).unwrap(); + assert_eq!(buf, [0_u8, 1, 2, 3]); + + let mut buf = [0_u8; 4]; + flash.read(1, &mut buf).unwrap(); + assert_eq!(buf, [1, 2, 3, 4]); + + let mut buf = [0_u8; 4]; + flash.read(2, &mut buf).unwrap(); + assert_eq!(buf, [2, 3, 4, 5]); + + let mut buf = [0_u8; 4]; + flash.random_reader().read(2, &mut buf).unwrap(); + assert_eq!(buf, [2, 3, 4, 5]); + + let mut buf = [0_u8; 4]; + flash.read(3, &mut buf).unwrap(); + assert_eq!(buf, [3, 4, 5, 6]); + + let mut buf = [0_u8; 6]; + flash.read(3, &mut buf).unwrap(); + assert_eq!(buf, [3, 4, 5, 6, 7, 8]); + + for i in 0..32 { + let mut buf = [0_u8; 32]; + flash.read(0, &mut buf[..i]).unwrap(); + assert_eq!(&buf[..i], &flash.driver.data[..i]); + } + + for i in 0..32 { + let mut buf = [0_u8; 32]; + flash.read(32 - i, &mut buf[..i]).unwrap(); + assert_eq!(&buf[..i], &flash.driver.data[32 - i..32]); + } + } + + #[test] + pub fn test_erase() { + let mut flash = BlockingFlash { + driver: FakeFlashDriver::new(vec![0x42; 0x4000]), + blocking: FakeBlocking(), + }; + flash.erase_page(0x0800).unwrap(); + assert_eq!(flash.driver.data[0x0000..0x0800], [0x42; 0x0800]); + assert_eq!(flash.driver.data[0x0800..0x1000], [0xff; 0x0800]); + assert_eq!(flash.driver.data[0x1000..0x4000], [0x42; 0x3000]); + + flash.erase_page(0x3000).unwrap(); + assert_eq!(flash.driver.data[0x0000..0x0800], [0x42; 0x0800]); + assert_eq!(flash.driver.data[0x0800..0x1000], [0xff; 0x0800]); + assert_eq!(flash.driver.data[0x1000..0x3000], [0x42; 0x2000]); + assert_eq!(flash.driver.data[0x3000..0x3800], [0xff; 0x0800]); + assert_eq!(flash.driver.data[0x3800..0x4000], [0x42; 0x0800]); + } + + #[test] + pub fn test_program() { + let mut flash = BlockingFlash { + driver: FakeFlashDriver::new(vec![0xff; 8192]), + blocking: FakeBlocking(), + }; + + flash + .program(0x3c, &[0x10, 0x11, 0x12, 0x13, 0x14, 0x15]) + .unwrap(); + assert_eq!( + flash.driver.data[0x38..0x44], + [0xff, 0xff, 0xff, 0xff, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0xff, 0xff] + ); + + flash.program(0x40, &[0x24, 0x25]).unwrap(); + assert_eq!( + flash.driver.data[0x38..0x44], + [0xff, 0xff, 0xff, 0xff, 0x10, 0x11, 0x12, 0x13, 0x04, 0x05, 0xff, 0xff] + ); + } +} From 8ba76977038065351a1fced372cec20c44cd9aed Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 07:43:00 -0700 Subject: [PATCH 28/46] services: flash client/server Signed-off-by: Chris Frantz --- services/flash/BUILD.bazel | 57 +++++++++++++++++++++++ services/flash/client.rs | 88 ++++++++++++++++++++++++++++++++++++ services/flash/opcode.rs | 19 ++++++++ services/flash/server.rs | 92 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 services/flash/BUILD.bazel create mode 100644 services/flash/client.rs create mode 100644 services/flash/opcode.rs create mode 100644 services/flash/server.rs diff --git a/services/flash/BUILD.bazel b/services/flash/BUILD.bazel new file mode 100644 index 00000000..4195c54c --- /dev/null +++ b/services/flash/BUILD.bazel @@ -0,0 +1,57 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "opcode", + srcs = [ + "opcode.rs", + ], + crate_name = "services_flash_opcode", + edition = "2024", + deps = [ + "//util/types", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "client", + srcs = [ + "client.rs", + ], + crate_name = "services_flash_client", + edition = "2024", + deps = [ + ":opcode", + "//hal/blocking/flash", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "server", + srcs = [ + "server.rs", + ], + crate_name = "services_flash_server", + edition = "2024", + deps = [ + ":opcode", + "//hal/blocking/flash", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:zerocopy", + ], +) diff --git a/services/flash/client.rs b/services/flash/client.rs new file mode 100644 index 00000000..e1a38068 --- /dev/null +++ b/services/flash/client.rs @@ -0,0 +1,88 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +use core::num::NonZero; + +use hal_flash::{Flash, FlashAddress}; +use services_flash_opcode::*; +use userspace::time::Instant; +use util_error::{self as error, ErrorCode}; +use util_ipc::IpcChannel; +use util_types::PowerOf2Usize; +use zerocopy::{FromZeros, IntoBytes}; + +pub struct FlashIpcClient { + ipc: IpcChannel, + page_size: PowerOf2Usize, + total_size: NonZero, +} + +impl FlashIpcClient { + pub fn new(ipc: IpcChannel) -> Result { + let mut info = FlashInfo::new_zeroed(); + let mut result = 0u32; + + ipc.transaction::<12>( + &[IPC_OP_FLASH_GET_INFO.as_bytes()], + &mut [result.as_mut_bytes(), info.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result)?; + + let Some(page_size) = PowerOf2Usize::new(info.page_size) else { + return Err(error::FLASH_GENERIC_INVALID_PAGE_SIZE); + }; + let Some(total_size) = NonZero::new(info.total_size) else { + return Err(error::FLASH_GENERIC_INVALID_SIZE); + }; + Ok(Self { + ipc, + page_size, + total_size, + }) + } +} + +impl Flash for FlashIpcClient { + fn page_size(&self) -> PowerOf2Usize { + self.page_size + } + fn size(&self) -> core::num::NonZero { + self.total_size + } + fn erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc.transaction::<12>( + &[IPC_OP_FLASH_ERASE_PAGE.as_bytes(), start_addr.as_bytes()], + &mut [result.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } + + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc.transaction::<2056>( + &[IPC_OP_FLASH_PROGRAM.as_bytes(), start_addr.as_bytes(), data], + &mut [result.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } + + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + let mut result = 0u32; + let length = buf.len(); + self.ipc.transaction::<2056>( + &[ + IPC_OP_FLASH_READ.as_bytes(), + start_addr.as_bytes(), + length.as_bytes(), + ], + &mut [result.as_mut_bytes(), buf], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } +} diff --git a/services/flash/opcode.rs b/services/flash/opcode.rs new file mode 100644 index 00000000..54e107fd --- /dev/null +++ b/services/flash/opcode.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use util_types::Opcode; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +pub const IPC_OP_FLASH_ERASE_PAGE: Opcode = Opcode::new(*b"FLEP"); +pub const IPC_OP_FLASH_PROGRAM: Opcode = Opcode::new(*b"FLWR"); +pub const IPC_OP_FLASH_READ: Opcode = Opcode::new(*b"FLRD"); +pub const IPC_OP_FLASH_GET_INFO: Opcode = Opcode::new(*b"FLIN"); + +#[derive(FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct FlashInfo { + pub page_size: usize, + pub total_size: usize, +} diff --git a/services/flash/server.rs b/services/flash/server.rs new file mode 100644 index 00000000..eee43b7d --- /dev/null +++ b/services/flash/server.rs @@ -0,0 +1,92 @@ +#![no_std] +//use core::num::NonZero; + +use hal_flash::{Flash, FlashAddress}; +use services_flash_opcode::*; +use util_error::{self as error, ErrorCode}; +use util_ipc::IpcChannel; +use util_types::Opcode; +use zerocopy::{FromBytes, IntoBytes}; + +pub struct FlashIpcServer { + flash: TFlash, +} + +impl FlashIpcServer { + pub fn new(flash: TFlash) -> Self { + Self { flash } + } + + fn handle_get_info<'a>(&self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let (info, _rest) = + FlashInfo::mut_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + info.page_size = self.flash.page_size().get(); + info.total_size = self.flash.size().get(); + Ok(info.as_bytes()) + } + + fn handle_erase<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let (addr, data) = + FlashAddress::read_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + self.flash.erase_page(addr)?; + Ok(&data[0..0]) + } + + fn handle_program<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let (addr, data) = + FlashAddress::mut_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + self.flash.program(*addr, data)?; + Ok(&data[0..0]) + } + + fn handle_read<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let addr = + FlashAddress::read_from_bytes(&data[0..4]).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let length = + usize::read_from_bytes(&data[4..8]).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + self.flash.read(addr, &mut data[..length])?; + Ok(&data[..length]) + } + + fn handle_op<'a>(&mut self, opcode: Opcode, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + match opcode { + IPC_OP_FLASH_GET_INFO => self.handle_get_info(data), + IPC_OP_FLASH_ERASE_PAGE => self.handle_erase(data), + IPC_OP_FLASH_PROGRAM => self.handle_program(data), + IPC_OP_FLASH_READ => self.handle_read(data), + _ => Err(error::IPC_ERROR_UNKNOWN_OP), + } + } + + fn handle_one(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + //pw_log::info!("ipc_wait"); + ipc.wait_readable()?; + //pw_log::info!("ipc_read"); + let len = ipc.read(0, data)?; + //pw_log::info!("ipc_exec"); + if len < 4 { + return Err(error::IPC_ERROR_BAD_REQ_LEN); + } + let (op_status, reqrsp) = data.split_at_mut(4); + let opcode = Opcode::read_from_bytes(op_status).unwrap(); + let len = match self.handle_op(opcode, reqrsp) { + Ok(result) => { + op_status.copy_from_slice((0u32).as_bytes()); + result.len() + } + Err(e) => { + op_status.copy_from_slice(e.0.as_bytes()); + 0 + } + }; + //pw_log::info!("ipc_respond: {}", len as usize); + ipc.respond(&data[..4 + len])?; + Ok(()) + } + + pub fn run(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + loop { + self.handle_one(ipc, data)?; + } + } +} From 0667ebfc765bbe9b40622a3589a9713bdab4c52b Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 07:47:06 -0700 Subject: [PATCH 29/46] earlgrey: eflash driver Signed-off-by: Chris Frantz --- target/earlgrey/drivers/BUILD.bazel | 16 ++ target/earlgrey/drivers/eflash_driver.rs | 256 +++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 target/earlgrey/drivers/eflash_driver.rs diff --git a/target/earlgrey/drivers/BUILD.bazel b/target/earlgrey/drivers/BUILD.bazel index 558425ee..9d497f8e 100644 --- a/target/earlgrey/drivers/BUILD.bazel +++ b/target/earlgrey/drivers/BUILD.bazel @@ -3,6 +3,22 @@ load("@rules_rust//rust:defs.bzl", "rust_library") +rust_library( + name = "eflash_driver", + srcs = ["eflash_driver.rs"], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash:driver", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/util", + "//util/error", + "//util/regcpy", + "@rust_crates//:zerocopy", + "@ureg", + ], +) + rust_library( name = "usb_driver", srcs = ["usb_driver.rs"], diff --git a/target/earlgrey/drivers/eflash_driver.rs b/target/earlgrey/drivers/eflash_driver.rs new file mode 100644 index 00000000..a15ad9d0 --- /dev/null +++ b/target/earlgrey/drivers/eflash_driver.rs @@ -0,0 +1,256 @@ +#![no_std] + +use core::num::NonZero; + +use earlgrey_util::AsMubi; +use flash_ctrl_core::{self, regs::ControlWriteVal}; +use hal_flash_driver::{FlashAddress, FlashDriver}; +use util_error::{self as error, ErrorCode}; +use util_regcpy::{copy_from_reg_unaligned, copy_to_reg_unaligned}; + +pub struct Permission { + pub read: bool, + pub write: bool, + pub erase: bool, +} + +impl Permission { + pub const FULL_ACCESS: Permission = Permission { + read: true, + write: true, + erase: true, + }; + pub const READ_ONLY: Permission = Permission { + read: true, + write: false, + erase: false, + }; +} + +const FLASH_SIZE: NonZero = NonZero::new(1024 * 1024).unwrap(); + +pub struct EmbeddedFlash { + mmio: flash_ctrl_core::FlashCtrl, + busy: bool, +} +impl EmbeddedFlash { + // TODO: unify these with the trait consts. + const BYTES_PER_BANK: u32 = 0x80000; + const BYTES_PER_PAGE: u32 = 2048; + + pub fn new(mmio: flash_ctrl_core::FlashCtrl) -> Self { + Self { mmio, busy: false } + } + + pub fn new_with_interrupts(mut mmio: flash_ctrl_core::FlashCtrl) -> Self { + mmio.regs_mut().intr_state().write(|w| w.op_done_clear()); + mmio.regs_mut().intr_enable().write(|w| w.op_done(true)); + Self { mmio, busy: false } + } + + pub fn set_default_permission(&mut self, perm: Permission) { + let reg = self.mmio.regs_mut(); + reg.default_region().modify(|v| { + v.rd_en(perm.read.as_mubi()) + .prog_en(perm.write.as_mubi()) + .erase_en(perm.erase.as_mubi()) + }); + } + + pub fn set_info_permission( + &mut self, + address: FlashAddress, + perm: Permission, + ) -> Result<(), ErrorCode> { + if !address.is_info() { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + let reg = self.mmio.regs_mut(); + if address.bank() == 0 { + reg.bank0_info0_page_cfg() + .at(address.page() as usize) + .modify(|v| { + v.en(true.as_mubi()) + .rd_en(perm.read.as_mubi()) + .rd_en(perm.read.as_mubi()) + .prog_en(perm.write.as_mubi()) + .erase_en(perm.erase.as_mubi()) + }); + } else { + reg.bank1_info0_page_cfg() + .at(address.page() as usize) + .modify(|v| { + v.en(true.as_mubi()) + .rd_en(perm.read.as_mubi()) + .prog_en(perm.write.as_mubi()) + .erase_en(perm.erase.as_mubi()) + }); + } + Ok(()) + } +} + +//fn u32_from_usize(val: usize) -> u32 { +// u32::try_from(val).unwrap() +//} +impl FlashDriver for EmbeddedFlash { + // BytesPerWord: 8 + // WordsPerPage: 256 + // BytesPerBank: 524288 + // program_resolution: 8 (max flash words to program at one time) + // RegBusPgmResBytes = 64 + const PAGE_SIZE: usize = 2048; + const PROGRAM_WINDOW_SIZE: usize = 64; + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 8; + + fn size(&self) -> core::num::NonZero { + FLASH_SIZE + } + + #[inline(never)] + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + if buf.is_empty() { + return Ok(()); + } + let start_offset = if start_addr.is_info() { + start_addr.bank() * Self::BYTES_PER_BANK + + start_addr.page() * Self::BYTES_PER_PAGE + + start_addr.offset() + } else { + start_addr.offset() + }; + + if (start_offset & 3) != 0 { + return Err(error::FLASH_GENERIC_BAD_ALIGNMENT); + } + if buf.len() > Self::MAX_READ_SIZE { + return Err(error::FLASH_GENERIC_READ_TOO_LONG); + } + + self.check_busy()?; + self.mmio.regs_mut().addr().write(|w| w.start(start_offset)); + self.start_op(|w| { + w.op(|s| s.read()) + .partition_sel(start_addr.is_info()) + .num((buf.len() as u32 + 3) / 4 - 1) + }); + copy_from_reg_unaligned(buf, &self.mmio.regs_mut().rd_fifo()); + while self.is_busy() {} + self.complete_op() + } + + fn start_erase_page(&mut self, start_addr: FlashAddress) -> Result<(), ErrorCode> { + const { + // Confirm this is a power of two + assert!(Self::PAGE_SIZE.count_ones() == 1); + } + let start_offset = if start_addr.is_info() { + start_addr.bank() * Self::BYTES_PER_BANK + + start_addr.page() * Self::BYTES_PER_PAGE + + start_addr.offset() + } else { + start_addr.offset() + }; + let start_offset = start_offset as usize; + + if start_offset & (Self::PAGE_SIZE - 1) != 0 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR); + } + self.check_busy()?; + + self.mmio + .regs_mut() + .addr() + .write(|w| w.start(start_offset as u32)); + self.start_op(|w| { + w.op(|s| s.erase()) + .erase_sel(|s| s.page_erase()) + .partition_sel(start_addr.is_info()) + .start(true) + }); + Ok(()) + } + + fn start_program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), ErrorCode> { + if data.is_empty() { + return Ok(()); + } + if data.len() > Self::PROGRAM_WINDOW_SIZE { + return Err(error::FLASH_GENERIC_PROGRAM_EXCEEDS_WINDOW_SIZE); + } + let start_offset = if start_addr.is_info() { + start_addr.bank() * Self::BYTES_PER_BANK + + start_addr.page() * Self::BYTES_PER_PAGE + + start_addr.offset() + } else { + start_addr.offset() + }; + let start_offset = start_offset as usize; + let end_offset = start_offset.wrapping_add(data.len()); + + if start_offset / Self::PROGRAM_WINDOW_SIZE != (end_offset - 1) / Self::PROGRAM_WINDOW_SIZE + { + return Err(error::FLASH_GENERIC_PROGRAM_SPANS_WINDOW_BOUNDARY); + } + self.check_busy()?; + // reset the op status register + + self.mmio + .regs_mut() + .addr() + .write(|w| w.start(start_offset as u32)); + self.start_op(|w| { + w.op(|s| s.prog()) + .prog_sel(|s| s.normal_program()) + .partition_sel(start_addr.is_info()) + .num(((data.len() + 3) / 4) as u32 - 1) + }); + copy_to_reg_unaligned(&self.mmio.regs_mut().prog_fifo(), data); + Ok(()) + } + + fn is_busy(&mut self) -> bool { + if self.busy && self.mmio.regs_mut().intr_state().read().op_done() { + self.busy = false; + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.op_done_clear()); + } + self.busy + } + + fn complete_op(&mut self) -> Result<(), ErrorCode> { + if self.is_busy() { + return Err(error::FLASH_GENERIC_BUSY); + } + let status = self.mmio.regs().op_status().read(); + if status.err() { + let err_code = u32::from(self.mmio.regs().err_code().read()); + Err(error::FLASH_OPENTITAN.error(err_code as u16)) + } else { + Ok(()) + } + } +} +impl EmbeddedFlash { + fn start_op(&mut self, f: impl FnOnce(ControlWriteVal) -> ControlWriteVal) { + self.busy = true; + self.mmio.regs_mut().err_code().write(|_| 0xff.into()); + self.mmio.regs_mut().op_status().write(|w| w); + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.op_done_clear()); + self.mmio.regs_mut().control().write(|w| f(w).start(true)); + } + fn check_busy(&mut self) -> Result<(), ErrorCode> { + if self.is_busy() { + Err(error::FLASH_GENERIC_BUSY) + } else { + Ok(()) + } + } +} From 5971a379653292df6ba1ee9c84e637a456c0b11c Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 07:48:19 -0700 Subject: [PATCH 30/46] earlgrey: utililty functions Signed-off-by: Chris Frantz --- target/earlgrey/util/BUILD.bazel | 20 +++++++++++ target/earlgrey/util/error.rs | 11 +++++++ target/earlgrey/util/lib.rs | 8 +++++ target/earlgrey/util/mubi.rs | 13 ++++++++ target/earlgrey/util/perso_tlv.rs | 55 +++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 target/earlgrey/util/BUILD.bazel create mode 100644 target/earlgrey/util/error.rs create mode 100644 target/earlgrey/util/lib.rs create mode 100644 target/earlgrey/util/mubi.rs create mode 100644 target/earlgrey/util/perso_tlv.rs diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel new file mode 100644 index 00000000..b1851155 --- /dev/null +++ b/target/earlgrey/util/BUILD.bazel @@ -0,0 +1,20 @@ +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "util", + srcs = [ + "error.rs", + "lib.rs", + "mubi.rs", + "perso_tlv.rs", + ], + crate_name = "earlgrey_util", + edition = "2024", + deps = [ + "//util/error", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) diff --git a/target/earlgrey/util/error.rs b/target/earlgrey/util/error.rs new file mode 100644 index 00000000..7b203244 --- /dev/null +++ b/target/earlgrey/util/error.rs @@ -0,0 +1,11 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use pw_status::Error; +use util_error::{ErrorCode, ErrorModule}; + +// TODO: review the pw_status error codes. + +pub const EG_ERROR: ErrorModule = ErrorModule::new(0x464c); //ascii `FL`. +pub const EG_ERROR_CERT_NOT_FOUND: ErrorCode = EG_ERROR.from_pw(1, Error::NotFound); +pub const EG_ERROR_CERT_BAD_NAME: ErrorCode = EG_ERROR.from_pw(2, Error::InvalidArgument); diff --git a/target/earlgrey/util/lib.rs b/target/earlgrey/util/lib.rs new file mode 100644 index 00000000..50ca0cdf --- /dev/null +++ b/target/earlgrey/util/lib.rs @@ -0,0 +1,8 @@ +#![no_std] + +pub mod error; +mod mubi; +mod perso_tlv; + +pub use mubi::AsMubi; +pub use perso_tlv::{PersoCertificate, PersoTlvType}; diff --git a/target/earlgrey/util/mubi.rs b/target/earlgrey/util/mubi.rs new file mode 100644 index 00000000..51fa77a5 --- /dev/null +++ b/target/earlgrey/util/mubi.rs @@ -0,0 +1,13 @@ +pub trait AsMubi { + fn as_mubi(&self) -> u32; +} + +impl AsMubi for bool { + fn as_mubi(&self) -> u32 { + if *self { + 6 + } else { + 9 + } + } +} diff --git a/target/earlgrey/util/perso_tlv.rs b/target/earlgrey/util/perso_tlv.rs new file mode 100644 index 00000000..8a31bf32 --- /dev/null +++ b/target/earlgrey/util/perso_tlv.rs @@ -0,0 +1,55 @@ +use crate::error::*; +use util_error::ErrorCode; + +#[derive(Clone, Copy, Default)] +#[repr(transparent)] +pub struct PersoTlvType(u8); + +#[allow(non_upper_case_globals)] +impl PersoTlvType { + pub const X509Tbs: PersoTlvType = PersoTlvType(0); + pub const X509Cert: PersoTlvType = PersoTlvType(1); + pub const DevSeed: PersoTlvType = PersoTlvType(2); + pub const CwtCert: PersoTlvType = PersoTlvType(3); + pub const WasTbsHmac: PersoTlvType = PersoTlvType(4); + pub const DeviceId: PersoTlvType = PersoTlvType(5); + pub const GenericSeed: PersoTlvType = PersoTlvType(6); + pub const PersoSha256Hash: PersoTlvType = PersoTlvType(7); +} + +pub struct PersoCertificate<'a> { + pub obj_type: PersoTlvType, + pub obj_size: usize, + pub name: &'a str, + pub certificate: &'a [u8], +} + +impl PersoCertificate<'_> { + pub fn from_bytes<'a>(data: &'a [u8]) -> Result<(PersoCertificate<'a>, &'a [u8]), ErrorCode> { + let type_size = u16::from_be_bytes(data[0..2].try_into().unwrap()); + let namelen_certsz = u16::from_be_bytes(data[2..4].try_into().unwrap()); + let rest = &data[4..]; + + if type_size == 0xFFFF || type_size == 0 { + return Err(EG_ERROR_CERT_NOT_FOUND); + } + // Really should check the object type and return an error if not a certificate. + let obj_type = PersoTlvType((type_size >> 12) as u8); + let obj_size = (type_size & 0x0FFF) as usize; + let namelen = (namelen_certsz >> 12) as usize; + let certsz = (namelen_certsz & 0x0FFF) as usize; + + let name = core::str::from_utf8(&rest[..namelen]).map_err(|_| EG_ERROR_CERT_BAD_NAME)?; + let certificate = &rest[namelen..namelen + certsz]; + let end = (obj_size + 7) & !7; + Ok(( + PersoCertificate { + obj_type, + obj_size, + name, + certificate, + }, + &rest[end..], + )) + } +} From e2e21ac8b7ad04572bca2676051531625ceb7fd5 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 07:49:28 -0700 Subject: [PATCH 31/46] earlgrey: eflash test Signed-off-by: Chris Frantz --- target/earlgrey/tests/eflash/BUILD.bazel | 156 +++++++++++++++++++ target/earlgrey/tests/eflash/flash_server.rs | 75 +++++++++ target/earlgrey/tests/eflash/flash_test.rs | 132 ++++++++++++++++ target/earlgrey/tests/eflash/system.json5 | 86 ++++++++++ target/earlgrey/tests/eflash/target.rs | 29 ++++ 5 files changed, 478 insertions(+) create mode 100644 target/earlgrey/tests/eflash/BUILD.bazel create mode 100644 target/earlgrey/tests/eflash/flash_server.rs create mode 100644 target/earlgrey/tests/eflash/flash_test.rs create mode 100644 target/earlgrey/tests/eflash/system.json5 create mode 100644 target/earlgrey/tests/eflash/target.rs diff --git a/target/earlgrey/tests/eflash/BUILD.bazel b/target/earlgrey/tests/eflash/BUILD.bazel new file mode 100644 index 00000000..b284b49b --- /dev/null +++ b/target/earlgrey/tests/eflash/BUILD.bazel @@ -0,0 +1,156 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "flash_server", + srcs = [ + "flash_server.rs", + ], + codegen_crate_name = "flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/registers:flash_ctrl_core", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_app( + name = "flash_test", + srcs = [ + "flash_test.rs", + ], + codegen_crate_name = "flash_test_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:client", + "//target/earlgrey/util", + "//util/console", + "//util/error", + "//util/ipc", + "@pigweed//pw_base64/rust:pw_base64", + "@pigweed//pw_kernel/lib/pw_assert", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "flash", + apps = [ + ":flash_server", + ":flash_test", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "flash_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":flash", +) + +opentitan_test( + name = "flash_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":flash", +) + +opentitan_test( + name = "flash_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":flash", +) + +opentitan_test( + name = "flash_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":flash", +) diff --git a/target/earlgrey/tests/eflash/flash_server.rs b/target/earlgrey/tests/eflash/flash_server.rs new file mode 100644 index 00000000..8a4e37cb --- /dev/null +++ b/target/earlgrey/tests/eflash/flash_server.rs @@ -0,0 +1,75 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use flash_server_codegen::{handle, signals}; +use pw_status::{Error}; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use hal_flash::{BlockingFlash, FlashAddress}; +use services_flash_server::FlashIpcServer; +use util_types::Blocking; +use util_ipc::IpcChannel; +use util_error::ErrorCode; +use eflash_driver::{EmbeddedFlash, Permission}; + +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + let w = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) + .unwrap(); + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn flash_server() -> Result<(), ErrorCode> { + let mut driver = EmbeddedFlash::new_with_interrupts( + unsafe { flash_ctrl_core::FlashCtrl::new() }); + driver.set_default_permission(Permission::FULL_ACCESS); + for i in 5..9 { + driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + let flash = BlockingFlash { + driver, + blocking: FlashCtrlInterrupt, + }; + let mut flash_server = FlashIpcServer::new(flash); + let mut buf = [0u8; 2056]; + let ipc = IpcChannel::new(handle::FLASH_SERVICE); + flash_server.run(&ipc, &mut buf) +} + +#[entry] +fn entry() -> ! { + pw_log::info!("🔄 RUNNING"); + let ret = flash_server(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { pw_log::info!("✅ PASSED"); Ok(()) } + Err(e) => { pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); Err(Error::Unknown) } + }; + + // Since this is written as a test, shut down with the return status from `main()`. + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/tests/eflash/flash_test.rs b/target/earlgrey/tests/eflash/flash_test.rs new file mode 100644 index 00000000..6dbe358c --- /dev/null +++ b/target/earlgrey/tests/eflash/flash_test.rs @@ -0,0 +1,132 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use flash_test_codegen::handle; +use pw_status::Error; +use userspace::{entry, syscall}; + +use earlgrey_util::PersoCertificate; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::ErrorCode; +use util_ipc::IpcChannel; + +fn get_manifest(flash: &mut FlashIpcClient) -> Result<(), ErrorCode> { + let mut buf = [0u8; 1024]; + flash.read(FlashAddress::data(0), &mut buf)?; + pw_log::info!("ROM_EXT manifest header:"); + util_console::hexdump::hexdump(&buf); + Ok(()) +} + +fn get_certificates(flash: &mut FlashIpcClient) -> Result<(), ErrorCode> { + const BEGIN_CERT: &'static str = "-----BEGIN CERTIFICATE-----"; + const END_CERT: &'static str = "-----END CERTIFICATE-----"; + + let mut buf = [0u8; 1024]; + let mut output = [0u8; 1200]; + + pw_log::info!("Reading UDS cert"); + // Read out the UDS and print it if it exists. + // The UDS (factory) cert is located in bank=0, page=9. + if flash.read(FlashAddress::info(0, 9, 0), &mut buf).is_ok() { + let cert = PersoCertificate::from_bytes(&buf); + if let Ok((uds, _rest)) = cert { + pw_log::info!( + "Certificate: {}\n{}\n{}\n{}", + uds.name, + BEGIN_CERT as &str, + pw_base64::encode_str(uds.certificate, &mut output) + .map_err(ErrorCode::kernel_error)? as &str, + END_CERT as &str, + ); + } + } + + pw_log::info!("Reading CDI certs"); + // Read out the CDI certificates and print them. + // The CDI (dice) certs are located in bank=1, page=9. + let mut offset = 0usize; + loop { + let sz = core::cmp::min(2048 - offset, buf.len()); + flash.read(FlashAddress::info(1, 9, offset as u32), &mut buf[..sz])?; + match PersoCertificate::from_bytes(&buf) { + Ok((cdi, _)) => { + pw_log::info!( + "Certificate: {}\n{}\n{}\n{}", + cdi.name, + BEGIN_CERT as &str, + pw_base64::encode_str(cdi.certificate, &mut output) + .map_err(ErrorCode::kernel_error)? as &str, + END_CERT as &str, + ); + offset += (cdi.obj_size + 7) & !7; + } + Err(_) => break, + } + } + Ok(()) +} + +fn erase_program_test(flash: &mut FlashIpcClient, addr: FlashAddress) -> Result<(), ErrorCode> { + pw_log::info!("Erasing {:08x}", addr.offset()); + flash.erase_page(addr)?; + pw_log::info!("Reading {:08x}", addr.offset()); + let mut buf = [0u8; 32]; + flash.read(addr, &mut buf)?; + util_console::hexdump::hexdump(&buf); + + pw_log::info!("Programming {:08x}", addr.offset()); + flash.program(addr, b"This is a test.")?; + + pw_log::info!("Reading {:08x}", addr.offset()); + flash.read(addr, &mut buf)?; + util_console::hexdump::hexdump(&buf); + + Ok(()) +} + +fn flash_test() -> Result<(), ErrorCode> { + let mut flash = FlashIpcClient::new(IpcChannel::new(handle::FLASH_SERVICE))?; + + pw_log::info!("Flash size: {}", flash.size().get() as usize); + pw_log::info!("Flash page size: {}", flash.page_size().get() as usize); + + get_manifest(&mut flash)?; + get_certificates(&mut flash)?; + + // We're currently executing in SlotA, so we should be able to access SlotB. + erase_program_test(&mut flash, FlashAddress::data(0x90000))?; + erase_program_test(&mut flash, FlashAddress::info(0, 5, 0))?; + Ok(()) +} + +#[entry] +fn entry() -> ! { + pw_log::info!("🔄 RUNNING"); + let ret = flash_test(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ PASSED"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); + Err(Error::Unknown) + } + }; + + // Since this is written as a test, shut down with the return status from `main()`. + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/tests/eflash/system.json5 b/target/earlgrey/tests/eflash/system.json5 new file mode 100644 index 00000000..244068e6 --- /dev/null +++ b/target/earlgrey/tests/eflash/system.json5 @@ -0,0 +1,86 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "flash_server", + flash_size_bytes: 16384, + processes: [ + { + name: "flash_server", + ram_size_bytes: 4096, + objects: [ + { + name: "flash_service", + type: "channel_handler", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + //{ name: "flash_ctrl_prog_empty", number: 160 }, + //{ name: "flash_ctrl_prog_lvl", number: 161 }, + //{ name: "flash_ctrl_rd_full", number: 162 }, + //{ name: "flash_ctrl_rd_lvl", number: 163 }, + { name: "flash_ctrl_op_done", number: 164 }, + //{ name: "flash_ctrl_corr_err", number: 165 }, + ], + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + } + ], + threads: [ + { + name: "flash_server_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + + { + name: "flash_test", + flash_size_bytes: 16384, + processes: [ + { + name: "flash_test", + ram_size_bytes: 6144, + objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "flash_service", + }, + ], + threads: [ + { + name: "flash_test_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + + ], +} diff --git a/target/earlgrey/tests/eflash/target.rs b/target/earlgrey/tests/eflash/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/tests/eflash/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); From d790a4a1eca89520ad8bbd2c119e4c699812960c Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 07:58:35 -0700 Subject: [PATCH 32/46] tooling: pass --test_arg to test runner Signed-off-by: Chris Frantz --- target/earlgrey/tooling/opentitan_runner.bzl | 2 +- target/earlgrey/tooling/opentitan_runner.py | 26 +++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/target/earlgrey/tooling/opentitan_runner.bzl b/target/earlgrey/tooling/opentitan_runner.bzl index fcde8db6..4c9dac21 100644 --- a/target/earlgrey/tooling/opentitan_runner.bzl +++ b/target/earlgrey/tooling/opentitan_runner.bzl @@ -54,7 +54,7 @@ def _opentitan_runner_impl(ctx): output = run_script, is_executable = True, content = """#!/bin/bash -{runner} --interface {interface} {load_bitstream} {mechanism} --elf {elf} --bin {bin} {optional_args} +{runner} --interface {interface} {load_bitstream} {mechanism} --elf {elf} --bin {bin} {optional_args} "$@" """.format( runner = runner.short_path, interface = ctx.attr.interface, diff --git a/target/earlgrey/tooling/opentitan_runner.py b/target/earlgrey/tooling/opentitan_runner.py index cb40f8bc..f1eecdac 100755 --- a/target/earlgrey/tooling/opentitan_runner.py +++ b/target/earlgrey/tooling/opentitan_runner.py @@ -64,6 +64,7 @@ def _parse_args(): action=argparse.BooleanOptionalAction, help="load a bitstream into the FPGA board", ) + parser.add_argument( "--mechanism", type=str, @@ -81,6 +82,13 @@ def _parse_args(): type=pathlib.Path, help="bin file", ) + parser.add_argument( + "--timestamp", + type=bool, + default=True, + action=argparse.BooleanOptionalAction, + help="Display a timestamp per line of console output", + ) parser.add_argument( "--exit-success", type=str, @@ -172,7 +180,12 @@ def load_bitstream(interface: str): def load_and_run( - image: Path, interface: str, mechanism: str, exit_success: str, exit_failure: str + image: Path, + interface: str, + mechanism: str, + exit_success: str, + exit_failure: str, + timestamp: bool, ) -> list[str]: """Prepare opentitantool arguments to load an image into a board and spawn a console.""" if interface == "verilator": @@ -209,7 +222,9 @@ def load_and_run( else: raise Exception("unknown mechanism", mechanism) - console_command = ["console", "--timestamp"] + console_command = ["console"] + if timestamp: + console_command.append("--timestamp") if exit_success: console_command.append(f"--exit-success={exit_success}") if exit_failure: @@ -280,7 +295,12 @@ def _main(args) -> int: load_bitstream(args.interface) cmd = load_and_run( - args.bin, args.interface, args.mechanism, args.exit_success, args.exit_failure + args.bin, + args.interface, + args.mechanism, + args.exit_success, + args.exit_failure, + args.timestamp, ) # TODO(cfrantz): add support for the tokenized console. return_code = simple_console(cmd) From 22bf35aa333b7f1668db2f7f017db239c275a3f2 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 07:59:36 -0700 Subject: [PATCH 33/46] earlgrey: make platform public Signed-off-by: Chris Frantz --- target/earlgrey/BUILD.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/target/earlgrey/BUILD.bazel b/target/earlgrey/BUILD.bazel index ccad4b87..a0dbc348 100644 --- a/target/earlgrey/BUILD.bazel +++ b/target/earlgrey/BUILD.bazel @@ -29,7 +29,7 @@ platform( "@pigweed//pw_log/rust:pw_log_backend": "@pigweed//pw_kernel:log_backend_basic", }, ), - visibility = [":__subpackages__"], + visibility = ["//visibility:public"], ) string_flag( @@ -60,7 +60,7 @@ config_setting( constraint_value( name = "target_earlgrey", constraint_setting = "@pigweed//pw_kernel/target:target", - visibility = [":__subpackages__"], + visibility = ["//visibility:public"], ) rust_library( From 8f29226e18cc55c305832f6288f9222e42c08eb0 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 14:54:14 -0700 Subject: [PATCH 34/46] usb_driver --- target/earlgrey/drivers/usb_driver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/target/earlgrey/drivers/usb_driver.rs b/target/earlgrey/drivers/usb_driver.rs index 26d04580..85cd947b 100644 --- a/target/earlgrey/drivers/usb_driver.rs +++ b/target/earlgrey/drivers/usb_driver.rs @@ -7,7 +7,7 @@ use aligned::A4; use aligned::Aligned; use core::cmp::min; use util_regcpy::{copy_to_reg_array, copy_to_reg_array_unaligned, copy_from_reg_array_unaligned}; -use console::traceln; +use util_console::traceln; use hal_usb::SetupPacket; use hal_usb::driver::UsbDriver; use hal_usb::driver::UsbEvent; From 073163255df366c92661fa007ecf91df33d678f7 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 14:54:44 -0700 Subject: [PATCH 35/46] tests: enhance usbdfu test Signed-off-by: Chris Frantz --- target/earlgrey/tests/usbdfu/BUILD.bazel | 30 ++++++ target/earlgrey/tests/usbdfu/flash_server.rs | 75 ++++++++++++++ target/earlgrey/tests/usbdfu/system.json5 | 51 +++++++++- target/earlgrey/tests/usbdfu/test_usb.rs | 100 +++++++++++++++---- 4 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 target/earlgrey/tests/usbdfu/flash_server.rs diff --git a/target/earlgrey/tests/usbdfu/BUILD.bazel b/target/earlgrey/tests/usbdfu/BUILD.bazel index 92bfc8dd..f895b54c 100644 --- a/target/earlgrey/tests/usbdfu/BUILD.bazel +++ b/target/earlgrey/tests/usbdfu/BUILD.bazel @@ -21,14 +21,19 @@ rust_app( tags = ["kernel"], visibility = ["//visibility:public"], deps = [ + "//hal/blocking/flash", "//hal/blocking/usb:hal_usb", "//protocol/usb/dfu", "//protocol/usb/stack", + "//services/flash:client", "//target/earlgrey/drivers:usb_driver", "//target/earlgrey/registers:pinmux", "//target/earlgrey/registers:top_earlgrey", "//target/earlgrey/registers:usbdev", + "//target/earlgrey/util", "//util/console", + "//util/error", + "//util/ipc", "@pigweed//pw_kernel/userspace", "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", @@ -36,10 +41,35 @@ rust_app( ], ) +rust_app( + name = "flash_server", + srcs = [ + "flash_server.rs", + ], + codegen_crate_name = "flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/registers:flash_ctrl_core", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + system_image( name = "usb", apps = [ ":test_usb_dfu", + ":flash_server", ], kernel = ":target", platform = "//target/earlgrey", diff --git a/target/earlgrey/tests/usbdfu/flash_server.rs b/target/earlgrey/tests/usbdfu/flash_server.rs new file mode 100644 index 00000000..8a4e37cb --- /dev/null +++ b/target/earlgrey/tests/usbdfu/flash_server.rs @@ -0,0 +1,75 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use flash_server_codegen::{handle, signals}; +use pw_status::{Error}; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use hal_flash::{BlockingFlash, FlashAddress}; +use services_flash_server::FlashIpcServer; +use util_types::Blocking; +use util_ipc::IpcChannel; +use util_error::ErrorCode; +use eflash_driver::{EmbeddedFlash, Permission}; + +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + let w = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) + .unwrap(); + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn flash_server() -> Result<(), ErrorCode> { + let mut driver = EmbeddedFlash::new_with_interrupts( + unsafe { flash_ctrl_core::FlashCtrl::new() }); + driver.set_default_permission(Permission::FULL_ACCESS); + for i in 5..9 { + driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + let flash = BlockingFlash { + driver, + blocking: FlashCtrlInterrupt, + }; + let mut flash_server = FlashIpcServer::new(flash); + let mut buf = [0u8; 2056]; + let ipc = IpcChannel::new(handle::FLASH_SERVICE); + flash_server.run(&ipc, &mut buf) +} + +#[entry] +fn entry() -> ! { + pw_log::info!("🔄 RUNNING"); + let ret = flash_server(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { pw_log::info!("✅ PASSED"); Ok(()) } + Err(e) => { pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); Err(Error::Unknown) } + }; + + // Since this is written as a test, shut down with the return status from `main()`. + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/tests/usbdfu/system.json5 b/target/earlgrey/tests/usbdfu/system.json5 index 874c763b..c85aeeaa 100644 --- a/target/earlgrey/tests/usbdfu/system.json5 +++ b/target/earlgrey/tests/usbdfu/system.json5 @@ -14,13 +14,62 @@ }, }, apps: [ + + { + name: "flash_server", + flash_size_bytes: 16384, + processes: [ + { + name: "flash_server", + ram_size_bytes: 4096, + objects: [ + { + name: "flash_service", + type: "channel_handler", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + //{ name: "flash_ctrl_prog_empty", number: 160 }, + //{ name: "flash_ctrl_prog_lvl", number: 161 }, + //{ name: "flash_ctrl_rd_full", number: 162 }, + //{ name: "flash_ctrl_rd_lvl", number: 163 }, + { name: "flash_ctrl_op_done", number: 164 }, + //{ name: "flash_ctrl_corr_err", number: 165 }, + ], + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + } + ], + threads: [ + { + name: "flash_server_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, { name: "test_usb_dfu", flash_size_bytes: 16384, processes: [{ name: "test_usb_dfu_process", - ram_size_bytes: 4096, + ram_size_bytes: 6144, objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "flash_service", + }, { name: "usbdev_interrupts", type: "interrupt", diff --git a/target/earlgrey/tests/usbdfu/test_usb.rs b/target/earlgrey/tests/usbdfu/test_usb.rs index 039eb8f7..e5f2ac08 100644 --- a/target/earlgrey/tests/usbdfu/test_usb.rs +++ b/target/earlgrey/tests/usbdfu/test_usb.rs @@ -17,12 +17,21 @@ use hal_usb::driver::UsbDriver; use usb_driver::UsbConfig; use usb_stack::{DescriptorSource, UsbAction, UsbClass}; +use earlgrey_util::PersoCertificate; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::{self as error, ErrorCode}; +use util_ipc::IpcChannel; + use protocol_usb_dfu::{DfuBuilder, DfuClass, DfuHandler, DfuResult, DfuStatus}; const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); -const USB_DFU_INTERFACE_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); +const DFU_FIRMWARE_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); +const DFU_UDS_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(5); +const DFU_CDI0_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(6); +const DFU_CDI1_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(7); const DFU_BUILDER: DfuBuilder = DfuBuilder::new( 0, // interface_num @@ -48,11 +57,13 @@ const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { max_power: 250, self_powered: false, remote_wakeup: false, - interfaces: &[DFU_BUILDER.interface( - 0, - USB_DFU_INTERFACE_HANDLE, - &[DFU_BUILDER.functional_descriptor()], - )], + interfaces: &[ + DFU_BUILDER.interface( 0, DFU_FIRMWARE_HANDLE, &[]), + DFU_BUILDER.interface( 1, DFU_UDS_CERT_HANDLE, &[]), + DFU_BUILDER.interface( 2, DFU_CDI0_CERT_HANDLE, &[]), + DFU_BUILDER.interface( 3, DFU_CDI1_CERT_HANDLE, + &[DFU_BUILDER.functional_descriptor()]), + ], }; const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { @@ -65,7 +76,10 @@ const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { const VENDOR_ID: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Google Inc.").as_ref(); const PRODUCT_ID_DEFAULT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Earlgrey DFU").as_ref(); -const USB_DFU: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("DFU Interface").as_ref(); +const DFU_FIRMWARE: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Firmware").as_ref(); +const DFU_UDS_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("UDS Certificate").as_ref(); +const DFU_CDI0_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDI0 Certificate").as_ref(); +const DFU_CDI1_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDI1 Certificate").as_ref(); struct MyDescriptors<'a> { serial_desc_bytes: StringDescriptorRef<'a>, @@ -89,13 +103,49 @@ impl DescriptorSource for MyDescriptors<'_> { USB_VENDOR_HANDLE => Some(VENDOR_ID), USB_PRODUCT_HANDLE => Some(self.product_desc_bytes), USB_SERIAL_HANDLE => Some(self.serial_desc_bytes), - USB_DFU_INTERFACE_HANDLE => Some(USB_DFU), + DFU_FIRMWARE_HANDLE => Some(DFU_FIRMWARE), + DFU_UDS_CERT_HANDLE => Some(DFU_UDS_CERT), + DFU_CDI0_CERT_HANDLE => Some(DFU_CDI0_CERT), + DFU_CDI1_CERT_HANDLE => Some(DFU_CDI1_CERT), _ => None, } } } -struct MyDfuHandler; +fn get_certificate(flash: &mut FlashIpcClient, n: u8, data: &mut [u8]) -> Result { + pw_log::info!("Reading certificate {}", n as usize); + let (partition, mut n) = match n { + 0 => (0, 0), // The UDS (dice) cert is located in bank=0, page=9. + 1 => (1, 0), // The CDI (dice) certs are located in bank=1, page=9. + 2 => (1, 1), + _ => return Err(DfuStatus::ErrFile), + }; + let mut offset = 0usize; + let mut buf = [0u8; 1024]; + loop { + let sz = core::cmp::min(2048 - offset, buf.len()); + flash.read(FlashAddress::info(partition, 9, offset as u32), &mut buf[..sz]).map_err(|_| DfuStatus::ErrUnknown)?; + match PersoCertificate::from_bytes(&buf) { + Ok((cert, _)) => { + if n == 0 { + let len = cert.certificate.len(); + pw_log::info!("Found cert: {} bytes", len as usize); + data[..len].copy_from_slice(cert.certificate); + return Ok(len); + } + offset += (cert.obj_size + 7) & !7; + n -= 1; + } + Err(_) => break, + } + } + Err(DfuStatus::ErrUnknown) +} + + +struct MyDfuHandler { + flash: FlashIpcClient, +} impl DfuHandler for MyDfuHandler { fn dnload(&mut self, alt: u8, block_num: u16, data: &[u8]) -> DfuResult { @@ -115,11 +165,10 @@ impl DfuHandler for MyDfuHandler { block_num, data.len() ); - // Send a 2K block with a fixed pattern - for (i, byte) in data.iter_mut().enumerate() { - *byte = (i & 0xFF) as u8; + match alt { + 1|2|3 => get_certificate(&mut self.flash, alt-1, data), + _ => Err(DfuStatus::ErrFile), } - Ok(data.len()) } fn manifest(&mut self) -> DfuResult { @@ -132,7 +181,7 @@ impl DfuHandler for MyDfuHandler { } } -fn handle_usb() -> Result<(), Error> { +fn handle_usb() -> Result<(), ErrorCode> { let mut serial_num_buffer = Aligned::([0_u8; 130]); let descriptors = MyDescriptors { serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned( @@ -147,7 +196,9 @@ fn handle_usb() -> Result<(), Error> { let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); let mut ep0 = usb_stack::SimpleEp0::new(); - let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, MyDfuHandler); + let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, MyDfuHandler { + flash: FlashIpcClient::new(IpcChannel::new(handle::FLASH_SERVICE))?, + }); loop { let wait_return = syscall::object_wait( @@ -168,11 +219,11 @@ fn handle_usb() -> Result<(), Error> { | signals::USBDEV_FRAME | signals::USBDEV_AV_SETUP_EMPTY, Instant::MAX, - )?; + ).map_err(ErrorCode::kernel_error)?; if wait_return.user_data != 0 { pw_log::error!("Incorrect WaitReturn values"); - return Err(Error::Unknown); + return Err(error::KERNEL_ERROR_UNKNOWN); } while let Some(event) = usb.poll() { @@ -201,8 +252,23 @@ fn usb_setup_pinmux() { #[entry] fn entry() -> ! { + pw_log::info!("🔄 RUNNING"); usb_setup_pinmux(); let ret = handle_usb(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ PASSED"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); + Err(Error::Unknown) + } + }; + + // Since this is written as a test, shut down with the return status from `main()`. let _ = syscall::debug_shutdown(ret); loop {} } From 92f501703c0847245eb5a0fa720ee52e9d139c8b Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 21:31:24 -0700 Subject: [PATCH 36/46] earlgrey: datatypes Signed-off-by: Chris Frantz --- target/earlgrey/util/BUILD.bazel | 9 ++ target/earlgrey/util/boot_log.rs | 70 +++++++++++ target/earlgrey/util/boot_svc.rs | 187 ++++++++++++++++++++++++++++++ target/earlgrey/util/error.rs | 2 + target/earlgrey/util/lib.rs | 23 ++++ target/earlgrey/util/misc.rs | 32 +++++ target/earlgrey/util/ret_ram.rs | 26 +++++ target/earlgrey/util/rom_error.rs | 152 ++++++++++++++++++++++++ target/earlgrey/util/tags.rs | 130 +++++++++++++++++++++ 9 files changed, 631 insertions(+) create mode 100644 target/earlgrey/util/boot_log.rs create mode 100644 target/earlgrey/util/boot_svc.rs create mode 100644 target/earlgrey/util/misc.rs create mode 100644 target/earlgrey/util/ret_ram.rs create mode 100644 target/earlgrey/util/rom_error.rs create mode 100644 target/earlgrey/util/tags.rs diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel index b1851155..091f9244 100644 --- a/target/earlgrey/util/BUILD.bazel +++ b/target/earlgrey/util/BUILD.bazel @@ -5,16 +5,25 @@ package(default_visibility = ["//visibility:public"]) rust_library( name = "util", srcs = [ + "boot_log.rs", + "boot_svc.rs", "error.rs", "lib.rs", + "misc.rs", "mubi.rs", "perso_tlv.rs", + "ret_ram.rs", + "rom_error.rs", + "tags.rs", ], crate_name = "earlgrey_util", edition = "2024", deps = [ "//util/error", + "//util/types", "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:ufmt", + "@rust_crates//:zerocopy", ], ) diff --git a/target/earlgrey/util/boot_log.rs b/target/earlgrey/util/boot_log.rs new file mode 100644 index 00000000..652ab63b --- /dev/null +++ b/target/earlgrey/util/boot_log.rs @@ -0,0 +1,70 @@ +use ufmt::derive::uDebug; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::misc::UnalignedU64; +use crate::tags::{BootSlot, HardenedBool, OwnershipState}; +use crate::CheckDigest; + +/// The BootLog provides information about how the ROM and ROM_EXT +/// booted the chip. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout, uDebug)] +#[repr(C)] +pub struct BootLog { + /// A SHA256 digest over all other fields in this struct. + pub digest: [u8; 32], + /// A tag that identifies this struct as the boot log ('BLOG'). + pub identifier: u32, + /// The chip version (a git hash prefix from the ROM). + pub chip_version: UnalignedU64, + /// The boot slot the ROM chose to boot the ROM_EXT. + pub rom_ext_slot: BootSlot, + /// The ROM_EXT major version number. + pub rom_ext_major: u32, + /// The ROM_EXT minor version number. + pub rom_ext_minor: u32, + /// The ROM_EXT size in bytes. + pub rom_ext_size: u32, + /// The ROM_EXT nonce (a value used to prevent replay of signed commands). + pub rom_ext_nonce: UnalignedU64, + /// The boot slot the ROM_EXT chose to boot the owner firmware. + pub bl0_slot: BootSlot, + /// The chip's ownership state. + pub ownership_state: OwnershipState, + /// The number of ownership transfers performed on this chip. + pub ownership_transfers: u32, + /// Minimum security version permitted for ROM_EXT payloads. + pub rom_ext_min_sec_ver: u32, + /// Minimum security version permitted for application payloads. + pub bl0_min_sec_ver: u32, + /// The primary BL0 boot slot. + pub primary_bl0_slot: BootSlot, + /// Whether the retention RAM was initialized on this boot. + pub retention_ram_initialized: HardenedBool, + /// Reserved for future use. + pub reserved: [u32; 8], +} + +impl CheckDigest for BootLog { + fn check_digest(&self, f: F) -> bool + where + F: Fn(&[u8]) -> [u8; 32], + { + let digest = f(&self.as_bytes()[32..]); + for (a, b) in self.digest.iter().zip(digest.iter().rev()) { + if *a != *b { + return false; + } + } + return true; + } + + fn set_digest(&mut self, f: F) + where + F: Fn(&[u8]) -> [u8; 32], + { + let digest = f(&self.as_bytes()[32..]); + for (a, b) in self.digest.iter_mut().zip(digest.iter().rev()) { + *a = *b; + } + } +} diff --git a/target/earlgrey/util/boot_svc.rs b/target/earlgrey/util/boot_svc.rs new file mode 100644 index 00000000..fc17ae64 --- /dev/null +++ b/target/earlgrey/util/boot_svc.rs @@ -0,0 +1,187 @@ +use core::mem::size_of; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unalign}; + +use crate::rom_error::RomError; +use crate::tags::{BootSlot, BootSvcKind, HardenedBool, OwnershipKeyAlg, UnlockMode}; +use crate::{CheckDigest, GetData}; + +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +/// The Boot Services header common to all boot services commands and responses. +pub struct BootSvc { + /// A SHA256 digest over the rest of the boot services message. + pub digest: [u8; 32], + /// A tag that identifies this struct as a boot services message ('BSVC'). + pub identifier: u32, + /// The type of boot services message that follows this header. + pub kind: BootSvcKind, + /// The length of the boot services message in bytes (including the header). + pub length: u32, + /// The message data. + pub data: [u8; 212], +} + +impl BootSvc { + const HEADER_LEN: usize = 44; + const TAG: u32 = u32::from_le_bytes(*b"BSVC"); +} + +impl CheckDigest for BootSvc { + fn check_digest(&self, f: F) -> bool + where + F: Fn(&[u8]) -> [u8; 32], + { + let digest = f(&self.as_bytes()[32..self.length as usize]); + for (a, b) in self.digest.iter().zip(digest.iter().rev()) { + if *a != *b { + return false; + } + } + return true; + } + + fn set_digest(&mut self, f: F) + where + F: Fn(&[u8]) -> [u8; 32], + { + let digest = f(&self.as_bytes()[32..self.length as usize]); + for (a, b) in self.digest.iter_mut().zip(digest.iter().rev()) { + *a = *b; + } + } +} + +macro_rules! impl_getdata { + ($t:ident, $tag:ident) => { + impl GetData<$t> for BootSvc { + fn get(&self) -> Option<&$t> { + if self.identifier != BootSvc::TAG + || self.length as usize != BootSvc::HEADER_LEN + size_of::<$t>() + || self.kind != BootSvcKind::$tag + { + return None; + } + let (result, _) = <$t>::ref_from_prefix(&self.data).unwrap(); + Some(result) + } + fn get_mut(&mut self) -> &mut $t { + self.identifier = BootSvc::TAG; + self.length = (BootSvc::HEADER_LEN + size_of::<$t>()) as u32; + self.kind = BootSvcKind::$tag; + let (result, _) = <$t>::mut_from_prefix(&mut self.data).unwrap(); + result + } + } + }; + ($t:ident) => { + impl_getdata!($t, $t); + }; +} + +/// An empty boot services message. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct Empty { + pub payload: [u8; 212], +} +impl_getdata!(Empty, EmptyRequest); + +/// Request to set the minimum owner stage firmware version. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct MinBl0SecVerRequest { + /// The desired minimum BL0 version. + pub ver: u32, +} +impl_getdata!(MinBl0SecVerRequest); + +/// Response to the minimum version request. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct MinBl0SecVerResponse { + /// The current minimum BL0 version. + pub ver: u32, + /// The status response to the request. + pub status: RomError, +} +impl_getdata!(MinBl0SecVerResponse); + +/// Request to set the next (one-time) owner stage boot slot. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct NextBl0SlotRequest { + /// The slot to boot. + pub next_bl0_slot: BootSlot, + /// The slot to configure as primary. + pub primary_bl0_slot: BootSlot, +} +impl_getdata!(NextBl0SlotRequest); + +/// Response to the set next boot slot request. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct NextBl0SlotResponse { + /// The status response to the request. + pub status: RomError, + /// The current primary slot. + pub primary_bl0_slot: BootSlot, +} +impl_getdata!(NextBl0SlotResponse); + +/// Request to unlock ownership of the chip. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct OwnershipUnlockRequest { + /// The desired unlock mode. + pub unlock_mode: UnlockMode, + /// The Device Identification Number of the chip. + pub din: Unalign, + /// Reserved for future use. + pub reserved: [u32; 7], + /// The algorithm of next owner's key (for unlock Endorsed mode). + pub next_owner_alg: OwnershipKeyAlg, + /// The ROM_EXT nonce. + pub nonce: Unalign, + /// The next owner's key (for unlock Endorsed mode). + pub next_owner_key: [u8; 96], + /// A signature over [unlock_mode..next_owner_key] with the current owner unlock key. + pub signature: [u8; 64], +} +impl_getdata!(OwnershipUnlockRequest); + +/// Response to the ownership unlock command. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct OwnershipUnlockResponse { + /// The status response to the request. + pub status: RomError, +} +impl_getdata!(OwnershipUnlockResponse); + +/// Request to activate ownership of the chip. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct OwnershipActivateRequest { + /// The new primary boot slot after activating ownership. + pub primary_bl0_slot: BootSlot, + /// The Device Identification Number of the chip. + pub din: Unalign, + /// Whether to erase the previous owner's data during activation. + pub erase_previous: HardenedBool, + /// Reserved for future use. + pub reserved: [u32; 31], + /// The ROM_EXT nonce. + pub nonce: Unalign, + /// A signature over [primary_bl0_slot..nonce] with the next owner's activate key. + pub signature: [u8; 64], +} +impl_getdata!(OwnershipActivateRequest); + +/// Response to the ownership activate command. +#[derive(Clone, FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct OwnershipActivateResponse { + /// The status response to the request. + pub status: RomError, +} +impl_getdata!(OwnershipActivateResponse); diff --git a/target/earlgrey/util/error.rs b/target/earlgrey/util/error.rs index 7b203244..035f1372 100644 --- a/target/earlgrey/util/error.rs +++ b/target/earlgrey/util/error.rs @@ -9,3 +9,5 @@ use util_error::{ErrorCode, ErrorModule}; pub const EG_ERROR: ErrorModule = ErrorModule::new(0x464c); //ascii `FL`. pub const EG_ERROR_CERT_NOT_FOUND: ErrorCode = EG_ERROR.from_pw(1, Error::NotFound); pub const EG_ERROR_CERT_BAD_NAME: ErrorCode = EG_ERROR.from_pw(2, Error::InvalidArgument); +pub const EG_ERROR_BAD_BOOT_LOG: ErrorCode = EG_ERROR.from_pw(3, Error::Unknown); +pub const EG_ERROR_BOOT_SLOT_UNKNOWN: ErrorCode = EG_ERROR.from_pw(4, Error::Unknown); diff --git a/target/earlgrey/util/lib.rs b/target/earlgrey/util/lib.rs index 50ca0cdf..2e8399c9 100644 --- a/target/earlgrey/util/lib.rs +++ b/target/earlgrey/util/lib.rs @@ -1,8 +1,31 @@ #![no_std] +pub mod boot_log; +pub mod boot_svc; pub mod error; +mod misc; mod mubi; mod perso_tlv; +pub mod ret_ram; +pub mod rom_error; +pub mod tags; pub use mubi::AsMubi; pub use perso_tlv::{PersoCertificate, PersoTlvType}; + +pub trait CheckDigest { + /// Check the digest on a data structure by calling `f` to generate a digest. + fn check_digest(&self, f: F) -> bool + where + F: Fn(&[u8]) -> [u8; 32]; + + /// Set the digest on a data structure by calling `f` to generate the digest. + fn set_digest(&mut self, f: F) + where + F: Fn(&[u8]) -> [u8; 32]; +} + +pub trait GetData { + fn get(&self) -> Option<&T>; + fn get_mut(&mut self) -> &mut T; +} diff --git a/target/earlgrey/util/misc.rs b/target/earlgrey/util/misc.rs new file mode 100644 index 00000000..b15733b1 --- /dev/null +++ b/target/earlgrey/util/misc.rs @@ -0,0 +1,32 @@ +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unalign}; + +#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] +pub struct UnalignedU64(Unalign); + +impl UnalignedU64 { + pub fn get(&self) -> u64 { + self.0.get() + } + pub fn set(&mut self, v: u64) { + self.0.set(v) + } +} + +impl ufmt::uDisplay for UnalignedU64 { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + let v = self.get(); + ufmt::uwrite!(f, "{:016x}", v) + } +} + +impl ufmt::uDebug for UnalignedU64 { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + ufmt::uDisplay::fmt(self, f) + } +} diff --git a/target/earlgrey/util/ret_ram.rs b/target/earlgrey/util/ret_ram.rs new file mode 100644 index 00000000..48787186 --- /dev/null +++ b/target/earlgrey/util/ret_ram.rs @@ -0,0 +1,26 @@ +use crate::boot_log::BootLog; +use crate::boot_svc::BootSvc; +use crate::rom_error::RomError; +use crate::tags::RetRamVersion; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct RetRam { + pub version: RetRamVersion, + pub reset_reasons: u32, + pub boot_svc: BootSvc, + pub reserved: [u8; 1652], + pub boot_log: BootLog, + pub last_shutdown_reason: RomError, + pub owner: [u8; 2048], +} + +impl RetRam { + pub unsafe fn mut_ref() -> &'static mut RetRam { + unsafe { + let rr = core::slice::from_raw_parts_mut(0x4060_0000 as *mut u8, 4096); + RetRam::mut_from_bytes(rr).unwrap() + } + } +} diff --git a/target/earlgrey/util/rom_error.rs b/target/earlgrey/util/rom_error.rs new file mode 100644 index 00000000..c8f5a0e6 --- /dev/null +++ b/target/earlgrey/util/rom_error.rs @@ -0,0 +1,152 @@ +#![allow(non_upper_case_globals)] + +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct RomError(pub u32); +impl RomError { + pub const Ok: Self = Self(1849); + pub const WriteBootdataThenReboot: Self = Self(746); + pub const NoData: Self = Self(1239); + pub const Unknown: Self = Self(4294967295); + pub const SigverifyBadRsaSignature: Self = Self(22238723); + pub const SigverifyBadSpxSignature: Self = Self(39015939); + pub const SigverifyBadKey: Self = Self(55793155); + pub const SigverifyBadRsaKey: Self = Self(72570371); + pub const SigverifyBadSpxKey: Self = Self(89347587); + pub const SigverifyLargeRsaSignature: Self = Self(106124803); + pub const SigverifyBadEcdsaSignature: Self = Self(122902019); + pub const SigverifyBadAuthPartition: Self = Self(139679235); + pub const SigverifyBadEcdsaKey: Self = Self(156456451); + pub const SigverifyBadSpxConfig: Self = Self(173233667); + pub const SigverifyEcdsaNotFound: Self = Self(190010885); + pub const SigverifySpxNotFound: Self = Self(206788101); + pub const KeymgrInternal: Self = Self(21712141); + pub const ManifestBadEntryPoint: Self = Self(21840141); + pub const ManifestBadCodeRegion: Self = Self(38617357); + pub const ManifestBadSignedRegion: Self = Self(55394573); + pub const ManifestBadExtension: Self = Self(72171789); + pub const ManifestBadVersionMajor: Self = Self(88949005); + pub const AlertBadIndex: Self = Self(21055491); + pub const AlertBadClass: Self = Self(37832707); + pub const AlertBadEnable: Self = Self(54609923); + pub const AlertBadEscalation: Self = Self(71387139); + pub const AlertBadCrc32: Self = Self(88164355); + pub const RomBootFailed: Self = Self(21844489); + pub const RomResetReasonFault: Self = Self(38621698); + pub const RomImmSection: Self = Self(55398915); + pub const Interrupt: Self = Self(4805122); + pub const EpmpBadCheck: Self = Self(21319693); + pub const KmacInvalidStatus: Self = Self(21709581); + pub const KmacInvalidKeySize: Self = Self(38486787); + pub const OtbnInvalidArgument: Self = Self(21122563); + pub const OtbnBadOffsetLen: Self = Self(37899779); + pub const OtbnExecutionFailed: Self = Self(54677005); + pub const OtbnSecWipeImemFailed: Self = Self(71454221); + pub const OtbnSecWipeDmemFailed: Self = Self(88231437); + pub const OtbnBadInsnCount: Self = Self(105008653); + pub const OtbnUnavailable: Self = Self(121785869); + pub const FlashCtrlDataRead: Self = Self(21381901); + pub const FlashCtrlInfoRead: Self = Self(38159117); + pub const FlashCtrlDataWrite: Self = Self(54936333); + pub const FlashCtrlInfoWrite: Self = Self(71713549); + pub const FlashCtrlDataErase: Self = Self(88490765); + pub const FlashCtrlInfoErase: Self = Self(105267981); + pub const FlashCtrlDataEraseVerify: Self = Self(122045197); + pub const BootPolicyBadIdentifier: Self = Self(21123085); + pub const BootPolicyBadLength: Self = Self(37900301); + pub const BootPolicyRollback: Self = Self(54677517); + pub const BootstrapEraseAddress: Self = Self(21123843); + pub const BootstrapProgramAddress: Self = Self(37901059); + pub const BootstrapInvalidState: Self = Self(54678275); + pub const BootstrapNotRequested: Self = Self(71455501); + pub const BootstrapDisabledRomExt: Self = Self(88232717); + pub const LogBadFormatSpecifier: Self = Self(21776141); + pub const BootDataNotFound: Self = Self(21120013); + pub const BootDataWriteCheck: Self = Self(37897229); + pub const BootDataInvalid: Self = Self(54674445); + pub const SpiDevicePayloadOverflow: Self = Self(22237197); + pub const AstInitNotDone: Self = Self(21058317); + pub const RstmgrBadInit: Self = Self(22172429); + pub const RndBadCrc32: Self = Self(22171139); + pub const BootSvcBadHeader: Self = Self(21119757); + pub const BootSvcBadSlot: Self = Self(37896963); + pub const BootSvcBadSecVer: Self = Self(54674179); + pub const RomExtBootFailed: Self = Self(22168841); + pub const XModemTimeoutStart: Self = Self(22564100); + pub const XModemTimeoutPacket: Self = Self(39341316); + pub const XModemTimeoutData: Self = Self(56118532); + pub const XModemTimeoutCrc: Self = Self(72895748); + pub const XModemTimeoutAck: Self = Self(89672964); + pub const XModemCrc: Self = Self(106450191); + pub const XModemEndOfFile: Self = Self(123227403); + pub const XModemCancel: Self = Self(140004609); + pub const XModemUnknown: Self = Self(156781826); + pub const XModemProtocol: Self = Self(173559043); + pub const XModemTooManyErrors: Self = Self(190336265); + pub const XModemBadLength: Self = Self(207113475); + pub const RomExtInterrupt: Self = Self(5392642); + pub const BootLogInvalid: Self = Self(21122061); + pub const Asn1Internal: Self = Self(21049613); + pub const Asn1StartInvalidArgument: Self = Self(37826819); + pub const Asn1PushBytesInvalidArgument: Self = Self(54604035); + pub const Asn1PushIntegerPadInvalidArgument: Self = Self(71381251); + pub const Asn1PushIntegerInvalidArgument: Self = Self(88158467); + pub const Asn1FinishBitstringInvalidArgument: Self = Self(104935683); + pub const Asn1BufferExhausted: Self = Self(121712904); + pub const RetRamBadVersion: Self = Self(22172162); + pub const RescueReboot: Self = Self(5395213); + pub const RescueBadMode: Self = Self(22172419); + pub const RescueImageTooBig: Self = Self(38949641); + pub const RescueSendStart: Self = Self(72504077); + pub const RescueInactivity: Self = Self(89281284); + pub const CertInternal: Self = Self(4408589); + pub const CertInvalidArgument: Self = Self(21185795); + pub const CertInvalidSize: Self = Self(37963023); + pub const OwnershipInvalidNonce: Self = Self(5199619); + pub const OwnershipInvalidMode: Self = Self(21976835); + pub const OwnershipInvalidSignature: Self = Self(38754051); + pub const OwnershipInvalidState: Self = Self(55531267); + pub const OwnershipInvalidRequest: Self = Self(72308483); + pub const OwnershipInvalidTag: Self = Self(89085699); + pub const OwnershipInvalidTagLength: Self = Self(105862915); + pub const OwnershipDuplicateItem: Self = Self(122640134); + pub const OwnershipFlashConfigLength: Self = Self(139417355); + pub const OwnershipInvalidInfoPage: Self = Self(156194563); + pub const OwnershipBadInfoPage: Self = Self(172971789); + pub const OwnershipNoOwner: Self = Self(189749005); + pub const OwnershipKeyNotFound: Self = Self(206526213); + pub const OwnershipInvalidDin: Self = Self(223303427); + pub const OwnershipUnlockDenied: Self = Self(240080647); + pub const OwnershipFlashConfigRomExt: Self = Self(256857859); + pub const OwnershipFlashConfigBounds: Self = Self(273635075); + pub const OwnershipInvalidAlgorithm: Self = Self(290412291); + pub const OwnershipFlashConfigSlots: Self = Self(307189507); + pub const OwnershipInvalidRescueBounds: Self = Self(323966723); + pub const OwnershipSignatureNotFound: Self = Self(340743941); + pub const OwnershipISFBNotPresent: Self = Self(1615812357); + pub const OwnershipISFBProductExpCnt: Self = Self(1632589579); + pub const OwnershipISFBStrikeMask: Self = Self(1649366791); + pub const OwnershipISFBProductExp: Self = Self(1666144007); + pub const OwnershipISFBFailed: Self = Self(1682921229); + pub const OwnershipISFBPage: Self = Self(1699698435); + pub const OwnershipISFBSize: Self = Self(1716475651); + pub const OwnershipOWNRVersion: Self = Self(1884247811); + pub const OwnershipAPPKVersion: Self = Self(1901025027); + pub const OwnershipFLSHVersion: Self = Self(1917802243); + pub const OwnershipINFOVersion: Self = Self(1934579459); + pub const OwnershipRESQVersion: Self = Self(1951356675); + pub const OwnershipISFBVersion: Self = Self(1968133891); + pub const PersoTlvInternal: Self = Self(5264397); + pub const PersoTlvCertObjNotFound: Self = Self(22041605); + pub const PersoTlvCertNameTooLong: Self = Self(38818827); + pub const PersoTlvOutputBufTooSmall: Self = Self(55596043); + pub const DiceInternal: Self = Self(4473613); + pub const DiceCwtCoseKeyNotFound: Self = Self(21250821); + pub const DiceCwtCoseKeyBadSize: Self = Self(21250829); + pub const DiceCwtKeyCoordsNotFound: Self = Self(38028037); + pub const DicePageCorrupted: Self = Self(54805251); + pub const UsbBadSetup: Self = Self(5591811); + pub const UsbBadEndpointNumber: Self = Self(22369027); +} diff --git a/target/earlgrey/util/tags.rs b/target/earlgrey/util/tags.rs new file mode 100644 index 00000000..a9325637 --- /dev/null +++ b/target/earlgrey/util/tags.rs @@ -0,0 +1,130 @@ +#![allow(non_upper_case_globals)] + +use ufmt::{uDebug, uDisplay}; +use util_types::impl_fourcc; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct ManifestIdentifier(pub u32); +impl ManifestIdentifier { + pub const ROM_EXT: Self = Self(u32::from_le_bytes(*b"OTRE")); + pub const APPLICATION: Self = Self(u32::from_le_bytes(*b"OTB0")); +} + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct HardenedBool(pub u32); +impl HardenedBool { + pub const True: Self = Self(0x739); + pub const False: Self = Self(0x1d4); +} + +impl uDisplay for HardenedBool { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + match *self { + HardenedBool::True => ufmt::uwrite!(f, "True"), + HardenedBool::False => ufmt::uwrite!(f, "False"), + unk => ufmt::uwrite!(f, "HardenedBool({:08x})", unk.0), + } + } +} + +impl uDebug for HardenedBool { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + ufmt::uDisplay::fmt(self, f) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct OwnershipState(pub u32); +impl OwnershipState { + pub const Recovery: Self = Self(0); + pub const LockedOwner: Self = Self(0x444e574f); + pub const UnlockedSelf: Self = Self(0x464c5355); + pub const UnlockedAny: Self = Self(0x594e4155); + pub const UnlockedEndorsed: Self = Self(0x444e4555); +} +impl_fourcc!(OwnershipState); + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct BootSlot(pub u32); +impl BootSlot { + pub const SlotA: Self = Self(u32::from_le_bytes(*b"AA__")); + pub const SlotB: Self = Self(u32::from_le_bytes(*b"__BB")); + pub const Unspecified: Self = Self(u32::from_le_bytes(*b"UUUU")); +} +impl_fourcc!(BootSlot); + +impl BootSlot { + pub fn opposite(self) -> Option { + match self { + BootSlot::SlotA => Some(BootSlot::SlotB), + BootSlot::SlotB => Some(BootSlot::SlotA), + _ => None, + } + } +} + +/// The unlock mode for the OwnershipUnlock command. +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct UnlockMode(pub u32); +impl UnlockMode { + /// Unlock the chip to accept any next owner. + pub const Any: Self = Self(0x00594e41); + /// Unlock the chip to accept only the endorsed next owner. + pub const Endorsed: Self = Self(0x4f444e45); + /// Unlock the chip to update the current owner configuration. + pub const Update: Self = Self(0x00445055); + /// Abort the unlock operation. + pub const Abort: Self = Self(0x54524241); +} +impl_fourcc!(UnlockMode); + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct BootSvcKind(pub u32); +impl BootSvcKind { + pub const EmptyRequest: Self = Self(u32::from_le_bytes(*b"EMPT")); + pub const EmptyResponse: Self = Self(u32::from_le_bytes(*b"TPME")); + pub const MinBl0SecVerRequest: Self = Self(u32::from_le_bytes(*b"MSEC")); + pub const MinBl0SecVerResponse: Self = Self(u32::from_le_bytes(*b"CESM")); + pub const NextBl0SlotRequest: Self = Self(u32::from_le_bytes(*b"NEXT")); + pub const NextBl0SlotResponse: Self = Self(u32::from_le_bytes(*b"TXEN")); + pub const OwnershipUnlockRequest: Self = Self(u32::from_le_bytes(*b"UNLK")); + pub const OwnershipUnlockResponse: Self = Self(u32::from_le_bytes(*b"KLNU")); + pub const OwnershipActivateRequest: Self = Self(u32::from_le_bytes(*b"ACTV")); + pub const OwnershipActivateResponse: Self = Self(u32::from_le_bytes(*b"VTCA")); +} +impl_fourcc!(BootSvcKind); + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct OwnershipKeyAlg(pub u32); +impl OwnershipKeyAlg { + pub const Rsa: Self = Self(u32::from_le_bytes(*b"RSA3")); + pub const EcdsaP256: Self = Self(u32::from_le_bytes(*b"P256")); + pub const SpxPure: Self = Self(u32::from_le_bytes(*b"S+Pu")); + pub const SpxPrehash: Self = Self(u32::from_le_bytes(*b"S+S2")); + pub const HybridSpxPure: Self = Self(u32::from_le_bytes(*b"H+Pu")); + pub const HybridSpxPrehash: Self = Self(u32::from_le_bytes(*b"H+S2")); +} +impl_fourcc!(OwnershipKeyAlg); + +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct RetRamVersion(pub u32); +impl RetRamVersion { + pub const Version3: Self = Self(u32::from_le_bytes(*b"RR03")); + pub const Version4: Self = Self(u32::from_le_bytes(*b"RR04")); +} +impl_fourcc!(RetRamVersion); From 3d3853590ab464610872a127eb347751828dad1b Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 21:31:56 -0700 Subject: [PATCH 37/46] util_types: fourcc Signed-off-by: Chris Frantz --- util/types/BUILD.bazel | 2 ++ util/types/fourcc.rs | 61 ++++++++++++++++++++++++++++++++++++++++++ util/types/lib.rs | 1 + 3 files changed, 64 insertions(+) create mode 100644 util/types/fourcc.rs diff --git a/util/types/BUILD.bazel b/util/types/BUILD.bazel index 5c2c7f77..8769f3fa 100644 --- a/util/types/BUILD.bazel +++ b/util/types/BUILD.bazel @@ -3,6 +3,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "types", srcs = [ + "fourcc.rs", "lib.rs", "opcode.rs", "power_of_2.rs", @@ -11,6 +12,7 @@ rust_library( edition = "2024", visibility = ["//visibility:public"], deps = [ + "@rust_crates//:ufmt", "@rust_crates//:zerocopy", ], ) diff --git a/util/types/fourcc.rs b/util/types/fourcc.rs new file mode 100644 index 00000000..dad7630a --- /dev/null +++ b/util/types/fourcc.rs @@ -0,0 +1,61 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +pub mod __private { + #[allow(unused_imports)] + pub use ufmt; +} + +/// A trait for `FourCC` identifiers. +/// This is unsafe to implement; you must be sure that the target type is 4 bytes in size and that +/// each byte contains a printable ascii character. +pub unsafe trait FourCC: Sized { + fn as_str(&self) -> &str { + unsafe { + let ptr = self as *const Self as *const u8; + let s = core::slice::from_raw_parts(ptr, 4); + core::str::from_utf8_unchecked(s) + } + } +} + +#[macro_export] +macro_rules! impl_fourcc { + ($t:ty) => { + const _: () = { + use $crate::fourcc::FourCC; + use $crate::fourcc::__private::ufmt; + + unsafe impl FourCC for $t {} + + impl ufmt::uDisplay for $t { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + let s = unsafe { + let ptr = self as *const Self as *const u8; + core::slice::from_raw_parts(ptr, 4) + }; + for byte in s { + if (0x20..0x7f).contains(byte) { + ufmt::uwrite!(f, "{}", *byte as char)?; + } else { + ufmt::uwrite!(f, "\\x{:02x}", *byte)?; + } + } + Ok(()) + } + } + + impl ufmt::uDebug for $t { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + ufmt::uDisplay::fmt(self, f) + } + } + }; + }; +} diff --git a/util/types/lib.rs b/util/types/lib.rs index 7206ced5..362eb5ce 100644 --- a/util/types/lib.rs +++ b/util/types/lib.rs @@ -3,6 +3,7 @@ #![no_std] +pub mod fourcc; mod opcode; mod power_of_2; From f830f98e112a3905f8e04f7a767a5feb2941cf9a Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 26 Apr 2026 21:32:11 -0700 Subject: [PATCH 38/46] tests: retram test Signed-off-by: Chris Frantz --- target/earlgrey/tests/retram/BUILD.bazel | 126 ++++++++++++++++++++ target/earlgrey/tests/retram/system.json5 | 49 ++++++++ target/earlgrey/tests/retram/target.rs | 29 +++++ target/earlgrey/tests/retram/test_retram.rs | 56 +++++++++ 4 files changed, 260 insertions(+) create mode 100644 target/earlgrey/tests/retram/BUILD.bazel create mode 100644 target/earlgrey/tests/retram/system.json5 create mode 100644 target/earlgrey/tests/retram/target.rs create mode 100644 target/earlgrey/tests/retram/test_retram.rs diff --git a/target/earlgrey/tests/retram/BUILD.bazel b/target/earlgrey/tests/retram/BUILD.bazel new file mode 100644 index 00000000..f230bafd --- /dev/null +++ b/target/earlgrey/tests/retram/BUILD.bazel @@ -0,0 +1,126 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "test_retram", + srcs = [ + "test_retram.rs", + ], + codegen_crate_name = "test_retram_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/util", + "//target/earlgrey/registers:rstmgr", + "//util/console", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:sha2", + ], +) + +system_image( + name = "retram", + apps = [ + ":test_retram", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "retram_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":retram", +) + +opentitan_test( + name = "retram_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":retram", +) + +opentitan_test( + name = "retram_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":retram", +) + +opentitan_test( + name = "retram_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":retram", +) diff --git a/target/earlgrey/tests/retram/system.json5 b/target/earlgrey/tests/retram/system.json5 new file mode 100644 index 00000000..1db0dba4 --- /dev/null +++ b/target/earlgrey/tests/retram/system.json5 @@ -0,0 +1,49 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "test_retram", + flash_size_bytes: 16384, + processes: [{ + name: "test_retram_process", + ram_size_bytes: 4096, + objects: [ + ], + memory_mappings: [ + { + name: "retram", + type: "device", + start_address: 0x40600000, + size_bytes: 0x1000, + }, + { + name: "rstmgr", + type: "device", + start_address: 0x40410000, + size_bytes: 0x80, + }, + + ], + threads: [ + { + name: "retram_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/retram/target.rs b/target/earlgrey/tests/retram/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/tests/retram/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/retram/test_retram.rs b/target/earlgrey/tests/retram/test_retram.rs new file mode 100644 index 00000000..3ce68c44 --- /dev/null +++ b/target/earlgrey/tests/retram/test_retram.rs @@ -0,0 +1,56 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +//use test_retram_codegen::{handle, signals}; +//use userspace::time::Instant; +use userspace::{entry, syscall }; +use pw_status::{ Result}; +use earlgrey_util::ret_ram::RetRam; +use earlgrey_util::{CheckDigest, GetData}; +use earlgrey_util::tags::BootSlot; +use earlgrey_util::boot_svc::NextBl0SlotRequest; +use util_console::println; + +use sha2::{Sha256, Digest}; +use rstmgr::RstmgrAon; + + +fn test_retram() -> Result<()> { + let mut rstmgr = unsafe { RstmgrAon::new() }; + let retram = unsafe { RetRam::mut_ref() }; + println!("Reset Reasons = {:08x}", retram.reset_reasons); + println!("BootLog = {:#?}", retram.boot_log); + let ok = retram.boot_log.check_digest(|data| Sha256::digest(data).into()); + println!("ok = {}", ok as bool); + + + if retram.reset_reasons == 1 { + // If power on reset, reset once more. + println!("Preparing to boot slot B"); + let next: &mut NextBl0SlotRequest = retram.boot_svc.get_mut(); + next.next_bl0_slot = BootSlot::Unspecified; + next.primary_bl0_slot = BootSlot::SlotB; + println!("Set digest on boot_svc"); + retram.boot_svc.set_digest(|data| Sha256::digest(data).into()); + + println!("Reboot"); + rstmgr.regs_mut().reset_req().write(|_| 6u32.into()); + } + Ok(()) +} + +#[entry] +fn entry() -> ! { + // Since this is written as a test, shut down with the return status from `main()`. + let ret = test_retram(); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} From e95efb77a0593d1ecf59634cb46ffc05e854fb8c Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Mon, 27 Apr 2026 19:32:16 -0700 Subject: [PATCH 39/46] earlgrey: sysmgr Signed-off-by: Chris Frantz --- target/earlgrey/services/sysmgr/BUILD.bazel | 47 ++++++ target/earlgrey/services/sysmgr/client.rs | 130 +++++++++++++++ target/earlgrey/services/sysmgr/server.rs | 170 ++++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 target/earlgrey/services/sysmgr/BUILD.bazel create mode 100644 target/earlgrey/services/sysmgr/client.rs create mode 100644 target/earlgrey/services/sysmgr/server.rs diff --git a/target/earlgrey/services/sysmgr/BUILD.bazel b/target/earlgrey/services/sysmgr/BUILD.bazel new file mode 100644 index 00000000..ddb07568 --- /dev/null +++ b/target/earlgrey/services/sysmgr/BUILD.bazel @@ -0,0 +1,47 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "client", + srcs = [ + "client.rs", + ], + crate_name = "earlgrey_sysmgr_client", + edition = "2024", + deps = [ + "//util/error", + "//util/ipc", + "//util/types", + "//target/earlgrey/util", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:ufmt", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "server", + srcs = [ + "server.rs", + ], + crate_name = "earlgrey_sysmgr_server", + edition = "2024", + deps = [ + ":client", + "//util/error", + "//util/ipc", + "//util/types", + "//target/earlgrey/util", + "//target/earlgrey/registers:rstmgr", + "//target/earlgrey/registers:lc_ctrl", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:sha2", + "@rust_crates//:zerocopy", + ], +) diff --git a/target/earlgrey/services/sysmgr/client.rs b/target/earlgrey/services/sysmgr/client.rs new file mode 100644 index 00000000..4aa286ec --- /dev/null +++ b/target/earlgrey/services/sysmgr/client.rs @@ -0,0 +1,130 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +#![no_std] +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, FromZeros}; + +use earlgrey_util::tags::{BootSlot, OwnershipState, HardenedBool}; +use userspace::time::Instant; +use util_error::ErrorCode; +use util_ipc::IpcChannel; +use ufmt::derive::uDebug; + +pub mod op { +use util_types::Opcode; +pub const SYSMGR_OP_GET_BOOT_INFO: Opcode = Opcode::new(*b"MGBI"); +pub const SYSMGR_OP_SET_BOOT_POLICY: Opcode = Opcode::new(*b"MGBP"); +pub const SYSMGR_OP_REQ_REBOOT: Opcode = Opcode::new(*b"MGRB"); +} + +pub struct SysmgrClient { + ipc: IpcChannel, +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct ChipInfo { + pub git_hash: u64, + pub lc_state: u32, + pub device_id: [u32; 8], + pub creator_id: u16, + pub product_id: u16, + pub revision: u8, + pub _pad: [u8; 7], +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct RomExtInfo { + pub boot_slot: BootSlot, + pub major: u32, + pub minor: u32, + pub min_sec_ver: u32, + pub size: u32, +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct ApplicationInfo { + pub boot_slot: BootSlot, + pub pref_slot: BootSlot, + pub min_sec_ver: u32, + pub size: u32, + pub firmware_domain: u32, +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct OwnershipInfo { + pub nonce: u64, + pub state: OwnershipState, + pub transfers: u32, +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct ResetInfo { + pub reason: u32, + pub retram_init: HardenedBool, + pub hardware_straps: u32, + pub software_straps: u32, +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct BootInfo { + pub chip: ChipInfo, + pub rom_ext: RomExtInfo, + pub app: ApplicationInfo, + pub ownership: OwnershipInfo, + pub reset: ResetInfo, +} + +#[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, uDebug)] +#[repr(C)] +pub struct BootPolicy { + pub pref_slot: BootSlot, + pub next_slot: BootSlot, +} + +impl SysmgrClient { + pub const fn new(ipc: IpcChannel) -> Self { + SysmgrClient { ipc } + } + + pub fn get_boot_info(&self) -> Result { + let mut result = 0u32; + let mut info = BootInfo::new_zeroed(); + self.ipc.transaction::<256>( + &[op::SYSMGR_OP_GET_BOOT_INFO.as_bytes()], + &mut [result.as_mut_bytes(), info.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result)?; + Ok(info) + } + + pub fn set_boot_policy(&self, pref: BootSlot, next: BootSlot) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc.transaction::<256>( + &[op::SYSMGR_OP_SET_BOOT_POLICY.as_bytes(), + pref.as_bytes(), + next.as_bytes(), + ], + &mut [result.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } + + pub fn request_reboot(&self) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc.transaction::<256>( + &[op::SYSMGR_OP_REQ_REBOOT.as_bytes(), + ], + &mut [result.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } + +} diff --git a/target/earlgrey/services/sysmgr/server.rs b/target/earlgrey/services/sysmgr/server.rs new file mode 100644 index 00000000..5aa56ac7 --- /dev/null +++ b/target/earlgrey/services/sysmgr/server.rs @@ -0,0 +1,170 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +use zerocopy::{FromBytes, IntoBytes}; + +use earlgrey_sysmgr_client::*; +use earlgrey_util::boot_svc::NextBl0SlotRequest; +use earlgrey_util::error as eg_error; +use earlgrey_util::ret_ram::RetRam; +use earlgrey_util::tags::BootSlot; +use earlgrey_util::CheckDigest; +use earlgrey_util::GetData; +use util_error::{self as error, ErrorCode}; +use util_ipc::IpcChannel; +use util_types::fourcc::FourCC; +use util_types::Opcode; + +use lc_ctrl::LcCtrl; +use rstmgr::RstmgrAon; +use sha2::{Digest, Sha256}; + +#[allow(dead_code)] +pub struct SysmgrServer { + info: BootInfo, + retram: &'static mut RetRam, +} + +impl SysmgrServer { + pub fn new() -> Result { + let lc_ctrl = unsafe { LcCtrl::new() }; + let lcreg = lc_ctrl.regs(); + + let retram = unsafe { RetRam::mut_ref() }; + if !retram + .boot_log + .check_digest(|data| Sha256::digest(data).into()) + { + return Err(eg_error::EG_ERROR_BAD_BOOT_LOG); + } + + let info = BootInfo { + chip: ChipInfo { + git_hash: retram.boot_log.chip_version.get(), + lc_state: u32::from(lcreg.lc_state().read()), + device_id: lcreg.device_id().read().into(), + creator_id: lcreg.hw_revision0().read().silicon_creator_id() as u16, + product_id: lcreg.hw_revision0().read().product_id() as u16, + revision: lcreg.hw_revision1().read().revision_id() as u8, + _pad: Default::default(), + }, + rom_ext: RomExtInfo { + boot_slot: retram.boot_log.rom_ext_slot, + major: retram.boot_log.rom_ext_major, + minor: retram.boot_log.rom_ext_minor, + min_sec_ver: retram.boot_log.rom_ext_min_sec_ver, + size: retram.boot_log.rom_ext_size, + }, + app: ApplicationInfo { + boot_slot: retram.boot_log.bl0_slot, + pref_slot: retram.boot_log.primary_bl0_slot, + min_sec_ver: retram.boot_log.bl0_min_sec_ver, + // TODO: get from config? + size: 400 * 1024, + // TODO: read from keymgr. + firmware_domain: 0, + }, + ownership: OwnershipInfo { + nonce: retram.boot_log.rom_ext_nonce.get(), + state: retram.boot_log.ownership_state, + transfers: retram.boot_log.ownership_transfers, + }, + reset: ResetInfo { + reason: retram.reset_reasons, + retram_init: retram.boot_log.retention_ram_initialized, + // TODO: read gpio strapping value. + hardware_straps: 0, + // TODO: get from config? + software_straps: 0, + }, + }; + + pw_log::info!("Earlgrey System Manager"); + pw_log::info!( + "chip: {:04x}-{:04x}-{:02x} / {:016x}", + info.chip.creator_id as u16, + info.chip.product_id as u16, + info.chip.revision as u8, + info.chip.git_hash as u64, + ); + pw_log::info!( + "ROM_EXT: {}.{} in {}", + info.rom_ext.major as u32, + info.rom_ext.minor as u32, + info.rom_ext.boot_slot.as_str() as &str, + ); + pw_log::info!( + "Application in {} (prefer {})", + info.app.boot_slot.as_str() as &str, + info.app.pref_slot.as_str() as &str, + ); + pw_log::info!("Reset reasons: {:08x}", info.reset.reason as u32); + + Ok(Self { info, retram }) + } + + fn handle_get_boot_info<'a>(&self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let info = self.info.as_bytes(); + data[..info.len()].copy_from_slice(info); + Ok(&data[..info.len()]) + } + + fn handle_req_reboot<'a>(&self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + pw_log::info!("RESET REQUESTED!"); + let mut rstmgr = unsafe { RstmgrAon::new() }; + rstmgr.regs_mut().reset_req().write(|_| 6u32.into()); + Ok(&data[0..0]) + } + + fn handle_set_boot_policy<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let (pref, rest) = + BootSlot::ref_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let (next, _rest) = + BootSlot::ref_from_prefix(rest).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let request: &mut NextBl0SlotRequest = self.retram.boot_svc.get_mut(); + request.next_bl0_slot = *next; + request.primary_bl0_slot = *pref; + self.retram + .boot_svc + .set_digest(|data| Sha256::digest(data).into()); + Ok(&data[0..0]) + } + + fn handle_op<'a>(&mut self, opcode: Opcode, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + match opcode { + op::SYSMGR_OP_GET_BOOT_INFO => self.handle_get_boot_info(data), + op::SYSMGR_OP_REQ_REBOOT => self.handle_req_reboot(data), + op::SYSMGR_OP_SET_BOOT_POLICY => self.handle_set_boot_policy(data), + _ => Err(error::IPC_ERROR_UNKNOWN_OP), + } + } + + fn handle_one(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + ipc.wait_readable()?; + let len = ipc.read(0, data)?; + if len < 4 { + return Err(error::IPC_ERROR_BAD_REQ_LEN); + } + let (op_status, reqrsp) = data.split_at_mut(4); + let opcode = Opcode::read_from_bytes(op_status).unwrap(); + let len = match self.handle_op(opcode, reqrsp) { + Ok(result) => { + op_status.copy_from_slice((0u32).as_bytes()); + result.len() + } + Err(e) => { + op_status.copy_from_slice(e.0.as_bytes()); + 0 + } + }; + ipc.respond(&data[..4 + len])?; + Ok(()) + } + + pub fn run(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + loop { + self.handle_one(ipc, data)?; + } + } +} From 2f5242d597efb8bada9c1ae410279714e3540dea Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Mon, 27 Apr 2026 19:33:06 -0700 Subject: [PATCH 40/46] earlgrey: update retram test to sysmgr test Signed-off-by: Chris Frantz --- target/earlgrey/tests/retram/BUILD.bazel | 27 ++++++++++- target/earlgrey/tests/retram/sysmgr.rs | 35 +++++++++++++++ target/earlgrey/tests/retram/system.json5 | 38 +++++++++++++++- target/earlgrey/tests/retram/test_retram.rs | 50 +++++++++++---------- 4 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 target/earlgrey/tests/retram/sysmgr.rs diff --git a/target/earlgrey/tests/retram/BUILD.bazel b/target/earlgrey/tests/retram/BUILD.bazel index f230bafd..4f45c07a 100644 --- a/target/earlgrey/tests/retram/BUILD.bazel +++ b/target/earlgrey/tests/retram/BUILD.bazel @@ -21,13 +21,35 @@ rust_app( tags = ["kernel"], visibility = ["//visibility:public"], deps = [ + "//target/earlgrey/services/sysmgr:client", "//target/earlgrey/util", - "//target/earlgrey/registers:rstmgr", "//util/console", + "//util/error", + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_app( + name = "sysmgr", + srcs = [ + "sysmgr.rs", + ], + codegen_crate_name = "sysmgr_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/services/sysmgr:server", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", "@pigweed//pw_kernel/userspace", "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", - "@rust_crates//:sha2", ], ) @@ -35,6 +57,7 @@ system_image( name = "retram", apps = [ ":test_retram", + ":sysmgr", ], kernel = ":target", platform = "//target/earlgrey", diff --git a/target/earlgrey/tests/retram/sysmgr.rs b/target/earlgrey/tests/retram/sysmgr.rs new file mode 100644 index 00000000..e95fc803 --- /dev/null +++ b/target/earlgrey/tests/retram/sysmgr.rs @@ -0,0 +1,35 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use sysmgr_codegen::{handle}; +use pw_status::Error; +use userspace::{entry, syscall}; + +use earlgrey_sysmgr_server::SysmgrServer; +use util_error::ErrorCode; +use util_ipc::IpcChannel; + +fn sysmgr_server() -> Result<(), ErrorCode> { + let mut sysmgr = SysmgrServer::new()?; + let mut buf = [0u8; 512]; + let ipc = IpcChannel::new(handle::SYSMGR_SERVICE); + sysmgr.run(&ipc, &mut buf) +} + +#[entry] +fn entry() -> ! { + let ret = sysmgr_server().map_err(|e| { + pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); + Error::Unknown + }); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/tests/retram/system.json5 b/target/earlgrey/tests/retram/system.json5 index 1db0dba4..0d2bb061 100644 --- a/target/earlgrey/tests/retram/system.json5 +++ b/target/earlgrey/tests/retram/system.json5 @@ -15,12 +15,16 @@ }, apps: [ { - name: "test_retram", + name: "sysmgr", flash_size_bytes: 16384, processes: [{ - name: "test_retram_process", + name: "sysmgr_process", ram_size_bytes: 4096, objects: [ + { + name: "sysmgr_service", + type: "channel_handler", + }, ], memory_mappings: [ { @@ -35,7 +39,37 @@ start_address: 0x40410000, size_bytes: 0x80, }, + { + name: "lc_ctrl", + type: "device", + start_address: 0x40140000, + size_bytes: 0x100, + }, + ], + threads: [ + { + name: "sysmgr_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }], + }, + { + name: "test_retram", + flash_size_bytes: 16384, + processes: [{ + name: "test_retram_process", + ram_size_bytes: 4096, + objects: [ + { + name: "sysmgr_service", + type: "channel_initiator", + handler_process: "sysmgr_process", + handler_object_name: "sysmgr_service", + }, + ], + memory_mappings: [ ], threads: [ { diff --git a/target/earlgrey/tests/retram/test_retram.rs b/target/earlgrey/tests/retram/test_retram.rs index 3ce68c44..3f07bbd3 100644 --- a/target/earlgrey/tests/retram/test_retram.rs +++ b/target/earlgrey/tests/retram/test_retram.rs @@ -3,48 +3,50 @@ #![no_std] #![no_main] -//use test_retram_codegen::{handle, signals}; -//use userspace::time::Instant; +use test_retram_codegen::{handle}; use userspace::{entry, syscall }; -use pw_status::{ Result}; -use earlgrey_util::ret_ram::RetRam; -use earlgrey_util::{CheckDigest, GetData}; +use pw_status::{ Error}; +use earlgrey_sysmgr_client::SysmgrClient; use earlgrey_util::tags::BootSlot; -use earlgrey_util::boot_svc::NextBl0SlotRequest; +use util_error::ErrorCode; +use util_ipc::IpcChannel; use util_console::println; -use sha2::{Sha256, Digest}; -use rstmgr::RstmgrAon; +fn test_retram() -> Result<(),ErrorCode> { + let sysmgr = SysmgrClient::new(IpcChannel::new(handle::SYSMGR_SERVICE)); -fn test_retram() -> Result<()> { - let mut rstmgr = unsafe { RstmgrAon::new() }; - let retram = unsafe { RetRam::mut_ref() }; - println!("Reset Reasons = {:08x}", retram.reset_reasons); - println!("BootLog = {:#?}", retram.boot_log); - let ok = retram.boot_log.check_digest(|data| Sha256::digest(data).into()); - println!("ok = {}", ok as bool); + let boot_info = sysmgr.get_boot_info()?; + println!("BootInfo = {:#?}", boot_info); - - if retram.reset_reasons == 1 { + if boot_info.reset.reason == 1 { // If power on reset, reset once more. println!("Preparing to boot slot B"); - let next: &mut NextBl0SlotRequest = retram.boot_svc.get_mut(); - next.next_bl0_slot = BootSlot::Unspecified; - next.primary_bl0_slot = BootSlot::SlotB; - println!("Set digest on boot_svc"); - retram.boot_svc.set_digest(|data| Sha256::digest(data).into()); + sysmgr.set_boot_policy(BootSlot::SlotB, BootSlot::Unspecified)?; println!("Reboot"); - rstmgr.regs_mut().reset_req().write(|_| 6u32.into()); + sysmgr.request_reboot()?; } Ok(()) } #[entry] fn entry() -> ! { + pw_log::info!("🔄 RUNNING"); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match test_retram() { + Ok(()) => { + pw_log::info!("✅ PASSED"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); + Err(Error::Unknown) + } + }; + // Since this is written as a test, shut down with the return status from `main()`. - let ret = test_retram(); let _ = syscall::debug_shutdown(ret); loop {} } From 0f2ccb68ac6688a01d9e2be03c1f80b9141fa04b Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Mon, 27 Apr 2026 21:22:27 -0700 Subject: [PATCH 41/46] earlgrey: hardware enablement firmware Signed-off-by: Chris Frantz --- target/earlgrey/firmware/hwe/BUILD.bazel | 185 +++++++++ target/earlgrey/firmware/hwe/flash_server.rs | 75 ++++ target/earlgrey/firmware/hwe/sysmgr.rs | 35 ++ target/earlgrey/firmware/hwe/system.json5 | 172 ++++++++ target/earlgrey/firmware/hwe/target.rs | 29 ++ target/earlgrey/firmware/hwe/usbdfu.rs | 396 +++++++++++++++++++ 6 files changed, 892 insertions(+) create mode 100644 target/earlgrey/firmware/hwe/BUILD.bazel create mode 100644 target/earlgrey/firmware/hwe/flash_server.rs create mode 100644 target/earlgrey/firmware/hwe/sysmgr.rs create mode 100644 target/earlgrey/firmware/hwe/system.json5 create mode 100644 target/earlgrey/firmware/hwe/target.rs create mode 100644 target/earlgrey/firmware/hwe/usbdfu.rs diff --git a/target/earlgrey/firmware/hwe/BUILD.bazel b/target/earlgrey/firmware/hwe/BUILD.bazel new file mode 100644 index 00000000..fd2737ab --- /dev/null +++ b/target/earlgrey/firmware/hwe/BUILD.bazel @@ -0,0 +1,185 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "usbdfu", + srcs = [ + "usbdfu.rs", + ], + codegen_crate_name = "usbdfu_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/services/sysmgr:client", + "//hal/blocking/flash", + "//hal/blocking/usb:hal_usb", + "//protocol/usb/dfu", + "//protocol/usb/stack", + "//services/flash:client", + "//target/earlgrey/drivers:usb_driver", + "//target/earlgrey/registers:pinmux", + "//target/earlgrey/registers:top_earlgrey", + "//target/earlgrey/registers:usbdev", + "//target/earlgrey/util", + "//util/console", + "//util/error", + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + "@rust_crates//:zerocopy", + ], +) + +rust_app( + name = "flash_server", + srcs = [ + "flash_server.rs", + ], + codegen_crate_name = "flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/registers:flash_ctrl_core", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_app( + name = "sysmgr", + srcs = [ + "sysmgr.rs", + ], + codegen_crate_name = "sysmgr_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/services/sysmgr:server", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "hwe", + apps = [ + ":sysmgr", + ":flash_server", + ":usbdfu", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "hwe_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":hwe", +) + +opentitan_test( + name = "hwe_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":hwe", +) + +opentitan_test( + name = "hwe_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":hwe", +) + +opentitan_test( + name = "hwe_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":hwe", +) diff --git a/target/earlgrey/firmware/hwe/flash_server.rs b/target/earlgrey/firmware/hwe/flash_server.rs new file mode 100644 index 00000000..8a4e37cb --- /dev/null +++ b/target/earlgrey/firmware/hwe/flash_server.rs @@ -0,0 +1,75 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use flash_server_codegen::{handle, signals}; +use pw_status::{Error}; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use hal_flash::{BlockingFlash, FlashAddress}; +use services_flash_server::FlashIpcServer; +use util_types::Blocking; +use util_ipc::IpcChannel; +use util_error::ErrorCode; +use eflash_driver::{EmbeddedFlash, Permission}; + +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + let w = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) + .unwrap(); + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn flash_server() -> Result<(), ErrorCode> { + let mut driver = EmbeddedFlash::new_with_interrupts( + unsafe { flash_ctrl_core::FlashCtrl::new() }); + driver.set_default_permission(Permission::FULL_ACCESS); + for i in 5..9 { + driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + let flash = BlockingFlash { + driver, + blocking: FlashCtrlInterrupt, + }; + let mut flash_server = FlashIpcServer::new(flash); + let mut buf = [0u8; 2056]; + let ipc = IpcChannel::new(handle::FLASH_SERVICE); + flash_server.run(&ipc, &mut buf) +} + +#[entry] +fn entry() -> ! { + pw_log::info!("🔄 RUNNING"); + let ret = flash_server(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { pw_log::info!("✅ PASSED"); Ok(()) } + Err(e) => { pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); Err(Error::Unknown) } + }; + + // Since this is written as a test, shut down with the return status from `main()`. + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/firmware/hwe/sysmgr.rs b/target/earlgrey/firmware/hwe/sysmgr.rs new file mode 100644 index 00000000..e95fc803 --- /dev/null +++ b/target/earlgrey/firmware/hwe/sysmgr.rs @@ -0,0 +1,35 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use sysmgr_codegen::{handle}; +use pw_status::Error; +use userspace::{entry, syscall}; + +use earlgrey_sysmgr_server::SysmgrServer; +use util_error::ErrorCode; +use util_ipc::IpcChannel; + +fn sysmgr_server() -> Result<(), ErrorCode> { + let mut sysmgr = SysmgrServer::new()?; + let mut buf = [0u8; 512]; + let ipc = IpcChannel::new(handle::SYSMGR_SERVICE); + sysmgr.run(&ipc, &mut buf) +} + +#[entry] +fn entry() -> ! { + let ret = sysmgr_server().map_err(|e| { + pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); + Error::Unknown + }); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/firmware/hwe/system.json5 b/target/earlgrey/firmware/hwe/system.json5 new file mode 100644 index 00000000..c7f798b9 --- /dev/null +++ b/target/earlgrey/firmware/hwe/system.json5 @@ -0,0 +1,172 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "sysmgr", + flash_size_bytes: 16384, + processes: [{ + name: "sysmgr_process", + ram_size_bytes: 4096, + objects: [ + { + name: "sysmgr_service", + type: "channel_handler", + }, + ], + memory_mappings: [ + { + name: "retram", + type: "device", + start_address: 0x40600000, + size_bytes: 0x1000, + }, + { + name: "rstmgr", + type: "device", + start_address: 0x40410000, + size_bytes: 0x80, + }, + { + name: "lc_ctrl", + type: "device", + start_address: 0x40140000, + size_bytes: 0x100, + }, + + ], + threads: [ + { + name: "sysmgr_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }], + }, + { + name: "flash_server", + flash_size_bytes: 16384, + processes: [ + { + name: "flash_server", + ram_size_bytes: 4096, + objects: [ + { + name: "flash_service", + type: "channel_handler", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + //{ name: "flash_ctrl_prog_empty", number: 160 }, + //{ name: "flash_ctrl_prog_lvl", number: 161 }, + //{ name: "flash_ctrl_rd_full", number: 162 }, + //{ name: "flash_ctrl_rd_lvl", number: 163 }, + { name: "flash_ctrl_op_done", number: 164 }, + //{ name: "flash_ctrl_corr_err", number: 165 }, + ], + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + } + ], + threads: [ + { + name: "flash_server_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + { + name: "usbdfu", + flash_size_bytes: 16384, + processes: [{ + name: "usbdfu_process", + ram_size_bytes: 6144, + objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "flash_service", + }, + { + name: "sysmgr_service", + type: "channel_initiator", + handler_process: "sysmgr_process", + handler_object_name: "sysmgr_service", + }, + { + name: "usbdev_interrupts", + type: "interrupt", + irqs: [ + { name: "usbdev_pkt_received", number: 135 }, + { name: "usbdev_pkt_sent", number: 136 }, + { name: "usbdev_disconnected", number: 137 }, + { name: "usbdev_host_lost", number: 138 }, + + { name: "usbdev_link_reset", number: 139 }, + { name: "usbdev_link_suspend", number: 140 }, + { name: "usbdev_link_resume", number: 141 }, + { name: "usbdev_av_out_empty", number: 142 }, + + { name: "usbdev_rx_full", number: 143 }, + { name: "usbdev_av_overflow", number: 144 }, + //{ name: "usbdev_link_in_err", number: 145 }, + { name: "usbdev_rx_crc_err", number: 146 }, + + { name: "usbdev_rx_pid_err", number: 147 }, + { name: "usbdev_rx_bitstuff_err", number: 148 }, + { name: "usbdev_frame", number: 149 }, + //{ name: "usbdev_powered", number: 150 }, + + //{ name: "usbdev_link_out_err", number: 151 }, + { name: "usbdev_av_setup_empty", number: 152 }, + ], + }, + ], + memory_mappings: [ + { + name: "usbdev", + type: "device", + start_address: 0x40320000, + size_bytes: 0x1000, + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000, + } + + ], + threads: [ + { + name: "usbdev_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }], + }, + ] +} diff --git a/target/earlgrey/firmware/hwe/target.rs b/target/earlgrey/firmware/hwe/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/firmware/hwe/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/firmware/hwe/usbdfu.rs b/target/earlgrey/firmware/hwe/usbdfu.rs new file mode 100644 index 00000000..70c71ba0 --- /dev/null +++ b/target/earlgrey/firmware/hwe/usbdfu.rs @@ -0,0 +1,396 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +#![allow(dead_code)] + +use pw_status::Error; +use usbdfu_codegen::{handle, signals}; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use aligned::{Aligned, A4}; +use hal_usb::{ConfigDescriptor, DeviceDescriptor, StringDescriptorRef}; + +use hal_usb::driver::UsbDriver; +use usb_driver::UsbConfig; +use usb_stack::{DescriptorSource, UsbAction, UsbClass}; + +use earlgrey_util::error as eg_error; +use earlgrey_util::PersoCertificate; +use earlgrey_util::tags::{BootSlot}; +use earlgrey_util::tags::ManifestIdentifier; +use earlgrey_sysmgr_client::{SysmgrClient, BootInfo}; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::{self as error, ErrorCode}; +use util_ipc::IpcChannel; +use zerocopy::{FromBytes}; + +use protocol_usb_dfu::{DfuBuilder, DfuClass, DfuHandler, DfuResult, DfuStatus}; + +const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); +const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); +const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); +const DFU_FIRMWARE_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); +const DFU_UDS_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(5); +const DFU_CDI0_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(6); +const DFU_CDI1_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(7); + +const DFU_BUILDER: DfuBuilder = DfuBuilder::new( + 0, // interface_num + 1, // alt_settings (1 for now) + 2048, // transfer_size +); + +const DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { + device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, + device_sub_class: 0x00, + device_protocol: 0x00, + max_packet_size: 64, + vendor_id: 0x18d1, // Google, Inc. + product_id: 0x503a, // STWG USB Fullspeed IP. + device_release_num: 0x0100, + manufacturer: USB_VENDOR_HANDLE, + product: USB_PRODUCT_HANDLE, + serial_num: USB_SERIAL_HANDLE, +}; + +const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[ + DFU_BUILDER.interface( 0, DFU_FIRMWARE_HANDLE, &[]), + DFU_BUILDER.interface( 1, DFU_UDS_CERT_HANDLE, &[]), + DFU_BUILDER.interface( 2, DFU_CDI0_CERT_HANDLE, &[]), + DFU_BUILDER.interface( 3, DFU_CDI1_CERT_HANDLE, + &[DFU_BUILDER.functional_descriptor()]), + ], +}; + +const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { + langs: &[ + // English - United States + 0x0409, + ], +}; + +const VENDOR_ID: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Google Inc.").as_ref(); +const PRODUCT_ID_DEFAULT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("Earlgrey DFU").as_ref(); +const DFU_FIRMWARE: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Firmware").as_ref(); +const DFU_UDS_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("UDS Certificate").as_ref(); +const DFU_CDI0_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDI0 Certificate").as_ref(); +const DFU_CDI1_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDI1 Certificate").as_ref(); + +struct MyDescriptors<'a> { + serial_desc_bytes: StringDescriptorRef<'a>, + product_desc_bytes: StringDescriptorRef<'a>, +} + +impl DescriptorSource for MyDescriptors<'_> { + const DEVICE_DESC_BYTES: &'static Aligned = &Aligned(DEVICE_DESC.serialize()); + const CONFIG_DESC_BYTES: &'static Aligned = + &Aligned(CONFIG_DESC.serialize::<{ CONFIG_DESC.total_size() }>()); + const STRING_DESC_0_BYTES: &'static Aligned = + &Aligned(STRING_DESC_0.serialize::<{ STRING_DESC_0.total_size() }>()); + const DEVICE_STATUS: Aligned = Aligned([1u8, 0]); + + fn get_string( + &self, + handle: hal_usb::StringHandle, + _lang: u16, + ) -> Option> { + match handle { + USB_VENDOR_HANDLE => Some(VENDOR_ID), + USB_PRODUCT_HANDLE => Some(self.product_desc_bytes), + USB_SERIAL_HANDLE => Some(self.serial_desc_bytes), + DFU_FIRMWARE_HANDLE => Some(DFU_FIRMWARE), + DFU_UDS_CERT_HANDLE => Some(DFU_UDS_CERT), + DFU_CDI0_CERT_HANDLE => Some(DFU_CDI0_CERT), + DFU_CDI1_CERT_HANDLE => Some(DFU_CDI1_CERT), + _ => None, + } + } +} + +fn get_certificate(flash: &mut FlashIpcClient, n: u8, data: &mut [u8]) -> Result { + pw_log::info!("Reading certificate {}", n as usize); + let (partition, mut n) = match n { + 0 => (0, 0), // The UDS (dice) cert is located in bank=0, page=9. + 1 => (1, 0), // The CDI (dice) certs are located in bank=1, page=9. + 2 => (1, 1), + _ => return Err(DfuStatus::ErrFile), + }; + let mut offset = 0usize; + let mut buf = [0u8; 1024]; + loop { + let sz = core::cmp::min(2048 - offset, buf.len()); + flash.read(FlashAddress::info(partition, 9, offset as u32), &mut buf[..sz]).map_err(|_| DfuStatus::ErrUnknown)?; + match PersoCertificate::from_bytes(&buf) { + Ok((cert, _)) => { + if n == 0 { + let len = cert.certificate.len(); + pw_log::info!("Found cert: {} bytes", len as usize); + data[..len].copy_from_slice(cert.certificate); + return Ok(len); + } + offset += (cert.obj_size + 7) & !7; + n -= 1; + } + Err(_) => break, + } + } + Err(DfuStatus::ErrUnknown) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum FwUpdateState { + Idle, + RomExt, + Application, + Done, +} + +struct FwUpdate { + state: FwUpdateState, + next_erase: u32, + start_block: u32, + rom_ext: BootSlot, + rom_ext_start: u32, + rom_ext_end: u32, + app: BootSlot, + app_start: u32, + app_end: u32, +} + +impl FwUpdate { + fn new(info: &BootInfo) -> Result { + let rom_ext= info.rom_ext.boot_slot.opposite().ok_or(eg_error::EG_ERROR_BOOT_SLOT_UNKNOWN)?; + let rom_ext_start= FwUpdate::addr(rom_ext); + let app = info.app.boot_slot.opposite().ok_or(eg_error::EG_ERROR_BOOT_SLOT_UNKNOWN)?; + let app_start= FwUpdate::addr(app) + info.rom_ext.size; + + Ok(FwUpdate { + state: FwUpdateState::Idle, + next_erase: 0, + start_block: 0, + rom_ext, + rom_ext_start, + rom_ext_end: rom_ext_start + info.rom_ext.size, + app, + app_start, + app_end: app_start + info.app.size, + }) + } + + fn addr(slot: BootSlot) -> u32 { + match slot { + BootSlot::SlotA => 0, + BootSlot::SlotB => 0x80000, + _ => unreachable!(), + } + } +} + +struct MyDfuHandler { + flash: FlashIpcClient, + sysmgr: SysmgrClient, + update: FwUpdate, +} + +impl MyDfuHandler { + fn flash_erase(&mut self, mut start: u32, end: u32) -> Result<(), ErrorCode> { + while start < end { + self.flash.erase_page(FlashAddress::data(start))?; + start += 2048; + } + Ok(()) + } + + fn flash_fw_block(&mut self, block_num: u32, data: &[u8]) -> Result<(), DfuStatus> { + if block_num == self.update.next_erase { + let id = ManifestIdentifier::read_from_bytes(&data[0x334..0x338]).unwrap(); + match id { + ManifestIdentifier::ROM_EXT => { + pw_log::info!("Flashing ROM_EXT region at {}-{}", + self.update.rom_ext_start as u32, + self.update.rom_ext_end as u32, + ); + self.flash_erase(self.update.rom_ext_start, self.update.rom_ext_end).map_err(|_| DfuStatus::ErrErase)?; + self.update.state = FwUpdateState::RomExt; + self.update.next_erase = 32; + self.update.start_block = block_num; + } + ManifestIdentifier::APPLICATION => { + pw_log::info!("Flashing Application region at {}-{}", + self.update.app_start as u32, + self.update.app_end as u32, + ); + self.flash_erase(self.update.app_start, self.update.app_end).map_err(|_| DfuStatus::ErrErase)?; + self.update.state = FwUpdateState::Application; + self.update.start_block = block_num; + } + _ => { + pw_log::error!("Unknown manifest ID: {:08x}", id.0 as u32); + return Err(DfuStatus::ErrUnknown); + } + } + } + + let block = block_num - self.update.start_block; + match self.update.state { + FwUpdateState::RomExt => { + self.flash.program(FlashAddress::data(self.update.rom_ext_start+block*2048), data).map_err(|_| DfuStatus::ErrProg)?; + } + FwUpdateState::Application => { + self.flash.program(FlashAddress::data(self.update.app_start+block*2048), data).map_err(|_| DfuStatus::ErrProg)?; + } + _ => { return Err(DfuStatus::ErrUnknown); } + } + + if data.len() < 2048 { + self.update.state = FwUpdateState::Done; + } + Ok(()) + } +} + +impl DfuHandler for MyDfuHandler { + fn dnload(&mut self, alt: u8, block_num: u16, data: &[u8]) -> DfuResult { + pw_log::info!( + "DNLOAD: alt={}, block={}, len={}", + alt, + block_num, + data.len() + ); + if alt == 0 { + match self.flash_fw_block(block_num as u32, data) { + Ok(()) => DfuResult::Ok, + Err(e) => DfuResult::Err(e), + } + } else { + DfuResult::Err(DfuStatus::ErrFile) + } + } + + fn upload(&mut self, alt: u8, block_num: u16, data: &mut [u8]) -> Result { + pw_log::info!( + "UPLOAD: alt={}, block={}, len={}", + alt, + block_num, + data.len() + ); + match alt { + 1|2|3 => get_certificate(&mut self.flash, alt-1, data), + _ => Err(DfuStatus::ErrFile), + } + } + + fn manifest(&mut self) -> DfuResult { + pw_log::info!("MANIFEST"); + if self.update.state == FwUpdateState::Done { + // TODO: check for errors. + let _ = self.sysmgr.set_boot_policy(self.update.app, BootSlot::Unspecified); + let _ = self.sysmgr.request_reboot(); + } + DfuResult::Ok + } + + fn abort(&mut self) { + pw_log::info!("ABORT"); + } +} + +fn handle_usb() -> Result<(), ErrorCode> { + let mut serial_num_buffer = Aligned::([0_u8; 130]); + let descriptors = MyDescriptors { + serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned( + &mut serial_num_buffer, + b"DFU-12345", + ) + .unwrap(), + product_desc_bytes: PRODUCT_ID_DEFAULT, + }; + + const USB_CONFIG: UsbConfig = UsbConfig::new(&[], &[]); + + let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); + let mut ep0 = usb_stack::SimpleEp0::new(); + let sysmgr = SysmgrClient::new(IpcChannel::new(handle::SYSMGR_SERVICE)); + let info = sysmgr.get_boot_info()?; + let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, MyDfuHandler { + flash: FlashIpcClient::new(IpcChannel::new(handle::FLASH_SERVICE))?, + sysmgr, + update: FwUpdate::new(&info)?, + }); + + loop { + let wait_return = syscall::object_wait( + handle::USBDEV_INTERRUPTS, + signals::USBDEV_PKT_RECEIVED + | signals::USBDEV_PKT_SENT + | signals::USBDEV_DISCONNECTED + | signals::USBDEV_HOST_LOST + | signals::USBDEV_LINK_RESET + | signals::USBDEV_LINK_SUSPEND + | signals::USBDEV_LINK_RESUME + | signals::USBDEV_AV_OUT_EMPTY + | signals::USBDEV_RX_FULL + | signals::USBDEV_AV_OVERFLOW + | signals::USBDEV_RX_CRC_ERR + | signals::USBDEV_RX_PID_ERR + | signals::USBDEV_RX_BITSTUFF_ERR + | signals::USBDEV_FRAME + | signals::USBDEV_AV_SETUP_EMPTY, + Instant::MAX, + ).map_err(ErrorCode::kernel_error)?; + + if wait_return.user_data != 0 { + pw_log::error!("Incorrect WaitReturn values"); + return Err(error::KERNEL_ERROR_UNKNOWN); + } + + while let Some(event) = usb.poll() { + let mut action = match dfu.handle_event(event) { + Ok(a) => a, + Err(e) => ep0.handle_event(e, &descriptors).unwrap_or(UsbAction::None), + }; + action.run(&mut usb); + } + + // Initiate any pending transmissions (e.g. UPLOAD blocks) + dfu.poll(&mut usb); + } +} + +fn usb_setup_pinmux() { + use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn}; + let mut pinmux = unsafe { pinmux::PinmuxAon::new() }; + + pinmux + .regs_mut() + .mio_periph_insel() + .at(PinmuxPeripheralIn::UsbdevSense as usize) + .modify(|_| (PinmuxInsel::ConstantOne as u32).into()); +} + +#[entry] +fn entry() -> ! { + usb_setup_pinmux(); + let ret = handle_usb().map_err(|e| { + pw_log::error!("usbdfu failed: {:08x}", u32::from(e) as u32); + Error::Unknown + }); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} From bb12c9bc493dda2adffae71e2253adc1172f2504 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 30 Apr 2026 15:11:14 -0700 Subject: [PATCH 42/46] util_ipc fixups Signed-off-by: Chris Frantz --- util/ipc/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/util/ipc/lib.rs b/util/ipc/lib.rs index 5ddf2e5b..eca0c82a 100644 --- a/util/ipc/lib.rs +++ b/util/ipc/lib.rs @@ -14,6 +14,8 @@ impl IpcChannel { IpcChannel(channel) } + pub fn handle(&self) -> u32 { self.0 } + pub fn check_status(code: u32) -> Result<(), ErrorCode> { if code == 0 { Ok(()) From 7541429de89e600b71cf80fc262e8925ec39aa62 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 30 Apr 2026 15:11:29 -0700 Subject: [PATCH 43/46] earlgrey: uart receiver driver Signed-off-by: Chris Frantz --- target/earlgrey/drivers/BUILD.bazel | 11 ++++++++ target/earlgrey/drivers/uart_receiver.rs | 33 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 target/earlgrey/drivers/uart_receiver.rs diff --git a/target/earlgrey/drivers/BUILD.bazel b/target/earlgrey/drivers/BUILD.bazel index 9d497f8e..095ac7c8 100644 --- a/target/earlgrey/drivers/BUILD.bazel +++ b/target/earlgrey/drivers/BUILD.bazel @@ -35,3 +35,14 @@ rust_library( "@ureg", ], ) + +rust_library( + name = "uart_receiver", + srcs = ["uart_receiver.rs"], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/registers:uart", + "@ureg", + ], +) diff --git a/target/earlgrey/drivers/uart_receiver.rs b/target/earlgrey/drivers/uart_receiver.rs new file mode 100644 index 00000000..5df05a02 --- /dev/null +++ b/target/earlgrey/drivers/uart_receiver.rs @@ -0,0 +1,33 @@ +#![no_std] +use uart; + +pub struct UartReceiver { + regs: uart::RegisterBlock>, +} + +impl UartReceiver { + pub unsafe fn new(ptr: *mut u32) -> Self { + Self { + regs: unsafe { uart::RegisterBlock::new(ptr) }, + } + } + + pub fn enable_receiver(&mut self) { + self.regs.ctrl().modify(|ctrl| ctrl.rx(true)); + } + + pub fn enable_interrupt(&mut self) { + self.regs.intr_enable().modify(|en| en.rx_watermark(true)); + self.regs.fifo_ctrl() + .modify(|fifo| fifo.rxilvl(|lvl| lvl.rxlvl1())); + } + + pub fn receive(&mut self) -> Option { + if !self.regs.status().read().rxempty() { + let value = u32::from(self.regs.rdata().read()); + Some(value as u8) + } else { + None + } + } +} From 52a50e7469a0b0c649dfc602f3601f7e8f2d8b34 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 30 Apr 2026 15:16:02 -0700 Subject: [PATCH 44/46] sysmgr_server_multi_ipc --- target/earlgrey/services/sysmgr/server.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/target/earlgrey/services/sysmgr/server.rs b/target/earlgrey/services/sysmgr/server.rs index 5aa56ac7..d9877a1a 100644 --- a/target/earlgrey/services/sysmgr/server.rs +++ b/target/earlgrey/services/sysmgr/server.rs @@ -4,6 +4,10 @@ #![no_std] use zerocopy::{FromBytes, IntoBytes}; +use userspace::syscall::Signals; +use userspace::time::Instant; +use userspace::{syscall}; + use earlgrey_sysmgr_client::*; use earlgrey_util::boot_svc::NextBl0SlotRequest; use earlgrey_util::error as eg_error; @@ -141,7 +145,7 @@ impl SysmgrServer { } fn handle_one(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { - ipc.wait_readable()?; + //ipc.wait_readable()?; let len = ipc.read(0, data)?; if len < 4 { return Err(error::IPC_ERROR_BAD_REQ_LEN); @@ -162,9 +166,15 @@ impl SysmgrServer { Ok(()) } - pub fn run(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + pub fn run(&mut self, wg: u32, ipc: &[&IpcChannel], data: &mut [u8]) -> Result<(), ErrorCode> { + for (i, chan) in ipc.iter().enumerate() { + syscall::wait_group_add(wg, chan.handle(), Signals::READABLE, i).map_err(ErrorCode::kernel_error)?; + } loop { - self.handle_one(ipc, data)?; + let w = syscall::object_wait(wg, Signals::READABLE, Instant::MAX).map_err(ErrorCode::kernel_error)?; + if w.pending_signals.contains(Signals::READABLE) { + self.handle_one(ipc[w.user_data], data)?; + } } } } From d27c221c520883549769917066f03f24e80fa1f4 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 30 Apr 2026 15:16:19 -0700 Subject: [PATCH 45/46] update retram test --- target/earlgrey/tests/retram/sysmgr.rs | 9 ++++++--- target/earlgrey/tests/retram/system.json5 | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/target/earlgrey/tests/retram/sysmgr.rs b/target/earlgrey/tests/retram/sysmgr.rs index e95fc803..4545fe4d 100644 --- a/target/earlgrey/tests/retram/sysmgr.rs +++ b/target/earlgrey/tests/retram/sysmgr.rs @@ -3,8 +3,8 @@ #![no_std] #![no_main] -use sysmgr_codegen::{handle}; use pw_status::Error; +use sysmgr_codegen::handle; use userspace::{entry, syscall}; use earlgrey_sysmgr_server::SysmgrServer; @@ -14,8 +14,11 @@ use util_ipc::IpcChannel; fn sysmgr_server() -> Result<(), ErrorCode> { let mut sysmgr = SysmgrServer::new()?; let mut buf = [0u8; 512]; - let ipc = IpcChannel::new(handle::SYSMGR_SERVICE); - sysmgr.run(&ipc, &mut buf) + sysmgr.run( + handle::SYSMGR_WAIT_GROUP, + &[&IpcChannel::new(handle::SYSMGR_SERVICE)], + &mut buf, + ) } #[entry] diff --git a/target/earlgrey/tests/retram/system.json5 b/target/earlgrey/tests/retram/system.json5 index 0d2bb061..386f53d1 100644 --- a/target/earlgrey/tests/retram/system.json5 +++ b/target/earlgrey/tests/retram/system.json5 @@ -21,6 +21,10 @@ name: "sysmgr_process", ram_size_bytes: 4096, objects: [ + { + name: "sysmgr_wait_group", + type: "wait_group", + }, { name: "sysmgr_service", type: "channel_handler", From 7e6958734de2ecf80d53f18bda48af80dbed582b Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Thu, 30 Apr 2026 15:16:51 -0700 Subject: [PATCH 46/46] earlgrey: add a platform task to hardware enablement Signed-off-by: Chris Frantz --- target/earlgrey/firmware/hwe/BUILD.bazel | 29 +++- target/earlgrey/firmware/hwe/platform.rs | 181 ++++++++++++++++++++++ target/earlgrey/firmware/hwe/sysmgr.rs | 6 +- target/earlgrey/firmware/hwe/system.json5 | 70 ++++++++- target/earlgrey/firmware/hwe/usbdfu.rs | 2 +- 5 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 target/earlgrey/firmware/hwe/platform.rs diff --git a/target/earlgrey/firmware/hwe/BUILD.bazel b/target/earlgrey/firmware/hwe/BUILD.bazel index fd2737ab..520d3d36 100644 --- a/target/earlgrey/firmware/hwe/BUILD.bazel +++ b/target/earlgrey/firmware/hwe/BUILD.bazel @@ -21,7 +21,6 @@ rust_app( tags = ["kernel"], visibility = ["//visibility:public"], deps = [ - "//target/earlgrey/services/sysmgr:client", "//hal/blocking/flash", "//hal/blocking/usb:hal_usb", "//protocol/usb/dfu", @@ -31,6 +30,7 @@ rust_app( "//target/earlgrey/registers:pinmux", "//target/earlgrey/registers:top_earlgrey", "//target/earlgrey/registers:usbdev", + "//target/earlgrey/services/sysmgr:client", "//target/earlgrey/util", "//util/console", "//util/error", @@ -88,10 +88,37 @@ rust_app( ], ) +rust_app( + name = "platform", + srcs = [ + "platform.rs", + ], + codegen_crate_name = "platform_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/drivers:uart_receiver", + "//target/earlgrey/registers:pinmux", + "//target/earlgrey/registers:top_earlgrey", + "//target/earlgrey/registers:uart", + "//target/earlgrey/services/sysmgr:client", + "//target/earlgrey/util", + "//util/console", + "//util/error", + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + system_image( name = "hwe", apps = [ ":sysmgr", + ":platform", ":flash_server", ":usbdfu", ], diff --git a/target/earlgrey/firmware/hwe/platform.rs b/target/earlgrey/firmware/hwe/platform.rs new file mode 100644 index 00000000..73220bb5 --- /dev/null +++ b/target/earlgrey/firmware/hwe/platform.rs @@ -0,0 +1,181 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +#![allow(unused_imports)] +use platform_codegen::{handle, signals}; +use pw_status::Error; +use userspace::{entry, syscall}; +use userspace::time::Instant; + + +use earlgrey_sysmgr_client::{SysmgrClient, BootInfo}; +use uart_receiver::UartReceiver; +use uart::Uart0; +use util_error::ErrorCode; +use util_ipc::IpcChannel; + +struct CommandProcessor { + line: [u8; 160], + pos: usize, +} + +impl Default for CommandProcessor { + fn default() -> Self { + Self { + line: [0u8; 160], + pos: 0, + } + } +} + +impl CommandProcessor { + + fn push(&mut self, byte: u8) -> bool { + match byte { + 8 | 127 => { + // Backspace + if self.pos > 0 { + self.pos -= 1; + util_console::print!("\x08 \x08"); + } + false + } + 21 => { + // Ctrl-U: Kill line + while self.pos > 0 { + self.pos -= 1; + util_console::print!("\x08 \x08"); + } + false + } + 23 => { + // Ctrl-W: Kill word + if self.pos > 0 && self.line[self.pos-1] == b' ' { + self.pos -= 1; + util_console::print!("\x08 \x08"); + } + while self.pos > 0 && self.line[self.pos-1] != b' ' { + self.pos -= 1; + util_console::print!("\x08 \x08"); + } + false + } + 13 => { + // Enter: accept line + util_console::println!(""); + true + } + _ => { + // Any other ASCII character: append to line. + if self.pos < self.line.len() && byte < 127 { + self.line[self.pos] = byte; + self.pos += 1; + util_console::print!("{}", byte as char); + } + false + } + } + } + + fn clear(&mut self) { + self.pos = 0; + } + + fn execute(&self) -> Result<(), ErrorCode> { + let mut n = 0; + let mut cmd = [""; 20]; + // SAFETY: this is safe because only ascii input is permitted. + let line = unsafe { core::str::from_utf8_unchecked(&self.line[..self.pos]) }; + for part in line.split(' ').filter(|x| !x.is_empty()) { + if n < cmd.len() { + cmd[n] = part; + n += 1; + } else { + util_console::println!("ERROR: too many arguments"); + } + } + + RootCommandHierarchy::exec(&cmd[..n]) + } +} + +struct RootCommandHierarchy; +impl RootCommandHierarchy { + fn exec(cmd: &[&str]) -> Result<(), ErrorCode> { + match cmd { + ["hello"] => util_console::println!("Hello world!"), + ["info"] => Self::handle_info()?, + ["reboot"] => Self::handle_reboot()?, + [_, ..] => util_console::println!("Unknown command: {}", cmd[0]), + [] => {}, + } + Ok(()) + } + + fn handle_info() -> Result<(), ErrorCode> { + let sysmgr = SysmgrClient::new(IpcChannel::new(handle::SYSMGR_PLATFORM)); + let info = sysmgr.get_boot_info()?; + util_console::println!("{:#?}", info); + Ok(()) + } + fn handle_reboot() -> Result<(), ErrorCode> { + let sysmgr = SysmgrClient::new(IpcChannel::new(handle::SYSMGR_PLATFORM)); + sysmgr.request_reboot()?; + Ok(()) + } + +} + +fn platform_server() -> Result<(), ErrorCode> { + let mut uart = unsafe { UartReceiver::new(Uart0::PTR) }; + + let mut cmd = CommandProcessor::default(); + + uart.enable_receiver(); + uart.enable_interrupt(); + loop { + let w= syscall::object_wait( + handle::UART_INTERRUPTS, + signals::UART0_RX_WATERMARK, + Instant::MAX, + ).map_err(ErrorCode::kernel_error)?; + if w.pending_signals.contains(signals::UART0_RX_WATERMARK) { + if let Some(byte) = uart.receive() { + if cmd.push(byte) { + let _ = cmd.execute(); + cmd.clear(); + } + } + let _ = syscall::interrupt_ack(handle::UART_INTERRUPTS, signals::UART0_RX_WATERMARK); + } + } +} + +fn uart_setup_pinmux() { + use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn}; + let mut pinmux = unsafe { pinmux::PinmuxAon::new() }; + pinmux + .regs_mut() + .mio_periph_insel() + .at(PinmuxPeripheralIn::Uart0Rx as usize) + .modify(|_| (PinmuxInsel::Ioc3 as u32).into()); +} + +#[entry] +fn entry() -> ! { + uart_setup_pinmux(); + let ret = platform_server().map_err(|e| { + pw_log::error!("❌ FAILED: {:08x}", u32::from(e) as u32); + Error::Unknown + }); + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/firmware/hwe/sysmgr.rs b/target/earlgrey/firmware/hwe/sysmgr.rs index e95fc803..fe5d1bd0 100644 --- a/target/earlgrey/firmware/hwe/sysmgr.rs +++ b/target/earlgrey/firmware/hwe/sysmgr.rs @@ -14,8 +14,10 @@ use util_ipc::IpcChannel; fn sysmgr_server() -> Result<(), ErrorCode> { let mut sysmgr = SysmgrServer::new()?; let mut buf = [0u8; 512]; - let ipc = IpcChannel::new(handle::SYSMGR_SERVICE); - sysmgr.run(&ipc, &mut buf) + sysmgr.run(handle::SYSMGR_WAIT_GROUP, &[ + &IpcChannel::new(handle::SYSMGR_USB), + &IpcChannel::new(handle::SYSMGR_PLATFORM), + ], &mut buf) } #[entry] diff --git a/target/earlgrey/firmware/hwe/system.json5 b/target/earlgrey/firmware/hwe/system.json5 index c7f798b9..b41a6e68 100644 --- a/target/earlgrey/firmware/hwe/system.json5 +++ b/target/earlgrey/firmware/hwe/system.json5 @@ -22,7 +22,15 @@ ram_size_bytes: 4096, objects: [ { - name: "sysmgr_service", + name: "sysmgr_wait_group", + type: "wait_group", + }, + { + name: "sysmgr_platform", + type: "channel_handler", + }, + { + name: "sysmgr_usb", type: "channel_handler", }, ], @@ -55,6 +63,62 @@ ], }], }, + + { + name: "platform", + flash_size_bytes: 16384, + processes: [ + { + name: "platform_server", + ram_size_bytes: 4096, + objects: [ + { + name: "sysmgr_platform", + type: "channel_initiator", + handler_process: "sysmgr_process", + handler_object_name: "sysmgr_platform", + }, + { + name: "uart_interrupts", + type: "interrupt", + irqs: [ + //{ name: "uart0_tx_watermark", number: 1 }, + { name: "uart0_rx_watermark", number: 2 }, + //{ name: "uart0_tx_done", number: 3 }, + //{ name: "uart0_rx_overflow", number: 4 }, + //{ name: "uart0_rx_frame_err", number: 5 }, + //{ name: "uart0_rx_break_err", number: 6 }, + //{ name: "uart0_rx_timeout", number: 7 }, + //{ name: "uart0_rx_parity_err", number: 8 }, + //{ name: "uart0_tx_empty", number: 9 }, + ], + }, + ], + memory_mappings: [ + { + name: "uart0", + type: "device", + start_address: 0x40000000, + size_bytes: 0x40, + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000, + } + + ], + threads: [ + { + name: "platform_server_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + { name: "flash_server", flash_size_bytes: 16384, @@ -111,10 +175,10 @@ handler_object_name: "flash_service", }, { - name: "sysmgr_service", + name: "sysmgr_usb", type: "channel_initiator", handler_process: "sysmgr_process", - handler_object_name: "sysmgr_service", + handler_object_name: "sysmgr_usb", }, { name: "usbdev_interrupts", diff --git a/target/earlgrey/firmware/hwe/usbdfu.rs b/target/earlgrey/firmware/hwe/usbdfu.rs index 70c71ba0..68bd3e9a 100644 --- a/target/earlgrey/firmware/hwe/usbdfu.rs +++ b/target/earlgrey/firmware/hwe/usbdfu.rs @@ -320,7 +320,7 @@ fn handle_usb() -> Result<(), ErrorCode> { let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); let mut ep0 = usb_stack::SimpleEp0::new(); - let sysmgr = SysmgrClient::new(IpcChannel::new(handle::SYSMGR_SERVICE)); + let sysmgr = SysmgrClient::new(IpcChannel::new(handle::SYSMGR_USB)); let info = sysmgr.get_boot_info()?; let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, MyDfuHandler { flash: FlashIpcClient::new(IpcChannel::new(handle::FLASH_SERVICE))?,