[testutils] Add SPI flash command utils

Add some utility functions to send SPI flash command sequences to a chip
connected to a spi_host (at chip select 0).

Rebase the DV passthrough test on these functions.

Signed-off-by: Alexander Williams <awill@opentitan.org>
diff --git a/sw/device/lib/testing/BUILD b/sw/device/lib/testing/BUILD
index 1df3d48..bd711f4 100644
--- a/sw/device/lib/testing/BUILD
+++ b/sw/device/lib/testing/BUILD
@@ -361,6 +361,19 @@
 )
 
 cc_library(
+    name = "spi_flash_testutils",
+    srcs = ["spi_flash_testutils.c"],
+    hdrs = ["spi_flash_testutils.h"],
+    target_compatible_with = [OPENTITAN_CPU],
+    deps = [
+        ":spi_device_testutils",
+        "//sw/device/lib/base:macros",
+        "//sw/device/lib/dif:spi_host",
+        "//sw/device/lib/testing/test_framework:check",
+    ],
+)
+
+cc_library(
     name = "usb_testutils",
     srcs = [
         "usb_testutils.c",
diff --git a/sw/device/lib/testing/spi_flash_testutils.c b/sw/device/lib/testing/spi_flash_testutils.c
new file mode 100644
index 0000000..3090058
--- /dev/null
+++ b/sw/device/lib/testing/spi_flash_testutils.c
@@ -0,0 +1,207 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/spi_flash_testutils.h"
+
+#include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/dif/dif_spi_host.h"
+#include "sw/device/lib/testing/spi_device_testutils.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+
+void spi_flash_testutils_read_id(dif_spi_host_t *spih,
+                                 spi_flash_testutils_jedec_id_t *id) {
+  CHECK(spih != NULL);
+  CHECK(id != NULL);
+
+  uint8_t buffer[32];
+  dif_spi_host_segment_t transaction[] = {
+      {
+          .type = kDifSpiHostSegmentTypeOpcode,
+          .opcode = kSpiDeviceFlashOpReadJedec,
+      },
+      {
+          .type = kDifSpiHostSegmentTypeRx,
+          .rx =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .buf = buffer,
+                  .length = sizeof(buffer),
+              },
+      },
+  };
+  CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                        ARRAYSIZE(transaction)));
+
+  size_t page = 0;
+  while ((page < sizeof(buffer)) && (buffer[page] == 0x7fu)) {
+    ++page;
+  }
+  CHECK(page + 3 <= sizeof(buffer));
+  id->continuation_len = page;
+  id->manufacturer_id = buffer[page];
+  id->device_id = buffer[page + 1];
+  id->device_id |= (uint16_t)buffer[page + 2] << 8;
+}
+
+void spi_flash_testutils_read_sfdp(dif_spi_host_t *spih, uint32_t address,
+                                   uint8_t *buffer, size_t length) {
+  CHECK(spih != NULL);
+  CHECK(buffer != NULL);
+
+  dif_spi_host_segment_t transaction[] = {
+      {
+          .type = kDifSpiHostSegmentTypeOpcode,
+          .opcode = kSpiDeviceFlashOpReadSfdp,
+      },
+      {
+          .type = kDifSpiHostSegmentTypeAddress,
+          .address =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .mode = kDifSpiHostAddrMode3b,
+                  .address = address,
+              },
+      },
+      {
+          .type = kDifSpiHostSegmentTypeDummy,
+          .dummy =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .length = 8,
+              },
+      },
+      {
+          .type = kDifSpiHostSegmentTypeRx,
+          .rx =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .buf = buffer,
+                  .length = length,
+              },
+      },
+  };
+  CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                        ARRAYSIZE(transaction)));
+}
+
+void spi_flash_testutils_wait_until_not_busy(dif_spi_host_t *spih) {
+  CHECK(spih != NULL);
+  uint8_t status;
+
+  do {
+    dif_spi_host_segment_t transaction[] = {
+        {
+            .type = kDifSpiHostSegmentTypeOpcode,
+            .opcode = kSpiDeviceFlashOpReadStatus1,
+        },
+        {
+            .type = kDifSpiHostSegmentTypeRx,
+            .rx =
+                {
+                    .width = kDifSpiHostWidthStandard,
+                    .buf = &status,
+                    .length = 1,
+                },
+        },
+    };
+    CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                          ARRAYSIZE(transaction)));
+  } while (status & kSpiFlashStatusBitWip);
+}
+
+void spi_flash_testutils_issue_write_enable(dif_spi_host_t *spih) {
+  CHECK(spih != NULL);
+  dif_spi_host_segment_t transaction[] = {
+      {
+          .type = kDifSpiHostSegmentTypeOpcode,
+          .opcode = kSpiDeviceFlashOpWriteEnable,
+      },
+  };
+  CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                        ARRAYSIZE(transaction)));
+}
+
+void spi_flash_testutils_erase_chip(dif_spi_host_t *spih) {
+  CHECK(spih != NULL);
+  spi_flash_testutils_issue_write_enable(spih);
+
+  dif_spi_host_segment_t transaction[] = {
+      {
+          .type = kDifSpiHostSegmentTypeOpcode,
+          .opcode = kSpiDeviceFlashOpChipErase,
+      },
+  };
+  CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                        ARRAYSIZE(transaction)));
+  spi_flash_testutils_wait_until_not_busy(spih);
+}
+
+void spi_flash_testutils_erase_sector(dif_spi_host_t *spih, uint32_t address,
+                                      bool addr_is_4b) {
+  CHECK(spih != NULL);
+  spi_flash_testutils_issue_write_enable(spih);
+
+  dif_spi_host_addr_mode_t addr_mode =
+      addr_is_4b ? kDifSpiHostAddrMode4b : kDifSpiHostAddrMode3b;
+  dif_spi_host_segment_t transaction[] = {
+      {
+          .type = kDifSpiHostSegmentTypeOpcode,
+          .opcode = kSpiDeviceFlashOpSectorErase,
+      },
+      {
+          .type = kDifSpiHostSegmentTypeAddress,
+          .address =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .mode = addr_mode,
+                  .address = address,
+              },
+      },
+  };
+  CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                        ARRAYSIZE(transaction)));
+
+  spi_flash_testutils_wait_until_not_busy(spih);
+}
+
+void spi_flash_testutils_program_page(dif_spi_host_t *spih, uint8_t *payload,
+                                      size_t length, uint32_t address,
+                                      bool addr_is_4b) {
+  CHECK(spih != NULL);
+  CHECK(payload != NULL);
+  CHECK(length <= 256);  // Length must be less than a page size.
+
+  spi_flash_testutils_issue_write_enable(spih);
+
+  dif_spi_host_addr_mode_t addr_mode =
+      addr_is_4b ? kDifSpiHostAddrMode4b : kDifSpiHostAddrMode3b;
+  dif_spi_host_segment_t transaction[] = {
+      {
+          .type = kDifSpiHostSegmentTypeOpcode,
+          .opcode = kSpiDeviceFlashOpPageProgram,
+      },
+      {
+          .type = kDifSpiHostSegmentTypeAddress,
+          .address =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .mode = addr_mode,
+                  .address = address,
+              },
+      },
+      {
+          .type = kDifSpiHostSegmentTypeTx,
+          .tx =
+              {
+                  .width = kDifSpiHostWidthStandard,
+                  .buf = payload,
+                  .length = length,
+              },
+      },
+  };
+  CHECK_DIF_OK(dif_spi_host_transaction(spih, /*csid=*/0, transaction,
+                                        ARRAYSIZE(transaction)));
+
+  spi_flash_testutils_wait_until_not_busy(spih);
+}
diff --git a/sw/device/lib/testing/spi_flash_testutils.h b/sw/device/lib/testing/spi_flash_testutils.h
new file mode 100644
index 0000000..04b8c87
--- /dev/null
+++ b/sw/device/lib/testing/spi_flash_testutils.h
@@ -0,0 +1,99 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_SPI_FLASH_TESTUTILS_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_SPI_FLASH_TESTUTILS_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "sw/device/lib/dif/dif_spi_host.h"
+
+typedef struct spi_flash_testutils_jedec_id {
+  uint16_t device_id;
+  uint8_t manufacturer_id;
+  uint8_t continuation_len;
+} spi_flash_testutils_jedec_id_t;
+
+/**
+ * Read out the JEDEC ID from the SPI flash.
+ *
+ * @param spih A SPI host handle.
+ * @param[out] id A pointer to where to store the ID.
+ */
+void spi_flash_testutils_read_id(dif_spi_host_t *spih,
+                                 spi_flash_testutils_jedec_id_t *id);
+
+/**
+ * Read out the SFDP from the indicated address and place the table contents
+ * into the buffer.
+ *
+ * @param spih A SPI host handle.
+ * @param buffer A pointer to a buffer that will hold the SFDP contents.
+ * @param length The number of bytes to write into `buffer`.
+ */
+void spi_flash_testutils_read_sfdp(dif_spi_host_t *spih, uint32_t address,
+                                   uint8_t *buffer, size_t length);
+
+typedef enum spi_flash_status_bit {
+  kSpiFlashStatusBitWip = 0x1,
+  kSpiFlashStatusBitWel = 0x2,
+} spi_flash_status_bit_t;
+
+/**
+ * Spin wait until a Read Status command shows the downstream SPI flash is no
+ * longer busy.
+ */
+void spi_flash_testutils_wait_until_not_busy(dif_spi_host_t *spih);
+
+/**
+ * Issue the Write Enable command to the downstream SPI flash.
+ */
+void spi_flash_testutils_issue_write_enable(dif_spi_host_t *spih);
+
+/**
+ * Perform full Chip Erase sequence, including the Write Enable and Chip Erase
+ * commands, and poll the status registers in a loop until the WIP bit clears.
+ *
+ * Does not return until the erase completes.
+ *
+ * @param spih A SPI host handle.
+ */
+void spi_flash_testutils_erase_chip(dif_spi_host_t *spih);
+
+/**
+ * Perform full Sector Erase sequence, including the Write Enable and Sector
+ * Erase commands, and poll the status registers in a loop until the WIP bit
+ * clears.
+ *
+ * Does not return until the erase completes.
+ *
+ * @param spih A SPI host handle.
+ * @param address An address contained within the desired sector.
+ * @param addr_is_4b True if `address` is 4 bytes long, else 3 bytes.
+ */
+void spi_flash_testutils_erase_sector(dif_spi_host_t *spih, uint32_t address,
+                                      bool addr_is_4b);
+
+/**
+ * Perform full Page Program sequence, including the Write Enable and Page
+ * Program commands, and poll the status registers in a loop until the WIP bit
+ * clears.
+ *
+ * Does not return until the programming operation completes.
+ *
+ * @param spih A SPI host handle.
+ * @param payload A pointer to the payload to be written to the page.
+ * @param length Number of bytes in the payload. Must be less than or equal to
+ *               256 bytes.
+ * @param address The start address where the payload programming should begin.
+ *                Note that an address + length that crosses a page boundary may
+ *                wrap around to the start of the page.
+ * @param addr_is_4b True if `address` is 4 bytes long, else 3 bytes.
+ */
+void spi_flash_testutils_program_page(dif_spi_host_t *spih, uint8_t *payload,
+                                      size_t length, uint32_t address,
+                                      bool addr_is_4b);
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_TESTING_SPI_FLASH_TESTUTILS_H_
diff --git a/sw/device/tests/sim_dv/BUILD b/sw/device/tests/sim_dv/BUILD
index 0be24d4..9bbed52 100644
--- a/sw/device/tests/sim_dv/BUILD
+++ b/sw/device/tests/sim_dv/BUILD
@@ -831,6 +831,7 @@
         "//sw/device/lib/runtime:log",
         "//sw/device/lib/testing:pinmux_testutils",
         "//sw/device/lib/testing:spi_device_testutils",
