Emulate the device on a second sandbox SPI bus and exercise the driver
through the spi_flash uclass.

The emulation implements byte, page and sequential operation and
advances the internal address counter the way the data sheet describes
for each of them. A transfer running past the 32 byte page of the
device therefore only comes back unharmed once the driver has selected
sequential operation, which is what its probe is for. The test asserts
that mode directly as well, so a failure says which of the two went
wrong.

The erase is checked on both counts that matter for a device that has
nothing to erase: that the area reads back as 0xff afterwards, and that
the fill stops at the end of the range it was given rather than running
into the data next to it.

Signed-off-by: Heiko Schocher <[email protected]>
---

 arch/sandbox/dts/test.dts         |  14 +++
 arch/sandbox/include/asm/test.h   |   8 ++
 configs/sandbox_defconfig         |   1 +
 drivers/mtd/spi/Kconfig           |   9 ++
 drivers/mtd/spi/Makefile          |   1 +
 drivers/mtd/spi/spi_ram_sandbox.c | 189 ++++++++++++++++++++++++++++++
 drivers/spi/Kconfig               |   2 +-
 test/dm/Makefile                  |   1 +
 test/dm/spi_ram.c                 |  96 +++++++++++++++
 9 files changed, 320 insertions(+), 1 deletion(-)
 create mode 100644 drivers/mtd/spi/spi_ram_sandbox.c
 create mode 100644 test/dm/spi_ram.c

diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index a5d8d3977a0..e192be1d1b5 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -1644,6 +1644,20 @@
                };
        };
 
