[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();
+ }
+}