* [PATCH v7 0/3] spi: Introduce driver-model support for SPI EEPROMs
@ 2026-10-03 14:45 João Loureiro
2026-10-03 14:45 ` [PATCH v7 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: João Loureiro @ 2026-10-03 14:45 UTC (permalink / raw)
To: u-boot; +Cc: Tom Rini, Simon Glass, João Loureiro
This series adds a UCLASS_SPI_EEPROM uclass, a driver for AT25-style
parts, a sandbox emulator and a DM test.
v7 addresses Simon's review of v6 2/3 and picks up his Reviewed-by on
patches 1 and 2. Thanks, Simon.
On the status-register read in probe(): it was diagnostic, not a
presence check - and on SPI it cannot be one. There is no acknowledge,
so an unpopulated socket does not make the transfer fail; MISO just
floats and the read returns 0x00 or 0xff. The only way the read could
fail is a controller or bus-claim error, and failing probe() for that
was, as you say, too strong. probe() now ignores a failed read and only
logs the value at debug level, with a comment saying why. Any real bus
problem still surfaces on the first spi_eeprom_read().
Testing on sandbox, against unpatched v2026.10-rc5: "ut dm" runs 583
tests to rc5's 582 with an identical list of failures (all
environment/fixture related), and the new spi_eeprom test passes. The
sandbox_vpl CI selection ("vpl or test_spl") passes 12/12 on both.
Changes in v7:
- Patch 2: the status-register read in probe() is diagnostic only and
can no longer fail probe(); comment explains why SPI cannot detect
presence this way.
- Patch 2: fix the comment above spi_eeprom_read_status(), which
described the return value wrongly.
- Patch 2: commit message nit, "the faster of the two".
- Patches 1 and 2: add Simon's Reviewed-by.
Changes in v6:
- Patch 3: build the sandbox SPI EEPROM emulator only in U-Boot
proper (not in TPL/VPL/SPL), fixing the sandbox_vpl CI failure.
- Resent as a new, correctly threaded series.
Changes in v5:
- New patch 1: test/dm/sf.c now asks for the emulator attached to its
own slave rather than the first one in UCLASS_SPI_EMUL, fixing a
heap corruption once a second SPI emulator exists.
- Patch 2 commit message reworded: imperative mood, "driver model"
rather than "driver mode", and it now explains the motivation.
- SPI_EEPROM_CMD_SIZE removed. Address length is an addr_len field in
driver data, overridable from the device tree, so 8-, 16- and 24-bit
addressed parts are handled.
- AT25_CMD_* opcodes moved to a private header,
drivers/misc/spi_eeprom_priv.h.
- Read path no longer computes "offset + size"; probe() rejects a
"size" that does not fit the configured address width.
- of_to_plat() dropped; geometry read in probe().
- "size", "pagesize" and "address-width" follow the in-tree atmel,at25
binding (dts/upstream/Bindings/eeprom/at25.yaml).
- Compatible list taken from that binding, plus the "atmel,at25"
fallback.
- Kconfig: "depends on MISC" dropped, help text expanded.
- include/spi_eeprom.h converted to kerneldoc.
- Sandbox EEPROM node in test.dts follows the binding.
Changes in v4:
- Emulator frees its backing store in a remove() method.
- Full kerneldoc for sandbox_spi_emul_get().
Changes in v3:
- Reindented with tabs; the series is now checkpatch-clean.
- Dropped the no-op write stub in favour of returning -ENOSYS.
- Removed dead code, fixed the read bounds check, corrected the
AT25160 geometry, switched to u8 types and added a MAINTAINERS entry.
- Added the sandbox emulator and the DM test.
João Loureiro (3):
test: dm: sf: Get the emulator attached to the flash slave
spi: Introduce initial driver-model support for SPI EEPROMs
sandbox: spi: Add SPI EEPROM emulator and DM test
MAINTAINERS | 9 ++
arch/sandbox/dts/test.dts | 16 +-
configs/sandbox_defconfig | 1 +
drivers/misc/Kconfig | 15 ++
drivers/misc/Makefile | 2 +
drivers/misc/spi_eeprom.c | 266 +++++++++++++++++++++++++++++++++
drivers/misc/spi_eeprom_emul.c | 130 ++++++++++++++++
drivers/misc/spi_eeprom_priv.h | 18 +++
drivers/spi/sandbox_spi.c | 37 ++++-
include/dm/uclass-id.h | 1 +
include/spi_eeprom.h | 95 ++++++++++++
test/dm/Makefile | 1 +
test/dm/sf.c | 9 +-
test/dm/spi_eeprom.c | 46 ++++++
14 files changed, 642 insertions(+), 4 deletions(-)
create mode 100644 drivers/misc/spi_eeprom.c
create mode 100644 drivers/misc/spi_eeprom_emul.c
create mode 100644 drivers/misc/spi_eeprom_priv.h
create mode 100644 include/spi_eeprom.h
create mode 100644 test/dm/spi_eeprom.c
base-commit: a06e89ab05eaa2b8344d521319399333cd760ae5
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v7 1/3] test: dm: sf: Get the emulator attached to the flash slave
2026-10-03 14:45 [PATCH v7 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
@ 2026-10-03 14:45 ` João Loureiro
2026-10-03 14:45 ` [PATCH v7 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
2026-10-03 14:45 ` [PATCH v7 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test João Loureiro
2 siblings, 0 replies; 4+ messages in thread
From: João Loureiro @ 2026-10-03 14:45 UTC (permalink / raw)
To: u-boot; +Cc: Tom Rini, Simon Glass, João Loureiro
dm_test_spi_flash() picks up its SPI emulator with
uclass_first_device_err(UCLASS_SPI_EMUL), which assumes the sandbox SPI
flash emulator is the only device in that uclass. That holds today only
because the flash emulator is bound lazily, on the first transfer.
As soon as a second SPI emulator is described in the device tree it is
bound during the devicetree scan and so comes first in the uclass. The
test then hands a foreign device to sandbox_sf_set_block_protect(),
which casts its private data to struct sandbox_spi_flash and writes
past the end of it, corrupting the heap.
Ask for the emulator attached to this particular slave instead, which
is what the test means, and which stays correct however many SPI
emulators exist.
Signed-off-by: João Loureiro <joaofl@gmail.com>
Reviewed-by: Simon Glass <sjg@chromium.org>
---
test/dm/sf.c | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/test/dm/sf.c b/test/dm/sf.c
index 3684d021709..0ec02ac694b 100644
--- a/test/dm/sf.c
+++ b/test/dm/sf.c
@@ -50,8 +50,13 @@ static int dm_test_spi_flash(struct unit_test_state *uts)
ut_assertok(spi_flash_read_dm(dev, 0, size, dst));
ut_asserteq_mem(src, dst, size);
- /* Try the write-protect stuff */
- ut_assertok(uclass_first_device_err(UCLASS_SPI_EMUL, &emul));
+ /*
+ * Try the write-protect stuff. Ask for the emulator attached to this
+ * particular slave rather than the first one in the uclass, since
+ * other SPI emulators may be present.
+ */
+ ut_assertok(sandbox_spi_get_emul(state_get_current(), dev->parent, dev,
+ &emul));
ut_asserteq(0, spl_flash_get_sw_write_prot(dev));
sandbox_sf_set_block_protect(emul, 1);
ut_asserteq(1, spl_flash_get_sw_write_prot(dev));
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v7 2/3] spi: Introduce initial driver-model support for SPI EEPROMs
2026-10-03 14:45 [PATCH v7 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
2026-10-03 14:45 ` [PATCH v7 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
@ 2026-10-03 14:45 ` João Loureiro
2026-10-03 14:45 ` [PATCH v7 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test João Loureiro
2 siblings, 0 replies; 4+ messages in thread
From: João Loureiro @ 2026-10-03 14:45 UTC (permalink / raw)
To: u-boot; +Cc: Tom Rini, Simon Glass, João Loureiro
U-Boot has driver-model support for EEPROMs on several buses - I2C
through UCLASS_I2C_EEPROM and 1-Wire through UCLASS_W1_EEPROM - but
nothing for SPI, even though SPI EEPROMs are common and are usually the
faster of the two.
These devices typically hold a board's identity: its serial number, a
unique device ID and the hardware variant. U-Boot needs that early,
both to decide which device tree and overlays to load for the variant
it happens to be running on, and to hand the unique IDs to the system
it is about to boot. Today a board that wants any of this has to
open-code raw SPI transfers in board code, which is neither reusable
nor testable.
Introduce a UCLASS_SPI_EEPROM uclass with read/write/size operations,
analogous to the I2C EEPROM uclass, along with a driver for AT25-style
parts. The chip geometry - total size, page size and address width - is
taken from the device tree as described by the atmel,at25 binding in
dts/upstream/Bindings/eeprom/at25.yaml, which requires those properties,
falling back to per-compatible defaults held in driver data. Devices
using 8-, 16- and 24-bit addressing are therefore all covered, and
adding a part later is a driver-data entry rather than a change to the
uclass API. The 9-bit addressing mode the binding also allows, where the
ninth address bit travels in the opcode, is not implemented.
Only reading is implemented for now; spi_eeprom_write() returns -ENOSYS.
Enable the uclass in sandbox_defconfig so that the emulator and DM test
added by the next patch are built.
Signed-off-by: João Loureiro <joaofl@gmail.com>
Reviewed-by: Simon Glass <sjg@chromium.org>
---
Changes in v7:
- The status-register read in probe() is diagnostic only and no longer
fails probe(); SPI has no acknowledge, so it cannot detect presence.
- Fix the comment above spi_eeprom_read_status().
- Commit message nit: "the faster of the two".
MAINTAINERS | 7 +
configs/sandbox_defconfig | 1 +
drivers/misc/Kconfig | 15 ++
drivers/misc/Makefile | 1 +
drivers/misc/spi_eeprom.c | 266 +++++++++++++++++++++++++++++++++
drivers/misc/spi_eeprom_priv.h | 18 +++
include/dm/uclass-id.h | 1 +
include/spi_eeprom.h | 95 ++++++++++++
8 files changed, 404 insertions(+)
create mode 100644 drivers/misc/spi_eeprom.c
create mode 100644 drivers/misc/spi_eeprom_priv.h
create mode 100644 include/spi_eeprom.h
diff --git a/MAINTAINERS b/MAINTAINERS
index eb4209bdea6..c45c47000c0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1708,6 +1708,13 @@ T: git https://git.u-boot-project.org/u-boot/custodians/u-boot-spi.git
F: drivers/spi/
F: include/spi*
+SPI EEPROM
+M: João Loureiro <joaofl@gmail.com>
+S: Maintained
+F: drivers/misc/spi_eeprom.c
+F: drivers/misc/spi_eeprom_priv.h
+F: include/spi_eeprom.h
+
SPI NAND
M: Dario Binacchi <dario.binacchi@amarulasolutions.com>
M: Michael Trimarchi <michael@amarulasolutions.com>
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index ca73080b06d..78ba1532ced 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -267,6 +267,7 @@ CONFIG_SYS_NAND_USE_FLASH_BBT=y
CONFIG_NAND_SANDBOX=y
CONFIG_SYS_NAND_ONFI_DETECTION=y
CONFIG_SYS_NAND_PAGE_SIZE=0x200
+CONFIG_SPI_EEPROM=y
CONFIG_SPI_FLASH_SANDBOX=y
CONFIG_BOOTDEV_SPI_FLASH=y
CONFIG_SPI_FLASH_ATMEL=y
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 44415f24ae1..8409635ef17 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -567,6 +567,21 @@ config I2C_EEPROM
help
Enable a generic driver for EEPROMs attached via I2C.
+config SPI_EEPROM
+ bool "Enable driver for generic SPI-attached EEPROMs"
+ help
+ Enable a generic driver for EEPROMs attached via SPI.
+
+ This provides the UCLASS_SPI_EEPROM uclass together with a driver
+ for AT25-style parts, i.e. devices described by the "atmel,at25"
+ device tree binding. The chip geometry (size, pagesize and
+ address-width) is read from the device tree, with per-compatible
+ defaults for the parts listed in the driver. Parts using 8-, 16-
+ and 24-bit addressing are supported; 9-bit addressing, where the
+ ninth address bit travels in the opcode, is not.
+
+ Only read access is implemented at present; spi_eeprom_write()
+ returns -ENOSYS.
config SPL_I2C_EEPROM
bool "Enable driver for generic I2C-attached EEPROMs for SPL"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index e2170212e5a..8c9f8eb9cfa 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -44,6 +44,7 @@ obj-$(CONFIG_GDSYS_SOC) += gdsys_soc.o
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_IHS_FPGA) += ihs_fpga.o
obj-$(CONFIG_IMX8) += imx8/
obj-$(CONFIG_IMX_ELE) += imx_ele/
diff --git a/drivers/misc/spi_eeprom.c b/drivers/misc/spi_eeprom.c
new file mode 100644
index 00000000000..d0930afe631
--- /dev/null
+++ b/drivers/misc/spi_eeprom.c
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2024 Koninklijke Philips N.V.
+ * Copyright (c) 2026 João Loureiro <joaofl@gmail.com>
+ */
+
+#define LOG_CATEGORY UCLASS_SPI_EEPROM
+
+#include <dm.h>
+#include <spi.h>
+#include <spi_eeprom.h>
+#include <linux/err.h>
+
+#include "spi_eeprom_priv.h"
+
+/* Longest address accepted by spi_eeprom_std_read(), in bytes */
+#define SPI_EEPROM_MAX_ADDR_LEN 3
+
+/**
+ * struct at25_chip - geometry defaults for an AT25-style EEPROM
+ *
+ * The atmel,at25 binding (dts/upstream/Bindings/eeprom/at25.yaml) requires
+ * "size", "pagesize" and "address-width" to be present in the device tree.
+ * These values are used when a device tree omits them, and are all zero for
+ * the generic "atmel,at25" compatible, where nothing can be assumed.
+ *
+ * @size: Total capacity in bytes
+ * @pagesize: Write page size in bytes
+ * @addr_len: Number of address bytes sent after the command opcode
+ */
+struct at25_chip {
+ u32 size;
+ u32 pagesize;
+ u8 addr_len;
+};
+
+int spi_eeprom_read(struct udevice *dev, int offset, u8 *buf, int size)
+{
+ const struct spi_eeprom_ops *ops = device_get_ops(dev);
+
+ if (!ops->read)
+ return -ENOSYS;
+
+ return ops->read(dev, offset, buf, size);
+}
+
+int spi_eeprom_write(struct udevice *dev, int offset, const u8 *buf, int size)
+{
+ const struct spi_eeprom_ops *ops = device_get_ops(dev);
+
+ if (!ops->write)
+ return -ENOSYS;
+
+ return ops->write(dev, offset, buf, size);
+}
+
+int spi_eeprom_size(struct udevice *dev)
+{
+ const struct spi_eeprom_ops *ops = device_get_ops(dev);
+
+ if (!ops->size)
+ return -ENOSYS;
+
+ return ops->size(dev);
+}
+
+static int spi_eeprom_read_cmd(struct udevice *dev, u8 *buf, int size,
+ const u8 *cmd, int cmd_size)
+{
+ struct spi_slave *slave = dev_get_parent_priv(dev);
+ int ret;
+
+ ret = dm_spi_claim_bus(dev);
+ if (ret) {
+ log_err("Failed to claim SPI bus: %d\n", ret);
+ return ret;
+ }
+
+ /* Send the command, keeping the transaction open */
+ ret = spi_xfer(slave, cmd_size * 8, cmd, NULL, SPI_XFER_BEGIN);
+ if (ret) {
+ log_err("Failed to send command: %d\n", ret);
+ goto release;
+ }
+
+ /* Read the data back and close the transaction */
+ ret = spi_xfer(slave, size * 8, NULL, buf, SPI_XFER_END);
+ if (ret)
+ log_err("Failed to read data: %d\n", ret);
+
+release:
+ dm_spi_release_bus(dev);
+ return ret;
+}
+
+/*
+ * Read the status register into @status. Returns 0 on success or a negative
+ * error code on failure.
+ *
+ * Status register layout (AT25-style devices):
+ * Bit 0 Ready/Busy: 1 while an internal write cycle is in progress
+ * Bit 1 Write Enable Latch
+ * Bits 2-3 Block write protection
+ * Bits 4-6 Reserved
+ * Bit 7 Write-protect enable
+ */
+static int spi_eeprom_read_status(struct udevice *dev, u8 *status)
+{
+ u8 cmd = AT25_CMD_READ_STATUS;
+
+ return spi_eeprom_read_cmd(dev, status, 1, &cmd, 1);
+}
+
+static int spi_eeprom_std_read(struct udevice *dev, int offset, u8 *buf,
+ int size)
+{
+ struct spi_eeprom *priv = dev_get_priv(dev);
+ u8 cmd[1 + SPI_EEPROM_MAX_ADDR_LEN];
+ int i;
+
+ if (offset < 0 || size < 0 || offset >= priv->size ||
+ size > priv->size - offset) {
+ log_err("Read out of bounds (offset %d, size %d, max %lu)\n",
+ offset, size, priv->size);
+ return -EINVAL;
+ }
+
+ cmd[0] = AT25_CMD_READ_DATA;
+ for (i = 0; i < priv->addr_len; i++)
+ cmd[1 + i] = offset >> (8 * (priv->addr_len - 1 - i));
+
+ return spi_eeprom_read_cmd(dev, buf, size, cmd, 1 + priv->addr_len);
+}
+
+static int spi_eeprom_std_size(struct udevice *dev)
+{
+ struct spi_eeprom *priv = dev_get_priv(dev);
+
+ return priv->size;
+}
+
+static const struct spi_eeprom_ops spi_eeprom_std_ops = {
+ .read = spi_eeprom_std_read,
+ .size = spi_eeprom_std_size,
+};
+
+/* Geometry is described by the device tree for the generic compatible */
+static const struct at25_chip at25_generic = { };
+
+static const struct at25_chip mchp_25aa010a = {
+ .size = 128,
+ .pagesize = 16,
+ .addr_len = 1,
+};
+
+static const struct at25_chip mchp_at25160bn = {
+ .size = 2048,
+ .pagesize = 32,
+ .addr_len = 2,
+};
+
+static const struct at25_chip atmel_at25256b = {
+ .size = 32768,
+ .pagesize = 64,
+ .addr_len = 2,
+};
+
+static const struct at25_chip st_m95640 = {
+ .size = 8192,
+ .pagesize = 32,
+ .addr_len = 2,
+};
+
+static const struct at25_chip st_m95256 = {
+ .size = 32768,
+ .pagesize = 64,
+ .addr_len = 2,
+};
+
+static const struct at25_chip st_m95m02 = {
+ .size = 262144,
+ .pagesize = 256,
+ .addr_len = 3,
+};
+
+static int spi_eeprom_std_probe(struct udevice *dev)
+{
+ const struct at25_chip *chip =
+ (const struct at25_chip *)dev_get_driver_data(dev);
+ struct spi_eeprom *priv = dev_get_priv(dev);
+ u32 addr_bits;
+ u8 status;
+
+ if (!chip)
+ chip = &at25_generic;
+
+ priv->size = dev_read_u32_default(dev, "size", chip->size);
+ priv->pagesize = dev_read_u32_default(dev, "pagesize", chip->pagesize);
+ addr_bits = dev_read_u32_default(dev, "address-width",
+ chip->addr_len * 8);
+
+ if (!priv->size || !priv->pagesize || !addr_bits) {
+ log_err("Missing 'size'/'pagesize'/'address-width', required by the atmel,at25 binding\n");
+ return -EINVAL;
+ }
+
+ /*
+ * The binding also allows 9-bit addressing, where the ninth address
+ * bit travels in the opcode. That is not implemented here.
+ */
+ switch (addr_bits) {
+ case 8:
+ case 16:
+ case 24:
+ priv->addr_len = addr_bits / 8;
+ break;
+ default:
+ log_err("Unsupported address width %u\n", addr_bits);
+ return -EOPNOTSUPP;
+ }
+
+ if (priv->size > 1UL << addr_bits) {
+ log_err("Size %lu exceeds the %u-bit address range\n",
+ priv->size, addr_bits);
+ return -EINVAL;
+ }
+
+ /*
+ * Diagnostic only. SPI has no acknowledge, so this cannot tell whether
+ * a chip is present: an empty socket simply reads back 0x00 or 0xff.
+ * Do not fail probe() if the read itself goes wrong.
+ */
+ if (!spi_eeprom_read_status(dev, &status))
+ log_debug("status register: 0x%02x\n", status);
+
+ return 0;
+}
+
+static const struct udevice_id spi_eeprom_std_ids[] = {
+ { .compatible = "microchip,25aa010a",
+ .data = (ulong)&mchp_25aa010a },
+ { .compatible = "microchip,at25160bn",
+ .data = (ulong)&mchp_at25160bn },
+ { .compatible = "atmel,at25256B",
+ .data = (ulong)&atmel_at25256b },
+ { .compatible = "st,m95640", .data = (ulong)&st_m95640 },
+ { .compatible = "st,m95256", .data = (ulong)&st_m95256 },
+ { .compatible = "st,m95m02", .data = (ulong)&st_m95m02 },
+ /* Fallback compatible required by the binding; must be last */
+ { .compatible = "atmel,at25", .data = (ulong)&at25_generic },
+ { }
+};
+
+U_BOOT_DRIVER(spi_eeprom_std) = {
+ .name = "spi_eeprom",
+ .id = UCLASS_SPI_EEPROM,
+ .of_match = spi_eeprom_std_ids,
+ .probe = spi_eeprom_std_probe,
+ .priv_auto = sizeof(struct spi_eeprom),
+ .ops = &spi_eeprom_std_ops,
+};
+
+UCLASS_DRIVER(spi_eeprom) = {
+ .id = UCLASS_SPI_EEPROM,
+ .name = "spi_eeprom",
+};
diff --git a/drivers/misc/spi_eeprom_priv.h b/drivers/misc/spi_eeprom_priv.h
new file mode 100644
index 00000000000..142ffc736c7
--- /dev/null
+++ b/drivers/misc/spi_eeprom_priv.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Command set shared by the AT25-style SPI EEPROM driver and its sandbox
+ * emulator. These opcodes are an implementation detail of the driver, not
+ * part of the UCLASS_SPI_EEPROM API.
+ *
+ * Copyright (c) 2024 Koninklijke Philips N.V.
+ * Copyright (c) 2026 João Loureiro <joaofl@gmail.com>
+ */
+
+#ifndef __SPI_EEPROM_PRIV_H
+#define __SPI_EEPROM_PRIV_H
+
+/* AT25-style command set */
+#define AT25_CMD_READ_DATA 0x03 /* Read data from memory array */
+#define AT25_CMD_READ_STATUS 0x05 /* Read status register */
+
+#endif /* __SPI_EEPROM_PRIV_H */
diff --git a/include/dm/uclass-id.h b/include/dm/uclass-id.h
index 36b5d87c304..d24b4a1a121 100644
--- a/include/dm/uclass-id.h
+++ b/include/dm/uclass-id.h
@@ -138,6 +138,7 @@ enum uclass_id {
UCLASS_SOC, /* SOC Device */
UCLASS_SOUND, /* Playing simple sounds */
UCLASS_SPI, /* SPI bus */
+ UCLASS_SPI_EEPROM, /* SPI EEPROM device */
UCLASS_SPI_FLASH, /* SPI flash */
UCLASS_SPI_GENERIC, /* Generic SPI flash target */
UCLASS_SPMI, /* System Power Management Interface bus */
diff --git a/include/spi_eeprom.h b/include/spi_eeprom.h
new file mode 100644
index 00000000000..64b5d3130be
--- /dev/null
+++ b/include/spi_eeprom.h
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2024 Koninklijke Philips N.V.
+ * Copyright (c) 2026 João Loureiro <joaofl@gmail.com>
+ */
+
+#ifndef __SPI_EEPROM
+#define __SPI_EEPROM
+
+#include <linux/errno.h>
+#include <linux/types.h>
+
+struct udevice;
+
+/**
+ * struct spi_eeprom_ops - operations provided by an SPI EEPROM driver
+ *
+ * @read: Read @size bytes from @offset into @buf
+ * @write: Write @size bytes from @buf at @offset
+ * @size: Return the capacity of the device in bytes
+ */
+struct spi_eeprom_ops {
+ int (*read)(struct udevice *dev, int offset, u8 *buf, int size);
+ int (*write)(struct udevice *dev, int offset, const u8 *buf,
+ int size);
+ int (*size)(struct udevice *dev);
+};
+
+/**
+ * struct spi_eeprom - geometry of an SPI EEPROM device
+ *
+ * @pagesize: The EEPROM's page size in bytes
+ * @size: The EEPROM's capacity in bytes
+ * @addr_len: Number of address bytes sent after the command opcode
+ */
+struct spi_eeprom {
+ unsigned long pagesize;
+ unsigned long size;
+ u8 addr_len;
+};
+
+#if CONFIG_IS_ENABLED(SPI_EEPROM)
+/**
+ * spi_eeprom_read() - read bytes from an SPI EEPROM chip
+ *
+ * @dev: Chip to read from
+ * @offset: Offset within chip to start reading
+ * @buf: Place to put data
+ * @size: Number of bytes to read
+ * Return: 0 on success, -ve on failure
+ */
+int spi_eeprom_read(struct udevice *dev, int offset, u8 *buf, int size);
+
+/**
+ * spi_eeprom_write() - write bytes to an SPI EEPROM chip
+ *
+ * @dev: Chip to write to
+ * @offset: Offset within chip to start writing
+ * @buf: Buffer containing data to write
+ * @size: Number of bytes to write
+ * Return: 0 on success, -ve on failure
+ */
+int spi_eeprom_write(struct udevice *dev, int offset, const u8 *buf,
+ int size);
+
+/**
+ * spi_eeprom_size() - get size of SPI EEPROM chip
+ *
+ * @dev: Chip to query
+ * Return: +ve size in bytes on success, -ve on failure
+ */
+int spi_eeprom_size(struct udevice *dev);
+
+#else /* !SPI_EEPROM */
+
+static inline int spi_eeprom_read(struct udevice *dev, int offset, u8 *buf,
+ int size)
+{
+ return -ENOSYS;
+}
+
+static inline int spi_eeprom_write(struct udevice *dev, int offset,
+ const u8 *buf, int size)
+{
+ return -ENOSYS;
+}
+
+static inline int spi_eeprom_size(struct udevice *dev)
+{
+ return -ENOSYS;
+}
+
+#endif /* SPI_EEPROM */
+
+#endif /* __SPI_EEPROM */
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v7 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test
2026-10-03 14:45 [PATCH v7 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
2026-10-03 14:45 ` [PATCH v7 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
2026-10-03 14:45 ` [PATCH v7 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
@ 2026-10-03 14:45 ` João Loureiro
2 siblings, 0 replies; 4+ messages in thread
From: João Loureiro @ 2026-10-03 14:45 UTC (permalink / raw)
To: u-boot; +Cc: Tom Rini, Simon Glass, João Loureiro
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 <joaofl@gmail.com>
Reviewed-by: Simon Glass <sjg@chromium.org>
Tested-by: Simon Glass <sjg@chromium.org> # sandbox
---
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 c45c47000c0..47f04166879 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1712,8 +1712,10 @@ SPI EEPROM
M: João Loureiro <joaofl@gmail.com>
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 <dario.binacchi@amarulasolutions.com>
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..201055d89e0 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_CROS_EC_I2C) += cros_ec_i2c.o
obj-$(CONFIG_CROS_EC_SPI) += cros_ec_spi.o
obj-$(CONFIG_SANDBOX) += p2sb_sandbox.o p2sb_emul.o
obj-$(CONFIG_SANDBOX) += swap_case.o
+obj-$(CONFIG_SANDBOX) += spi_eeprom_emul.o
endif
ifdef CONFIG_$(PHASE_)DM_I2C
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 <joaofl@gmail.com>
+ */
+
+#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 cc51fd33079..51ee02ea873 100644
--- a/test/dm/Makefile
+++ b/test/dm/Makefile
@@ -121,6 +121,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 <joaofl@gmail.com>
+ */
+
+#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
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-03 15:20 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-03 14:45 [PATCH v7 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
2026-10-03 14:45 ` [PATCH v7 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
2026-10-03 14:45 ` [PATCH v7 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
2026-10-03 14:45 ` [PATCH v7 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test João Loureiro
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.