Merge "Add clock gating tests for video/audio clocks"
diff --git a/hw/bitstream/vivado/BUILD b/hw/bitstream/vivado/BUILD
index acc80f9..b192368 100644
--- a/hw/bitstream/vivado/BUILD
+++ b/hw/bitstream/vivado/BUILD
@@ -121,11 +121,30 @@
     tags = ["manual"],
 )
 
+bitstream_splice(
+    name = "fpga_nexus_mask_test_rom_no_otp",
+    testonly = True,
+    src = ":fpga_nexus_test_rom",
+    data = "//sw/device/lib/testing/test_rom:test_rom_no_otp_fpga_nexus_scr_vmem",
+    meminfo = ":rom_mmi",
+    tags = ["manual"],
+)
+
+bitstream_splice(
+    name = "fpga_nexus_mask_test_rom_no_otp_empty",
+    testonly = True,
+    src = ":fpga_nexus_mask_test_rom_no_otp",
+    data = "//hw/top_matcha/data:otp_img_empty",
+    meminfo = ":otp_mmi",
+    tags = ["manual"],
+)
+
 pkg_tar(
     name = "fpga_nexus_smoketest",
     testonly = True,
     srcs = [
         ":fpga_nexus_mask_test_rom",
+        ":fpga_nexus_mask_test_rom_no_otp_empty",
         ":logs",
         ":otp_mmi",
         ":rom_mmi",
diff --git a/hw/top_matcha/data/BUILD b/hw/top_matcha/data/BUILD
index 6fd346d..ee7aa69 100644
--- a/hw/top_matcha/data/BUILD
+++ b/hw/top_matcha/data/BUILD
@@ -2,7 +2,7 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-load("@lowrisc_opentitan//rules:otp.bzl", "otp_image")
+load("@lowrisc_opentitan//rules:otp.bzl", "otp_image", "otp_json", "otp_partition")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -30,3 +30,199 @@
         "@lowrisc_opentitan//hw/ip/otp_ctrl/data:otp_json_hw_cfg",
     ],
 )
+
+################
+
+otp_json(
+    name = "otp_json_empty",
+    partitions = [
+        otp_partition(
+            name = "SECRET0",
+            items = {
+                "TEST_UNLOCK_TOKEN": "0",
+                "TEST_EXIT_TOKEN": "0",
+            },
+            lock = False,
+        ),
+        otp_partition(
+            name = "SECRET1",
+            items = {
+                "FLASH_ADDR_KEY_SEED": "0",
+                "FLASH_DATA_KEY_SEED": "0",
+                "SRAM_DATA_KEY_SEED": "0",
+            },
+            lock = False,
+        ),
+        otp_partition(
+            name = "SECRET2",
+            items = {
+                "RMA_TOKEN": "0",
+                "CREATOR_ROOT_KEY_SHARE0": "0",
+                "CREATOR_ROOT_KEY_SHARE1": "0",
+            },
+            lock = False,
+        ),
+        otp_partition(
+            name = "LIFE_CYCLE",
+            count = 8,
+            # Should we set this to TEST_UNLOCKED0?
+            state = "TEST_UNLOCKED0",
+        ),
+    ],
+    seed = "0",
+)
+
+otp_json(
+    name = "otp_json_creator_sw_cfg",
+    partitions = [
+        otp_partition(
+            name = "CREATOR_SW_CFG",
+            items = {
+                "CREATOR_SW_CFG_DIGEST": "0x0",
+                # Use software mod_exp implementation for signature
+                # verification. See the definition of `hardened_bool_t` in
+                # sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_SIGVERIFY_RSA_MOD_EXP_IBEX_EN": "0",
+                # Mark the first three keys as valid and remaining as invalid
+                # since we currently have only three keys. See the definition of
+                # `hardened_byte_bool_t` in sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_SIGVERIFY_RSA_KEY_EN": "0",
+                # Enable use of entropy for countermeasures. See the definition
+                # of `hardened_bool_t` in sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_RNG_EN": "0",
+                # ROM execution is enabled if this item is set to a non-zero
+                # value.
+                "CREATOR_SW_CFG_ROM_EXEC_EN": "0",
+                # Value to write to the cpuctrl CSR in `rom_init()`.
+                # See:
+                # https://ibex-core.readthedocs.io/en/latest/03_reference/cs_registers.html#cpu-control-register-cpuctrl
+                "CREATOR_SW_CFG_CPUCTRL": "0",
+                "CREATOR_SW_CFG_JITTER_EN": "0",
+                # Value of the min_security_version_rom_ext field of the
+                # default boot data.
+                "CREATOR_SW_CFG_MIN_SEC_VER_ROM_EXT": "0x0",
+                # Value of the min_security_version_bl0 field of the default
+                # boot data.
+                "CREATOR_SW_CFG_MIN_SEC_VER_BL0": "0x0",
+                # Enable the default boot data in PROD and PROD_END life cycle
+                # states. See the definition of `hardened_bool_t` in
+                # sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_DEFAULT_BOOT_DATA_IN_PROD_EN": "0",
+                # Enable AST initialization.
+                "CREATOR_SW_CFG_AST_INIT_EN": "0",
+                # TODO: This enables a busyloop in the ROM to give time to
+                # trigger an RMA lifecycle transition via JTAG.  The current
+                # value of 10 cycles is useful for test code which verifies
+                # the path through the ROM.  This value is not useful for a
+                # real chip.
+                "CREATOR_SW_CFG_RMA_SPIN_EN": "0",
+                "CREATOR_SW_CFG_RMA_SPIN_CYCLES": "0",
+                # Entropy source health check default values. This needs to be
+                # populated when `CREATOR_SW_CFG_RNG_EN` is set to true.
+                "CREATOR_SW_CFG_RNG_REPCNT_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_REPCNTS_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_ADAPTP_HI_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_ADAPTP_LO_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_BUCKET_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_MARKOV_HI_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_MARKOV_LO_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_EXTHT_HI_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_EXTHT_LO_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_ALERT_THRESHOLD": "0",
+                "CREATOR_SW_CFG_RNG_HEALTH_CONFIG_DIGEST": "0",
+            },
+        ),
+    ],
+)
+
+otp_json(
+    name = "otp_json_owner_sw_cfg",
+    partitions = [
+        otp_partition(
+            name = "OWNER_SW_CFG",
+            items = {
+                "OWNER_SW_CFG_DIGEST": "0x0",
+                # Enable bootstrap. See `hardened_bool_t` in
+                # sw/device/lib/base/hardened.h.
+                "OWNER_SW_CFG_ROM_BOOTSTRAP_EN": "0",
+                # Set to 0x739 to use the ROM_EXT hash measurement as the key
+                # manager attestation binding value.
+                "OWNER_SW_CFG_ROM_KEYMGR_ROM_EXT_MEAS_EN": "0x0",
+                # Report errors without any redaction.
+                "OWNER_SW_CFG_ROM_ERROR_REPORTING": "0",
+                # Set the enables to kAlertEnableNone.
+                # See `alert_enable_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_ALERT_CLASS_EN": "0",
+                # Set the esclation policies to kAlertEscalateNone.
+                # See `alert_escalate_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_ALERT_ESCALATION": "0",
+                # Set the classifiactions to kAlertClassX.
+                # See `alert_class_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_ALERT_CLASSIFICATION": ["0"] * 80,
+                # Set the classifiactions to kAlertClassX. See `alert_class_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_LOCAL_ALERT_CLASSIFICATION": ["0"] * 16,
+                # Set the alert accumulation thresholds to 0 per class.
+                "OWNER_SW_CFG_ROM_ALERT_ACCUM_THRESH": ["0x00000000"] * 4,
+                # Set the alert timeout cycles to 0 per class.
+                "OWNER_SW_CFG_ROM_ALERT_TIMEOUT_CYCLES": ["0x00000000"] * 4,
+                # Set the alert phase cycles to 0,10,10,0xFFFFFFFF for classes
+                # A and B, and to all zeros for classes C and D.
+                "OWNER_SW_CFG_ROM_ALERT_PHASE_CYCLES": [
+                    "0x0",
+                    "0",
+                    "0",
+                    "0",
+                    "0x0",
+                    "0",
+                    "0",
+                    "0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                ],
+            },
+        ),
+    ],
+)
+
+otp_json(
+    name = "otp_json_hw_cfg",
+    partitions = [
+        otp_partition(
+            name = "HW_CFG",
+            items = {
+                "DEVICE_ID": "0",
+                # Cryptolib and chip-level tests require access to the CSRNG
+                # software interfaces.
+                "EN_CSRNG_SW_APP_READ": False,
+                "EN_SRAM_IFETCH": False,
+                # Cryptolib and chip-level tests require access to the
+                # entropy_src FW data interface.
+                "EN_ENTROPY_SRC_FW_READ": False,
+                # Cryptolib and chip-level tests require access to the
+                # entropy_src FW override interface.
+                "EN_ENTROPY_SRC_FW_OVER": False,
+            },
+            lock = False,
+        ),
+    ],
+)
+
+otp_image(
+    name = "otp_img_empty",
+    src = ":otp_json_empty",
+    overlays = [
+        ":otp_json_creator_sw_cfg",
+        ":otp_json_owner_sw_cfg",
+        ":otp_json_hw_cfg",
+    ],
+)
diff --git a/hw/top_matcha/data/clocks_nexus.xdc b/hw/top_matcha/data/clocks_nexus.xdc
index 74cbd85..8763a73 100644
--- a/hw/top_matcha/data/clocks_nexus.xdc
+++ b/hw/top_matcha/data/clocks_nexus.xdc
@@ -2,7 +2,7 @@
 ## Licensed under the Apache License, Version 2.0, see LICENSE for details.
 ## SPDX-License-Identifier: Apache-2.0
 
-## Clock Signal
+## Clock Signal - 100M >> 1/100e6 = 1e-8s = 10ns
 create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports IO_CLK]
 
 ## Rename MMCM outputs for less bug-prone parsing.
