Merge tag 'spi-nor/for-7.2' into mtd/next

SPI NOR changes for 7.2

Notable changes:

- Big set of cleanups and improvements to the locking support. This
  series contains some cleanups and bug fixes for code and documentation
  around write protection. Then support is added for complement locking,
  which allows finer grained configuration of what is considered locked
  and unlocked. Then complement locking is enabled on a bunch of Winbond
  W25 flashes.

- Fix die erase support on Spansion flashes. Die erase is only supported
  on multi-die flashes, but the die erase opcode was set for all. When
  the opcode is set, it overrides the default chip erase opcode which
  should be used for single-die flashes. Only set the opcode on
  multi-die flashes. Also, the opcode was not set on multi-die s28hx-t
  flashes. Set it so they can use die-erase correctly.
This commit is contained in:
Miquel Raynal
2026-06-21 17:05:31 +02:00
9 changed files with 671 additions and 91 deletions

View File

@@ -203,3 +203,173 @@ 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
If the flash features a Complement (CMP) bit, we can protect with
more granularity above half of the capacity. Let's lock all but one
block, then unlock one more block::
root@1:~# all_but_one=$((($size / $bs) - ($ss / $bs)))
root@1:~# flash_lock -u /dev/mtd0
root@1:~# flash_lock -l /dev/mtd0 $ss $all_but_one # all but the first
root@1:~# show_sectors
software locked sectors
region (in hex) | status | #sectors
------------------+----------+---------
00000000-0000ffff | unlocked | 1
00010000-03ffffff | locked | 1023
root@1:~# flash_lock -u /dev/mtd0 $ss $(($ss / $bs)) # all but the two first
root@1:~# show_sectors
software locked sectors
region (in hex) | status | #sectors
------------------+----------+---------
00000000-0001ffff | unlocked | 2
00020000-03ffffff | locked | 1022
root@1:~# flash_lock -u /dev/mtd0
root@1:~# flash_lock -l /dev/mtd0 0 $all_but_one # same from the other side
root@1:~# show_sectors
software locked sectors
region (in hex) | status | #sectors
------------------+----------+---------
00000000-03feffff | locked | 1023
03ff0000-03ffffff | unlocked | 1
root@1:~# flash_lock -u /dev/mtd0 $(($size - (2 * $ss))) $(($ss / $bs)) # all but two
root@1:~# show_sectors
software locked sectors
region (in hex) | status | #sectors
------------------+----------+---------
00000000-03fdffff | locked | 1022
03fe0000-03ffffff | unlocked | 2

View File

@@ -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

View File

@@ -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
@@ -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.
@@ -2909,6 +2974,9 @@ static void spi_nor_init_flags(struct spi_nor *nor)
nor->flags |= SNOR_F_HAS_SR_BP3_BIT6;
}
if (flags & SPI_NOR_HAS_CMP)
nor->flags |= SNOR_F_HAS_SR2_CMP_BIT6;
if (flags & SPI_NOR_RWW && nor->params->n_banks > 1 &&
!nor->controller_ops)
nor->flags |= SNOR_F_RWW;
@@ -3261,10 +3329,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 &&

View File

@@ -141,6 +141,7 @@ enum spi_nor_option_flags {
SNOR_F_ECC = BIT(15),
SNOR_F_NO_WP = BIT(16),
SNOR_F_SWAP16 = BIT(17),
SNOR_F_HAS_SR2_CMP_BIT6 = BIT(18),
};
struct spi_nor_read_command {
@@ -279,9 +280,17 @@ 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.
*
* 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);
@@ -483,6 +492,8 @@ struct spi_nor_id {
* SPI_NOR_NO_ERASE: no erase command needed.
* SPI_NOR_QUAD_PP: flash supports Quad Input Page Program.
* SPI_NOR_RWW: flash supports reads while write.
* SPI_NOR_HAS_CMP: flash SR2 has complement (CMP) protect bit. Must
* be used with SPI_NOR_HAS_LOCK.
*
* @no_sfdp_flags: flags that indicate support that can be discovered via SFDP.
* Used when SFDP tables are not defined in the flash. These
@@ -531,6 +542,7 @@ struct flash_info {
#define SPI_NOR_NO_ERASE BIT(6)
#define SPI_NOR_QUAD_PP BIT(8)
#define SPI_NOR_RWW BIT(9)
#define SPI_NOR_HAS_CMP BIT(10)
u8 no_sfdp_flags;
#define SPI_NOR_SKIP_SFDP BIT(0)
@@ -632,6 +644,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);
@@ -672,7 +685,9 @@ 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);
void spi_nor_set_mtd_otp_ops(struct spi_nor *nor);
@@ -705,6 +720,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);

