Merge at opentitan f243e680

Change-Id: I5c447581944309d63f2d58ec95647815f462bae0
diff --git a/hw/dv/BUILD b/hw/dv/BUILD
new file mode 100644
index 0000000..29144e4
--- /dev/null
+++ b/hw/dv/BUILD
@@ -0,0 +1,11 @@
+# Copyright 2022 Google contributors.
+
+package(default_visibility = ["//visibility:public"])
+
+# For matcha to get the directory of this folder and run fusesoc.
+exports_files(["BUILD"])
+
+filegroup(
+    name ="all_files",
+    srcs = glob(["**"]),
+)
diff --git a/hw/dv/dpi/spidpi/spidpi.c b/hw/dv/dpi/spidpi/spidpi.c
index 3827635..a6d9314 100644
--- a/hw/dv/dpi/spidpi/spidpi.c
+++ b/hw/dv/dpi/spidpi/spidpi.c
@@ -51,7 +51,12 @@
   ctx->driving = P2D_CSB | ((ctx->cpol) ? P2D_SCK : 0);
   char cwd[PATH_MAX];
   char *cwd_rv;
-  cwd_rv = getcwd(cwd, sizeof(cwd));
+  const char *test_out_dir = getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+  if (test_out_dir) {
+    cwd_rv = strncpy(cwd, test_out_dir, sizeof(cwd));
+  } else {
+    cwd_rv = getcwd(cwd, sizeof(cwd));
+  }
   assert(cwd_rv != NULL);
 
   int rv;
diff --git a/hw/dv/dpi/uartdpi/uartdpi.c b/hw/dv/dpi/uartdpi/uartdpi.c
index fa69858..479162f 100644
--- a/hw/dv/dpi/uartdpi/uartdpi.c
+++ b/hw/dv/dpi/uartdpi/uartdpi.c
@@ -57,10 +57,20 @@
 
     } else {
       FILE *log_file;
-      log_file = fopen(log_file_path, "w");
+      const char *test_out_dir = getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+      const int kMaxPathLen = 512;
+      char test_out_log_file[kMaxPathLen] = {0};
+      if (test_out_dir) {
+        snprintf(test_out_log_file, kMaxPathLen, "%s/%s", test_out_dir,
+                 log_file_path),
+            log_file = fopen(test_out_log_file, "w");
+      } else {
+        log_file = fopen(log_file_path, "w");
+      }
       if (!log_file) {
         fprintf(stderr, "UART: Unable to open log file at %s: %s\n",
-                log_file_path, strerror(errno));
+                test_out_dir ? test_out_log_file : log_file_path,
+                strerror(errno));
       } else {
         // Switch log file output to line buffering to ensure lines written to
         // the UART device show up in the log file as soon as a newline
@@ -70,7 +80,7 @@
 
         ctx->log_file = log_file;
         printf("UART: Additionally writing all UART output to '%s'.\n",
-               log_file_path);
+               test_out_dir ? test_out_log_file : log_file_path);
       }
     }
   }
diff --git a/hw/dv/dpi/usbdpi/usbdpi.c b/hw/dv/dpi/usbdpi/usbdpi.c
index 8c3c9c5..3ccfcb3 100644
--- a/hw/dv/dpi/usbdpi/usbdpi.c
+++ b/hw/dv/dpi/usbdpi/usbdpi.c
@@ -112,7 +112,12 @@
 
   char cwd[FILENAME_MAX];
   char *cwd_rv;
-  cwd_rv = getcwd(cwd, sizeof(cwd));
+  const char *test_out_dir = getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+  if (test_out_dir) {
+    cwd_rv = strncpy(cwd, test_out_dir, sizeof(cwd));
+  } else {
+    cwd_rv = getcwd(cwd, sizeof(cwd));
+  }
   assert(cwd_rv != NULL);
 
   // Monitor log file
diff --git a/hw/dv/verilator/simutil_verilator/cpp/verilator_sim_ctrl.cc b/hw/dv/verilator/simutil_verilator/cpp/verilator_sim_ctrl.cc
index 3c8ddea..53589d9 100644
--- a/hw/dv/verilator/simutil_verilator/cpp/verilator_sim_ctrl.cc
+++ b/hw/dv/verilator/simutil_verilator/cpp/verilator_sim_ctrl.cc
@@ -319,11 +319,23 @@
 }
 
 const char *VerilatorSimCtrl::GetTraceFileName() const {
+  const char *test_output_dir = getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+  constexpr int kMaxPathLen = 512;
+  static char trace_file[kMaxPathLen];
 #ifdef VM_TRACE_FMT_FST
-  return "sim.fst";
+  if (test_output_dir) {
+    snprintf(trace_file, kMaxPathLen, "%s/sim.fst", test_output_dir);
+  } else {
+    return "sim.fst";
+  }
 #else
-  return "sim.vcd";
+  if (test_output_dir) {
+    snprintf(trace_file, kMaxPathLen, "%s/sim.fst", test_output_dir);
+  } else {
+    return "sim.vcd";
+  }
 #endif
+  return trace_file;
 }
 
 void VerilatorSimCtrl::Run() {
diff --git a/hw/ip/otbn/BUILD b/hw/ip/otbn/BUILD
index ba54a6a..67d0bb7 100644
--- a/hw/ip/otbn/BUILD
+++ b/hw/ip/otbn/BUILD
@@ -10,3 +10,39 @@
         "//hw/ip/otbn/data:all_files",
     ],
 )
