Add a sandbox emulator for an AT25-style SPI EEPROM so the new SPI
EEPROM uclass can be exercised without real hardware.

The sandbox SPI controller previously always bound the SPI flash
emulator for every chip select. Allow a slave node to select its own
emulator through a sandbox,emul phandle (mirroring the sandbox I2C
bus), falling back to the SPI flash emulator when none is given so that
existing behaviour is preserved.

A test EEPROM is wired up on chip select 3 of the sandbox SPI bus and a
test/dm test verifies size reporting, patterned reads, out-of-bounds
rejection and the unimplemented write path. The node follows the
atmel,at25 binding: it carries the "atmel,at25" fallback compatible and
the required size, pagesize and address-width properties.

Signed-off-by: João Loureiro <[email protected]>
Reviewed-by: Simon Glass <[email protected]>
Tested-by: Simon Glass <[email protected]>  # sandbox
---
Simon: I have kept your Tested-by, but please do re-test rather than
rely on it. The v4 tree segfaulted on a full "ut dm" - test/dm/sf.c
took the first UCLASS_SPI_EMUL device as the flash emulator, which the
emulator added here displaces, so sandbox_sf_set_block_protect() wrote
past the end of the wrong private struct. Running "ut dm spi_eeprom"
alone passes and hides it. Patch 1 fixes the test.

Also changed since v4: the DT node now carries the "atmel,at25"
fallback compatible with the required size/pagesize/address-width, and
the emulator includes drivers/misc/spi_eeprom_priv.h.
 MAINTAINERS                    |   2 +
 arch/sandbox/dts/test.dts      |  16 +++-
 drivers/misc/Makefile          |   1 +
 drivers/misc/spi_eeprom_emul.c | 130 +++++++++++++++++++++++++++++++++
 drivers/spi/sandbox_spi.c      |  37 +++++++++-
 test/dm/Makefile               |   1 +
 test/dm/spi_eeprom.c           |  46 ++++++++++++
 7 files changed, 231 insertions(+), 2 deletions(-)
 create mode 100644 drivers/misc/spi_eeprom_emul.c
 create mode 100644 test/dm/spi_eeprom.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 893cf421378..92e26829e1a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1712,8 +1712,10 @@ SPI EEPROM
 M:     João Loureiro <[email protected]>
 S:     Maintained
 F:     drivers/misc/spi_eeprom.c
+F:     drivers/misc/spi_eeprom_emul.c
 F:     drivers/misc/spi_eeprom_priv.h
 F:     include/spi_eeprom.h
+F:     test/dm/spi_eeprom.c
 
 SPI NAND
 M:     Dario Binacchi <[email protected]>
diff --git a/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts
index d24feec5422..7d7f0376348 100644
--- a/arch/sandbox/dts/test.dts
+++ b/arch/sandbox/dts/test.dts
@@ -1618,7 +1618,7 @@
                #size-cells = <0>;
                reg = <0 1>;
                compatible = "sandbox,spi";
-               cs-gpios = <0>, <0>, <&gpio_a 0>;
+               cs-gpios = <0>, <0>, <&gpio_a 0>, <0>;
                pinctrl-names = "default";
                pinctrl-0 = <&pinmux_spi0_pins>;
 
@@ -1636,6 +1636,20 @@
                        spi-cpol;
                        spi-cpha;
                };