View File

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/debugfs.h>
#include <linux/math64.h>
#include <linux/mtd/spi-nor.h>
#include <linux/spi/spi.h>
#include <linux/spi/spi-mem.h>
@@ -28,6 +29,8 @@ static const char *const snor_f_names[] = {
SNOR_F_NAME(RWW),
SNOR_F_NAME(ECC),
SNOR_F_NAME(NO_WP),
SNOR_F_NAME(SWAP16),
SNOR_F_NAME(HAS_SR2_CMP_BIT6),
};
#undef SNOR_F_NAME
@@ -76,11 +79,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 = &params->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);
@@ -138,12 +144,12 @@ 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");
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;
@@ -158,10 +164,69 @@ 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);
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)
{
@@ -247,6 +312,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)

View File

@@ -674,7 +674,9 @@ static int s25hx_t_late_init(struct spi_nor *nor)
params->ready = cypress_nor_sr_ready_and_clear;
cypress_nor_ecc_init(nor);
params->die_erase_opcode = SPINOR_OP_CYPRESS_DIE_ERASE;
if (params->n_dice > 1)
params->die_erase_opcode = SPINOR_OP_CYPRESS_DIE_ERASE;
return 0;
}
@@ -760,6 +762,9 @@ static int s28hx_t_late_init(struct spi_nor *nor)
params->ready = cypress_nor_sr_ready_and_clear;
cypress_nor_ecc_init(nor);
if (params->n_dice > 1)
params->die_erase_opcode = SPINOR_OP_CYPRESS_DIE_ERASE;
return 0;
}

View File

