mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Mario Behn <mario.behn@gmail.com>
To: Mark Brown <broonie@kernel.org>, Michal Simek <michal.simek@amd.com>
Cc: linux-spi@vger.kernel.org, linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, Mario Behn <mario.behn@gmail.com>
Subject: [PATCH v1 1/2] spi: zynq-qspi: read flash above 16 MiB via the extended address register
Date: Thu,  5 Nov 2026 21:37:33 +0100	[thread overview]
Message-ID: <20261105203734.979168-2-mario.behn@gmail.com> (raw)
In-Reply-To: <20261105203734.979168-1-mario.behn@gmail.com>


The Zynq QSPI controller sends at most 3 address bytes, and spi-nor needs
4-byte addresses for a flash larger than 16 MiB (W25Q256 on the Pluto+), so
the probe failed with -22 and no /dev/mtd* appeared.

Keep the flash in 3-byte mode and handle a 4-byte-address operation
inside the controller driver: write enable, Write Extended Address
Register with addr >> 24, the operation with the 3-byte opcode and the
low 24 address bits, then the register back to 0, so that U-Boot and the
BootROM find bank 0 after a reset. adjust_op_size keeps one read inside
a 16 MiB bank. EN4B and EX4B are swallowed so that the flash never
leaves 3-byte mode.

Also stop writing the address bytes through xqspi->txbuf into the
spi_mem_op structure; use a local buffer.

This stage is READ ONLY: every operation that does not read data (write
enable, program, erase, status register write) is refused with -EROFS.
Verified on the Pluto+ (W25Q256, ef4019): the md5 of all four partitions
equals the one read on 6.12, including the 30 MiB partition that crosses
the bank boundary.

Signed-off-by: Mario Behn <mario.behn@gmail.com>
---
 drivers/spi/spi-zynq-qspi.c | 152 +++++++++++++++++++++++++++++++++---
 1 file changed, 142 insertions(+), 10 deletions(-)

diff --git a/drivers/spi/spi-zynq-qspi.c b/drivers/spi/spi-zynq-qspi.c
index d762aaf452af..e20019a03964 100644
--- a/drivers/spi/spi-zynq-qspi.c
+++ b/drivers/spi/spi-zynq-qspi.c
@@ -16,6 +16,7 @@
 #include <linux/spi/spi.h>
 #include <linux/workqueue.h>
 #include <linux/spi/spi-mem.h>
+#include <linux/mtd/spi-nor.h>
 
 /* Register offset definitions */
 #define ZYNQ_QSPI_CONFIG_OFFSET		0x00 /* Configuration  Register, RW */
@@ -222,21 +223,69 @@ static void zynq_qspi_init_hw(struct zynq_qspi *xqspi, unsigned int num_cs)
 			ZYNQ_QSPI_ENABLE_ENABLE_MASK);
 }
 
+/*
+ * The controller sends at most 3 address bytes. A flash larger than 16 MiB
+ * is therefore kept in 3-byte mode and reached through the Extended Address
+ * Register (EAR): an operation with a 4-byte address is turned into
+ * WREN + WREAR(addr >> 24) + the 3-byte form of the operation, and the EAR is
+ * set back to 0 afterwards, so that U-Boot and the BootROM find the flash in
+ * bank 0 after a reset (the same scheme as the ADI 6.12 spi-nor code, but in
+ * the controller driver instead of the spi-nor core).
+ */
+#define ZYNQ_QSPI_BANK_SIZE		0x1000000U
+#define ZYNQ_QSPI_OP_WREAR		0xc5	/* Write Extended Address Register */
+
 static bool zynq_qspi_supports_op(struct spi_mem *mem,
 				  const struct spi_mem_op *op)
 {
-	if (!spi_mem_default_supports_op(mem, op))
+	struct spi_mem_op nop = *op;
+
+	/* check the shape of the operation as if the address had 3 bytes */
+	if (nop.addr.nbytes == 4)
+		nop.addr.nbytes = 3;
+	if (!spi_mem_default_supports_op(mem, &nop))
 		return false;
 
-	/*
-	 * The number of address bytes should be equal to or less than 3 bytes.
-	 */
-	if (op->addr.nbytes > 3)
+	if (op->addr.nbytes > 4)
 		return false;
 
 	return true;
 }
 
