| // 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 struct mtk_snand_of_id mt7986_soc_id = { .soc = SNAND_SOC_MT7986 }; |
| |
| 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 }, |
| { .compatible = "mediatek,mt7986-snand", .data = &mt7986_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"); |