test_rom: initial bancha support

Changes in support of booting CHERIoT firmware images on a bancha system,
This has been tested only with OTP_IS_RAM and requires the 2nd-level
firmware image be loaded from SPI into RAM.

NB: 2nd-level firmware images currently lack the expected manifest

Specific changes:
- many files forked to isolate / simplify adding CHERI support
- memory layout is somewhat different per CHERIoT requirements (e.g.
  r/w data is collected in one ELF section for CGP-relative addressing)
- remove PMP usage because CHERIoT does not support it and it's use
  can be done with CHERI caps
- static_critical section size differs from non-CHERI
- eflash, flash_ctrl, spi_flash, and bootstrap api's take capabilities
  for the MMIO regions (using mmio_region_t) instead of crafting pointers
  from raw addresses
- tag the build target with "cheri" so we can filter it out for CI

Bypass-Presubmit-Reason: verified as part of topic

Change-Id: Iecbb7f9eaedc52a8988e7da9015d89e058f9d844
diff --git a/hw/top_matcha/sw/autogen/BUILD b/hw/top_matcha/sw/autogen/BUILD
index cc139ca..29229ae 100644
--- a/hw/top_matcha/sw/autogen/BUILD
+++ b/hw/top_matcha/sw/autogen/BUILD
@@ -22,6 +22,11 @@
     includes = ["top_matcha_memory.ld"],
 )
 
+ld_library(
+    name = "top_matcha_memory_cheri",
+    includes = ["top_matcha_memory_cheri.ld"],
+)
+
 filegroup(
     name = "all_files",
     srcs = glob(["**"]),
diff --git a/hw/top_matcha/sw/autogen/top_matcha_memory_cheri.ld b/hw/top_matcha/sw/autogen/top_matcha_memory_cheri.ld
new file mode 100644
index 0000000..d2fbb4b
--- /dev/null
+++ b/hw/top_matcha/sw/autogen/top_matcha_memory_cheri.ld
@@ -0,0 +1,41 @@
+/* Copyright lowRISC contributors. */
+/* Licensed under the Apache License, Version 2.0, see LICENSE for details. */
+/* SPDX-License-Identifier: Apache-2.0 */
+
+/**
+ * Partial linker script for chip memory configuration with CHERIoT.
+ * NB: ROM is super-sized (for now) to deal with bloat
+ * NB: 2nd-level boot goes to RAM because eFLASH is untagged
+ * NB: RAM is partitioned into:
+ *   ram_rom - memory used by the boot rom (e.g. data, bss, stack)
+ *   ram_main - loaded 2nd-level firmware
+ */
+MEMORY {
+  ram_ret_aon(rwx) : ORIGIN = 0x40600000, LENGTH = 0x1000
+  eflash(rx) : ORIGIN = 0x20000000, LENGTH = 0x100000
+  ram_main(rwx) : ORIGIN = 0x10000000, LENGTH = 0x300000
+  ram_rom(rw) : ORIGIN = 0x10300000, LENGTH = 0x100000
+  rom(rx) : ORIGIN = 0x00008000, LENGTH = 0xb000
+  ram_ml_dmem(rwx) : ORIGIN = 0x5A000000, LENGTH = 0x400000
+}
+
+/**
+ * Stack at the top of ram_rom.
+ */
+_stack_size = 16384;
+_stack_end = ORIGIN(ram_rom) + LENGTH(ram_rom);
+_stack_start = _stack_end - _stack_size;
+
+/**
+ * Size of the `.static_critical` section at the bottom of the main SRAM (in
+ * bytes).
+ * NB: non-CHERIoT is 8132
+ */
+_static_critical_size = 8312;
+
+/**
+ * `.chip_info` at the top of ROM.
+ */
+_chip_info_size = 128;
+_chip_info_end   = ORIGIN(rom) + LENGTH(rom);
+_chip_info_start = _chip_info_end - _chip_info_size;
diff --git a/sw/device/examples/hello_world_multicore/hello_world_multicore_sc_loaders_extflash.c b/sw/device/examples/hello_world_multicore/hello_world_multicore_sc_loaders_extflash.c
index 50b2dd7..765e3f5 100644
--- a/sw/device/examples/hello_world_multicore/hello_world_multicore_sc_loaders_extflash.c
+++ b/sw/device/examples/hello_world_multicore/hello_world_multicore_sc_loaders_extflash.c
@@ -32,4 +32,12 @@
       (TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR + TOP_MATCHA_RAM_ML_DMEM_SIZE_BYTES)));
 }
 
-void load_init(void) { spi_flash_init(); }
+void load_init(void) {
+  const mmio_region_t spi_host_addr =
+      mmio_region_from_addr(TOP_MATCHA_SPI_HOST0_BASE_ADDR);
+  const mmio_region_t eflash_addr =
+      mmio_region_from_addr(TOP_MATCHA_FLASH_CTRL_CORE_BASE_ADDR);
+  const mmio_region_t otp_addr =
+      mmio_region_from_addr(TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR);
+  spi_flash_init(spi_host_addr, eflash_addr, otp_addr);
+}
diff --git a/sw/device/lib/eflash.c b/sw/device/lib/eflash.c
index 579616a..ff07bc4 100644
--- a/sw/device/lib/eflash.c
+++ b/sw/device/lib/eflash.c
@@ -25,10 +25,9 @@
 
 static dif_flash_ctrl_state_t flash_ctrl;
 
-dif_result_t eflash_init(void) {
-  CHECK_DIF_OK(dif_flash_ctrl_init_state(
-      &flash_ctrl,
-      mmio_region_from_addr(TOP_MATCHA_FLASH_CTRL_CORE_BASE_ADDR)));
+dif_result_t eflash_init(mmio_region_t base_addr, mmio_region_t otp_addr) {
+  CHECK_DIF_OK(dif_flash_ctrl_init_state(&flash_ctrl, base_addr));
+  flash_ctrl_init(base_addr, otp_addr);
   return kDifOk;
 }
 
diff --git a/sw/device/lib/eflash.h b/sw/device/lib/eflash.h
index 0161629..e4c6a2a 100644
--- a/sw/device/lib/eflash.h
+++ b/sw/device/lib/eflash.h
@@ -20,6 +20,7 @@
 #include <stdint.h>
 
 #include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/base/mmio.h"
 #include "sw/device/lib/dif/dif_base.h"
 
 #define EFLASH_PAGE_SIZE (256)
@@ -28,7 +29,8 @@
 extern "C" {
 #endif
 
-OT_WARN_UNUSED_RESULT dif_result_t eflash_init(void);
+OT_WARN_UNUSED_RESULT dif_result_t eflash_init(mmio_region_t base_addr,
+                                               mmio_region_t otp_addr);
 OT_WARN_UNUSED_RESULT dif_result_t eflash_chip_erase(void);
 OT_WARN_UNUSED_RESULT dif_result_t eflash_write_page(const void* dst,
                                                      uint8_t* src);
diff --git a/sw/device/lib/spi_flash.c b/sw/device/lib/spi_flash.c
index 26eae0a..f80c508 100644
--- a/sw/device/lib/spi_flash.c
+++ b/sw/device/lib/spi_flash.c
@@ -70,16 +70,16 @@
   return kDifOk;
 }
 
-dif_result_t spi_flash_init(void) {
-  CHECK_DIF_OK(dif_spi_host_init(
-      mmio_region_from_addr(TOP_MATCHA_SPI_HOST0_BASE_ADDR), &spi_host0));
+dif_result_t spi_flash_init(mmio_region_t spi_host_addr,
+                            mmio_region_t eflash_addr, mmio_region_t otp_addr) {
+  CHECK_DIF_OK(dif_spi_host_init(spi_host_addr, &spi_host0));
   dif_spi_host_config_t config = {
       .spi_clock = kClockFreqSpiFlashHz,
       .peripheral_clock_freq_hz = kClockFreqCpuHz,
   };
   CHECK_DIF_OK(dif_spi_host_configure(&spi_host0, config));
   CHECK_DIF_OK(dif_spi_host_output_set_enabled(&spi_host0, /*enabled=*/true));
-  CHECK_DIF_OK(eflash_init());
+  CHECK_DIF_OK(eflash_init(eflash_addr, otp_addr));
   return kDifOk;
 }
 
diff --git a/sw/device/lib/spi_flash.h b/sw/device/lib/spi_flash.h
index 90fd77d..e29c69f 100644
--- a/sw/device/lib/spi_flash.h
+++ b/sw/device/lib/spi_flash.h
@@ -20,6 +20,7 @@
 
 #include <stdint.h>
 
+#include "sw/device/lib/base/mmio.h"
 #include "sw/device/lib/dif/dif_base.h"
 
 #define SPI_PAGE_SIZE (512)
@@ -29,7 +30,8 @@
 #endif
 
 /* General SPI flash related methods */
-dif_result_t spi_flash_init(void);
+dif_result_t spi_flash_init(mmio_region_t spi_host_addr,
+                            mmio_region_t eflash_addr, mmio_region_t otp_addr);
 dif_result_t spi_flash_read_page(uint32_t page, uint8_t* buf);
 
 /* Tar filesystem related methods */
diff --git a/sw/device/lib/testing/test_rom/BUILD b/sw/device/lib/testing/test_rom/BUILD
index 83f3c27..4458399 100644
--- a/sw/device/lib/testing/test_rom/BUILD
+++ b/sw/device/lib/testing/test_rom/BUILD
@@ -42,6 +42,7 @@
     deps = [
         ":linker_script",
         ":test_rom_lib",
+        "@lowrisc_opentitan//sw/device/lib/crt",
         "//sw/device/silicon_creator/rom:bootstrap_no_otp",
     ],
 )
@@ -54,6 +55,7 @@
     ],
     deps = [
         ":test_rom_lib",
+        "@lowrisc_opentitan//sw/device/lib/crt",
         "//sw/device/silicon_creator/rom:bootstrap",
     ],
 )