+static int zynq_qspi_adjust_op_size(struct spi_mem *mem,
+				    struct spi_mem_op *op)
+{
+	if (op->addr.nbytes == 4 && op->data.nbytes) {
+		u32 room = ZYNQ_QSPI_BANK_SIZE - (op->addr.val & (ZYNQ_QSPI_BANK_SIZE - 1));
+
+		/* one operation must not cross a 16 MiB bank */
+		if (op->data.nbytes > room)
+			op->data.nbytes = room;
+	}
+
+	return 0;
+}
+
+/* opcode with a 4-byte address -> the same opcode with a 3-byte address */
+static u8 zynq_qspi_opcode_3b(u8 opcode)
+{
+	switch (opcode) {
+	case SPINOR_OP_READ_4B:		return SPINOR_OP_READ;
+	case SPINOR_OP_READ_FAST_4B:	return SPINOR_OP_READ_FAST;
+	case SPINOR_OP_READ_1_1_2_4B:	return SPINOR_OP_READ_1_1_2;
+	case SPINOR_OP_READ_1_2_2_4B:	return SPINOR_OP_READ_1_2_2;
+	case SPINOR_OP_READ_1_1_4_4B:	return SPINOR_OP_READ_1_1_4;
+	case SPINOR_OP_READ_1_4_4_4B:	return SPINOR_OP_READ_1_4_4;
+	case SPINOR_OP_PP_4B:		return SPINOR_OP_PP;
+	case SPINOR_OP_PP_1_1_4_4B:	return SPINOR_OP_PP_1_1_4;
+	case SPINOR_OP_PP_1_4_4_4B:	return SPINOR_OP_PP_1_4_4;
+	case SPINOR_OP_BE_4K_4B:	return SPINOR_OP_BE_4K;
+	case SPINOR_OP_BE_32K_4B:	return SPINOR_OP_BE_32K;
+	case SPINOR_OP_SE_4B:		return SPINOR_OP_SE;
+	default:			return opcode;	/* already a 3-byte opcode */
+	}
+}
+
 /**
  * zynq_qspi_rxfifo_op - Read 1..4 bytes from RxFIFO to RX buffer
  * @xqspi:	Pointer to the zynq_qspi structure
@@ -522,12 +571,13 @@ static irqreturn_t zynq_qspi_irq(int irq, void *dev_id)
  *
  * Return: 0 in case of success, a negative error code otherwise.
  */