@@ -11,6 +11,11 @@
 create_generated_clock -name clk_usb_48 [get_pin clkgen/pll/CLKOUT1]
 create_generated_clock -name clk_aon [get_pin clkgen/pll/CLKOUT4]
 
+# Store clock periods in variables
+set clk_main_period   [get_property PERIOD [get_clocks clk_main]]
+set clk_usb_48_period [get_property PERIOD [get_clocks clk_usb_48]]
+set clk_aon_period    [get_property PERIOD [get_clocks clk_aon]]
+
 ## Clock Domain Crossings
 set clks_10_unbuf [get_clocks -of_objects [get_pin clkgen/pll/CLKOUT0]]
 set clks_48_unbuf [get_clocks -of_objects [get_pin clkgen/pll/CLKOUT1]]
@@ -42,15 +47,15 @@
     ] \
   ]
 
-## JTAG clocks
+## JTAG clocks TODO - clock frequency
 create_clock -add -name lc_jtag_tck -period 200.00 -waveform {0 5} [get_pin top_*/u_pinmux_aon/u_pinmux_strap_sampling/u_pinmux_jtag_buf_lc/prim_clock_buf_tck/gen_xilinx.u_impl_xilinx/gen_fpga_buf.gen_bufg.bufg_i/O]
 create_clock -add -name rv_jtag_tck -period 200.00 -waveform {0 5} [get_pin top_*/u_pinmux_aon/u_pinmux_strap_sampling/u_pinmux_jtag_buf_rv/prim_clock_buf_tck/gen_xilinx.u_impl_xilinx/gen_fpga_buf.gen_bufg.bufg_i/O]
 
 ## SPI clocks
 set spi_dev_data [get_ports {SPI_DEV_D0 SPI_DEV_D1 SPI_DEV_D2 SPI_DEV_D3}]
-create_clock -add -name clk_spi  -period 100.00 -waveform {0 50} [get_ports SPI_DEV_CLK]
-create_clock -add -name clk_cs  -period 1000.00 -waveform {0 50} [get_ports SPI_DEV_CS_L]
-set_input_delay -clock clk_spi 5 ${spi_dev_data} -add_delay
+create_clock -add -name clk_spi  -period $clk_main_period             -waveform {0 50} [get_ports SPI_DEV_CLK]
+create_clock -add -name clk_cs   -period [expr $clk_main_period * 10] -waveform {0 50} [get_ports SPI_DEV_CS_L]
+set_input_delay  -clock clk_spi 5 ${spi_dev_data} -add_delay
 set_output_delay -clock clk_spi 5 ${spi_dev_data} -add_delay
 # set_input_delay -clock clk_spi -clock_fall -min ${spi_dev_in_delay_min} ${spi_dev_data} -add_delay
 # set_input_delay -clock clk_spi -clock_fall -max ${spi_dev_in_delay_max} ${spi_dev_data} -add_delay
diff --git a/hw/top_matcha/dv/chip_sim_cfg.hjson b/hw/top_matcha/dv/chip_sim_cfg.hjson
index 227194e..e9ced65 100644
--- a/hw/top_matcha/dv/chip_sim_cfg.hjson
+++ b/hw/top_matcha/dv/chip_sim_cfg.hjson
@@ -2091,6 +2091,7 @@
         "chip_sw_sysrst_ctrl_inputs",
         "chip_sw_uart_smoketest",
         "chip_sw_uart_tx_rx",
+        "chip_tap_straps_rma",
       ]
     }
     {
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 fcc9184..026607f 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
@@ -6,13 +6,15 @@
 #include "sw/device/lib/testing/test_framework/check.h"
 
 void load_smc(void) {
-  CHECK_DIF_OK(
-      load_file_from_tar("smc.bin", (void*)TOP_MATCHA_RAM_SMC_BASE_ADDR));
+  CHECK_DIF_OK(load_file_from_tar(
+      "smc.bin", (void*)TOP_MATCHA_RAM_SMC_BASE_ADDR,
+      (TOP_MATCHA_RAM_SMC_BASE_ADDR + TOP_MATCHA_RAM_SMC_SIZE_BYTES)));
 }
 
 void load_kelvin(void) {
-  CHECK_DIF_OK(load_file_from_tar("kelvin.bin",
-                                  (void*)TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR));
+  CHECK_DIF_OK(load_file_from_tar(
+      "kelvin.bin", (void*)TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR,
+      (TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR + TOP_MATCHA_RAM_ML_DMEM_SIZE_BYTES)));
 }
 
 void load_init(void) { spi_flash_init(); }
diff --git a/sw/device/lib/spi_flash.c b/sw/device/lib/spi_flash.c
index 5d1e2be..0e8932e 100644
--- a/sw/device/lib/spi_flash.c
+++ b/sw/device/lib/spi_flash.c
@@ -58,34 +58,47 @@
   return size;
 }
 
