// SPDX-License-Identifier: GPL-2.0 /* * Copyright (C) 2020 MediaTek Inc. All Rights Reserved. * * Author: Weijie Gao */ #include #include #include #include #include #include #include #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("MeidaTek SPI-NAND Flash Controller Driver");