+       spi@1 {
+               #address-cells = <1>;
+               #size-cells = <0>;
+               reg = <1 1>;
+               compatible = "sandbox,spi";
+
+               sram@0 {
+                       reg = <0>;
+                       compatible = "microchip,mchp23lcv1024";
+                       spi-max-frequency = <20000000>;
+                       sandbox,emul = "sandbox_spi_ram_emul";
+               };
+       };
+
        syscon0: syscon@0 {
                compatible = "sandbox,syscon0";
                reg = <0x10 16>;
diff --git a/arch/sandbox/include/asm/test.h b/arch/sandbox/include/asm/test.h
index 0438790028b..9f25cc690cf 100644
--- a/arch/sandbox/include/asm/test.h
+++ b/arch/sandbox/include/asm/test.h
@@ -251,6 +251,14 @@ uint sandbox_spi_get_mode(struct udevice *dev);
  */
 uint sandbox_spi_get_wordlen(struct udevice *dev);
 
+/**
+ * sandbox_spi_ram_is_sequential() - check the mode of the emulated SPI SRAM
+ *
+ * @dev:       the emulation device
+ * Return: true if the MODE register selects sequential operation
+ */
+bool sandbox_spi_ram_is_sequential(struct udevice *dev);
+
 /**
  * sandbox_get_pch_spi_protect() - Get the PCI SPI protection status
  *
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index f39d2bb062f..23ef9670c0e 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -277,6 +277,7 @@ CONFIG_SPI_FLASH_STMICRO=y
 CONFIG_SPI_FLASH_SST=y
 CONFIG_SPI_FLASH_WINBOND=y
 CONFIG_SPI_RAM=y
+CONFIG_SPI_RAM_SANDBOX=y
 CONFIG_NVMXIP_QSPI=y
 CONFIG_MULTIPLEXER=y
 CONFIG_MUX_MMIO=y
diff --git a/drivers/mtd/spi/Kconfig b/drivers/mtd/spi/Kconfig
index 22bdbc27f9b..1a6daafc94a 100644
--- a/drivers/mtd/spi/Kconfig
+++ b/drivers/mtd/spi/Kconfig
@@ -259,6 +259,15 @@ config SPI_RAM
 
          If unsure, say N
 
+config SPI_RAM_SANDBOX
+       bool "Sandbox emulation of the Microchip 23LCV1024"
+       depends on SANDBOX && SPI_RAM
+       help
+         Emulate a 23LCV1024 on the sandbox SPI bus, so that the driver can
+         be exercised by the unit tests. The emulation implements all three
+         operating modes of the device, which is what makes it able to tell
+         whether the driver selects the right one.
+
 config SPI_FLASH_USE_4K_SECTORS
        bool "Use small 4096 B erase sectors"
        depends on SPI_FLASH
diff --git a/drivers/mtd/spi/Makefile b/drivers/mtd/spi/Makefile
index 324c90d423f..2581a735286 100644
--- a/drivers/mtd/spi/Makefile
+++ b/drivers/mtd/spi/Makefile
@@ -19,6 +19,7 @@ endif
 obj-$(CONFIG_SPI_FLASH) += spi-nor.o
 obj-$(CONFIG_SPI_FLASH_DATAFLASH) += sf_dataflash.o
 obj-$(CONFIG_SPI_RAM) += spi_ram.o
+obj-$(CONFIG_SPI_RAM_SANDBOX) += spi_ram_sandbox.o
 obj-$(CONFIG_$(PHASE_)SPI_FLASH_MTD) += sf_mtd.o
 obj-$(CONFIG_SPI_FLASH_SANDBOX) += sandbox.o
 obj-$(CONFIG_$(PHASE_)BOOTDEV_SPI_FLASH) += sf_bootdev.o
diff --git a/drivers/mtd/spi/spi_ram_sandbox.c 
b/drivers/mtd/spi/spi_ram_sandbox.c
new file mode 100644
index 00000000000..b785010aac0
--- /dev/null
+++ b/drivers/mtd/spi/spi_ram_sandbox.c
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Sandbox emulation of the Microchip 23LCV1024 SPI SRAM
+ *
+ * Copyright (C) 2026 Heiko Schocher, Nabla Software Engineering 
<[email protected]>
+ */
+
+#include <dm.h>
+#include <log.h>
+#include <malloc.h>
+#include <spi.h>
+#include <asm/test.h>
+#include <linux/bitops.h>
+#include <linux/sizes.h>
+
+/* Instruction set of the 23x series serial SRAMs */
+#define MCHP23_CMD_WRITE_STATUS        0x01
+#define MCHP23_CMD_WRITE       0x02
+#define MCHP23_CMD_READ                0x03
+
+/* MODE register, bits 7:6 select byte, page or sequential operation */
+#define MCHP23_MODE_MASK       GENMASK(7, 6)
+#define MCHP23_MODE_BYTE       0x00
+#define MCHP23_MODE_SEQ                BIT(6)
+#define MCHP23_MODE_PAGE       BIT(7)
+
+#define MCHP23_PAGE_SIZE       32
+#define MCHP23_ADDR_LEN                3
+#define MCHP23_SIZE            SZ_128K
+
+/**
+ * struct sandbox_spi_ram_priv - state of the emulated SRAM
+ *
+ * @data:      the emulated memory array
+ * @mode:      contents of the MODE register, see MCHP23_MODE_*
+ * @cmd:       opcode of the command being processed
+ * @addr:      address that command started at
+ */
+struct sandbox_spi_ram_priv {
+       u8 *data;
+       u8 mode;
+       u8 cmd;
+       u32 addr;
+};
+
+bool sandbox_spi_ram_is_sequential(struct udevice *dev)
+{
+       struct sandbox_spi_ram_priv *priv = dev_get_priv(dev);
+
+       return priv->mode == MCHP23_MODE_SEQ;
+}
+
+/**
+ * sandbox_spi_ram_addr() - address of the n-th byte of the current command
+ *
+ * The internal address counter of the device is advanced differently in
+ * each of the three operating modes, which is what makes the mode matter
+ * for transfers of more than one byte.
+ *
+ * @priv:      state of the emulated SRAM
+ * @n:         position of the byte within the data phase
+ * Return: index into the emulated memory array
+ */
+static u32 sandbox_spi_ram_addr(struct sandbox_spi_ram_priv *priv, uint n)
+{
+       switch (priv->mode) {
+       case MCHP23_MODE_SEQ:
+               /* The counter runs across the whole array and wraps around */
+               return (priv->addr + n) % MCHP23_SIZE;
+       case MCHP23_MODE_PAGE:
+               /* The counter wraps around within the addressed page */
+               return (priv->addr & ~(MCHP23_PAGE_SIZE - 1)) |
+                      ((priv->addr + n) & (MCHP23_PAGE_SIZE - 1));
+       default:
+               /* Byte operation transfers one byte and gets no further */
+               return priv->addr;
+       }
+}
+
+/*
+ * The command and the data it carries arrive as two transfers, the first
+ * one holding the opcode and, for a read or a write, the address.
+ */
+static int sandbox_spi_ram_xfer(struct udevice *dev, unsigned int bitlen,
+                               const void *dout, void *din, unsigned long 
flags)
+{
+       struct sandbox_spi_ram_priv *priv = dev_get_priv(dev);
+       const u8 *out = dout;
+       u8 *in = din;
+       uint bytes = bitlen / 8;
+       uint n;
+
+       if (bitlen % 8) {
+               log_err("transfer of %u bits is not a whole number of bytes\n",
+                       bitlen);
+               return -EINVAL;
+       }
+
+       if (flags & SPI_XFER_BEGIN) {
+               if (!out || !bytes)
+                       return -EINVAL;
+
+               priv->cmd = out[0];
+
+               switch (priv->cmd) {
+               case MCHP23_CMD_WRITE_STATUS:
+                       if (bytes < 2)
+                               return -EINVAL;
+                       priv->mode = out[1] & MCHP23_MODE_MASK;
+                       break;
+               case MCHP23_CMD_READ:
+               case MCHP23_CMD_WRITE:
+                       if (bytes < 1 + MCHP23_ADDR_LEN)
+                               return -EINVAL;
+                       priv->addr = (out[1] << 16) | (out[2] << 8) | out[3];
+                       if (priv->addr >= MCHP23_SIZE)
+                               return -EINVAL;
+                       break;
+               default:
+                       log_err("unknown command %#x\n", priv->cmd);
+                       return -EINVAL;
+               }
+
+               return 0;
+       }
+
+       for (n = 0; n < bytes; n++) {
+               u32 addr = sandbox_spi_ram_addr(priv, n);
+
+               switch (priv->cmd) {
+               case MCHP23_CMD_READ:
+                       if (!in)
+                               return -EINVAL;
+                       in[n] = priv->data[addr];
+                       break;
+               case MCHP23_CMD_WRITE:
+                       if (!out)
+                               return -EINVAL;
+                       priv->data[addr] = out[n];
+                       break;
+               default:
+                       return -EINVAL;
+               }
+       }
+
+       return 0;
+}
+
+static int sandbox_spi_ram_probe(struct udevice *dev)
+{
+       struct sandbox_spi_ram_priv *priv = dev_get_priv(dev);
+
+       /* The device comes out of reset in byte operation */
+       priv->mode = MCHP23_MODE_BYTE;
+
+       priv->data = calloc(1, MCHP23_SIZE);
+       if (!priv->data)
+               return -ENOMEM;
+
+       return 0;
+}
+
+static int sandbox_spi_ram_remove(struct udevice *dev)
+{
+       struct sandbox_spi_ram_priv *priv = dev_get_priv(dev);
+
+       free(priv->data);
+       priv->data = NULL;
+
+       return 0;
+}
+
+static const struct dm_spi_emul_ops sandbox_spi_ram_emul_ops = {
+       .xfer = sandbox_spi_ram_xfer,
+};
+
+/*
+ * No of_match, so that a device tree scan never binds this driver. It is
+ * bound by name, on the node of the slave it emulates, which already
+ * carries the compatible of the real device.
+ */
+U_BOOT_DRIVER(sandbox_spi_ram_emul) = {
+       .name           = "sandbox_spi_ram_emul",
+       .id             = UCLASS_SPI_EMUL,
+       .probe          = sandbox_spi_ram_probe,
+       .remove         = sandbox_spi_ram_remove,
+       .priv_auto      = sizeof(struct sandbox_spi_ram_priv),
+       .ops            = &sandbox_spi_ram_emul_ops,
+};
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index 007ad5e7733..7127337c6ac 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -518,7 +518,7 @@ config SANDBOX_SPI
 config SANDBOX_SPI_MAX_BUS
        int
        depends on SANDBOX
-       default 1
+       default 2
 
 config SANDBOX_SPI_MAX_CS
        int
diff --git a/test/dm/Makefile b/test/dm/Makefile
index 486e6b956fc..97102c051cf 100644
--- a/test/dm/Makefile
+++ b/test/dm/Makefile
@@ -119,6 +119,7 @@ obj-$(CONFIG_SIMPLE_PM_BUS) += simple-pm-bus.o
 obj-$(CONFIG_SOC_DEVICE) += soc.o
 obj-$(CONFIG_SOUND) += sound.o
 obj-$(CONFIG_DM_SPI) += spi.o
+obj-$(CONFIG_SPI_RAM) += spi_ram.o
 obj-$(CONFIG_SPMI) += spmi.o
 obj-y += syscon.o
 obj-$(CONFIG_RESET_SYSCON) += syscon-reset.o
diff --git a/test/dm/spi_ram.c b/test/dm/spi_ram.c
new file mode 100644
index 00000000000..3de6a5ab025
--- /dev/null
+++ b/test/dm/spi_ram.c
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for the Microchip 23LCV1024 SPI SRAM
+ *
+ * Copyright (C) 2026 Heiko Schocher, Nabla Software Engineering 
<[email protected]>
+ */
+
+#include <dm.h>
+#include <spi.h>
+#include <spi_flash.h>
+#include <asm/state.h>
+#include <asm/test.h>
+#include <dm/test.h>
+#include <linux/sizes.h>
+#include <test/test.h>
+#include <test/ut.h>
+
+#define SPI_RAM_PATH   "/spi@1/sram@0"
+#define SPI_RAM_CS     0
+
+/*
+ * Long enough to run past the 32 byte page of the device, and starting
+ * just before a page boundary, so that the transfer only comes back
+ * unharmed if the driver put the device into sequential operation.
+ */
+#define SPI_RAM_OFFSET 0x1f
+#define SPI_RAM_LEN    100
+
+static int dm_test_spi_ram(struct unit_test_state *uts)
+{
+       struct sandbox_state *state = state_get_current();
+       struct udevice *dev, *bus, *emul;
+       struct spi_flash *flash;
+       u8 src[SPI_RAM_LEN], dst[SPI_RAM_LEN];
+       int i;
+
+       ut_assertok(uclass_get_device_by_ofnode(UCLASS_SPI_FLASH,
+                                               ofnode_path(SPI_RAM_PATH),
+                                               &dev));
+
+       flash = dev_get_uclass_priv(dev);
+       ut_asserteq(SZ_128K, flash->size);
+       ut_asserteq_str("mchp23lcv1024", flash->name);
+
+       /*
+        * The device is useless in the byte operation it powers up in, so
+        * the probe has to have switched it over.
+        */
+       bus = dev_get_parent(dev);
+       emul = state->spi[dev_seq(bus)][SPI_RAM_CS].emul;
+       ut_assertnonnull(emul);
+       ut_assert(sandbox_spi_ram_is_sequential(emul));
+
+       for (i = 0; i < SPI_RAM_LEN; i++)
+               src[i] = i + 1;
+
+       ut_assertok(spi_flash_write_dm(dev, SPI_RAM_OFFSET, SPI_RAM_LEN, src));
+
+       memset(dst, 0, SPI_RAM_LEN);
+       ut_assertok(spi_flash_read_dm(dev, SPI_RAM_OFFSET, SPI_RAM_LEN, dst));
+       ut_asserteq_mem(src, dst, SPI_RAM_LEN);
+
+       /* Neighbouring cells must not have been touched */
+       ut_assertok(spi_flash_read_dm(dev, 0, SPI_RAM_OFFSET, dst));
+       for (i = 0; i < SPI_RAM_OFFSET; i++)
+               ut_asserteq(0, dst[i]);
+
+       /*
+        * A RAM needs no erase before a write, but an erase that was asked
+        * for has to leave behind what an erased flash reads, and it has to
+        * stop at the end of the range it was given.
+        */
+       ut_assertok(spi_flash_erase_dm(dev, 0, SPI_RAM_OFFSET));
+
+       ut_assertok(spi_flash_read_dm(dev, 0, SPI_RAM_OFFSET, dst));
+       for (i = 0; i < SPI_RAM_OFFSET; i++)
+               ut_asserteq(0xff, dst[i]);
+
+       ut_assertok(spi_flash_read_dm(dev, SPI_RAM_OFFSET, SPI_RAM_LEN, dst));
+       ut_asserteq_mem(src, dst, SPI_RAM_LEN);
+
+       /* Now erase the data itself, over more than one staging step */
+       ut_assertok(spi_flash_erase_dm(dev, SPI_RAM_OFFSET, SPI_RAM_LEN));
+       ut_assertok(spi_flash_read_dm(dev, SPI_RAM_OFFSET, SPI_RAM_LEN, dst));
+       for (i = 0; i < SPI_RAM_LEN; i++)
+               ut_asserteq(0xff, dst[i]);
+
+       /*
+        * Since we are about to destroy all devices, we must tell sandbox
+        * to forget the emulation device
+        */
+       sandbox_sf_unbind_emul(state, dev_seq(bus), SPI_RAM_CS);
+
+       return 0;
+}
+DM_TEST(dm_test_spi_ram, UTF_SCAN_PDATA | UTF_SCAN_FDT);
-- 
2.55.0

Reply via email to