@@ -34,7 +34,16 @@ 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)
static u8 spi_nor_get_sr_cmp_mask(struct spi_nor *nor)
{
if (!(nor->flags & SNOR_F_NO_READ_CR) &&
nor->flags & SNOR_F_HAS_SR2_CMP_BIT6)
return SR2_CMP_BIT6;
else
return 0;
}
u64 spi_nor_get_min_prot_length_sr(struct spi_nor *nor)
{
unsigned int bp_slots, bp_slots_needed;
/*
@@ -55,13 +64,16 @@ 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,
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 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 & mask;
u8 cmp_mask = spi_nor_get_sr_cmp_mask(nor);
u8 bp, val = sr[0] & bp_mask;
bool tb = (nor->flags & SNOR_F_HAS_SR_TB) ? sr[0] & tb_mask : 0;
bool cmp = sr[1] & cmp_mask;
if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6 && val & SR_BP3_BIT6)
val = (val & ~SR_BP3_BIT6) | SR_BP3;
@@ -69,22 +81,37 @@ static void spi_nor_get_locked_range_sr(struct spi_nor *nor, u8 sr, loff_t *ofs,
bp = val >> SR_BP_SHIFT;
if (!bp) {
/* No protection */
*ofs = 0;
*len = 0;
if (!cmp) {
/* No protection */
*ofs = 0;
*len = 0;
} else {
/* Full protection */
*ofs = 0;
*len = nor->params->size;
}
return;
}
min_prot_len = spi_nor_get_min_prot_length_sr(nor);
*len = min_prot_len << (bp - 1);
if (*len > nor->params->size)
*len = nor->params->size;
if (nor->flags & SNOR_F_HAS_SR_TB && sr & tb_mask)
*ofs = 0;
else
*ofs = nor->params->size - *len;
if (cmp)
*len = nor->params->size - *len;
if (!cmp) {
if (tb)
*ofs = 0;
else
*ofs = nor->params->size - *len;
} else {
if (tb)
*ofs = nor->params->size - *len;
else
*ofs = 0;
}
}
/*
@@ -92,7 +119,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,27 +140,102 @@ 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)
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);
}
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, bool cmp)
{
u8 bp_mask = spi_nor_get_sr_bp_mask(nor);
u8 tb_mask = spi_nor_get_sr_tb_mask(nor);
u8 cmp_mask = spi_nor_get_sr_cmp_mask(nor);
int ret;
new_sr[0] = old_sr[0] & ~bp_mask & ~tb_mask;
new_sr[1] = old_sr[1] & ~cmp_mask;
/* Build BP field */
ret = spi_nor_sr_set_bp_mask(nor, &new_sr[0], pow);
if (ret)
return ret;
/* Build TB field */
if ((!cmp && !use_top) || (cmp && use_top))
new_sr[0] |= tb_mask;
/* Build CMP field */
if (cmp)
new_sr[1] |= cmp_mask;
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);
u8 cmp_mask = spi_nor_get_sr_cmp_mask(nor);
u8 sr_cr[2] = {};
if (!sr) {
if (spi_nor_read_sr(nor, nor->bouncebuf))
return;
sr_cr[0] = nor->bouncebuf[0];
if (!(nor->flags & SNOR_F_NO_READ_CR)) {
if (spi_nor_read_cr(nor, nor->bouncebuf))
return;
}
sr_cr[1] = nor->bouncebuf[0];
sr = sr_cr;
}
nor->dfs_sr_cache[0] = sr[0] & (bp_mask | tb_mask | SR_SRWD);
nor->dfs_sr_cache[1] = sr[1] & cmp_mask;
}
/*
* 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
* register
* (SR). Does not support these features found in newer SR bitfields:
* - SEC: sector/block protect - only handle SEC=0 (block protect)
* - CMP: complement protect - only support CMP=0 (range is not complemented)
*
* Support for the following is provided conditionally for some flash:
* - TB: top/bottom protect
* - CMP: complement protect (BP and TP describe the unlocked part, while
* the reminder is locked)
*
* Sample table portion for 8MB flash (Winbond w25q64fw):
*
@@ -159,20 +261,31 @@ 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;
u8 mask = spi_nor_get_sr_bp_mask(nor);
u8 tb_mask = spi_nor_get_sr_tb_mask(nor);
u8 pow, val;
u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor);
u8 status_old[2] = {}, status_new[2] = {}, status_new_cmp[2] = {};
u8 *best_status_new = status_new;
loff_t ofs_old, ofs_new, ofs_new_cmp;
u64 len_old, len_new, len_new_cmp;
loff_t lock_len;
bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB;
bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB,
can_be_cmp = spi_nor_get_sr_cmp_mask(nor);
bool use_top;
int ret;
u8 pow;
ret = spi_nor_read_sr(nor, nor->bouncebuf);
if (ret)
return ret;
status_old = nor->bouncebuf[0];
status_old[0] = nor->bouncebuf[0];
if (!(nor->flags & SNOR_F_NO_READ_CR)) {
ret = spi_nor_read_cr(nor, nor->bouncebuf);
if (ret)
return ret;
status_old[1] = 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))
@@ -199,46 +312,82 @@ 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) {
val = mask;
} else {
min_prot_len = spi_nor_get_min_prot_length_sr(nor);
if (lock_len == nor->params->size)
pow = (nor->flags & SNOR_F_HAS_4BIT_BP) ? GENMASK(3, 0) : GENMASK(2, 0);
else
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;
ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top, false);
if (ret)
return ret;
if (val & ~mask)
return -EINVAL;
/*
* In case the region asked is not fully met, maybe we can try with the
* complement feature
*/
spi_nor_get_locked_range_sr(nor, status_new, &ofs_new, &len_new);
if (can_be_cmp && len_new != lock_len) {
pow = ilog2(nor->params->size - lock_len) - ilog2(min_prot_len) + 1;
ret = spi_nor_build_sr(nor, status_old, status_new_cmp, pow, use_top, true);
if (ret)
return ret;
/* Don't "lock" with no region! */
if (!(val & mask))
return -EINVAL;
/*
* ilog2() "floors" the result, which means in some cases we may have to
* manually reduce the scope when the complement feature is used.
* The uAPI is to never lock more than what is requested, but less is accepted.
* Make sure we are not covering a too wide range, reduce it otherwise.
*/
spi_nor_get_locked_range_sr(nor, status_new_cmp, &ofs_new_cmp, &len_new_cmp);
if (len_new_cmp > lock_len) {
pow++;
ret = spi_nor_build_sr(nor, status_old, status_new_cmp, pow, use_top, true);
if (ret)
return ret;
}
/* Pick the CMP configuration if we cover a closer range */
spi_nor_get_locked_range_sr(nor, status_new, &ofs_new, &len_new);
spi_nor_get_locked_range_sr(nor, status_new_cmp, &ofs_new_cmp, &len_new_cmp);
if (len_new_cmp <= lock_len &&
(lock_len - len_new_cmp) < (lock_len - len_new))
best_status_new = status_new_cmp;
}
status_new = (status_old & ~mask & ~tb_mask) | val;
/*
* Disallow further writes if WP# pin is neither left floating nor
* wrongly tied to GND (that includes internal pull-downs).
* WP# pin hard strapped to GND can be a valid use case.
*/
if (!(nor->flags & SNOR_F_NO_WP))
status_new |= SR_SRWD;
best_status_new[0] |= SR_SRWD;
if (!use_top)
status_new |= tb_mask;
spi_nor_get_locked_range_sr(nor, status_old, &ofs_old, &len_old);
spi_nor_get_locked_range_sr(nor, best_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 == status_old)
if (best_status_new[0] == status_old[0] && best_status_new[1] == status_old[1])
return 0;
/* Only modify protection if it will not unlock other areas */
if ((status_new & mask) < (status_old & 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);
if (nor->flags & SNOR_F_NO_READ_CR)
ret = spi_nor_write_sr_and_check(nor, best_status_new[0]);
else
ret = spi_nor_write_sr_cr_and_check(nor, best_status_new);
if (ret)
return ret;
spi_nor_cache_sr_lock_bits(nor, best_status_new);
return 0;
}
/*
@@ -248,20 +397,31 @@ 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;
u8 mask = spi_nor_get_sr_bp_mask(nor);
u8 tb_mask = spi_nor_get_sr_tb_mask(nor);
u8 pow, val;
u64 min_prot_len = spi_nor_get_min_prot_length_sr(nor);
u8 status_old[2] = {}, status_new[2] = {}, status_new_cmp[2] = {};
u8 *best_status_new = status_new;
loff_t ofs_old, ofs_new, ofs_new_cmp;
u64 len_old, len_new, len_new_cmp;
loff_t lock_len;
bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB;
bool can_be_top = true, can_be_bottom = nor->flags & SNOR_F_HAS_SR_TB,
can_be_cmp = spi_nor_get_sr_cmp_mask(nor);
bool use_top;
int ret;
u8 pow;
ret = spi_nor_read_sr(nor, nor->bouncebuf);
if (ret)
return ret;
status_old = nor->bouncebuf[0];
status_old[0] = nor->bouncebuf[0];
if (!(nor->flags & SNOR_F_NO_READ_CR)) {
ret = spi_nor_read_cr(nor, nor->bouncebuf);
if (ret)
return ret;
status_old[1] = 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))
@@ -282,45 +442,86 @@ 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;
if (lock_len == 0) {
val = 0; /* fully unlocked */
} else {
min_prot_len = spi_nor_get_min_prot_length_sr(nor);
if (lock_len == 0)
pow = 0; /* fully unlocked */
else
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;
ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top, false);
if (ret)
return ret;
/* Some power-of-two sizes are not supported */
if (val & ~mask)
return -EINVAL;
/*
* In case the region asked is not fully met, maybe we can try with the
* complement feature
*/
spi_nor_get_locked_range_sr(nor, status_new, &ofs_new, &len_new);
if (can_be_cmp && len_new != lock_len) {
pow = ilog2(nor->params->size - lock_len) - ilog2(min_prot_len) + 1;
ret = spi_nor_build_sr(nor, status_old, status_new_cmp, pow, use_top, true);
if (ret)
return ret;
/*
* ilog2() "floors" the result, which means in some cases we may have to
* manually reduce the scope when the complement feature is used.
* The uAPI is to never unlock more than what is requested, but less is accepted.
* Make sure we are not covering a too small range, increase it otherwise.
*/
spi_nor_get_locked_range_sr(nor, status_new_cmp, &ofs_new_cmp, &len_new_cmp);
if (len_new_cmp < lock_len) {
pow--;
ret = spi_nor_build_sr(nor, status_old, status_new_cmp, pow, use_top, true);
if (ret)
return ret;
}
/* Pick the CMP configuration if we cover a closer range */
spi_nor_get_locked_range_sr(nor, status_new, &ofs_new, &len_new);
spi_nor_get_locked_range_sr(nor, status_new_cmp, &ofs_new_cmp, &len_new_cmp);
if (len_new_cmp <= lock_len &&
(lock_len - len_new_cmp) < (lock_len - len_new))
best_status_new = status_new_cmp;
}
status_new = (status_old & ~mask & ~tb_mask) | val;
/* Don't protect status register if we're fully unlocked */
if (lock_len == 0)
status_new &= ~SR_SRWD;
if (!use_top)
status_new |= tb_mask;
best_status_new[0] &= ~SR_SRWD;
/* Don't bother if they're the same */
if (status_new == status_old)
if (best_status_new[0] == status_old[0] && best_status_new[1] == status_old[1])
return 0;
/* Only modify protection if it will not lock other areas */
if ((status_new & mask) > (status_old & mask))
spi_nor_get_locked_range_sr(nor, status_old, &ofs_old, &len_old);
spi_nor_get_locked_range_sr(nor, best_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);
if (nor->flags & SNOR_F_NO_READ_CR)
ret = spi_nor_write_sr_and_check(nor, best_status_new[0]);
else
ret = spi_nor_write_sr_cr_and_check(nor, best_status_new);
if (ret)
return ret;
spi_nor_cache_sr_lock_bits(nor, best_status_new);
return 0;
}
/*
@@ -332,13 +533,24 @@ static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len)
*/
static int spi_nor_sr_is_locked(struct spi_nor *nor, loff_t ofs, u64 len)
{
u8 sr_cr[2] = {};
int ret;
ret = spi_nor_read_sr(nor, nor->bouncebuf);
if (ret)
return ret;
return spi_nor_is_locked_sr(nor, ofs, len, nor->bouncebuf[0]);
sr_cr[0] = nor->bouncebuf[0];
if (!(nor->flags & SNOR_F_NO_READ_CR)) {
ret = spi_nor_read_cr(nor, nor->bouncebuf);
if (ret)
return ret;
sr_cr[1] = nor->bouncebuf[0];
}
return spi_nor_is_locked_sr(nor, ofs, len, sr_cr);
}
static const struct spi_nor_locking_ops spi_nor_sr_locking_ops = {
@@ -352,6 +564,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);

