From 829dff83597615208aedf0f5abb3878b47f2314d Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:25 +0200 Subject: mtd: spi-nor: debugfs: Fix the flags list As mentioned above the spi_nor_option_flags enumeration in core.h, this list should be kept in sync with the one in the core. Add the missing flag. Fixes: 6a42bc97ccda ("mtd: spi-nor: core: Allow specifying the byte order in Octal DTR mode") Reviewed-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/debugfs.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/mtd/spi-nor/debugfs.c b/drivers/mtd/spi-nor/debugfs.c index fa6956144d2e..d700e0b27182 100644 --- a/drivers/mtd/spi-nor/debugfs.c +++ b/drivers/mtd/spi-nor/debugfs.c @@ -28,6 +28,7 @@ static const char *const snor_f_names[] = { SNOR_F_NAME(RWW), SNOR_F_NAME(ECC), SNOR_F_NAME(NO_WP), + SNOR_F_NAME(SWAP16), }; #undef SNOR_F_NAME -- cgit v1.2.3 From e1d456b26bf23e30db305a6184e8abd9ab68bbf2 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:26 +0200 Subject: mtd: spi-nor: swp: Improve locking user experience In the case of the first block being locked (or the few first blocks), if the user want to fully unlock the device it has two possibilities: - either it asks to unlock the entire device, and this works; - or it asks to unlock just the block(s) that are currently locked, which fails. It fails because the conditions "can_be_top" and "can_be_bottom" are true. Indeed, in this case, we unlock everything, so the TB bit does not matter. However in the current implementation, use_top would be true (as this is the favourite option) and lock_len, which in practice should be reduced down to 0, is set to "nor->params->size - (ofs + len)" which is a positive number. This is wrong. An easy way is to simply add an extra condition. In the unlock() path, if we can achieve the same result from both sides, it means we unlock everything and lock_len must simply be 0. A comment is added to clarify that logic. Fixes: 3dd8012a8eeb ("mtd: spi-nor: add TB (Top/Bottom) protect support") Cc: stable@kernel.org Signed-off-by: Miquel Raynal Reviewed-by: Michael Walle Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index e67a81dbb6bf..d5f4bf555cfc 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -282,8 +282,15 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) /* Prefer top, if both are valid */ use_top = can_be_top; - /* lock_len: length of region that should remain locked */ - if (use_top) + /* + * lock_len: length of region that should remain locked. + * + * When can_be_top and can_be_bottom booleans are true, both adjacent + * regions are unlocked, thus the entire flash can be unlocked. + */ + if (can_be_top && can_be_bottom) + lock_len = 0; + else if (use_top) lock_len = nor->params->size - (ofs + len); else lock_len = ofs; -- cgit v1.2.3 From a6470e2162e9c3779a4bd6ff3bed1b81d796e46e Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:27 +0200 Subject: mtd: spi-nor: Drop duplicate Kconfig dependency I do not think the MTD dependency is needed twice. This is likely a duplicate coming from a former rebase when the spi-nor core got cleaned up a while ago. Remove the extra line. Fixes: b35b9a10362d ("mtd: spi-nor: Move m25p80 code in spi-nor.c") Signed-off-by: Miquel Raynal Reviewed-by: Michael Walle Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/Kconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/mtd/spi-nor/Kconfig b/drivers/mtd/spi-nor/Kconfig index 24cd25de2b8b..fd05a24d64a9 100644 --- a/drivers/mtd/spi-nor/Kconfig +++ b/drivers/mtd/spi-nor/Kconfig @@ -1,7 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-only menuconfig MTD_SPI_NOR tristate "SPI NOR device support" - depends on MTD depends on MTD && SPI_MASTER select SPI_MEM help -- cgit v1.2.3 From f316b8535887c50be009902bd02098fddb47e2e7 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:28 +0200 Subject: mtd: spi-nor: Make sure the QE bit is kept enabled if useful Not all chips implement the 4BAIT table which typically indicates the program capability, while many of them do implement the relevant SFDP parts indicating the read capabilities. In such a situation, programs can happen in single mode (1-1-1) and reads in quad mode (1-1-4 or 1-4-4). For the reads to work in such condition, the QE bit must be set. In case we later use the spi_nor_write_16bit_sr_and_check() helper with a chip with such configuration, the QE bit would get incorrectly cleared. Make sure this doesn't happen by keeping the QE bit under a simpler condition: - the quad enable hook is there (no change) - and at least one of the two protocols is based on quad I/O cycles Signed-off-by: Miquel Raynal Reviewed-by: Pratyush Yadav Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/core.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c index 5dd0b3cb5250..394c27de02d6 100644 --- a/drivers/mtd/spi-nor/core.c +++ b/drivers/mtd/spi-nor/core.c @@ -869,8 +869,8 @@ static int spi_nor_write_16bit_sr_and_check(struct spi_nor *nor, u8 sr1) ret = spi_nor_read_cr(nor, &sr_cr[1]); if (ret) return ret; - } else if (spi_nor_get_protocol_width(nor->read_proto) == 4 && - spi_nor_get_protocol_width(nor->write_proto) == 4 && + } else if ((spi_nor_get_protocol_width(nor->read_proto) == 4 || + spi_nor_get_protocol_width(nor->write_proto) == 4) && nor->params->quad_enable) { /* * If the Status Register 2 Read command (35h) is not -- cgit v1.2.3 From 7c4e909b176f661e834853fa726a6e58acab00e1 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:29 +0200 Subject: mtd: spi-nor: Improve opcodes documentation There are two status registers, named 1 and 2. The current wording is misleading as "1" may refer to the status register ID as well as the number of bytes required (which, in this case can be 1 or 2). Clarify the comments by aligning them on the same pattern: "{read,write} status {1,2} register" Reviewed-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- include/linux/mtd/spi-nor.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/linux/mtd/spi-nor.h b/include/linux/mtd/spi-nor.h index cdcfe0fd2e7d..90a0cf583512 100644 --- a/include/linux/mtd/spi-nor.h +++ b/include/linux/mtd/spi-nor.h @@ -21,8 +21,8 @@ /* Flash opcodes. */ #define SPINOR_OP_WRDI 0x04 /* Write disable */ #define SPINOR_OP_WREN 0x06 /* Write enable */ -#define SPINOR_OP_RDSR 0x05 /* Read status register */ -#define SPINOR_OP_WRSR 0x01 /* Write status register 1 byte */ +#define SPINOR_OP_RDSR 0x05 /* Read status register 1 */ +#define SPINOR_OP_WRSR 0x01 /* Write status register 1 */ #define SPINOR_OP_RDSR2 0x3f /* Read status register 2 */ #define SPINOR_OP_WRSR2 0x3e /* Write status register 2 */ #define SPINOR_OP_READ 0x03 /* Read data bytes (low frequency) */ -- cgit v1.2.3 From 154f375f7d31f2f57b4d312ac22562d64a7bb64f Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:30 +0200 Subject: mtd: spi-nor: debugfs: Align variable access with the rest of the file The "params" variable is used everywhere else, align this particular line of the file to use "params" directly rather than the "nor" pointer. Reviewed-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/debugfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mtd/spi-nor/debugfs.c b/drivers/mtd/spi-nor/debugfs.c index d700e0b27182..69830ad43990 100644 --- a/drivers/mtd/spi-nor/debugfs.c +++ b/drivers/mtd/spi-nor/debugfs.c @@ -139,7 +139,7 @@ static int spi_nor_params_show(struct seq_file *s, void *data) if (!(nor->flags & SNOR_F_NO_OP_CHIP_ERASE)) { string_get_size(params->size, 1, STRING_UNITS_2, buf, sizeof(buf)); - seq_printf(s, " %02x (%s)\n", nor->params->die_erase_opcode, buf); + seq_printf(s, " %02x (%s)\n", params->die_erase_opcode, buf); } seq_puts(s, "\nsector map\n"); -- cgit v1.2.3 From 461e2f8a2d943ea2b1b966e8ffc8e5ac02c6a686 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:31 +0200 Subject: mtd: spi-nor: debugfs: Enhance output Align the number of dashes to the bigger column width (the title in this case) to make the output more pleasant and aligned with what is done in the "params" file output. Reviewed-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/debugfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mtd/spi-nor/debugfs.c b/drivers/mtd/spi-nor/debugfs.c index 69830ad43990..d0191eb9f879 100644 --- a/drivers/mtd/spi-nor/debugfs.c +++ b/drivers/mtd/spi-nor/debugfs.c @@ -144,7 +144,7 @@ static int spi_nor_params_show(struct seq_file *s, void *data) seq_puts(s, "\nsector map\n"); seq_puts(s, " region (in hex) | erase mask | overlaid\n"); - seq_puts(s, " ------------------+------------+----------\n"); + seq_puts(s, " ------------------+------------+---------\n"); for (i = 0; i < erase_map->n_regions; i++) { u64 start = region[i].offset; u64 end = start + region[i].size - 1; -- cgit v1.2.3 From 5eeff82d389e381422f48a4bde4b7f4a5a2dc584 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:32 +0200 Subject: mtd: spi-nor: swp: Explain the MEMLOCK ioctl implementation behaviour Add more details about how these requests are actually handled in the SPI NOR core. Their behaviour was not entirely clear to me at first, and explaining them in plain English sounds the way to go. Reviewed-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/core.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/drivers/mtd/spi-nor/core.h b/drivers/mtd/spi-nor/core.h index e838c40a2589..65bb4b5e1e2b 100644 --- a/drivers/mtd/spi-nor/core.h +++ b/drivers/mtd/spi-nor/core.h @@ -279,9 +279,14 @@ struct spi_nor_erase_map { /** * struct spi_nor_locking_ops - SPI NOR locking methods - * @lock: lock a region of the SPI NOR. - * @unlock: unlock a region of the SPI NOR. - * @is_locked: check if a region of the SPI NOR is completely locked + * @lock: lock a region of the SPI NOR, never locks more than what is + * requested, ie. may lock less. + * @unlock: unlock a region of the SPI NOR, may unlock more than what is + * requested. + * @is_locked: check if a region of the SPI NOR is completely locked, returns + * false otherwise. This feedback may be misleading because users + * may get an "unlocked" status even though a subpart of the region + * is effectively locked. */ struct spi_nor_locking_ops { int (*lock)(struct spi_nor *nor, loff_t ofs, u64 len); -- cgit v1.2.3 From cb3021466daa3d7f87cdec8c294649315dad711f Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:33 +0200 Subject: mtd: spi-nor: swp: Clarify a comment The comment states that some power of two sizes are not supported. This is very device dependent (based on the size), so modulate a bit the sentence to make it more accurate. Reviewed-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index d5f4bf555cfc..f221d6361b57 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -305,7 +305,7 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3) val = (val & ~SR_BP3) | SR_BP3_BIT6; - /* Some power-of-two sizes are not supported */ + /* Some power-of-two sizes may not be supported */ if (val & ~mask) return -EINVAL; } -- cgit v1.2.3 From 2188bbbb4a20ca1864de8e1447d13e5d19e8355a Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:34 +0200 Subject: mtd: spi-nor: swp: Use a pointer for SR instead of a single byte At this stage, the Status Register is most often seen as a single byte. This is subject to change when we will need to read the CMP bit which is located in the Control Register (kind of secondary status register). Both will need to be carried. Change a few prototypes to carry a u8 pointer. This way it also makes it very clear where we access the first register, and where we will access the second. There is no functional change. Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 48 ++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index f221d6361b57..61830f18a147 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -55,13 +55,13 @@ static u64 spi_nor_get_min_prot_length_sr(struct spi_nor *nor) return sector_size; } -static void spi_nor_get_locked_range_sr(struct spi_nor *nor, u8 sr, loff_t *ofs, +static void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_t *ofs, u64 *len) { u64 min_prot_len; u8 mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); - u8 bp, val = sr & mask; + u8 bp, val = sr[0] & mask; if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3_BIT6) val = (val & ~SR_BP3_BIT6) | SR_BP3; @@ -81,7 +81,7 @@ static void spi_nor_get_locked_range_sr(struct spi_nor *nor, u8 sr, loff_t *ofs, if (*len > nor->params->size) *len = nor->params->size; - if (nor->flags & SNOR_F_HAS_SR_TB && sr & tb_mask) + if (nor->flags & SNOR_F_HAS_SR_TB && sr[0] & tb_mask) *ofs = 0; else *ofs = nor->params->size - *len; @@ -92,7 +92,7 @@ static void spi_nor_get_locked_range_sr(struct spi_nor *nor, u8 sr, loff_t *ofs, * (if @locked is false); false otherwise. */ static bool spi_nor_check_lock_status_sr(struct spi_nor *nor, loff_t ofs, - u64 len, u8 sr, bool locked) + u64 len, const u8 *sr, bool locked) { loff_t lock_offs, lock_offs_max, offs_max; u64 lock_len; @@ -113,13 +113,13 @@ static bool spi_nor_check_lock_status_sr(struct spi_nor *nor, loff_t ofs, return (ofs >= lock_offs_max) || (offs_max <= lock_offs); } -static bool spi_nor_is_locked_sr(struct spi_nor *nor, loff_t ofs, u64 len, u8 sr) +static bool spi_nor_is_locked_sr(struct spi_nor *nor, loff_t ofs, u64 len, const u8 *sr) { return spi_nor_check_lock_status_sr(nor, ofs, len, sr, true); } static bool spi_nor_is_unlocked_sr(struct spi_nor *nor, loff_t ofs, u64 len, - u8 sr) + const u8 *sr) { return spi_nor_check_lock_status_sr(nor, ofs, len, sr, false); } @@ -160,7 +160,8 @@ static bool spi_nor_is_unlocked_sr(struct spi_nor *nor, loff_t ofs, u64 len, static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) { u64 min_prot_len; - int ret, status_old, status_new; + int ret; + u8 status_old[1] = {}, status_new[1] = {}; u8 mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); u8 pow, val; @@ -172,7 +173,7 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) if (ret) return ret; - status_old = nor->bouncebuf[0]; + status_old[0] = nor->bouncebuf[0]; /* If nothing in our range is unlocked, we don't need to do anything */ if (spi_nor_is_locked_sr(nor, ofs, len, status_old)) @@ -217,7 +218,7 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) return -EINVAL; } - status_new = (status_old & ~mask & ~tb_mask) | val; + status_new[0] = (status_old[0] & ~mask & ~tb_mask) | val; /* * Disallow further writes if WP# pin is neither left floating nor @@ -225,20 +226,20 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) * WP# pin hard strapped to GND can be a valid use case. */ if (!(nor->flags & SNOR_F_NO_WP)) - status_new |= SR_SRWD; + status_new[0] |= SR_SRWD; if (!use_top) - status_new |= tb_mask; + status_new[0] |= tb_mask; /* Don't bother if they're the same */ - if (status_new == status_old) + if (status_new[0] == status_old[0]) return 0; /* Only modify protection if it will not unlock other areas */ - if ((status_new & mask) < (status_old & mask)) + if ((status_new[0] & mask) < (status_old[0] & mask)) return -EINVAL; - return spi_nor_write_sr_and_check(nor, status_new); + return spi_nor_write_sr_and_check(nor, status_new[0]); } /* @@ -249,7 +250,8 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) { u64 min_prot_len; - int ret, status_old, status_new; + int ret; + u8 status_old[1], status_new[1]; u8 mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); u8 pow, val; @@ -261,7 +263,7 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) if (ret) return ret; - status_old = nor->bouncebuf[0]; + status_old[0] = nor->bouncebuf[0]; /* If nothing in our range is locked, we don't need to do anything */ if (spi_nor_is_unlocked_sr(nor, ofs, len, status_old)) @@ -310,24 +312,24 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) return -EINVAL; } - status_new = (status_old & ~mask & ~tb_mask) | val; + status_new[0] = (status_old[0] & ~mask & ~tb_mask) | val; /* Don't protect status register if we're fully unlocked */ if (lock_len == 0) - status_new &= ~SR_SRWD; + status_new[0] &= ~SR_SRWD; if (!use_top) - status_new |= tb_mask; + status_new[0] |= tb_mask; /* Don't bother if they're the same */ - if (status_new == status_old) + if (status_new[0] == status_old[0]) return 0; /* Only modify protection if it will not lock other areas */ - if ((status_new & mask) > (status_old & mask)) + if ((status_new[0] & mask) > (status_old[0] & mask)) return -EINVAL; - return spi_nor_write_sr_and_check(nor, status_new); + return spi_nor_write_sr_and_check(nor, status_new[0]); } /* @@ -345,7 +347,7 @@ static int spi_nor_sr_is_locked(struct spi_nor *nor, loff_t ofs, u64 len) if (ret) return ret; - return spi_nor_is_locked_sr(nor, ofs, len, nor->bouncebuf[0]); + return spi_nor_is_locked_sr(nor, ofs, len, nor->bouncebuf); } static const struct spi_nor_locking_ops spi_nor_sr_locking_ops = { -- cgit v1.2.3 From 82877fd772f7884474add88c476108b1dd670ef6 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:35 +0200 Subject: mtd: spi-nor: swp: Create a helper that writes SR, CR and checks There are many helpers already to either read and/or write SR and/or CR, as well as sometimes check the returned values. In order to be able to switch from a 1 byte status register to a 2 bytes status register while keeping the same level of verification, let's introduce a new helper that writes them both (atomically) and then reads them back (separated) to compare the values. In case 2 bytes registers are not supported, we still have the usual fallback available in the helper being exported to the rest of the core. Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/core.c | 65 ++++++++++++++++++++++++++++++++++++++++++++++ drivers/mtd/spi-nor/core.h | 1 + 2 files changed, 66 insertions(+) diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c index 394c27de02d6..2799c21d0b67 100644 --- a/drivers/mtd/spi-nor/core.c +++ b/drivers/mtd/spi-nor/core.c @@ -976,6 +976,54 @@ int spi_nor_write_16bit_cr_and_check(struct spi_nor *nor, u8 cr) return 0; } +/** + * spi_nor_write_16bit_sr_cr_and_check() - Write the Status Register 1 and the + * Configuration Register in one shot. Ensure that the bytes written in both + * registers match the received value. + * @nor: pointer to a 'struct spi_nor'. + * @regs: two-byte array with values to be written to the status and + * configuration registers. + * + * Return: 0 on success, -errno otherwise. + */ +static int spi_nor_write_16bit_sr_cr_and_check(struct spi_nor *nor, const u8 *regs) +{ + u8 written_regs[2]; + int ret; + + written_regs[0] = regs[0]; + written_regs[1] = regs[1]; + nor->bouncebuf[0] = regs[0]; + nor->bouncebuf[1] = regs[1]; + + ret = spi_nor_write_sr(nor, nor->bouncebuf, 2); + if (ret) + return ret; + + ret = spi_nor_read_sr(nor, &nor->bouncebuf[0]); + if (ret) + return ret; + + if (written_regs[0] != nor->bouncebuf[0]) { + dev_dbg(nor->dev, "SR: Read back test failed\n"); + return -EIO; + } + + if (nor->flags & SNOR_F_NO_READ_CR) + return 0; + + ret = spi_nor_read_cr(nor, &nor->bouncebuf[1]); + if (ret) + return ret; + + if (written_regs[1] != nor->bouncebuf[1]) { + dev_dbg(nor->dev, "CR: read back test failed\n"); + return -EIO; + } + + return 0; +} + /** * spi_nor_write_sr_and_check() - Write the Status Register 1 and ensure that * the byte written match the received value without affecting other bits in the @@ -993,6 +1041,23 @@ int spi_nor_write_sr_and_check(struct spi_nor *nor, u8 sr1) return spi_nor_write_sr1_and_check(nor, sr1); } +/** + * spi_nor_write_sr_cr_and_check() - Write the Status Register 1 and ensure that + * the byte written match the received value. Same for the Control Register if + * available. + * @nor: pointer to a 'struct spi_nor'. + * @regs: byte array to be written to the registers. + * + * Return: 0 on success, -errno otherwise. + */ +int spi_nor_write_sr_cr_and_check(struct spi_nor *nor, const u8 *regs) +{ + if (nor->flags & SNOR_F_HAS_16BIT_SR) + return spi_nor_write_16bit_sr_cr_and_check(nor, regs); + + return spi_nor_write_sr1_and_check(nor, regs[0]); +} + /** * spi_nor_write_sr2() - Write the Status Register 2 using the * SPINOR_OP_WRSR2 (3eh) command. diff --git a/drivers/mtd/spi-nor/core.h b/drivers/mtd/spi-nor/core.h index 65bb4b5e1e2b..0753d0a6f8b8 100644 --- a/drivers/mtd/spi-nor/core.h +++ b/drivers/mtd/spi-nor/core.h @@ -637,6 +637,7 @@ int spi_nor_read_cr(struct spi_nor *nor, u8 *cr); int spi_nor_write_sr(struct spi_nor *nor, const u8 *sr, size_t len); int spi_nor_write_sr_and_check(struct spi_nor *nor, u8 sr1); int spi_nor_write_16bit_cr_and_check(struct spi_nor *nor, u8 cr); +int spi_nor_write_sr_cr_and_check(struct spi_nor *nor, const u8 *regs); ssize_t spi_nor_read_data(struct spi_nor *nor, loff_t from, size_t len, u8 *buf); -- cgit v1.2.3 From 8851f82645b42307b74dba190bee5a19cec97ae3 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:36 +0200 Subject: mtd: spi-nor: swp: Rename a mask "mask" is not very descriptive when we already manipulate two masks, and soon will manipulate three. Rename it "bp_mask" to align with the existing "tb_mask" and soon "cmp_mask". Signed-off-by: Miquel Raynal Reviewed-by: Tudor Ambarus Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index 61830f18a147..07269e09370a 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -59,9 +59,9 @@ static void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_ u64 *len) { u64 min_prot_len; - u8 mask = spi_nor_get_sr_bp_mask(nor); + u8 bp_mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); - u8 bp, val = sr[0] & mask; + u8 bp, val = sr[0] & bp_mask; if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3_BIT6) val = (val & ~SR_BP3_BIT6) | SR_BP3; @@ -162,7 +162,7 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) u64 min_prot_len; int ret; u8 status_old[1] = {}, status_new[1] = {}; - u8 mask = spi_nor_get_sr_bp_mask(nor); + u8 bp_mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); u8 pow, val; loff_t lock_len; @@ -201,7 +201,7 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) lock_len = ofs + len; if (lock_len == nor->params->size) { - val = mask; + val = bp_mask; } else { min_prot_len = spi_nor_get_min_prot_length_sr(nor); pow = ilog2(lock_len) - ilog2(min_prot_len) + 1; @@ -210,15 +210,15 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3) val = (val & ~SR_BP3) | SR_BP3_BIT6; - if (val & ~mask) + if (val & ~bp_mask) return -EINVAL; /* Don't "lock" with no region! */ - if (!(val & mask)) + if (!(val & bp_mask)) return -EINVAL; } - status_new[0] = (status_old[0] & ~mask & ~tb_mask) | val; + status_new[0] = (status_old[0] & ~bp_mask & ~tb_mask) | val; /* * Disallow further writes if WP# pin is neither left floating nor @@ -236,7 +236,7 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) return 0; /* Only modify protection if it will not unlock other areas */ - if ((status_new[0] & mask) < (status_old[0] & mask)) + if ((status_new[0] & bp_mask) < (status_old[0] & bp_mask)) return -EINVAL; return spi_nor_write_sr_and_check(nor, status_new[0]); @@ -252,7 +252,7 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) u64 min_prot_len; int ret; u8 status_old[1], status_new[1]; - u8 mask = spi_nor_get_sr_bp_mask(nor); + u8 bp_mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); u8 pow, val; loff_t lock_len; @@ -308,11 +308,11 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) val = (val & ~SR_BP3) | SR_BP3_BIT6; /* Some power-of-two sizes may not be supported */ - if (val & ~mask) + if (val & ~bp_mask) return -EINVAL; } - status_new[0] = (status_old[0] & ~mask & ~tb_mask) | val; + status_new[0] = (status_old[0] & ~bp_mask & ~tb_mask) | val; /* Don't protect status register if we're fully unlocked */ if (lock_len == 0) @@ -326,7 +326,7 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) return 0; /* Only modify protection if it will not lock other areas */ - if ((status_new[0] & mask) > (status_old[0] & mask)) + if ((status_new[0] & bp_mask) > (status_old[0] & bp_mask)) return -EINVAL; return spi_nor_write_sr_and_check(nor, status_new[0]); -- cgit v1.2.3 From ba32a259dd32af911e4f4fd49cdd7f32a40ccde5 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:37 +0200 Subject: mtd: spi-nor: swp: Create a TB intermediate variable Ease the future reuse of the tb (Top/Bottom) boolean by creating an intermediate variable. Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index 07269e09370a..540cd221c455 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -62,6 +62,7 @@ static void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_ u8 bp_mask = spi_nor_get_sr_bp_mask(nor); u8 tb_mask = spi_nor_get_sr_tb_mask(nor); u8 bp, val = sr[0] & bp_mask; + bool tb = (nor->flags & SNOR_F_HAS_SR_TB) ? sr[0] & tb_mask : 0; if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3_BIT6) val = (val & ~SR_BP3_BIT6) | SR_BP3; @@ -81,7 +82,7 @@ static void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_ if (*len > nor->params->size) *len = nor->params->size; - if (nor->flags & SNOR_F_HAS_SR_TB && sr[0] & tb_mask) + if (tb) *ofs = 0; else *ofs = nor->params->size - *len; -- cgit v1.2.3 From 290e83833688b4df0da746af4c5fb87dbed2834a Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:38 +0200 Subject: mtd: spi-nor: swp: Create helpers for building the SR register The status register contains 3 or 4 BP (Block Protect) bits, 0 or 1 TB (Top/Bottom) bit, soon 0 or 1 CMP (Complement) bit. The last BP bit and the TB bit locations change between vendors. The whole logic of buildling the content of the status register based on some input conditions is used two times and soon will be used 4 times. Create dedicated helpers for these steps. Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 83 +++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index 540cd221c455..8a75cd59cb10 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -125,6 +125,43 @@ static bool spi_nor_is_unlocked_sr(struct spi_nor *nor, loff_t ofs, u64 len, return spi_nor_check_lock_status_sr(nor, ofs, len, sr, false); } +static int spi_nor_sr_set_bp_mask(struct spi_nor *nor, u8 *sr, u8 pow) +{ + u8 mask = spi_nor_get_sr_bp_mask(nor); + u8 val = pow << SR_BP_SHIFT; + + if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3) + val = (val & ~SR_BP3) | SR_BP3_BIT6; + + if (val & ~mask) + return -EINVAL; + + sr[0] |= val; + + return 0; +} + +static int spi_nor_build_sr(struct spi_nor *nor, const u8 *old_sr, u8 *new_sr, + u8 pow, bool use_top) +{ + u8 bp_mask = spi_nor_get_sr_bp_mask(nor); + u8 tb_mask = spi_nor_get_sr_tb_mask(nor); + int ret; + + new_sr[0] = old_sr[0] & ~bp_mask & ~tb_mask; + + /* Build BP field */ + ret = spi_nor_sr_set_bp_mask(nor, &new_sr[0], pow); + if (ret) + return ret; + + /* Build TB field */ + if (!use_top) + new_sr[0] |= tb_mask; + + return 0; +} + /* * Lock a region of the flash. Compatible with ST Micro and similar flash. * Supports the block protection bits BP{0,1,2}/BP{0,1,2,3} in the status @@ -164,11 +201,10 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) int ret; u8 status_old[1] = {}, status_new[1] = {}; u8 bp_mask = spi_nor_get_sr_bp_mask(nor); - u8 tb_mask = spi_nor_get_sr_tb_mask(nor); - u8 pow, val; loff_t lock_len; bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB; bool use_top; + u8 pow; ret = spi_nor_read_sr(nor, nor->bouncebuf); if (ret) @@ -202,24 +238,19 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) lock_len = ofs + len; if (lock_len == nor->params->size) { - val = bp_mask; + pow = (nor->flags & SNOR_F_HAS_4BIT_BP) ? GENMASK(3, 0) : GENMASK(2, 0); } else { min_prot_len = spi_nor_get_min_prot_length_sr(nor); pow = ilog2(lock_len) - ilog2(min_prot_len) + 1; - val = pow << SR_BP_SHIFT; - - if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3) - val = (val & ~SR_BP3) | SR_BP3_BIT6; - - if (val & ~bp_mask) - return -EINVAL; - - /* Don't "lock" with no region! */ - if (!(val & bp_mask)) - return -EINVAL; } - status_new[0] = (status_old[0] & ~bp_mask & ~tb_mask) | val; + ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top); + if (ret) + return ret; + + /* Don't "lock" with no region! */ + if (!(status_new[0] & bp_mask)) + return -EINVAL; /* * Disallow further writes if WP# pin is neither left floating nor @@ -229,9 +260,6 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) if (!(nor->flags & SNOR_F_NO_WP)) status_new[0] |= SR_SRWD; - if (!use_top) - status_new[0] |= tb_mask; - /* Don't bother if they're the same */ if (status_new[0] == status_old[0]) return 0; @@ -254,11 +282,10 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) int ret; u8 status_old[1], status_new[1]; u8 bp_mask = spi_nor_get_sr_bp_mask(nor); - u8 tb_mask = spi_nor_get_sr_tb_mask(nor); - u8 pow, val; loff_t lock_len; bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB; bool use_top; + u8 pow; ret = spi_nor_read_sr(nor, nor->bouncebuf); if (ret) @@ -299,29 +326,21 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) lock_len = ofs; if (lock_len == 0) { - val = 0; /* fully unlocked */ + pow = 0; /* fully unlocked */ } else { min_prot_len = spi_nor_get_min_prot_length_sr(nor); pow = ilog2(lock_len) - ilog2(min_prot_len) + 1; - val = pow << SR_BP_SHIFT; - - if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3) - val = (val & ~SR_BP3) | SR_BP3_BIT6; - /* Some power-of-two sizes may not be supported */ - if (val & ~bp_mask) - return -EINVAL; } - status_new[0] = (status_old[0] & ~bp_mask & ~tb_mask) | val; + ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top); + if (ret) + return ret; /* Don't protect status register if we're fully unlocked */ if (lock_len == 0) status_new[0] &= ~SR_SRWD; - if (!use_top) - status_new[0] |= tb_mask; - /* Don't bother if they're the same */ if (status_new[0] == status_old[0]) return 0; -- cgit v1.2.3 From c672673c4b7b82d21537cd5aa32b98f48c20a2b2 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:39 +0200 Subject: mtd: spi-nor: swp: Simplify checking the locked/unlocked range In both the locking/unlocking steps, at the end we verify whether we do not lock/unlock more than requested (in which case an error must be returned). While being possible to do that with very simple mask comparisons, it does not scale when adding extra locking features such as the CMP possibility. In order to make these checks slightly easier to read and more future proof, use existing helpers to read the (future) status register, extract the covered range, and compare it with very usual algebric comparisons. Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index 8a75cd59cb10..229074d4e121 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -200,7 +200,8 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) u64 min_prot_len; int ret; u8 status_old[1] = {}, status_new[1] = {}; - u8 bp_mask = spi_nor_get_sr_bp_mask(nor); + loff_t ofs_old, ofs_new; + u64 len_old, len_new; loff_t lock_len; bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB; bool use_top; @@ -248,10 +249,6 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) if (ret) return ret; - /* Don't "lock" with no region! */ - if (!(status_new[0] & bp_mask)) - return -EINVAL; - /* * Disallow further writes if WP# pin is neither left floating nor * wrongly tied to GND (that includes internal pull-downs). @@ -260,12 +257,20 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) if (!(nor->flags & SNOR_F_NO_WP)) status_new[0] |= SR_SRWD; + spi_nor_get_locked_range_sr(nor, status_old, &ofs_old, &len_old); + spi_nor_get_locked_range_sr(nor, status_new, &ofs_new, &len_new); + + /* Don't "lock" with no region! */ + if (!len_new) + return -EINVAL; + /* Don't bother if they're the same */ if (status_new[0] == status_old[0]) return 0; /* Only modify protection if it will not unlock other areas */ - if ((status_new[0] & bp_mask) < (status_old[0] & bp_mask)) + if (len_old && + (ofs_old < ofs_new || (ofs_new + len_new) < (ofs_old + len_old))) return -EINVAL; return spi_nor_write_sr_and_check(nor, status_new[0]); @@ -281,7 +286,8 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) u64 min_prot_len; int ret; u8 status_old[1], status_new[1]; - u8 bp_mask = spi_nor_get_sr_bp_mask(nor); + loff_t ofs_old, ofs_new; + u64 len_old, len_new; loff_t lock_len; bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB; bool use_top; @@ -346,7 +352,10 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) return 0; /* Only modify protection if it will not lock other areas */ - if ((status_new[0] & bp_mask) > (status_old[0] & bp_mask)) + spi_nor_get_locked_range_sr(nor, status_old, &ofs_old, &len_old); + spi_nor_get_locked_range_sr(nor, status_new, &ofs_new, &len_new); + if (len_old && len_new && + (ofs_new < ofs_old || (ofs_old + len_old) < (ofs_new + len_new))) return -EINVAL; return spi_nor_write_sr_and_check(nor, status_new[0]); -- cgit v1.2.3 From 0e005045292dde634ed3973b5070eb2de62dc1df Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:40 +0200 Subject: mtd: spi-nor: swp: Cosmetic changes As a final preparation step for the introduction of CMP support, make a few more cosmetic changes to simplify the reading of the diff when adding the CMP feature. In particular, define "min_prot_len" earlier as it will be reused and move the definition of the "ret" variable at the end of the stack just because it looks better. Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/swp.c | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index 229074d4e121..2d1b1c8a535a 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -197,14 +197,14 @@ static int spi_nor_build_sr(struct spi_nor *nor, const u8 *old_sr, u8 *new_sr, */ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) { - u64 min_prot_len; - int ret; + u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor); u8 status_old[1] = {}, status_new[1] = {}; loff_t ofs_old, ofs_new; u64 len_old, len_new; loff_t lock_len; bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB; bool use_top; + int ret; u8 pow; ret = spi_nor_read_sr(nor, nor->bouncebuf); @@ -238,12 +238,10 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) else lock_len = ofs + len; - if (lock_len == nor->params->size) { + if (lock_len == nor->params->size) pow = (nor->flags & SNOR_F_HAS_4BIT_BP) ? GENMASK(3, 0) : GENMASK(2, 0); - } else { - min_prot_len = spi_nor_get_min_prot_length_sr(nor); + else pow = ilog2(lock_len) - ilog2(min_prot_len) + 1; - } ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top); if (ret) @@ -283,14 +281,14 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) */ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) { - u64 min_prot_len; - int ret; + u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor); u8 status_old[1], status_new[1]; loff_t ofs_old, ofs_new; u64 len_old, len_new; loff_t lock_len; bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB; bool use_top; + int ret; u8 pow; ret = spi_nor_read_sr(nor, nor->bouncebuf); @@ -331,14 +329,11 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) else lock_len = ofs; - if (lock_len == 0) { + if (lock_len == 0) pow = 0; /* fully unlocked */ - } else { - min_prot_len = spi_nor_get_min_prot_length_sr(nor); + else pow = ilog2(lock_len) - ilog2(min_prot_len) + 1; - } - ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top); if (ret) return ret; -- cgit v1.2.3 From b7b63475903cc9967ae53c579bc1ad9ed2519080 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:41 +0200 Subject: mtd: spi-nor: Create a local SR cache In order to be able to generate debugfs output without having to actually reach the flash, create a SPI NOR local cache of the status registers. What matters in our case are all the bits related to sector locking. As such, in order to make it clear that this cache is not intended to be used anywhere else, we zero the irrelevant bits. The cache is initialized once during the early init, and then maintained every time the write protection scheme is updated. Suggested-by: Michael Walle Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/core.c | 4 +++- drivers/mtd/spi-nor/core.h | 1 + drivers/mtd/spi-nor/swp.c | 35 +++++++++++++++++++++++++++++++++-- include/linux/mtd/spi-nor.h | 2 ++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c index 2799c21d0b67..1fc71d6e0fec 100644 --- a/drivers/mtd/spi-nor/core.c +++ b/drivers/mtd/spi-nor/core.c @@ -3326,10 +3326,12 @@ static int spi_nor_init(struct spi_nor *nor) * protection bits are volatile. The latter is indicated by * SNOR_F_SWP_IS_VOLATILE. */ + spi_nor_cache_sr_lock_bits(nor, NULL); if (IS_ENABLED(CONFIG_MTD_SPI_NOR_SWP_DISABLE) || (IS_ENABLED(CONFIG_MTD_SPI_NOR_SWP_DISABLE_ON_VOLATILE) && - nor->flags & SNOR_F_SWP_IS_VOLATILE)) + nor->flags & SNOR_F_SWP_IS_VOLATILE)) { spi_nor_try_unlock_all(nor); + } if (nor->addr_nbytes == 4 && nor->read_proto != SNOR_PROTO_8_8_8_DTR && diff --git a/drivers/mtd/spi-nor/core.h b/drivers/mtd/spi-nor/core.h index 0753d0a6f8b8..cd355e94b97e 100644 --- a/drivers/mtd/spi-nor/core.h +++ b/drivers/mtd/spi-nor/core.h @@ -679,6 +679,7 @@ int spi_nor_post_bfpt_fixups(struct spi_nor *nor, void spi_nor_init_default_locking_ops(struct spi_nor *nor); void spi_nor_try_unlock_all(struct spi_nor *nor); +void spi_nor_cache_sr_lock_bits(struct spi_nor *nor, u8 *sr); void spi_nor_set_mtd_locking_ops(struct spi_nor *nor); void spi_nor_set_mtd_otp_ops(struct spi_nor *nor); diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index 2d1b1c8a535a..cd37fec08c0e 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -162,6 +162,25 @@ static int spi_nor_build_sr(struct spi_nor *nor, const u8 *old_sr, u8 *new_sr, return 0; } +/* + * Keep a local cache containing all lock-related bits for debugfs use only. + * This way, debugfs never needs to access the flash directly. + */ +void spi_nor_cache_sr_lock_bits(struct spi_nor *nor, u8 *sr) +{ + u8 bp_mask = spi_nor_get_sr_bp_mask(nor); + u8 tb_mask = spi_nor_get_sr_tb_mask(nor); + + if (!sr) { + if (spi_nor_read_sr(nor, nor->bouncebuf)) + return; + + sr = nor->bouncebuf; + } + + nor->dfs_sr_cache[0] = sr[0] & (bp_mask | tb_mask | SR_SRWD); +} + /* * Lock a region of the flash. Compatible with ST Micro and similar flash. * Supports the block protection bits BP{0,1,2}/BP{0,1,2,3} in the status @@ -271,7 +290,13 @@ static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len) (ofs_old < ofs_new || (ofs_new + len_new) < (ofs_old + len_old))) return -EINVAL; - return spi_nor_write_sr_and_check(nor, status_new[0]); + ret = spi_nor_write_sr_and_check(nor, status_new[0]); + if (ret) + return ret; + + spi_nor_cache_sr_lock_bits(nor, status_new); + + return 0; } /* @@ -353,7 +378,13 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len) (ofs_new < ofs_old || (ofs_old + len_old) < (ofs_new + len_new))) return -EINVAL; - return spi_nor_write_sr_and_check(nor, status_new[0]); + ret = spi_nor_write_sr_and_check(nor, status_new[0]); + if (ret) + return ret; + + spi_nor_cache_sr_lock_bits(nor, status_new); + + return 0; } /* diff --git a/include/linux/mtd/spi-nor.h b/include/linux/mtd/spi-nor.h index 90a0cf583512..9ad77f9e76c2 100644 --- a/include/linux/mtd/spi-nor.h +++ b/include/linux/mtd/spi-nor.h @@ -371,6 +371,7 @@ struct spi_nor_flash_parameter; * @reg_proto: the SPI protocol for read_reg/write_reg/erase operations * @sfdp: the SFDP data of the flash * @debugfs_root: pointer to the debugfs directory + * @dfs_sr_cache: Status Register cached value for debugfs use only * @controller_ops: SPI NOR controller driver specific operations. * @params: [FLASH-SPECIFIC] SPI NOR flash parameters and settings. * The structure includes legacy flash parameters and @@ -409,6 +410,7 @@ struct spi_nor { enum spi_nor_cmd_ext cmd_ext_type; struct sfdp *sfdp; struct dentry *debugfs_root; + u8 dfs_sr_cache[2]; const struct spi_nor_controller_ops *controller_ops; -- cgit v1.2.3 From 47eb01f46e2d0741e058006dafa1fdbbbccf9b15 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:42 +0200 Subject: mtd: spi-nor: debugfs: Add locking support The ioctl output may be counter intuitive in some cases. Asking for a "locked status" over a region that is only partially locked will return "unlocked" whereas in practice maybe the biggest part is actually locked. Knowing what is the real software locking state through debugfs would be very convenient for development/debugging purposes, hence this proposal for adding an extra block at the end of the file: a "locked sectors" array which lists every section, if it is locked or not, showing both the address ranges and the sizes in numbers of "lock sectors" (which on small density devices is typically different than erase blocks). Here is an example of output, what is after the "sector map" is new. $ cat /sys/kernel/debug/spi-nor/spi0.0/params name (null) id ef a0 20 00 00 00 size 64.0 MiB write size 1 page size 256 address nbytes 4 flags HAS_SR_TB | 4B_OPCODES | HAS_4BAIT | HAS_LOCK | HAS_16BIT_SR | HAS_SR_TB_BIT6 | HAS_4BIT_BP | SOFT_RESET | NO_WP opcodes read 0xec dummy cycles 6 erase 0xdc program 0x34 8D extension none protocols read 1S-4S-4S write 1S-1S-4S register 1S-1S-1S erase commands 21 (4.00 KiB) [1] dc (64.0 KiB) [3] c7 (64.0 MiB) sector map region (in hex) | erase mask | overlaid ------------------+------------+--------- 00000000-03ffffff | [ 3] | no locked sectors region (in hex) | status | #sectors ------------------+----------+--------- 00000000-03ffffff | unlocked | 1024 Signed-off-by: Miquel Raynal Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/core.h | 8 ++++++++ drivers/mtd/spi-nor/debugfs.c | 28 ++++++++++++++++++++++++++++ drivers/mtd/spi-nor/swp.c | 13 +++++++++---- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/drivers/mtd/spi-nor/core.h b/drivers/mtd/spi-nor/core.h index cd355e94b97e..ce46771ecdc8 100644 --- a/drivers/mtd/spi-nor/core.h +++ b/drivers/mtd/spi-nor/core.h @@ -287,6 +287,9 @@ struct spi_nor_erase_map { * false otherwise. This feedback may be misleading because users * may get an "unlocked" status even though a subpart of the region * is effectively locked. + * + * If in doubt during development, check-out the debugfs output which tries to + * be more user friendly. */ struct spi_nor_locking_ops { int (*lock)(struct spi_nor *nor, loff_t ofs, u64 len); @@ -678,6 +681,7 @@ int spi_nor_post_bfpt_fixups(struct spi_nor *nor, const struct sfdp_bfpt *bfpt); void spi_nor_init_default_locking_ops(struct spi_nor *nor); +bool spi_nor_has_default_locking_ops(struct spi_nor *nor); void spi_nor_try_unlock_all(struct spi_nor *nor); void spi_nor_cache_sr_lock_bits(struct spi_nor *nor, u8 *sr); void spi_nor_set_mtd_locking_ops(struct spi_nor *nor); @@ -712,6 +716,10 @@ static inline bool spi_nor_needs_sfdp(const struct spi_nor *nor) return !nor->info->size; } +u64 spi_nor_get_min_prot_length_sr(struct spi_nor *nor); +void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_t *ofs, u64 *len); +bool spi_nor_is_locked_sr(struct spi_nor *nor, loff_t ofs, u64 len, const u8 *sr); + #ifdef CONFIG_DEBUG_FS void spi_nor_debugfs_register(struct spi_nor *nor); void spi_nor_debugfs_shutdown(void); diff --git a/drivers/mtd/spi-nor/debugfs.c b/drivers/mtd/spi-nor/debugfs.c index d0191eb9f879..82df8ad4176c 100644 --- a/drivers/mtd/spi-nor/debugfs.c +++ b/drivers/mtd/spi-nor/debugfs.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include #include #include #include @@ -77,11 +78,14 @@ static void spi_nor_print_flags(struct seq_file *s, unsigned long flags, static int spi_nor_params_show(struct seq_file *s, void *data) { struct spi_nor *nor = s->private; + u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor); struct spi_nor_flash_parameter *params = nor->params; struct spi_nor_erase_map *erase_map = ¶ms->erase_map; struct spi_nor_erase_region *region = erase_map->regions; const struct flash_info *info = nor->info; char buf[16], *str; + loff_t lock_start; + u64 lock_length; unsigned int i; seq_printf(s, "name\t\t%s\n", info->name); @@ -159,6 +163,30 @@ static int spi_nor_params_show(struct seq_file *s, void *data) region[i].overlaid ? "yes" : "no"); } + if (!spi_nor_has_default_locking_ops(nor)) + return 0; + + seq_puts(s, "\nlocked sectors\n"); + seq_puts(s, " region (in hex) | status | #sectors\n"); + seq_puts(s, " ------------------+----------+---------\n"); + + spi_nor_get_locked_range_sr(nor, nor->dfs_sr_cache, &lock_start, &lock_length); + if (!lock_length || lock_length == params->size) { + seq_printf(s, " %08llx-%08llx | %s | %llu\n", 0ULL, params->size - 1, + lock_length ? " locked" : "unlocked", + div_u64(params->size, min_prot_len)); + } else if (!lock_start) { + seq_printf(s, " %08llx-%08llx | %s | %llu\n", 0ULL, lock_length - 1, + " locked", div_u64(lock_length, min_prot_len)); + seq_printf(s, " %08llx-%08llx | %s | %llu\n", lock_length, params->size - 1, + "unlocked", div_u64(params->size - lock_length, min_prot_len)); + } else { + seq_printf(s, " %08llx-%08llx | %s | %llu\n", 0ULL, lock_start - 1, + "unlocked", div_u64(lock_start, min_prot_len)); + seq_printf(s, " %08llx-%08llx | %s | %llu\n", lock_start, params->size - 1, + " locked", div_u64(lock_length, min_prot_len)); + } + return 0; } DEFINE_SHOW_ATTRIBUTE(spi_nor_params); diff --git a/drivers/mtd/spi-nor/swp.c b/drivers/mtd/spi-nor/swp.c index cd37fec08c0e..2411d8f1012d 100644 --- a/drivers/mtd/spi-nor/swp.c +++ b/drivers/mtd/spi-nor/swp.c @@ -34,7 +34,7 @@ static u8 spi_nor_get_sr_tb_mask(struct spi_nor *nor) return 0; } -static u64 spi_nor_get_min_prot_length_sr(struct spi_nor *nor) +u64 spi_nor_get_min_prot_length_sr(struct spi_nor *nor) { unsigned int bp_slots, bp_slots_needed; /* @@ -55,8 +55,8 @@ static u64 spi_nor_get_min_prot_length_sr(struct spi_nor *nor) return sector_size; } -static void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_t *ofs, - u64 *len) +void spi_nor_get_locked_range_sr(struct spi_nor *nor, const u8 *sr, loff_t *ofs, + u64 *len) { u64 min_prot_len; u8 bp_mask = spi_nor_get_sr_bp_mask(nor); @@ -114,7 +114,7 @@ static bool spi_nor_check_lock_status_sr(struct spi_nor *nor, loff_t ofs, return (ofs >= lock_offs_max) || (offs_max <= lock_offs); } -static bool spi_nor_is_locked_sr(struct spi_nor *nor, loff_t ofs, u64 len, const u8 *sr) +bool spi_nor_is_locked_sr(struct spi_nor *nor, loff_t ofs, u64 len, const u8 *sr) { return spi_nor_check_lock_status_sr(nor, ofs, len, sr, true); } @@ -416,6 +416,11 @@ void spi_nor_init_default_locking_ops(struct spi_nor *nor) nor->params->locking_ops = &spi_nor_sr_locking_ops; } +bool spi_nor_has_default_locking_ops(struct spi_nor *nor) +{ + return nor->params->locking_ops == &spi_nor_sr_locking_ops; +} + static int spi_nor_lock(struct mtd_info *mtd, loff_t ofs, u64 len) { struct spi_nor *nor = mtd_to_spi_nor(mtd); -- cgit v1.2.3 From f05f0d62c5c6b981315bd10017ec98504165e355 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:43 +0200 Subject: mtd: spi-nor: debugfs: Add a locked sectors map In order to get a very clear view of the sectors being locked, besides the `params` output giving the ranges, we may want to see a proper map of the sectors and for each of them, their status. Depending on the use case, this map may be easier to parse by humans and gives a more acurate feeling of the situation. At least myself, for the few locking-related developments I recently went through, I found it very useful to get a clearer mental model of what was locked/unlocked. Here is an example of output: $ cat /sys/kernel/debug/spi-nor/spi0.0/locked-sectors-map Locked sectors map (x: locked, .: unlocked, unit: 64kiB) 0x00000000 (# 0): ................ ................ ................ ................ 0x00400000 (# 64): ................ ................ ................ ................ 0x00800000 (# 128): ................ ................ ................ ................ 0x00c00000 (# 192): ................ ................ ................ ................ 0x01000000 (# 256): ................ ................ ................ ................ 0x01400000 (# 320): ................ ................ ................ ................ 0x01800000 (# 384): ................ ................ ................ ................ 0x01c00000 (# 448): ................ ................ ................ ................ 0x02000000 (# 512): ................ ................ ................ ................ 0x02400000 (# 576): ................ ................ ................ ................ 0x02800000 (# 640): ................ ................ ................ ................ 0x02c00000 (# 704): ................ ................ ................ ................ 0x03000000 (# 768): ................ ................ ................ ................ 0x03400000 (# 832): ................ ................ ................ ................ 0x03800000 (# 896): ................ ................ ................ ................ 0x03c00000 (# 960): ................ ................ ................ ..............xx The output is wrapped at 64 sectors, spaces every 16 sectors are improving the readability, every line starts by the first sector offset (hex) and number (decimal). Signed-off-by: Miquel Raynal [pratyush@kernel.org: split the debugfs_create_file() into two lines] Signed-off-by: Pratyush Yadav --- drivers/mtd/spi-nor/debugfs.c | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/drivers/mtd/spi-nor/debugfs.c b/drivers/mtd/spi-nor/debugfs.c index 82df8ad4176c..ef710bd4f307 100644 --- a/drivers/mtd/spi-nor/debugfs.c +++ b/drivers/mtd/spi-nor/debugfs.c @@ -191,6 +191,41 @@ static int spi_nor_params_show(struct seq_file *s, void *data) } DEFINE_SHOW_ATTRIBUTE(spi_nor_params); +static int spi_nor_locked_sectors_map_show(struct seq_file *s, void *data) +{ + struct spi_nor *nor = s->private; + struct spi_nor_flash_parameter *params = nor->params; + u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor); + unsigned int sector = 0; + u64 offset = 0; + bool locked; + int i; + + seq_printf(s, "Locked sectors map (x: locked, .: unlocked, unit: %lldkiB)\n", + min_prot_len / 1024); + while (offset < params->size) { + seq_printf(s, " 0x%08llx (#%5d): ", offset, sector); + for (i = 0; i < 64 && offset < params->size; i++) { + locked = spi_nor_is_locked_sr(nor, offset, min_prot_len, + nor->dfs_sr_cache); + if (locked) + seq_puts(s, "x"); + else + seq_puts(s, "."); + + if (((i + 1) % 16) == 0) + seq_puts(s, " "); + + offset += min_prot_len; + sector++; + } + seq_puts(s, "\n"); + } + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(spi_nor_locked_sectors_map); + static void spi_nor_print_read_cmd(struct seq_file *s, u32 cap, struct spi_nor_read_command *cmd) { @@ -276,6 +311,9 @@ void spi_nor_debugfs_register(struct spi_nor *nor) debugfs_create_file("params", 0444, d, nor, &spi_nor_params_fops); debugfs_create_file("capabilities", 0444, d, nor, &spi_nor_capabilities_fops); + if (spi_nor_has_default_locking_ops(nor)) + debugfs_create_file("locked-sectors-map", 0444, d, nor, + &spi_nor_locked_sectors_map_fops); } void spi_nor_debugfs_shutdown(void) -- cgit v1.2.3 From 6000eab4515a2a381557a48c35e248a3418f2bc6 Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:44 +0200 Subject: mtd: spi-nor: Add steps for testing locking support As recently raised on the mailing list, it may be useful to propose a list of steps to go through in order to prove the devices have been described correctly, especially since all the block protection information is not stored in any kind of table and is instead filled manually by developers. Use the debugfs output to ease the comparison between expectations and reality. [rdunlap@infradead.org: fix build warning] Link: https://lore.kernel.org/linux-mtd/20260526172341.773398-1-rdunlap@infradead.org/ Signed-off-by: Miquel Raynal Signed-off-by: Randy Dunlap Signed-off-by: Pratyush Yadav --- Documentation/driver-api/mtd/spi-nor.rst | 133 +++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/Documentation/driver-api/mtd/spi-nor.rst b/Documentation/driver-api/mtd/spi-nor.rst index 148fa4288760..f817a9193731 100644 --- a/Documentation/driver-api/mtd/spi-nor.rst +++ b/Documentation/driver-api/mtd/spi-nor.rst @@ -203,3 +203,136 @@ section, after the ``---`` marker. mtd.writesize = 1 mtd.oobsize = 0 regions = 0 + +5) If your flash supports locking, please go through the following test + procedure to make sure it correctly behaves. The below example + expects the typical situation where eraseblocks and lock sectors have + the same size. In case you enabled MTD_SPI_NOR_USE_4K_SECTORS, you + must adapt `bs` accordingly. + + Warning: These tests may hard lock your device! Make sure: + + - The device is not hard locked already (#WP strapped to low and + SR_SRWD bit set) + - If you have a WPn pin, you may want to set `no-wp` in your DT for + the time of the test, to only make use of software protection. + Otherwise, clearing the locking state depends on the WPn + signal and if it is tied to low, the flash will be permanently + locked. + + Test full chip locking and make sure expectations, the MEMISLOCKED + ioctl output, the debugfs output and experimental results are all + aligned:: + + root@1:~# alias show_sectors='grep -A4 "locked sectors" /sys/kernel/debug/spi-nor/spi0.0/params' + root@1:~# flash_lock -u /dev/mtd0 + root@1:~# flash_lock -i /dev/mtd0 + Device: /dev/mtd0 + Start: 0 + Len: 0x4000000 + Lock status: unlocked + Return code: 0 + root@1:~# mtd_debug erase /dev/mtd0 0 2097152 + Erased 2097152 bytes from address 0x00000000 in flash + root@1:~# mtd_debug write /dev/mtd0 0 2097152 spi_test + Copied 2097152 bytes from spi_test to address 0x00000000 in flash + root@1:~# mtd_debug read /dev/mtd0 0 2097152 spi_read + Copied 2097152 bytes from address 0x00000000 in flash to spi_read + root@1:~# sha256sum spi* + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_read + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_test + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-03ffffff | unlocked | 1024 + + root@1:~# flash_lock -l /dev/mtd0 + root@1:~# flash_lock -i /dev/mtd0 + Device: /dev/mtd0 + Start: 0 + Len: 0x4000000 + Lock status: locked + Return code: 1 + root@1:~# mtd_debug erase /dev/mtd0 0 2097152 + Erased 2097152 bytes from address 0x00000000 in flash + root@1:~# mtd_debug read /dev/mtd0 0 2097152 spi_read + Copied 2097152 bytes from address 0x00000000 in flash to spi_read + root@1:~# sha256sum spi* + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_read + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_test + root@1:~# dd if=/dev/urandom of=./spi_test2 bs=1M count=2 + 2+0 records in + 2+0 records out + root@1:~# mtd_debug write /dev/mtd0 0 2097152 spi_test2 + Copied 2097152 bytes from spi_test2 to address 0x00000000 in flash + root@1:~# mtd_debug read /dev/mtd0 0 2097152 spi_read2 + Copied 2097152 bytes from address 0x00000000 in flash to spi_read2 + root@1:~# sha256sum spi* + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_read + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_read2 + c444216a6ba2a4a66cccd60a0dd062bce4b865dd52b200ef5e21838c4b899ac8 spi_test + bea9334df51c620440f86751cba0799214a016329f1736f9456d40cf40efdc88 spi_test2 + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-03ffffff | locked | 1024 + root@1:~# flash_lock -u /dev/mtd0 + + Once we trust the debugfs output we can use it to test various + situations. Check top locking/unlocking (end of the device):: + + root@1:~# size=$(cat /sys/class/mtd/mtd0/size) + root@1:~# bs=$(cat /sys/class/mtd/mtd0/erasesize) + root@1:~# nsectors=$(grep unlocked /sys/kernel/debug/spi-nor/spi0.0/params | sed -e 's/.*unlocked | //') + root@1:~# ss=$(($size / $nsectors)) + root@1:~# bps=$(($ss / $bs)) + + root@1:~# flash_lock -u /dev/mtd0 + root@1:~# flash_lock -l /dev/mtd0 $(($size - (2 * $ss))) $((2 * $bps)) # last two + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-03fdffff | unlocked | 1022 + 03fe0000-03ffffff | locked | 2 + root@1:~# flash_lock -u /dev/mtd0 $(($size - (2 * $ss))) $((1 * $bps)) # last one + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-03feffff | unlocked | 1023 + 03ff0000-03ffffff | locked | 1 + + If the flash features 4 block protection bits (BP), we can protect + more than 4MB (typically 128 64kiB-blocks or more), with a finer + grain than locking the entire device:: + + root@1:~# flash_lock -u /dev/mtd0 + root@1:~# flash_lock -l /dev/mtd0 $(($size - (2**7 * $ss))) $((2**7 * $bps)) + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-037fffff | unlocked | 896 + 03800000-03ffffff | locked | 128 + + If the flash features a Top/Bottom (TB) bit, we can protect the + beginning of the flash:: + + root@1:~# flash_lock -u /dev/mtd0 + root@1:~# flash_lock -l /dev/mtd0 0 $((2 * $bps)) # first two + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-0001ffff | locked | 2 + 00020000-03ffffff | unlocked | 1022 + root@1:~# flash_lock -u /dev/mtd0 $ss $((1 * $bps)) # first one + root@1:~# show_sectors + software locked sectors + region (in hex) | status | #sectors + ------------------+----------+--------- + 00000000-0000ffff | locked | 1 + 00010000-03ffffff | unlocked | 1023 -- cgit v1.2.3 From e3fb31d8847fef2ce37c5f60bc77d3f731a2419b Mon Sep 17 00:00:00 2001 From: Miquel Raynal Date: Tue, 26 May 2026 16:56:45 +0200 Subject: mtd: spi-nor: swp: Add support for the complement feature The current locking implementation allows to select a power of two number of blocks, which is going to