| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2025, SaluteDevices. All Rights Reserved. |
| * |
| * Author: Martin Kurbanov <mmkurbanov@salutedevices.com> |
| */ |
| |
| #include <linux/mtd/mtd.h> |
| #include <linux/mtd/spinand.h> |
| |
| /** |
| * spinand_otp_page_size() - Get SPI-NAND OTP page size |
| * @spinand: the spinand device |
| * |
| * Return: the OTP page size. |
| */ |
| size_t spinand_otp_page_size(struct spinand_device *spinand) |
| { |
| struct nand_device *nand = spinand_to_nand(spinand); |
| |
| return nanddev_page_size(nand) + nanddev_per_page_oobsize(nand); |
| } |
| |
| static size_t spinand_otp_size(struct spinand_device *spinand, |
| const struct spinand_otp_layout *layout) |
| { |
| return layout->npages * spinand_otp_page_size(spinand); |
| } |
| |
| /** |
| * spinand_fact_otp_size() - Get SPI-NAND factory OTP area size |
| * @spinand: the spinand device |
| * |
| * Return: the OTP size. |
| */ |
| size_t spinand_fact_otp_size(struct spinand_device *spinand) |
| { |
| return spinand_otp_size(spinand, &spinand->fact_otp->layout); |
| } |
| |
| /** |
| * spinand_user_otp_size() - Get SPI-NAND user OTP area size |
| * @spinand: the spinand device |
| * |
| * Return: the OTP size. |
| */ |
| size_t spinand_user_otp_size(struct spinand_device *spinand) |
| { |
| return spinand_otp_size(spinand, &spinand->user_otp->layout); |
| } |
| |
| static int spinand_otp_check_bounds(struct spinand_device *spinand, loff_t ofs, |
| size_t len, |
| const struct spinand_otp_layout *layout) |
| { |
| if (ofs < 0 || ofs + len > spinand_otp_size(spinand, layout)) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static int spinand_user_otp_check_bounds(struct spinand_device *spinand, |
| loff_t ofs, size_t len) |
| { |
| return spinand_otp_check_bounds(spinand, ofs, len, |
| &spinand->user_otp->layout); |
| } |
| |
| static int spinand_otp_rw(struct spinand_device *spinand, loff_t ofs, |
| size_t len, size_t *retlen, u8 *buf, bool is_write, |
| const struct spinand_otp_layout *layout) |
| { |
| struct nand_page_io_req req = {}; |
| unsigned long long page; |
| size_t copied = 0; |
| size_t otp_pagesize = spinand_otp_page_size(spinand); |
| int ret; |
| |
| if (!len) |
| return 0; |
| |
| ret = spinand_otp_check_bounds(spinand, ofs, len, layout); |
| if (ret) |
| return ret; |
| |
| ret = spinand_upd_cfg(spinand, CFG_OTP_ENABLE, CFG_OTP_ENABLE); |
| if (ret) |
| return ret; |
| |
| page = ofs; |
| req.dataoffs = do_div(page, otp_pagesize); |
| req.pos.page = page + layout->start_page; |
| req.type = is_write ? NAND_PAGE_WRITE : NAND_PAGE_READ; |
| req.mode = MTD_OPS_RAW; |
| req.databuf.in = buf; |
| |
| while (copied < len) { |
| req.datalen = min_t(unsigned int, |
| otp_pagesize - req.dataoffs, |
| len - copied); |
| |
| if (is_write) |
| ret = spinand_write_page(spinand, &req); |
| else |
| ret = spinand_read_page(spinand, &req); |
| |
| if (ret < 0) |
| break; |
| |
| req.databuf.in += req.datalen; |
| req.pos.page++; |
| req.dataoffs = 0; |
| copied += req.datalen; |
| } |
| |
| *retlen = copied; |
| |
| if (spinand_upd_cfg(spinand, CFG_OTP_ENABLE, 0)) { |
| dev_warn(&spinand_to_mtd(spinand)->dev, |
| "Can not disable OTP mode\n"); |
| ret = -EIO; |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * spinand_fact_otp_read() - Read from OTP area |
| * @spinand: the spinand device |
| * @ofs: the offset to read |
| * @len: the number of data bytes to read |
| * @retlen: the pointer to variable to store the number of read bytes |
| * @buf: the buffer to store the read data |
| * |
| * Return: 0 on success, an error code otherwise. |
| */ |
| int spinand_fact_otp_read(struct spinand_device *spinand, loff_t ofs, |
| size_t len, size_t *retlen, u8 *buf) |
| { |
| return spinand_otp_rw(spinand, ofs, len, retlen, buf, false, |
| &spinand->fact_otp->layout); |
| } |
| |
| /** |
| * spinand_user_otp_read() - Read from OTP area |
| * @spinand: the spinand device |
| * @ofs: the offset to read |
| * @len: the number of data bytes to read |
| * @retlen: the pointer to variable to store the number of read bytes |
| * @buf: the buffer to store the read data |
| * |
| * Return: 0 on success, an error code otherwise. |
| */ |
| int spinand_user_otp_read(struct spinand_device *spinand, loff_t ofs, |
| size_t len, size_t *retlen, u8 *buf) |
| { |
| return spinand_otp_rw(spinand, ofs, len, retlen, buf, false, |
| &spinand->user_otp->layout); |
| } |
| |
| /** |
| * spinand_user_otp_write() - Write to OTP area |
| * @spinand: the spinand device |
| * @ofs: the offset to write to |
| * @len: the number of bytes to write |
| * @retlen: the pointer to variable to store the number of written bytes |
| * @buf: the buffer with data to write |
| * |
| * Return: 0 on success, an error code otherwise. |
| */ |
| int spinand_user_otp_write(struct spinand_device *spinand, loff_t ofs, |
| size_t len, size_t *retlen, const u8 *buf) |
| { |
| return spinand_otp_rw(spinand, ofs, len, retlen, (u8 *)buf, true, |
| &spinand->user_otp->layout); |
| } |
| |
| static int spinand_mtd_otp_info(struct mtd_info *mtd, size_t len, |
| size_t *retlen, struct otp_info *buf, |
| bool is_fact) |
| { |
| struct spinand_device *spinand = mtd_to_spinand(mtd); |
| int ret; |
| |
| *retlen = 0; |
| |
| mutex_lock(&spinand->lock); |
| |
| if (is_fact) |
| ret = spinand->fact_otp->ops->info(spinand, len, buf, retlen); |
| else |
| ret = spinand->user_otp->ops->info(spinand, len, buf, retlen); |
| |
| mutex_unlock(&spinand->lock); |
| |
| return ret; |
| } |
| |
| static int spinand_mtd_fact_otp_info(struct mtd_info *mtd, size_t len, |
| size_t *retlen, struct otp_info *buf) |
| { |
| return spinand_mtd_otp_info(mtd, len, retlen, buf, true); |
| } |
| |
| static int spinand_mtd_user_otp_info(struct mtd_info *mtd, size_t len, |
| size_t *retlen, struct otp_info *buf) |
| { |
| return spinand_mtd_otp_info(mtd, len, retlen, buf, false); |
| } |
| |
| static int spinand_mtd_otp_read(struct mtd_info *mtd, loff_t ofs, size_t len, |
| size_t *retlen, u8 *buf, bool is_fact) |
| { |
| struct spinand_device *spinand = mtd_to_spinand(mtd); |
| int ret; |
| |
| *retlen = 0; |
| |
| if (!len) |
| return 0; |
| |
| ret = spinand_otp_check_bounds(spinand, ofs, len, |
| is_fact ? &spinand->fact_otp->layout : |
| &spinand->user_otp->layout); |
| if (ret) |
| return ret; |
| |
| mutex_lock(&spinand->lock); |
| |
| if (is_fact) |
| ret = spinand->fact_otp->ops->read(spinand, ofs, len, retlen, |
| buf); |
| else |
| ret = spinand->user_otp->ops->read(spinand, ofs, len, retlen, |
| buf); |
| |
| mutex_unlock(&spinand->lock); |
| |
| return ret; |
| } |
| |
| static int spinand_mtd_fact_otp_read(struct mtd_info *mtd, loff_t ofs, |
| size_t len, size_t *retlen, u8 *buf) |
| { |
| return spinand_mtd_otp_read(mtd, ofs, len, retlen, buf, true); |
| } |
| |
| static int spinand_mtd_user_otp_read(struct mtd_info *mtd, loff_t ofs, |
| size_t len, size_t *retlen, u8 *buf) |
| { |
| return spinand_mtd_otp_read(mtd, ofs, len, retlen, buf, false); |
| } |
| |
| static int spinand_mtd_user_otp_write(struct mtd_info *mtd, loff_t ofs, |
| size_t len, size_t *retlen, const u8 *buf) |
| { |
| struct spinand_device *spinand = mtd_to_spinand(mtd); |
| const struct spinand_user_otp_ops *ops = spinand->user_otp->ops; |
| int ret; |
| |
| *retlen = 0; |
| |
| if (!len) |
| return 0; |
| |
| ret = spinand_user_otp_check_bounds(spinand, ofs, len); |
| if (ret) |
| return ret; |
| |
| mutex_lock(&spinand->lock); |
| ret = ops->write(spinand, ofs, len, retlen, buf); |
| mutex_unlock(&spinand->lock); |
| |
| return ret; |
| } |
| |
| static int spinand_mtd_user_otp_erase(struct mtd_info *mtd, loff_t ofs, |
| size_t len) |
| { |
| struct spinand_device *spinand = mtd_to_spinand(mtd); |
| const struct spinand_user_otp_ops *ops = spinand->user_otp->ops; |
| int ret; |
| |
| if (!len) |
| return 0; |
| |
| ret = spinand_user_otp_check_bounds(spinand, ofs, len); |
| if (ret) |
| return ret; |
| |
| mutex_lock(&spinand->lock); |
| ret = ops->erase(spinand, ofs, len); |
| mutex_unlock(&spinand->lock); |
| |
| return ret; |
| } |
| |
| static int spinand_mtd_user_otp_lock(struct mtd_info *mtd, loff_t ofs, |
| size_t len) |
| { |
| struct spinand_device *spinand = mtd_to_spinand(mtd); |
| const struct spinand_user_otp_ops *ops = spinand->user_otp->ops; |
| int ret; |
| |
| if (!len) |
| return 0; |
| |
| ret = spinand_user_otp_check_bounds(spinand, ofs, len); |
| if (ret) |
| return ret; |
| |
| mutex_lock(&spinand->lock); |
| ret = ops->lock(spinand, ofs, len); |
| mutex_unlock(&spinand->lock); |
| |
| return ret; |
| } |
| |
| /** |
| * spinand_set_mtd_otp_ops() - Setup OTP methods |
| * @spinand: the spinand device |
| * |
| * Setup OTP methods. |
| * |
| * Return: 0 on success, a negative error code otherwise. |
| */ |
| int spinand_set_mtd_otp_ops(struct spinand_device *spinand) |
| { |
| struct mtd_info *mtd = spinand_to_mtd(spinand); |
| const struct spinand_fact_otp_ops *fact_ops = spinand->fact_otp->ops; |
| const struct spinand_user_otp_ops *user_ops = spinand->user_otp->ops; |
| |
| if (!user_ops && !fact_ops) |
| return -EINVAL; |
| |
| if (user_ops) { |
| if (user_ops->info) |
| mtd->_get_user_prot_info = spinand_mtd_user_otp_info; |
| |
| if (user_ops->read) |
| mtd->_read_user_prot_reg = spinand_mtd_user_otp_read; |
| |
| if (user_ops->write) |
| mtd->_write_user_prot_reg = spinand_mtd_user_otp_write; |
| |
| if (user_ops->lock) |
| mtd->_lock_user_prot_reg = spinand_mtd_user_otp_lock; |
| |
| if (user_ops->erase) |
| mtd->_erase_user_prot_reg = spinand_mtd_user_otp_erase; |
| } |
| |
| if (fact_ops) { |
| if (fact_ops->info) |
| mtd->_get_fact_prot_info = spinand_mtd_fact_otp_info; |
| |
| if (fact_ops->read) |
| mtd->_read_fact_prot_reg = spinand_mtd_fact_otp_read; |
| } |
| |
| return 0; |
| } |