* [PATCH v1 0/2] spi: zynq-qspi: use a flash larger than 16 MiB
@ 2026-11-05 20:37 Mario Behn
2026-11-05 20:37 ` [PATCH v1 1/2] spi: zynq-qspi: read flash above 16 MiB via the extended address register Mario Behn
2026-11-05 20:37 ` [PATCH v1 2/2] spi: zynq-qspi: program and erase flash above 16 MiB via the EAR Mario Behn
0 siblings, 2 replies; 3+ messages in thread
From: Mario Behn @ 2026-11-05 20:37 UTC (permalink / raw)
To: Mark Brown, Michal Simek
Cc: linux-spi, linux-arm-kernel, linux-kernel, Mario Behn
The Zynq-7000 QSPI controller sends at most 3 address bytes. spi-nor
needs 4-byte addresses for a flash larger than 16 MiB, so on a board
with a 32 MiB W25Q256 (the ADALM-PLUTO+ has one) the probe fails with
-EINVAL and no mtd device appears.
These two patches keep the flash in 3-byte mode and handle an operation
with a 4-byte address inside the controller driver, through the flash's
Extended Address Register (EAR): write enable, write EAR with
addr >> 24, the operation in its 3-byte form, then the EAR back to 0.
The EAR is 0 between operations, so a warm reset leaves the BootROM and
U-Boot in bank 0.
Patch 1 makes the flash readable (every other operation is refused).
Patch 2 adds program and erase, which need the write enable again after
the EAR write, a check of the bank register before the operation, and
waiting for the flash to be idle before the EAR is restored.
Tested on an ADALM-PLUTO+ (Zynq-7010, W25Q256, JEDEC id ef4019) with
v7.3 plus these patches:
- the four partitions (mtd0 to mtd3, the last one 30 MiB and crossing
the 16 MiB bank boundary) read back with the same md5 as on a vendor
6.12 kernel that handles the 4-byte addresses in spi-nor;
- the U-Boot environment in mtd1 is written with fw_setenv, survives a
reboot and is read back;
- one 64 KiB block in bank 1: erase, program a random pattern, read
back (equal), restore;
- 14 MiB (224 erase blocks) in bank 1: erase, program, read back
(equal), restore; about 110 s each way;
- a 26 MB firmware image written into the 30 MiB partition from the
running system and verified byte for byte, after which the board
boots from QSPI;
- no driver error in the kernel log during any of it.
Not tested: other flash chips, boards with a flash on another chip
select, or the quad program path with a flash that is not a W25Q256.
Mario Behn (2):
spi: zynq-qspi: read flash above 16 MiB via the extended address register
spi: zynq-qspi: program and erase flash above 16 MiB via the EAR
drivers/spi/spi-zynq-qspi.c | 1 file changed
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v1 1/2] spi: zynq-qspi: read flash above 16 MiB via the extended address register
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
2026-11-05 20:37 ` [PATCH v1 2/2] spi: zynq-qspi: program and erase flash above 16 MiB via the EAR Mario Behn
1 sibling, 0 replies; 3+ messages in thread
From: Mario Behn @ 2026-11-05 20:37 UTC (permalink / raw)
To: Mark Brown, Michal Simek
Cc: linux-spi, linux-arm-kernel, linux-kernel, Mario Behn
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
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH v1 2/2] spi: zynq-qspi: program and erase flash above 16 MiB via the EAR
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 ` [PATCH v1 1/2] spi: zynq-qspi: read flash above 16 MiB via the extended address register Mario Behn
@ 2026-11-05 20:37 ` Mario Behn
1 sibling, 0 replies; 3+ messages in thread
From: Mario Behn @ 2026-11-05 20:37 UTC (permalink / raw)
To: Mark Brown, Michal Simek
Cc: linux-spi, linux-arm-kernel, linux-kernel, Mario Behn
The read-only stage turned every operation with a 4-byte address into
WREN, WREAR(addr >> 24), the 3-byte form of the operation and WREAR(0),
and refused programs and erases. Carry them the same way, with the
things they need:
- the write enable the spi-nor core sent before the operation is used
up by the WREAR command, so it is sent again after it (the ADI 6.12
spi-nor code does the same);
- a program or an erase with the wrong bank selected lands 16 MiB away
from where it was meant, so the bank register is read back (RDEAR)
before the operation and the write is refused with -EIO if it does
not hold the expected value;
- after a program or an erase the flash is busy and ignores everything
but Read Status, so the bank register is restored only once the WIP
bit is clear (bounded at 3 s, a 64 KiB block erase takes up to 2 s on
the W25Q256), then WREN, WREAR(0), WRDI so the write enable latch is
not left set.
The bank register is 0 between operations, so operations in bank 0 keep
going out unchanged and a warm reboot leaves the BootROM and U-Boot in
bank 0. Address-less commands (write enable and disable, status register
reads and writes, chip erase) go out as they are.
Signed-off-by: Mario Behn <mario.behn@gmail.com>
---
drivers/spi/spi-zynq-qspi.c | 113 +++++++++++++++++++++++++++++++-----
1 file changed, 98 insertions(+), 15 deletions(-)
diff --git a/drivers/spi/spi-zynq-qspi.c b/drivers/spi/spi-zynq-qspi.c
index e20019a03964..c72c00a82a99 100644
--- a/drivers/spi/spi-zynq-qspi.c
+++ b/drivers/spi/spi-zynq-qspi.c
@@ -230,10 +230,14 @@ static void zynq_qspi_init_hw(struct zynq_qspi *xqspi, unsigned int num_cs)
* 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).
+ * the controller driver instead of the spi-nor core). A program or an erase
+ * above 16 MiB needs a second write enable after the WREAR, and the flash is
+ * busy afterwards, so the bank register is restored once it is ready again.
*/
#define ZYNQ_QSPI_BANK_SIZE 0x1000000U
#define ZYNQ_QSPI_OP_WREAR 0xc5 /* Write Extended Address Register */
+#define ZYNQ_QSPI_OP_RDEAR 0xc8 /* Read Extended Address Register */
+#define ZYNQ_QSPI_READY_TIMEOUT_MS 3000
static bool zynq_qspi_supports_op(struct spi_mem *mem,
const struct spi_mem_op *op)
@@ -661,12 +665,44 @@ static int zynq_qspi_exec_plain_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)
+/* a program or an erase: the flash is busy afterwards */
+static bool zynq_qspi_op_is_write(const struct spi_mem_op *op)
{
- if (op->data.nbytes && op->data.dir == SPI_MEM_DATA_IN)
- return true;
- return false;
+ return !(op->data.nbytes && op->data.dir == SPI_MEM_DATA_IN);
+}
+
+/*
+ * Wait until the flash has finished a program or an erase (WIP clear). While
+ * it is busy it ignores every command but Read Status, so the bank register
+ * cannot be restored before this.
+ */
+static int zynq_qspi_wait_ready(struct spi_mem *mem, const struct spi_mem_op *like)
+{
+ u8 *sr = kmalloc(1, GFP_KERNEL);
+ struct spi_mem_op rdsr = SPI_MEM_OP(SPI_MEM_OP_CMD(SPINOR_OP_RDSR, 1),
+ SPI_MEM_OP_NO_ADDR,
+ SPI_MEM_OP_NO_DUMMY,
+ SPI_MEM_OP_DATA_IN(1, sr, 1));
+ unsigned long deadline;
+ int ret;
+
+ if (!sr)
+ return -ENOMEM;
+ rdsr.max_freq = like->max_freq;
+ /* a 64 KiB block erase takes up to 2 s on the W25Q256 */
+ deadline = jiffies + msecs_to_jiffies(ZYNQ_QSPI_READY_TIMEOUT_MS);
+ for (;;) {
+ ret = zynq_qspi_exec_plain_op(mem, &rdsr);
+ if (ret || !(*sr & SR_WIP))
+ break;
+ if (time_after(jiffies, deadline)) {
+ ret = -ETIMEDOUT;
+ break;
+ }
+ usleep_range(200, 500);
+ }
+ kfree(sr);
+ return ret;
}
static int zynq_qspi_ear_cmds(struct spi_mem *mem, const struct spi_mem_op *like,
@@ -692,16 +728,60 @@ static int zynq_qspi_ear_cmds(struct spi_mem *mem, const struct spi_mem_op *like
ret = zynq_qspi_exec_plain_op(mem, &wren);
if (!ret)
ret = zynq_qspi_exec_plain_op(mem, &wrear);
+ /*
+ * A program or an erase with the wrong bank selected lands 16 MiB away
+ * from where it was meant, so read the register back before going on.
+ */
+ if (!ret && wren_after) {
+ struct spi_mem_op rdear = SPI_MEM_OP(SPI_MEM_OP_CMD(ZYNQ_QSPI_OP_RDEAR, 1),
+ SPI_MEM_OP_NO_ADDR,
+ SPI_MEM_OP_NO_DUMMY,
+ SPI_MEM_OP_DATA_IN(1, buf, 1));
+
+ rdear.max_freq = like->max_freq;
+ *buf = ~ear;
+ ret = zynq_qspi_exec_plain_op(mem, &rdear);
+ if (!ret && *buf != ear) {
+ dev_err(&mem->spi->dev,
+ "bank register %u not taken (reads %u), write refused\n",
+ ear, *buf);
+ ret = -EIO;
+ }
+ }
if (!ret && wren_after)
ret = zynq_qspi_exec_plain_op(mem, &wren);
kfree(buf);
return ret;
}
+/*
+ * Back to bank 0: after a program or an erase wait for the flash first, then
+ * WREN + WREAR(0) + WRDI (the write enable latch must not stay set).
+ */
+static int zynq_qspi_ear_restore(struct spi_mem *mem, const struct spi_mem_op *like,
+ bool after_write)
+{
+ struct spi_mem_op wrdi = SPI_MEM_OP(SPI_MEM_OP_CMD(SPINOR_OP_WRDI, 1),
+ SPI_MEM_OP_NO_ADDR,
+ SPI_MEM_OP_NO_DUMMY,
+ SPI_MEM_OP_NO_DATA);
+ int ret = 0;
+
+ wrdi.max_freq = like->max_freq;
+ if (after_write)
+ ret = zynq_qspi_wait_ready(mem, like);
+ if (!ret)
+ ret = zynq_qspi_ear_cmds(mem, like, 0, false);
+ if (!ret)
+ ret = zynq_qspi_exec_plain_op(mem, &wrdi);
+ 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;
+ bool write;
u8 ear;
int ret, ret2;
@@ -710,23 +790,26 @@ static int zynq_qspi_exec_mem_op(struct spi_mem *mem,
(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;
- }
-
+ /*
+ * Operations without a 4-byte address go out as they are: reads and
+ * writes in bank 0, and the address-less commands (write enable and
+ * disable, status register reads and writes, chip erase).
+ */
if (op->addr.nbytes != 4)
return zynq_qspi_exec_plain_op(mem, op);
+ write = zynq_qspi_op_is_write(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);
+ /*
+ * The write enable the caller sent before a program or an erase is
+ * consumed by the WREAR command, so it is sent again after it.
+ */
if (ear) {
- ret = zynq_qspi_ear_cmds(mem, op, ear, false);
+ ret = zynq_qspi_ear_cmds(mem, op, ear, write);
if (ret)
return ret;
}
@@ -734,7 +817,7 @@ static int zynq_qspi_exec_mem_op(struct spi_mem *mem,
ret = zynq_qspi_exec_plain_op(mem, &nop);
if (ear) {
- ret2 = zynq_qspi_ear_cmds(mem, op, 0, false);
+ ret2 = zynq_qspi_ear_restore(mem, op, write);
if (!ret)
ret = ret2;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-05 20:38 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH v1 1/2] spi: zynq-qspi: read flash above 16 MiB via the extended address register Mario Behn
2026-11-05 20:37 ` [PATCH v1 2/2] spi: zynq-qspi: program and erase flash above 16 MiB via the EAR Mario Behn
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox