[spi_passthru] Test status-modifying opcodes

1. Test EN4B, EX4B, WREN and WRDI.
2. Test reading the extended status register.

Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/sw/device/lib/testing/json/command.h b/sw/device/lib/testing/json/command.h
index d7c3fb9..c34ecb3 100644
--- a/sw/device/lib/testing/json/command.h
+++ b/sw/device/lib/testing/json/command.h
@@ -16,6 +16,8 @@
     value(_, GpioGet) \
     value(_, PinmuxConfig) \
     value(_, SpiConfigureJedecId) \
+    value(_, SpiReadStatus) \
+    value(_, SpiWriteStatus) \
     value(_, SwStrapRead)
 UJSON_SERDE_ENUM(TestCommand, test_command_t, ENUM_TEST_COMMAND);
 
diff --git a/sw/device/lib/testing/json/spi_passthru.h b/sw/device/lib/testing/json/spi_passthru.h
index 6efe820..9ebe145 100644
--- a/sw/device/lib/testing/json/spi_passthru.h
+++ b/sw/device/lib/testing/json/spi_passthru.h
@@ -17,6 +17,11 @@
     field(continuation_len, uint8_t)
 UJSON_SERDE_STRUCT(ConfigJedecId, config_jedec_id_t, STRUCT_CONFIG_JEDEC_ID);
 
+#define STRUCT_STATUS_REGISTER(field, string) \
+    field(status, uint32_t) \
+    field(addr_4b, bool)
+UJSON_SERDE_STRUCT(StatusRegister, status_register_t, STRUCT_STATUS_REGISTER);
+
 // clang-format on
 #ifdef __cplusplus
 }
