#include <linux/math64.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/spi-nor.h>
#include "core.h"
static u8 spi_nor_get_sr_bp_mask(struct spi_nor *nor)
{
u8 mask = SR_BP2 | SR_BP1 | SR_BP0;
if (nor->flags & SNOR_F_HAS_SR_BP3_BIT6)
return mask | SR_BP3_BIT6;
if (nor->flags & SNOR_F_HAS_4BIT_BP)
return mask | SR_BP3;
return mask;
}
static u8 spi_nor_get_sr_tb_mask(struct spi_nor *nor)
{
if (nor->flags & SNOR_F_HAS_SR_TB_BIT6)
return SR_TB_BIT6;
else if (nor->flags & SNOR_F_HAS_SR_TB)
return SR_TB_BIT5;
else
return 0;
}
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;
unsigned int sector_size = nor->info->sector_size ?: SPI_NOR_DEFAULT_SECTOR_SIZE;
u64 n_sectors = div_u64(nor->params->size, sector_size);
u8 mask = spi_nor_get_sr_bp_mask(nor);
bp_slots = (1 << hweight8(mask)) - 2;
bp_slots_needed = ilog2(n_sectors);
if (bp_slots_needed > bp_slots)
return sector_size << (bp_slots_needed - bp_slots);
else
return sector_size;
}
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);
u8 tb_mask = spi_nor_get_sr_tb_mask(nor);
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;
bp = val >> SR_BP_SHIFT;
if (!bp) {
if (!cmp) {
*ofs = 0;
*len = 0;
} else {
*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 (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;
}
}
static bool spi_nor_check_lock_status_sr(struct spi_nor *nor, loff_t ofs,
u64 len, const u8 *sr, bool locked)
{
loff_t lock_offs, lock_offs_max, offs_max;
u64 lock_len;
if (!len)
return true;
spi_nor_get_locked_range_sr(nor, sr, &lock_offs, &lock_len);
lock_offs_max = lock_offs + lock_len;
offs_max = ofs + len;
if (locked)
return (offs_max <= lock_offs_max) && (ofs >= lock_offs);
else
return (ofs >= lock_offs_max) || (offs_max <= lock_offs);
}
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,
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;
ret = spi_nor_sr_set_bp_mask(nor, &new_sr[0], pow);
if (ret)
return ret;
if ((!cmp && !use_top) || (cmp && use_top))
new_sr[0] |= tb_mask;
if (cmp)
new_sr[1] |= cmp_mask;
return 0;
}
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;
}
static int spi_nor_sr_lock(struct spi_nor *nor, loff_t ofs, u64 len)
{
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,
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[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 (spi_nor_is_locked_sr(nor, ofs, len, status_old))
return 0;
if (!spi_nor_is_locked_sr(nor, 0, ofs, status_old))
can_be_bottom = false;
if (!spi_nor_is_locked_sr(nor, ofs + len, nor->params->size - (ofs + len),
status_old))
can_be_top = false;
if (!can_be_bottom && !can_be_top)
return -EINVAL;
use_top = can_be_top;
if (use_top)
lock_len = nor->params->size - ofs;
else
lock_len = ofs + len;
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;
ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top, false);
if (ret)
return ret;
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;
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;
}
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;
}
if (!(nor->flags & SNOR_F_NO_WP))
best_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, best_status_new, &ofs_new, &len_new);
if (!len_new)
return -EINVAL;
if (best_status_new[0] == status_old[0] && best_status_new[1] == status_old[1])
return 0;
if (len_old &&
(ofs_old < ofs_new || (ofs_new + len_new) < (ofs_old + len_old)))
return -EINVAL;
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;
}
static int spi_nor_sr_unlock(struct spi_nor *nor, loff_t ofs, u64 len)
{
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,
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[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 (spi_nor_is_unlocked_sr(nor, ofs, len, status_old))
return 0;
if (!spi_nor_is_unlocked_sr(nor, 0, ofs, status_old))
can_be_top = false;
if (!spi_nor_is_unlocked_sr(nor, ofs + len, nor->params->size - (ofs + len),
status_old))
can_be_bottom = false;
if (!can_be_bottom && !can_be_top)
return -EINVAL;
use_top = can_be_top;
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)
pow = 0;
else
pow = ilog2(lock_len) - ilog2(min_prot_len) + 1;
ret = spi_nor_build_sr(nor, status_old, status_new, pow, use_top, false);
if (ret)
return ret;
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;
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;
}
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;
}
if (lock_len == 0)
best_status_new[0] &= ~SR_SRWD;
if (best_status_new[0] == status_old[0] && best_status_new[1] == status_old[1])
return 0;
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;
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;
}
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;
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 = {
.lock = spi_nor_sr_lock,
.unlock = spi_nor_sr_unlock,
.is_locked = spi_nor_sr_is_locked,
};
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);
int ret;
ret = spi_nor_prep_and_lock(nor);
if (ret)
return ret;
ret = nor->params->locking_ops->lock(nor, ofs, len);
spi_nor_unlock_and_unprep(nor);
return ret;
}
static int spi_nor_unlock(struct mtd_info *mtd, loff_t ofs, u64 len)
{
struct spi_nor *nor = mtd_to_spi_nor(mtd);
int ret;
ret = spi_nor_prep_and_lock(nor);
if (ret)
return ret;
ret = nor->params->locking_ops->unlock(nor, ofs, len);
spi_nor_unlock_and_unprep(nor);
return ret;
}
static int spi_nor_is_locked(struct mtd_info *mtd, loff_t ofs, u64 len)
{
struct spi_nor *nor = mtd_to_spi_nor(mtd);
int ret;
ret = spi_nor_prep_and_lock(nor);
if (ret)
return ret;
ret = nor->params->locking_ops->is_locked(nor, ofs, len);
spi_nor_unlock_and_unprep(nor);
return ret;
}
void spi_nor_try_unlock_all(struct spi_nor *nor)
{
int ret;
if (!(nor->flags & SNOR_F_HAS_LOCK))
return;
dev_dbg(nor->dev, "Unprotecting entire flash array\n");
ret = spi_nor_unlock(&nor->mtd, 0, nor->params->size);
if (ret)
dev_dbg(nor->dev, "Failed to unlock the entire flash memory array\n");
}
void spi_nor_set_mtd_locking_ops(struct spi_nor *nor)
{
struct mtd_info *mtd = &nor->mtd;
if (!nor->params->locking_ops)
return;
mtd->_lock = spi_nor_lock;
mtd->_unlock = spi_nor_unlock;
mtd->_is_locked = spi_nor_is_locked;
}