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
next prev parent 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®