All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v6 0/3] spi: Introduce driver-model support for SPI EEPROMs
@ 2026-09-23 21:40 João Loureiro
  2026-09-23 21:40 ` [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: João Loureiro @ 2026-09-23 21:40 UTC (permalink / raw)
  To: u-boot; +Cc: trini, sjg, João Loureiro

This series adds a UCLASS_SPI_EEPROM uclass, a driver for AT25-style
parts, a sandbox emulator and a DM test.

v6 is a resend that fixes two problems with v5, and makes no other
functional change.

Tom reported that v5 broke the sandbox_vpl CI job. Patch 3 added the
sandbox SPI EEPROM emulator to drivers/misc/Makefile with
obj-$(CONFIG_SANDBOX), which is not phase-aware, so the emulator was
also linked into TPL, VPL and SPL. With it in the image, sandbox_vpl
stops right after "Trying to boot from sandbox_file", and the CI log
shows nothing useful. The emulator now sits in the existing
"ifndef CONFIG_XPL_BUILD" block next to the other sandbox emulators,
as i2c_eeprom_emul.o does. The "vpl or test_spl" test.py selection
used by the sandbox_vpl CI job fails 12 tests with v5 applied on
v2026.10-rc5 and passes all 12 with v6.

Simon pointed out that v5 was threaded wrongly on patchwork. I had
sent it with --in-reply-to pointing at the v4 cover letter, so the
patches ended up in the old thread. v6 is a fresh thread with all
three patches replying to this cover letter.

Testing on sandbox: "ut dm" gives the same set of failures as
unpatched v2026.10-rc5 here (all environment/fixture related), and the
new spi_eeprom test passes along with spi_flash. The sandbox_vpl
test.py selection passes, as above.

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      | 267 +++++++++++++++++++++++++++++++++
 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, 643 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.43.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave
  2026-09-23 21:40 [PATCH v6 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
@ 2026-09-23 21:40 ` João Loureiro
  2026-09-27 16:41   ` Simon Glass
  2026-09-23 21:40 ` [PATCH v6 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
  2026-09-23 21:40 ` [PATCH v6 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test João Loureiro
  2 siblings, 1 reply; 6+ messages in thread
From: João Loureiro @ 2026-09-23 21:40 UTC (permalink / raw)
  To: u-boot; +Cc: trini, sjg, 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>
---
 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.43.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH v6 2/3] spi: Introduce initial driver-model support for SPI EEPROMs
  2026-09-23 21:40 [PATCH v6 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
  2026-09-23 21:40 ` [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
@ 2026-09-23 21:40 ` João Loureiro
  2026-09-27 16:42   ` Simon Glass
  2026-09-23 21:40 ` [PATCH v6 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test João Loureiro
  2 siblings, 1 reply; 6+ messages in thread
From: João Loureiro @ 2026-09-23 21:40 UTC (permalink / raw)
  To: u-boot; +Cc: trini, sjg, 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 part 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>
---
 MAINTAINERS                    |   7 +
 configs/sandbox_defconfig      |   1 +
 drivers/misc/Kconfig           |  15 ++
 drivers/misc/Makefile          |   1 +
 drivers/misc/spi_eeprom.c      | 267 +++++++++++++++++++++++++++++++++
 drivers/misc/spi_eeprom_priv.h |  18 +++
 include/dm/uclass-id.h         |   1 +
 include/spi_eeprom.h           |  95 ++++++++++++
 8 files changed, 405 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..3c64c0ef9f5
--- /dev/null
+++ b/drivers/misc/spi_eeprom.c
@@ -0,0 +1,267 @@
+// 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. Returns the register value 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;
+	int ret;
+
+	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;
+	}
+
+	ret = spi_eeprom_read_status(dev, &status);
+	if (ret) {
+		log_err("Failed to read status register: %d\n", ret);
+		return ret;
+	}
+
+	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.43.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH v6 3/3] sandbox: spi: Add SPI EEPROM emulator and DM test
  2026-09-23 21:40 [PATCH v6 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
  2026-09-23 21:40 ` [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
  2026-09-23 21:40 ` [PATCH v6 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
@ 2026-09-23 21:40 ` João Loureiro
  2 siblings, 0 replies; 6+ messages in thread
From: João Loureiro @ 2026-09-23 21:40 UTC (permalink / raw)
  To: u-boot; +Cc: trini, sjg, 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
---
Changes in v6:
- Build the emulator only in U-Boot proper, inside the existing
  "ifndef CONFIG_XPL_BUILD" block, so it is no longer linked into
  TPL/VPL/SPL. This fixes the sandbox_vpl CI failure.

 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.43.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave
  2026-09-23 21:40 ` [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
@ 2026-09-27 16:41   ` Simon Glass
  0 siblings, 0 replies; 6+ messages in thread
From: Simon Glass @ 2026-09-27 16:41 UTC (permalink / raw)
  To: joaofl; +Cc: u-boot, trini, sjg

On 2026-09-23T21:40:27, João Loureiro <joaofl@gmail.com> wrote:
> test: dm: sf: Get the emulator attached to the flash slave
>
> 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>
>
> test/dm/sf.c | 9 +++++++--
>  1 file changed, 7 insertions(+), 2 deletions(-)

Reviewed-by: Simon Glass <sjg@chromium.org>

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v6 2/3] spi: Introduce initial driver-model support for SPI EEPROMs
  2026-09-23 21:40 ` [PATCH v6 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
@ 2026-09-27 16:42   ` Simon Glass
  0 siblings, 0 replies; 6+ messages in thread
From: Simon Glass @ 2026-09-27 16:42 UTC (permalink / raw)
  To: joaofl; +Cc: u-boot, trini, sjg

Hi João,

On 2026-09-23T21:40:27, João Loureiro <joaofl@gmail.com> wrote:
> spi: Introduce initial driver-model support for SPI EEPROMs
>
> 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 part 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,
> [...]
>
> MAINTAINERS                    |   7 ++
>  configs/sandbox_defconfig      |   1 +
>  drivers/misc/Kconfig           |  15 +++
>  drivers/misc/Makefile          |   1 +
>  drivers/misc/spi_eeprom.c      | 267 +++++++++++++++++++++++++++++++++++++++++
>  drivers/misc/spi_eeprom_priv.h |  18 +++
>  include/dm/uclass-id.h         |   1 +
>  include/spi_eeprom.h           |  95 +++++++++++++++
>  8 files changed, 405 insertions(+)

> even though SPI EEPROMs are common and are usually the faster part of the two.

Nit: 'the faster of the two'.

> diff --git a/drivers/misc/spi_eeprom.c b/drivers/misc/spi_eeprom.c
> @@ -0,0 +1,267 @@
> +     ret = spi_eeprom_read_status(dev, &status);
> +     if (ret) {
> +             log_err("Failed to read status register: %d\n", ret);
> +             return ret;
> +     }
> +
> +     log_debug("status register: 0x%02x\n", status);
> +
> +     return 0;
> +}

Just to check, is this status-register read a deliberate presence
check, or only diagnostic? If diagnostic, a bus glitch or unpopulated
socket will fail probe() and take the whole board down, which seems
strong. If deliberate, please say so in the comment above.

Reviewed-by: Simon Glass <sjg@chromium.org>

Regards,
Simon

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-09-27 16:42 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-23 21:40 [PATCH v6 0/3] spi: Introduce driver-model support for SPI EEPROMs João Loureiro
2026-09-23 21:40 ` [PATCH v6 1/3] test: dm: sf: Get the emulator attached to the flash slave João Loureiro
2026-09-27 16:41   ` Simon Glass
2026-09-23 21:40 ` [PATCH v6 2/3] spi: Introduce initial driver-model support for SPI EEPROMs João Loureiro
2026-09-27 16:42   ` Simon Glass
2026-09-23 21:40 ` [PATCH v6 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.