-
Notifications
You must be signed in to change notification settings - Fork 24
Add opentitanlib-based testing #316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
da9a474
earlgrey: device_id helper function
cfrantz caef465
earlgrey: usbdev with host-based test harness
cfrantz a3b3107
earlgrey: enhance usbserial test
cfrantz ea7c954
earlgrey: Add host-based usbdfu test harness
cfrantz 0f06d25
Add README.md to target/earlgrey/tests
cfrantz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # OpenTitan Earlgrey Integration Tests | ||
|
|
||
| This directory contains integration tests for the OpenTitan Earlgrey target, running on the Pigweed Maize microkernel. | ||
|
|
||
| ## Test Directory Structure | ||
|
|
||
| Each subdirectory here represents a test suite or a specific test target: | ||
|
|
||
| * **[`drivers/gpio`](drivers/gpio)**: Tests the GPIO driver functionality, verifying that pins can be configured, read, written, and toggled. | ||
| * **[`eflash`](eflash)**: Tests the embedded flash driver operations (erase, program, read) using a secure `flash_server` userspace process. | ||
| * **[`ipc/user`](ipc/user)**: Tests userspace IPC channel communication between different processes on the microkernel. | ||
| * **[`logging`](logging)**: Tests the microkernel logging subsystems and `pw_log` routing. | ||
| * **[`threads/kernel`](threads/kernel)**: Tests kernel-level threading, context switching, and scheduling invariants. | ||
| * **[`uart`](uart)**: Tests UART driver initialization and loopback communication (using interrupt-driven RX/TX). | ||
| * **[`unittest_runner`](unittest_runner)**: Runs the upstream Pigweed kernel unit and integration test suite on the target hardware. | ||
| * **[`usbdev`](usbdev)**: Tests basic USB device controller initialization and host-side enumeration. | ||
| * **[`usbdfu`](usbdfu)**: Tests USB Device Firmware Upgrade (DFU) protocol, including loopback download/upload verification and certificate reading. | ||
| * **[`usbserial`](usbserial)**: Tests USB CDC-ACM (Virtual Serial Port) bidirectional data transmission using an echo loop. | ||
|
|
||
| --- | ||
|
|
||
| ## Guide for Writing New Tests | ||
|
|
||
| When writing new integration tests, you can choose between two main patterns depending on whether the test requires host-side interaction (like USB or JTAG) or is self-contained on the target. | ||
|
|
||
| ### 1. Self-Contained Tests | ||
|
|
||
| Self-contained tests run entirely on the Ibex core and report their results via the UART console. The Bazel test runner (`opentitan_test` macro) monitors the console output to determine success or failure. | ||
|
|
||
| * **Success Criteria**: By default, the test runner looks for the string `PASS\n` in the console output. Ensure your firmware prints this (e.g., `pw_log::info!("✅ TEST PASS")` or similar) when all assertions succeed. | ||
| * **Failure Criteria**: By default, the test runner matches the regex `FAIL: .+\n` for failures. If an assertion fails or a panic occurs, ensure the output matches this pattern. | ||
| * **Customization**: You can override these defaults in the `opentitan_test` rule using `exit_success` and `exit_failure` attributes. | ||
|
|
||
| Example firmware structure: | ||
| ```rust | ||
| #[entry] | ||
| fn entry() -> Result<(), Error> { | ||
| pw_log::info!("🔄 MY_TEST START"); | ||
| let result = run_my_test_logic(); | ||
| match result { | ||
| Ok(()) => { | ||
| pw_log::info!("PASS"); // Triggers Bazel test success | ||
| Ok(()) | ||
| } | ||
| Err(e) => { | ||
| pw_log::error!("FAIL: {:?}", e); // Triggers Bazel test failure | ||
| Err(Error::Unknown) | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 2. Tests with Host-Based Test Harnesses | ||
|
|
||
| If your test involves external interfaces (e.g., verifying USB enumeration, DFU transfers, or JTAG operations), you must write a host-based test harness that runs on the host machine and coordinates with the target. | ||
|
|
||
| * **Harness Definition**: Define the host harness as a Rust binary using `opentitan_rust_binary` (which applies the necessary transition for `opentitanlib` compatibility) and link it via the `test_harness` attribute of `opentitan_test`. | ||
| * **Target Reset**: The host harness should start by resetting the target (e.g., `transport.reset(UartRx::Clear)?`) to ensure a clean boot and capture early boot logs. | ||
| * **Device Correlation (Best Practice)**: On systems with multiple connected devices, it is critical to ensure the host harness talks to the correct target. | ||
| * **Firmware**: Read the unique 256-bit Device ID from the Lifecycle Controller (`lc_ctrl`) and print it to the console (e.g., `Serial Number: <hex>`) early in the boot sequence. If using USB, also use this Device ID to populate the USB Serial Number string descriptor. | ||
| * **Harness**: Monitor the UART console first, capture the printed serial number using regex, and then use that serial number to look up and open the correct USB/JTAG device (e.g., `device_by_id_with_timeout(vid, pid, Some(serial), timeout)`). | ||
| * **Console Monitoring**: Use `UartConsole::wait_for` to synchronize host actions with target states (e.g., waiting for `🔄 RUNNING` before starting USB transfers). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # USB Device Test (`usbdev`) | ||
|
|
||
| This test verifies the USB device functionality on the OpenTitan Earlgrey target. It ensures that the USB device can be enumerated by the host and that it correctly exposes its Device ID as the USB serial number descriptor. | ||
|
|
||
| ## Test Components | ||
|
|
||
| The test consists of two parts: | ||
|
|
||
| 1. **Firmware (`test_usb.rs`)**: Runs on the Ibex core of the OpenTitan. | ||
| * Maps and accesses the Lifecycle Controller (`lc_ctrl`) to read the 256-bit Device ID. | ||
| * Formats the Device ID as a 64-character hex string and prints it to the console (`Serial Number: <hex>`). | ||
| * Converts the Device ID bytes into a UTF-16 USB string descriptor. | ||
| * Initializes the USB device controller and exposes the Device ID as the USB Serial Number descriptor. | ||
|
|
||
| 2. **Host Harness (`host_usb_check.rs`)**: Runs on the host machine controlling the test. | ||
| * Resets the target board to ensure a clean boot and capture all logs. | ||
| * Monitors the UART console and waits for the `🔄 RUNNING` log. | ||
| * Uses regex to capture the printed serial number from the console logs: `Serial Number: ([0-9a-fA-F]{64})`. | ||
| * Enables VBUS on the target board (if supported by the transport). | ||
| * Polls the host USB bus for the OpenTitan USB device (matching VID/PID). | ||
| * Once detected, queries the USB device's Serial Number descriptor. | ||
| * Verifies that the USB Serial Number descriptor matches the serial number captured from the console logs. | ||
|
|
||
| ## Running the Test | ||
|
|
||
| ### On CW310 (FPGA) Hardware | ||
|
|
||
| To run the test on a connected CW310 board: | ||
|
|
||
| ```bash | ||
| bazelisk test --test_output=all --cache_test_results=no //target/earlgrey/tests/usbdev:usb_hyper310_test | ||
| ``` | ||
|
|
||
| The test harness will automatically handle bitstream loading, bootstrapping the firmware, resetting the target, and performing the verification. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| // Licensed under the Apache-2.0 license | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| use anyhow::{bail, ensure, Context, Result}; | ||
| use clap::Parser; | ||
| use std::path::PathBuf; | ||
| use std::process::Command; | ||
| use std::time::Duration; | ||
|
|
||
| use opentitanlib::io::console::ConsoleExt; | ||
| use opentitanlib::io::uart::Uart; | ||
| use opentitanlib::test_utils::init::InitializeTest; | ||
| use opentitanlib::transport::Capability; | ||
| use opentitanlib::uart::console::UartConsole; | ||
| use std::time::Instant; | ||
|
|
||
| use usb::{port_path_string, UsbDeviceHandle, UsbOpts}; | ||
|
|
||
| #[derive(Debug, Parser)] | ||
| struct Opts { | ||
| #[command(flatten)] | ||
| init: InitializeTest, | ||
|
|
||
| /// Console/USB timeout. | ||
| #[arg(long, value_parser = humantime::parse_duration, default_value = "60s")] | ||
| timeout: Duration, | ||
|
|
||
| /// USB options. | ||
| #[command(flatten)] | ||
| usb: UsbOpts, | ||
|
|
||
| /// Wait for the USB device to appear before continuing with the test. | ||
| #[arg(long, action = clap::ArgAction::Set, default_value = "true")] | ||
| wait_for_usb_device: bool, | ||
|
|
||
| /// Wait for the firmware to emit a PASS/FAIL result. | ||
| #[arg(long, action = clap::ArgAction::Set, default_value = "true")] | ||
| wait_for_firmware: bool, | ||
|
|
||
| /// Executable to run after USB device connection. | ||
| /// This harness will spawn a process to execute and continue monitoring the UART | ||
| /// until the test passes (or fails). After that, the process will be killed. | ||
| /// If `wait_for_usb_device` is true, the harness will pass two extra arguments | ||
| /// to the executable to specify the bus and address of the USB device, as follows: | ||
| /// `--device <bus>:<addr>`. | ||
| #[arg(long)] | ||
| exec: Option<PathBuf>, | ||
|
|
||
| /// Arguments to pass to the executable. | ||
| #[arg(long)] | ||
| exec_arg: Vec<std::ffi::OsString>, | ||
| } | ||
|
|
||
| fn wait_for_device(opts: &Opts, uart: &dyn Uart) -> Result<UsbDeviceHandle> { | ||
| let stop = Instant::now() + opts.timeout; | ||
| log::info!("waiting for device (with UART log)..."); | ||
| while Instant::now() < stop { | ||
| let mut devices = opts.usb.wait_for_device(Duration::ZERO)?; | ||
| if !devices.is_empty() { | ||
| if devices.len() > 1 { | ||
| log::error!("several USB devices found:"); | ||
| for dev in &devices { | ||
| log::error!( | ||
| "- bus={} address={}", | ||
| dev.device().bus_number(), | ||
| dev.device().address() | ||
| ); | ||
| } | ||
| bail!("several USB devices found"); | ||
| } | ||
| let device = devices.remove(0); | ||
| log::info!( | ||
| "device found at bus={}, address={}, path={}", | ||
| device.device().bus_number(), | ||
| device.device().address(), | ||
| port_path_string(&device.device())? | ||
| ); | ||
| return Ok(device); | ||
| } | ||
|
|
||
| let mut buf = [0u8; 256]; | ||
| match uart.read_timeout(&mut buf, Duration::from_millis(10)) { | ||
| Ok(n) if n > 0 => { | ||
| use std::io::Write; | ||
| std::io::stdout().write_all(&buf[..n])?; | ||
| std::io::stdout().flush()?; | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| bail!("no USB device found"); | ||
| } | ||
|
|
||
| fn main() -> Result<()> { | ||
| let opts = Opts::parse(); | ||
| opts.init.init_logging(); | ||
| let transport = opts.init.init_target()?; | ||
|
|
||
| transport | ||
| .capabilities()? | ||
| .request(Capability::USB) | ||
| .ok() | ||
| .context("This transport does not support USB")?; | ||
|
|
||
| // Certain backends such as QEMU will not enumerate USB device until | ||
| // we request the USB context. | ||
| let _usb_context = transport.usb().context("Cannot get USB context")?; | ||
|
|
||
| // Wait until test is running. | ||
| let uart = transport.uart("console")?; | ||
| log::info!("Resetting target..."); | ||
| transport.reset(opentitanlib::app::UartRx::Clear)?; | ||
| log::info!("waiting for RUNNING on console..."); | ||
| if let Err(e) = UartConsole::wait_for(&*uart, r"RUNNING", Duration::from_secs(5)) { | ||
| log::warn!("Failed waiting for RUNNING (non-fatal): {e}"); | ||
| } | ||
|
|
||
| opts.usb.apply_strappings(&transport, true)?; | ||
| // Enable VBUS sense on the board if necessary. | ||
| if opts.usb.vbus_control_available() { | ||
| opts.usb.enable_vbus(&transport, true)?; | ||
| } | ||
| // Sense VBUS if available. | ||
| if opts.usb.vbus_sense_available() { | ||
| ensure!( | ||
| opts.usb.vbus_present(&transport)?, | ||
| "OT USB does not appear to be connected to a host (VBUS not detected)" | ||
| ); | ||
| } | ||
|
|
||
| let mut captured_serial = None; | ||
| if opts.wait_for_usb_device { | ||
| log::info!("waiting for Serial Number on console..."); | ||
| let res = UartConsole::wait_for(&*uart, r"Serial Number: ([0-9a-fA-F]{64})", opts.timeout)?; | ||
| captured_serial = Some(res[1].clone()); | ||
| log::info!( | ||
| "Captured Serial Number: {}", | ||
| captured_serial.as_ref().unwrap() | ||
| ); | ||
| } | ||
|
|
||
| // Wait for USB device to appear. | ||
| let device = if opts.wait_for_usb_device { | ||
| Some(wait_for_device(&opts, &*uart)?) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| if let (Some(handle), Some(expected_serial)) = (&device, &captured_serial) { | ||
| let device_desc = handle.device().device_descriptor()?; | ||
| let usb_serial = handle.read_serial_number_string_ascii(&device_desc)?; | ||
| log::info!("USB Serial Number: {usb_serial}"); | ||
| if usb_serial.as_str() != expected_serial.as_str() { | ||
| bail!( | ||
| "Serial number mismatch: expected (UART) {}, got (USB) {}", | ||
| expected_serial, | ||
| usb_serial | ||
| ); | ||
| } | ||
| log::info!("Serial number match: {usb_serial}"); | ||
| } | ||
|
|
||
| // Run executable if requested. | ||
| let child = match opts.exec { | ||
| Some(exec) => { | ||
| let mut cmd = Command::new(exec); | ||
| if let Some(device) = device { | ||
| cmd.arg("--device").arg(format!( | ||
| "{}:{}", | ||
| device.device().bus_number(), | ||
| device.device().address() | ||
| )); | ||
| } | ||
| cmd.args(opts.exec_arg); | ||
| log::info!( | ||
| "calling {:?} on {:?}", | ||
| cmd.get_program(), | ||
| cmd.get_args().collect::<Vec<_>>() | ||
| ); | ||
| Some(cmd.spawn().context("could not start executable")?) | ||
| } | ||
| None => None, | ||
| }; | ||
|
|
||
| // Wait for test to pass. | ||
| if opts.wait_for_firmware { | ||
| log::info!("wait for pass..."); | ||
| let res = UartConsole::wait_for(&*uart, r"PASS|FAIL", opts.timeout)?; | ||
| match res[0].as_str() { | ||
| "PASS" => (), | ||
| "FAIL" => bail!("device code reported a failure"), | ||
| _ => (), | ||
| }; | ||
| } | ||
|
|
||
| // Kill executable (if running). | ||
| if let Some(mut child) = child { | ||
| match child.try_wait() { | ||
| Ok(Some(status)) => log::info!("executable exited with: {status}"), | ||
| Ok(None) => { | ||
| log::info!("executable did not finish and will be killed"); | ||
| let _ = child.kill(); | ||
| } | ||
| Err(e) => { | ||
| println!("error attempting to get executable status: {e}"); | ||
| log::info!("killing executable"); | ||
| let _ = child.kill(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.