+
+genrule(
+    name = "otbn_simple_smoke_test_obj",
+    srcs = [
+        "dv/smoke/smoke_test.s",
+    ],
+    outs = [
+        "otbn_simple_smoke_test.o",
+    ],
+    cmd = """
+    export RV32_TOOL_AS=$(location @lowrisc_rv32imcb_files//:bin/riscv32-unknown-elf-as)
+    $(location //hw/ip/otbn/util:otbn_as) -o $@ $<
+    """,
+    tools = [
+        "//hw/ip/otbn/util:otbn_as",
+        "@lowrisc_rv32imcb_files//:bin/riscv32-unknown-elf-as",
+    ],
+)
+
+genrule(
+    name = "otbn_simple_smoke_test_elf",
+    srcs = [
+        ":otbn_simple_smoke_test.o",
+    ],
+    outs = [
+        "otbn_simple_smoke_test.elf",
+    ],
+    cmd = """
+    export RV32_TOOL_LD=$(location @lowrisc_rv32imcb_files//:bin/riscv32-unknown-elf-ld)
+    $(location //hw/ip/otbn/util:otbn_ld) -o $@ $<
+    """,
+    tools = [
+        "//hw/ip/otbn/util:otbn_ld",
+        "@lowrisc_rv32imcb_files//:bin/riscv32-unknown-elf-ld",
+    ],
+)
diff --git a/hw/ip/rv_dm/data/rv_dm.hjson b/hw/ip/rv_dm/data/rv_dm.hjson
index 5151e59..0c8407f 100644
--- a/hw/ip/rv_dm/data/rv_dm.hjson
+++ b/hw/ip/rv_dm/data/rv_dm.hjson
@@ -31,7 +31,8 @@
   param_list: [
     { name:    "NrHarts",
       type:    "int",
-      default: "1",
+      // TODO(b/271173103).
+      default: "2",
       desc:    "Number of hardware threads in the system."
       local:   "true"
     },
@@ -110,11 +111,13 @@
                power down of the core and bus-attached peripherals.
                '''
     },
-    { struct:  "logic [rv_dm_reg_pkg::NrHarts-1:0]"
+    { struct:  "logic"
       type:    "uni"
       name:    "debug_req"
       act:     "req"
       desc:    "This is the debug request interrupt going to the main processor."
+      // TODO(b/271173103).
+      width:   "2"
     },
   ]
   countermeasures: [
diff --git a/hw/ip/rv_dm/rtl/rv_dm_reg_pkg.sv b/hw/ip/rv_dm/rtl/rv_dm_reg_pkg.sv
index eb2c96c..f7791ca 100644
--- a/hw/ip/rv_dm/rtl/rv_dm_reg_pkg.sv
+++ b/hw/ip/rv_dm/rtl/rv_dm_reg_pkg.sv
@@ -7,7 +7,7 @@
 package rv_dm_reg_pkg;
 
   // Param list
-  parameter int NrHarts = 1;
+  parameter int NrHarts = 2;
   parameter int NumAlerts = 1;
 
   // Address widths within the block
diff --git a/hw/ip/tlul/rtl/tlul_pkg.sv b/hw/ip/tlul/rtl/tlul_pkg.sv
index 4e9401f..34f601d 100644
--- a/hw/ip/tlul/rtl/tlul_pkg.sv
+++ b/hw/ip/tlul/rtl/tlul_pkg.sv
@@ -13,17 +13,43 @@
   // both in terms of area and timing.
   parameter ArbiterImpl = "PPC";
 
+  // TODO(hoangm): Below enums have been modified to support TL-UH. Eventually
+  // we'll want to upstream these changes to OT.
   typedef enum logic [2:0] {
     PutFullData    = 3'h 0,
     PutPartialData = 3'h 1,
-    Get            = 3'h 4
+    ArithmeticData = 3'h 2,
+    LogicalData    = 3'h 3,
+    Get            = 3'h 4,
+    Intent         = 3'h 5
   } tl_a_op_e;
 
   typedef enum logic [2:0] {
     AccessAck     = 3'h 0,
-    AccessAckData = 3'h 1
+    AccessAckData = 3'h 1,
+    HintAck       = 3'h 2
   } tl_d_op_e;
 
+  typedef enum logic [2:0] {
+    Min  = 3'h 0,
+    Max  = 3'h 1,
+    MinU = 3'h 2,
+    MaxU = 3'h 3,
+    Add  = 3'h 4
+  } tl_a_arith_param_e;
+
+  typedef enum logic [2:0] {
+    Xor  = 3'h 0,
+    Or   = 3'h 1,
+    And  = 3'h 2,
+    Swap = 3'h 3
+  } tl_a_logical_param_e;
+
+  typedef enum logic [2:0] {
+    PrefetchRead  = 3'h 0,
+    PrefetchWrite = 3'h 1
+  } tl_a_intent_param_e;
+
   parameter int H2DCmdMaxWidth  = 57;
   parameter int H2DCmdIntgWidth = 7;
   parameter int H2DCmdFullWidth = H2DCmdMaxWidth + H2DCmdIntgWidth;
diff --git a/hw/lint/BUILD b/hw/lint/BUILD
new file mode 100644
index 0000000..2f3e391
--- /dev/null
+++ b/hw/lint/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2022 Google contributors.
+
+package(default_visibility = ["//visibility:public"])
+
+# For matcha to get the directory of this folder and run fusesoc.
+exports_files(["BUILD"])
diff --git a/hw/vendor/BUILD b/hw/vendor/BUILD
new file mode 100644
index 0000000..2f3e391
--- /dev/null
+++ b/hw/vendor/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2022 Google contributors.
+
+package(default_visibility = ["//visibility:public"])
+
+# For matcha to get the directory of this folder and run fusesoc.
+exports_files(["BUILD"])
diff --git a/python-requirements.txt b/python-requirements.txt
index 700f56d..bb655f6 100644
--- a/python-requirements.txt
+++ b/python-requirements.txt
@@ -5,7 +5,8 @@
 # Keep sorted.
 hjson==3.1.0
 jsonschema==4.17.3; python_version >= "3.7"
-libcst==0.4.1
+# Need to pin to 0.4.9 to be compatible with python3.11
+libcst==0.4.9
 mako==1.1.6
 pluralizer==1.2.0
 pycryptodome==3.15.0
@@ -50,12 +51,22 @@
 anytree==2.8.0
 
 # Development version with OT-specific changes
-git+https://github.com/lowRISC/fusesoc.git@ot-0.3
+# The version is different from upstream OT because ot-0.3 builds HW twice
+# somehow and doubles the build time.
+git+https://github.com/lowRISC/fusesoc.git@ot-0.1
 
 # Development version with OT-specific changes
-git+https://github.com/lowRISC/edalize.git@v0.4.0
+# The version is different from upstream OT because v0.4.0 builds HW twice
+# somehow and doubles the build time.
+git+https://github.com/lowRISC/edalize.git@ot-0.1
 
 # Development version of minimal ChipWhisperer toolchain with latest features
 # and bug fixes. We fix the version for improved stability and manually update
 # if necessary.
 git+https://github.com/newaetech/chipwhisperer-minimal.git@2643131b71e528791446ee1bab7359120288f4ab#egg=chipwhisperer
+
+# Dependencies: @matcha//util/image_to_c.py
+Pillow == 9.3.0
+
+# Dependencies: @matcha//util/run_live_cam.py
+pyserial == 3.5
diff --git a/rules/fusesoc.bzl b/rules/fusesoc.bzl
index dbb7e02..242bad3 100644
--- a/rules/fusesoc.bzl
+++ b/rules/fusesoc.bzl
@@ -67,14 +67,24 @@
     args.add_all(ctx.attr.systems)
     args.add_all(flags)
 
+    _inputs = ctx.files.srcs + ctx.files.cores
+
+    # For some reason, the sanboxed fusesoc would call vivado twice. Add an
+    # option to use the system installed fusesoc.
+    if ctx.attr.use_system_fusesoc:
+        _exec = "fusesoc"
+    else:
+        _exec = ctx.executable._fusesoc
+        _inputs += ctx.files._fusesoc
+
     # Note: the `fileset_top` flag used above is specific to the OpenTitan
     # project to select the correct RTL fileset.
     ctx.actions.run(
         mnemonic = "FuseSoC",
         outputs = outputs,
-        inputs = ctx.files.srcs + ctx.files.cores + ctx.files._fusesoc,
+        inputs = _inputs,
         arguments = [args],
-        executable = ctx.executable._fusesoc,
+        executable = _exec,
         use_default_shell_env = False,
         execution_requirements = {
             "no-sandbox": "",
@@ -104,6 +114,10 @@
         ),
         "verilator_options": attr.label(),
         "make_options": attr.label(),
+        "use_system_fusesoc": attr.bool(
+            default = False,
+            doc = "Use non-sanboxed fusesoc (for FPGA vivado build)",
+        ),
         "_fusesoc": attr.label(
             default = entry_point("fusesoc"),
             executable = True,
diff --git a/rules/opentitan.bzl b/rules/opentitan.bzl
index 9a68c64..7d96cfd 100644
--- a/rules/opentitan.bzl
+++ b/rules/opentitan.bzl
@@ -326,7 +326,7 @@
         "platform": attr.string(default = OPENTITAN_PLATFORM),
         "_cleanup_script": attr.label(
             allow_single_file = True,
-            default = Label("@//rules/scripts:expand_tabs.sh"),
+            default = Label("@lowrisc_opentitan//rules/scripts:expand_tabs.sh"),
         ),
         "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
     },
@@ -481,7 +481,7 @@
         "otp": attr.label(allow_single_file = True),
         "vmem": attr.label(allow_single_file = True),
         "_tool": attr.label(
-            default = "@//util/design:gen-flash-img",
+            default = "@lowrisc_opentitan//util/design:gen-flash-img",
             executable = True,
             cfg = "exec",
         ),
@@ -529,7 +529,7 @@
         "srcs": attr.label_list(allow_files = True),
         "platform": attr.string(default = OPENTITAN_PLATFORM),
         "_tool": attr.label(
-            default = "@//util/device_sw_utils:extract_sw_logs_db",
+            default = "@lowrisc_opentitan//util/device_sw_utils:extract_sw_logs_db",
             cfg = "exec",
             executable = True,
         ),
diff --git a/rules/opentitan_test.bzl b/rules/opentitan_test.bzl
index 03870ed..455e371 100644
--- a/rules/opentitan_test.bzl
+++ b/rules/opentitan_test.bzl
@@ -2,7 +2,7 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-load("@//rules:opentitan.bzl", "opentitan_flash_binary", "opentitan_rom_binary")
+load("@lowrisc_opentitan//rules:opentitan.bzl", "opentitan_flash_binary", "opentitan_rom_binary")
 load("@bazel_skylib//lib:shell.bzl", "shell")
 load("@bazel_skylib//lib:collections.bzl", "collections")
 
diff --git a/rules/ujson.bzl b/rules/ujson.bzl
index c7f922a..649510b 100644
--- a/rules/ujson.bzl
+++ b/rules/ujson.bzl
@@ -11,6 +11,9 @@
     module = ctx.actions.declare_file("{}.rs".format(ctx.label.name))
     ujson_lib = ctx.attr.ujson_lib[CcInfo].compilation_context.headers.to_list()
 
+    # Get the include search path for ujson.
+    ujson_lib_root = ctx.attr.ujson_lib[CcInfo].compilation_context.quote_includes.to_list()
+
     srcs = []
     includes = []
     for src in ctx.attr.srcs:
@@ -29,7 +32,8 @@
     # 3. Substitute all `rust_attr` for `#`, thus creating rust attributes.
     # 4. Format it with `rustfmt` so it looks nice and can be inspected.
     command = """
-        {preprocessor} -nostdinc -I. -DRUST_PREPROCESSOR_EMIT=1 -DNOSTDINC=1 {defines} $@ \
+        {preprocessor} -nostdinc -I{ujson_lib_root_includes} \
+        -DRUST_PREPROCESSOR_EMIT=1 -DNOSTDINC=1 {defines} $@ \
         | grep -v '#' \
         | sed -e "s/rust_attr/#/g" \
         | {rustfmt} > {module}""".format(
@@ -37,6 +41,7 @@
         defines = " ".join(defines),
         module = module.path,
         rustfmt = rustfmt.path,
+        ujson_lib_root_includes = " -I".join(ujson_lib_root),
     )
 
     ctx.actions.run_shell(
diff --git a/sw/device/lib/dif/dif_rstmgr.c b/sw/device/lib/dif/dif_rstmgr.c
index d3daf35..4379653 100644
--- a/sw/device/lib/dif/dif_rstmgr.c
+++ b/sw/device/lib/dif/dif_rstmgr.c
@@ -32,9 +32,10 @@
                                            << RSTMGR_RESET_INFO_HW_REQ_OFFSET),
               "kDifRstmgrResetInfoHwReq must match the register definition!");
 
-static_assert(
-    RSTMGR_PARAM_NUM_SW_RESETS == 8,
-    "Number of software resets has changed, please update this file!");
+// Turn off the static_assert to enable non-earlgrey top run
+// static_assert(
+//     RSTMGR_PARAM_NUM_SW_RESETS == 8,
+//     "Number of software resets has changed, please update this file!");
 
 // The Reset Manager implementation will have to be updated if the number
 // of software resets grows, as it would span across multiple registers, so
diff --git a/sw/device/lib/runtime/ibex.h b/sw/device/lib/runtime/ibex.h
index abe608b..634c972 100644
--- a/sw/device/lib/runtime/ibex.h
+++ b/sw/device/lib/runtime/ibex.h
@@ -35,6 +35,7 @@
   kIbexExcLoadAccessFault = 5,
   kIbexExcStoreAccessFault = 7,
   kIbexExcUserECall = 8,
+  kIbexExcSupervisorECall = 9,
   kIbexExcMachineECall = 11,
   kIbexExcMax = 31
 } ibex_exc_t;
diff --git a/sw/device/tests/BUILD b/sw/device/tests/BUILD
index 6153b52..95eb3ed 100644
--- a/sw/device/tests/BUILD
+++ b/sw/device/tests/BUILD
@@ -1117,6 +1117,10 @@
     ],
     test_harness = "//sw/host/tests/chip/gpio",
     verilator = verilator_params(
+        tags = [
+            # TODO(opentitan/#17812): Verilator test is broken
+            "broken",
+        ],
         timeout = "eternal",
         test_cmds = [],
     ),
@@ -1883,6 +1887,10 @@
         timeout = "eternal",
         exit_failure = ROM_BOOT_FAILURE_MSG,
         rom = "//sw/device/silicon_creator/rom:rom_with_fake_keys",
+        tags = [
+            # TODO(opentitan/#17813): The test is broken
+            "broken",
+        ],
     ),
     deps = [
         "//hw/top_earlgrey/sw/autogen:top_earlgrey",
diff --git a/sw/device/tests/autogen/BUILD b/sw/device/tests/autogen/BUILD
index d139bea..efb602a 100644
--- a/sw/device/tests/autogen/BUILD
+++ b/sw/device/tests/autogen/BUILD
@@ -102,3 +102,52 @@
         "//sw/device/lib/testing/test_framework:ottf_main",
     ],
 )
+
+opentitan_functest(
+    name = "alert_renode_test",
+    srcs = ["alert_test.c"],
+    deps = [
+        "//hw/top_earlgrey/sw/autogen:top_earlgrey",
+        "//sw/device/lib/base:memory",
+        "//sw/device/lib/base:mmio",
+        "//sw/device/lib/dif:adc_ctrl",
+        "//sw/device/lib/dif:aes",
+        "//sw/device/lib/dif:alert_handler",
+        "//sw/device/lib/dif:aon_timer",
+        "//sw/device/lib/dif:clkmgr",
+        "//sw/device/lib/dif:csrng",
+        "//sw/device/lib/dif:edn",
+        "//sw/device/lib/dif:entropy_src",
+        "//sw/device/lib/dif:flash_ctrl",
+        "//sw/device/lib/dif:gpio",
+        "//sw/device/lib/dif:hmac",
+        "//sw/device/lib/dif:i2c",
+        "//sw/device/lib/dif:keymgr",
+        "//sw/device/lib/dif:kmac",
+        "//sw/device/lib/dif:lc_ctrl",
+        "//sw/device/lib/dif:otbn",
+        "//sw/device/lib/dif:otp_ctrl",
+        "//sw/device/lib/dif:pattgen",
+        "//sw/device/lib/dif:pinmux",
+        "//sw/device/lib/dif:pwm",
+        "//sw/device/lib/dif:pwrmgr",
+        "//sw/device/lib/dif:rom_ctrl",
+        "//sw/device/lib/dif:rstmgr",
+        "//sw/device/lib/dif:rv_core_ibex",
+        "//sw/device/lib/dif:rv_plic",
+        "//sw/device/lib/dif:rv_timer",
+        "//sw/device/lib/dif:sensor_ctrl",
+        "//sw/device/lib/dif:spi_device",
+        "//sw/device/lib/dif:spi_host",
+        "//sw/device/lib/dif:sram_ctrl",
+        "//sw/device/lib/dif:sysrst_ctrl",
+        "//sw/device/lib/dif:uart",
+        "//sw/device/lib/dif:usbdev",
+        "//sw/device/lib/runtime:log",
+        "//sw/device/lib/testing:alert_handler_testutils",
+        "//sw/device/lib/testing/test_framework:ottf_main",
+    ],
+    copts = [
+        "-DDISABLE_RENODE_TEST",
+    ],
+)
diff --git a/sw/device/tests/autogen/alert_test.c b/sw/device/tests/autogen/alert_test.c
index 0a8413c..3aeec02 100644
--- a/sw/device/tests/autogen/alert_test.c
+++ b/sw/device/tests/autogen/alert_test.c
@@ -280,6 +280,7 @@
   bool is_cause;
   dif_alert_handler_alert_t exp_alert;
 
+#ifndef DISABLE_RENODE_TEST
   // Write adc_ctrl's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_adc_ctrl_alert_force(&adc_ctrl_aon, kDifAdcCtrlAlertFatalFault + i));
@@ -294,6 +295,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write aes's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
@@ -325,6 +327,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write clkmgr's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
     CHECK_DIF_OK(dif_clkmgr_alert_force(&clkmgr_aon, kDifClkmgrAlertRecovFault + i));
@@ -339,6 +342,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write csrng's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
@@ -355,6 +359,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write edn's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
     CHECK_DIF_OK(dif_edn_alert_force(&edn0, kDifEdnAlertRecovAlert + i));
@@ -369,7 +374,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write edn's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
     CHECK_DIF_OK(dif_edn_alert_force(&edn1, kDifEdnAlertRecovAlert + i));
@@ -384,7 +391,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write entropy_src's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
     CHECK_DIF_OK(dif_entropy_src_alert_force(&entropy_src, kDifEntropySrcAlertRecovAlert + i));
@@ -399,6 +408,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write flash_ctrl's alert_test reg and check alert_cause.
   for (int i = 0; i < 5; ++i) {
@@ -490,6 +500,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write keymgr's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
     CHECK_DIF_OK(dif_keymgr_alert_force(&keymgr, kDifKeymgrAlertRecovOperationErr + i));
@@ -504,6 +515,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write kmac's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
@@ -565,6 +577,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write pattgen's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_pattgen_alert_force(&pattgen, kDifPattgenAlertFatalFault + i));
@@ -579,7 +592,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write pinmux's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_pinmux_alert_force(&pinmux_aon, kDifPinmuxAlertFatalFault + i));
@@ -594,7 +609,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write pwm's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_pwm_alert_force(&pwm_aon, kDifPwmAlertFatalFault + i));
@@ -609,6 +626,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write pwrmgr's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
@@ -670,6 +688,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write rv_plic's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_rv_plic_alert_force(&rv_plic, kDifRvPlicAlertFatalFault + i));
@@ -684,6 +703,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write rv_timer's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
@@ -700,6 +720,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write sensor_ctrl's alert_test reg and check alert_cause.
   for (int i = 0; i < 2; ++i) {
     CHECK_DIF_OK(dif_sensor_ctrl_alert_force(&sensor_ctrl, kDifSensorCtrlAlertRecovAlert + i));
@@ -714,7 +735,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write spi_device's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_spi_device_alert_force(&spi_device, kDifSpiDeviceAlertFatalFault + i));
@@ -729,6 +752,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write spi_host's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
@@ -760,6 +784,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write sram_ctrl's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_sram_ctrl_alert_force(&sram_ctrl_main, kDifSramCtrlAlertFatalError + i));
@@ -774,7 +799,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write sram_ctrl's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_sram_ctrl_alert_force(&sram_ctrl_ret_aon, kDifSramCtrlAlertFatalError + i));
@@ -789,7 +816,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
+#ifndef DISABLE_RENODE_TEST
   // Write sysrst_ctrl's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_sysrst_ctrl_alert_force(&sysrst_ctrl_aon, kDifSysrstCtrlAlertFatalFault + i));
@@ -804,6 +833,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 
   // Write uart's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
@@ -865,6 +895,7 @@
         &alert_handler, exp_alert));
   }
 
+#ifndef DISABLE_RENODE_TEST
   // Write usbdev's alert_test reg and check alert_cause.
   for (int i = 0; i < 1; ++i) {
     CHECK_DIF_OK(dif_usbdev_alert_force(&usbdev, kDifUsbdevAlertFatalFault + i));
@@ -879,6 +910,7 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+#endif
 }
 
 bool test_main(void) {
diff --git a/sw/device/tests/sim_dv/i2c_host_tx_rx_test.c b/sw/device/tests/sim_dv/i2c_host_tx_rx_test.c
index a0a5649..03c8333 100644
--- a/sw/device/tests/sim_dv/i2c_host_tx_rx_test.c
+++ b/sw/device/tests/sim_dv/i2c_host_tx_rx_test.c
@@ -9,8 +9,8 @@
 #include "sw/device/lib/dif/dif_pinmux.h"
 #include "sw/device/lib/dif/dif_rv_plic.h"
 #include "sw/device/lib/runtime/hart.h"
-#include "sw/device/lib/runtime/irq.h"
 #include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/runtime/irq.h"
 #include "sw/device/lib/testing/i2c_testutils.h"
 #include "sw/device/lib/testing/rand_testutils.h"
 #include "sw/device/lib/testing/test_framework/check.h"
diff --git a/sw/host/opentitanlib/BUILD b/sw/host/opentitanlib/BUILD
index d7ab74a..6eeb38f 100644
--- a/sw/host/opentitanlib/BUILD
+++ b/sw/host/opentitanlib/BUILD
@@ -46,6 +46,7 @@
         "src/backend/cw310.rs",
         "src/backend/hyperdebug.rs",
         "src/backend/mod.rs",
+        "src/backend/nexus.rs",
         "src/backend/proxy.rs",
         "src/backend/ti50emulator.rs",
         "src/backend/ultradebug.rs",
@@ -118,6 +119,9 @@
         "src/transport/hyperdebug/mod.rs",
         "src/transport/hyperdebug/spi.rs",
         "src/transport/mod.rs",
+        "src/transport/nexus/gpio.rs",
+        "src/transport/nexus/mod.rs",
+        "src/transport/nexus/spi.rs",
         "src/transport/proxy/emu.rs",
         "src/transport/proxy/gpio.rs",
         "src/transport/proxy/i2c.rs",
diff --git a/sw/host/opentitanlib/src/app/config/mod.rs b/sw/host/opentitanlib/src/app/config/mod.rs
index 7913363..36669e0 100644
--- a/sw/host/opentitanlib/src/app/config/mod.rs
+++ b/sw/host/opentitanlib/src/app/config/mod.rs
@@ -67,5 +67,6 @@
         "/__builtin__/hyperdebug_cw310.json" => include_str!("hyperdebug_cw310.json"),
         "/__builtin__/opentitan_ultradebug.json" => include_str!("opentitan_ultradebug.json"),
         "/__builtin__/opentitan_verilator.json" => include_str!("opentitan_verilator.json"),
+        "/__builtin__/nexus.json" => include_str!("nexus.json"),
     };
 }
diff --git a/sw/host/opentitanlib/src/app/config/nexus.json b/sw/host/opentitanlib/src/app/config/nexus.json
new file mode 100644
index 0000000..e34a640
--- /dev/null
+++ b/sw/host/opentitanlib/src/app/config/nexus.json
@@ -0,0 +1,64 @@
+{
+  "interface": "nexus",
+  "pins": [
+    {
+      "name": "RESET",
+      "alias_of": "RESET_B",
+      "mode": "PushPull",
+      "level": true,
+      "pull_mode": "None"
+    },
+    {
+      "name": "SW_STRAP0",
+      "mode": "PushPull",
+      "level": false,
+      "pull_mode": "None"
+    },
+    {
+      "name": "SW_STRAP1",
+      "mode": "PushPull",
+      "level": false,
+      "pull_mode": "None"
+    },
+    {
+      "name": "SW_STRAP2",
+      "mode": "PushPull",
+      "level": false,
+      "pull_mode": "None"
+    }
+  ],
+  "strappings": [
+    {
+      "name": "ROM_BOOTSTRAP",
+      "pins": [
+        {
+          "name": "SW_STRAP0",
+          "level": true
+        },
+        {
+          "name": "SW_STRAP1",
+          "level": true
+        },
+        {
+          "name": "SW_STRAP2",
+          "level": true
+        }
+      ]
+    },
+    {
+      "name": "RESET",
+      "pins": [
+        {
+          "name": "RESET",
+          "level": false
+        }
+      ]
+    }
+  ],
+  "spi": [
+    {
+      "name": "BOOTSTRAP",
+      "alias_of": "0"
+    }
+  ]
+}
diff --git a/sw/host/opentitanlib/src/backend/mod.rs b/sw/host/opentitanlib/src/backend/mod.rs
index cca0d56..0570fe3 100644
--- a/sw/host/opentitanlib/src/backend/mod.rs
+++ b/sw/host/opentitanlib/src/backend/mod.rs
@@ -16,6 +16,7 @@
 
 mod cw310;
 mod hyperdebug;
+mod nexus;
 mod proxy;
 mod ti50emulator;
 mod ultradebug;
@@ -89,6 +90,10 @@
             cw310::create(args)?,
             Some(Path::new("/__builtin__/opentitan_cw310.json")),
         ),
+        "nexus" => (
+            nexus::create(args)?,
+            Some(Path::new("/__builtin__/nexus.json")),
+        ),
         _ => return Err(Error::UnknownInterface(interface.to_string()).into()),
     };
     let mut env = TransportWrapperBuilder::new(backend);
diff --git a/sw/host/opentitanlib/src/backend/nexus.rs b/sw/host/opentitanlib/src/backend/nexus.rs
new file mode 100644
index 0000000..5e7292c
--- /dev/null
+++ b/sw/host/opentitanlib/src/backend/nexus.rs
@@ -0,0 +1,17 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::transport::nexus::Nexus;
+use crate::transport::Transport;
+use anyhow::Result;
+
+use crate::backend::BackendOpts;
+
+pub fn create(args: &BackendOpts) -> Result<Box<dyn Transport>> {
+    Ok(Box::new(Nexus::new(
+        args.usb_vid,
+        args.usb_pid,
+        args.usb_serial.clone(),
+    )))
+}
diff --git a/sw/host/opentitanlib/src/spiflash/flash.rs b/sw/host/opentitanlib/src/spiflash/flash.rs
index 615be32..0012797 100644
--- a/sw/host/opentitanlib/src/spiflash/flash.rs
+++ b/sw/host/opentitanlib/src/spiflash/flash.rs
@@ -55,8 +55,11 @@
 impl SpiFlash {
     // Well known SPI Flash opcodes.
     pub const READ: u8 = 0x03;
+    pub const READ4: u8 = 0x13;
     pub const PAGE_PROGRAM: u8 = 0x02;
     pub const SECTOR_ERASE: u8 = 0x20;
+    pub const BLOCK_ERASE_3B: u8 = 0xd8;
+    pub const BLOCK_ERASE_4B: u8 = 0xdc;
     pub const CHIP_ERASE: u8 = 0xc7;
     pub const WRITE_ENABLE: u8 = 0x06;
     pub const WRITE_DISABLE: u8 = 0x04;
@@ -230,8 +233,12 @@
     ) -> Result<&Self> {
         // Break the read up according to the maximum chunksize the backend can handle.
         for chunk in buffer.chunks_mut(spi.max_chunk_size()?) {
+            let opcode = match self.address_mode {
+                AddressMode::Mode3b => SpiFlash::READ,
+                AddressMode::Mode4b => SpiFlash::READ4,
+            };
             spi.run_eeprom_transactions(&mut [Transaction::Read(
-                MODE_111.cmd_addr(SpiFlash::READ, address, self.address_mode),
+                MODE_111.cmd_addr(opcode, address, self.address_mode),
                 chunk,
             )])?;
             address += chunk.len() as u32;
@@ -250,12 +257,49 @@
         Ok(self)
     }
 
+    // Erase in 256kB blocks.
+    pub fn block_erase(&self, spi: &dyn Target, address: u32, length: u32) -> Result<()> {
+        self.block_erase_with_progress(spi, address, length, |_, _| {})
+    }
+
     /// Erase a segment of the SPI flash starting at `address` for `length` bytes.
     /// The address and length must be sector aligned.
     pub fn erase(&self, spi: &dyn Target, address: u32, length: u32) -> Result<&Self> {
         self.erase_with_progress(spi, address, length, |_, _| {})
     }
 
+    /// Block erase, and a progress bar.
+    pub fn block_erase_with_progress(
+        &self,
+        spi: &dyn Target,
+        address: u32,
+        length: u32,
+        progress: impl Fn(u32, u32),
+    ) -> Result<()> {
+        // 256kB sectors
+        let sector_size = 256 * 1024;
+        if address % sector_size != 0 {
+            return Err(Error::BadEraseAddress(address, sector_size).into());
+        }
+        if length % sector_size != 0 {
+            return Err(Error::BadEraseAddress(length, sector_size).into());
+        }
+        let end = address + length;
+        for addr in (address..end).step_by(sector_size as usize) {
+            let opcode = match self.address_mode {
+                AddressMode::Mode3b => SpiFlash::BLOCK_ERASE_3B,
+                AddressMode::Mode4b => SpiFlash::BLOCK_ERASE_4B,
+            };
+            spi.run_eeprom_transactions(&mut [
+                Transaction::Command(MODE_111.cmd(SpiFlash::WRITE_ENABLE)),
+                Transaction::Command(MODE_111.cmd_addr(opcode, address, self.address_mode)),
+                Transaction::WaitForBusyClear,
+            ])?;
+            progress(addr, sector_size);
+        }
+        Ok(())
+    }
+
     /// Erase a segment of the SPI flash starting at `address` for `length` bytes.
     /// The address and length must be sector aligned.
     /// The `progress` callback will be invoked after each chunk of the erase operation.
diff --git a/sw/host/opentitanlib/src/spiflash/sfdp.rs b/sw/host/opentitanlib/src/spiflash/sfdp.rs
index 0a109c5..4b83a1a 100644
--- a/sw/host/opentitanlib/src/spiflash/sfdp.rs
+++ b/sw/host/opentitanlib/src/spiflash/sfdp.rs
@@ -941,7 +941,7 @@
                 let start = (8 + i * 8) as usize;
                 let end = start + 8;
                 let phdr = SfdpPhdr::try_from(&buf[start..end])?;
-                len = std::cmp::max(len, phdr.offset + (phdr.dwords * 4) as u32);
+                len = std::cmp::max(len, phdr.offset + ((phdr.dwords as u32) * 4) as u32);
                 log::debug!("computed sfdp len = {}", len);
             }
             Ok(len as usize)
diff --git a/sw/host/opentitanlib/src/transport/mod.rs b/sw/host/opentitanlib/src/transport/mod.rs
index cd484d6..cef1824 100644
--- a/sw/host/opentitanlib/src/transport/mod.rs
+++ b/sw/host/opentitanlib/src/transport/mod.rs
@@ -19,6 +19,7 @@
 pub mod common;
 pub mod cw310;
 pub mod hyperdebug;
+pub mod nexus;
 pub mod proxy;
 pub mod ti50emulator;
 pub mod ultradebug;
diff --git a/sw/host/opentitanlib/src/transport/nexus/gpio.rs b/sw/host/opentitanlib/src/transport/nexus/gpio.rs
new file mode 100644
index 0000000..91350f8
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/nexus/gpio.rs
@@ -0,0 +1,113 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{ensure, Context, Result};
+use lazy_static::lazy_static;
+use safe_ftdi as ftdi;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use crate::collection;
+use crate::io::gpio::{GpioError, GpioPin, PinMode, PullMode};
+use crate::transport::nexus::Nexus;
+use crate::transport::ultradebug::mpsse;
+use crate::util::parse_int::ParseInt;
+
+/// Represents the Nexus GPIO pins.
+pub struct NexusGpio {
+    pub device: Rc<RefCell<mpsse::Context>>,
+}
+
+impl NexusGpio {
+    pub const PIN_SW_STRAP0: u8 = 4;
+    pub const PIN_SW_STRAP1: u8 = 5;
+    pub const PIN_SW_STRAP2: u8 = 6;
+    pub const PIN_RESET_B: u8 = 7;
+    const LAST_PIN_NUM: u8 = 7;
+
+    pub fn open(nexus: &Nexus) -> Result<Self> {
+        Ok(NexusGpio {
+            device: nexus.mpsse(ftdi::Interface::A)?,
+        })
+    }
+
+    pub fn pin(&self, pinname: &str) -> Result<NexusGpioPin> {
+        Ok(NexusGpioPin {
+            device: self.device.clone(),
+            pin_id: self.pin_name_to_number(pinname)?,
+        })
+    }
+
+    /// Given an nexus pin name, return its pin number.
+    pub fn pin_name_to_number(&self, pinname: &str) -> Result<u8> {
+        // If the pinname is an integer, use it; otherwise try to see if it
+        // is a symbolic name of a pin.
+        if let Ok(pinnum) = u8::from_str(pinname) {
+            ensure!(
+                pinnum <= NexusGpio::LAST_PIN_NUM,
+                GpioError::InvalidPinNumber(pinnum)
+            );
+            return Ok(pinnum);
+        }
+        let pinname = pinname.to_uppercase();
+        let pn = pinname.as_str();
+        PIN_NAMES
+            .get(pn)
+            .copied()
+            .ok_or_else(|| GpioError::InvalidPinName(pinname).into())
+    }
+}
+
+pub struct NexusGpioPin {
+    device: Rc<RefCell<mpsse::Context>>,
+    pin_id: u8,
+}
+
+impl GpioPin for NexusGpioPin {
+    /// Reads the value of the the GPIO pin `id`.
+    fn read(&self) -> Result<bool> {
+        let bits = self.device.borrow_mut().gpio_get().context("FTDI error")?;
+        Ok(bits & (1 << self.pin_id) != 0)
+    }
+
+    /// Sets the value of the GPIO pin `id` to `value`.
+    fn write(&self, value: bool) -> Result<()> {
+        self.device
+            .borrow_mut()
+            .gpio_set(self.pin_id, value)
+            .context("FTDI error")?;
+        Ok(())
+    }
+
+    /// Sets the `direction` of GPIO `id` as input or output.
+    fn set_mode(&self, mode: PinMode) -> Result<()> {
+        let direction = match mode {
+            PinMode::Input => false,
+            PinMode::PushPull => true,
+            PinMode::OpenDrain => return Err(GpioError::UnsupportedPinMode(mode).into()),
+                                                PinMode::AnalogInput |
+                                                PinMode::AnalogOutput |
+                                                PinMode::Alternate => todo!(),
+        };
+        self.device
+            .borrow_mut()
+            .gpio_set_direction(self.pin_id, direction)
+            .context("FTDI error")?;
+        Ok(())
+    }
+
+    fn set_pull_mode(&self, _mode: PullMode) -> Result<()> {
+        Ok(())
+    }
+}
+
+lazy_static! {
+    static ref PIN_NAMES: HashMap<&'static str, u8> = collection! {
+        "SW_STRAP0" => 4,
+        "SW_STRAP1" => 5,
+        "SW_STRAP2" => 6,
+        "RESET_B" => 7,
+    };
+}
diff --git a/sw/host/opentitanlib/src/transport/nexus/mod.rs b/sw/host/opentitanlib/src/transport/nexus/mod.rs
new file mode 100644
index 0000000..40db0a5
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/nexus/mod.rs
@@ -0,0 +1,119 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{bail, ensure, Context, Result};
+use safe_ftdi as ftdi;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::io::gpio::GpioPin;
+use crate::io::spi::Target;
+use crate::transport::{
+    Capabilities, Capability, Transport, TransportError, TransportInterfaceType,
+};
+use crate::transport::ultradebug::mpsse;
+
+pub mod gpio;
+pub mod spi;
+
+#[derive(Default)]
+pub struct Nexus {
+    pub usb_vid: Option<u16>,
+    pub usb_pid: Option<u16>,
+    pub usb_serial: Option<String>,
+    mpsse_a: RefCell<Option<Rc<RefCell<mpsse::Context>>>>,
+    inner: RefCell<Inner>,
+}
+
+#[derive(Default)]
+struct Inner {
+    gpio: Option<Rc<gpio::NexusGpio>>,
+    spi: Option<Rc<dyn Target>>,
+}
+
+impl Nexus {
+    /// Create a new `Nexus` struct, optionally specifying the USB vid/pid/serial number.
+    pub fn new(usb_vid: Option<u16>, usb_pid: Option<u16>, usb_serial: Option<String>) -> Self {
+        Nexus {
+            usb_vid,
+            usb_pid,
+            usb_serial,
+            ..Default::default()
+        }
+    }
+
+    /// Construct an `ftdi::Device` for the specified `interface` on the Nexus device.
+    pub fn from_interface(&self, interface: ftdi::Interface) -> Result<ftdi::Device> {
+        Ok(ftdi::Device::from_description_serial(
+            interface,
+            self.usb_vid.unwrap_or(0x0403),
+            self.usb_pid.unwrap_or(0x6011),
+            None,
+            self.usb_serial.clone(),
+        )
+        .context("FTDI error")?)
+    }
+
+    fn mpsse_interface_a(&self) -> Result<Rc<RefCell<mpsse::Context>>> {
+        let mut mpsse_a = self.mpsse_a.borrow_mut();
+        if mpsse_a.is_none() {
+            let device = self.from_interface(ftdi::Interface::A)?;
+            device.set_timeouts(5000, 5000);
+            let mut mpdev = mpsse::Context::new(device).context("FTDI error")?;
+            mpdev.gpio_direction.insert(
+                mpsse::GpioDirection::OUT_0 |
+                mpsse::GpioDirection::OUT_1 |
+                mpsse::GpioDirection::OUT_3 |
+                mpsse::GpioDirection::OUT_4 |
+                mpsse::GpioDirection::OUT_5 |
+                mpsse::GpioDirection::OUT_6 |
+                mpsse::GpioDirection::OUT_7);
+            let _ = mpdev.gpio_get().context("FTDI error")?;
+            mpdev.gpio_value &= 0xF8;
+            *mpsse_a = Some(Rc::new(RefCell::new(mpdev)));
+        }
+        Ok(Rc::clone(mpsse_a.as_ref().unwrap()))
+    }
+
+    /// Construct an `mpsse::Context` for the requested interface.
+    pub fn mpsse(&self, interface: ftdi::Interface) -> Result<Rc<RefCell<mpsse::Context>>> {
+        match interface {
+            ftdi::Interface::A => self.mpsse_interface_a(),
+            _ => {
+                bail!(TransportError::UsbOpenError(format!(
+                    "I don't know how to create an MPSSE context for interface {:?}",
+                    interface
+                )));
+            }
+        }
+    }
+}
+
+impl Transport for Nexus {
+    fn capabilities(&self) -> Result<Capabilities> {
+        Ok(Capabilities::new(
+            Capability::GPIO | Capability::SPI,
+        ))
+    }
+
+    fn gpio_pin(&self, instance: &str) -> Result<Rc<dyn GpioPin>> {
+        let mut inner = self.inner.borrow_mut();
+        if inner.gpio.is_none() {
+            inner.gpio = Some(Rc::new(gpio::NexusGpio::open(self)?));
+        }
+        Ok(Rc::new(inner.gpio.as_ref().unwrap().pin(instance)?))
+    }
+
+    fn spi(&self, instance: &str) -> Result<Rc<dyn Target>> {
+        ensure!(
+            instance == "0",
+            TransportError::InvalidInstance(TransportInterfaceType::Spi, instance.to_string())
+        );
+        let mut inner = self.inner.borrow_mut();
+        if inner.spi.is_none() {
+            inner.spi = Some(Rc::new(spi::NexusSpi::open(self)?));
+        }
+        Ok(Rc::clone(inner.spi.as_ref().unwrap()))
+    }
+}
diff --git a/sw/host/opentitanlib/src/transport/nexus/spi.rs b/sw/host/opentitanlib/src/transport/nexus/spi.rs
new file mode 100644
index 0000000..96c8522
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/nexus/spi.rs
@@ -0,0 +1,198 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{Context, Result};
+use safe_ftdi as ftdi;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::io::spi::{
+    AssertChipSelect, ClockPolarity, SpiError, Target, TargetChipDeassert, Transfer, TransferMode,
+};
+use crate::transport::nexus::Nexus;
+use crate::transport::ultradebug::mpsse;
+
+struct Inner {
+    mode: TransferMode,
+    cs_asserted_count: u32,
+}
+
+/// Represents the Nexus SPI device.
+pub struct NexusSpi {
+    pub device: Rc<RefCell<mpsse::Context>>,
+    inner: RefCell<Inner>,
+}
+
+impl NexusSpi {
+    pub const PIN_CLOCK: u8 = 0;
+    pub const PIN_MOSI: u8 = 1;
+    pub const PIN_MISO: u8 = 2;
+    pub const PIN_CHIP_SELECT: u8 = 3;
+    pub const MASK_CHIP_SELECT: u8 = 1u8 << Self::PIN_CHIP_SELECT;
+    pub const PIN_SPI_ZB: u8 = 4;
+    pub fn open(ultradebug: &Nexus) -> Result<Self> {
+        // let mpsse = ultradebug.mpsse(ftdi::Interface::B)?;
+        let mpsse = ultradebug.mpsse(ftdi::Interface::A)?;
+        // Note: platforms ultradebugs tristate their SPI lines
+        // unless SPI_ZB is driven low.  Non-platforms ultradebugs
+        // don't use SPI_ZB, so this is safe for both types of devices.
+        // log::debug!("Setting SPI_ZB");
+        // mpsse
+        //     .borrow_mut()
+        //     .gpio_set(NexusSpi::PIN_SPI_ZB, false)
+        //     .context("FTDI error f")?;
+
+        Ok(NexusSpi {
+            device: mpsse,
+            inner: RefCell::new(Inner {
+                mode: TransferMode::Mode0,
+                cs_asserted_count: 0,
+            }),
+        })
+    }
+
+    fn do_assert_cs(&self, assert: bool) -> Result<()> {
+        let device = self.device.borrow();
+        // Assert or deassert CS#
+        device
+            .execute(&mut [mpsse::Command::SetLowGpio(
+                device.gpio_direction,
+                if assert {
+                    device.gpio_value & !Self::MASK_CHIP_SELECT
+                } else {
+                    device.gpio_value | Self::MASK_CHIP_SELECT
+                },
+            )])
+            .context("FTDI error g")?;
+        Ok(())
+    }
+}
+
+impl Target for NexusSpi {
+    fn get_transfer_mode(&self) -> Result<TransferMode> {
+        Ok(self.inner.borrow().mode)
+    }
+    fn set_transfer_mode(&self, mode: TransferMode) -> Result<()> {
+        self.inner.borrow_mut().mode = mode;
+        Ok(())
+    }
+
+    fn get_bits_per_word(&self) -> Result<u32> {
+        Ok(8)
+    }
+    fn set_bits_per_word(&self, bits_per_word: u32) -> Result<()> {
+        match bits_per_word {
+            8 => Ok(()),
+            _ => Err(SpiError::InvalidWordSize(bits_per_word).into()),
+        }
+    }
+
+    fn get_max_speed(&self) -> Result<u32> {
+        Ok(self.device.borrow().max_clock_frequency)
+    }
+    fn set_max_speed(&self, frequency: u32) -> Result<()> {
+        let mut device = self.device.borrow_mut();
+        device
+            .set_clock_frequency(frequency)
+            .context("FTDI errorh ")?;
+        Ok(())
+    }
+
+    fn get_max_transfer_count(&self) -> Result<usize> {
+        // Arbitrary value: number of `Transfers` that can be in a single transaction.
+        Ok(42)
+    }
+
+    fn max_chunk_size(&self) -> Result<usize> {
+        // Size of the FTDI read buffer.  We can't perform a read larger than this;
+        // the FTDI device simply won't read any more.
+        Ok(65536)
+    }
+
+    fn run_transaction(&self, transaction: &mut [Transfer]) -> Result<()> {
+        let (rdedge, wredge) = match self.inner.borrow().mode.polarity() {
+            ClockPolarity::IdleLow => (mpsse::ClockEdge::Rising, mpsse::ClockEdge::Falling),
+            ClockPolarity::IdleHigh => (mpsse::ClockEdge::Falling, mpsse::ClockEdge::Rising),
+        };
+
+        let mut command = Vec::new();
+        let device = self.device.borrow();
+        let cs_not_already_asserted = self.inner.borrow().cs_asserted_count == 0;
+        if cs_not_already_asserted {
+            // Assert CS# (drive low).
+            command.push(mpsse::Command::SetLowGpio(
+                device.gpio_direction,
+                device.gpio_value & !Self::MASK_CHIP_SELECT,
+            ));
+        }
+        // Translate SPI Read/Write Transactions into MPSSE Commands.
+        for transfer in transaction.iter_mut() {
+            command.push(match transfer {
+                Transfer::Read(buf) => mpsse::Command::ReadData(
+                    mpsse::DataShiftOptions {
+                        read_clock_edge: rdedge,
+                        read_data: true,
+                        ..Default::default()
+                    },
+                    buf,
+                ),
+                Transfer::Write(buf) => mpsse::Command::WriteData(
+                    mpsse::DataShiftOptions {
+                        write_clock_edge: wredge,
+                        write_data: true,
+                        ..Default::default()
+                    },
+                    buf,
+                ),
+                Transfer::Both(wbuf, rbuf) => mpsse::Command::TransactData(
+                    mpsse::DataShiftOptions {
+                        write_clock_edge: wredge,
+                        write_data: true,
+                        ..Default::default()
+                    },
+                    wbuf,
+                    mpsse::DataShiftOptions {
+                        read_clock_edge: rdedge,
+                        read_data: true,
+                        ..Default::default()
+                    },
+                    rbuf,
+                ),
+            });
+        }
+        if cs_not_already_asserted {
+            // Release CS# (allow to float high).
+            command.push(mpsse::Command::SetLowGpio(
+                device.gpio_direction,
+                device.gpio_value | Self::MASK_CHIP_SELECT,
+            ));
+        }
+        device.execute(&mut command).context("FTDI error i")?;
+        Ok(())
+    }
+
+    fn assert_cs(self: Rc<Self>) -> Result<AssertChipSelect> {
+        {
+            let mut inner = self.inner.borrow_mut();
+            if inner.cs_asserted_count == 0 {
+                self.do_assert_cs(true)?;
+            }
+            inner.cs_asserted_count += 1;
+        }
+        Ok(AssertChipSelect::new(self))
+    }
+}
+
+impl TargetChipDeassert for NexusSpi {
+    fn deassert_cs(&self) {
+        let mut inner = self.inner.borrow_mut();
+        inner.cs_asserted_count -= 1;
+        if inner.cs_asserted_count == 0 {
+            // We cannot propagate errors through `Drop::drop()`, so panic on any error.  (Logging
+            // would be another option.)
+            self.do_assert_cs(false)
+                .expect("Error while deasserting CS");
+        }
+    }
+}
diff --git a/sw/host/opentitantool/src/command/spi.rs b/sw/host/opentitantool/src/command/spi.rs
index 97d8734..935da52 100644
--- a/sw/host/opentitantool/src/command/spi.rs
+++ b/sw/host/opentitantool/src/command/spi.rs
@@ -8,11 +8,13 @@
 use std::fs::{self, File};
 use std::io::{self, Write};
 use std::path::PathBuf;
-use std::time::Instant;
+use std::time::{Duration, Instant};
 use structopt::StructOpt;
 
 use opentitanlib::app::command::CommandDispatch;
 use opentitanlib::app::{self, TransportWrapper};
+// Use PinMode if gpio is enabled
+// use opentitanlib::io::gpio::PinMode;
 use opentitanlib::io::spi::{SpiParams, Transfer};
 use opentitanlib::spiflash::SpiFlash;
 use opentitanlib::tpm;
@@ -215,6 +217,25 @@
     bytes_per_second: f64,
 }
 
+#[derive(Debug, StructOpt)]
+pub struct SpiBlockErase {
+    #[structopt(short, long, help = "Start offset.")]
+    start: u32,
+    #[structopt(short = "n", long, help = "Number of bytes to erase.")]
+    length: u32,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct SpiBlockEraseResponse {
+    length: u32,
+    bytes_per_second: f64,
+}
+
+#[derive(Debug, StructOpt)]
+pub struct SpiChipErase {}
+#[derive(Debug, serde::Serialize)]
+pub struct SpiChipEraseResponse {}
+
 impl CommandDispatch for SpiErase {
     fn run(
         &self,
@@ -242,6 +263,49 @@
     }
 }
 
+impl CommandDispatch for SpiBlockErase {
+    fn run(
+        &self,
+        context: &dyn Any,
+        transport: &TransportWrapper,
+    ) -> Result<Option<Box<dyn Annotate>>> {
+        transport.capabilities()?.request(Capability::SPI).ok()?;
+        let context = context.downcast_ref::<SpiCommand>().unwrap();
+        let spi = context.params.create(transport, "BOOTSTRAP")?;
+        let mut flash = SpiFlash::from_spi(&*spi)?;
+        flash.set_address_mode_auto(&*spi)?;
+
+        let progress = app::progress_bar(self.length as u64);
+        let t0 = Instant::now();
+        flash.block_erase_with_progress(&*spi, self.start, self.length, |_, chunk| {
+            progress.inc(chunk as u64);
+        })?;
+        progress.finish();
+        let duration = t0.elapsed().as_secs_f64();
+
+        Ok(Some(Box::new(SpiBlockEraseResponse {
+            length: self.length,
+            bytes_per_second: self.length as f64 / duration,
+        })))
+    }
+}
+
+impl CommandDispatch for SpiChipErase {
+    fn run(
+        &self,
+        context: &dyn Any,
+        transport: &TransportWrapper,
+    ) -> Result<Option<Box<dyn Annotate>>> {
+        transport.capabilities()?.request(Capability::SPI).ok()?;
+        let context = context.downcast_ref::<SpiCommand>().unwrap();
+        let spi = context.params.create(transport, "BOOTSTRAP")?;
+        let mut flash = SpiFlash::from_spi(&*spi)?;
+        flash.set_address_mode_auto(&*spi)?;
+        flash.chip_erase(&*spi)?;
+        Ok(Some(Box::new(SpiChipEraseResponse {})))
+    }
+}
+
 /// Program data into a SPI EEPROM.
 #[derive(Debug, StructOpt)]
 pub struct SpiProgram {
@@ -257,6 +321,7 @@
     bytes_per_second: f64,
 }
 
+/// Program data into a SPI EEPROM.
 impl CommandDispatch for SpiProgram {
     fn run(
         &self,
@@ -286,6 +351,93 @@
 }
 
 #[derive(Debug, StructOpt)]
+pub struct SpiReadMemory {
+    #[structopt(short, long, default_value = "0", help = "Start offset.")]
+    start: u32,
+    #[structopt(short, long, help = "Length to readback.")]
+    length: u32,
+    #[structopt(short, long, help = "Hexdump the data.")]
+    hexdump: bool,
+    #[structopt(name = "FILE", default_value = "-")]
+    filename: PathBuf,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct SpiReadMemoryResponse {
+    length: usize,
+    bytes_per_second: f64,
+}
+
+impl SpiReadMemory {
+    fn write_file(&self, mut writer: impl Write, buffer: &[u8]) -> Result<()> {
+        if self.hexdump {
+            hexdump(writer, buffer)?;
+        } else {
+            writer.write_all(buffer)?;
+        }
+        Ok(())
+    }
+}
+
+impl CommandDispatch for SpiReadMemory {
+    fn run(
+        &self,
+        context: &dyn Any,
+        transport: &TransportWrapper,
+    ) -> Result<Option<Box<dyn Annotate>>> {
+        transport.capabilities()?.request(Capability::SPI).ok()?;
+        transport.capabilities()?.request(Capability::GPIO).ok()?;
+        let context = context.downcast_ref::<SpiCommand>().unwrap();
+        let spi = context.params.create(transport, "BOOTSTRAP")?;
+        // Rename to gpio if the gpio loop is used
+        let _gpio = transport.gpio_pin("SW_STRAP0")?;
+        spi.set_max_speed(1_000_000)?;
+
+        let mut buffer = vec![0u8; self.length as usize];
+        let mut write_buffer = Vec::with_capacity(8);
+        write_buffer.extend(self.start.to_be_bytes());
+        write_buffer.extend(self.length.to_be_bytes());
+        spi.run_transaction(&mut [Transfer::Write(&write_buffer)])?;
+        // TODO(atv): For some reason, the GPIO does not
+        // read back a changed value in this context (but does, if you try to read it separately)
+        // If we can resolve this, uncomment out this polling block. For now, however, wait 50ms.
+        // gpio.set_mode(PinMode::Input)?;
+        // loop {
+        //     let val = gpio.read()?;
+        //     if val {
+        //         break;
+        //     }
+        //     std::thread::sleep(Duration::from_millis(50));
+        // }
+        // gpio.set_mode(PinMode::PushPull)?;
+        std::thread::sleep(Duration::from_millis(50));
+        let t0 = Instant::now();
+        let chunk_size = spi.max_chunk_size()?;
+        let mut remaining = buffer.len();
+        for i in 0..=(buffer.len() / chunk_size) {
+            let chunk_len = std::cmp::min(chunk_size, remaining);
+            let start = i * chunk_size;
+            let end = start + chunk_len;
+            spi.run_transaction(&mut [Transfer::Read(&mut buffer[start..end])])?;
+            remaining -= chunk_len;
+        }
+        let duration = t0.elapsed().as_secs_f64();
+
+        if self.filename.to_str() == Some("-") {
+            self.write_file(io::stdout(), &buffer)?;
+            Ok(None)
+        } else {
+            let file = File::create(&self.filename)?;
+            self.write_file(file, &buffer)?;
+            Ok(Some(Box::new(SpiReadMemoryResponse {
+                length: buffer.len(),
+                bytes_per_second: buffer.len() as f64 / duration,
+            })))
+        }
+    }
+}
+
+#[derive(Debug, StructOpt)]
 pub struct SpiTpm {
     #[structopt(subcommand)]
     command: super::tpm::TpmSubCommand,
@@ -312,7 +464,10 @@
     ReadId(SpiReadId),
     Read(SpiRead),
     Erase(SpiErase),
+    BlockErase(SpiBlockErase),
+    ChipErase(SpiChipErase),
     Program(SpiProgram),
+    ReadMemory(SpiReadMemory),
     Tpm(SpiTpm),
 }
 
diff --git a/third_party/rust/BUILD b/third_party/rust/BUILD
index 220a746..96ae281 100644
--- a/third_party/rust/BUILD
+++ b/third_party/rust/BUILD
@@ -8,7 +8,7 @@
     annotations = {
         "libudev-sys": [crate.annotation(
             patch_args = ["-p1"],
-            patches = ["@//third_party/rust/patches:libudev-sys-0.1.4.patch"],
+            patches = ["@lowrisc_opentitan//third_party/rust/patches:libudev-sys-0.1.4.patch"],
         )],
     },
     cargo_lockfile = "//third_party/rust:Cargo.lock",
diff --git a/third_party/rust/crates/defs.bzl b/third_party/rust/crates/defs.bzl
index 87101ce..90c0520 100644
--- a/third_party/rust/crates/defs.bzl
+++ b/third_party/rust/crates/defs.bzl
@@ -1206,7 +1206,7 @@
             "-p1",
         ],
         patches = [
-            "@//third_party/rust/patches:libudev-sys-0.1.4.patch",
+            "@lowrisc_opentitan//third_party/rust/patches:libudev-sys-0.1.4.patch",
         ],
         sha256 = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324",
         type = "tar.gz",
diff --git a/third_party/rust/repos.bzl b/third_party/rust/repos.bzl
index d3c3f6a..7c848db 100644
--- a/third_party/rust/repos.bzl
+++ b/third_party/rust/repos.bzl
@@ -2,7 +2,7 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-load("@//rules:repo.bzl", "http_archive_or_local")
+load("@lowrisc_opentitan//rules:repo.bzl", "http_archive_or_local")
 
 def rust_repos(rules_rust = None, safe_ftdi = None, serde_annotate = None):
     # We use forked/patched Rust Bazel rules to enable caching repository rules
diff --git a/util/prep-bazel-airgapped-build.sh b/util/prep-bazel-airgapped-build.sh
index 6b28ecc..be07ba6 100755
--- a/util/prep-bazel-airgapped-build.sh
+++ b/util/prep-bazel-airgapped-build.sh
@@ -164,7 +164,7 @@
   readonly LATEST_BISTREAM_HASH_FILE="${SYSTEM_BITSTREAM_CACHE}/latest.txt"
   # The revision named in latest.txt is not necessarily on disk. Induce the
   # cache backend to fetch the latest bitstreams.
-  BITSTREAM=latest ${BAZELISK} fetch @bitstreams//...
+  BITSTREAM=--offline ${BAZELISK} fetch @bitstreams//...
   cp "${LATEST_BISTREAM_HASH_FILE}" \
     "${BAZEL_AIRGAPPED_DIR}/${BAZEL_BITSTREAMS_CACHE}/"
   LATEST_BISTREAM_HASH=$(cat "${LATEST_BISTREAM_HASH_FILE}")
diff --git a/util/reggen/bus_interfaces.py b/util/reggen/bus_interfaces.py
index fbeb99c..c699ea4 100644
--- a/util/reggen/bus_interfaces.py
+++ b/util/reggen/bus_interfaces.py
@@ -50,7 +50,8 @@
 
             protocol = check_str(ed['protocol'],
                                  'protocol field of ' + entry_what)
-            if protocol != 'tlul':
+            # TODO(hoangm): Investigate TLUH OT upstream support.
+            if protocol != 'tlul' and protocol != 'tluh' :
                 raise ValueError('Unknown protocol {!r} at {}'
                                  .format(protocol, entry_what))
 
diff --git a/util/regtool/Cargo.toml b/util/regtool/Cargo.toml
new file mode 100644
index 0000000..ed409cc
--- /dev/null
+++ b/util/regtool/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "regtool"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
diff --git a/util/regtool/src/lib.rs b/util/regtool/src/lib.rs
new file mode 100644
index 0000000..3584abc
--- /dev/null
+++ b/util/regtool/src/lib.rs
@@ -0,0 +1,124 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#![allow(clippy::needless_doctest_main)]
+
+//! A library for build scripts to generate `.rs` files using `regtool.py`.
+//!
+//! This library is intended to be used as a `build-dependencies` entry in
+//! `Cargo.toml`:
+//!
+//! ```toml
+//! [build-dependencies]
+//! regtool = { path = "path/to/regtool" }
+//! ```
+//!
+//! By default, the library expects the environment variable `REGTOOL` to point
+//! to `regtool.py`. Either set the variable before running cargo, or use
+//! `Build::regtool(...)` from `build.rs` to set the path to the script
+//! explicitly.
+//!
+//! # Examples
+//!
+//! Use the `Build` struct to process `hw/ip/uart/data/uart.hjson`:
+//!
+//! build.rs:
+//!
+//! ```no_run
+//! fn main() {
+//!     regtool::Build::new()
+//!         .in_file_path("hw/ip/uart/data/uart.hjson")
+//!         .generate("uart.rs");
+//! }
+//! ```
+//!
+//! src/main.rs:
+//!
+//! ```no_run
+//! include!(concat!(env!("OUT_DIR"), "/uart.rs"));
+//! ```
+
+use std::env;
+use std::path::Path;
+use std::path::PathBuf;
+use std::process::Command;
+
+#[derive(Clone, Debug)]
+pub struct Build {
+    in_file_path: Option<PathBuf>, // Must be set; no default
+    out_dir: Option<PathBuf>,      // default: $OUT_DIR
+
+    python: Option<PathBuf>,
+    regtool: Option<PathBuf>, // Default: $REGTOOL
+}
+
+macro_rules! option_path_setter {
+    ($name:ident) => {
+        pub fn $name<P: AsRef<Path>>(&mut self, $name: P) -> &mut Build {
+            self.$name = Some($name.as_ref().to_owned());
+            self
+        }
+    };
+}
+
+impl Build {
+    pub fn new() -> Self {
+        Self {
+            in_file_path: None,
+            out_dir: None,
+
+            python: None,
+            regtool: None,
+        }
+    }
+
+    // Generate setter functions for Option<PathBuf> fields:
+    option_path_setter!(in_file_path);
+    option_path_setter!(out_dir);
+    option_path_setter!(python);
+    option_path_setter!(regtool);
+
+    // Run regtool. If out_file is an absolute path, write output to out_file,
+    // otherwise write output to out_dir/out_file.
+    pub fn generate(&self, out_file: &str) {
+        let regtool = if let Some(regtool) = &self.regtool {
+            regtool.to_owned()
+        } else {
+            println!("cargo:rerun-if-env-changed=REGTOOL");
+            PathBuf::from(env::var("REGTOOL").expect("missing environment variable 'REGTOOL'"))
+        };
+        println!("cargo:rerun-if-changed={}", regtool.display());
+
+        let mut out_file_path = if let Some(out_dir) = &self.out_dir {
+            out_dir.to_owned()
+        } else {
+            PathBuf::from(env::var("OUT_DIR").unwrap())
+        };
+        // NB: If out_file is absolute, it replaces the current path.
+        out_file_path.push(out_file);
+
+        let in_file_path: &Path = self
+            .in_file_path
+            .as_ref()
+            .expect("'in_file_path' is not set");
+        println!("cargo:rerun-if-changed={}", in_file_path.display());
+
+        let mut cmd;
+        if let Some(python) = &self.python {
+            cmd = Command::new(python);
+            cmd.arg(regtool);
+        } else {
+            cmd = Command::new(regtool);
+        }
+        cmd.arg("-R").arg("-o").arg(out_file_path).arg(in_file_path);
+        println!("Running: {:?}", cmd);
+        assert!(cmd.status().unwrap().success());
+    }
+}
+
+impl Default for Build {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/util/tlgen/xbar.sim_cfg.hjson.tpl b/util/tlgen/xbar.sim_cfg.hjson.tpl
index 681945f..627c05b 100644
--- a/util/tlgen/xbar.sim_cfg.hjson.tpl
+++ b/util/tlgen/xbar.sim_cfg.hjson.tpl
@@ -16,13 +16,13 @@
   testplan: "{proj_root}/hw/ip/tlul/data/tlul_testplan.hjson"
 
   // Add xbar_main specific exclusion files.
-  vcs_cov_excl_files: ["{proj_root}/hw/top_earlgrey/ip/{dut}/dv/autogen/xbar_cov_excl.el"]
+  vcs_cov_excl_files: ["{proj_root}/${xbar.ip_path}/dv/autogen/xbar_cov_excl.el"]
 
   // replace common cover.cfg with a generated one, which includes xbar toggle exclusions
   overrides: [
     {
       name: default_vcs_cov_cfg_file
-      value: "-cm_hier {proj_root}/hw/top_earlgrey/ip/{dut}/dv/autogen/xbar_cover.cfg"
+      value: "-cm_hier {proj_root}/${xbar.ip_path}/dv/autogen/xbar_cover.cfg"
     }
   ]
   // Import additional common sim cfg files.
diff --git a/util/topgen/BUILD b/util/topgen/BUILD
index fa87f82..cc73719 100644
--- a/util/topgen/BUILD
+++ b/util/topgen/BUILD
@@ -11,7 +11,9 @@
     name = "topgen",
     srcs = [
         "__init__.py",
+        "entropy_buffer_generator.py",
         "gen_top_docs.py",
+        "strong_random.py",
         "validate.py",
     ],
     deps = [
diff --git a/util/topgen/templates/BUILD.tpl b/util/topgen/templates/BUILD.tpl
index 858d019..e659f12 100644
--- a/util/topgen/templates/BUILD.tpl
+++ b/util/topgen/templates/BUILD.tpl
@@ -51,3 +51,22 @@
         "//sw/device/lib/testing/test_framework:ottf_main",
     ],
 )
+
+opentitan_functest(
+    name = "alert_renode_test",
+    srcs = ["alert_test.c"],
+    deps = [
+        "//hw/top_earlgrey/sw/autogen:top_earlgrey",
+        "//sw/device/lib/base:memory",
+        "//sw/device/lib/base:mmio",
+% for n in sorted(alert_peripheral_names + ["alert_handler"]):
+        "//sw/device/lib/dif:${n}",
+% endfor
+        "//sw/device/lib/runtime:log",
+        "//sw/device/lib/testing:alert_handler_testutils",
+        "//sw/device/lib/testing/test_framework:ottf_main",
+    ],
+    copts = [
+        "-DDISABLE_RENODE_TEST",
+    ],
+)
diff --git a/util/topgen/templates/alert_test.c.tpl b/util/topgen/templates/alert_test.c.tpl
index 98a1c16..c098eef 100644
--- a/util/topgen/templates/alert_test.c.tpl
+++ b/util/topgen/templates/alert_test.c.tpl
@@ -5,6 +5,12 @@
 
 ${gencmd}
 <%
+## list renode unsupported peripherals
+unsupported_peripherals = [
+  "adc_ctrl", "clkmgr", "edn", "entropy_src", "keymgr", "pattgen",
+  "pinmux", "pwm", "rv_plic", "sensor_ctrl", "spi_device", "sram_ctrl",
+  "sysrst_ctrl", "usbdev"
+]
 alert_peripheral_names = sorted({p.name for p in helper.alert_peripherals})
 %>\
 #include "sw/device/lib/base/mmio.h"
@@ -98,6 +104,9 @@
   dif_alert_handler_alert_t exp_alert;
   % for p in helper.alert_peripherals:
 
+  % if p.name in unsupported_peripherals:
+#ifndef DISABLE_RENODE_TEST
+  % endif
   // Write ${p.name}'s alert_test reg and check alert_cause.
   for (int i = 0; i < ${p.num_alerts}; ++i) {
     CHECK_DIF_OK(dif_${p.name}_alert_force(&${p.inst_name}, ${p.dif_alert_name} + i));
@@ -112,6 +121,9 @@
     CHECK_DIF_OK(dif_alert_handler_alert_acknowledge(
         &alert_handler, exp_alert));
   }
+  % if p.name in unsupported_peripherals:
+#endif
+  % endif
   % endfor
 }
 
diff --git a/util/topgen/templates/xbar_env_pkg__params.sv.tpl b/util/topgen/templates/xbar_env_pkg__params.sv.tpl
index 996511f..805b06a 100644
--- a/util/topgen/templates/xbar_env_pkg__params.sv.tpl
+++ b/util/topgen/templates/xbar_env_pkg__params.sv.tpl
@@ -21,7 +21,7 @@
         for host, devices in xbar["connections"].items():
           for dev_name in devices:
             if is_device_a_xbar(dev_name):
-              edge_devices.extend(get_xbar_edge_nodes())
+              edge_devices.extend(get_xbar_edge_nodes(dev_name))
             else:
               edge_devices.append(dev_name)