+        "//sw/device/lib/testing:spi_flash_testutils",
         "//sw/device/lib/testing/test_framework:ottf_main",
     ],
 )
diff --git a/sw/device/tests/sim_dv/spi_passthrough_test.c b/sw/device/tests/sim_dv/spi_passthrough_test.c
index 2e4c7a6..266d98b 100644
--- a/sw/device/tests/sim_dv/spi_passthrough_test.c
+++ b/sw/device/tests/sim_dv/spi_passthrough_test.c
@@ -18,6 +18,7 @@
 #include "sw/device/lib/runtime/log.h"
 #include "sw/device/lib/testing/pinmux_testutils.h"
 #include "sw/device/lib/testing/spi_device_testutils.h"
+#include "sw/device/lib/testing/spi_flash_testutils.h"
 #include "sw/device/lib/testing/test_framework/check.h"
 #include "sw/device/lib/testing/test_framework/ottf_main.h"
 
@@ -135,11 +136,6 @@
     //     },
 };
 
-enum spi_flash_status_bit {
-  kSpiFlashStatusBitWip = 0x1,
-  kSpiFlashStatusBitWel = 0x2,
-};
-
 /**
  * Initialize the provided SPI host. For the most part, the values provided are
  * filler, as spi_host0 will be in passthrough mode and spi_host1 will remain
@@ -162,48 +158,6 @@
 }
 
 /**
- * Spin wait until a Read Status command shows the downstream SPI flash is no
- * longer busy.
- */
-void wait_until_not_busy(void) {
-  uint8_t status;
-
-  do {
-    dif_spi_host_segment_t transaction[] = {
-        {
-            .type = kDifSpiHostSegmentTypeOpcode,
-            .opcode = kSpiDeviceFlashOpReadStatus1,
-        },
-        {
-            .type = kDifSpiHostSegmentTypeRx,
-            .rx =
-                {
-                    .width = kDifSpiHostWidthStandard,
-                    .buf = &status,
-                    .length = 1,
-                },
-        },
-    };
-    CHECK_DIF_OK(dif_spi_host_transaction(&spi_host0, /*csid=*/0, transaction,
-                                          ARRAYSIZE(transaction)));
-  } while (status & kSpiFlashStatusBitWip);
-}
-
-/**
- * Issue the Write Enable command to the downstream SPI flash.
- */
-void issue_write_enable(void) {
-  dif_spi_host_segment_t transaction[] = {
-      {
-          .type = kDifSpiHostSegmentTypeOpcode,
-          .opcode = kSpiDeviceFlashOpWriteEnable,
-      },
-  };
-  CHECK_DIF_OK(dif_spi_host_transaction(&spi_host0, /*csid=*/0, transaction,
-                                        ARRAYSIZE(transaction)));
-}
-
-/**
  * Handle an incoming Write Status command.
  *
  * Modifies the internal status register and relays the command out to the
@@ -229,7 +183,7 @@
   status |= (payload << offset);
   CHECK_DIF_OK(dif_spi_device_set_flash_status_registers(&spi_device, status));
 
-  issue_write_enable();
+  spi_flash_testutils_issue_write_enable(&spi_host0);
 
   dif_spi_host_segment_t transaction[] = {
       {
@@ -248,7 +202,7 @@
   };
   CHECK_DIF_OK(dif_spi_host_transaction(&spi_host0, /*csid=*/0, transaction,
                                         ARRAYSIZE(transaction)));