diff --git a/sw/device/tests/BUILD b/sw/device/tests/BUILD
index 6e09e85..7824a1d 100644
--- a/sw/device/tests/BUILD
+++ b/sw/device/tests/BUILD
@@ -2195,7 +2195,9 @@
 
 opentitan_functest(
     name = "spi_passthru_test",
-    srcs = ["spi_passthru_test.c"],
+    srcs = [
+        "spi_passthru_test.c",
+    ],
     cw310 = cw310_params(
         # This test will need hyperdebug once we start using multi-lane
         # SPI modes.
@@ -2214,6 +2216,7 @@
     deps = [
         "//sw/device/lib/dif:gpio",
         "//sw/device/lib/dif:spi_device",
+        "//sw/device/lib/testing:spi_device_testutils",
         "//sw/device/lib/testing/json:command",
         "//sw/device/lib/testing/json:spi_passthru",
         "//sw/device/lib/testing/test_framework:ottf_flow_control",
diff --git a/sw/device/tests/spi_passthru_test.c b/sw/device/tests/spi_passthru_test.c
index d2eb4c5..0d4d068 100644
--- a/sw/device/tests/spi_passthru_test.c
+++ b/sw/device/tests/spi_passthru_test.c
@@ -11,6 +11,7 @@
 #include "sw/device/lib/dif/dif_spi_device.h"
 #include "sw/device/lib/runtime/log.h"
 #include "sw/device/lib/testing/json/command.h"
+#include "sw/device/lib/testing/spi_device_testutils.h"
 #include "sw/device/lib/testing/test_framework/check.h"
 #include "sw/device/lib/testing/test_framework/ottf_flow_control.h"
 #include "sw/device/lib/testing/test_framework/ottf_main.h"
@@ -23,7 +24,7 @@
 
 static dif_spi_device_handle_t spid;
 
-status_t configure_jedec_id(ujson_t *uj, dif_spi_device_handle_t *spid) {
+static status_t configure_jedec_id(ujson_t *uj, dif_spi_device_handle_t *spid) {
   config_jedec_id_t config;
   TRY(ujson_deserialize_config_jedec_id_t(uj, &config));
   dif_spi_device_flash_id_t id = {
@@ -33,23 +34,28 @@
       .num_continuation_code = config.continuation_len,
   };
   TRY(dif_spi_device_set_flash_id(spid, id));
-
-  dif_spi_device_flash_command_t read_id = {
-      .opcode = 0x9F,
-      .address_type = kDifSpiDeviceFlashAddrDisabled,
-      .dummy_cycles = 0,
-      .payload_io_type = kDifSpiDevicePayloadIoSingle,
-      .passthrough_swap_address = false,
-      .payload_dir_to_host = true,
-      .payload_swap_enable = false,
-      .upload = false,
-      .set_busy_status = false,
-  };
-  TRY(dif_spi_device_set_flash_command_slot(spid, 3, kDifToggleEnabled,
-                                            read_id));
   return RESP_OK_STATUS(uj);
 }
 
+static status_t write_status_register(ujson_t *uj,
+                                      dif_spi_device_handle_t *spid) {
+  status_register_t sr;
+  TRY(ujson_deserialize_status_register_t(uj, &sr));
+  TRY(dif_spi_device_set_flash_status_registers(spid, sr.status));
+  return RESP_OK_STATUS(uj);
+}
+
+static status_t read_status_register(ujson_t *uj,
+                                     dif_spi_device_handle_t *spid) {
+  status_register_t sr;
+  dif_toggle_t addr_4b;
+  TRY(dif_spi_device_get_flash_status_registers(spid, &sr.status));
+  TRY(dif_spi_device_get_4b_address_mode(spid, &addr_4b));
+  sr.addr_4b = addr_4b;
+  RESP_OK(ujson_serialize_status_register_t, uj, &sr);
+  return OK_STATUS();
+}
+
 status_t command_processor(ujson_t *uj) {
   while (true) {
     test_command_t command;
@@ -58,6 +64,12 @@
       case kTestCommandSpiConfigureJedecId:
         RESP_ERR(uj, configure_jedec_id(uj, &spid));
         break;
+      case kTestCommandSpiReadStatus:
+        RESP_ERR(uj, read_status_register(uj, &spid));
+        break;
+      case kTestCommandSpiWriteStatus:
+        RESP_ERR(uj, write_status_register(uj, &spid));
+        break;
       default:
         LOG_ERROR("Unrecognized command: %d", command);
         RESP_ERR(uj, INVALID_ARGUMENT());
@@ -71,14 +83,12 @@
   CHECK_DIF_OK(dif_spi_device_init_handle(
       mmio_region_from_addr(TOP_EARLGREY_SPI_DEVICE_BASE_ADDR), &spid));
 
-  dif_spi_device_config_t dev_cfg = {
-      .clock_polarity = kDifSpiDeviceEdgePositive,
-      .data_phase = kDifSpiDeviceEdgeNegative,
-      .tx_order = kDifSpiDeviceBitOrderMsbToLsb,
-      .rx_order = kDifSpiDeviceBitOrderMsbToLsb,
-      .device_mode = kDifSpiDeviceModePassthrough,
-  };
-  CHECK_DIF_OK(dif_spi_device_configure(&spid, dev_cfg));
+  // We want to block passthru of the first 5 read commands, corresponding to
+  // ReadStatus{1,2,3}, ReadJedecID and ReadSfdp.
+  // We also block all write commands.
+  spi_device_testutils_configure_passthrough(&spid,
+                                             /*filters=*/0x1F,
+                                             /*upload_write_commands=*/true);
 
   dif_spi_device_passthrough_intercept_config_t passthru_cfg = {
       .status = true,
diff --git a/sw/host/opentitanlib/src/spiflash/flash.rs b/sw/host/opentitanlib/src/spiflash/flash.rs
index ba2848e..1f1ff08 100644
--- a/sw/host/opentitanlib/src/spiflash/flash.rs
+++ b/sw/host/opentitanlib/src/spiflash/flash.rs
@@ -16,6 +16,8 @@
     BadEraseAddress(u32, u32),
     #[error("erase length {0} not a multiple of {1} bytes")]
     BadEraseLength(u32, u32),
+    #[error("bad sequence length: {0}")]
+    BadSequenceLength(usize),
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -70,6 +72,9 @@
     pub const WRITE_ENABLE: u8 = 0x06;
     pub const WRITE_DISABLE: u8 = 0x04;
     pub const READ_STATUS: u8 = 0x05;
+    // Winbond parts use 0x35 and 0x15 for extended status reads.
+    pub const READ_STATUS2: u8 = 0x35;
+    pub const READ_STATUS3: u8 = 0x15;
     pub const READ_ID: u8 = 0x9f;
     pub const ENTER_4B: u8 = 0xb7;
     pub const EXIT_4B: u8 = 0xe9;
@@ -123,6 +128,23 @@
         Ok(buf[0])
     }
 
+    /// Read the extended status register from the `spi` target.
+    pub fn read_status_ex(spi: &dyn Target, seq: Option<&[u8]>) -> Result<u32> {
+        let seq = seq.unwrap_or(&[Self::READ_STATUS, Self::READ_STATUS2, Self::READ_STATUS3]);
+        ensure!(
+            seq.len() > 0 && seq.len() <= 3,
+            Error::BadSequenceLength(seq.len())
+        );
+        let mut buf = [0u8; 4];
+        for (op, byte) in seq.iter().zip(buf.iter_mut()) {
+            spi.run_transaction(&mut [
+                Transfer::Write(std::slice::from_ref(op)),
+                Transfer::Read(std::slice::from_mut(byte)),
+            ])?;
+        }
+        Ok(u32::from_le_bytes(buf))
+    }
+
     /// Poll the status register waiting for the busy bit to clear.
     pub fn wait_for_busy_clear(spi: &dyn Target) -> Result<()> {
         while SpiFlash::read_status(spi)? & SpiFlash::STATUS_WIP != 0 {
diff --git a/sw/host/opentitanlib/src/test_utils/spi_passthru.rs b/sw/host/opentitanlib/src/test_utils/spi_passthru.rs
index de18a89..f64af10 100644
--- a/sw/host/opentitanlib/src/test_utils/spi_passthru.rs
+++ b/sw/host/opentitanlib/src/test_utils/spi_passthru.rs
@@ -21,3 +21,17 @@
         Ok(())
     }
 }
+
+impl StatusRegister {
+    pub fn read(uart: &dyn Uart) -> Result<Self> {
+        TestCommand::SpiReadStatus.send(&*uart)?;
+        StatusRegister::recv(uart, Duration::from_secs(300), false)
+    }
+
+    pub fn write(&self, uart: &dyn Uart) -> Result<()> {
+        TestCommand::SpiWriteStatus.send(&*uart)?;
+        self.send(uart)?;
+        Status::recv(uart, Duration::from_secs(300), false)?;
+        Ok(())
+    }
+}
diff --git a/sw/host/tests/chip/spi_passthru/src/main.rs b/sw/host/tests/chip/spi_passthru/src/main.rs
index 71cdf4e..bfa2408 100644
--- a/sw/host/tests/chip/spi_passthru/src/main.rs
+++ b/sw/host/tests/chip/spi_passthru/src/main.rs
@@ -8,11 +8,15 @@
 
 use opentitanlib::app::TransportWrapper;
 use opentitanlib::execute_test;
+use opentitanlib::io::spi::Transfer;
 use opentitanlib::spiflash::SpiFlash;
 use opentitanlib::test_utils::init::InitializeTest;
-use opentitanlib::test_utils::spi_passthru::ConfigJedecId;
+use opentitanlib::test_utils::spi_passthru::{ConfigJedecId, StatusRegister};
 use opentitanlib::uart::console::UartConsole;
 
+//const FLASH_STATUS_WIP: u32 = 0x01;
+const FLASH_STATUS_WEL: u32 = 0x02;
+
 #[derive(Debug, StructOpt)]
 struct Opts {
     #[structopt(flatten)]
@@ -60,6 +64,76 @@
     Ok(())
 }
 
+fn test_enter_exit_4b_mode(opts: &Opts, transport: &TransportWrapper) -> Result<()> {
+    let uart = transport.uart("console")?;
+    let spi = transport.spi(&opts.spi)?;
+
+    log::info!("Entering 4B address mode");
+    spi.run_transaction(&mut [Transfer::Write(&[SpiFlash::ENTER_4B])])?;
+    let sr = StatusRegister::read(&*uart)?;
+    assert!(sr.addr_4b, "expected to be in 4b mode");
+
+    log::info!("Exiting 4B address mode");
+    spi.run_transaction(&mut [Transfer::Write(&[SpiFlash::EXIT_4B])])?;
+    let sr = StatusRegister::read(&*uart)?;
+    assert!(!sr.addr_4b, "expected to be in 3b mode");
+    Ok(())
+}
+
+fn test_write_enable_disable(opts: &Opts, transport: &TransportWrapper) -> Result<()> {
+    let uart = transport.uart("console")?;
+    let spi = transport.spi(&opts.spi)?;
+
+    log::info!("Sending WRITE_ENABLE");
+    spi.run_transaction(&mut [Transfer::Write(&[SpiFlash::WRITE_ENABLE])])?;
+    let status = SpiFlash::read_status(&*spi)?;
+    let sr = StatusRegister::read(&*uart)?;
+    assert!(
+        status as u32 & FLASH_STATUS_WEL != 0,
+        "expected WEL set via read_status"
+    );
+    assert!(
+        sr.status & FLASH_STATUS_WEL != 0,
+        "expected WEL set on the device"
+    );
+
+    log::info!("Sending WRITE_DISABLE");
+    spi.run_transaction(&mut [Transfer::Write(&[SpiFlash::WRITE_DISABLE])])?;
+    let status = SpiFlash::read_status(&*spi)?;
+    let sr = StatusRegister::read(&*uart)?;
+    assert!(
+        status as u32 & FLASH_STATUS_WEL == 0,
+        "expected WEL clear via read_status"
+    );
+    assert!(
+        sr.status & FLASH_STATUS_WEL == 0,
+        "expected WEL clear on the device"
+    );
+    Ok(())
+}
+
+fn test_read_status_extended(opts: &Opts, transport: &TransportWrapper) -> Result<()> {
+    let uart = transport.uart("console")?;
+    let spi = transport.spi(&opts.spi)?;
+
+    let sr = StatusRegister {
+        status: 0x5A55AA,
+        addr_4b: false,
+    };
+    sr.write(&*uart)?;
+    // Note: because we're programming the flash_status register in firmware,
+    // we require one CS low-to-high transition to latch the values from the
+    // CSR into the spi device.  We'd normally expect this type of register
+    // setup to be done at init time and that the first SPI transaction will
+    // be a READ_ID or READ_SFDP, thus latching the flash_status contents.
+    //
+    // In this test program, we simply issue a NOP transaction to the device.
+    spi.run_transaction(&mut [Transfer::Write(&[SpiFlash::NOP])])?;
+    let value = SpiFlash::read_status_ex(&*spi, None)?;
+    assert_eq!(value, sr.status);
+    Ok(())
+}
+
 fn main() -> Result<()> {
     let opts = Opts::from_args();
     opts.init.init_logging();
@@ -71,5 +145,8 @@
     uart.clear_rx_buffer()?;
 
     execute_test!(test_jedec_id, &opts, &transport);
+    execute_test!(test_enter_exit_4b_mode, &opts, &transport);
+    execute_test!(test_write_enable_disable, &opts, &transport);
+    execute_test!(test_read_status_extended, &opts, &transport);
     Ok(())
 }