@@ -82,7 +84,6 @@
         "@lowrisc_opentitan//sw/device/lib/base:abs_mmio",
         "@lowrisc_opentitan//sw/device/lib/base:bitfield",
         "@lowrisc_opentitan//sw/device/lib/base:mmio",
-        "@lowrisc_opentitan//sw/device/lib/crt",
         "@lowrisc_opentitan//sw/device/lib/dif:clkmgr",
         "@lowrisc_opentitan//sw/device/lib/dif:flash_ctrl",
         "@lowrisc_opentitan//sw/device/lib/dif:gpio",
@@ -163,3 +164,52 @@
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:flash_ctrl",
     ],
 )
+
+# Only build for CHERIoT targets. Requires that bazel be invoked with
+# --config=cheriot-baremetal --copt=-D_CHERIOT_BAREMETAL_=1
+# so that all dependencies use those options.
+opentitan_rom_binary(
+    name = "test_rom_no_otp_cheri",
+    visibility = ["//visibility:private"],
+    # NB: fpga_nexus is used for renode sims
+    per_device_deps = {
+        "fpga_nexus": [NEXUS_CORE_TARGETS.get("secure_core")],
+    },
+    srcs = [
+        "test_rom_cheri.c",
+        "test_rom_start_cheri.S",
+        # XXX move to opentitan/sw/device/lib/crt?
+        "crt_cheri.S",
+    ],
+    defines = ["OTP_IS_RAM"],
+    deps = [
+        ":linker_script_cheri",
+        ":baremetal_lib",
+        ":test_rom_lib",
+        "//sw/device/silicon_creator/rom:bootstrap_no_otp",
+    ],
+    tags = [
+        "cheri",
+    ],
+)
+
+cc_library(
+    name = "baremetal_lib",
+    hdrs = [
+        "cheriot-baremetal.h",
+    ],
+)
+
+ld_library(
+    name = "linker_script_cheri",
+    visibility = ["//visibility:private"],
+    script = "test_rom_cheri.ld",
+    deps = [
+        "//hw/top_matcha/sw/autogen:top_matcha_memory_cheri",
+        "@lowrisc_opentitan//sw/device:info_sections",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/base:static_critical_sections",
+    ],
+    tags = [
+        "cheri",
+    ],
+)
diff --git a/sw/device/lib/testing/test_rom/cheriot-baremetal.h b/sw/device/lib/testing/test_rom/cheriot-baremetal.h
new file mode 100644
index 0000000..38bed19
--- /dev/null
+++ b/sw/device/lib/testing/test_rom/cheriot-baremetal.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef SW_DEVICE_LIB_TESTING_TEST_ROM_CHERIOT_BAREMETAL_H_
+#define SW_DEVICE_LIB_TESTING_TEST_ROM_CHERIOT_BAREMETAL_H_
+
+// XXX just make this a noop?
+#ifndef _CHERIOT_BAREMETAL_
+#error "This file is only useful when compiling for baremetal"
+#endif
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define CHERI_PERM_GLOBAL (1U << 0)
+#define CHERI_PERM_LOAD_GLOBAL (1U << 1)
+#define CHERI_PERM_STORE (1U << 2)
+#define CHERI_PERM_LOAD_MUTABLE (1U << 3)
+#define CHERI_PERM_STORE_LOCAL (1U << 4)
+#define CHERI_PERM_LOAD (1U << 5)
+#define CHERI_PERM_LOAD_STORE_CAP (1U << 6)
+#define CHERI_PERM_ACCESS_SYS (1U << 7)
+#define CHERI_PERM_EXECUTE (1U << 8)
+#define CHERI_PERM_UNSEAL (1U << 9)
+#define CHERI_PERM_SEAL (1U << 10)
+#define CHERI_PERM_USER0 (1U << 11)
+
+#define cgetlen(foo) __builtin_cheri_length_get(foo)
+#define cgetperms(foo) __builtin_cheri_perms_get(foo)
+#define cgettype(foo) __builtin_cheri_type_get(foo)
+#define cgettag(foo) __builtin_cheri_tag_get(foo)
+#define cgetoffset(foo) __builtin_cheri_offset_get(foo)
+#define csetoffset(a, b) __builtin_cheri_offset_set((a), (b))
+#define cincoffset(a, b) __builtin_cheri_offset_increment((a), (b))
+#define cgetaddr(a) __builtin_cheri_address_get(a)
+#define csetaddr(a, b) __builtin_cheri_address_set((a), (b))
+#define cgetbase(foo) __builtin_cheri_base_get(foo)
+#define candperms(a, b) __builtin_cheri_perms_and((a), (b))
+#define cseal(a, b) __builtin_cheri_seal((a), (b))
+#define cunseal(a, b) __builtin_cheri_unseal((a), (b))
+#define csetbounds(a, b) __builtin_cheri_bounds_set((a), (b))
+#define csetboundsext(a, b) __builtin_cheri_bounds_set_exact((a), (b))
+#define ccheckperms(a, b) __builtin_cheri_perms_check((a), (b))
+#define cchecktype(a, b) __builtin_cheri_type_check((a), (b))
+#define cbuildcap(a, b) __builtin_cheri_cap_build((a), (b))
+#define ccopytype(a, b) __builtin_cheri_cap_type_copy((a), (b))
+#define ccseal(a, b) __builtin_cheri_conditional_seal((a), (b))
+#define cequalexact(a, b) __builtin_cheri_equal_exact((a), (b))
+
+// Derives a capability from the root cap for doing MMIO to a
+// device at a fixed address.
+static inline uintptr_t cderivecap(const uintptr_t root_cap,
+                                   const uint32_t paddr,
+                                   const size_t size_bytes,
+                                   const uint32_t perms) {
+  return candperms(
+      csetbounds(
+          csetaddr(root_cap, paddr),
+          size_bytes),
+      perms);
+}
+
+// Constructs a capability for invoking a function at the specified
+// address using mtcc. This is intended only for passing control to
+// the next stage firmware.
+static inline uintptr_t cderivepcc(const uintptr_t paddr) {
+  volatile uintptr_t ret;
+  uint32_t paddr_temp;
+  __asm(
+      "cgetaddr %1, %2\n"
+      "cspecialr %0, mtcc\n"
+      "csetaddr %0, %0, %1\n"
+      : "=C"(ret), "=&r"(paddr_temp)
+      : "C"(paddr));
+  return ret;
+}
+
+#endif  // SW_DEVICE_LIB_TESTING_TEST_ROM_CHERIOT_BAREMETAL_H_
diff --git a/sw/device/lib/testing/test_rom/crt_cheri.S b/sw/device/lib/testing/test_rom/crt_cheri.S
new file mode 100644
index 0000000..fe908f9
--- /dev/null
+++ b/sw/device/lib/testing/test_rom/crt_cheri.S
@@ -0,0 +1,143 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * CHERI CRT library
+ *
+ * Utility functions written in assembly that can be used before the C
+ * runtime has been initialized. These functions should not be used once
+ * the C runtime has been initialized.
+ *
+ * The name of this library is historical. In many toolchains, a file called
+ * "crt0.o" is linked into each executable, which does something similar to
+ * what these functions 
+ */
+
+  // NOTE: The "ax" flag below is necessary to ensure that this section
+  // is allocated space in ROM by the linker.
+  .section .crt, "ax", @progbits
+
+  /**
+   * Write zeros into the section bounded by the start and end pointers.
+   * The section must be word (4 byte) aligned. It is valid for the section
+   * to have a length of 0 (i.e. the start and end pointers may be equal).
+   *
+   * This function follows the standard ILP32 calling convention for arguments
+   * but does not require a valid stack pointer, thread pointer or global
+   * pointer.
+   *
+   * Clobbers a0 and t0.
+   *
+   * @param a0 pointer to start of section to clear (inclusive).
+   * @param a1 pointer to end of section to clear (exclusive).
+   */
+  .balign 4
+  .global crt_section_clear
+  .type crt_section_clear, @function
+crt_section_clear:
+
+  // Check that start is before end.
+  bgeu a0, a1, .L_clear_nothing
+
+  // Check that start and end are word aligned.
+  or   t0, a0, a1
+  andi t0, t0, 0x3
+  bnez t0, .L_clear_error
+
+.L_clear_loop:
+  // Write zero into section memory word-by-word.
+  // TODO: unroll and/or uses csc
+  csw   zero, 0(ca0)
+//  addi a0, a0, 4
+  cincoffsetimm ca0, ca0, 4
+  bltu a0, a1, .L_clear_loop
+  cret
+
+.L_clear_nothing:
+  // If section length is 0 just return. Otherwise end is before start
+  // which is invalid so trigger an error.
+  bne a0, a1, .L_clear_error
+  cret
+
+.L_clear_error:
+  unimp
+
+  // Set function size to allow disassembly.
+  .size crt_section_clear, .-crt_section_clear
+
+// -----------------------------------------------------------------------------
+
+  /**
+   * Copy data from the given source into the section bounded by the start and
+   * end pointers. Both the section and the source must be word (4 byte) aligned.
+   * It is valid for the section to have a length of 0 (i.e. the start and end
+   * pointers may be equal). The source is assumed to have the same length as the
+   * target section.
+   *
+   * The destination section and the source must not overlap.
+   *
+   * This function follows the standard ILP32 calling convention for arguments
+   * but does not require a valid stack pointer, thread pointer or global
+   * pointer.
+   *
+   * Clobbers a0, a2, t0 and t1.
+   *
+   * @param a0 pointer to start of destination section (inclusive).
+   * @param a1 pointer to end of destination section (exclusive).
+   * @param a2 pointer to source data (inclusive).
+   */
+  .balign 4
+  .global crt_section_copy
+  .type crt_section_copy, @function
+crt_section_copy:
+
+  // Check that start is before end.
+  bgeu a0, a1, .L_copy_nothing
+
+  // Check that start, end and src are word aligned.
+  or   t0, a0, a1
+  or   t0, t0, a2
+  andi t0, t0, 0x3
+  bnez t0, .L_copy_error
+
+  // Check that source does not destructively overlap destination
+  // (assuming a forwards copy).
+  //
+  // src  start          end
+  //  |     |             |
+  //  +-------------+     |
+  //  |   source    |     |
+  //  +-------------+     |
+  //        +-------------+
+  //        | destination |
+  //        +-------------+
+  //        |             |
+  //      start          end
+  //
+  // TODO: disallow all overlap since it indicates API misuse?
+  sub  t0, a0, a2           // (start - src) mod 2**32
+  sub  t1, a1, a0           // end - start
+  bltu t0, t1, .L_copy_error
+
+.L_copy_loop:
+  // Copy data from src into section word-by-word.
+  // TODO: unroll
+  clw   t0, 0(ca2)
+//  addi a2, a2, 4
+  cincoffsetimm ca2, ca2, 4
+  csw   t0, 0(ca0)
+//  addi a0, a0, 4
+  cincoffsetimm ca0, ca0, 4
+  bltu a0, a1, .L_copy_loop
+  cret
+
+.L_copy_nothing:
+  // If section length is 0 just return. Otherwise end is before start
+  // which is invalid so trigger an error.
+  bne a0, a1, .L_copy_error
+  cret
+
+.L_copy_error:
+  unimp
+  .size crt_section_copy, .-crt_section_copy
diff --git a/sw/device/lib/testing/test_rom/test_rom.c b/sw/device/lib/testing/test_rom/test_rom.c
index 9c262ba..d95b6a5 100644
--- a/sw/device/lib/testing/test_rom/test_rom.c
+++ b/sw/device/lib/testing/test_rom/test_rom.c
@@ -162,7 +162,8 @@
   CHECK_DIF_OK(dif_rstmgr_reset_info_get(&rstmgr, &reset_reasons));
 
   // Store the reset reason in retention RAM and clear the register.