+               eeprom@3 {
+                       reg = <3>;
+                       compatible = "microchip,at25160bn", "atmel,at25";
+                       spi-max-frequency = <1000000>;
+                       size = <2048>;
+                       pagesize = <32>;
+                       address-width = <16>;
+                       sandbox,emul = <&spi_eeprom_emul>;
+               };
+       };
+
+       spi_eeprom_emul: spi-eeprom-emul {
+               compatible = "sandbox,spi-eeprom";
+               sandbox,size = <2048>;
        };
 
        syscon0: syscon@0 {
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index 8c9f8eb9cfa..8fa0fe75b53 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -45,6 +45,7 @@ obj-$(CONFIG_IRQ) += irq-uclass.o
 obj-$(CONFIG_SANDBOX) += irq_sandbox.o irq_sandbox_test.o
 obj-$(CONFIG_$(PHASE_)I2C_EEPROM) += i2c_eeprom.o
 obj-$(CONFIG_$(PHASE_)SPI_EEPROM) += spi_eeprom.o
+obj-$(CONFIG_SANDBOX) += spi_eeprom_emul.o
 obj-$(CONFIG_IHS_FPGA) += ihs_fpga.o
 obj-$(CONFIG_IMX8) += imx8/
 obj-$(CONFIG_IMX_ELE) += imx_ele/
diff --git a/drivers/misc/spi_eeprom_emul.c b/drivers/misc/spi_eeprom_emul.c
new file mode 100644
index 00000000000..397e73cf0cd
--- /dev/null
+++ b/drivers/misc/spi_eeprom_emul.c
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Sandbox emulation of an AT25-style SPI EEPROM.
+ *
+ * Copyright (c) 2024 Koninklijke Philips N.V.
+ * Copyright (c) 2026 João Loureiro <[email protected]>
+ */
+
+#define LOG_CATEGORY UCLASS_SPI_EMUL
+
+#include <dm.h>
+#include <malloc.h>
+#include <spi.h>
+#include <linux/err.h>
+
+#include "spi_eeprom_priv.h"
+
+#define SANDBOX_SPI_EEPROM_SIZE                2048
+
+/**
+ * struct sandbox_spi_eeprom - state of the emulated EEPROM
+ *
+ * @data:      Backing store for the memory array
+ * @size:      Size of the memory array in bytes
+ * @cmd:       Opcode of the command currently being processed
+ * @cmd_len:   Number of command/address bytes consumed so far
+ * @addr:      Byte address decoded from the command
+ */
+struct sandbox_spi_eeprom {
+       u8 *data;
+       uint size;
+       u8 cmd;
+       uint cmd_len;
+       uint addr;
+};
+
+static int sandbox_spi_eeprom_xfer(struct udevice *dev, uint bitlen,
+                                  const void *dout, void *din, ulong flags)
+{
+       struct sandbox_spi_eeprom *priv = dev_get_priv(dev);
+       const u8 *tx = dout;
+       u8 *rx = din;
+       uint bytes = bitlen / 8;
+       uint i;
+
+       if (bitlen % 8)
+               return -EINVAL;
+
+       /* A new transaction starts with the command/address phase */
+       if (flags & SPI_XFER_BEGIN) {
+               priv->cmd = 0;
+               priv->cmd_len = 0;
+               priv->addr = 0;
+       }
+
+       for (i = 0; i < bytes; i++) {
+               if (tx) {
+                       /* Command phase: first byte is the opcode */
+                       if (priv->cmd_len == 0)
+                               priv->cmd = tx[i];
+                       else if (priv->cmd == AT25_CMD_READ_DATA)
+                               priv->addr = (priv->addr << 8) | tx[i];
+                       priv->cmd_len++;
+               } else if (rx) {
+                       /* Data phase: serve the requested register/array */
+                       switch (priv->cmd) {
+                       case AT25_CMD_READ_STATUS:
+                               /* Ready, write disabled, not protected */
+                               rx[i] = 0x00;
+                               break;
+                       case AT25_CMD_READ_DATA:
+                               rx[i] = priv->addr < priv->size ?
+                                       priv->data[priv->addr] : 0xff;
+                               priv->addr++;
+                               break;
+                       default:
+                               rx[i] = 0xff;
+                               break;
+                       }
+               }
+       }
+
+       return 0;
+}
+
+static int sandbox_spi_eeprom_probe(struct udevice *dev)
+{
+       struct sandbox_spi_eeprom *priv = dev_get_priv(dev);
+       uint i;
+
+       priv->size = dev_read_u32_default(dev, "sandbox,size",
+                                         SANDBOX_SPI_EEPROM_SIZE);
+       priv->data = calloc(1, priv->size);
+       if (!priv->data)
+               return -ENOMEM;
+
+       /* Fill with a known pattern so reads can be verified */
+       for (i = 0; i < priv->size; i++)
+               priv->data[i] = i & 0xff;
+
+       return 0;
+}
+
+static int sandbox_spi_eeprom_remove(struct udevice *dev)
+{
+       struct sandbox_spi_eeprom *priv = dev_get_priv(dev);
+
+       free(priv->data);
+
+       return 0;
+}
+
+static const struct dm_spi_emul_ops sandbox_spi_eeprom_ops = {
+       .xfer   = sandbox_spi_eeprom_xfer,
+};
+
+static const struct udevice_id sandbox_spi_eeprom_ids[] = {
+       { .compatible = "sandbox,spi-eeprom" },
+       { }
+};
+
+U_BOOT_DRIVER(sandbox_spi_eeprom) = {
+       .name           = "sandbox_spi_eeprom",
+       .id             = UCLASS_SPI_EMUL,
+       .of_match       = sandbox_spi_eeprom_ids,
+       .probe          = sandbox_spi_eeprom_probe,
+       .remove         = sandbox_spi_eeprom_remove,
+       .priv_auto      = sizeof(struct sandbox_spi_eeprom),
+       .ops            = &sandbox_spi_eeprom_ops,
+};
diff --git a/drivers/spi/sandbox_spi.c b/drivers/spi/sandbox_spi.c
index 3ee97d67f4a..aa4edcf64b2 100644
--- a/drivers/spi/sandbox_spi.c
+++ b/drivers/spi/sandbox_spi.c
@@ -77,6 +77,41 @@ static int sandbox_spi_set_wordlen(struct udevice *dev, 
unsigned int wordlen)
        return 0;
 }
 
