[mask_rom] Add mask ROM ePMP functional test
Adds a test ROM that shares the same ePMP configuration as the mask
ROM. It takes about 30-60s to execute in verilator on my laptop.
The test attempts to execute code in RAM, eFlash and the read-only
part of the ROM to check that instruction access faults are
generated correctly. It also tests that the region unlocked by
a call to `mask_rom_epmp_unlock_rom_ext_rx` is correct.
Blocking unwanted execution is the primary purpose of the ePMP module.
For now the test does not check other types of access are allowed
or blocked. It may be desirable to add other tests for these
accesses (e.g. a stack overflow test for the stack guard) later.
The test is unusual in that it executes from ROM and does not
communicate any status information until the test is over (because
the address space used by DV is blocked by the ePMP configuration
that is being tested).
Disabling the ePMP setup code results in the following output:
```
I00000 mask_rom_epmp_test.c:358] Starting MaskROM ePMP functional test.
E00001 mask_rom_epmp_test.c:363] CHECK-fail: epmp_state_check(&epmp) == kErrorOk
E00002 mask_rom_epmp_test.c:251] CHECK-fail: execute(illegal_ins_ro, kExceptionInstructionAccessFault)
E00003 mask_rom_epmp_test.c:263] CHECK-fail: execute(illegal_ins_rw, kExceptionInstructionAccessFault)
E00004 mask_rom_epmp_test.c:275] CHECK-fail: execute(&eflash[0], kExceptionInstructionAccessFault)
E00005 mask_rom_epmp_test.c:276] CHECK-fail: execute(&eflash[eflash_len - 1], kExceptionInstructionAccessFault)
E00006 mask_rom_epmp_test.c:283] eflash execution not blocked @ 0x20000418
E00007 mask_rom_epmp_test.c:331] CHECK-fail: epmp_state_check(epmp) == kErrorOk
E00008 mask_rom_epmp_test.c:342] CHECK-fail: execute(&image[-1], kExceptionInstructionAccessFault)
E00009 mask_rom_epmp_test.c:343] CHECK-fail: execute(&image[image_len], kExceptionInstructionAccessFault)
E00010 mask_rom_epmp_test.c:378] CHECK-fail: epmp_unlock_test_status(&epmp)
I00011 test_status.c:34] FAIL!
```
Signed-off-by: Michael Munday <mike.munday@lowrisc.org>
diff --git a/sw/device/silicon_creator/mask_rom/mask_rom_epmp_test.c b/sw/device/silicon_creator/mask_rom/mask_rom_epmp_test.c
new file mode 100644
index 0000000..084f4da
--- /dev/null
+++ b/sw/device/silicon_creator/mask_rom/mask_rom_epmp_test.c
@@ -0,0 +1,403 @@
+// 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/mask_rom/mask_rom_epmp.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "sw/device/lib/arch/device.h"
+#include "sw/device/lib/base/csr.h"
+#include "sw/device/lib/base/memory.h"
+#include "sw/device/lib/base/stdasm.h"
+#include "sw/device/lib/pinmux.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/testing/test_status.h"
+#include "sw/device/silicon_creator/lib/base/abs_mmio.h"
+#include "sw/device/silicon_creator/lib/drivers/uart.h"
+#include "sw/device/silicon_creator/lib/epmp_test_unlock.h"
+#include "sw/device/silicon_creator/mask_rom/mask_rom_epmp.h"
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
+#include "sram_ctrl_regs.h" // Generated.
+
+/**
+ * Mask ROM ePMP test.
+ *
+ * This test uses the mask ROM linker script and ePMP setup code to initialize
+ * its own ePMP configuration and then attempts to execute instructions in
+ * various address spaces. Typically execution in these address spaces should be
+ * blocked unless the unlock function has been called with a region containing
+ * the address of the access.
+ */
+
+/**
+ * Exception types that may be encountered.
+ *
+ * TODO(#7190): use global definitions instead.
+ */
+typedef enum exception {
+ kExceptionNone = -1,
+ kExceptionInstructionAccessFault = 1,
+ kExceptionIllegalInstruction = 2,
+ kExceptionBreakpoint = 3,
+ kExceptionLoadAccessFault = 5,
+ kExceptionStoreAccessFault = 7,
+ kExceptionECallFromUMode = 8,
+ kExceptionECallFromMMode = 11,
+} exception_t;
+
+/**
+ * Get the value of the `mcause` register.
+ *
+ * @returns The encoded interrupt or exception cause.
+ */
+static uint32_t get_mcause(void) {
+ uint32_t mcause;
+ CSR_READ(CSR_REG_MCAUSE, &mcause);
+ return mcause;
+}
+
+/**
+ * Get the value of the `mepc` register.
+ *
+ * @returns The value of the machine exception program counter.
+ */
+static uint32_t get_mepc(void) {
+ uint32_t mepc;
+ CSR_READ(CSR_REG_MEPC, &mepc);
+ return mepc;
+}
+
+/**
+ * Set the value of the `mepc` register.
+ *
+ * After an exception has been handled execution will be resumed at the address
+ * contained within `mepc`.
+ *
+ * @param pc The value to set the machine exception program counter to.
+ */
+static void set_mepc(uint32_t pc) { CSR_WRITE(CSR_REG_MEPC, pc); }
+
+/**
+ * Interrupt handlers.
+ *
+ * If operating correctly this test should only trigger exceptions. Interrupts
+ * are therefore not recovered.
+ */
+void mask_rom_nmi_handler(void) { wait_for_interrupt(); }
+void mask_rom_interrupt_handler(void) { wait_for_interrupt(); }
+
+/**
+ * The type of last exception (if any) received.
+ *
+ * Set by the exception handler.
+ */
+volatile exception_t exception_received = kExceptionNone;
+
+/**
+ * The `mepc` value for the last exception (if any) received.
+ *
+ * Set by the exception handler.
+ */
+volatile uintptr_t exception_pc = 0;
+
+/**
+ * Exception handler.
+ *
+ * Handle instruction access faults and illegal instructions by setting
+ * `exception_received` and `exception_pc` and then returning to the code
+ * that jumped (via a call) to the offending instruction.
+ *
+ * This will likely only work correctly if the instruction exception was
+ * caused by a jump from `execute` to an invalid instruction (whether illegal
+ * or inaccessible).
+ *
+ * For all other exceptions hang (could also shutdown) so as not to hide them.
+ */
+void mask_rom_exception_handler(void) {
+ uint32_t mcause = get_mcause();
+ if (mcause == kExceptionInstructionAccessFault ||
+ mcause == kExceptionIllegalInstruction) {
+ exception_received = (exception_t)mcause;
+ exception_pc = get_mepc();
+
+ // Return to caller.
+ uintptr_t ret = (uintptr_t)__builtin_return_address(0);
+ set_mepc((uint32_t)ret);
+ return;
+ }
+
+ // Wait forever if an unexpected exception is encountered.
+ wait_for_interrupt();
+}
+
+/**
+ * Attempt to execute the code at `pc` by calling it like a function.
+ *
+ * Typically the contents of `pc` should be an invalid instruction such
+ * as an all zero value. In this case if execution was blocked by PMP an
+ * instruction fault exception will be raised. If however execution was
+ * allowed then an illegal instruction exception will be raised instead.
+ *
+ * The interrupt handler will arrange for control to be returned to the
+ * caller on encountering either an instruction fault or illegal
+ * instruction error so this function will report a result in either
+ * case.
+ *
+ * @param pc The address of the instruction to try and execute.
+ * @param expect The expected exception that will be raised.
+ * @returns Whether the expected exception was raised at the correct PC.
+ */
+static bool execute(const void *pc, exception_t expect) {
+ exception_pc = 0;
+ exception_received = kExceptionNone;
+
+ // Jump to the target PC.
+ //
+ // Using a `call` here (`jal` or `jalr`) sets the return address (`ra`)
+ // register. When an exception is raised the interrupt handler will recover
+ // by restarting execution at the address in `ra` thereby making it appear
+ // as if the call returned normally.
+ //
+ // ...
+ // jal ra, pc # <- Set return address and jump to pc.
+ // ... # <- Interrupt handler restarts execution at the next
+ // # instruction in the caller, here.
+ //
+ // pc:
+ // unimp # <- Illegal instruction or access fault. Enter interrupt
+ // handler.
+ //
+ ((void (*)(void))pc)();
+
+ // Be careful to ensure that the exception was raised when trying to
+ // execute `pc` just in case a valid instruction is actually executed
+ // and then execution continued to a point where an exception is
+ // raised.
+ if (exception_received != kExceptionNone && exception_pc != (uintptr_t)pc) {
+ return false;
+ }
+ return exception_received == expect;
+}
+
+/**
+ * An instruction that has no bits set.
+ *
+ * Attempts to execute this instruction, `unimp`, will result in an illegal
+ * instruction exception.
+ *
+ * Note that if compressed instructions are enabled only the first two bytes
+ * will be decoded (as `c.unimp`).
+ */
+static const uint32_t kUnimpInstruction = 0;
+
+/**
+ * Illegal instruction residing in .rodata.
+ */
+static const uint32_t illegal_ins_ro[] = {
+ kUnimpInstruction,
+};
+
+/**
+ * Illegal instruction residing in .data.
+ */
+static uint32_t illegal_ins_rw[] = {
+ kUnimpInstruction,
+};
+
+/**
+ * Report whether the given pointer points to a location with the provided
+ * address space.
+ *
+ * @param ptr Pointer to test.
+ * @param start Address of the start of the address space.
+ * @param size The size of the address space in bytes.
+ * @returns Whether the pointer is in the address space.
+ */
+static bool is_in_address_space(const void *ptr, uintptr_t start,
+ uintptr_t size) {
+ return (uintptr_t)ptr >= start && (uintptr_t)ptr < (start + size);
+}
+
+/**
+ * Enable execution of SRAM for the given controller.
+ *
+ * @param ctrl_addr Base address for SRAM controller.
+ * @returns Whether execution was enabled successfully or not.
+ */
+static bool sram_exec_enable(uint32_t ctrl_addr) {
+ // TODO: use the SRAM driver or DIF when available.
+ const uint32_t kEnableExecution = 5;
+ abs_mmio_write32(ctrl_addr + SRAM_CTRL_EXEC_REG_OFFSET, kEnableExecution);
+ return abs_mmio_read32(ctrl_addr + SRAM_CTRL_EXEC_REG_OFFSET) ==
+ kEnableExecution;
+}
+
+/**
+ * Set to false if a test fails.
+ */
+static bool passed = true;
+
+/**
+ * Custom CHECK macro to assert a condition that if false should cause the
+ * test to fail. Note: we can't use the normal CHECK macro because it tries to
+ * write to the DV address space but that is locked by the ePMP configuration.
+ */
+#define CHECK(condition) \
+ if (!(condition)) { \
+ LOG_ERROR("CHECK-fail: " #condition); \
+ passed = false; \
+ }
+
+/**
+ * Test that .rodata in the ROM is not executable.
+ */
+static void test_noexec_rodata(void) {
+ CHECK(is_in_address_space(illegal_ins_ro, TOP_EARLGREY_ROM_CTRL_ROM_BASE_ADDR,
+ TOP_EARLGREY_ROM_CTRL_ROM_SIZE_BYTES));
+ CHECK(execute(illegal_ins_ro, kExceptionInstructionAccessFault));
+}
+
+/**
+ * Test that the .data section in RAM is not executable.
+ */
+static void test_noexec_rwdata(void) {
+ if (!sram_exec_enable(TOP_EARLGREY_SRAM_CTRL_MAIN_BASE_ADDR)) {
+ base_printf("failed to enable main RAM execution\n");
+ }
+ CHECK(is_in_address_space(illegal_ins_rw, TOP_EARLGREY_RAM_MAIN_BASE_ADDR,
+ TOP_EARLGREY_RAM_MAIN_SIZE_BYTES));
+ CHECK(execute(illegal_ins_rw, kExceptionInstructionAccessFault));
+}
+
+/**
+ * Test that eFlash is not executable.
+ */
+static void test_noexec_eflash(void) {
+ // Ideally we'd check all of eFlash but that takes a very long time in
+ // simulation. Instead, check the first and last words are not executable and
+ // check a sample of other addresses.
+ uint32_t *eflash = (uint32_t *)TOP_EARLGREY_EFLASH_BASE_ADDR;
+ size_t eflash_len = TOP_EARLGREY_EFLASH_SIZE_BYTES / sizeof(eflash[0]);
+ CHECK(execute(&eflash[0], kExceptionInstructionAccessFault));
+ CHECK(execute(&eflash[eflash_len - 1], kExceptionInstructionAccessFault));
+
+ // Step size is picked arbitrarily but should provide a reasonable sample of
+ // addresses.
+ size_t step = eflash_len / 999;
+ for (size_t i = step; i < eflash_len; i += step) {
+ if (!execute(&eflash[i], kExceptionInstructionAccessFault)) {
+ LOG_ERROR("eflash execution not blocked @ %p", &eflash[i]);
+ passed = false;
+ break;
+ }
+ }
+}
+
+/**
+ * Test that the MMIO address space (specifically the retention RAM) is not
+ * executable.
+ */
+static void test_noexec_mmio(void) {
+ // Note: execution of retention RAM always fails regardless of controller or
+ // ePMP configurations however it doesn't hurt to check it anyway.
+ if (!sram_exec_enable(TOP_EARLGREY_SRAM_CTRL_RET_AON_BASE_ADDR)) {
+ base_printf("failed to enable retention RAM execution\n");
+ }
+ uint32_t *ret_ram = (uint32_t *)TOP_EARLGREY_RAM_RET_AON_BASE_ADDR;
+ size_t ret_ram_len = TOP_EARLGREY_RAM_RET_AON_SIZE_BYTES / sizeof(ret_ram[0]);
+ ret_ram[0] = kUnimpInstruction;
+ CHECK(execute(&ret_ram[0], kExceptionInstructionAccessFault));
+ ret_ram[ret_ram_len - 1] = kUnimpInstruction;
+ CHECK(execute(&ret_ram[ret_ram_len - 1], kExceptionInstructionAccessFault));
+}
+
+/**
+ * Test the function used to unlock execution of the ROM extension.
+ *
+ * Unlock a section of eFlash to simulate the unlocking of the ROM_EXT text.
+ * Accesses within the unlocked region should execute (and generate an illegal
+ * instruction exception in this case) while accesses outside the unlocked
+ * region should still fail with an instruction access fault exception.
+ *
+ * @param epmp The ePMP state to update.
+ */
+static void test_unlock_exec_eflash(epmp_state_t *epmp) {
+ // Define a region to unlock (this is somewhat arbitrary but must be word-
+ // aligned).
+ uint32_t *eflash = (uint32_t *)TOP_EARLGREY_EFLASH_BASE_ADDR;
+ size_t eflash_len = TOP_EARLGREY_EFLASH_SIZE_BYTES / sizeof(eflash[0]);
+ uint32_t *image = &eflash[eflash_len / 5];
+ size_t image_len = eflash_len / 7;
+ epmp_region_t region = {.start = (uintptr_t)&image[0],
+ .end = (uintptr_t)&image[image_len]};
+
+ // Unlock execution of the region and check that the same changes are made
+ // to the ePMP state.
+ mask_rom_epmp_unlock_rom_ext_rx(epmp, region);
+ CHECK(epmp_state_check(epmp) == kErrorOk);
+
+ // Verify that execution within the region succeeds.
+ // The image must consist of `unimp` instructions so that an illegal
+ // instruction exception is generated.
+ CHECK(image[0] == kUnimpInstruction);
+ CHECK(execute(&image[0], kExceptionIllegalInstruction));
+ CHECK(image[image_len - 1] == kUnimpInstruction);
+ CHECK(execute(&image[image_len - 1], kExceptionIllegalInstruction));
+
+ // Verify that execution just outside the region still fails.
+ CHECK(execute(&image[-1], kExceptionInstructionAccessFault));
+ CHECK(execute(&image[image_len], kExceptionInstructionAccessFault));
+}
+
+void mask_rom_main(void) {
+ // Initialize pinmux configuration so we can use the UART.
+ pinmux_init();
+
+ // Configure UART0 as stdout.
+ uart_init(kUartNCOValue);
+ base_set_stdout((buffer_sink_t){
+ .data = NULL,
+ .sink = uart_sink,
+ });
+
+ // Start the tests.
+ LOG_INFO("Starting MaskROM ePMP functional test.");
+
+ // Initialize shadow copy of the ePMP register configuration.
+ epmp_state_t epmp;
+ mask_rom_epmp_state_init(&epmp);
+ CHECK(epmp_state_check(&epmp) == kErrorOk);
+
+ // Test that execution outside the ROM text is blocked by default.
+ test_noexec_rodata();
+ test_noexec_rwdata();
+ test_noexec_eflash();
+ test_noexec_mmio();
+
+ // Test that execution is unlocked for a sub-region of eFlash correctly.
+ // Simulates the unlocking of the ROM extension text.
+ test_unlock_exec_eflash(&epmp);
+
+ // The test of the mask ROM's ePMP configuration is now complete. Unlock the
+ // DV address space so that the test result can be reported. Assumes that PMP
+ // entry 6 is allocated for this purpose.
+ CHECK(epmp_unlock_test_status(&epmp));
+
+ // Report the test status.
+ //
+ // Note that it is only now, after the DV address space has been unlocked that
+ // we can signal that the test has started unfortunately.
+ test_status_set(kTestStatusInTest);
+ test_status_set(passed ? kTestStatusPassed : kTestStatusFailed);
+
+ // Unreachable if reporting the test status correctly caused the
+ // test to stop.
+ while (true) {
+ wait_for_interrupt();
+ }
+}
diff --git a/sw/device/silicon_creator/mask_rom/meson.build b/sw/device/silicon_creator/mask_rom/meson.build
index 9894c6c..f5961bd 100644
--- a/sw/device/silicon_creator/mask_rom/meson.build
+++ b/sw/device/silicon_creator/mask_rom/meson.build
@@ -51,6 +51,104 @@
)
)
+# Mask ROM ePMP test library.
+mask_rom_epmp_test_lib = declare_dependency(
+ sources: [
+ hw_ip_entropy_src_reg_h,
+ hw_ip_csrng_reg_h,
+ hw_ip_edn_reg_h,
+ 'mask_rom_start.S',
+ ],
+ link_args: rom_link_args,
+ dependencies: [
+ freestanding_headers,
+ sw_silicon_creator_lib_driver_uart,
+ sw_silicon_creator_lib_epmp_test_unlock,
+ sw_silicon_creator_lib_fake_deps,
+ sw_silicon_creator_mask_rom_epmp,
+ sw_lib_crt,
+ sw_lib_pinmux,
+ sw_lib_runtime_print,
+ sw_lib_testing_test_status,
+ ],
+ link_with: static_library(
+ 'mask_rom_epmp_test_lib',
+ sources: [
+ hw_ip_sram_ctrl_reg_h,
+ 'mask_rom_epmp_test.c',
+ ],
+ link_depends: [rom_linkfile],
+ )
+)
+
+# Mask ROM ePMP test images
+foreach device_name, device_lib : sw_lib_arch_core_devices
+ mask_rom_epmp_test_elf = executable(
+ 'mask_rom_epmp_test_' + device_name,
+ name_suffix: 'elf',
+ link_depends: rom_link_deps,
+ link_args: [
+ '-Wl,-Map=@0@/mask_rom_@1@.map'.format(meson.current_build_dir(), device_name),
+ ],
+ dependencies: [
+ device_lib,
+ mask_rom_epmp_test_lib,
+ ],
+ )
+
+ target_name = 'mask_rom_epmp_test_@0@_' + device_name
+
+ mask_rom_epmp_test_dis = custom_target(
+ target_name.format('dis'),
+ input: mask_rom_epmp_test_elf,
+ kwargs: elf_to_dis_custom_target_args,
+ )
+
+ mask_rom_epmp_test_bin = custom_target(
+ target_name.format('bin'),
+ input: mask_rom_epmp_test_elf,
+ kwargs: elf_to_bin_custom_target_args,
+ )
+
+ mask_rom_epmp_test_vmem32 = custom_target(
+ target_name.format('vmem32'),
+ input: mask_rom_epmp_test_bin,
+ kwargs: bin_to_vmem32_custom_target_args,
+ )
+
+ mask_rom_epmp_test_vmem64 = custom_target(
+ target_name.format('vmem64'),
+ input: mask_rom_epmp_test_bin,
+ kwargs: bin_to_vmem64_custom_target_args,
+ )
+
+ mask_rom_epmp_test_scrambled = custom_target(
+ target_name.format('scrambled'),
+ command: scramble_image_command,
+ depend_files: scramble_image_depend_files,
+ input: mask_rom_epmp_test_elf,
+ output: scramble_image_outputs,
+ build_by_default: true,
+ )
+
+ custom_target(
+ target_name.format('export'),
+ command: export_target_command,
+ depend_files: [export_target_depend_files,],
+ input: [
+ mask_rom_epmp_test_elf,
+ mask_rom_epmp_test_dis,
+ mask_rom_epmp_test_bin,
+ mask_rom_epmp_test_vmem32,
+ mask_rom_epmp_test_vmem64,
+ mask_rom_epmp_test_scrambled,
+ ],
+ output: target_name.format('export'),
+ build_always_stale: true,
+ build_by_default: true,
+ )
+endforeach
+
# MaskROM library.
mask_rom_lib = declare_dependency(
sources: [
diff --git a/test/systemtest/earlgrey/test_sim_verilator.py b/test/systemtest/earlgrey/test_sim_verilator.py
index 44b5f7a..c4c40ff 100644
--- a/test/systemtest/earlgrey/test_sim_verilator.py
+++ b/test/systemtest/earlgrey/test_sim_verilator.py
@@ -9,7 +9,7 @@
import pytest
-from .. import config, silicon_creator_config, utils
+from .. import config, silicon_creator_config, roms_config, utils
log = logging.getLogger(__name__)
@@ -179,6 +179,43 @@
return None
+@pytest.fixture(params=roms_config.TEST_ROMS_SELFCHECKING,
+ ids=lambda param: param['name'])
+def rom_selfchecking(request, bin_dir):
+ """ A self-checking ROM image for Verilator simulation
+
+ Returns:
+ A set (image_path, verilator_extra_args)
+ """
+
+ rom_config = request.param
+
+ if 'name' not in rom_config:
+ raise RuntimeError("Key 'name' not found in TEST_ROMS_SELFCHECKING")
+
+ if 'targets' in rom_config and 'sim_verilator' not in rom_config['targets']:
+ pytest.skip("Test %s skipped on Verilator." % rom_config['name'])
+
+ if 'binary_name' in rom_config:
+ binary_name = rom_config['binary_name']
+ else:
+ binary_name = rom_config['name']
+
+ if 'verilator_extra_args' in rom_config:
+ verilator_extra_args = rom_config['verilator_extra_args']
+ else:
+ verilator_extra_args = []
+
+ # Allow tests to optionally specify their subdir within the project.
+ test_dir = rom_config.get('test_dir', 'sw/device/tests')
+
+ test_filename = binary_name + '_sim_verilator.scr.40.vmem'
+ bin_path = bin_dir / test_dir / test_filename
+ assert bin_path.is_file()
+
+ return (bin_path, verilator_extra_args)
+
+
@pytest.fixture(params=config.TEST_APPS_SELFCHECKING,
ids=lambda param: param['name'])
def app_selfchecking(request, bin_dir):
@@ -277,6 +314,29 @@
assert result_msg == 'PASSED'
+def test_roms_selfchecking(tmp_path, bin_dir, rom_selfchecking):
+ """
+ Run a self-checking ROM image on a Earl Grey Verilator simulation
+
+ The ROM is initialized with the default boot ROM, the flash is initialized
+ to zero.
+
+ Self-checking ROMs are expected to return PASS or FAIL in the end.
+ """
+
+ sim_path = bin_dir / "hw/top_earlgrey/Vchip_earlgrey_verilator"
+ rom_vmem_path = rom_selfchecking[0]
+ otp_img_path = bin_dir / "sw/device/otp_img/otp_img_sim_verilator.vmem"
+
+ sim = VerilatorSimEarlgrey(sim_path, rom_vmem_path, otp_img_path, tmp_path)
+
+ sim.run(flash_elf=None, extra_sim_args=rom_selfchecking[1])
+
+ assert_selfchecking_test_passes(sim)
+
+ sim.terminate()
+
+
def test_apps_selfchecking(tmp_path, bin_dir, app_selfchecking):
"""
Run a self-checking application on a Earl Grey Verilator simulation
diff --git a/test/systemtest/roms_config.py b/test/systemtest/roms_config.py
new file mode 100644
index 0000000..61cc55a
--- /dev/null
+++ b/test/systemtest/roms_config.py
@@ -0,0 +1,24 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+# List of self-checking test ROM images, which return PASS or FAIL after
+# completion.
+#
+# Each list entry is a dict with the following keys:
+#
+# name:
+# Name of the test (required)
+# verilator_extra_args:
+# A list of additional command-line arguments passed to the Verilator
+# simulation (optional).
+# targets:
+# List of targets for which the test is executed. The test will be executed
+# on all targets if not given (optional).
+TEST_ROMS_SELFCHECKING = [
+ {
+ "name": "mask_rom_epmp_test",
+ "targets": ["sim_verilator"],
+ "test_dir": "sw/device/silicon_creator/mask_rom",
+ },
+]