-  volatile retention_sram_t *ret_ram = retention_sram_get();
+  volatile retention_sram_t *ret_ram =
+      (volatile retention_sram_t *)TOP_MATCHA_RAM_RET_AON_BASE_ADDR;
   ret_ram->reset_reasons = reset_reasons;
   CHECK_DIF_OK(dif_rstmgr_reset_info_clear(&rstmgr));
 
@@ -216,12 +217,18 @@
     CHECK_DIF_OK(dif_flash_ctrl_set_default_region_properties(
         &flash_ctrl, default_properties));
   }
-  if (bootstrap_requested() == kHardenedBoolTrue) {
+  const mmio_region_t otp_addr =
+      mmio_region_from_addr(TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR);
+  const mmio_region_t gpio_addr =
+      mmio_region_from_addr(TOP_MATCHA_GPIO_BASE_ADDR);
+  if (bootstrap_requested(otp_addr, gpio_addr) == kHardenedBoolTrue) {
     // This log statement is used to synchronize the rom and DV testbench
     // for specific test cases.
     LOG_INFO("Boot strap requested");
 
-    rom_error_t bootstrap_err = bootstrap();
+    const mmio_region_t spi_device_addr =
+        mmio_region_from_addr(TOP_MATCHA_SPI_DEVICE_BASE_ADDR);
+    rom_error_t bootstrap_err = bootstrap(otp_addr, gpio_addr, spi_device_addr);
     if (bootstrap_err != kErrorOk) {
       LOG_ERROR("Bootstrap failed with status code: %08x",
                 (uint32_t)bootstrap_err);
@@ -240,7 +247,11 @@
       entry_point >
           TOP_MATCHA_EFLASH_BASE_ADDR + TOP_MATCHA_EFLASH_SIZE_BYTES) {
     LOG_INFO("Attempting to load from external flash...");
-    spi_flash_init();
+    const mmio_region_t spi_host_addr =
+        mmio_region_from_addr(TOP_MATCHA_SPI_HOST0_BASE_ADDR);
+    const mmio_region_t eflash_addr =
+        mmio_region_from_addr(TOP_MATCHA_FLASH_CTRL_CORE_BASE_ADDR);
+    spi_flash_init(spi_host_addr, eflash_addr, otp_addr);
     dif_result_t load_result = load_file_from_tar(
         "matcha-tock-bundle.bin", (void *)TOP_MATCHA_EFLASH_BASE_ADDR,
         TOP_MATCHA_EFLASH_BASE_ADDR + TOP_MATCHA_EFLASH_SIZE_BYTES);
diff --git a/sw/device/lib/testing/test_rom/test_rom_cheri.c b/sw/device/lib/testing/test_rom/test_rom_cheri.c
new file mode 100644
index 0000000..fe7e7bc
--- /dev/null
+++ b/sw/device/lib/testing/test_rom/test_rom_cheri.c
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2023 Google LLC
+ * Copyright lowRISC contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "cheriot-baremetal.h"
+
+#include "flash_ctrl_regs.h"
+#include "hw/top_matcha/sw/autogen/top_matcha.h"  // Generated.
+#include "otp_ctrl_regs.h"
+#include "sw/device/lib/arch/device.h"
+#include "sw/device/lib/base/abs_mmio.h"
+#include "sw/device/lib/base/bitfield.h"
+#include "sw/device/lib/base/csr.h"
+#include "sw/device/lib/base/mmio.h"
+#include "sw/device/lib/dif/dif_base.h"
+#include "sw/device/lib/dif/dif_clkmgr.h"
+#include "sw/device/lib/dif/dif_flash_ctrl.h"
+#include "sw/device/lib/dif/dif_gpio.h"
+#include "sw/device/lib/dif/dif_pinmux.h"
+#include "sw/device/lib/dif/dif_rstmgr.h"
+#include "sw/device/lib/dif/dif_rv_core_ibex.h"
+#include "sw/device/lib/dif/dif_uart.h"
+#include "sw/device/lib/runtime/hart.h"
+#include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/runtime/print.h"
+#include "sw/device/lib/spi_flash.h"
+#include "sw/device/lib/testing/flash_ctrl_testutils.h"
+#include "sw/device/lib/testing/pinmux_testutils.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/test_framework/status.h"
+#include "sw/device/lib/testing/test_rom/chip_info.h"  // Generated.
+#include "sw/device/silicon_creator/lib/base/sec_mmio.h"
+#include "sw/device/silicon_creator/lib/drivers/flash_ctrl.h"
+#include "sw/device/silicon_creator/lib/drivers/retention_sram.h"
+#include "sw/device/silicon_creator/lib/manifest.h"
+#include "sw/device/silicon_creator/rom/bootstrap.h"
+
+// 2nd-level firmware filename loaded from SPI
+#define CHERIOT_FIRMWARE "cheriot-firmware.bin"
+
+// RAM configuration where 2nd-level firmware is loaded
+// TODO(sleffler): needs to come from SOT
+#define RAM_START 0x10000000
+#define RAM_SIZE_BYTES 0x300000
+
+/**
+ * Type alias for the OTTF entry point.
+ *
+ * The entry point address obtained from the OTTF manifest must be cast to a
+ * pointer to this type before being called.
+ */
+typedef void ottf_entry_point(void);
+
+static dif_clkmgr_t clkmgr;
+static dif_flash_ctrl_state_t flash_ctrl;
+static dif_pinmux_t pinmux;
+static dif_rstmgr_t rstmgr;
+static dif_uart_t uart0;
+static dif_rv_core_ibex_t ibex;
+
+// Helper to craft an mmio_region_t for fixed address.
+static inline mmio_region_t get_mmio_region(const uintptr_t root_cap,
+                                            const uint32_t base_addr,
+                                            const uint32_t size_bytes) {
+  return mmio_region_from_addr(cderivecap(root_cap, base_addr, size_bytes,
+                                          CHERI_PERM_LOAD | CHERI_PERM_STORE));
+}
+
+// Helper to read a 32-bit value from a fixed address.
+static inline uint32_t mmio_read32(const uintptr_t root_cap,
+                                   const uint32_t base_addr) {
+  return abs_mmio_read32(
+      cderivecap(root_cap, base_addr, sizeof(uint32_t), CHERI_PERM_LOAD));
+}
+
+// `test_in_rom = True` tests can override this symbol to provide their own
+// rom tests. By default, it simply jumps into the OTTF's flash.
+OT_WEAK
+bool rom_test_main(const uintptr_t root_cap) {
+  // Check the otp to see if execute should start
+  // val: 0xFFFFFFFF
+#if defined(OTP_IS_RAM)
+  uint32_t otp_val = 0xffffffff;
+#else
+  uint32_t otp_val = mmio_read32(
+      root_cap, TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR +
+                    OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET +
+                    OTP_CTRL_PARAM_CREATOR_SW_CFG_ROM_EXEC_EN_OFFSET);
+#endif
+
+  if (otp_val == 0) {
+    test_status_set(kTestStatusInBootRomHalt);
+    // Abort simply forever loops on a wait_for_interrupt;
+    abort();
+  }
+
+  // Initialize Ibex cpuctrl (contains icache / security feature enablements).
+  uint32_t cpuctrl_csr;
+  CSR_READ(CSR_REG_CPUCTRL, &cpuctrl_csr);
+  // val: 0x1
+#if defined(OTP_IS_RAM)
+  uint32_t cpuctrl_otp_val = 0x1;
+#else
+  uint32_t cpuctrl_otp_val =
+      mmio_read32(root_cap, TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR +
+                                OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET +
+                                OTP_CTRL_PARAM_CREATOR_SW_CFG_CPUCTRL_OFFSET);
+#endif
+  cpuctrl_csr = bitfield_field32_write(
+      cpuctrl_csr, (bitfield_field32_t){.mask = 0x3f, .index = 0},
+      cpuctrl_otp_val);
+  CSR_WRITE(CSR_REG_CPUCTRL, cpuctrl_csr);
+
+  // Initial sec_mmio, required by bootstrap and its dependencies.
+  sec_mmio_init();
+
+  // Configure the pinmux.
+  CHECK_DIF_OK(
+      dif_pinmux_init(get_mmio_region(root_cap, TOP_MATCHA_PINMUX_AON_BASE_ADDR,
+                                      TOP_MATCHA_PINMUX_AON_SIZE_BYTES),
+                      &pinmux));
+  pinmux_testutils_init(&pinmux);
+
+  CHECK_DIF_OK(
+      dif_rstmgr_init(get_mmio_region(root_cap, TOP_MATCHA_RSTMGR_AON_BASE_ADDR,
+                                      TOP_MATCHA_RSTMGR_AON_SIZE_BYTES),
+                      &rstmgr));
+
+  // Initialize the flash.
+  CHECK_DIF_OK(dif_flash_ctrl_init_state(
+      &flash_ctrl,
+      get_mmio_region(root_cap, TOP_MATCHA_FLASH_CTRL_CORE_BASE_ADDR,
+                      TOP_MATCHA_FLASH_CTRL_CORE_SIZE_BYTES)));
+  CHECK_DIF_OK(dif_flash_ctrl_start_controller_init(&flash_ctrl));
+  flash_ctrl_testutils_wait_for_init(&flash_ctrl);
+  CHECK_DIF_OK(
+      dif_flash_ctrl_set_flash_enablement(&flash_ctrl, kDifToggleEnabled));
+
+  // Setup the UART for printing messages to the console.
+  if (kDeviceType != kDeviceSimDV) {
+    CHECK_DIF_OK(
+        dif_uart_init(get_mmio_region(root_cap, TOP_MATCHA_UART0_BASE_ADDR,
+                                      TOP_MATCHA_UART0_SIZE_BYTES),
+                      &uart0));
+    CHECK_DIF_OK(
+        dif_uart_configure(&uart0, (dif_uart_config_t){
+                                       .baudrate = kUartBaudrate,
+                                       .clk_freq_hz = kClockFreqPeripheralHz,
+                                       .parity_enable = kDifToggleDisabled,
+                                       .parity = kDifUartParityEven,
+                                       .tx_enable = kDifToggleEnabled,
+                                       .rx_enable = kDifToggleEnabled,
+                                   }));
+    base_uart_stdout(&uart0);
+  }
+
+  // Print the chip version information
+  LOG_INFO("%s", chip_info);
+
+  // Skip sram_init for test_rom
+  dif_rstmgr_reset_info_bitfield_t reset_reasons;
+  CHECK_DIF_OK(dif_rstmgr_reset_info_get(&rstmgr, &reset_reasons));
+
+  // Store the reset reason in retention RAM and clear the register.
+  volatile retention_sram_t *ret_ram = (volatile retention_sram_t *)cderivecap(
+      root_cap, TOP_MATCHA_RAM_RET_AON_BASE_ADDR,
+      TOP_MATCHA_RAM_RET_AON_SIZE_BYTES, CHERI_PERM_STORE);
+  ret_ram->reset_reasons = reset_reasons;
+  CHECK_DIF_OK(dif_rstmgr_reset_info_clear(&rstmgr));
+
+  // Write 0x54534554 (ASCII: TEST) to the end of the retention SRAM creator
+  // area to be able to determine the type of ROM in tests.
+  volatile uint32_t *creator_last_word =
+      &ret_ram->reserved_creator[ARRAYSIZE(ret_ram->reserved_creator) - 1];
+  *creator_last_word = TEST_ROM_IDENTIFIER;
+
+  // Print the FPGA version-id.
+  // This is guaranteed to be zero on all non-FPGA implementations.
+  dif_rv_core_ibex_fpga_info_t fpga;
+  CHECK_DIF_OK(dif_rv_core_ibex_init(
+      get_mmio_region(root_cap, TOP_MATCHA_RV_CORE_IBEX_SEC_CFG_BASE_ADDR,
+                      TOP_MATCHA_RV_CORE_IBEX_SEC_CFG_SIZE_BYTES),
+      &ibex));
+  CHECK_DIF_OK(dif_rv_core_ibex_read_fpga_info(&ibex, &fpga));
+  if (fpga != 0) {
+    LOG_INFO("TestROM:%08x", fpga);
+  }
+
+  // Enable clock jitter if requested.
+  // The kJitterEnabled symbol defaults to false across all hardware platforms.
+  // However, in DV simulation, it may be overridden via a backdoor write with
+  // the plusarg: `+en_jitter=1`.
+  if (kJitterEnabled) {
+    CHECK_DIF_OK(dif_clkmgr_init(
+        get_mmio_region(root_cap, TOP_MATCHA_CLKMGR_AON_BASE_ADDR,
+                        TOP_MATCHA_CLKMGR_AON_SIZE_BYTES),
+        &clkmgr));
+    CHECK_DIF_OK(dif_clkmgr_jitter_set_enabled(&clkmgr, kDifToggleEnabled));
+    LOG_INFO("Jitter is enabled");
+  }
+
+  // Check the otp to see if flash scramble should be enabled.
+  // val: 0x0
+#if defined(OTP_IS_RAM)
+  otp_val = 0;
+#else
+  otp_val = mmio_read32(
+      root_cap,
+      TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET +
+          OTP_CTRL_PARAM_CREATOR_SW_CFG_FLASH_DATA_DEFAULT_CFG_OFFSET);
+#endif
+
+  if (otp_val != 0) {
+    dif_flash_ctrl_region_properties_t default_properties;
+    CHECK_DIF_OK(dif_flash_ctrl_get_default_region_properties(
+        &flash_ctrl, &default_properties));
+    default_properties.scramble_en =
+        bitfield_field32_read(otp_val, FLASH_CTRL_OTP_FIELD_SCRAMBLING);
+    default_properties.ecc_en =
+        bitfield_field32_read(otp_val, FLASH_CTRL_OTP_FIELD_ECC);
+    default_properties.high_endurance_en =
+        bitfield_field32_read(otp_val, FLASH_CTRL_OTP_FIELD_HE);
+    CHECK_DIF_OK(dif_flash_ctrl_set_default_region_properties(
+        &flash_ctrl, default_properties));
+  }
+  // TODO(sleffler): cannot run from eFLASH so this is pointless
+  const mmio_region_t otp_addr =
+      get_mmio_region(root_cap, TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR,
+                      TOP_MATCHA_OTP_CTRL_CORE_SIZE_BYTES);
+  const mmio_region_t gpio_addr = get_mmio_region(
+      root_cap, TOP_MATCHA_GPIO_BASE_ADDR, TOP_MATCHA_GPIO_SIZE_BYTES);
+  if (bootstrap_requested(otp_addr, gpio_addr) == kHardenedBoolTrue) {
+    // This log statement is used to synchronize the rom and DV testbench
+    // for specific test cases.
+    LOG_INFO("Boot strap requested");
+
+    const mmio_region_t spi_device_addr =
+        get_mmio_region(root_cap, TOP_MATCHA_SPI_DEVICE_BASE_ADDR,
+                        TOP_MATCHA_SPI_DEVICE_BASE_ADDR);
+    rom_error_t bootstrap_err = bootstrap(otp_addr, gpio_addr, spi_device_addr);
+    if (bootstrap_err != kErrorOk) {
+      LOG_ERROR("Bootstrap failed with status code: %08x",
+                (uint32_t)bootstrap_err);
+      // Currently the only way to recover is by a hard reset.
+      test_status_set(kTestStatusFailed);
+    }
+  }
+  CHECK_DIF_OK(
+      dif_flash_ctrl_set_exec_enablement(&flash_ctrl, kDifToggleEnabled));
+
+  // Always select slot A (NB: there is no hw address translation so ignore the
+  // manifest).
+  const manifest_t *manifest =
+      (const manifest_t *)cderivecap(root_cap, TOP_MATCHA_EFLASH_BASE_ADDR,
+                                     sizeof(manifest_t), CHERI_PERM_LOAD);
+  uintptr_t entry_point = manifest_entry_point_get(manifest);
+
+  if (entry_point < TOP_MATCHA_EFLASH_BASE_ADDR + sizeof(manifest_t) ||
+      entry_point >
+          TOP_MATCHA_EFLASH_BASE_ADDR + TOP_MATCHA_EFLASH_SIZE_BYTES) {
+    LOG_INFO("Attempting to load from external flash...");
+    const mmio_region_t spi_host_addr =
+        get_mmio_region(root_cap, TOP_MATCHA_SPI_HOST0_BASE_ADDR,
+                        TOP_MATCHA_SPI_HOST0_SIZE_BYTES);
+    const mmio_region_t eflash_addr =
+        get_mmio_region(root_cap, TOP_MATCHA_FLASH_CTRL_CORE_BASE_ADDR,
+                        TOP_MATCHA_FLASH_CTRL_CORE_SIZE_BYTES);
+    spi_flash_init(spi_host_addr, eflash_addr, otp_addr);
+    // XXX max_mem_addr should not include the stack
+#if 0
+    // NB: cannot run from eFLASH because global caps cannot be represented
+    //   without tagged memory; this is left here in case something changes.
+    dif_result_t load_result = load_file_from_tar(
+        CHERIOT_FIRMWARE,
+        (void*) cderivecap(root_cap,
+            TOP_MATCHA_EFLASH_BASE_ADDR, TOP_MATCHA_EFLASH_SIZE_BYTES,
+            CHERI_PERM_LOAD | CHERI_PERM_STORE),
+        TOP_MATCHA_EFLASH_BASE_ADDR + TOP_MATCHA_EFLASH_SIZE_BYTES);
+    if (load_result == kDifOk) {
+      entry_point = manifest_entry_point_get(manifest);
+#else
+    // Load from SPI to RAM.
+    dif_result_t load_result = load_file_from_tar(
+        CHERIOT_FIRMWARE,
+        // TODO(sleffler): get memory configuration from SOT
+        (void *)cderivecap(root_cap, RAM_START, RAM_SIZE_BYTES,
+                           CHERI_PERM_LOAD | CHERI_PERM_STORE),
+        RAM_START + RAM_SIZE_BYTES);
+    if (load_result == kDifOk) {
+      // TODO(sleffler): get entry point from manifest
+      entry_point = 0x10000000;
+#endif
+    } else {
+      LOG_FATAL("Failed to load program from SPI flash!");
+      abort();
+    }
+  }
+
+  // Jump to the OTTF in flash. Within the flash binary, it is the
+  // responsibily of the OTTF to set up its own stack, and to never return.
+  // TODO(sleffler): sanitize environment before jump
+  LOG_INFO("Test ROM complete, jumping to flash (addr: %x)!", entry_point);
+  ((ottf_entry_point *)cderivepcc(entry_point))();
+
+  // If the flash image returns, we should abort anyway.
+  abort();
+}
+
+void _boot_start(const uintptr_t root_cap) {
+  test_status_set(kTestStatusInBootRom);
+  test_status_set(rom_test_main(root_cap) ? kTestStatusPassed
+                                          : kTestStatusFailed);
+
+  abort();
+}
diff --git a/sw/device/lib/testing/test_rom/test_rom_cheri.ld b/sw/device/lib/testing/test_rom/test_rom_cheri.ld
new file mode 100644
index 0000000..f55e26a
--- /dev/null
+++ b/sw/device/lib/testing/test_rom/test_rom_cheri.ld
@@ -0,0 +1,140 @@
+/* Copyright 2023 Google LLC. */
+/* Copyright lowRISC contributors. */
+/* Licensed under the Apache License, Version 2.0, see LICENSE for details. */
+/* SPDX-License-Identifier: Apache-2.0 */
+
+/**
+ * Linker script for an OpenTitan (test) boot ROM on CHERIoT.
+ */
+
+OUTPUT_ARCH(riscv)
+
+/**
+ * Indicate that there are no dynamic libraries, whatsoever.
+ */
+__DYNAMIC = 0;
+
+INCLUDE hw/top_matcha/sw/autogen/top_matcha_memory_cheri.ld
+
+/**
+ * The boot address, which indicates the location of the initial interrupt
+ * vector.
+ */
+_boot_address = ORIGIN(rom);
+
+/**
+ * Symbols to be used in the setup of the address translation for ROM_EXT.
+ */
+
+_rom_digest_size = 32;
+/*_chip_info_start = ORIGIN(rom) + LENGTH(rom) - _rom_digest_size - _chip_info_size;*/
+
+/* DV Log offset (has to be different to other boot stages). */
+_dv_log_offset = 0x0;
+
+/**
+ * We define an entry point only for documentation purposes (and to stop LLD
+ * erroring). In reality, we don't use this information within the ROM image, as
+ * we start at a fixed offset.
+ */
+ENTRY(_reset_start);
+
+/**
+ * NOTE: We have to align each section to word boundaries as our current
+ * s19->slm conversion scripts are not able to handle non-word aligned sections.
+ */
+SECTIONS {
+  /**
+   * Ibex interrupt vector. See test_rom_start.S for more information.
+   *
+   * This has to be set up at the boot address, so that execution jumps to the
+   * reset handler correctly.
+   * XXX not used on CHERIoT where only direct mode is supported
+   */
+  .vectors _boot_address : ALIGN(4) {
+    KEEP(*(.vectors))
+  } > rom
+  /**
+   * Standard text section, containing program code.
+   */
+  .text : ALIGN(4) {
+    *(.text)
+    *(.text.*)
+    *(.crt)  /* currently in crt_cheri.S, could be merged */
+
+    /**
+     * Read-only data section, containing all large compile-time constants, like
+     * strings. Note this goes inside the output text segment to get pcc-relative
+     * addressing; otherwise it is considered global DATA and cgp-relative
+     * addressing is generated.
+     */
+    /* Small read-only data comes before regular read-only data for the same
+     * reasons as in the data section */
+    *(.srodata)
+    *(.srodata.*)
+    *(.rodata)
+    *(.rodata.*)
+
+    /**
+     * Immutable chip_info data, containing build-time-recorded information.
+     */
+    (*(.chip_info))
+  } > rom
+
+  /**
+   * Standard mutable data section, at the bottom of RAM. This is
+   * initialized from the .idata section at runtime by the CRT.
+   */
+  .data : ALIGN(4) {
+    _data_start = .;
+    _data_init_start = LOADADDR(.data);
+
+    /**
+     * Critical static data.
+     * NB: want only in RAM but this data needs to be in the same
+     *     segment as other data to be addressable through cgp
+     */
+    KEEP(*(.static_critical.boot_measurements))
+    KEEP(*(.static_critical.epmp_state))
+    KEEP(*(.static_critical.sec_mmio_ctx))
+
+    /* Small data should come before larger data. This helps to ensure small
+     * globals are within 2048 bytes of the value of `gp`, making their accesses
+     * hopefully only take one instruction. */
+    *(.sdata)
+    *(.sdata.*)
+
+    /* Other data will likely need multiple instructions to load, so we're less
+     * concerned about address materialisation taking more than one instruction.
+     */
+    *(.data)
+    *(.data.*)
+
+    /* Ensure section end is word-aligned. */
+    . = ALIGN(4);
+    _data_end = .;
+    _data_init_end = LOADADDR(.data) + SIZEOF(.data);
+
+    _bss_start = .;
+    /* Small BSS comes before regular BSS for the same reasons as in the data
+     * section */
+    *(.sbss)
+    *(.sbss.*)
+    *(.bss)
+    *(.bss.*)
+    . = ALIGN(4);
+    _bss_end = .;
+
+    /* This puts it in ram_main at runtime (for the VMA), but puts the section
+     * into rom for load time (for the LMA). This is why `_data_init_*` uses
+     * `LOADADDR`. */
+  } > ram_rom AT> rom
+
+  /**
+   * Discard capability relocation data. There are no global caps
+   * to be relocated. XXX need to undestand where these are coming from
+   */
+  /DISCARD/ : { *(__cap_relocs) }
+
+  INCLUDE external/lowrisc_opentitan/sw/device/info_sections.ld
+}
diff --git a/sw/device/lib/testing/test_rom/test_rom_start_cheri.S b/sw/device/lib/testing/test_rom/test_rom_start_cheri.S
new file mode 100644
index 0000000..4949f9e
--- /dev/null
+++ b/sw/device/lib/testing/test_rom/test_rom_start_cheri.S
@@ -0,0 +1,322 @@
+// Copyright 2023 Google LLC
+// Copyright lowRISC contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// CHERIoT version of test_rom_start.
+// NB: no puppeteer support
+
+#include "hw/top_matcha/sw/autogen/top_matcha_memory.h"
+#include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/base/multibits_asm.h"
+#include "ast_regs.h"
+#include "csrng_regs.h"
+#include "edn_regs.h"
+#include "entropy_src_regs.h"
+#include "otp_ctrl_regs.h"
+#include "sensor_ctrl_regs.h"
+#include "sram_ctrl_regs.h"
+
+//.include "assembly-helpers.s"
+
+/// Load the absolute address of a symbol.
+.macro la_abs reg, symbol
+    lui             \reg, %hi(\symbol)
+    addi            \reg, \reg, %lo(\symbol)
+.endm
+
+/**
+ * Helper macro for zeroing a single register.
+ * Use c.li to guarantee it's 2 bytes and in the base ISA.
+ */
+.macro zeroOne reg
+	c.li \reg, 0
+.endm
+
+/**
+ * Helper macro for applying a macro to each argument in a list.  Calls `m`
+ * once for each subsequent argument.
+ */
+.macro forall m rhead rtail:vararg
+	\m \rhead
+	.ifnb \rtail
+	forall \m \rtail
+	.endif
+.endm
+
+/// Zero all of the registers in a list
+.macro zeroRegisters reg1, regs:vararg
+	forall zeroOne, \reg1, \regs
+.endm
+
+
+/**
+ * Test ROM interrupt vectors.
+ *
+ * After reset all interrupts are disabled. Only exceptions (interrupt 0) and
+ * non-maskable interrupts (interrupt 31) are possible. For simplicity however
+ * we just set all interrupt handlers in the Test ROM to use the same handler,
+ * which loops forever.
+ *
+ * Interrupt vectors in Ibex have 32 entries for 32 possible interrupts. The
+ * vector must be 256-byte aligned, as Ibex's vectoring mechanism requires that.
+ *
+ * Note that the Ibex reset handler (entry point) immediately follows this
+ * interrupt vector and can be thought of as an extra entry.
+ *
+ * More information about Ibex's interrupts can be found here:
+ *   https://ibex-core.readthedocs.io/en/latest/03_reference/exception_interrupts.html
+ */
+
+  // Push Test ROM interrupt vector options.
+  .option push
+
+  // Disable RISC-V instruction compression: we need all instructions to
+  // be exactly word wide in the interrupt vector.
+  .option norvc
+
+  // Disable RISC-V linker relaxation, as it can compress instructions at
+  // link-time, which we also really don't want.
+  .option norelax
+
+  // NOTE: The "ax" flag below is necessary to ensure that this section
+  // is allocated executable space in ROM by the linker.
+  .section .vectors, "ax"
+  .balign 256
+  .global _test_rom_interrupt_vector
+  .type _test_rom_interrupt_vector, @function
+_test_rom_interrupt_vector:
+
+  // Each jump instruction must be exactly 4 bytes in order to ensure that the
+  // entries are properly located.
+  .rept 32
+  j _test_rom_irq_handler
+  .endr
+
+  // Ibex reset vector, the initial entry point after reset. (This falls at IRQ
+  // handler 0x80.)
+  j _reset_start
+
+  // Set size so this vector can be disassembled.
+  .size _test_rom_interrupt_vector, .-_test_rom_interrupt_vector
+
+  // Pop ROM interrupt vector options.
+  //
+  // Re-enable compressed instructions, linker relaxation.
+  .option pop
+
+// -----------------------------------------------------------------------------
+/**
+ * Test ROM runtime initialization code.
+ */
+
+  // NOTE: The "ax" flag below is necessary to ensure that this section
+  // is allocated executable space in ROM by the linker.
+  .section .crt, "ax"
+
+  /**
+   * Entry point after reset. This symbol is jumped to from the handler
+   * for IRQ 0x80.
+   */
+  .balign 4
+  .global _reset_start
+  .type _reset_start, @function
+
+_reset_start:
+  // Set up the trap vector, mostly for debugging
+  cspecialr  ca2, mtcc
+  la_abs  t0, _test_rom_irq_handler
+  csetaddr  ca2, ca2, t0
+  cspecialw  mtcc, ca2
+
+  cspecialr ca4, mtdc     // Keep the RW memory root in ca4
+
+  // Set up the stack at the end of RAM. XXX set proper perms
+  la_abs  t0, _stack_start
+  csetaddr csp, ca4, t0
+  la_abs  t1, _stack_end
+  sub t1, t1, t0
+  csetbounds csp, csp, t1
+  cincoffset csp, csp, t1
+
+  // Set up the global pointer. This assumes .bss follows .data.
+  la_abs t0, _data_start
+  csetaddr cgp, ca4, t0
+  la_abs t1, _bss_end
+  sub t1, t1, t0
+  csetbounds cgp, cgp, t1
+  srli t1, t1, 1
+  cincoffset cgp, cgp, t1
+
+  // Clobber all writeable registers
+//  zeroRegisters  x1, x2, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15
+
+  // Explicit fall-through to `_start`.
+
+  .size _reset_start, .-_reset_start
+
+// -----------------------------------------------------------------------------
+
+  /**
+   * Callable entry point for the boot rom.
+   *
+   * Currently, this zeroes the `.bss` section, copies initial data to
+   * `.data`, and then jumps to the program entry point.
+   */
+  .balign 4
+  .global _start
+  .type _start, @function
+_start:
+  cspecialr ca4, mtdc     // Keep the RW memory root in ca4
+
+#if !OT_IS_ENGLISH_BREAKFAST
+#if !OTP_IS_RAM
+  // Check if AST initialization should be skipped.
+  li   t0, (TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + \
+            OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET)
+  cincoffset ca0, ca4, t0
+  clw  t0, OTP_CTRL_PARAM_CREATOR_SW_CFG_AST_INIT_EN_OFFSET(ca0)
+  li   t1, MULTIBIT_ASM_BOOL4_TRUE
+  bne  t0, t1, .L_ast_init_skip
+
+  // Copy the AST configuration from OTP.
+  li   t0, (TOP_MATCHA_AST_BASE_ADDR)
+  cincoffset   ca0, ca4, t0
+  li   t0, (TOP_MATCHA_AST_BASE_ADDR + AST_REGAL_REG_OFFSET + 4)
+  cincoffset   ca1, ca4, t0
+  li   t0, (TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + \
+            OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET + \
+            OTP_CTRL_PARAM_CREATOR_SW_CFG_AST_CFG_OFFSET)
+  cincoffset   ca2, ca4, t0
+  cjal crt_section_copy
+#else // OTP_IS_RAM
+  j .L_ast_init_skip
+#endif
+
+  // Wait for AST initialization to complete.
+  li   t0, TOP_MATCHA_SENSOR_CTRL_BASE_ADDR
+  cincoffset   ca0, ca4, t0
+.L_ast_done_loop:
+  clw   t0, SENSOR_CTRL_STATUS_REG_OFFSET(ca0)
+  srli t0, t0, SENSOR_CTRL_STATUS_AST_INIT_DONE_BIT // no-op as bit index is currently 0
+  andi t0, t0, 0x1
+  beqz t0, .L_ast_done_loop
+
+.L_ast_init_skip:
+  // The following sequence enables the minimum level of entropy required to
+  // initialize memory scrambling, as well as the entropy distribution network.
+  li t0, TOP_MATCHA_ENTROPY_SRC_BASE_ADDR
+  cincoffset ca0, ca4, t0
+
+  // Note for BOOT_ROM initialization the FIPS_ENABLE bit is set to kMultiBitBool4False
+  // to prevent the release of FIPS entropy until all the thresholds are set
+  li t0, (MULTIBIT_ASM_BOOL4_FALSE << ENTROPY_SRC_CONF_FIPS_ENABLE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_FALSE << ENTROPY_SRC_CONF_ENTROPY_DATA_REG_ENABLE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_FALSE << ENTROPY_SRC_CONF_THRESHOLD_SCOPE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_FALSE << ENTROPY_SRC_CONF_RNG_BIT_ENABLE_OFFSET)
+  csw t0, ENTROPY_SRC_CONF_REG_OFFSET(ca0)
+
+  li t0, (MULTIBIT_ASM_BOOL4_TRUE << ENTROPY_SRC_MODULE_ENABLE_MODULE_ENABLE_OFFSET)
+  csw t0, ENTROPY_SRC_MODULE_ENABLE_REG_OFFSET(ca0)
+
+  li t0, TOP_MATCHA_CSRNG_BASE_ADDR
+  cincoffset ca0, ca4, t0
+  li t0, (MULTIBIT_ASM_BOOL4_TRUE << CSRNG_CTRL_ENABLE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_TRUE << CSRNG_CTRL_SW_APP_ENABLE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_TRUE << CSRNG_CTRL_READ_INT_STATE_OFFSET)
+  csw t0, CSRNG_CTRL_REG_OFFSET(ca0)
+
+  li t0, TOP_MATCHA_EDN0_BASE_ADDR
+  cincoffset ca0, ca4, t0
+  li t0, (MULTIBIT_ASM_BOOL4_TRUE << EDN_CTRL_EDN_ENABLE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_TRUE << EDN_CTRL_BOOT_REQ_MODE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_FALSE << EDN_CTRL_AUTO_REQ_MODE_OFFSET) | \
+         (MULTIBIT_ASM_BOOL4_FALSE << EDN_CTRL_CMD_FIFO_RST_OFFSET)
+  csw t0, EDN_CTRL_REG_OFFSET(ca0)
+
+#if 0
+  // Remove address space protections by configuring entry 15 as
+  // read-write-execute for the entire address space and then clearing
+  // all other entries.
+  // NOTE: This should happen before attemting to access any address outside
+  // the initial ePMP RX region at reset, e.g. `kDeviceType` which is in
+  // .rodata.
+  li   t0, (0x9f << 24) // Locked NAPOT read-write-execute.
+  csrw pmpcfg3, t0
+  li   t0, 0x7fffffff   // NAPOT encoded region covering entire 34-bit address space.
+  csrw pmpaddr15, t0
+  csrw pmpcfg0, zero
+  csrw pmpcfg1, zero
+  csrw pmpcfg2, zero
+#else
+  // No PMP support on CHERIoT. Could use caps to do the equivalent.
+#endif
+#endif // !OT_IS_ENGLISH_BREAKFAST
+  // Scramble and initialize main memory (main SRAM).
+  // Memory accesses will stall until initialization is complete.
+  // Skip SRAM initialization for DV sim device type, as the testbench handles
+  // this to optimize test run times.
+  la_abs   a0, kDeviceType
+  csetaddr ca0, ca4, a0
+  clw   t0, (ca0)
+  beqz  t0, .L_sram_init_skip
+  li    t0, TOP_MATCHA_SRAM_CTRL_MAIN_REGS_BASE_ADDR
+  cincoffset ca0, ca4, t0
+  li    t0, (1 << SRAM_CTRL_CTRL_INIT_BIT)
+  csw   t0, SRAM_CTRL_CTRL_REG_OFFSET(ca0)
+
+.L_sram_init_skip:
+  // Zero out the `.bss` segment.
+  la_abs   a0, _bss_start
+  csetaddr ca0, ca4, a0
+  la_abs   a1, _bss_end
+  cjal crt_section_clear
+
+  // Initialize the `.data` segment from the `.idata` segment.
+  la_abs   a0, _data_start
+  csetaddr ca0, ca4, a0     // dst start
+  la_abs   a1, _data_end
+  csetaddr ca1, ca4, a1     // dst end
+  la_abs   a2, _data_init_start
+  csetaddr ca2, ca4, a2     // src start
+  cjal crt_section_copy
+
+  // Clobber all temporary registers.
+  zeroRegisters t0, t1, t2
+  cmove ca0, ca4  // Pass root cap along
+  // Clobber all argument registers.
+  zeroRegisters a1, a2, a3, a4, a5
+  // XXX is stack zero'd?
+  // Jump into the C program entry point.
+  cjal _boot_start
+
+  // Enter a wait for interrupt loop, the device should reset shortly.
+.L_wfi_loop:
+  wfi
+  j   .L_wfi_loop
+
+  .size _start, .-_start
+
+// -----------------------------------------------------------------------------
+
+  /**
+   * Test ROM IRQ/exception handler; loops forever.
+   */
+  .balign 4
+  .section .text
+  .global _test_rom_irq_handler
+  .type _test_rom_irq_handler, @function
+_test_rom_irq_handler:
+  wfi
+  j _test_rom_irq_handler
+  .size _test_rom_irq_handler, .-_test_rom_irq_handler
diff --git a/sw/device/silicon_creator/rom/bootstrap.c b/sw/device/silicon_creator/rom/bootstrap.c
index da24b57..f31b154 100644
--- a/sw/device/silicon_creator/rom/bootstrap.c
+++ b/sw/device/silicon_creator/rom/bootstrap.c
@@ -346,12 +346,13 @@
   return error;
 }
 
-hardened_bool_t bootstrap_requested(void) {
+hardened_bool_t bootstrap_requested(mmio_region_t otp_addr,
+                                    mmio_region_t gpio_addr) {
 #if defined(OTP_IS_RAM)
   uint32_t res = kHardenedBoolTrue;
 #else
   uint32_t res =
-      otp_read32(OTP_CTRL_PARAM_OWNER_SW_CFG_ROM_BOOTSTRAP_EN_OFFSET);
+      otp_read32(otp_addr, OTP_CTRL_PARAM_OWNER_SW_CFG_ROM_BOOTSTRAP_EN_OFFSET);
 #endif
   if (launder32(res) != kHardenedBoolTrue) {
     return kHardenedBoolFalse;
@@ -361,9 +362,7 @@
   // A single read is sufficient since we expect strong pull-ups on the strap
   // pins.
   res ^= SW_STRAP_BOOTSTRAP;
-  res ^=
-      abs_mmio_read32(TOP_MATCHA_GPIO_BASE_ADDR + GPIO_DATA_IN_REG_OFFSET) &
-      SW_STRAP_MASK;
+  res ^= mmio_region_read32(gpio_addr, GPIO_DATA_IN_REG_OFFSET) & SW_STRAP_MASK;
   if (launder32(res) != kHardenedBoolTrue) {
     return kHardenedBoolFalse;
   }
@@ -371,14 +370,16 @@
   return res;
 }
 
-rom_error_t bootstrap(void) {
-  hardened_bool_t requested = bootstrap_requested();
+// NB: assumes flash_ctrl_init is already called
+rom_error_t bootstrap(mmio_region_t otp_addr, mmio_region_t gpio_addr,
+                      mmio_region_t spi_device_addr) {
+  hardened_bool_t requested = bootstrap_requested(otp_addr, gpio_addr);
   if (launder32(requested) != kHardenedBoolTrue) {
     return kErrorBootstrapNotRequested;
   }
   HARDENED_CHECK_EQ(requested, kHardenedBoolTrue);
 
-  spi_device_init();
+  spi_device_init(spi_device_addr);
 
   // Bootstrap event loop.
   bootstrap_state_t state = kBootstrapStateErase;
diff --git a/sw/device/silicon_creator/rom/bootstrap.h b/sw/device/silicon_creator/rom/bootstrap.h
index 36b9667..758ae0b 100644
--- a/sw/device/silicon_creator/rom/bootstrap.h
+++ b/sw/device/silicon_creator/rom/bootstrap.h
@@ -20,6 +20,7 @@
 #define SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_
 
 #include "sw/device/lib/base/hardened.h"
+#include "sw/device/lib/base/mmio.h"
 #include "sw/device/silicon_creator/lib/error.h"
 
 #ifdef __cplusplus
@@ -34,7 +35,8 @@
  *
  * @return Whether bootstrap is requested.
  */
-hardened_bool_t bootstrap_requested(void);
+hardened_bool_t bootstrap_requested(mmio_region_t otp_addr,
+                                    mmio_region_t gpio_addr);
 
 /**
  * Bootstraps the data partition of the embedded flash with data received by the
@@ -52,7 +54,8 @@
  *
  * @return Result of the operation.
  */
-rom_error_t bootstrap(void);
+rom_error_t bootstrap(mmio_region_t otp_addr, mmio_region_t gpio_addr,
+                      mmio_region_t spi_device_addr);
 
 #ifdef __cplusplus
 }
diff --git a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc.c b/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc.c
index 92666f7..9a77e38 100644
--- a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc.c
+++ b/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc.c
@@ -61,7 +61,13 @@
   test_status_set(kTestStatusInTest);
   init_uart(TOP_MATCHA_UART0_BASE_ADDR, &uart);
   LOG_INFO("kelvin_test_sc");
-  spi_flash_init();
+  const mmio_region_t spi_host_addr =
+      mmio_region_from_addr(TOP_MATCHA_SPI_HOST0_BASE_ADDR);
+  const mmio_region_t eflash_addr =
+      mmio_region_from_addr(TOP_MATCHA_FLASH_CTRL_CORE_BASE_ADDR);
+  const mmio_region_t otp_addr =
+      mmio_region_from_addr(TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR);
+  spi_flash_init(spi_host_addr, eflash_addr, otp_addr);
 
   // Copy binary to SMC RAM.
   CHECK_DIF_OK(load_file_from_tar(