diff --git a/Cargo.lock b/Cargo.lock index fdaa8c61a..02fd2af31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1634,6 +1634,7 @@ dependencies = [ "hif", "humility-cli", "humility-cmd-auxflash", + "humility-cmd-caboose", "humility-cmd-console-proxy", "humility-cmd-counters", "humility-cmd-dashboard", @@ -1705,6 +1706,20 @@ dependencies = [ "trycmd", ] +[[package]] +name = "humility-caboose" +version = "0.1.0" +dependencies = [ + "anyhow", + "humility-arch-arm", + "humility-core", + "humility-doppel", + "humility-probes-core", + "thiserror 2.0.18", + "tlvc", + "tlvc-text", +] + [[package]] name = "humility-cli" version = "0.1.0" @@ -1738,6 +1753,18 @@ dependencies = [ "tlvc", ] +[[package]] +name = "humility-cmd-caboose" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "humility-caboose", + "humility-cli", + "humility-probes-core", + "tlvc-text", +] + [[package]] name = "humility-cmd-console-proxy" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 582a9a39e..a9b5d1757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "humility-arch-cortex", "humility-auxflash", "humility-bin", + "humility-caboose", "humility-cli", "humility-core", "humility-doppel", @@ -26,6 +27,7 @@ members = [ "humility-validate", "humility-vpd-lib", "cmd/auxflash", + "cmd/caboose", "cmd/console-proxy", "cmd/counters", "cmd/dashboard", @@ -116,6 +118,7 @@ humility = { path = "./humility-core", package = "humility-core" } humility-arch-arm = { path = "./humility-arch-arm" } humility-auxflash = { path = "./humility-auxflash" } humility-cortex = { path = "./humility-arch-cortex" } +humility-caboose = { path = "./humility-caboose" } humility-cli = { path = "./humility-cli", default-features = false } humility-doppel = { path = "./humility-doppel" } humility-dump-agent = { path = "./humility-dump-agent" } @@ -134,6 +137,7 @@ humility-stack = { path = "./humility-stack" } humility-validate = { path = "./humility-validate" } humility-vpd-lib = { path = "./humility-vpd-lib" } cmd-auxflash = { path = "./cmd/auxflash", package = "humility-cmd-auxflash" } +cmd-caboose = { path = "./cmd/caboose", package = "humility-cmd-caboose" } cmd-console-proxy = { path = "./cmd/console-proxy", package = "humility-cmd-console-proxy" } cmd-counters = { path = "./cmd/counters", package = "humility-cmd-counters" } cmd-dashboard = { path = "./cmd/dashboard", package = "humility-cmd-dashboard" } diff --git a/README.md b/README.md index a14570f26..951a298cb 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,7 @@ a specified target. (In the above example, one could execute `humility ## Commands - [humility auxflash](#humility-auxflash): manipulate auxiliary flash +- [humility caboose](#humility-caboose): read from the caboose - [humility console-proxy](#humility-console-proxy): SP/host console uart proxy - [humility counters](#humility-counters): display event counters - [humility dashboard](#humility-dashboard): dashboard for Hubris sensor data @@ -295,6 +296,11 @@ This subcommand should be rarely used; `humility flash` will automatically program auxiliary flash when needed. +### `humility caboose` + +Tools to read from the caboose (without an archive) + + ### `humility console-proxy` Act as a proxy for the host serial console when it is jumpered to the SP. diff --git a/cmd/caboose/Cargo.toml b/cmd/caboose/Cargo.toml new file mode 100644 index 000000000..4c9dd0035 --- /dev/null +++ b/cmd/caboose/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "humility-cmd-caboose" +version = "0.1.0" +edition.workspace = true +description = "read from the caboose" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +tlvc-text.workspace = true + +humility-caboose.workspace = true +humility-cli.workspace = true +humility-probes-core.workspace = true diff --git a/cmd/caboose/src/lib.rs b/cmd/caboose/src/lib.rs new file mode 100644 index 000000000..51e345833 --- /dev/null +++ b/cmd/caboose/src/lib.rs @@ -0,0 +1,51 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ## `humility caboose` +//! +//! Tools to read from the caboose (without an archive) + +use anyhow::Result; +use clap::Parser; +use humility_cli::{ExecutionContext, humility_cmd}; + +#[derive(Parser, Debug)] +#[clap(name = "caboose", about = env!("CARGO_PKG_DESCRIPTION"))] +pub struct CabooseArgs { + #[clap(subcommand)] + cmd: CabooseCommand, +} + +#[derive(Parser, Debug)] +enum CabooseCommand { + Read, +} + +fn caboose(subargs: CabooseArgs, context: &mut ExecutionContext) -> Result<()> { + let log = context.log(); + match subargs.cmd { + CabooseCommand::Read => { + let mut probe = context.cli.attach_probe(None)?; + let mut t = humility_caboose::read_tlvc_caboose(&mut probe, log)?; + + // Strip raw bytes from the end, for pretty-printing + if let Some(tlvc_text::Piece::Bytes(bs)) = t.last() + && bs.iter().all(|c| *c == 0xFF) + { + t.pop(); + } + + if t.is_empty() { + panic!("caboose is empty"); + } + + let mut text = vec![]; + tlvc_text::save(&mut text, &t).unwrap(); + println!("{}", std::str::from_utf8(&text).unwrap()); + Ok(()) + } + } +} + +humility_cmd!(CabooseArgs, caboose); diff --git a/humility-bin/Cargo.toml b/humility-bin/Cargo.toml index d0cb77600..64cd328ec 100644 --- a/humility-bin/Cargo.toml +++ b/humility-bin/Cargo.toml @@ -35,6 +35,7 @@ humility = { workspace = true } humility-cortex = { workspace = true } humility-cli = { workspace = true, default-features = false } cmd-auxflash = { workspace = true } +cmd-caboose = { workspace = true } cmd-console-proxy = { workspace = true } cmd-counters = { workspace = true } cmd-dashboard = { workspace = true } diff --git a/humility-caboose/Cargo.toml b/humility-caboose/Cargo.toml new file mode 100644 index 000000000..93bd1085c --- /dev/null +++ b/humility-caboose/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "humility-caboose" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow.workspace = true +thiserror.workspace = true +tlvc.workspace = true +tlvc-text.workspace = true + +humility.workspace = true +humility-arch-arm.workspace = true +humility-doppel.workspace = true +humility-probes-core.workspace = true diff --git a/humility-caboose/src/lib.rs b/humility-caboose/src/lib.rs new file mode 100644 index 000000000..baeae5fd7 --- /dev/null +++ b/humility-caboose/src/lib.rs @@ -0,0 +1,170 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Library to get [caboose](https://hubris.oxide.computer/reference/#caboose) +//! information from an attached system +//! +//! The main entry point is [`read_tlvc_caboose`]. +#![warn(missing_docs)] +use anyhow::Result; + +use humility::{ + core::Core, + log::{Logger, info}, +}; +use humility_doppel::{ + CABOOSE_MAGIC, POSSIBLE_HEADER_MAGIC, POSSIBLE_IMAGE_HEADER_OFFSETS, +}; + +/// Error returned when trying to read a caboose +#[derive(Debug, thiserror::Error)] +pub enum CabooseError { + /// Failed when calling [`Core::halt`] + #[error("could not halt core")] + CouldNotHalt(#[source] anyhow::Error), + + /// Failed when calling [`Core::read_reg`] on the `PC` register + #[error("could not read PC")] + CouldNotReadPc(#[source] anyhow::Error), + + /// Failed when calling [`Core::run`] + #[error("could not run core")] + CouldNotRun(#[source] anyhow::Error), + + /// Program counter does not match any of our expected ranges + #[error("could not find image start point based on program counter {0:#x}")] + UnknownPc(u32), + + /// Flash read failed + #[error("flash read failed")] + FlashReadFailed(#[source] anyhow::Error), + + /// Failed to find [`POSSIBLE_HEADER_MAGIC`] at any expected offset + #[error("failed to find header magic with base address {0:#x}")] + MissingMagic(u32), + + /// Could not compute caboose offset + #[error("could not compute caboose offset; it is probably missing")] + MissingCaboose, + + /// Found a magic value which is not [`CABOOSE_MAGIC`] + #[error("bad caboose magic: expected {CABOOSE_MAGIC:#x}, got {actual:#x}")] + BadCabooseMagic { + /// Magic value that was found in the system + actual: u32, + }, + + /// Caboose exceeds [`humility::core::CORE_MAX_READSIZE`] + #[error( + "caboose size {caboose_size} exceeds max read size {}", + humility::core::CORE_MAX_READSIZE + )] + GiganticCaboose { + /// Actual size of the caboose + caboose_size: usize, + }, + + /// Forwarded error from the TLVC library + #[error("tlvc error: {0}")] + TlvcError(String), // tlvc::Error does not implement the `Error` trait +} + +/// Reads a TLVC-encoded caboose from an attached system +pub fn read_tlvc_caboose( + core: &mut humility_probes_core::ProbeCore, + log: &Logger, +) -> Result, CabooseError> { + // We'll get the program counter to estimate where we should try to read the + // image header, since we don't necessarily have our chip info here. + core.halt().map_err(CabooseError::CouldNotHalt)?; + let pc = core + .read_reg(humility_arch_arm::ARMRegister::PC) + .map_err(CabooseError::CouldNotReadPc)?; + core.run().map_err(CabooseError::CouldNotRun)?; + + // Find the flash base by looking at the program counter + // + // This assumes that we're executing from flash, which is true for all + // systems that we care about. + let base = if (0x10000..0x50000).contains(&pc) { + 0x10000 + } else if (0x50000..0x90000).contains(&pc) { + 0x50000 + } else if (0x08000000..0x08100000).contains(&pc) { + 0x08000000 + } else { + return Err(CabooseError::UnknownPc(pc)); + }; + info!(log, "found flash base at {base:#x} (pc at {pc:#x})"); + + // Find the image header by looking for the appropriate magic word + let mut found_header = None; + for header_offset in POSSIBLE_IMAGE_HEADER_OFFSETS { + let header_magic = core + .read_word_32(base + header_offset) + .map_err(CabooseError::FlashReadFailed)?; + if POSSIBLE_HEADER_MAGIC.contains(&header_magic) { + found_header = Some(header_offset); + info!(log, "found image header at {:#x}", base + header_offset); + break; + } + } + let Some(header_offset) = found_header else { + return Err(CabooseError::MissingMagic(base)); + }; + + // Check that the caboose exists and is valid + let image_size_addr = base + .checked_add(header_offset) + .and_then(|b| b.checked_add(4)) + .ok_or(CabooseError::MissingCaboose)?; + let image_size = core + .read_word_32(image_size_addr) + .map_err(CabooseError::FlashReadFailed)?; + + let caboose_size_addr = base + .checked_add(image_size) + .and_then(|b| b.checked_sub(4)) + .ok_or(CabooseError::MissingCaboose)?; + let caboose_size = core + .read_word_32(caboose_size_addr) + .map_err(CabooseError::FlashReadFailed)?; + + let caboose_magic_addr = (base + image_size) + .checked_sub(caboose_size) + .ok_or(CabooseError::MissingCaboose)?; + let caboose_magic = core + .read_word_32(caboose_magic_addr) + .map_err(CabooseError::FlashReadFailed)?; + if caboose_magic != CABOOSE_MAGIC { + return Err(CabooseError::BadCabooseMagic { actual: caboose_magic }); + } + + // Compute start and end for the raw caboose range (including magic and len) + let raw_caboose_start = base + image_size - caboose_size; + let raw_caboose_end = base + image_size; + info!( + log, + "found caboose at {:#x}..{:#x}", raw_caboose_start, raw_caboose_end, + ); + + // The caboose data range skips the magic word and length word + let caboose_data_range = raw_caboose_start + 4..raw_caboose_end - 4; + let caboose_data_size = caboose_data_range.len(); + + if caboose_data_size > humility::core::CORE_MAX_READSIZE { + return Err(CabooseError::GiganticCaboose { + caboose_size: caboose_data_size, + }); + } + + // Read the whole caboose into memory + let mut caboose_data = vec![0u8; caboose_data_size]; + core.read_8(caboose_data_range.start, &mut caboose_data) + .map_err(CabooseError::FlashReadFailed)?; + + let reader = tlvc::TlvcReader::begin(caboose_data.as_slice()) + .map_err(|e| CabooseError::TlvcError(format!("{e:?}")))?; + Ok(tlvc_text::dump(reader)) +} diff --git a/humility-doppel/src/lib.rs b/humility-doppel/src/lib.rs index a6adddb23..146ff8d45 100644 --- a/humility-doppel/src/lib.rs +++ b/humility-doppel/src/lib.rs @@ -42,6 +42,23 @@ use zerocopy::{ Immutable, IntoBytes, KnownLayout, LittleEndian, U16, U32, U64, }; +/// Magic value when reading the caboose +pub const CABOOSE_MAGIC: u32 = 0xCAB0_005E; + +/// Value for [`ImageHeader::magic`] +/// +/// There are two generations of value, but they have the same header layout. +pub const POSSIBLE_HEADER_MAGIC: [u32; 2] = [0x15356637, 0x64_CE_D6_CA]; + +/// Possible offsets for the image header relative to the beginning of flash +/// +/// The exact offset depends on MCU and versions of the PAC crates. +/// +/// - 0xbc and 0xc0 are possible values for the STM32G0 +/// - 0x298 is for the STM32H7 +/// - 0x130 is for the LPC55 +pub const POSSIBLE_IMAGE_HEADER_OFFSETS: [u32; 4] = [0xbc, 0xc0, 0x130, 0x298]; + #[derive(Copy, Clone, Debug, Eq, PartialEq, Load)] pub struct TaskDesc { pub entry_point: u32,