-int contains(const void* src, const char* str2) {
-  const char* str1 = (const char*)src;
-  const char* cursor = str2;
-  while (*str1 && *cursor) {
-    cursor = (*str1 == *cursor) ? cursor + 1 : str2;
-    str1++;
+size_t str_size(const char* str) {
+  size_t size = 0;
+  char* ptr = (char*)str;
+  while (*ptr++) {
+    size++;
   }
-  return *cursor ? 0 : 1;
+  return size;
 }
 
-// Returns the offset of the file in the tar.
+// Returns the offset of the file(or prefix) in the tar.
 // `size_out` will be filled with the size of the file.
-static size_t find_file_in_tar(const char* filename, size_t* size_out) {
+static size_t find_file_in_tar(const char* filename, size_t start_cursor,
+                               size_t* size_out, char* found_filename,
+                               size_t filename_max_len) {
   if (!size_out || !filename) {
     return 0;
   }
-
+  const char kTarMagic[] = "ustar";
   tar_header tar;
-  size_t cursor = 0;
+  size_t cursor = start_cursor;
   CHECK_DIF_OK(spi_flash_read_page(cursor, (uint8_t*)&tar));
   while (cursor < kSpiFlashBytes) {
+    // Check the tar header magic field to validate the header info.
+    if (memcmp((const char*)&tar.magic, kTarMagic, str_size(kTarMagic)) != 0) {
+      break;
+    }
     int size = parse_octal((uint8_t*)&tar.size);
     cursor += 512;
-
-    if (contains(&tar.name, filename)) {
+    const char* tar_name = (const char*)&tar.name;
+    size_t tar_name_len = str_size(tar_name);
+    if (memcmp(tar_name, filename, str_size(filename)) == 0) {
       if (size_out) {
         *size_out = size;
       }
+      if (found_filename) {
+        size_t copy_len = (tar_name_len < filename_max_len - 1)
+                              ? tar_name_len
+                              : filename_max_len - 1;
+        memcpy(found_filename, tar_name, copy_len);
+        found_filename[copy_len] = '\0';
+      }
       return cursor;
     }
 
@@ -96,11 +109,11 @@
   return 0;
 }
 
-dif_result_t load_file_from_tar(const char* filename, void* addr) {
-  size_t size = 0;
-  size_t bin_offset = find_file_in_tar(filename, &size);
-  if (!bin_offset) return kDifError;
-
+static dif_result_t copy_flash_to_mem(size_t bin_offset, size_t size,
+                                      void* addr) {
+  if (!addr) {
+    return kDifBadArg;
+  }
   uint8_t page[SPI_PAGE_SIZE];
   size_t remaining = size;
   while (remaining > 0) {
@@ -116,3 +129,38 @@
 
   return kDifOk;
 }
+
+dif_result_t load_file_from_tar(const char* filename, void* addr,
+                                size_t max_mem_addr) {
+  if (!filename) {
+    return kDifBadArg;
+  }
+  size_t size = 0;
+  size_t bin_offset =
+      find_file_in_tar(filename, /*start_cursor=*/0, &size,
+                       /*found_filename=*/NULL, /*max_filename_len=*/0);
+  if (!bin_offset) return kDifError;
+  if ((size_t)addr + size > max_mem_addr) {
+    return kDifBadArg;
+  }
+  return copy_flash_to_mem(bin_offset, size, addr);
+}
+
+dif_result_t load_file_prefix_from_tar(const char* file_prefix, void* addr,
+                                       size_t max_mem_addr, size_t* tar_offset,
+                                       char* filename, size_t filename_len) {
+  if (!tar_offset || !file_prefix || !filename) {
+    return kDifBadArg;
+  }
+  size_t size = 0;
+  size_t start_offset = *tar_offset;
+  size_t bin_offset = find_file_in_tar(file_prefix, start_offset, &size,
+                                       filename, filename_len);
+  if (!bin_offset) return kDifOutOfRange;
+  if ((size_t)addr + size > max_mem_addr) {
+    return kDifBadArg;
+  }
+  // Update the tar_offset for the future call.
+  *tar_offset = bin_offset + ((size + 511) & ~511);
+  return copy_flash_to_mem(bin_offset, size, addr);
+}
diff --git a/sw/device/lib/spi_flash.h b/sw/device/lib/spi_flash.h
index 649fd85..746bc23 100644
--- a/sw/device/lib/spi_flash.h
+++ b/sw/device/lib/spi_flash.h
@@ -18,7 +18,12 @@
 dif_result_t spi_flash_read_page(uint32_t page, uint8_t* buf);
 
 /* Tar filesystem related methods */
-dif_result_t load_file_from_tar(const char* filename, void* addr);
+dif_result_t load_file_from_tar(const char* filename, void* addr,
+                                size_t max_mem_addr);
+
+dif_result_t load_file_prefix_from_tar(const char* file_prefix, void* addr,
+                                       size_t max_mem_addr, size_t* bin_offset,
+                                       char* filename, size_t filename_len);
 
 #if defined(__cplusplus)
 }
diff --git a/sw/device/lib/testing/test_rom/BUILD b/sw/device/lib/testing/test_rom/BUILD
index 31ef4bc..c5fa7d0 100644
--- a/sw/device/lib/testing/test_rom/BUILD
+++ b/sw/device/lib/testing/test_rom/BUILD
@@ -28,15 +28,39 @@
     name = "test_rom",
     deps = [
         ":linker_script",
+        ":test_rom_otp_lib",
+    ],
+)
+
+opentitan_rom_binary(
+    name = "test_rom_no_otp",
+    srcs = [
+        "test_rom.c",
+        "test_rom_start.S",
+    ],
+    defines = ["OTP_IS_RAM"],
+    deps = [
+        ":linker_script",
         ":test_rom_lib",
+        "//sw/device/silicon_creator/rom:bootstrap_no_otp",
+    ],
+)
+
+cc_library(
+    name = "test_rom_otp_lib",
+    srcs = [
+        "test_rom.c",
+        "test_rom_start.S",
+    ],
+    deps = [
+        ":test_rom_lib",
+        "//sw/device/silicon_creator/rom:bootstrap",
     ],
 )
 
 cc_library(
     name = "test_rom_lib",
     srcs = [
-        "test_rom.c",
-        "test_rom_start.S",
     ],
     target_compatible_with = [OPENTITAN_CPU],
     deps = [
@@ -46,8 +70,8 @@
         "//hw/top_matcha/ip/flash_ctrl/data/autogen:flash_ctrl_regs",
         "//hw/top_matcha/ip/sensor_ctrl/data:sensor_ctrl_regs",
         "//hw/top_matcha/sw/autogen:top_matcha",
-        "//sw/device/lib/testing:pinmux_testutils",
         "//sw/device/lib/dif:pinmux",
+        "//sw/device/lib/testing:pinmux_testutils",
         "@lowrisc_opentitan//hw/ip/csrng/data:csrng_regs",
         "@lowrisc_opentitan//hw/ip/edn/data:edn_regs",
         "@lowrisc_opentitan//hw/ip/entropy_src/data:entropy_src_regs",
@@ -79,7 +103,6 @@
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/base:static_critical_sec_mmio",
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:flash_ctrl",
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:retention_sram",
-        "@lowrisc_opentitan//sw/device/silicon_creator/rom:bootstrap",
     ],
 )
 
diff --git a/sw/device/lib/testing/test_rom/test_rom.c b/sw/device/lib/testing/test_rom/test_rom.c
index 02303a0..0f2ed0e 100644
--- a/sw/device/lib/testing/test_rom/test_rom.c
+++ b/sw/device/lib/testing/test_rom/test_rom.c
@@ -73,9 +73,14 @@
 OT_WEAK
 bool rom_test_main(void) {
   // 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 = abs_mmio_read32(
       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);
@@ -86,9 +91,14 @@
   // 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 = abs_mmio_read32(
       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);
@@ -170,9 +180,14 @@
   }
 
   // Check the otp to see if flash scramble should be enabled.
+  // val: 0x0
+#if defined(OTP_IS_RAM)
+  otp_val = 0;
+#else
   otp_val = abs_mmio_read32(
       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;
diff --git a/sw/device/lib/testing/test_rom/test_rom_start.S b/sw/device/lib/testing/test_rom/test_rom_start.S
index 8db5211..a326517 100644
--- a/sw/device/lib/testing/test_rom/test_rom_start.S
+++ b/sw/device/lib/testing/test_rom/test_rom_start.S
@@ -219,6 +219,7 @@
 #endif
 _start:
 #if !OT_IS_ENGLISH_BREAKFAST
+#if !OTP_IS_RAM
   // Check if AST initialization should be skipped.
   li   a0, (TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + \
             OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET)
@@ -233,6 +234,9 @@
             OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET + \
             OTP_CTRL_PARAM_CREATOR_SW_CFG_AST_CFG_OFFSET)
   call crt_section_copy
+#else
+  j .L_ast_init_skip
+#endif
 
   // Wait for AST initialization to complete.
   li   a0, TOP_MATCHA_SENSOR_CTRL_BASE_ADDR
diff --git a/sw/device/silicon_creator/BUILD b/sw/device/silicon_creator/BUILD
new file mode 100644
index 0000000..34910b0
--- /dev/null
+++ b/sw/device/silicon_creator/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2022 Google LLC.
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+package(default_visibility = ["//visibility:public"])
diff --git a/sw/device/silicon_creator/rom/BUILD b/sw/device/silicon_creator/rom/BUILD
new file mode 100644
index 0000000..9e1d2ff
--- /dev/null
+++ b/sw/device/silicon_creator/rom/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2022 Google LLC.
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+package(default_visibility = ["//visibility:public"])
+
+BOOTSTRAP_DEPS = [
+        "//hw/top_matcha/sw/autogen:top_matcha",
+        "//hw/top_matcha/ip/flash_ctrl/data/autogen:flash_ctrl_regs",
+        "@lowrisc_opentitan//hw/ip/gpio/data:gpio_regs",
+        "@lowrisc_opentitan//hw/ip/otp_ctrl/data:otp_ctrl_regs",
+        "@lowrisc_opentitan//sw/device/lib/base:abs_mmio",
+        "@lowrisc_opentitan//sw/device/lib/base:bitfield",
+        "@lowrisc_opentitan//sw/device/lib/base:hardened",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib:error",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/base:chip",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:flash_ctrl",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:otp",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:rstmgr",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:spi_device",
+    ]
+
+cc_library(
+    name = "bootstrap",
+    srcs = ["bootstrap.c"],
+    hdrs = ["bootstrap.h"],
+    deps = BOOTSTRAP_DEPS,
+)
+
+cc_library(
+    name = "bootstrap_no_otp",
+    srcs = ["bootstrap.c"],
+    hdrs = ["bootstrap.h"],
+    defines = ["OTP_IS_RAM"],
+    deps = BOOTSTRAP_DEPS,
+)
diff --git a/sw/device/silicon_creator/rom/bootstrap.c b/sw/device/silicon_creator/rom/bootstrap.c
new file mode 100644
index 0000000..f77c592
--- /dev/null
+++ b/sw/device/silicon_creator/rom/bootstrap.c
@@ -0,0 +1,392 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/silicon_creator/rom/bootstrap.h"
+
+#include <stdalign.h>
+
+#include "sw/device/lib/base/abs_mmio.h"
+#include "sw/device/lib/base/bitfield.h"
+#include "sw/device/lib/base/hardened.h"
+#include "sw/device/silicon_creator/lib/base/chip.h"
+#include "sw/device/silicon_creator/lib/drivers/flash_ctrl.h"
+#include "sw/device/silicon_creator/lib/drivers/otp.h"
+#include "sw/device/silicon_creator/lib/drivers/rstmgr.h"
+#include "sw/device/silicon_creator/lib/drivers/spi_device.h"
+#include "sw/device/silicon_creator/lib/error.h"
+
+#include "flash_ctrl_regs.h"
+#include "gpio_regs.h"
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "otp_ctrl_regs.h"
+
+enum {
+  /*
+   * Maximum flash address, exclusive.
+   */
+  kMaxAddress =
+      FLASH_CTRL_PARAM_BYTES_PER_BANK * FLASH_CTRL_PARAM_REG_NUM_BANKS,
+};
+
+static_assert(FLASH_CTRL_PARAM_REG_NUM_BANKS == 2, "Flash must have 2 banks");
+
+/**
+ * Bootstrap states.
+ *
+ * OpenTitan bootstrap consists of three states between which the chip
+ * transitions sequentially.
+ *
+ * Encoding generated with
+ * $ ./util/design/sparse-fsm-encode.py -d 5 -m 3 -n 32 \
+ *     -s 375382971 --language=c
+ *
+ * Minimum Hamming distance: 17
+ * Maximum Hamming distance: 19
+ * Minimum Hamming weight: 16
+ * Maximum Hamming weight: 19
+ */
+typedef enum bootstrap_state {
+  /**
+   * Initial bootstrap state where the chip waits for a SECTOR_ERASE or
+   * CHIP_ERASE command.
+   */
+  kBootstrapStateErase = 0xd4576543,
+  /**
+   * Second bootstrap state where the chip verifies that all data banks have
+   * been erased.
+   */
+  kBootstrapStateEraseVerify = 0xf3c71bac,
+  /**
+   * Final bootstrap state. This is the main program loop where the chip handles
+   * erase, program, and reset commands.
+   */
+  kBootstrapStateProgram = 0xbdd8ca60,
+} bootstrap_state_t;
+
+/**
+ * Handles access permissions and erases both data banks of the embedded flash.
+ *
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_chip_erase(void) {
+  flash_ctrl_bank_erase_perms_set(kHardenedBoolTrue);
+  rom_error_t err_0 = flash_ctrl_data_erase(0, kFlashCtrlEraseTypeBank);
+  rom_error_t err_1 = flash_ctrl_data_erase(FLASH_CTRL_PARAM_BYTES_PER_BANK,
+                                            kFlashCtrlEraseTypeBank);
+  flash_ctrl_bank_erase_perms_set(kHardenedBoolFalse);
+
+  HARDENED_RETURN_IF_ERROR(err_0);
+  return err_1;
+}
+
+/**
+ * Handles access permissions and erases a 4 KiB region in the data partition of
+ * the embedded flash.
+ *
+ * Since OpenTitan's flash page size is 2 KiB, this function erases two
+ * consecutive pages.
+ *
+ * @param addr Address that falls within the 4 KiB region being deleted.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_sector_erase(uint32_t addr) {
+  static_assert(FLASH_CTRL_PARAM_BYTES_PER_PAGE == 2048,
+                "Page size must be 2 KiB");
+  enum {
+    /**
+     * Mask for truncating `addr` to the lower 4 KiB aligned address.
+     */
+    kPageAddrMask = ~UINT32_C(4096) + 1,
+  };
+
+  if (addr >= kMaxAddress) {
+    return kErrorBootstrapEraseAddress;
+  }
+  addr &= kPageAddrMask;
+
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4False,
+      .erase = kMultiBitBool4True,
+  });
+  rom_error_t err_0 = flash_ctrl_data_erase(addr, kFlashCtrlEraseTypePage);
+  rom_error_t err_1 = flash_ctrl_data_erase(
+      addr + FLASH_CTRL_PARAM_BYTES_PER_PAGE, kFlashCtrlEraseTypePage);
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4False,
+      .erase = kMultiBitBool4False,
+  });
+
+  HARDENED_RETURN_IF_ERROR(err_0);
+  return err_1;
+}
+
+/**
+ * Handles access permissions and programs up to 256 bytes of flash memory
+ * starting at `addr`.
+ *
+ * If `byte_count` is not a multiple of flash word size, it's rounded up to next
+ * flash word and missing bytes in `data` are set to `0xff`.
+ *
+ * @param addr Address to write to, must be flash word aligned.
+ * @param byte_count Number of bytes to write. Rounded up to next flash word if
+ * not a multiple of flash word size. Missing bytes in `data` are set to `0xff`.
+ * @param data Data to write, must be word aligned. If `byte_count` is not a
+ * multiple of flash word size, `data` must have enough space until the next
+ * flash word.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_page_program(uint32_t addr, size_t byte_count,
+                                          uint8_t *data) {
+  static_assert(__builtin_popcount(FLASH_CTRL_PARAM_BYTES_PER_WORD) == 1,
+                "Bytes per flash word must be a power of two.");
+  enum {
+    /**
+     * Mask for checking that `addr` is flash word aligned.
+     */
+    kFlashWordMask = FLASH_CTRL_PARAM_BYTES_PER_WORD - 1,
+    /**
+     * SPI flash programming page size in bytes.
+     */
+    kFlashProgPageSize = 256,
+    /**
+     * Mask for checking whether `addr` is flash programming page aligned.
+     *
+     * Flash programming page size is 256 bytes, writes that start at an `addr`
+     * with a non-zero LSB wrap to the start of the 256 byte region.
+     */
+    kFlashProgPageMask = kFlashProgPageSize - 1,
+  };
+
+  if (addr & kFlashWordMask || addr >= kMaxAddress) {
+    return kErrorBootstrapProgramAddress;
+  }
+
+  // Round up to next flash word and fill missing bytes with `0xff`.
+  size_t flash_word_misalignment = byte_count & kFlashWordMask;
+  if (flash_word_misalignment > 0) {
+    size_t padding_byte_count =
+        FLASH_CTRL_PARAM_BYTES_PER_WORD - flash_word_misalignment;
+    for (size_t i = 0; i < padding_byte_count; ++i) {
+      data[byte_count++] = 0xff;
+    }
+  }
+  size_t rem_word_count = byte_count / sizeof(uint32_t);
+
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4True,
+      .erase = kMultiBitBool4False,
+  });
+  // Perform two writes if the start address is not page-aligned (256 bytes).
+  // Note: Address is flash-word-aligned (8 bytes) due to the check above.
+  rom_error_t err_0 = kErrorOk;
+  size_t prog_page_misalignment = addr & kFlashProgPageMask;
+  if (prog_page_misalignment > 0) {
+    size_t word_count =
+        (kFlashProgPageSize - prog_page_misalignment) / sizeof(uint32_t);
+    if (word_count > rem_word_count) {
+      word_count = rem_word_count;
+    }
+    err_0 = flash_ctrl_data_write(addr, word_count, data);
+    rem_word_count -= word_count;
+    data += word_count * sizeof(uint32_t);
+    // Wrap to the beginning of the current page since PAGE_PROGRAM modifies
+    // a single page only.
+    addr &= ~kFlashProgPageMask;
+  }
+  rom_error_t err_1 = kErrorOk;
+  if (rem_word_count > 0) {
+    err_1 = flash_ctrl_data_write(addr, rem_word_count, data);
+  }
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4False,
+      .erase = kMultiBitBool4False,
+  });
+
+  HARDENED_RETURN_IF_ERROR(err_0);
+  return err_1;
+}
+
+/**
+ * Bootstrap state 1: Wait for an erase command and erase the data
+ * partition.
+ *
+ * This function erases both data banks of the flash regardless of the type of
+ * the erase command (CHIP_ERASE or SECTOR_ERASE).
+ *
+ * @param state Bootstrap state.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_handle_erase(bootstrap_state_t *state) {
+  HARDENED_CHECK_EQ(*state, kBootstrapStateErase);
+
+  spi_device_cmd_t cmd;
+  RETURN_IF_ERROR(spi_device_cmd_get(&cmd));
+  // Erase requires WREN, ignore if WEL is not set.
+  if (!bitfield_bit32_read(spi_device_flash_status_get(), kSpiDeviceWelBit)) {
+    return kErrorOk;
+  }
+
+  rom_error_t error = kErrorUnknown;
+  switch (cmd.opcode) {
+    case kSpiDeviceOpcodeChipErase:
+    case kSpiDeviceOpcodeSectorErase:
+      error = bootstrap_chip_erase();
+      HARDENED_RETURN_IF_ERROR(error);
+      *state = kBootstrapStateEraseVerify;
+      // Note: We clear WIP and WEN bits in `bootstrap_handle_erase_verify()`
+      // after checking that both data banks have been erased.
+      break;
+    default:
+      // Ignore any other command, e.g. PAGE_PROGRAM, RESET, and clear WIP and
+      // WEN bits right away.
+      spi_device_flash_status_clear();
+      error = kErrorOk;
+  }
+
+  return error;
+}
+
+/**
+ * Bootstrap state 2: Verify that all data banks have been erased.
+ *
+ * This function also clears the WIP and WEN bits of the flash status register.
+ *
+ * @param state Bootstrap state.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_handle_erase_verify(bootstrap_state_t *state) {
+  HARDENED_CHECK_EQ(*state, kBootstrapStateEraseVerify);
+
+  rom_error_t err_0 = flash_ctrl_data_erase_verify(0, kFlashCtrlEraseTypeBank);
+  rom_error_t err_1 = flash_ctrl_data_erase_verify(
+      FLASH_CTRL_PARAM_BYTES_PER_BANK, kFlashCtrlEraseTypeBank);
+  HARDENED_RETURN_IF_ERROR(err_0);
+  HARDENED_RETURN_IF_ERROR(err_1);
+
+  *state = kBootstrapStateProgram;
+  spi_device_flash_status_clear();
+  return err_0;
+}
+
+/**
+ * Bootstrap state 3: (Erase/)Program loop.
+ *
+ * @param state Bootstrap state.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_handle_program(bootstrap_state_t *state) {
+  static_assert(alignof(spi_device_cmd_t) >= sizeof(uint32_t) &&
+                    offsetof(spi_device_cmd_t, payload) >= sizeof(uint32_t),
+                "Payload must be word aligned.");
+  static_assert(
+      sizeof((spi_device_cmd_t){0}.payload) % FLASH_CTRL_PARAM_BYTES_PER_WORD ==
+          0,
+      "Payload size must be a multiple of flash word size.");
+
+  HARDENED_CHECK_EQ(*state, kBootstrapStateProgram);
+
+  spi_device_cmd_t cmd;
+  RETURN_IF_ERROR(spi_device_cmd_get(&cmd));
+  // Erase and program require WREN, ignore if WEL is not set.
+  if (cmd.opcode != kSpiDeviceOpcodeReset &&
+      !bitfield_bit32_read(spi_device_flash_status_get(), kSpiDeviceWelBit)) {
+    return kErrorOk;
+  }
+
+  rom_error_t error = kErrorUnknown;
+  switch (cmd.opcode) {
+    case kSpiDeviceOpcodeChipErase:
+      error = bootstrap_chip_erase();
+      break;
+    case kSpiDeviceOpcodeSectorErase:
+      error = bootstrap_sector_erase(cmd.address);
+      break;
+    case kSpiDeviceOpcodePageProgram:
+      error = bootstrap_page_program(cmd.address, cmd.payload_byte_count,
+                                     cmd.payload);
+      break;
+    case kSpiDeviceOpcodeReset:
+      rstmgr_reset();
+#ifdef OT_PLATFORM_RV32
+      HARDENED_UNREACHABLE();
+#else
+      // If this is an off-target test, return `kErrorUnknown` to be able to
+      // test without requiring EXPECT_DEATH.
+      error = kErrorUnknown;
+#endif
+      break;
+    default:
+      // We don't expect any other commands but we can potentially end up
+      // here with a 0x0 opcode due to glitches on SPI or strap lines (see
+      // #11871).
+      error = kErrorOk;
+  }
+  HARDENED_RETURN_IF_ERROR(error);
+
+  spi_device_flash_status_clear();
+  return error;
+}
+
+hardened_bool_t bootstrap_requested(void) {
+#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);
+#endif
+  if (launder32(res) != kHardenedBoolTrue) {
+    return kHardenedBoolFalse;
+  }
+  HARDENED_CHECK_EQ(res, kHardenedBoolTrue);
+
+  // 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;
+  if (launder32(res) != kHardenedBoolTrue) {
+    return kHardenedBoolFalse;
+  }
+  HARDENED_CHECK_EQ(res, kHardenedBoolTrue);
+  return res;
+}
+
+rom_error_t bootstrap(void) {
+  hardened_bool_t requested = bootstrap_requested();
+  if (launder32(requested) != kHardenedBoolTrue) {
+    return kErrorBootstrapNotRequested;
+  }
+  HARDENED_CHECK_EQ(requested, kHardenedBoolTrue);
+
+  spi_device_init();
+
+  // Bootstrap event loop.
+  bootstrap_state_t state = kBootstrapStateErase;
+  rom_error_t error = kErrorUnknown;
+  while (true) {
+    switch (launder32(state)) {
+      case kBootstrapStateErase:
+        HARDENED_CHECK_EQ(state, kBootstrapStateErase);
+        error = bootstrap_handle_erase(&state);
+        break;
+      case kBootstrapStateEraseVerify:
+        HARDENED_CHECK_EQ(state, kBootstrapStateEraseVerify);
+        error = bootstrap_handle_erase_verify(&state);
+        break;
+      case kBootstrapStateProgram:
+        HARDENED_CHECK_EQ(state, kBootstrapStateProgram);
+        error = bootstrap_handle_program(&state);
+        break;
+      default:
+        error = kErrorBootstrapInvalidState;
+    }
+    HARDENED_RETURN_IF_ERROR(error);
+  }
+  HARDENED_UNREACHABLE();
+}
diff --git a/sw/device/silicon_creator/rom/bootstrap.h b/sw/device/silicon_creator/rom/bootstrap.h
new file mode 100644
index 0000000..fd4c725
--- /dev/null
+++ b/sw/device/silicon_creator/rom/bootstrap.h
@@ -0,0 +1,48 @@
+// 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
+
+#ifndef SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_
+#define SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_
+
+#include "sw/device/lib/base/hardened.h"
+#include "sw/device/silicon_creator/lib/error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Checks whether bootstrap is requested.
+ *
+ * The return value of this function also depends on the `ROM_BOOTSTRAP_EN` OTP
+ * item.
+ *
+ * @return Whether bootstrap is requested.
+ */
+hardened_bool_t bootstrap_requested(void);
+
+/**
+ * Bootstraps the data partition of the embedded flash with data received by the
+ * spi_device.
+ *
+ * OpenTitan bootstrap uses the typical SPI flash EEPROM commands. A typical
+ * bootstrap session involves:
+ * - Asserting bootstrap pins to enter bootstrap mode,
+ * - Erasing the chip (WREN, CHIP_ERASE, busy loop ...),
+ * - Programming the chip (WREN, PAGE_PROGRAM, busy loop ...), and
+ * - Resetting the chip (RESET).
+ *
+ * This function only returns on error since a successful bootstrap ends with a
+ * chip reset.
+ *
+ * @return Result of the operation.
+ */
+rom_error_t bootstrap(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_
diff --git a/sw/device/tests/BUILD b/sw/device/tests/BUILD
index fc00761..21913dc 100644
--- a/sw/device/tests/BUILD
+++ b/sw/device/tests/BUILD
@@ -670,7 +670,7 @@
     deps = [
         ":test_lib_base",
         "//sw/device/lib/testing/test_rom:linker_script",
-        "//sw/device/lib/testing/test_rom:test_rom_lib",
+        "//sw/device/lib/testing/test_rom:test_rom_otp_lib",
     ],
 )
 
