[bazel] Add SRAM program test rule for CW310

Issue #13968

Signed-off-by: Dan McArdle <dmcardle@google.com>
diff --git a/rules/opentitan_gdb_test.bzl b/rules/opentitan_gdb_test.bzl
new file mode 100644
index 0000000..d843b3a
--- /dev/null
+++ b/rules/opentitan_gdb_test.bzl
@@ -0,0 +1,125 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+load("@lowrisc_opentitan//rules:rv.bzl", "rv_rule")
+
+def _opentitan_gdb_fpga_cw310_test(ctx):
+    test_script = '''#!/usr/bin/env bash
+        set -e
+
+        # Do not mask failures in the left-hand side of pipelines.
+        set -o pipefail
+
+        COLOR_RED='\\x1b[0;31m'
+        COLOR_GREEN='\\x1b[0;32m'
+        COLOR_PURPLE='\\x1b[0;35m'
+
+        function prefix_lines() {{
+            LABEL="$1"
+            COLOR="$2"
+            COLOR_RESET='\\x1b[m'
+            sed -Eu "s/(.*)/$COLOR[$LABEL]$COLOR_RESET \\1/"
+        }}
+
+        set -x
+
+        ./opentitantool load-bitstream --rom-kind=TestRom rom.bit
+
+        (openocd -f /usr/share/openocd/scripts/interface/ftdi/olimex-arm-usb-tiny-h.cfg \\
+            -c "adapter speed 500; transport select jtag; reset_config trst_and_srst" \\
+            -f {openocd_earlgrey_config} \\
+            |& prefix_lines OPENOCD "$COLOR_PURPLE") &
+        OPENOCD_PID=$!
+
+        # For debugging, it may be useful to use `--init-command`, which causes
+        # GDB to drop to the interactive prompt when the script ends rather than
+        # exiting.
+        (/tools/riscv/bin/riscv32-unknown-elf-gdb --command=script.gdb \\
+            |& prefix_lines GDB "$COLOR_GREEN") &
+
+        ./opentitantool console \\
+            --timeout 15s \\
+            --exit-success='{exit_success_pattern}' \\
+            |& prefix_lines CONSOLE "$COLOR_RED"
+
+        # Send TERM signal to OpenOCD and wait for all background jobs to complete. Note
+        # that GDB will exit naturally at the end of its script.
+        kill $OPENOCD_PID
+        wait
+    '''.format(
+        openocd_earlgrey_config = ctx.file._openocd_earlgrey_config.path,
+        exit_success_pattern = ctx.attr.exit_success_pattern,
+    )
+
+    # Write the GDB script to disk and load it with GDB's `--command` argument.
+    # This enables us to separate lines with whitespace, whereas if we piped the
+    # string into GDB's stdin, each newline would cause it to repeat the
+    # previous command.
+    gdb_script_file = ctx.actions.declare_file("{}.gdb".format(ctx.label.name))
+    test_script_file = ctx.actions.declare_file("{}.sh".format(ctx.label.name))
+
+    ctx.actions.write(output = gdb_script_file, content = ctx.attr.gdb_script)
+    ctx.actions.write(output = test_script_file, content = test_script)
+
+    # Construct a dict that we can pass to `ctx.runfiles()`, mapping symlink
+    # names to real file paths.
+    gdb_script_symlinks_flipped = {}
+    for label in ctx.attr.gdb_script_symlinks:
+        label_files = label.files.to_list()
+        if len(label_files) != 1:
+            fail("gdb_script_symlinks labels must have exactly one file, but", label, "has these files:", label_files)
+        gdb_script_symlinks_flipped[ctx.attr.gdb_script_symlinks[label]] = label_files[0]
+
+    gdb_script_runfiles = ctx.runfiles(
+        symlinks = gdb_script_symlinks_flipped,
+        files = gdb_script_symlinks_flipped.values(),
+    )
+    test_script_runfiles = ctx.runfiles(
+        # Relative paths provided by `File.path` seem to not work
+        # for generated files. Symlinking the runtime files wherever
+        # Bazel will run `test_script_file` sidesteps the issue.
+        symlinks = {
+            "opentitantool": ctx.file._opentitantool,
+            "rom.bit": ctx.file.rom_bitstream,
+            "script.gdb": gdb_script_file,
+        },
+        files = [
+            ctx.file._openocd_earlgrey_config,
+            ctx.file._opentitantool,
+            ctx.file.rom_bitstream,
+            gdb_script_file,
+        ],
+    )
+    runfiles = test_script_runfiles.merge(gdb_script_runfiles)
+
+    return [DefaultInfo(
+        executable = test_script_file,
+        runfiles = runfiles,
+    )]
+
+# Orchestrate opentitantool, OpenOCD, and GDB to load the given program into
+# SRAM and execute it in-place. This rule assumes that a CW310 FPGA and an
+# ARM-USB-TINY-H JTAG debugger are attached to the host.
+opentitan_gdb_fpga_cw310_test = rv_rule(
+    implementation = _opentitan_gdb_fpga_cw310_test,
+    attrs = {
+        "exit_success_pattern": attr.string(mandatory = True),
+        "gdb_script": attr.string(mandatory = True),
+        "gdb_script_symlinks": attr.label_keyed_string_dict(allow_files = True),
+        "rom_bitstream": attr.label(
+            mandatory = True,
+            allow_single_file = True,
+        ),
+        "_opentitantool": attr.label(
+            default = "//sw/host/opentitantool",
+            allow_single_file = True,
+            cfg = "exec",
+        ),
+        "_openocd_earlgrey_config": attr.label(
+            default = "//util/openocd/target:lowrisc-earlgrey.cfg",
+            allow_single_file = True,
+        ),
+    },
+    test = True,
+)
diff --git a/sw/device/examples/sram_program/BUILD b/sw/device/examples/sram_program/BUILD
index a2b8754..de13c81 100644
--- a/sw/device/examples/sram_program/BUILD
+++ b/sw/device/examples/sram_program/BUILD
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 load("//rules:opentitan.bzl", "opentitan_ram_binary")
+load("//rules:opentitan_gdb_test.bzl", "opentitan_gdb_fpga_cw310_test")
 load("//rules:linker.bzl", "ld_library")
 
 package(default_visibility = ["//visibility:public"])