-  wait_until_not_busy();
+  spi_flash_testutils_wait_until_not_busy(&spi_host0);
   CHECK_DIF_OK(dif_spi_device_clear_flash_busy_bit(&spi_device));
 }
 
@@ -258,17 +212,7 @@
  * Relays the command out to the downstream SPI flash.
  */
 void handle_chip_erase(void) {
-  issue_write_enable();
-
-  dif_spi_host_segment_t transaction[] = {
-      {
-          .type = kDifSpiHostSegmentTypeOpcode,
-          .opcode = kSpiDeviceFlashOpChipErase,
-      },
-  };
-  CHECK_DIF_OK(dif_spi_host_transaction(&spi_host0, /*csid=*/0, transaction,
-                                        ARRAYSIZE(transaction)));
-  wait_until_not_busy();
+  spi_flash_testutils_erase_chip(&spi_host0);
   CHECK_DIF_OK(dif_spi_device_clear_flash_busy_bit(&spi_device));
 }
 
@@ -290,30 +234,8 @@
   CHECK_DIF_OK(
       dif_spi_device_get_4b_address_mode(&spi_device, &addr4b_enabled));
 
-  issue_write_enable();
-
-  dif_spi_host_addr_mode_t addr_mode = dif_toggle_to_bool(addr4b_enabled)
-                                           ? kDifSpiHostAddrMode4b
-                                           : kDifSpiHostAddrMode3b;
-  dif_spi_host_segment_t transaction[] = {
-      {
-          .type = kDifSpiHostSegmentTypeOpcode,
-          .opcode = kSpiDeviceFlashOpSectorErase,
-      },
-      {
-          .type = kDifSpiHostSegmentTypeAddress,
-          .address =
-              {
-                  .width = kDifSpiHostWidthStandard,
-                  .mode = addr_mode,
-                  .address = address,
-              },
-      },
-  };
-  CHECK_DIF_OK(dif_spi_host_transaction(&spi_host0, /*csid=*/0, transaction,
-                                        ARRAYSIZE(transaction)));
-
-  wait_until_not_busy();
+  bool addr_is_4b = dif_toggle_to_bool(addr4b_enabled);
+  spi_flash_testutils_erase_sector(&spi_host0, address, addr_is_4b);
   CHECK_DIF_OK(dif_spi_device_clear_flash_busy_bit(&spi_device));
 }
 