-static int zynq_qspi_exec_mem_op(struct spi_mem *mem,
-				 const struct spi_mem_op *op)
+static int zynq_qspi_exec_plain_op(struct spi_mem *mem,
+				   const struct spi_mem_op *op)
 {
 	struct zynq_qspi *xqspi = spi_controller_get_devdata(mem->spi->controller);
 	int err = 0, i;
 	u8 *tmpbuf;
+	u8 addrbuf[3];
 
 	zynq_qspi_chipselect(mem->spi, true);
 	zynq_qspi_config_op(xqspi, mem->spi, op);
@@ -547,10 +597,10 @@ static int zynq_qspi_exec_mem_op(struct spi_mem *mem,
 	}
 
 	if (op->addr.nbytes) {
-		for (i = 0; i < op->addr.nbytes; i++) {
-			xqspi->txbuf[i] = op->addr.val >>
+		for (i = 0; i < op->addr.nbytes; i++)
+			addrbuf[i] = op->addr.val >>
 					(8 * (op->addr.nbytes - i - 1));
-		}
+		xqspi->txbuf = addrbuf;
 
 		reinit_completion(&xqspi->data_completion);
 		xqspi->rxbuf = NULL;
@@ -611,9 +661,91 @@ static int zynq_qspi_exec_mem_op(struct spi_mem *mem,
 	return err;
 }
 
+/* Stage 2: the flash is read-only through this driver. */
+static bool zynq_qspi_op_is_read(const struct spi_mem_op *op)
+{
+	if (op->data.nbytes && op->data.dir == SPI_MEM_DATA_IN)
+		return true;
+	return false;
+}
+
+static int zynq_qspi_ear_cmds(struct spi_mem *mem, const struct spi_mem_op *like,
+			      u8 ear, bool wren_after)
+{
+	struct spi_mem_op wren = SPI_MEM_OP(SPI_MEM_OP_CMD(SPINOR_OP_WREN, 1),
+					    SPI_MEM_OP_NO_ADDR,
+					    SPI_MEM_OP_NO_DUMMY,
+					    SPI_MEM_OP_NO_DATA);
+	u8 *buf = kmalloc(1, GFP_KERNEL);
+	struct spi_mem_op wrear = SPI_MEM_OP(SPI_MEM_OP_CMD(ZYNQ_QSPI_OP_WREAR, 1),
+					     SPI_MEM_OP_NO_ADDR,
+					     SPI_MEM_OP_NO_DUMMY,
+					     SPI_MEM_OP_DATA_OUT(1, buf, 1));
+	int ret;
+
+	if (!buf)
+		return -ENOMEM;
+	*buf = ear;
+	wren.max_freq = like->max_freq;
+	wrear.max_freq = like->max_freq;
+
+	ret = zynq_qspi_exec_plain_op(mem, &wren);
+	if (!ret)
+		ret = zynq_qspi_exec_plain_op(mem, &wrear);
+	if (!ret && wren_after)
+		ret = zynq_qspi_exec_plain_op(mem, &wren);
+	kfree(buf);
+	return ret;
+}
+
+static int zynq_qspi_exec_mem_op(struct spi_mem *mem,
+				 const struct spi_mem_op *op)
+{
+	struct spi_mem_op nop = *op;
+	u8 ear;
+	int ret, ret2;
+
+	/* Leaving 4-byte mode is not needed: the flash is never put in it. */
+	if (!op->addr.nbytes && !op->data.nbytes && !op->dummy.nbytes &&
+	    (op->cmd.opcode == SPINOR_OP_EN4B || op->cmd.opcode == SPINOR_OP_EX4B))
+		return 0;
+
+	/* read-only guard: only operations that read data from the flash */
+	if (!zynq_qspi_op_is_read(op)) {
+		dev_warn_ratelimited(&mem->spi->dev,
+				     "read-only: op %#x refused\n", op->cmd.opcode);
+		return -EROFS;
+	}
+
+	if (op->addr.nbytes != 4)
+		return zynq_qspi_exec_plain_op(mem, op);
+
+	ear = (op->addr.val >> 24) & 0xff;
+	nop.cmd.opcode = zynq_qspi_opcode_3b(op->cmd.opcode);
+	nop.addr.nbytes = 3;
+	nop.addr.val = op->addr.val & (ZYNQ_QSPI_BANK_SIZE - 1);
+
+	if (ear) {
+		ret = zynq_qspi_ear_cmds(mem, op, ear, false);
+		if (ret)
+			return ret;
+	}
+
+	ret = zynq_qspi_exec_plain_op(mem, &nop);
+
+	if (ear) {
+		ret2 = zynq_qspi_ear_cmds(mem, op, 0, false);
+		if (!ret)
+			ret = ret2;
+	}
+
+	return ret;
+}
+
 static const struct spi_controller_mem_ops zynq_qspi_mem_ops = {
 	.supports_op = zynq_qspi_supports_op,
 	.exec_op = zynq_qspi_exec_mem_op,
+	.adjust_op_size = zynq_qspi_adjust_op_size,
 };
 
 static const struct spi_controller_mem_caps zynq_qspi_mem_caps = {
-- 
2.53.0


  reply	other threads:[~2026-11-05 20:38 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 20:37 [PATCH v1 0/2] spi: zynq-qspi: use a flash larger than 16 MiB Mario Behn
2026-11-05 20:37 ` Mario Behn [this message]
2026-11-05 20:37 ` [PATCH v1 2/2] spi: zynq-qspi: program and erase flash above 16 MiB via the EAR Mario Behn

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261105203734.979168-2-mario.behn@gmail.com \
    --to=mario.behn@gmail.com \
    --cc=broonie@kernel.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-spi@vger.kernel.org \
    --cc=michal.simek@amd.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®