Emulate the device on a second sandbox SPI bus and exercise the driver
through the MTD layer.

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.

Test also if erase returns -EOPNOTSUPP and data is not touched.

A second test gives the device two partitions in the test device tree
and writes through the one whose offset is on no erase block boundary,
which a device without an erase size does not have to care about.

Signed-off-by: Heiko Schocher <[email protected]>
---
checkpatch asks for a blank line after the two test functions, before
the DM_TEST() that registers them. But in test/dm/ all over is without
a blank line, so ignore this warning.

Changes in v2:
Added comments from Simon:
- The test drives the device through the MTD layer instead of the
  spi_flash uclass.
- Uses sandbox_spi_get_emul() rather than reaching into state->spi[][].
- Added a spi1 alias so that dev_seq() of the bus does not depend on
  probe order.
- The erase part checks for -EOPNOTSUPP now. The v1 comment about
  covering more than one staging step went with the erase loop.
- The emulation lives in its own node with its own compatible, which is
  what patch 4 resolves the phandle to.
- Added the emulation and the test to the MAINTAINERS entry.
Found while Stefan Bigler tested the series on hardware:
- Added partitions to the device in the test device tree and a second
  test that writes through one of them, which is what "mtd list" on his
  board fell over.

 MAINTAINERS                     |   2 +
 arch/sandbox/dts/test.dts       |  37 +++++++
 arch/sandbox/include/asm/test.h |   8 ++
 configs/sandbox_defconfig       |   1 +
 drivers/mtd/Kconfig             |   9 ++
 drivers/mtd/Makefile            |   1 +
 drivers/mtd/spi_ram_sandbox.c   | 190 ++++++++++++++++++++++++++++++++
 drivers/spi/Kconfig             |   2 +-
 test/dm/Makefile                |   1 +
 test/dm/spi_ram.c               | 167 ++++++++++++++++++++++++++++
 10 files changed, 417 insertions(+), 1 deletion(-)
 create mode 100644 drivers/mtd/spi_ram_sandbox.c
 create mode 100644 test/dm/spi_ram.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 1669972e724..7a03e075e5b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1653,6 +1653,8 @@ SPI RAM
 M:     Heiko Schocher <[email protected]>
 S:     Maintained
 F:     drivers/mtd/spi_ram.c
+F:     drivers/mtd/spi_ram_sandbox.c
+F:     test/dm/spi_ram.c
 
 SPI-NOR
 M:     Vignesh R <[email protected]>
diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index a5d8d3977a0..39f7cedda41 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -57,6 +57,7 @@
                rtc0 = &rtc_0;
                rtc1 = &rtc_1;
                spi0 = "/spi@0";
+               spi1 = "/spi@1";
                testfdt6 = "/e-test";
                testbus3 = "/some-bus";
                testfdt0 = "/some-bus/c-test@0";
@@ -1644,6 +1645,42 @@
                };
        };
 
+       spi@1 {
+               #address-cells = <1>;
+               #size-cells = <0>;
+               reg = <1 1>;
+               compatible = "sandbox,spi";
+
+               sram@0 {
+                       #address-cells = <1>;
+                       #size-cells = <1>;
+                       reg = <0>;
+                       compatible = "microchip,mchp23lcv1024";
+                       spi-max-frequency = <20000000>;
+                       sandbox,emul = <&spi_ram_emul>;
+
+                       spi_ram_emul: emul {
+                               compatible = "sandbox,spi-ram-emul";
+                       };
+
+                       partitions {
+                               compatible = "fixed-partitions";
+                               #address-cells = <1>;
+                               #size-cells = <1>;
+
+                               partition@0 {
+                                       label = "spi-ram-part0";
+                                       reg = <0x0 0xa000>;
+                               };
+
+                               partition@a000 {
+                                       label = "spi-ram-part1";
+                                       reg = <0xa000 0x1000>;
+                               };
+                       };
+               };
+       };
+
        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 0210d54f462..264f57418c8 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -261,6 +261,7 @@ CONFIG_MMC_SANDBOX=y
 CONFIG_MMC_SDHCI=y
 CONFIG_DM_MTD=y
 CONFIG_SPI_RAM=y
+CONFIG_SPI_RAM_SANDBOX=y
 CONFIG_MTD_RAW_NAND=y
 CONFIG_SYS_MAX_NAND_DEVICE=8
 CONFIG_SYS_NAND_USE_FLASH_BBT=y
