diff options
Diffstat (limited to 'target/linux/mediatek/files-5.10/drivers/mtd/mtk-snand/mtk-snand-mtd.c')
-rw-r--r-- | target/linux/mediatek/files-5.10/drivers/mtd/mtk-snand/mtk-snand-mtd.c | 677 |
1 files changed, 677 insertions, 0 deletions
diff --git a/target/linux/mediatek/files-5.10/drivers/mtd/mtk-snand/mtk-snand-mtd.c b/target/linux/mediatek/files-5.10/drivers/mtd/mtk-snand/mtk-snand-mtd.c new file mode 100644 index 0000000000..b0fcda446d --- /dev/null +++ b/target/linux/mediatek/files-5.10/drivers/mtd/mtk-snand/mtk-snand-mtd.c @@ -0,0 +1,677 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2020 MediaTek Inc. All Rights Reserved. + * + * Author: Weijie Gao <weijie.gao@mediatek.com> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/mutex.h> +#include <linux/clk.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/dma-mapping.h> +#include <linux/wait.h> +#include <linux/mtd/mtd.h> +#include <linux/mtd/partitions.h> +#include <linux/of_platform.h> + +#include "mtk-snand.h" +#include "mtk-snand-os.h" + +struct mtk_snand_of_id { + enum mtk_snand_soc soc; +}; + +struct mtk_snand_mtd { + struct mtk_snand_plat_dev pdev; + + struct clk *nfi_clk; + struct clk *pad_clk; + struct clk *ecc_clk; + + void __iomem *nfi_regs; + void __iomem *ecc_regs; + + int irq; + + bool quad_spi; + enum mtk_snand_soc soc; + + struct mtd_info mtd; + struct mtk_snand *snf; + struct mtk_snand_chip_info cinfo; + uint8_t *page_cache; + struct mutex lock; +}; + +#define mtd_to_msm(mtd) container_of(mtd, struct mtk_snand_mtd, mtd) + +static int mtk_snand_mtd_erase(struct mtd_info *mtd, struct erase_info *instr) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + u64 start_addr, end_addr; + int ret; + + /* Do not allow write past end of device */ + if ((instr->addr + instr->len) > mtd->size) { + dev_err(msm->pdev.dev, + "attempt to erase beyond end of device\n"); + return -EINVAL; + } + + start_addr = instr->addr & (~mtd->erasesize_mask); + end_addr = instr->addr + instr->len; + if (end_addr & mtd->erasesize_mask) { + end_addr = (end_addr + mtd->erasesize_mask) & + (~mtd->erasesize_mask); + } + + mutex_lock(&msm->lock); + + while (start_addr < end_addr) { + if (mtk_snand_block_isbad(msm->snf, start_addr)) { + instr->fail_addr = start_addr; + ret = -EIO; + break; + } + + ret = mtk_snand_erase_block(msm->snf, start_addr); + if (ret) { + instr->fail_addr = start_addr; + break; + } + + start_addr += mtd->erasesize; + } + + mutex_unlock(&msm->lock); + + return ret; +} + +static int mtk_snand_mtd_read_data(struct mtk_snand_mtd *msm, uint64_t addr, + struct mtd_oob_ops *ops) +{ + struct mtd_info *mtd = &msm->mtd; + size_t len, ooblen, maxooblen, chklen; + uint32_t col, ooboffs; + uint8_t *datcache, *oobcache; + bool ecc_failed = false, raw = ops->mode == MTD_OPS_RAW ? true : false; + int ret, max_bitflips = 0; + + col = addr & mtd->writesize_mask; + addr &= ~mtd->writesize_mask; + maxooblen = mtd_oobavail(mtd, ops); + ooboffs = ops->ooboffs; + ooblen = ops->ooblen; + len = ops->len; + + datcache = len ? msm->page_cache : NULL; + oobcache = ooblen ? msm->page_cache + mtd->writesize : NULL; + + ops->oobretlen = 0; + ops->retlen = 0; + + while (len || ooblen) { + if (ops->mode == MTD_OPS_AUTO_OOB) + ret = mtk_snand_read_page_auto_oob(msm->snf, addr, + datcache, oobcache, maxooblen, NULL, raw); + else + ret = mtk_snand_read_page(msm->snf, addr, datcache, + oobcache, raw); + + if (ret < 0 && ret != -EBADMSG) + return ret; + + if (ret == -EBADMSG) { + mtd->ecc_stats.failed++; + ecc_failed = true; + } else { + mtd->ecc_stats.corrected += ret; + max_bitflips = max_t(int, ret, max_bitflips); + } + + if (len) { + /* Move data */ + chklen = mtd->writesize - col; + if (chklen > len) + chklen = len; + + memcpy(ops->datbuf + ops->retlen, datcache + col, + chklen); + len -= chklen; + col = 0; /* (col + chklen) % */ + ops->retlen += chklen; + } + + if (ooblen) { + /* Move oob */ + chklen = maxooblen - ooboffs; + if (chklen > ooblen) + chklen = ooblen; + + memcpy(ops->oobbuf + ops->oobretlen, oobcache + ooboffs, + chklen); + ooblen -= chklen; + ooboffs = 0; /* (ooboffs + chklen) % maxooblen; */ + ops->oobretlen += chklen; + } + + addr += mtd->writesize; + } + + return ecc_failed ? -EBADMSG : max_bitflips; +} + +static int mtk_snand_mtd_read_oob(struct mtd_info *mtd, loff_t from, + struct mtd_oob_ops *ops) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + uint32_t maxooblen; + int ret; + + if (!ops->oobbuf && !ops->datbuf) { + if (ops->ooblen || ops->len) + return -EINVAL; + + return 0; + } + + switch (ops->mode) { + case MTD_OPS_PLACE_OOB: + case MTD_OPS_AUTO_OOB: + case MTD_OPS_RAW: + break; + default: + dev_err(msm->pdev.dev, "unsupported oob mode: %u\n", ops->mode); + return -EINVAL; + } + + maxooblen = mtd_oobavail(mtd, ops); + + /* Do not allow read past end of device */ + if (ops->datbuf && (from + ops->len) > mtd->size) { + dev_err(msm->pdev.dev, + "attempt to read beyond end of device\n"); + return -EINVAL; + } + + if (unlikely(ops->ooboffs >= maxooblen)) { + dev_err(msm->pdev.dev, "attempt to start read outside oob\n"); + return -EINVAL; + } + + if (unlikely(from >= mtd->size || + ops->ooboffs + ops->ooblen > ((mtd->size >> mtd->writesize_shift) - + (from >> mtd->writesize_shift)) * maxooblen)) { + dev_err(msm->pdev.dev, + "attempt to read beyond end of device\n"); + return -EINVAL; + } + + mutex_lock(&msm->lock); + ret = mtk_snand_mtd_read_data(msm, from, ops); + mutex_unlock(&msm->lock); + + return ret; +} + +static int mtk_snand_mtd_write_data(struct mtk_snand_mtd *msm, uint64_t addr, + struct mtd_oob_ops *ops) +{ + struct mtd_info *mtd = &msm->mtd; + size_t len, ooblen, maxooblen, chklen, oobwrlen; + uint32_t col, ooboffs; + uint8_t *datcache, *oobcache; + bool raw = ops->mode == MTD_OPS_RAW ? true : false; + int ret; + + col = addr & mtd->writesize_mask; + addr &= ~mtd->writesize_mask; + maxooblen = mtd_oobavail(mtd, ops); + ooboffs = ops->ooboffs; + ooblen = ops->ooblen; + len = ops->len; + + datcache = len ? msm->page_cache : NULL; + oobcache = ooblen ? msm->page_cache + mtd->writesize : NULL; + + ops->oobretlen = 0; + ops->retlen = 0; + + while (len || ooblen) { + if (len) { + /* Move data */ + chklen = mtd->writesize - col; + if (chklen > len) + chklen = len; + + memset(datcache, 0xff, col); + memcpy(datcache + col, ops->datbuf + ops->retlen, + chklen); + memset(datcache + col + chklen, 0xff, + mtd->writesize - col - chklen); + len -= chklen; + col = 0; /* (col + chklen) % */ + ops->retlen += chklen; + } + + oobwrlen = 0; + if (ooblen) { + /* Move oob */ + chklen = maxooblen - ooboffs; + if (chklen > ooblen) + chklen = ooblen; + + memset(oobcache, 0xff, ooboffs); + memcpy(oobcache + ooboffs, + ops->oobbuf + ops->oobretlen, chklen); + memset(oobcache + ooboffs + chklen, 0xff, + mtd->oobsize - ooboffs - chklen); + oobwrlen = chklen + ooboffs; + ooblen -= chklen; + ooboffs = 0; /* (ooboffs + chklen) % maxooblen; */ + ops->oobretlen += chklen; + } + + if (ops->mode == MTD_OPS_AUTO_OOB) + ret = mtk_snand_write_page_auto_oob(msm->snf, addr, + datcache, oobcache, oobwrlen, NULL, raw); + else + ret = mtk_snand_write_page(msm->snf, addr, datcache, + oobcache, raw); + + if (ret) + return ret; + + addr += mtd->writesize; + } + + return 0; +} + +static int mtk_snand_mtd_write_oob(struct mtd_info *mtd, loff_t to, + struct mtd_oob_ops *ops) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + uint32_t maxooblen; + int ret; + + if (!ops->oobbuf && !ops->datbuf) { + if (ops->ooblen || ops->len) + return -EINVAL; + + return 0; + } + + switch (ops->mode) { + case MTD_OPS_PLACE_OOB: + case MTD_OPS_AUTO_OOB: + case MTD_OPS_RAW: + break; + default: + dev_err(msm->pdev.dev, "unsupported oob mode: %u\n", ops->mode); + return -EINVAL; + } + + maxooblen = mtd_oobavail(mtd, ops); + + /* Do not allow write past end of device */ + if (ops->datbuf && (to + ops->len) > mtd->size) { + dev_err(msm->pdev.dev, + "attempt to write beyond end of device\n"); + return -EINVAL; + } + + if (unlikely(ops->ooboffs >= maxooblen)) { + dev_err(msm->pdev.dev, + "attempt to start write outside oob\n"); + return -EINVAL; + } + + if (unlikely(to >= mtd->size || + ops->ooboffs + ops->ooblen > ((mtd->size >> mtd->writesize_shift) - + (to >> mtd->writesize_shift)) * maxooblen)) { + dev_err(msm->pdev.dev, + "attempt to write beyond end of device\n"); + return -EINVAL; + } + + mutex_lock(&msm->lock); + ret = mtk_snand_mtd_write_data(msm, to, ops); + mutex_unlock(&msm->lock); + + return ret; +} + +static int mtk_snand_mtd_block_isbad(struct mtd_info *mtd, loff_t offs) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + int ret; + + mutex_lock(&msm->lock); + ret = mtk_snand_block_isbad(msm->snf, offs); + mutex_unlock(&msm->lock); + + return ret; +} + +static int mtk_snand_mtd_block_markbad(struct mtd_info *mtd, loff_t offs) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + int ret; + + mutex_lock(&msm->lock); + ret = mtk_snand_block_markbad(msm->snf, offs); + mutex_unlock(&msm->lock); + + return ret; +} + +static int mtk_snand_ooblayout_ecc(struct mtd_info *mtd, int section, + struct mtd_oob_region *oobecc) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + + if (section) + return -ERANGE; + + oobecc->offset = msm->cinfo.fdm_size * msm->cinfo.num_sectors; + oobecc->length = mtd->oobsize - oobecc->offset; + + return 0; +} + +static int mtk_snand_ooblayout_free(struct mtd_info *mtd, int section, + struct mtd_oob_region *oobfree) +{ + struct mtk_snand_mtd *msm = mtd_to_msm(mtd); + + if (section >= msm->cinfo.num_sectors) + return -ERANGE; + + oobfree->length = msm->cinfo.fdm_size - 1; + oobfree->offset = section * msm->cinfo.fdm_size + 1; + + return 0; +} + +static irqreturn_t mtk_snand_irq(int irq, void *id) +{ + struct mtk_snand_mtd *msm = id; + int ret; + + ret = mtk_snand_irq_process(msm->snf); + if (ret > 0) + return IRQ_HANDLED; + + return IRQ_NONE; +} + +static int mtk_snand_enable_clk(struct mtk_snand_mtd *msm) +{ + int ret; + + ret = clk_prepare_enable(msm->nfi_clk); + if (ret) { + dev_err(msm->pdev.dev, "unable to enable nfi clk\n"); + return ret; + } + + ret = clk_prepare_enable(msm->pad_clk); + if (ret) { + dev_err(msm->pdev.dev, "unable to enable pad clk\n"); + clk_disable_unprepare(msm->nfi_clk); + return ret; + } + + ret = clk_prepare_enable(msm->ecc_clk); + if (ret) { + dev_err(msm->pdev.dev, "unable to enable ecc clk\n"); + clk_disable_unprepare(msm->nfi_clk); + clk_disable_unprepare(msm->pad_clk); + return ret; + } + + return 0; +} + +static void mtk_snand_disable_clk(struct mtk_snand_mtd *msm) +{ + clk_disable_unprepare(msm->nfi_clk); + clk_disable_unprepare(msm->pad_clk); + clk_disable_unprepare(msm->ecc_clk); +} + +static const struct mtd_ooblayout_ops mtk_snand_ooblayout = { + .ecc = mtk_snand_ooblayout_ecc, + .free = mtk_snand_ooblayout_free, +}; + +static struct mtk_snand_of_id mt7622_soc_id = { .soc = SNAND_SOC_MT7622 }; +static struct mtk_snand_of_id mt7629_soc_id = { .soc = SNAND_SOC_MT7629 }; + +static const struct of_device_id mtk_snand_ids[] = { + { .compatible = "mediatek,mt7622-snand", .data = &mt7622_soc_id }, + { .compatible = "mediatek,mt7629-snand", .data = &mt7629_soc_id }, + { }, +}; + +MODULE_DEVICE_TABLE(of, mtk_snand_ids); + +static int mtk_snand_probe(struct platform_device *pdev) +{ + struct mtk_snand_platdata mtk_snand_pdata = {}; + struct device_node *np = pdev->dev.of_node; + const struct of_device_id *of_soc_id; + const struct mtk_snand_of_id *soc_id; + struct mtk_snand_mtd *msm; + struct mtd_info *mtd; + struct resource *r; + uint32_t size; + int ret; + + of_soc_id = of_match_node(mtk_snand_ids, np); + if (!of_soc_id) + return -EINVAL; + + soc_id = of_soc_id->data; + + msm = devm_kzalloc(&pdev->dev, sizeof(*msm), GFP_KERNEL); + if (!msm) + return -ENOMEM; + + r = platform_get_resource_byname(pdev, IORESOURCE_MEM, "nfi"); + msm->nfi_regs = devm_ioremap_resource(&pdev->dev, r); + if (IS_ERR(msm->nfi_regs)) { + ret = PTR_ERR(msm->nfi_regs); + goto errout1; + } + + r = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ecc"); + msm->ecc_regs = devm_ioremap_resource(&pdev->dev, r); + if (IS_ERR(msm->ecc_regs)) { + ret = PTR_ERR(msm->ecc_regs); + goto errout1; + } + + msm->pdev.dev = &pdev->dev; + msm->quad_spi = of_property_read_bool(np, "mediatek,quad-spi"); + msm->soc = soc_id->soc; + + msm->nfi_clk = devm_clk_get(msm->pdev.dev, "nfi_clk"); + if (IS_ERR(msm->nfi_clk)) { + ret = PTR_ERR(msm->nfi_clk); + dev_err(msm->pdev.dev, "unable to get nfi_clk, err = %d\n", + ret); + goto errout1; + } + + msm->ecc_clk = devm_clk_get(msm->pdev.dev, "ecc_clk"); + if (IS_ERR(msm->ecc_clk)) { + ret = PTR_ERR(msm->ecc_clk); + dev_err(msm->pdev.dev, "unable to get ecc_clk, err = %d\n", + ret); + goto errout1; + } + + msm->pad_clk = devm_clk_get(msm->pdev.dev, "pad_clk"); + if (IS_ERR(msm->pad_clk)) { + ret = PTR_ERR(msm->pad_clk); + dev_err(msm->pdev.dev, "unable to get pad_clk, err = %d\n", + ret); + goto errout1; + } + + ret = mtk_snand_enable_clk(msm); + if (ret) + goto errout1; + + /* Probe SPI-NAND Flash */ + mtk_snand_pdata.soc = msm->soc; + mtk_snand_pdata.quad_spi = msm->quad_spi; + mtk_snand_pdata.nfi_base = msm->nfi_regs; + mtk_snand_pdata.ecc_base = msm->ecc_regs; + + ret = mtk_snand_init(&msm->pdev, &mtk_snand_pdata, &msm->snf); + if (ret) + goto errout1; + + msm->irq = platform_get_irq(pdev, 0); + if (msm->irq >= 0) { + ret = devm_request_irq(msm->pdev.dev, msm->irq, mtk_snand_irq, + 0x0, "mtk-snand", msm); + if (ret) { + dev_err(msm->pdev.dev, "failed to request snfi irq\n"); + goto errout2; + } + + ret = dma_set_mask(msm->pdev.dev, DMA_BIT_MASK(32)); + if (ret) { + dev_err(msm->pdev.dev, "failed to set dma mask\n"); + goto errout3; + } + } + + mtk_snand_get_chip_info(msm->snf, &msm->cinfo); + + size = msm->cinfo.pagesize + msm->cinfo.sparesize; + msm->page_cache = devm_kmalloc(msm->pdev.dev, size, GFP_KERNEL); + if (!msm->page_cache) { + dev_err(msm->pdev.dev, "failed to allocate page cache\n"); + ret = -ENOMEM; + goto errout3; + } + + mutex_init(&msm->lock); + + dev_info(msm->pdev.dev, + "chip is %s, size %lluMB, page size %u, oob size %u\n", + msm->cinfo.model, msm->cinfo.chipsize >> 20, + msm->cinfo.pagesize, msm->cinfo.sparesize); + + /* Initialize mtd for SPI-NAND */ + mtd = &msm->mtd; + + mtd->owner = THIS_MODULE; + mtd->dev.parent = &pdev->dev; + mtd->type = MTD_NANDFLASH; + mtd->flags = MTD_CAP_NANDFLASH; + + mtd_set_of_node(mtd, np); + + mtd->size = msm->cinfo.chipsize; + mtd->erasesize = msm->cinfo.blocksize; + mtd->writesize = msm->cinfo.pagesize; + mtd->writebufsize = mtd->writesize; + mtd->oobsize = msm->cinfo.sparesize; + mtd->oobavail = msm->cinfo.num_sectors * (msm->cinfo.fdm_size - 1); + + mtd->erasesize_shift = ffs(mtd->erasesize) - 1; + mtd->writesize_shift = ffs(mtd->writesize) - 1; + mtd->erasesize_mask = (1 << mtd->erasesize_shift) - 1; + mtd->writesize_mask = (1 << mtd->writesize_shift) - 1; + + mtd->ooblayout = &mtk_snand_ooblayout; + + mtd->ecc_strength = msm->cinfo.ecc_strength; + mtd->bitflip_threshold = (mtd->ecc_strength * 3) / 4; + mtd->ecc_step_size = msm->cinfo.sector_size; + + mtd->_erase = mtk_snand_mtd_erase; + mtd->_read_oob = mtk_snand_mtd_read_oob; + mtd->_write_oob = mtk_snand_mtd_write_oob; + mtd->_block_isbad = mtk_snand_mtd_block_isbad; + mtd->_block_markbad = mtk_snand_mtd_block_markbad; + + ret = mtd_device_register(mtd, NULL, 0); + if (ret) { + dev_err(msm->pdev.dev, "failed to register mtd partition\n"); + goto errout4; + } + + platform_set_drvdata(pdev, msm); + + return 0; + +errout4: + devm_kfree(msm->pdev.dev, msm->page_cache); + +errout3: + if (msm->irq >= 0) + devm_free_irq(msm->pdev.dev, msm->irq, msm); + +errout2: + mtk_snand_cleanup(msm->snf); + +errout1: + devm_kfree(msm->pdev.dev, msm); + + platform_set_drvdata(pdev, NULL); + + return ret; +} + +static int mtk_snand_remove(struct platform_device *pdev) +{ + struct mtk_snand_mtd *msm = platform_get_drvdata(pdev); + struct mtd_info *mtd = &msm->mtd; + int ret; + + ret = mtd_device_unregister(mtd); + if (ret) + return ret; + + mtk_snand_cleanup(msm->snf); + + if (msm->irq >= 0) + devm_free_irq(msm->pdev.dev, msm->irq, msm); + + mtk_snand_disable_clk(msm); + + devm_kfree(msm->pdev.dev, msm->page_cache); + devm_kfree(msm->pdev.dev, msm); + + platform_set_drvdata(pdev, NULL); + + return 0; +} + +static struct platform_driver mtk_snand_driver = { + .probe = mtk_snand_probe, + .remove = mtk_snand_remove, + .driver = { + .name = "mtk-snand", + .of_match_table = mtk_snand_ids, + }, +}; + +module_platform_driver(mtk_snand_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Weijie Gao <weijie.gao@mediatek.com>"); +MODULE_DESCRIPTION("MeidaTek SPI-NAND Flash Controller Driver"); |