@@ -346,39 +268,9 @@
   CHECK_DIF_OK(
       dif_spi_device_get_4b_address_mode(&spi_device, &addr4b_enabled));
 
-  issue_write_enable();
-
-  dif_spi_host_addr_mode_t addr_mode = dif_toggle_to_bool(addr4b_enabled)
-                                           ? kDifSpiHostAddrMode4b
-                                           : kDifSpiHostAddrMode3b;
-  dif_spi_host_segment_t transaction[] = {
-      {
-          .type = kDifSpiHostSegmentTypeOpcode,
-          .opcode = kSpiDeviceFlashOpPageProgram,
-      },
-      {
-          .type = kDifSpiHostSegmentTypeAddress,
-          .address =
-              {
-                  .width = kDifSpiHostWidthStandard,
-                  .mode = addr_mode,
-                  .address = address,
-              },
-      },
-      {
-          .type = kDifSpiHostSegmentTypeTx,
-          .tx =
-              {
-                  .width = kDifSpiHostWidthStandard,
-                  .buf = payload,
-                  .length = payload_occupancy,
-              },
-      },
-  };
-  CHECK_DIF_OK(dif_spi_host_transaction(&spi_host0, /*csid=*/0, transaction,
-                                        ARRAYSIZE(transaction)));
-
-  wait_until_not_busy();
+  bool addr_is_4b = dif_toggle_to_bool(addr4b_enabled);
+  spi_flash_testutils_program_page(&spi_host0, payload, payload_occupancy,
+                                   address, addr_is_4b);
   CHECK_DIF_OK(dif_spi_device_clear_flash_busy_bit(&spi_device));
 }