mirror of https://gitee.com/openkylin/linux.git
378 lines
9.1 KiB
C
378 lines
9.1 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* OTP support for SPI NOR flashes
|
|
*
|
|
* Copyright (C) 2021 Michael Walle <michael@walle.cc>
|
|
*/
|
|
|
|
#include <linux/log2.h>
|
|
#include <linux/mtd/mtd.h>
|
|
#include <linux/mtd/spi-nor.h>
|
|
|
|
#include "core.h"
|
|
|
|
#define spi_nor_otp_region_len(nor) ((nor)->params->otp.org->len)
|
|
#define spi_nor_otp_n_regions(nor) ((nor)->params->otp.org->n_regions)
|
|
|
|
/**
|
|
* spi_nor_otp_read_secr() - read OTP data
|
|
* @nor: pointer to 'struct spi_nor'
|
|
* @from: offset to read from
|
|
* @len: number of bytes to read
|
|
* @buf: pointer to dst buffer
|
|
*
|
|
* Read OTP data from one region by using the SPINOR_OP_RSECR commands. This
|
|
* method is used on GigaDevice and Winbond flashes.
|
|
*
|
|
* Return: number of bytes read successfully, -errno otherwise
|
|
*/
|
|
int spi_nor_otp_read_secr(struct spi_nor *nor, loff_t addr, size_t len, u8 *buf)
|
|
{
|
|
u8 addr_width, read_opcode, read_dummy;
|
|
struct spi_mem_dirmap_desc *rdesc;
|
|
enum spi_nor_protocol read_proto;
|
|
int ret;
|
|
|
|
read_opcode = nor->read_opcode;
|
|
addr_width = nor->addr_width;
|
|
read_dummy = nor->read_dummy;
|
|
read_proto = nor->read_proto;
|
|
rdesc = nor->dirmap.rdesc;
|
|
|
|
nor->read_opcode = SPINOR_OP_RSECR;
|
|
nor->addr_width = 3;
|
|
nor->read_dummy = 8;
|
|
nor->read_proto = SNOR_PROTO_1_1_1;
|
|
nor->dirmap.rdesc = NULL;
|
|
|
|
ret = spi_nor_read_data(nor, addr, len, buf);
|
|
|
|
nor->read_opcode = read_opcode;
|
|
nor->addr_width = addr_width;
|
|
nor->read_dummy = read_dummy;
|
|
nor->read_proto = read_proto;
|
|
nor->dirmap.rdesc = rdesc;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* spi_nor_otp_write_secr() - write OTP data
|
|
* @nor: pointer to 'struct spi_nor'
|
|
* @to: offset to write to
|
|
* @len: number of bytes to write
|
|
* @buf: pointer to src buffer
|
|
*
|
|
* Write OTP data to one region by using the SPINOR_OP_PSECR commands. This
|
|
* method is used on GigaDevice and Winbond flashes.
|
|
*
|
|
* Please note, the write must not span multiple OTP regions.
|
|
*
|
|
* Return: number of bytes written successfully, -errno otherwise
|
|
*/
|
|
int spi_nor_otp_write_secr(struct spi_nor *nor, loff_t addr, size_t len,
|
|
const u8 *buf)
|
|
{
|
|
enum spi_nor_protocol write_proto;
|
|
struct spi_mem_dirmap_desc *wdesc;
|
|
u8 addr_width, program_opcode;
|
|
int ret, written;
|
|
|
|
program_opcode = nor->program_opcode;
|
|
addr_width = nor->addr_width;
|
|
write_proto = nor->write_proto;
|
|
wdesc = nor->dirmap.wdesc;
|
|
|
|
nor->program_opcode = SPINOR_OP_PSECR;
|
|
nor->addr_width = 3;
|
|
nor->write_proto = SNOR_PROTO_1_1_1;
|
|
nor->dirmap.wdesc = NULL;
|
|
|
|
/*
|
|
* We only support a write to one single page. For now all winbond
|
|
* flashes only have one page per OTP region.
|
|
*/
|
|
ret = spi_nor_write_enable(nor);
|
|
if (ret)
|
|
goto out;
|
|
|
|
written = spi_nor_write_data(nor, addr, len, buf);
|
|
if (written < 0)
|
|
goto out;
|
|
|
|
ret = spi_nor_wait_till_ready(nor);
|
|
|
|
out:
|
|
nor->program_opcode = program_opcode;
|
|
nor->addr_width = addr_width;
|
|
nor->write_proto = write_proto;
|
|
nor->dirmap.wdesc = wdesc;
|
|
|
|
return ret ?: written;
|
|
}
|
|
|
|
static int spi_nor_otp_lock_bit_cr(unsigned int region)
|
|
{
|
|
static const int lock_bits[] = { SR2_LB1, SR2_LB2, SR2_LB3 };
|
|
|
|
if (region >= ARRAY_SIZE(lock_bits))
|
|
return -EINVAL;
|
|
|
|
return lock_bits[region];
|
|
}
|
|
|
|
/**
|
|
* spi_nor_otp_lock_sr2() - lock the OTP region
|
|
* @nor: pointer to 'struct spi_nor'
|
|
* @region: OTP region
|
|
*
|
|
* Lock the OTP region by writing the status register-2. This method is used on
|
|
* GigaDevice and Winbond flashes.
|
|
*
|
|
* Return: 0 on success, -errno otherwise.
|
|
*/
|
|
int spi_nor_otp_lock_sr2(struct spi_nor *nor, unsigned int region)
|
|
{
|
|
u8 *cr = nor->bouncebuf;
|
|
int ret, lock_bit;
|
|
|
|
lock_bit = spi_nor_otp_lock_bit_cr(region);
|
|
if (lock_bit < 0)
|
|
return lock_bit;
|
|
|
|
ret = spi_nor_read_cr(nor, cr);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* no need to write the register if region is already locked */
|
|
if (cr[0] & lock_bit)
|
|
return 0;
|
|
|
|
cr[0] |= lock_bit;
|
|
|
|
return spi_nor_write_16bit_cr_and_check(nor, cr[0]);
|
|
}
|
|
|
|
/**
|
|
* spi_nor_otp_is_locked_sr2() - get the OTP region lock status
|
|
* @nor: pointer to 'struct spi_nor'
|
|
* @region: OTP region
|
|
*
|
|
* Retrieve the OTP region lock bit by reading the status register-2. This
|
|
* method is used on GigaDevice and Winbond flashes.
|
|
*
|
|
* Return: 0 on success, -errno otherwise.
|
|
*/
|
|
int spi_nor_otp_is_locked_sr2(struct spi_nor *nor, unsigned int region)
|
|
{
|
|
u8 *cr = nor->bouncebuf;
|
|
int ret, lock_bit;
|
|
|
|
lock_bit = spi_nor_otp_lock_bit_cr(region);
|
|
if (lock_bit < 0)
|
|
return lock_bit;
|
|
|
|
ret = spi_nor_read_cr(nor, cr);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return cr[0] & lock_bit;
|
|
}
|
|
|
|
static loff_t spi_nor_otp_region_start(const struct spi_nor *nor, unsigned int region)
|
|
{
|
|
const struct spi_nor_otp_organization *org = nor->params->otp.org;
|
|
|
|
return org->base + region * org->offset;
|
|
}
|
|
|
|
static size_t spi_nor_otp_size(struct spi_nor *nor)
|
|
{
|
|
return spi_nor_otp_n_regions(nor) * spi_nor_otp_region_len(nor);
|
|
}
|
|
|
|
/* Translate the file offsets from and to OTP regions. */
|
|
static loff_t spi_nor_otp_region_to_offset(struct spi_nor *nor, unsigned int region)
|
|
{
|
|
return region * spi_nor_otp_region_len(nor);
|
|
}
|
|
|
|
static unsigned int spi_nor_otp_offset_to_region(struct spi_nor *nor, loff_t ofs)
|
|
{
|
|
return div64_u64(ofs, spi_nor_otp_region_len(nor));
|
|
}
|
|
|
|
static int spi_nor_mtd_otp_info(struct mtd_info *mtd, size_t len,
|
|
size_t *retlen, struct otp_info *buf)
|
|
{
|
|
struct spi_nor *nor = mtd_to_spi_nor(mtd);
|
|
const struct spi_nor_otp_ops *ops = nor->params->otp.ops;
|
|
unsigned int n_regions = spi_nor_otp_n_regions(nor);
|
|
unsigned int i;
|
|
int ret, locked;
|
|
|
|
if (len < n_regions * sizeof(*buf))
|
|
return -ENOSPC;
|
|
|
|
ret = spi_nor_lock_and_prep(nor);
|
|
if (ret)
|
|
return ret;
|
|
|
|
for (i = 0; i < n_regions; i++) {
|
|
buf->start = spi_nor_otp_region_to_offset(nor, i);
|
|
buf->length = spi_nor_otp_region_len(nor);
|
|
|
|
locked = ops->is_locked(nor, i);
|
|
if (locked < 0) {
|
|
ret = locked;
|
|
goto out;
|
|
}
|
|
|
|
buf->locked = !!locked;
|
|
buf++;
|
|
}
|
|
|
|
*retlen = n_regions * sizeof(*buf);
|
|
|
|
out:
|
|
spi_nor_unlock_and_unprep(nor);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int spi_nor_mtd_otp_read_write(struct mtd_info *mtd, loff_t ofs,
|
|
size_t total_len, size_t *retlen,
|
|
const u8 *buf, bool is_write)
|
|
{
|
|
struct spi_nor *nor = mtd_to_spi_nor(mtd);
|
|
const struct spi_nor_otp_ops *ops = nor->params->otp.ops;
|
|
const size_t rlen = spi_nor_otp_region_len(nor);
|
|
loff_t rstart, rofs;
|
|
unsigned int region;
|
|
size_t len;
|
|
int ret;
|
|
|
|
if (ofs < 0 || ofs >= spi_nor_otp_size(nor))
|
|
return 0;
|
|
|
|
ret = spi_nor_lock_and_prep(nor);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* don't access beyond the end */
|
|
total_len = min_t(size_t, total_len, spi_nor_otp_size(nor) - ofs);
|
|
|
|
*retlen = 0;
|
|
while (total_len) {
|
|
/*
|
|
* The OTP regions are mapped into a contiguous area starting
|
|
* at 0 as expected by the MTD layer. This will map the MTD
|
|
* file offsets to the address of an OTP region as used in the
|
|
* actual SPI commands.
|
|
*/
|
|
region = spi_nor_otp_offset_to_region(nor, ofs);
|
|
rstart = spi_nor_otp_region_start(nor, region);
|
|
|
|
/*
|
|
* The size of a OTP region is expected to be a power of two,
|
|
* thus we can just mask the lower bits and get the offset into
|
|
* a region.
|
|
*/
|
|
rofs = ofs & (rlen - 1);
|
|
|
|
/* don't access beyond one OTP region */
|
|
len = min_t(size_t, total_len, rlen - rofs);
|
|
|
|
if (is_write)
|
|
ret = ops->write(nor, rstart + rofs, len, buf);
|
|
else
|
|
ret = ops->read(nor, rstart + rofs, len, (u8 *)buf);
|
|
if (ret == 0)
|
|
ret = -EIO;
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
*retlen += ret;
|
|
ofs += ret;
|
|
buf += ret;
|
|
total_len -= ret;
|
|
}
|
|
ret = 0;
|
|
|
|
out:
|
|
spi_nor_unlock_and_unprep(nor);
|
|
return ret;
|
|
}
|
|
|
|
static int spi_nor_mtd_otp_read(struct mtd_info *mtd, loff_t from, size_t len,
|
|
size_t *retlen, u8 *buf)
|
|
{
|
|
return spi_nor_mtd_otp_read_write(mtd, from, len, retlen, buf, false);
|
|
}
|
|
|
|
static int spi_nor_mtd_otp_write(struct mtd_info *mtd, loff_t to, size_t len,
|
|
size_t *retlen, const u8 *buf)
|
|
{
|
|
return spi_nor_mtd_otp_read_write(mtd, to, len, retlen, buf, true);
|
|
}
|
|
|
|
static int spi_nor_mtd_otp_lock(struct mtd_info *mtd, loff_t from, size_t len)
|
|
{
|
|
struct spi_nor *nor = mtd_to_spi_nor(mtd);
|
|
const struct spi_nor_otp_ops *ops = nor->params->otp.ops;
|
|
const size_t rlen = spi_nor_otp_region_len(nor);
|
|
unsigned int region;
|
|
int ret;
|
|
|
|
if (from < 0 || (from + len) > spi_nor_otp_size(nor))
|
|
return -EINVAL;
|
|
|
|
/* the user has to explicitly ask for whole regions */
|
|
if (!IS_ALIGNED(len, rlen) || !IS_ALIGNED(from, rlen))
|
|
return -EINVAL;
|
|
|
|
ret = spi_nor_lock_and_prep(nor);
|
|
if (ret)
|
|
return ret;
|
|
|
|
while (len) {
|
|
region = spi_nor_otp_offset_to_region(nor, from);
|
|
ret = ops->lock(nor, region);
|
|
if (ret)
|
|
goto out;
|
|
|
|
len -= rlen;
|
|
from += rlen;
|
|
}
|
|
|
|
out:
|
|
spi_nor_unlock_and_unprep(nor);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void spi_nor_otp_init(struct spi_nor *nor)
|
|
{
|
|
struct mtd_info *mtd = &nor->mtd;
|
|
|
|
if (!nor->params->otp.ops)
|
|
return;
|
|
|
|
if (WARN_ON(!is_power_of_2(spi_nor_otp_region_len(nor))))
|
|
return;
|
|
|
|
/*
|
|
* We only support user_prot callbacks (yet).
|
|
*
|
|
* Some SPI NOR flashes like Macronix ones can be ordered in two
|
|
* different variants. One with a factory locked OTP area and one where
|
|
* it is left to the user to write to it. The factory locked OTP is
|
|
* usually preprogrammed with an "electrical serial number". We don't
|
|
* support these for now.
|
|
*/
|
|
mtd->_get_user_prot_info = spi_nor_mtd_otp_info;
|
|
mtd->_read_user_prot_reg = spi_nor_mtd_otp_read;
|
|
mtd->_write_user_prot_reg = spi_nor_mtd_otp_write;
|
|
mtd->_lock_user_prot_reg = spi_nor_mtd_otp_lock;
|
|
}
|