[test] Implement iteration 2 of e2e_bootstrap_rma This new test uses OpenOCD to connect to lc_ctrl's JTAG TAP, requests a transition to the RMA state, and verifies that the transition occurred. Signed-off-by: Dan McArdle <dmcardle@opentitan.org>
diff --git a/sw/device/silicon_creator/rom/e2e/BUILD b/sw/device/silicon_creator/rom/e2e/BUILD index 36067cc..8c0e9da 100644 --- a/sw/device/silicon_creator/rom/e2e/BUILD +++ b/sw/device/silicon_creator/rom/e2e/BUILD
@@ -455,7 +455,7 @@ otp_partition( name = "CREATOR_SW_CFG", items = { - "CREATOR_SW_CFG_RMA_SPIN_EN": "0x739", # HARDENED_BOOL_TRUE + "CREATOR_SW_CFG_RMA_SPIN_EN": hex(CONST.TRUE), "CREATOR_SW_CFG_RMA_SPIN_CYCLES": "10", }, ), @@ -503,21 +503,32 @@ ) opentitan_functest( - name = "e2e_bootstrap_rma_iteration_1", + name = "e2e_bootstrap_rma", srcs = ["rom_e2e_bootstrap_rma_test.c"], cw310 = cw310_params( bitstream = ":bitstream_bootstrap_rma", - tags = ["vivado"], + tags = [ + "jtag", + "vivado", + ], test_cmds = [ "--rom-kind=rom", - "--bitstream=\"$(location {bitstream})\"", - "--bootstrap=\"$(location {flash})\"", + "--bitstream=\"$(rootpath {bitstream})\"", + "--bootstrap=\"$(rootpath {flash})\"", + "--openocd=\"$(rootpath //third_party/openocd:openocd_bin)\"", + "--openocd-adapter-config=\"$(rootpath //third_party/openocd:jtag_adapter_cfg)\"", ], ), + data = [ + "//third_party/openocd:jtag_adapter_cfg", + "//third_party/openocd:openocd_bin", + ], key = LC_KEY_TYPES[CONST.LCV.PROD][0], targets = ["cw310_rom"], test_harness = "//sw/host/tests/rom/e2e_bootstrap_rma", deps = [ + "//hw/top_earlgrey/sw/autogen:top_earlgrey", + "//sw/device/lib/base:macros", "//sw/device/lib/runtime:log", "//sw/device/lib/testing/test_framework:ottf_main", "//sw/device/silicon_creator/lib/drivers:retention_sram",
diff --git a/sw/host/opentitanlib/src/app/config/opentitan.json b/sw/host/opentitanlib/src/app/config/opentitan.json index aa83e85..9e5309a 100644 --- a/sw/host/opentitanlib/src/app/config/opentitan.json +++ b/sw/host/opentitanlib/src/app/config/opentitan.json
@@ -62,6 +62,19 @@ ] }, { + "name": "PINMUX_TAP_LC", + "pins": [ + { + "name": "TAP_STRAP0", + "level": true + }, + { + "name": "TAP_STRAP1", + "level": false + } + ] + }, + { "name": "RESET", "pins": [ {
diff --git a/sw/host/tests/rom/e2e_bootstrap_rma/BUILD b/sw/host/tests/rom/e2e_bootstrap_rma/BUILD index 1f943a0..069a4de 100644 --- a/sw/host/tests/rom/e2e_bootstrap_rma/BUILD +++ b/sw/host/tests/rom/e2e_bootstrap_rma/BUILD
@@ -2,7 +2,7 @@ # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 -load("@rules_rust//rust:defs.bzl", "rust_binary") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") package(default_visibility = ["//visibility:public"]) @@ -16,6 +16,23 @@ "//third_party/rust/crates:anyhow", "//third_party/rust/crates:humantime", "//third_party/rust/crates:log", + "//third_party/rust/crates:num_enum", + "//third_party/rust/crates:regex", + "//third_party/rust/crates:structopt", + ], +) + +rust_test( + name = "e2e_bootstrap_rma_test", + srcs = [ + "src/main.rs", + ], + deps = [ + "//sw/host/opentitanlib", + "//third_party/rust/crates:anyhow", + "//third_party/rust/crates:humantime", + "//third_party/rust/crates:log", + "//third_party/rust/crates:num_enum", "//third_party/rust/crates:regex", "//third_party/rust/crates:structopt", ],
diff --git a/sw/host/tests/rom/e2e_bootstrap_rma/src/main.rs b/sw/host/tests/rom/e2e_bootstrap_rma/src/main.rs index a55e573..b3392ad 100644 --- a/sw/host/tests/rom/e2e_bootstrap_rma/src/main.rs +++ b/sw/host/tests/rom/e2e_bootstrap_rma/src/main.rs
@@ -4,45 +4,302 @@ use anyhow::{bail, Result}; use opentitanlib::dif::rstmgr::DifRstmgrResetInfo; +use opentitanlib::transport::common::fpga::ClearBitstream; use regex::Regex; +use std::fs::File; +use std::io::Write; +use std::process::{Command, Stdio}; use std::time::Duration; use structopt::StructOpt; use opentitanlib::app::TransportWrapper; +use opentitanlib::chip::boolean::MultiBitBool8; +use opentitanlib::dif::lc_ctrl::{ + DifLcCtrlState, DifLcCtrlToken, LcBit, LcCtrlReg, LcCtrlStatusBit, LcCtrlTransitionCmdBit, + LcCtrlTransitionRegwenBit, +}; use opentitanlib::execute_test; use opentitanlib::test_utils::init::InitializeTest; use opentitanlib::uart::console::{ExitStatus, UartConsole}; +const CONSOLE_TIMEOUT: Duration = Duration::from_secs(5); +const JTAG_TAP_NAME: &str = "lc_ctrl.tap.0"; + #[derive(Debug, StructOpt)] struct Opts { #[structopt(flatten)] init: InitializeTest, - #[structopt( - long, parse(try_from_str=humantime::parse_duration), - default_value = "50s", - help = "Console receive timeout", - )] - timeout: Duration, + #[structopt(long, help = "Path to OpenOCD binary")] + openocd: String, + + #[structopt(long, help = "Path to OpenOCD JTAG adapter config")] + openocd_adapter_config: String, } -/// Orchestrate iteration 1 of the rom_bootstrap_rma testpoint. This never +// RAII-style struct that clears the FPGA's bitstream when it goes out of scope. +struct ScopedBitstream<'a> { + transport: &'a TransportWrapper, +} + +impl Drop for ScopedBitstream<'_> { + fn drop(&mut self) { + self.transport + .dispatch(&ClearBitstream {}) + .expect("Should dispatch ClearBitstream command successfully"); + } +} + +/// A domain-specific language for reading and writing lifecycle controller +/// registers. It compiles to TCL that OpenOCD can interpret. The goal is to +/// provide a notation for our tests rather than map one-to-one with TCL syntax +/// or OpenOCD primitives. +#[derive(Debug)] +enum OpenocdTclBlock<'a> { + /// Exits OpenOCD with a non-zero status when the register does not contain + /// the given value. + AssertRegEq(LcCtrlReg, u32), + + /// Polls the register a finite number of times until its value matches the + /// expectation. Exits OpenOCD with a non-zero status if it runs out of + /// tries. + PollUntilRegEq(LcCtrlReg, u32), + + /// Writes the given value to the register. + WriteReg(LcCtrlReg, u32), + + /// Echoes the given string to stdout. The string must be trivially + /// quotable, so it must not contain double quotes, backslashes, etc. + Echo(&'a str), +} + +impl OpenocdTclBlock<'_> { + /// Compiles this operation to a JimTCL string that OpenOCD can execute. + fn to_tcl(self) -> String { + match self { + // Contrary to OpenOCD's documentation [0], I can't get `shutdown + // [error]` to affect OpenOCD's exit code. My hacky workaround is + // to create my own fatal error by using a nonexistent command. + // + // [0]: https://openocd.org/doc/html/General-Commands.html + OpenocdTclBlock::AssertRegEq(register, expectation) => { + let reg_offset = register.word_offset(); + format!( + r#" +set reg_value [ {JTAG_TAP_NAME} riscv dmi_read 0x{reg_offset:x} ] +if {{ $reg_value != {expectation} }} {{ + echo "Expected {register:?} == 0x{expectation:x}, but it is $reg_value" + nonexistent_command_that_causes_openocd_to_exit_with_error +}}"# + ) + } + OpenocdTclBlock::PollUntilRegEq(register, expectation) => { + const MAX_ATTEMPTS: u32 = 1000; + const SLEEP_MILLIS: u32 = 5; + let reg_offset = register.word_offset(); + format!( + r#" +for {{ set i 0 }} {{ $i < {MAX_ATTEMPTS} }} {{ set i [expr {{$i + 1}}] }} {{ + set reg_value [ {JTAG_TAP_NAME} riscv dmi_read 0x{reg_offset:x} ] + if {{ $reg_value == {expectation} }} {{ + break + }} + sleep {SLEEP_MILLIS} +}} +if {{ $i == {MAX_ATTEMPTS} }} {{ + echo "Ran out of attempts before {register:?} became 0x{expectation:x}" + nonexistent_command_that_causes_openocd_to_exit_with_error +}}"# + ) + } + OpenocdTclBlock::WriteReg(register, value) => { + let reg_offset = register.word_offset(); + format!("{JTAG_TAP_NAME} riscv dmi_write 0x{reg_offset:x} 0x{value:x}") + } + OpenocdTclBlock::Echo(s) => { + // Do the bare minimum validation before we naively quote `s`. + assert!(!s.chars().any(|c| ['\n', '\r', '"', '\\'].contains(&c))); + format!("echo \"{s}\"") + } + } + } +} + +/// Resets the device with the given strappings applied. Clears the UART RX +/// buffer. Removes the strappings before returning. +fn reset(transport: &TransportWrapper, strappings: &[&str], reset_delay: Duration) -> Result<()> { + log::info!("Resetting target..."); + for strapping in strappings.iter() { + transport.apply_pin_strapping(strapping)?; + } + transport.reset_target(reset_delay, true)?; + for strapping in strappings.iter() { + transport.remove_pin_strapping(strapping)?; + } + Ok(()) +} + +/// Orchestrates iteration 2 of the rom_bootstrap_rma testpoint. Issue the life +/// cycle RMA command and ensure the RMA transition is completed successfully. +fn test_rma_command(opts: &Opts, transport: &TransportWrapper) -> Result<()> { + use LcCtrlReg::{ + ClaimTransitionIf, LcState, LcTransitionCnt, Status, TransitionCmd, TransitionRegwen, + TransitionTarget, TransitionToken0, TransitionToken1, TransitionToken2, TransitionToken3, + }; + use OpenocdTclBlock::{AssertRegEq, Echo, PollUntilRegEq, WriteReg}; + + // This is the preimage of the RMA token. The postimage is already stored in + // the OTP's SECRET2 partition. + let token = DifLcCtrlToken::from([ + 0x53, 0xa3, 0x81, 0x2b, 0x5a, 0x4c, 0x04, 0xa4, // + 0x85, 0xda, 0xac, 0x25, 0x2d, 0x14, 0x5c, 0xaf, + ]); + let [token0, token1, token2, token3] = token.into_register_values(); + + let script_transition_to_rma = [ + Echo("--- Waiting for controller to be ready..."), + PollUntilRegEq( + Status, + LcBit::union([LcCtrlStatusBit::Initialized, LcCtrlStatusBit::Ready]), + ), + Echo("--- Lifecycle controller is ready!"), + AssertRegEq(LcState, DifLcCtrlState::Prod.redundant_encoding()), + AssertRegEq(LcTransitionCnt, 5), + WriteReg(ClaimTransitionIf, u8::from(MultiBitBool8::True).into()), + AssertRegEq(ClaimTransitionIf, u8::from(MultiBitBool8::True).into()), + AssertRegEq( + TransitionRegwen, + LcBit::union([LcCtrlTransitionRegwenBit::TransitionRegwen]), + ), + WriteReg(TransitionToken0, token0), + WriteReg(TransitionToken1, token1), + WriteReg(TransitionToken2, token2), + WriteReg(TransitionToken3, token3), + AssertRegEq(TransitionToken0, token0), + AssertRegEq(TransitionToken1, token1), + AssertRegEq(TransitionToken2, token2), + AssertRegEq(TransitionToken3, token3), + WriteReg(TransitionTarget, DifLcCtrlState::Rma.redundant_encoding()), + AssertRegEq(TransitionTarget, DifLcCtrlState::Rma.redundant_encoding()), + Echo("--- Initiate the life cycle transition"), + WriteReg(TransitionCmd, LcBit::union([LcCtrlTransitionCmdBit::Start])), + AssertRegEq(TransitionRegwen, 0), + AssertRegEq(Status, LcBit::union([LcCtrlStatusBit::Initialized])), + Echo("--- Poll the STATUS register while device erases flash..."), + PollUntilRegEq( + Status, + LcBit::union([ + LcCtrlStatusBit::Initialized, + LcCtrlStatusBit::TransitionSuccessful, + ]), + ), + AssertRegEq(LcState, DifLcCtrlState::PostTransition.redundant_encoding()), + Echo("--- Lifecycle transition complete"), + ]; + + let script_contents: String = script_transition_to_rma + .map(OpenocdTclBlock::to_tcl) + .join("\n"); + + const SCRIPT_FILE_PATH: &str = "tmp_script.cfg"; + File::create(SCRIPT_FILE_PATH)?.write_all(script_contents.as_bytes())?; + + // In subsequent runs of this test, OpenOCD will fail with the following + // error unless we preemptively clear the bitstream. + // ``` + // Error: dmi_read is not implemented for this target. + // + // Error: Unsupported DTM version: 15 + // Finished test test_rma_command: Err(OpenOCD exited with code 1 + // ``` + // + // There's another failure mode: after starting the transition, sometimes + // OpenOCD will complain that the DMI operation timed out. Retrying the test + // never resolves it, but clearing the FPGA bitstream *sometimes* resolves + // it. + // ``` + // Error: DMI operation didn't complete in 2 seconds. The target is + // either really slow or broken. You could increase the timeout with + // riscv set_command_timeout_sec. + // ``` + // + // To minimize flakiness, clear the bitstream before leaving this function. + let _scoped_bitstream = ScopedBitstream { transport }; + + reset( + transport, + &["RMA_BOOTSTRAP", "PINMUX_TAP_LC"], + opts.init.bootstrap.options.reset_delay, + )?; + + let openocd_exit = Command::new(&opts.openocd) + .args(["-f", &opts.openocd_adapter_config]) + .args(["-c", "adapter speed 200;"]) + // Tell OpenOCD how to connect to the lifecycle controller's RISC-V TAP. + // This works because lc_ctrl uses an instance of the RISC-V debug + // module [0]. Its IRLEN is hardcoded to 5 [1] and its IDCODE is 1 [2]. + // + // [0]: https://docs.opentitan.org/hw/ip/lc_ctrl/doc/#life-cycle-tap-controller + // [1]: See where `dmi_jtag_tap` is created in hw/vendor/pulp_riscv_dbg/src/dmi_jtag.sv + // [2]: See where `dmi_jtag` is instantiated in hw/ip/lc_ctrl/rtl/lc_ctrl.sv + // + // Because these commands are command-line args, they *must* end with + // semicolons to satisfy the TCL lexer. When a TCL program is read from + // a file, newlines are sufficient to separate statements. + .args([ + "-c", + "transport select jtag;", + "-c", + "jtag newtap lc_ctrl tap -irlen 5 -expected-id 0x00000001 -ignore-bypass;", + "-c", + &format!( + "target create {JTAG_TAP_NAME} riscv -chain-position lc_ctrl.tap -rtos hwthread;" + ), + "-c", + "init;", + ]) + .args(["-f", SCRIPT_FILE_PATH]) + .args(["-c", "shutdown;"]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + .wait()?; + + if !openocd_exit.success() { + bail!("OpenOCD exited with status {}", openocd_exit); + } + + // Reset the device and confirm that its lifecycle state is now RMA by + // reading the "LCV:" line from the UART. + let uart = transport.uart("console")?; + reset(transport, &[], opts.init.bootstrap.options.reset_delay)?; + let mut console = UartConsole { + timeout: Some(CONSOLE_TIMEOUT), + exit_success: Some(Regex::new(&format!( + "LCV:{:x}\r\n", + DifLcCtrlState::Rma.redundant_encoding() + ))?), + exit_failure: Some(Regex::new(r"LCV:[0-9a-f]+\r\n")?), + ..Default::default() + }; + let result = console.interact(&*uart, None, Some(&mut std::io::stdout()))?; + if result != ExitStatus::ExitSuccess { + bail!("FAIL: {:?}", result); + }; + Ok(()) +} + +/// Orchestrates iteration 1 of the rom_bootstrap_rma testpoint. This never /// issues the life cycle RMA command. It verifies that the ROM times out on /// spin cycles, automatically resets the device, and logs the expected reset /// reasons. fn test_no_rma_command(opts: &Opts, transport: &TransportWrapper) -> Result<()> { - let reset_delay = opts.init.bootstrap.options.reset_delay; let uart = transport.uart("console")?; - - log::info!("Applying RMA_BOOTSTRAP strapping..."); - transport.apply_pin_strapping("RMA_BOOTSTRAP")?; - - log::info!("Resetting target..."); - let clear_uart_rx = true; - transport.reset_target(reset_delay, clear_uart_rx)?; - - log::info!("Removing RMA_BOOTSTRAP strapping..."); - transport.remove_pin_strapping("RMA_BOOTSTRAP")?; + reset( + transport, + &["RMA_BOOTSTRAP"], + opts.init.bootstrap.options.reset_delay, + )?; log::info!("Waiting for reset reasons on console..."); @@ -50,7 +307,7 @@ // sw/device/silicon_creator/rom/e2e/rom_e2e_bootstrap_rma_test.c. let expected_bitfield = u32::from(DifRstmgrResetInfo::Por) | u32::from(DifRstmgrResetInfo::Sw); let mut console = UartConsole { - timeout: Some(opts.timeout), + timeout: Some(CONSOLE_TIMEOUT), exit_success: Some(Regex::new(&format!( "reset_info_bitfield: 0x{expected_bitfield:x}\r\n" ))?), @@ -64,19 +321,59 @@ Ok(()) } -/// Orchestrate iteration 2 of the rom_bootstrap_rma testpoint. Issue the life -/// cycle RMA command and ensure the RMA transition is completed successfully. -fn test_rma_command(_opts: &Opts, _transport: &TransportWrapper) -> Result<()> { - // TODO(lowRISC/opentitan#15677) Implement iteration 2 of rom_bootstrap_rma. - Ok(()) -} - fn main() -> Result<()> { let opts = Opts::from_args(); opts.init.init_logging(); - let transport = opts.init.init_target()?; execute_test!(test_no_rma_command, &opts, &transport); execute_test!(test_rma_command, &opts, &transport); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn openocd_statements() { + assert_eq!( + OpenocdTclBlock::AssertRegEq(LcCtrlReg::LcIdState, 0x3c).to_tcl(), + r#" +set reg_value [ lc_ctrl.tap.0 riscv dmi_read 0xf ] +if { $reg_value != 60 } { + echo "Expected LcIdState == 0x3c, but it is $reg_value" + nonexistent_command_that_causes_openocd_to_exit_with_error +}"# + ); + + assert_eq!( + OpenocdTclBlock::PollUntilRegEq(LcCtrlReg::TransitionToken0, 0x1234).to_tcl(), + r#" +for { set i 0 } { $i < 1000 } { set i [expr {$i + 1}] } { + set reg_value [ lc_ctrl.tap.0 riscv dmi_read 0x6 ] + if { $reg_value == 4660 } { + break + } + sleep 5 +} +if { $i == 1000 } { + echo "Ran out of attempts before TransitionToken0 became 0x1234" + nonexistent_command_that_causes_openocd_to_exit_with_error +}"# + ); + assert_eq!( + OpenocdTclBlock::WriteReg(LcCtrlReg::TransitionToken3, 0xabcd).to_tcl(), + "lc_ctrl.tap.0 riscv dmi_write 0x9 0xabcd" + ); + + assert_eq!(OpenocdTclBlock::Echo("bar").to_tcl(), "echo \"bar\""); + } + + /// Stringifying echo statements should panic when they contain unsupported + /// characters. + #[test] + #[should_panic] + fn openocd_statements_echo_panic() { + let _s = OpenocdTclBlock::Echo("bad string \"").to_tcl(); + } +}