@@ -31,6 +32,48 @@
         "//hw/top_earlgrey/sw/autogen:top_earlgrey",
         "//sw/device/lib/base:macros",
         "//sw/device/lib/runtime:log",
+        "//sw/device/lib/testing:pinmux_testutils",
         "//sw/device/lib/testing/test_framework:check",
     ],
 )
+
+opentitan_gdb_fpga_cw310_test(
+    name = "sram_program_fpga_cw310_test",
+    exit_success_pattern = "sram_program\\.c:47\\] PC: 0x1000208c, SRAM: \\[0x10000000, 0x10020000\\)",
+    gdb_script = """
+        target extended-remote :3333
+        set pagination off
+
+        echo :::: Send OpenOCD the 'reset halt' command.\\n
+        monitor reset halt
+
+        echo :::: Set RAM as RWX by executing code between _start and test_rom_main()\\n
+        file test_rom.elf
+        break rom_test_main
+        continue
+        delete breakpoints
+
+        echo :::: Load the SRAM program onto the device and check integrity.\\n
+        file sram_program.elf
+        load sram_program.elf
+        compare-sections
+
+        info registers
+        echo :::: Update registers before calling sram_main().\\n
+        set $sp = _stack_end
+        set $gp = __global_pointer$
+        info registers
+
+        echo :::: Call sram_main().\\n
+        print sram_main()
+
+        echo :::: Done.\\n
+    """,
+    gdb_script_symlinks = {
+        ":sram_program_fpga_cw310.elf": "sram_program.elf",
+        "//sw/device/lib/testing/test_rom:test_rom_fpga_cw310.elf": "test_rom.elf",
+    },
+    # TODO(#13968) Switch to ROM instead of Test ROM.
+    rom_bitstream = "//hw/bitstream:test_rom",
+    tags = ["manual"],
+)
diff --git a/sw/device/examples/sram_program/sram_program.c b/sw/device/examples/sram_program/sram_program.c
index 248682c..28095dd 100644
--- a/sw/device/examples/sram_program/sram_program.c
+++ b/sw/device/examples/sram_program/sram_program.c
@@ -9,11 +9,13 @@
 #include "sw/device/lib/dif/dif_uart.h"
 #include "sw/device/lib/runtime/log.h"
 #include "sw/device/lib/runtime/print.h"
+#include "sw/device/lib/testing/pinmux_testutils.h"
 #include "sw/device/lib/testing/test_framework/check.h"
 
 #include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
 
 static dif_uart_t uart0;
+static dif_pinmux_t pinmux;
 
 enum {
   kSramStart = TOP_EARLGREY_SRAM_CTRL_MAIN_RAM_BASE_ADDR,
@@ -22,6 +24,11 @@
 };
 
 void sram_main() {
+  // Configure the pinmux.
+  CHECK_DIF_OK(dif_pinmux_init(
+      mmio_region_from_addr(TOP_EARLGREY_PINMUX_AON_BASE_ADDR), &pinmux));
+  pinmux_testutils_init(&pinmux);
+
   // Initialize UART.
   CHECK_DIF_OK(dif_uart_init(
       mmio_region_from_addr(TOP_EARLGREY_UART0_BASE_ADDR), &uart0));
diff --git a/util/openocd/target/BUILD b/util/openocd/target/BUILD
new file mode 100644
index 0000000..896cbde
--- /dev/null
+++ b/util/openocd/target/BUILD
@@ -0,0 +1,5 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+exports_files(["lowrisc-earlgrey.cfg"])