View File

@@ -73,6 +73,26 @@ static const struct spi_nor_fixups w25q256_fixups = {
.post_bfpt = w25q256_post_bfpt_fixups,
};
static int
winbond_rdcr_post_bfpt_fixup(struct spi_nor *nor,
const struct sfdp_parameter_header *bfpt_header,
const struct sfdp_bfpt *bfpt)
{
/*
* W25H02NW, unlike its W25H512NW nor W25H01NW cousins, improperly sets
* the QE BFPT configuration bits, indicating a non readable CR. This is
* both incorrect and impractical, as the chip features a CMP bit for its
* locking scheme that lays in the Control Register, and needs to be read.
*/
nor->flags &= ~SNOR_F_NO_READ_CR;
return 0;
}
static const struct spi_nor_fixups winbond_rdcr_fixup = {
.post_bfpt = winbond_rdcr_post_bfpt_fixup,
};
/**
* winbond_nor_select_die() - Set active die.
* @nor: pointer to 'struct spi_nor'.
@@ -348,27 +368,36 @@ static const struct flash_info winbond_nor_parts[] = {
}, {
/* W25Q01NWxxIQ */
.id = SNOR_ID(0xef, 0x60, 0x21),
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 | SPI_NOR_4BIT_BP,
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 |
SPI_NOR_4BIT_BP | SPI_NOR_HAS_CMP,
.fixups = &winbond_rdcr_fixup,
}, {
/* W25Q01NWxxIM */
.id = SNOR_ID(0xef, 0x80, 0x21),
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 | SPI_NOR_4BIT_BP,
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 |
SPI_NOR_4BIT_BP | SPI_NOR_HAS_CMP,
}, {
/* W25Q02NWxxIM */
.id = SNOR_ID(0xef, 0x80, 0x22),
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 | SPI_NOR_4BIT_BP,
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 |
SPI_NOR_4BIT_BP | SPI_NOR_HAS_CMP,
.fixups = &winbond_rdcr_fixup,
}, {
/* W25H512NWxxAM */
.id = SNOR_ID(0xef, 0xa0, 0x20),
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 | SPI_NOR_4BIT_BP,
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 |
SPI_NOR_4BIT_BP | SPI_NOR_HAS_CMP,
}, {
/* W25H01NWxxAM */
.id = SNOR_ID(0xef, 0xa0, 0x21),
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 | SPI_NOR_4BIT_BP,
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 |
SPI_NOR_4BIT_BP | SPI_NOR_HAS_CMP,
}, {
/* W25H02NWxxAM */
.id = SNOR_ID(0xef, 0xa0, 0x22),
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 | SPI_NOR_4BIT_BP,
.flags = SPI_NOR_HAS_LOCK | SPI_NOR_HAS_TB | SPI_NOR_TB_SR_BIT6 |
SPI_NOR_4BIT_BP | SPI_NOR_HAS_CMP,
.fixups = &winbond_rdcr_fixup,
},
};

View File

@@ -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) */
@@ -125,6 +125,7 @@
#define SR2_LB1 BIT(3) /* Security Register Lock Bit 1 */
#define SR2_LB2 BIT(4) /* Security Register Lock Bit 2 */
#define SR2_LB3 BIT(5) /* Security Register Lock Bit 3 */
#define SR2_CMP_BIT6 BIT(6)
#define SR2_QUAD_EN_BIT7 BIT(7)
/* Supported SPI protocols */
@@ -371,6 +372,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 +411,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;