@@ -994,10 +994,9 @@
 
 matcha_dv_test(
     name = "clkmgr_reset_frequency_test",
-    srcs = ["@lowrisc_opentitan//sw/device/tests:clkmgr_reset_frequency_test.c"],
+    srcs = [":clkmgr_reset_frequency_test.c"],
     deps = [
-        ":test_dv_lib_opentitan",
-        "//sw/device/lib/arch:device",
+        ":test_dv_lib",
         "//sw/device/lib/testing:clkmgr_testutils",
         "@lowrisc_opentitan//sw/device/lib/dif:sensor_ctrl",
         "@lowrisc_opentitan//sw/device/lib/testing:aon_timer_testutils",
diff --git a/sw/device/tests/clkmgr_reset_frequency_test.c b/sw/device/tests/clkmgr_reset_frequency_test.c
new file mode 100644
index 0000000..1a574af
--- /dev/null
+++ b/sw/device/tests/clkmgr_reset_frequency_test.c
@@ -0,0 +1,92 @@
+// 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
+
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "sw/device/lib/base/memory.h"
+#include "sw/device/lib/dif/dif_sensor_ctrl.h"
+#include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/testing/aon_timer_testutils.h"
+#include "sw/device/lib/testing/clkmgr_testutils.h"
+#include "sw/device/lib/testing/rstmgr_testutils.h"
+#include "sw/device/lib/testing/sensor_ctrl_testutils.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/test_framework/ottf_main.h"
+
+OTTF_DEFINE_TEST_CONFIG();
+
+/**
+ * This test measure clock counts with clkmgr frequency measurements, performing
+ * 100 measurements per round. Measurement errors (fast or slow clocks) are
+ * recorded as recoverable error in clkmgr.
+ *
+ * This test configures the clock thresholds so it generates errors, after 100
+ * measurements it checks that some errors are found, then a reset is triggered,
+ * and it checks the measurements should be disabled, and no errors should be
+ * reported.
+ *
+ * Notice the test overrides the hardware behavior so it comes out with
+ * calibrated USB clock, otherwise the USB clock frequency will be incorrect.
+ * USB calibration should be a separate test, and may be vendor-specific.
+ */
+const unsigned int kMeasurementsPerRound = 100;
+
+bool test_main(void) {
+  dif_clkmgr_t clkmgr;
+  dif_rstmgr_t rstmgr;
+  dif_sensor_ctrl_t sensor_ctrl;
+
+  const uint32_t kMeasurementDelayMicros =
+      aon_timer_testutils_get_us_from_aon_cycles(kMeasurementsPerRound);
+
+  CHECK_DIF_OK(dif_clkmgr_init(
+      mmio_region_from_addr(TOP_MATCHA_CLKMGR_AON_BASE_ADDR), &clkmgr));
+  CHECK_DIF_OK(dif_sensor_ctrl_init(
+      mmio_region_from_addr(TOP_MATCHA_SENSOR_CTRL_BASE_ADDR), &sensor_ctrl));
+  CHECK_DIF_OK(dif_rstmgr_init(
+      mmio_region_from_addr(TOP_MATCHA_RSTMGR_AON_BASE_ADDR), &rstmgr));
+
+  LOG_INFO("TEST: wait for ast init");
+  IBEX_SPIN_FOR(sensor_ctrl_ast_init_done(&sensor_ctrl),
+                1000);  // Time out at 1000us
+  LOG_INFO("TEST: done ast init");
+
+  if (rstmgr_testutils_reset_info_any(&rstmgr, kDifRstmgrResetInfoPor)) {
+    LOG_INFO("POR reset");
+
+    // Configure the counters to trigger an error by setting them for external
+    // clocks.
+    clkmgr_testutils_enable_clock_counts_with_expected_thresholds(
+        &clkmgr, /*jitter_enabled=*/false, /*external_clk=*/true,
+        /*low_speed=*/true);
+    busy_spin_micros(kMeasurementDelayMicros);
+
+    // Check we get errors, but let the counters keep going.
+    dif_clkmgr_recov_err_codes_t err_codes;
+    CHECK_DIF_OK(dif_clkmgr_recov_err_code_get_codes(&clkmgr, &err_codes));
+    CHECK(err_codes != 0);
+
+    // Trigger a rstmgr SW reset.
+    CHECK_DIF_OK(dif_rstmgr_software_device_reset(&rstmgr));
+
+    // Add delay such that the software reset can take effect
+    busy_spin_micros(100);
+
+    // This should not be reached.
+    LOG_ERROR("This is unreachable since a reset should have been triggered");
+    return false;
+  } else if (rstmgr_testutils_reset_info_any(&rstmgr, kDifRstmgrResetInfoSw)) {
+    LOG_INFO("Back from rstmgr SW reset");
+    CHECK(clkmgr_testutils_check_measurement_enables(&clkmgr,
+                                                     kDifToggleDisabled));
+    CHECK(clkmgr_testutils_check_measurement_counts(&clkmgr));
+    return true;
+  } else {
+    dif_rstmgr_reset_info_bitfield_t rst_info;
+    CHECK_DIF_OK(dif_rstmgr_reset_info_get(&rstmgr, &rst_info));
+    LOG_ERROR("Unexpected rst_info 0x%x", rst_info);
+    return false;
+  }
+  return true;
+}
diff --git a/sw/device/tests/kelvin/fpga_tests/BUILD b/sw/device/tests/kelvin/fpga_tests/BUILD
index 5c0f975..364d0cf 100644
--- a/sw/device/tests/kelvin/fpga_tests/BUILD
+++ b/sw/device/tests/kelvin/fpga_tests/BUILD
@@ -11,8 +11,6 @@
     name = "kelvin_test_sc_extflash",
     srcs = [
         "kelvin_test_sc.c",
-        "kelvin_test_sc_loaders.h",
-        "kelvin_test_sc_loaders_extflash.c",
     ],
     copts = [
         "-nostdlib",
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 d27bf6f..0031960 100644
--- a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc.c
+++ b/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc.c
@@ -6,11 +6,11 @@
 #include "sw/device/lib/dif/dif_smc_ctrl.h"
 #include "sw/device/lib/dif/dif_uart.h"
 #include "sw/device/lib/runtime/print.h"
+#include "sw/device/lib/spi_flash.h"
 #include "sw/device/lib/testing/test_framework/check.h"
 #include "sw/device/lib/testing/test_framework/ottf_test_config.h"
 #include "sw/device/lib/testing/test_framework/status.h"
 #include "sw/device/lib/testing/test_framework/test_util.h"
-#include "sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders.h"
 
 /*
  * Sample program to run some code on all cores in the system.
@@ -28,11 +28,12 @@
   test_status_set(kTestStatusInTest);
   init_uart(TOP_MATCHA_UART0_BASE_ADDR, &uart);
   LOG_INFO("kelvin_test_sc");
-  // Do any initialization needed before loading SMC and Kelvin.
-  load_init();
+  spi_flash_init();
 
   // Copy binary to SMC RAM.
-  load_smc();
+  CHECK_DIF_OK(load_file_from_tar(
+      "smc.bin", (void*)TOP_MATCHA_RAM_SMC_BASE_ADDR,
+      (TOP_MATCHA_RAM_SMC_BASE_ADDR + TOP_MATCHA_RAM_SMC_SIZE_BYTES)));
 
   mmio_region_t ml_dmem =
       mmio_region_from_addr(TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR);
@@ -43,34 +44,64 @@
     mmio_region_write32(ml_dmem, i * sizeof(uint32_t), 0);
   }
 
-  // Copy binary to ML_DMEM.
-  load_kelvin();
-
   // Enable SMC.
   CHECK_DIF_OK(dif_smc_ctrl_init(
       mmio_region_from_addr(TOP_MATCHA_SMC_CTRL_BASE_ADDR), &smc_ctrl));
   CHECK_DIF_OK(dif_smc_ctrl_set_en(&smc_ctrl));
 
-  // Start up Kelvin.
+  // Reset and Freeze Kelvin.
   mmio_region_t base_addr =
       mmio_region_from_addr(TOP_MATCHA_ML_TOP_CORE_BASE_ADDR);
   mmio_region_write32(base_addr, ML_TOP_CTRL_REG_OFFSET,
-                      ML_TOP_CTRL_REG_RESVAL);
-  mmio_region_write32(base_addr, ML_TOP_CTRL_REG_OFFSET, 0x0);
-  uint32_t intr_state =
-      mmio_region_read32(base_addr, ML_TOP_INTR_STATE_REG_OFFSET);
-  // TODO(ykwang): Change this to interrupt handler.
-  while (intr_state == 0x0) {
+                      ML_TOP_CTRL_REG_RESVAL | (1 << ML_TOP_CTRL_FREEZE_BIT));
+
+  // Copy binary to ML_DMEM.
+  char kelvin_bin_name[256] = {'\0'};
+  size_t bin_offset = 0;
+  dif_result_t kelvin_load_result;
+  int test_num = 0;
+  int failed_test = 0;
+  while ((kelvin_load_result = load_file_prefix_from_tar(
+              "kelvin_", (void*)TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR,
+              (TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR +
+               TOP_MATCHA_ML_TOP_DMEM_SIZE_BYTES),
+              &bin_offset, kelvin_bin_name, sizeof(kelvin_bin_name))) !=
+         kDifOutOfRange) {
+    CHECK_DIF_OK(kelvin_load_result, "Failed to load %s", kelvin_bin_name);
+    test_num++;
+    LOG_INFO("Test %d: Run kelvin binary %s", test_num, kelvin_bin_name);
+    // Start up Kelvin.
+    mmio_region_write32(base_addr, ML_TOP_CTRL_REG_OFFSET, 0x0);
+    uint32_t intr_state =
+        mmio_region_read32(base_addr, ML_TOP_INTR_STATE_REG_OFFSET);
+    // TODO(ykwang): Change this to interrupt handler.
+    while (intr_state == 0x0) {
+      busy_spin_micros(10 * 1000);  // Wait for 10ms.
+      intr_state = mmio_region_read32(base_addr, ML_TOP_INTR_STATE_REG_OFFSET);
+    }
+    // Received interrupts from Kelvin core, check if only FINISH asserted
+    if (intr_state != (1 << ML_TOP_INTR_STATE_FINISH_BIT)) {
+      LOG_ERROR("INTR_STATE read out: expected : 0x%x | actual: 0x%x",
+                (1 << ML_TOP_INTR_STATE_FINISH_BIT), intr_state);
+      failed_test++;
+    }
+    // Reset and halt Kelvin
+    mmio_region_write32(base_addr, ML_TOP_CTRL_REG_OFFSET,
+                        ML_TOP_CTRL_REG_RESVAL | (1 << ML_TOP_CTRL_FREEZE_BIT));
+    // Write 1 to clear INTR_STATE
+    mmio_region_write32(base_addr, ML_TOP_INTR_STATE_REG_OFFSET, intr_state);
     intr_state = mmio_region_read32(base_addr, ML_TOP_INTR_STATE_REG_OFFSET);
-    busy_spin_micros(10 * 1000);  // Wait for 10ms.
+    CHECK(
+        intr_state == 0,
+        "ML_TOP_Core offset 0 INTR_STATE  read  again - Expected: 0 | Actual: "
+        "0x%x",
+        intr_state);
   }
-
-  LOG_INFO("Kelvin finished executing.");
-  // Received interrupts from Kelvin core, check if only FINISH asserted
-  CHECK(intr_state == (1 << ML_TOP_INTR_STATE_FINISH_BIT),
-        "INTR_STATE read out: expected : 0x%x | actual: 0x%x",
-        (1 << ML_TOP_INTR_STATE_FINISH_BIT), intr_state);
-
-  test_status_set(kTestStatusPassed);
+  LOG_INFO("Total test: %d, failed test: %d", test_num, failed_test);
+  if (failed_test == 0) {
+    test_status_set(kTestStatusPassed);
+  } else {
+    test_status_set(kTestStatusFailed);
+  }
   asm volatile("wfi");
 }
diff --git a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders.h b/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders.h
deleted file mode 100644
index fd0161f..0000000
--- a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders.h
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2023 Google LLC
-
-#ifndef SW_DEVICE_TESTS_KELVIN_FPGA_TESTS_KELVIN_TEST_SC_LOADERS_H_
-#define SW_DEVICE_TESTS_KELVIN_FPGA_TESTS_KELVIN_TEST_SC_LOADERS_H_
-
-#if defined(__cplusplus)
-extern "C" {
-#endif
-
-void load_init(void);
-void load_smc(void);
-void load_kelvin(void);
-
-#if defined(__cplusplus)
-}
-#endif
-
-#endif  // SW_DEVICE_TESTS_KELVIN_FPGA_TESTS_KELVIN_TEST_SC_LOADERS_H_
diff --git a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders_extflash.c b/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders_extflash.c
deleted file mode 100644
index 15abee6..0000000
--- a/sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders_extflash.c
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2023 Google LLC
-
-#include "hw/top_matcha/sw/autogen/top_matcha.h"
-#include "sw/device/lib/spi_flash.h"
-#include "sw/device/lib/testing/test_framework/check.h"
-#include "sw/device/tests/kelvin/fpga_tests/kelvin_test_sc_loaders.h"
-
-void load_smc(void) {
-  CHECK_DIF_OK(
-      load_file_from_tar("smc.bin", (void*)TOP_MATCHA_RAM_SMC_BASE_ADDR));
-}
-
-void load_kelvin(void) {
-  CHECK_DIF_OK(load_file_from_tar("kelvin.bin",
-                                  (void*)TOP_MATCHA_ML_TOP_DMEM_BASE_ADDR));
-}
-
-void load_init(void) { spi_flash_init(); }
diff --git a/third_party/kelvin-binary/BUILD b/third_party/kelvin-binary/BUILD
index 8f15b4e..beb141e 100644
--- a/third_party/kelvin-binary/BUILD
+++ b/third_party/kelvin-binary/BUILD
@@ -5,12 +5,16 @@
 
 load("@matcha//rules:matcha.bzl", "matcha_extflash_tar")
 
-[matcha_extflash_tar(
-    name = "kelvin_fpga_test_{}_extflash".format(test[:-4]),
-    kelvin_binary = "@kelvin-binary//:{}".format(test),
+tarball_data = {}
+
+[tarball_data.update({"@kelvin-binary//:{}".format(test): "kelvin_{}".format(test)}) for test in glob(["*.bin"])]
+
+matcha_extflash_tar(
+    name = "kelvin_fpga_test_extflash",
     sc_binary = "@matcha//sw/device/tests/kelvin/fpga_tests:kelvin_test_sc_extflash_fpga_nexus_bin",
     smc_binary = "@matcha//sw/device/tests/kelvin/fpga_tests:kelvin_test_smc_fpga_nexus_bin",
+    data = tarball_data,
     tags = [
         "kelvin_fpga",
     ],
-) for test in glob(["*.bin"])]
+)