diff --git a/drivers/mtd/Kconfig b/drivers/mtd/Kconfig
index 437e6ebd196..bfd241edfc3 100644
--- a/drivers/mtd/Kconfig
+++ b/drivers/mtd/Kconfig
@@ -208,6 +208,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 RENESAS_RPC_HF
        bool "Renesas R-Car Gen3 RPC HyperFlash driver"
        depends on RCAR_64 && DM_MTD
diff --git a/drivers/mtd/Makefile b/drivers/mtd/Makefile
index a19498da920..e61580f95de 100644
--- a/drivers/mtd/Makefile
+++ b/drivers/mtd/Makefile
@@ -14,6 +14,7 @@ mtd-$(CONFIG_FLASH_CFI_LEGACY) += jedec_flash.o
 mtd-$(CONFIG_STM32_FLASH) += stm32_flash.o
 mtd-$(CONFIG_RENESAS_RPC_HF) += renesas_rpc_hf.o
 mtd-$(CONFIG_SPI_RAM) += spi_ram.o
+mtd-$(CONFIG_SPI_RAM_SANDBOX) += spi_ram_sandbox.o
 mtd-$(CONFIG_HBMC_AM654) += hbmc-am654.o
 
 # U-Boot build
diff --git a/drivers/mtd/spi_ram_sandbox.c b/drivers/mtd/spi_ram_sandbox.c
new file mode 100644
index 00000000000..520c3077a5e
--- /dev/null
+++ b/drivers/mtd/spi_ram_sandbox.c
@@ -0,0 +1,190 @@
+// 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,
+};
+
+static const struct udevice_id sandbox_spi_ram_ids[] = {
+       { .compatible = "sandbox,spi-ram-emul" },
+       { }
+};
+
+U_BOOT_DRIVER(sandbox_spi_ram_emul) = {
+       .name           = "sandbox_spi_ram_emul",
+       .id             = UCLASS_SPI_EMUL,
+       .of_match       = sandbox_spi_ram_ids,
+       .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..1020fe1f6ee
--- /dev/null
+++ b/test/dm/spi_ram.c
@@ -0,0 +1,167 @@
+// 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 <mtd.h>
+#include <spi.h>
+#include <spi_flash.h>
+#include <asm/state.h>
+#include <asm/test.h>
+#include <dm/test.h>
+#include <linux/mtd/mtd.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
+
+/*
+ * The second partition of the device in the test device tree. Its offset is
+ * on no erase block boundary, which a device that cannot erase does not have
+ * to care about.
+ */
+#define SPI_RAM_PART           "spi-ram-part1"
+#define SPI_RAM_PART_OFFSET    0xa000
+#define SPI_RAM_PART_SIZE      0x1000
+
+static int dm_test_spi_ram(struct unit_test_state *uts)
+{
+       struct sandbox_state *state = state_get_current();
+       struct udevice *dev, *bus, *emul;
+       struct erase_info instr = { };
+       struct mtd_info *mtd, *found;
+       u8 src[SPI_RAM_LEN], dst[SPI_RAM_LEN];
+       struct spi_slave *slave;
+       size_t retlen;
+       int i;
+
+       ut_assertok(uclass_get_device_by_ofnode(UCLASS_MTD,
+                                               ofnode_path(SPI_RAM_PATH),
+                                               &dev));
+
+       mtd = dev_get_uclass_priv(dev);
+       ut_asserteq(SZ_128K, mtd->size);
+       ut_asserteq(MTD_RAM, mtd->type);
+
+       /*
+        * Registering with the MTD layer is what makes the mtd command able
+        * to reach the device, so check the name it was given resolves back
+        * to it rather than checking the string itself.
+        */
+       found = get_mtd_device_nm(mtd->name);
+       ut_asserteq_ptr(mtd, found);
+       put_mtd_device(found);
+
+       /*
+        * 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);
+       ut_assertok(spi_get_bus_and_cs(dev_seq(bus), SPI_RAM_CS, &bus, &slave));
+       ut_assertok(sandbox_spi_get_emul(state, bus, slave->dev, &emul));
+       ut_assert(sandbox_spi_ram_is_sequential(emul));
+
+       for (i = 0; i < SPI_RAM_LEN; i++)
+               src[i] = i + 1;
+
+       ut_assertok(mtd_write(mtd, SPI_RAM_OFFSET, SPI_RAM_LEN, &retlen, src));
+       ut_asserteq(SPI_RAM_LEN, retlen);
+
+       memset(dst, 0, SPI_RAM_LEN);
+       ut_assertok(mtd_read(mtd, SPI_RAM_OFFSET, SPI_RAM_LEN, &retlen, dst));
+       ut_asserteq(SPI_RAM_LEN, retlen);
+       ut_asserteq_mem(src, dst, SPI_RAM_LEN);
+
+       /* Neighbouring cells must not have been touched */
+       ut_assertok(mtd_read(mtd, 0, SPI_RAM_OFFSET, &retlen, dst));
+       for (i = 0; i < SPI_RAM_OFFSET; i++)
+               ut_asserteq(0, dst[i]);
+
+       /*
+        * The device cannot erase and says so through MTD_NO_ERASE, so a
+        * caller asking for one has to be told rather than left believing
+        * the area was cleared.
+        */
+       ut_assert(mtd->flags & MTD_NO_ERASE);
+       instr.addr = 0;
+       instr.len = SPI_RAM_OFFSET;
+       ut_asserteq(-EOPNOTSUPP, mtd_erase(mtd, &instr));
+
+       ut_assertok(mtd_read(mtd, SPI_RAM_OFFSET, SPI_RAM_LEN, &retlen, dst));
+       ut_asserteq_mem(src, dst, SPI_RAM_LEN);
+
+       /*
+        * Since we are about to destroy all devices, we must tell sandbox
+        * to forget the emulation device
+        */
+       sandbox_spi_unbind_emul(state, dev_seq(bus), SPI_RAM_CS);
+
+       return 0;
+}
+DM_TEST(dm_test_spi_ram, UTF_SCAN_PDATA | UTF_SCAN_FDT);
+
+static int dm_test_spi_ram_part(struct unit_test_state *uts)
+{
+       struct sandbox_state *state = state_get_current();
+       struct udevice *dev, *bus;
+       struct mtd_info *mtd, *part;
+       u8 src[SPI_RAM_LEN], dst[SPI_RAM_LEN];
+       size_t retlen;
+       int i;
+
+       ut_assertok(uclass_get_device_by_ofnode(UCLASS_MTD,
+                                               ofnode_path(SPI_RAM_PATH),
+                                               &dev));
+       mtd = dev_get_uclass_priv(dev);
+
+       /*
+        * This is what the mtd command does through mtd_probe_devices(),
+        * called here for this device alone so that the test does not depend
+        * on what else is registered with the MTD layer.
+        */
+       ut_assertok(add_mtd_partitions_of(mtd));
+
+       part = get_mtd_device_nm(SPI_RAM_PART);
+       ut_assert(!IS_ERR_OR_NULL(part));
+
+       /*
+        * The device has no erase size to align a partition to, so the
+        * partition is aligned on the write size and stays writeable, at
+        * whatever offset the device tree puts it.
+        */
+       ut_assert(part->flags & MTD_NO_ERASE);
+       ut_assert(part->flags & MTD_WRITEABLE);
+       ut_asserteq(SPI_RAM_PART_SIZE, part->size);
+
+       for (i = 0; i < SPI_RAM_LEN; i++)
+               src[i] = 0xa0 + i;
+
+       ut_assertok(mtd_write(part, 0, SPI_RAM_LEN, &retlen, src));
+       ut_asserteq(SPI_RAM_LEN, retlen);
+       put_mtd_device(part);
+
+       /* what the partition took has to land at its offset in the device */
+       memset(dst, 0, SPI_RAM_LEN);
+       ut_assertok(mtd_read(mtd, SPI_RAM_PART_OFFSET, SPI_RAM_LEN, &retlen,
+                            dst));
+       ut_asserteq_mem(src, dst, SPI_RAM_LEN);
+
+       bus = dev_get_parent(dev);
+       sandbox_spi_unbind_emul(state, dev_seq(bus), SPI_RAM_CS);
+
+       return 0;
+}
+DM_TEST(dm_test_spi_ram_part, UTF_SCAN_PDATA | UTF_SCAN_FDT);
-- 
2.55.0

Reply via email to