+/**
+ * sandbox_spi_emul_get() - find the emulator for a SPI slave
+ *
+ * A slave can name a dedicated emulator through a "sandbox,emul" phandle (for
+ * example an SPI EEPROM). When no such phandle is present we fall back to the
+ * built-in SPI flash emulator, preserving the previous behaviour.
+ *
+ * @state: Sandbox state, used to cache the bound emulator per chip select
+ * @bus: SPI bus the slave is attached to
+ * @slave: SPI slave to find the emulator for
+ * @emulp: Returns the emulator device on success
+ * Return: 0 if OK, -ve on error
+ */
+static int sandbox_spi_emul_get(struct sandbox_state *state, struct udevice 
*bus,
+                               struct udevice *slave, struct udevice **emulp)
+{
+       struct sandbox_spi_info *info;
+       int ret;
+
+       info = &state->spi[dev_seq(bus)][spi_chip_select(slave)];
+       if (info->emul) {
+               *emulp = info->emul;
+               return 0;
+       }
+
+       ret = uclass_get_device_by_phandle(UCLASS_SPI_EMUL, slave, 
"sandbox,emul",
+                                          emulp);
+       if (!ret) {
+               info->emul = *emulp;
+               return 0;
+       }
+
+       return sandbox_spi_get_emul(state, bus, slave, emulp);
+}
+
 static int sandbox_spi_xfer(struct udevice *slave, unsigned int bitlen,
                            const void *dout, void *din, unsigned long flags)
 {
@@ -106,7 +141,7 @@ static int sandbox_spi_xfer(struct udevice *slave, unsigned 
int bitlen,
                       busnum, cs);
                return -ENOENT;
        }
-       ret = sandbox_spi_get_emul(state, bus, slave, &emul);
+       ret = sandbox_spi_emul_get(state, bus, slave, &emul);
        if (ret) {
                printf("%s: busnum=%u, cs=%u: no emulation available 
(err=%d)\n",
                       __func__, busnum, cs, ret);
diff --git a/test/dm/Makefile b/test/dm/Makefile
index fb3e6a7008f..3a8599d6bc2 100644
--- a/test/dm/Makefile
+++ b/test/dm/Makefile
@@ -120,6 +120,7 @@ obj-$(CONFIG_SMEM) += smem.o
 obj-$(CONFIG_SOC_DEVICE) += soc.o
 obj-$(CONFIG_SOUND) += sound.o
 obj-$(CONFIG_DM_SPI) += spi.o
+obj-$(CONFIG_SPI_EEPROM) += spi_eeprom.o
 obj-$(CONFIG_SPMI) += spmi.o
 obj-y += syscon.o
 obj-$(CONFIG_RESET_SYSCON) += syscon-reset.o
diff --git a/test/dm/spi_eeprom.c b/test/dm/spi_eeprom.c
new file mode 100644
index 00000000000..b943ada0035
--- /dev/null
+++ b/test/dm/spi_eeprom.c
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Tests for the SPI EEPROM uclass
+ *
+ * Copyright (c) 2024 Koninklijke Philips N.V.
+ * Copyright (c) 2026 João Loureiro <[email protected]>
+ */
+
+#include <dm.h>
+#include <spi_eeprom.h>
+#include <dm/test.h>
+#include <test/test.h>
+#include <test/ut.h>
+
+/* Read from the emulated SPI EEPROM and check the uclass operations */
+static int dm_test_spi_eeprom(struct unit_test_state *uts)
+{
+       struct udevice *dev;
+       u8 buf[16];
+       int i;
+
+       ut_assertok(uclass_get_device_by_name(UCLASS_SPI_EEPROM, "eeprom@3",
+                                             &dev));
+
+       /* The emulator advertises a 2 KiB array */
+       ut_asserteq(2048, spi_eeprom_size(dev));
+
+       /* The backing store is filled with a (addr & 0xff) pattern */
+       ut_assertok(spi_eeprom_read(dev, 0, buf, sizeof(buf)));
+       for (i = 0; i < sizeof(buf); i++)
+               ut_asserteq(i & 0xff, buf[i]);
+
+       /* Reads honour the requested offset */
+       ut_assertok(spi_eeprom_read(dev, 0x100, buf, sizeof(buf)));
+       for (i = 0; i < sizeof(buf); i++)
+               ut_asserteq((0x100 + i) & 0xff, buf[i]);
+
+       /* Reads past the end of the array are rejected */
+       ut_asserteq(-EINVAL, spi_eeprom_read(dev, 2040, buf, sizeof(buf)));
+
+       /* Writing is not implemented yet */
+       ut_asserteq(-ENOSYS, spi_eeprom_write(dev, 0, buf, 1));
+
+       return 0;
+}
+DM_TEST(dm_test_spi_eeprom, UTF_SCAN_PDATA | UTF_SCAN_FDT);
-- 
2.55.0

Reply via email to