From a8374683868634012ac873d628fa581fcc452e9c Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 16 Jun 2026 16:28:44 +0300 Subject: [PATCH 01/21] mtd: spinand: add support for HeYangTek HYF1GQ4UDACAE The HeYangTek HYF1GQ4UDACAE is a 1 Gbit (128 MiB) SLC SPI-NAND with 2048 + 64 byte pages and on-die 4-bit / 512-byte ECC; its JEDEC manufacturer ID is 0xc9. The die is GD5F1GQ4-compatible, so the OOB layout is taken from the in-tree gd5fxgq4xa. The die exposes only a coarse 2-bit ECC status with no fine-grained bitflip-count register, so the status is decoded into a representative number of corrected bitflips. It is found, among others, on some Keenetic KN-3411 (Buddy 6) units. Datasheet: https://www.heyangtek.cn/previewfile.jsp?file=ABUIABA9GAAgwsvRnwYo-eDpsgc Signed-off-by: Aleksei Sviridkin Signed-off-by: Miquel Raynal --- drivers/mtd/nand/spi/Makefile | 2 +- drivers/mtd/nand/spi/core.c | 1 + drivers/mtd/nand/spi/heyangtek.c | 132 +++++++++++++++++++++++++++++++ include/linux/mtd/spinand.h | 1 + 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 drivers/mtd/nand/spi/heyangtek.c diff --git a/drivers/mtd/nand/spi/Makefile b/drivers/mtd/nand/spi/Makefile index a47bd22cd309..b5ccb44860df 100644 --- a/drivers/mtd/nand/spi/Makefile +++ b/drivers/mtd/nand/spi/Makefile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 spinand-objs := core.o otp.o -spinand-objs += alliancememory.o ato.o dosilicon.o esmt.o fmsh.o foresee.o gigadevice.o +spinand-objs += alliancememory.o ato.o dosilicon.o esmt.o fmsh.o foresee.o gigadevice.o heyangtek.o spinand-objs += macronix.o micron.o paragon.o skyhigh.o toshiba.o winbond.o xtx.o obj-$(CONFIG_MTD_SPI_NAND) += spinand.o diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c index f86786344d52..35365b67dd8e 100644 --- a/drivers/mtd/nand/spi/core.c +++ b/drivers/mtd/nand/spi/core.c @@ -1359,6 +1359,7 @@ static const struct spinand_manufacturer *spinand_manufacturers[] = { &fmsh_spinand_manufacturer, &foresee_spinand_manufacturer, &gigadevice_spinand_manufacturer, + &heyangtek_spinand_manufacturer, ¯onix_spinand_manufacturer, µn_spinand_manufacturer, ¶gon_spinand_manufacturer, diff --git a/drivers/mtd/nand/spi/heyangtek.c b/drivers/mtd/nand/spi/heyangtek.c new file mode 100644 index 000000000000..7fc50fd3de08 --- /dev/null +++ b/drivers/mtd/nand/spi/heyangtek.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Authors: + * Andrey Zolotarev - the main driver logic + * Aleksei Sviridkin - adaptation to the mainline Linux kernel + * + * Based on: + * https://github.com/keenetic/kernel-49/commit/bacade569fb12bc0ad31ba09bca9b890118fbca7 + */ + +#include +#include +#include + +#define SPINAND_MFR_HEYANGTEK 0xc9 + +#define HYF1GQ4_STATUS_ECC_LIMIT_BITFLIPS (3 << 4) + +static SPINAND_OP_VARIANTS(read_cache_variants, + SPINAND_PAGE_READ_FROM_CACHE_1S_4S_4S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_1S_4S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_2S_2S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_1S_2S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_FAST_1S_1S_1S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_1S_1S_OP(0, 1, NULL, 0, 0)); + +static SPINAND_OP_VARIANTS(write_cache_variants, + SPINAND_PROG_LOAD_1S_1S_4S_OP(true, 0, NULL, 0), + SPINAND_PROG_LOAD_1S_1S_1S_OP(true, 0, NULL, 0)); + +static SPINAND_OP_VARIANTS(update_cache_variants, + SPINAND_PROG_LOAD_1S_1S_4S_OP(false, 0, NULL, 0), + SPINAND_PROG_LOAD_1S_1S_1S_OP(false, 0, NULL, 0)); + +/* + * HYF1GQ4UDACAE is a GD5F1GQ4-compatible die, so the OOB layout is taken + * from gd5fxgq4xa: the on-die ECC parity occupies bytes 8..15 of each + * 16-byte section, the bad block marker sits in byte 0 and the remaining + * bytes are exposed as free. + */ +static int hyf1gq4_ooblayout_ecc(struct mtd_info *mtd, int section, + struct mtd_oob_region *region) +{ + if (section > 3) + return -ERANGE; + + region->offset = (16 * section) + 8; + region->length = 8; + + return 0; +} + +static int hyf1gq4_ooblayout_free(struct mtd_info *mtd, int section, + struct mtd_oob_region *region) +{ + if (section > 3) + return -ERANGE; + + if (section) { + region->offset = 16 * section; + region->length = 8; + } else { + /* section 0 has one byte reserved for the bad block marker */ + region->offset = 1; + region->length = 7; + } + + return 0; +} + +static const struct mtd_ooblayout_ops hyf1gq4_ooblayout = { + .ecc = hyf1gq4_ooblayout_ecc, + .free = hyf1gq4_ooblayout_free, +}; + +static int hyf1gq4_ecc_get_status(struct spinand_device *spinand, u8 status) +{ + struct nand_device *nand = spinand_to_nand(spinand); + + switch (status & STATUS_ECC_MASK) { + case STATUS_ECC_NO_BITFLIPS: + return 0; + + case STATUS_ECC_UNCOR_ERROR: + return -EBADMSG; + + case STATUS_ECC_HAS_BITFLIPS: + /* + * The die exposes only a coarse 2-bit ECC status and has no + * register for the exact bitflip count. This code means + * "corrected, below the refresh threshold", so report half of + * the ECC strength as a representative value. + */ + return nanddev_get_ecc_conf(nand)->strength / 2; + + case HYF1GQ4_STATUS_ECC_LIMIT_BITFLIPS: + /* + * "Corrected, refresh recommended": report the full ECC + * strength so the upper layers relocate the data. + */ + return nanddev_get_ecc_conf(nand)->strength; + + default: + break; + } + + return -EINVAL; +} + +static const struct spinand_info heyangtek_spinand_table[] = { + SPINAND_INFO("HYF1GQ4UDACAE", + SPINAND_ID(SPINAND_READID_METHOD_OPCODE_ADDR, 0x21), + NAND_MEMORG(1, 2048, 64, 64, 1024, 20, 1, 1, 1), + NAND_ECCREQ(4, 512), + SPINAND_INFO_OP_VARIANTS(&read_cache_variants, + &write_cache_variants, + &update_cache_variants), + SPINAND_HAS_QE_BIT, + SPINAND_ECCINFO(&hyf1gq4_ooblayout, + hyf1gq4_ecc_get_status)), +}; + +static const struct spinand_manufacturer_ops heyangtek_spinand_manuf_ops = { +}; + +const struct spinand_manufacturer heyangtek_spinand_manufacturer = { + .id = SPINAND_MFR_HEYANGTEK, + .name = "HeYangTek", + .chips = heyangtek_spinand_table, + .nchips = ARRAY_SIZE(heyangtek_spinand_table), + .ops = &heyangtek_spinand_manuf_ops, +}; diff --git a/include/linux/mtd/spinand.h b/include/linux/mtd/spinand.h index ec6efcfeef83..5f4c00ae72a7 100644 --- a/include/linux/mtd/spinand.h +++ b/include/linux/mtd/spinand.h @@ -437,6 +437,7 @@ extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer; extern const struct spinand_manufacturer fmsh_spinand_manufacturer; extern const struct spinand_manufacturer foresee_spinand_manufacturer; extern const struct spinand_manufacturer gigadevice_spinand_manufacturer; +extern const struct spinand_manufacturer heyangtek_spinand_manufacturer; extern const struct spinand_manufacturer macronix_spinand_manufacturer; extern const struct spinand_manufacturer micron_spinand_manufacturer; extern const struct spinand_manufacturer paragon_spinand_manufacturer; From ee60be8929c7badf1194e3149a8aef930cfd77b8 Mon Sep 17 00:00:00 2001 From: "Miquel Raynal (DAVE)" Date: Fri, 29 May 2026 18:29:56 +0200 Subject: [PATCH 02/21] mtd: rawnand: pl353: Update timings at the right moment If several CE are wired, we would write the registers for every chip one after the other, and reselect the correct timings for the first chip the use wants to use after probe. This is not exactly efficient and could slightly be improved since we already have a helper that applies the configuration if there is a chip change. Instead of programming the registers in ->setup_interface(), let's just drop the pointer to the chip and let the nand_select_target() helper do its magic. Cc: Olivier Sobrie Signed-off-by: Miquel Raynal (DAVE) Acked-by: Olivier Sobrie Tested-by: Olivier Sobrie Acked-by: Michal Simek Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/pl35x-nand-controller.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/mtd/nand/raw/pl35x-nand-controller.c b/drivers/mtd/nand/raw/pl35x-nand-controller.c index 06f8f1e14b9c..7dd71bd69deb 100644 --- a/drivers/mtd/nand/raw/pl35x-nand-controller.c +++ b/drivers/mtd/nand/raw/pl35x-nand-controller.c @@ -862,8 +862,11 @@ static int pl35x_nfc_setup_interface(struct nand_chip *chip, int cs, PL35X_SMC_NAND_TAR_CYCLES(tmgs.t_ar) | PL35X_SMC_NAND_TRR_CYCLES(tmgs.t_rr); - writel(plnand->timings, nfc->conf_regs + PL35X_SMC_CYCLES); - pl35x_smc_update_regs(nfc); + /* + * Reset nfc->selected_chip so the next command will cause the timing + * registers to be updated in ->*_select_target(). + */ + nfc->selected_chip = NULL; return 0; } From 80ecacd054ffeb60cd28e46ed5cd6bd0d2de318b Mon Sep 17 00:00:00 2001 From: "Miquel Raynal (DAVE)" Date: Fri, 29 May 2026 18:29:57 +0200 Subject: [PATCH 03/21] mtd: rawnand: pl353: Make sure we use the monolithic helpers for raw accesses Any access not using the hardware ECC engine should be monolithic because the controller has its very own way of handling the end of a transaction during operation configuration, so we cannot easily make repeated reads. This has the side effect of fixing support for software ECC engines. Suggested-by: Andrea Scian Cc: stable@vger.kernel.org Fixes: 08d8c62164a3 ("mtd: rawnand: pl353: Add support for the ARM PL353 SMC NAND controller") Signed-off-by: Miquel Raynal (DAVE) Acked-by: Michal Simek Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/pl35x-nand-controller.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/mtd/nand/raw/pl35x-nand-controller.c b/drivers/mtd/nand/raw/pl35x-nand-controller.c index 7dd71bd69deb..0ce3796d161f 100644 --- a/drivers/mtd/nand/raw/pl35x-nand-controller.c +++ b/drivers/mtd/nand/raw/pl35x-nand-controller.c @@ -917,7 +917,6 @@ static int pl35x_nand_init_hw_ecc_controller(struct pl35x_nandc *nfc, chip->ecc.steps = mtd->writesize / chip->ecc.size; chip->ecc.read_page = pl35x_nand_read_page_hwecc; chip->ecc.write_page = pl35x_nand_write_page_hwecc; - chip->ecc.write_page_raw = nand_monolithic_write_page_raw; pl35x_smc_set_ecc_pg_size(nfc, chip, mtd->writesize); nfc->ecc_buf = devm_kmalloc(nfc->dev, chip->ecc.bytes * chip->ecc.steps, @@ -984,7 +983,6 @@ static int pl35x_nand_attach_chip(struct nand_chip *chip) case NAND_ECC_ENGINE_TYPE_NONE: case NAND_ECC_ENGINE_TYPE_SOFT: dev_dbg(nfc->dev, "Using software ECC (Hamming 1-bit/512B)\n"); - chip->ecc.write_page_raw = nand_monolithic_write_page_raw; break; case NAND_ECC_ENGINE_TYPE_ON_HOST: dev_dbg(nfc->dev, "Using hardware ECC\n"); @@ -998,6 +996,9 @@ static int pl35x_nand_attach_chip(struct nand_chip *chip) return -EINVAL; } + chip->ecc.read_page_raw = nand_monolithic_read_page_raw; + chip->ecc.write_page_raw = nand_monolithic_write_page_raw; + return 0; } From 2b7baaddf1bc3e39206a0354449fdc349945b86b Mon Sep 17 00:00:00 2001 From: "Miquel Raynal (DAVE)" Date: Fri, 29 May 2026 18:29:58 +0200 Subject: [PATCH 04/21] mtd: rawnand: pl353: Fix debug prints They are partially incorrect since "software" engine does not mean hamming, the "none" cae is also falling into this print, and on-die means there is some kind of hardware support; we prefer to use the wording on-host vs. on-die. Fix all those prints. Fixes: 1e06dbfdfb85 ("mtd: rawnand: pl353: Add message about ECC mode") Signed-off-by: Miquel Raynal (DAVE) Acked-by: Michal Simek Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/pl35x-nand-controller.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/mtd/nand/raw/pl35x-nand-controller.c b/drivers/mtd/nand/raw/pl35x-nand-controller.c index 0ce3796d161f..bd89aaadd1b2 100644 --- a/drivers/mtd/nand/raw/pl35x-nand-controller.c +++ b/drivers/mtd/nand/raw/pl35x-nand-controller.c @@ -975,17 +975,19 @@ static int pl35x_nand_attach_chip(struct nand_chip *chip) switch (chip->ecc.engine_type) { case NAND_ECC_ENGINE_TYPE_ON_DIE: - dev_dbg(nfc->dev, "Using on-die ECC\n"); + dev_dbg(nfc->dev, "Using on-die hardware ECC\n"); /* Keep these legacy BBT descriptors for ON_DIE situations */ chip->bbt_td = &bbt_main_descr; chip->bbt_md = &bbt_mirror_descr; fallthrough; case NAND_ECC_ENGINE_TYPE_NONE: + dev_dbg(nfc->dev, "Using no ECC engine\n"); + break; case NAND_ECC_ENGINE_TYPE_SOFT: - dev_dbg(nfc->dev, "Using software ECC (Hamming 1-bit/512B)\n"); + dev_dbg(nfc->dev, "Using software ECC\n"); break; case NAND_ECC_ENGINE_TYPE_ON_HOST: - dev_dbg(nfc->dev, "Using hardware ECC\n"); + dev_dbg(nfc->dev, "Using on-host hardware ECC\n"); ret = pl35x_nand_init_hw_ecc_controller(nfc, chip); if (ret) return ret; From d5a5c9eb2ee9ff5b4cc0be15ac7f4879d4c2f247 Mon Sep 17 00:00:00 2001 From: Ziyang Huang Date: Tue, 23 Jun 2026 22:54:22 +0800 Subject: [PATCH 05/21] mtd: spinand: fmsh: add support for FM25G{01,02}B Add support for FudanMicro FM25G01B SPI NAND and FudanMicro FM25G02B SPI NAND. FM25G01B datasheet: https://www.fmsh.com/nvm/FM25G01B_ds_eng.pdf FM25G02B datasheet: https://www.fmsh.com/nvm/FM25G02B_ds_eng.pdf Signed-off-by: Ziyang Huang Signed-off-by: Miquel Raynal --- drivers/mtd/nand/spi/fmsh.c | 95 +++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/drivers/mtd/nand/spi/fmsh.c b/drivers/mtd/nand/spi/fmsh.c index f417955f7d1c..be5db9f9502b 100644 --- a/drivers/mtd/nand/spi/fmsh.c +++ b/drivers/mtd/nand/spi/fmsh.c @@ -9,6 +9,16 @@ #include #include +#define FM25G01B_STATUS_ECC_MASK (7 << 4) + #define FM25G01B_STATUS_ECC_NO_BITFLIPS (0 << 4) + #define FM25G01B_STATUS_ECC_1_3_BITFLIPS (1 << 4) + #define FM25G01B_STATUS_ECC_4_BITFLIPS (2 << 4) + #define FM25G01B_STATUS_ECC_5_BITFLIPS (3 << 4) + #define FM25G01B_STATUS_ECC_6_BITFLIPS (4 << 4) + #define FM25G01B_STATUS_ECC_7_BITFLIPS (5 << 4) + #define FM25G01B_STATUS_ECC_8_BITFLIPS (6 << 4) + #define FM25G01B_STATUS_ECC_UNCOR_ERROR (7 << 4) + #define FM25S01BI3_STATUS_ECC_MASK (7 << 4) #define FM25S01BI3_STATUS_ECC_NO_BITFLIPS (0 << 4) #define FM25S01BI3_STATUS_ECC_1_3_BITFLIPS (1 << 4) @@ -34,6 +44,66 @@ static SPINAND_OP_VARIANTS(update_cache_variants, SPINAND_PROG_LOAD_1S_1S_4S_OP(false, 0, NULL, 0), SPINAND_PROG_LOAD_1S_1S_1S_OP(false, 0, NULL, 0)); +static int fm25g01b_ooblayout_ecc(struct mtd_info *mtd, int section, + struct mtd_oob_region *region) +{ + if (section) + return -ERANGE; + + region->offset = 64; + region->length = 64; + + return 0; +} + +static int fm25g01b_ooblayout_free(struct mtd_info *mtd, int section, + struct mtd_oob_region *region) +{ + if (section) + return -ERANGE; + + /* reserve 2 bytes for the BBM */ + region->offset = 2; + region->length = 62; + + return 0; +} + +static int fm25g01b_ecc_get_status(struct spinand_device *spinand, + u8 status) +{ + switch (status & FM25G01B_STATUS_ECC_MASK) { + case FM25G01B_STATUS_ECC_NO_BITFLIPS: + return 0; + + case FM25G01B_STATUS_ECC_1_3_BITFLIPS: + return 3; + + case FM25G01B_STATUS_ECC_4_BITFLIPS: + return 4; + + case FM25G01B_STATUS_ECC_5_BITFLIPS: + return 5; + + case FM25G01B_STATUS_ECC_6_BITFLIPS: + return 6; + + case FM25G01B_STATUS_ECC_7_BITFLIPS: + return 7; + + case FM25G01B_STATUS_ECC_8_BITFLIPS: + return 8; + + case FM25G01B_STATUS_ECC_UNCOR_ERROR: + return -EBADMSG; + + default: + break; + } + + return -EINVAL; +} + static int fm25s01a_ooblayout_ecc(struct mtd_info *mtd, int section, struct mtd_oob_region *region) { @@ -102,6 +172,11 @@ static int fm25s01bi3_ooblayout_free(struct mtd_info *mtd, int section, return 0; } +static const struct mtd_ooblayout_ops fm25g01b_ooblayout = { + .ecc = fm25g01b_ooblayout_ecc, + .free = fm25g01b_ooblayout_free, +}; + static const struct mtd_ooblayout_ops fm25s01a_ooblayout = { .ecc = fm25s01a_ooblayout_ecc, .free = fm25s01a_ooblayout_free, @@ -113,6 +188,26 @@ static const struct mtd_ooblayout_ops fm25s01bi3_ooblayout = { }; static const struct spinand_info fmsh_spinand_table[] = { + SPINAND_INFO("FM25G01B", + SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xd1), + NAND_MEMORG(1, 2048, 128, 64, 1024, 21, 1, 1, 1), + NAND_ECCREQ(8, 528), + SPINAND_INFO_OP_VARIANTS(&read_cache_variants, + &write_cache_variants, + &update_cache_variants), + SPINAND_HAS_QE_BIT, + SPINAND_ECCINFO(&fm25g01b_ooblayout, + fm25g01b_ecc_get_status)), + SPINAND_INFO("FM25G02B", + SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xd2), + NAND_MEMORG(1, 2048, 128, 64, 2048, 41, 1, 1, 1), + NAND_ECCREQ(8, 528), + SPINAND_INFO_OP_VARIANTS(&read_cache_variants, + &write_cache_variants, + &update_cache_variants), + SPINAND_HAS_QE_BIT, + SPINAND_ECCINFO(&fm25g01b_ooblayout, + fm25g01b_ecc_get_status)), SPINAND_INFO("FM25S01A", SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xE4), NAND_MEMORG(1, 2048, 64, 64, 1024, 20, 1, 1, 1), From 591b5ac17301acfbc8204dfa377353f2b20efc5b Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Mon, 29 Jun 2026 18:23:32 -0700 Subject: [PATCH 06/21] mtd: rawnand: atmel: use struct_size The comment above makes it clear that this is a single element for legacy handling. Clarify that with struct_size and avoid manual pointer math. Signed-off-by: Rosen Penev Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/atmel/nand-controller.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/mtd/nand/raw/atmel/nand-controller.c b/drivers/mtd/nand/raw/atmel/nand-controller.c index e7fdf532c5fe..8a19408abb63 100644 --- a/drivers/mtd/nand/raw/atmel/nand-controller.c +++ b/drivers/mtd/nand/raw/atmel/nand-controller.c @@ -1799,8 +1799,7 @@ atmel_nand_controller_legacy_add_nands(struct atmel_nand_controller *nc) * Legacy bindings only allow connecting a single NAND with a unique CS * line to the controller. */ - nand = devm_kzalloc(nc->dev, sizeof(*nand) + sizeof(*nand->cs), - GFP_KERNEL); + nand = devm_kzalloc(nc->dev, struct_size(nand, cs, 1), GFP_KERNEL); if (!nand) return -ENOMEM; From adfc275b317c02cd043b0cf28b8cfb7459b041f0 Mon Sep 17 00:00:00 2001 From: Pengpeng Hou Date: Wed, 1 Jul 2026 13:39:09 +0800 Subject: [PATCH 07/21] mtd: parsers: redboot: reject unterminated FIS names RedBoot FIS partition names are stored in a fixed 16-byte field that is expected to be NUL-terminated. parse_redboot_partitions() used strlen() to size the names area and later copied the same field with strcpy(), so a malformed table entry without a terminator could make both operations read beyond the descriptor. Validate each accepted FIS name with strnlen() before adding it to the partition list. Signed-off-by: Pengpeng Hou Signed-off-by: Miquel Raynal --- drivers/mtd/parsers/redboot.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/mtd/parsers/redboot.c b/drivers/mtd/parsers/redboot.c index bf162c44eafe..120b2eab21fc 100644 --- a/drivers/mtd/parsers/redboot.c +++ b/drivers/mtd/parsers/redboot.c @@ -192,6 +192,7 @@ static int parse_redboot_partitions(struct mtd_info *master, for (i = 0; i < numslots; i++) { struct fis_list *new_fl, **prev; + size_t name_len; if (buf[i].name[0] == 0xff) { if (buf[i].name[1] == 0xff) { @@ -203,8 +204,14 @@ static int parse_redboot_partitions(struct mtd_info *master, if (!redboot_checksum(&buf[i])) break; + name_len = strnlen(buf[i].name, sizeof(buf[i].name)); + if (name_len == sizeof(buf[i].name)) { + ret = -EINVAL; + goto out; + } + new_fl = kmalloc_obj(struct fis_list); - namelen += strlen(buf[i].name) + 1; + namelen += name_len + 1; if (!new_fl) { ret = -ENOMEM; goto out; From 4529aababe4212b9b6d70e83ceb6a99f9691c811 Mon Sep 17 00:00:00 2001 From: Stephan Gerhold Date: Tue, 7 Jul 2026 13:56:01 +0200 Subject: [PATCH 08/21] dt-bindings: mtd: qcom,nandc: Add MDM9607 QPIC NAND controller Add the qcom,mdm9607-nand compatible for the QPIC NAND controller used inside the MDM9607 SoC. On MDM9607 and other recent SoCs, the QPIC hardware requires 3 clocks (core, aon, ahb). However, access to these clocks is restricted to the RPM firmware that controls the shared power resources for the whole SoC. The clocks cannot be controlled separately, for the OS view of the hardware there is only a single RPM_SMD_QPIC_CLK clock that implicitly enables all of the 3 clocks. The only exception to this are some IPQ* SoC that are not using RPM, there the clocks are directly controlled by the kernel via the clock controller (GCC). Require only one clock in the dt-bindings for MDM9607 to avoid having to define dummy clock entries. Reviewed-by: Krzysztof Kozlowski Reviewed-by: Manivannan Sadhasivam Signed-off-by: Stephan Gerhold Signed-off-by: Miquel Raynal --- .../devicetree/bindings/mtd/qcom,nandc.yaml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Documentation/devicetree/bindings/mtd/qcom,nandc.yaml b/Documentation/devicetree/bindings/mtd/qcom,nandc.yaml index 5511389960f0..a916cac53af6 100644 --- a/Documentation/devicetree/bindings/mtd/qcom,nandc.yaml +++ b/Documentation/devicetree/bindings/mtd/qcom,nandc.yaml @@ -22,17 +22,20 @@ properties: - qcom,ipq4019-nand - qcom,ipq6018-nand - qcom,ipq8074-nand + - qcom,mdm9607-nand - qcom,sdx55-nand reg: maxItems: 1 clocks: + minItems: 1 items: - description: Core Clock - description: Always ON Clock clock-names: + minItems: 1 items: - const: core - const: aon @@ -101,6 +104,27 @@ allOf: items: - const: rxtx + # On MDM9607, the OS can only control a single clock. + # The 3 hardware clocks (core, aon, ahb) are invisible to the OS. + - if: + properties: + compatible: + contains: + enum: + - qcom,mdm9607-nand + then: + properties: + clocks: + maxItems: 1 + clock-names: + maxItems: 1 + else: + properties: + clocks: + minItems: 2 + clock-names: + minItems: 2 + - if: properties: compatible: @@ -121,6 +145,7 @@ allOf: - qcom,ipq4019-nand - qcom,ipq6018-nand - qcom,ipq8074-nand + - qcom,mdm9607-nand - qcom,sdx55-nand then: From 533ec816312baecdb45bbcb8a279c6561aa71215 Mon Sep 17 00:00:00 2001 From: Stephan Gerhold Date: Tue, 7 Jul 2026 13:56:02 +0200 Subject: [PATCH 09/21] mtd: rawnand: qcom: Make "aon" clock optional Some SoCs (e.g. MDM9607, SDX55) have only a single separately controllable clock for the NAND controller. The actual clocks in the hardware are managed by the firmware and turned on all together when needed. In this case, there is no separate "aon" clock that can be described in the device tree from the OS point of view. Make the second "aon" clock optional to avoid an error when it is missing. For platforms that really need it, the dt-bindings are responsible for validating that. Reviewed-by: Krzysztof Kozlowski Reviewed-by: Manivannan Sadhasivam Signed-off-by: Stephan Gerhold Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/qcom_nandc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mtd/nand/raw/qcom_nandc.c b/drivers/mtd/nand/raw/qcom_nandc.c index 4b80ce084d9a..0251dd591d40 100644 --- a/drivers/mtd/nand/raw/qcom_nandc.c +++ b/drivers/mtd/nand/raw/qcom_nandc.c @@ -2280,7 +2280,7 @@ static int qcom_nandc_probe(struct platform_device *pdev) if (IS_ERR(nandc->core_clk)) return PTR_ERR(nandc->core_clk); - nandc->aon_clk = devm_clk_get(dev, "aon"); + nandc->aon_clk = devm_clk_get_optional(dev, "aon"); if (IS_ERR(nandc->aon_clk)) return PTR_ERR(nandc->aon_clk); From 0732595d2f8cf846be3327c8c9453eb10b10ed39 Mon Sep 17 00:00:00 2001 From: Stephan Gerhold Date: Tue, 7 Jul 2026 13:56:03 +0200 Subject: [PATCH 10/21] mtd: rawnand: qcom: Make has_onfi_read_op separate from qpic_version2 QPIC v1.5 requires using the OP_PAGE_READ_ONFI_READ command, but is missing the rest of the hardware changes that are currently covered by the QPIC v2 (qpic_version2) check in the driver. Split that into an extra has_onfi_read_op feature flag so it can be separately enabled. No functional change. Reviewed-by: Manivannan Sadhasivam Signed-off-by: Stephan Gerhold Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/qcom_nandc.c | 15 ++++++++------- include/linux/mtd/nand-qpic-common.h | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/drivers/mtd/nand/raw/qcom_nandc.c b/drivers/mtd/nand/raw/qcom_nandc.c index 0251dd591d40..9217e8de5512 100644 --- a/drivers/mtd/nand/raw/qcom_nandc.c +++ b/drivers/mtd/nand/raw/qcom_nandc.c @@ -1564,7 +1564,7 @@ static int qcom_op_cmd_mapping(struct nand_chip *chip, u8 opcode, cmd = OP_FETCH_ID; break; case NAND_CMD_PARAM: - if (nandc->props->qpic_version2) + if (nandc->props->has_onfi_read_op) cmd = OP_PAGE_READ_ONFI_READ; else cmd = OP_PAGE_READ; @@ -1903,7 +1903,7 @@ static int qcom_param_page_type_exec(struct nand_chip *chip, const struct nand_ nandc->regs->ecc_buf_cfg = cpu_to_le32(ECC_CFG_ECC_DISABLE); /* configure CMD1 and VLD for ONFI param probing in QPIC v1 */ - if (!nandc->props->qpic_version2) { + if (!nandc->props->has_onfi_read_op) { nandc->regs->vld = cpu_to_le32((nandc->vld & ~READ_START_VLD)); nandc->regs->cmd1 = cpu_to_le32((nandc->cmd1 & ~READ_ADDR_MASK) | FIELD_PREP(READ_ADDR_MASK, NAND_CMD_PARAM)); @@ -1911,7 +1911,7 @@ static int qcom_param_page_type_exec(struct nand_chip *chip, const struct nand_ nandc->regs->exec = cpu_to_le32(1); - if (!nandc->props->qpic_version2) { + if (!nandc->props->has_onfi_read_op) { nandc->regs->orig_cmd1 = cpu_to_le32(nandc->cmd1); nandc->regs->orig_vld = cpu_to_le32(nandc->vld); } @@ -1925,7 +1925,7 @@ static int qcom_param_page_type_exec(struct nand_chip *chip, const struct nand_ else nandc_set_read_loc_first(chip, reg_base, 0, len, 1); - if (!nandc->props->qpic_version2) { + if (!nandc->props->has_onfi_read_op) { qcom_write_reg_dma(nandc, &nandc->regs->vld, NAND_DEV_CMD_VLD, 1, 0); qcom_write_reg_dma(nandc, &nandc->regs->cmd1, NAND_DEV_CMD1, 1, NAND_BAM_NEXT_SGL); } @@ -1939,7 +1939,7 @@ static int qcom_param_page_type_exec(struct nand_chip *chip, const struct nand_ nandc->buf_count, 0); /* restore CMD1 and VLD regs */ - if (!nandc->props->qpic_version2) { + if (!nandc->props->has_onfi_read_op) { qcom_write_reg_dma(nandc, &nandc->regs->orig_cmd1, NAND_DEV_CMD1_RESTORE, 1, 0); qcom_write_reg_dma(nandc, &nandc->regs->orig_vld, NAND_DEV_CMD_VLD_RESTORE, 1, NAND_BAM_NEXT_SGL); @@ -2041,7 +2041,7 @@ static int qcom_nandc_setup(struct qcom_nand_controller *nandc) if (!nandc->props->nandc_part_of_qpic) nandc_write(nandc, SFLASHC_BURST_CFG, 0); - if (!nandc->props->qpic_version2) + if (!nandc->props->has_onfi_read_op) nandc_write(nandc, dev_cmd_reg_addr(nandc, NAND_DEV_CMD_VLD), NAND_DEV_CMD_VLD_VAL); @@ -2063,7 +2063,7 @@ static int qcom_nandc_setup(struct qcom_nand_controller *nandc) } /* save the original values of these registers */ - if (!nandc->props->qpic_version2) { + if (!nandc->props->has_onfi_read_op) { nandc->cmd1 = nandc_read(nandc, dev_cmd_reg_addr(nandc, NAND_DEV_CMD1)); nandc->vld = NAND_DEV_CMD_VLD_VAL; } @@ -2385,6 +2385,7 @@ static const struct qcom_nandc_props sdx55_nandc_props = { .ecc_modes = (ECC_BCH_4BIT | ECC_BCH_8BIT), .supports_bam = true, .nandc_part_of_qpic = true, + .has_onfi_read_op = true, .qpic_version2 = true, .dev_cmd_reg_start = 0x7000, .bam_offset = 0x30000, diff --git a/include/linux/mtd/nand-qpic-common.h b/include/linux/mtd/nand-qpic-common.h index 006ca8c978a9..437448995187 100644 --- a/include/linux/mtd/nand-qpic-common.h +++ b/include/linux/mtd/nand-qpic-common.h @@ -443,6 +443,7 @@ struct qcom_nand_controller { * @dev_cmd_reg_start - NAND_DEV_CMD_* registers starting offset * @supports_bam - whether NAND controller is using BAM * @nandc_part_of_qpic - whether NAND controller is part of qpic IP + * @has_onfi_read_op - whether ONFI param page read command is supported * @qpic_version2 - flag to indicate QPIC IP version 2 * @use_codeword_fixup - whether NAND has different layout for boot partitions */ @@ -452,6 +453,7 @@ struct qcom_nandc_props { u32 bam_offset; bool supports_bam; bool nandc_part_of_qpic; + bool has_onfi_read_op; bool qpic_version2; bool use_codeword_fixup; }; From 69e3c504e18bf1fec03fdf6293320860ec832af8 Mon Sep 17 00:00:00 2001 From: Stephan Gerhold Date: Tue, 7 Jul 2026 13:56:04 +0200 Subject: [PATCH 11/21] mtd: rawnand: qcom: Add MDM9607 compatible MDM9607 has QPIC v1.5 that supports the OP_PAGE_READ_ONFI_READ command, but is missing the rest of the hardware changes in QPIC v2. Add the new qcom,mdm9607-nand compatible and set it to use has_onfi_read_op without also setting qpic_version2. Reviewed-by: Manivannan Sadhasivam Signed-off-by: Stephan Gerhold Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/qcom_nandc.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/drivers/mtd/nand/raw/qcom_nandc.c b/drivers/mtd/nand/raw/qcom_nandc.c index 9217e8de5512..d7642db2e2df 100644 --- a/drivers/mtd/nand/raw/qcom_nandc.c +++ b/drivers/mtd/nand/raw/qcom_nandc.c @@ -2381,6 +2381,15 @@ static const struct qcom_nandc_props ipq8074_nandc_props = { .bam_offset = 0x30000, }; +static const struct qcom_nandc_props mdm9607_nandc_props = { + .ecc_modes = (ECC_BCH_4BIT | ECC_BCH_8BIT), + .supports_bam = true, + .nandc_part_of_qpic = true, + .has_onfi_read_op = true, + .dev_cmd_reg_start = 0x7000, + .bam_offset = 0x30000, +}; + static const struct qcom_nandc_props sdx55_nandc_props = { .ecc_modes = (ECC_BCH_4BIT | ECC_BCH_8BIT), .supports_bam = true, @@ -2412,6 +2421,10 @@ static const struct of_device_id qcom_nandc_of_match[] = { .compatible = "qcom,ipq8074-nand", .data = &ipq8074_nandc_props, }, + { + .compatible = "qcom,mdm9607-nand", + .data = &mdm9607_nandc_props, + }, { .compatible = "qcom,sdx55-nand", .data = &sdx55_nandc_props, From 5b2444b4d575d8117809c57801562ef37ca2d4af Mon Sep 17 00:00:00 2001 From: Pengpeng Hou Date: Wed, 8 Jul 2026 09:47:12 +0800 Subject: [PATCH 12/21] mtd: nand: realtek-ecc: add missing MODULE_DEVICE_TABLE() The Realtek external ECC engine driver has an OF match table wired into its platform driver, but the table is not exported with MODULE_DEVICE_TABLE(). When the driver is built as a module, the missing OF module alias prevents automatic module loading from the compatible string. Add the missing MODULE_DEVICE_TABLE() entry. Fixes: 3148d0e5b1c5 ("mtd: nand: realtek-ecc: Add Realtek external ECC engine support") Cc: stable@vger.kernel.org Signed-off-by: Pengpeng Hou Signed-off-by: Miquel Raynal --- drivers/mtd/nand/ecc-realtek.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/mtd/nand/ecc-realtek.c b/drivers/mtd/nand/ecc-realtek.c index 7d003fd72027..76d75e2107df 100644 --- a/drivers/mtd/nand/ecc-realtek.c +++ b/drivers/mtd/nand/ecc-realtek.c @@ -450,6 +450,7 @@ static const struct of_device_id rtl_ecc_of_ids[] = { }, { /* sentinel */ }, }; +MODULE_DEVICE_TABLE(of, rtl_ecc_of_ids); static struct platform_driver rtl_ecc_driver = { .driver = { From 0ee27d8e4c99765215d906b4ea1fb6bf7e9650d7 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Tue, 14 Jul 2026 19:18:00 -0600 Subject: [PATCH 13/21] mtd: rawnand: add Toshiba TC58NVG1S3H Without a full-ID entry, the non-ONFI TC58NVG1S3H falls back to the generic 0xda extended-ID decoding. It is therefore identified only as a generic 256 MiB Toshiba NAND. Without a model-specific interface configuration, the core leaves it in the conservative reset timing mode. Add its full five-byte ID with the datasheet geometry, OOB size and ECC requirements. Its interface timings match the mode-4-derived profile already used by the related TH58NVG2S3HBAI4 and TH58NVG3S0HBAI4, so share that profile and select it for the new model. Signed-off-by: James Hilliard Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/nand_ids.c | 3 +++ drivers/mtd/nand/raw/nand_toshiba.c | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/mtd/nand/raw/nand_ids.c b/drivers/mtd/nand/raw/nand_ids.c index 62a8cf86d9e2..08707ce9d7c0 100644 --- a/drivers/mtd/nand/raw/nand_ids.c +++ b/drivers/mtd/nand/raw/nand_ids.c @@ -29,6 +29,9 @@ struct nand_flash_dev nand_flash_ids[] = { {"TC58NVG0S3E 1G 3.3V 8-bit", { .id = {0x98, 0xd1, 0x90, 0x15, 0x76, 0x14, 0x01, 0x00} }, SZ_2K, SZ_128, SZ_128K, 0, 8, 64, NAND_ECC_INFO(1, SZ_512), }, + {"TC58NVG1S3H 2G 3.3V 8-bit", + { .id = {0x98, 0xda, 0x90, 0x15, 0x76} }, + SZ_2K, SZ_256, SZ_128K, 0, 5, 128, NAND_ECC_INFO(8, SZ_512) }, {"TC58NVG2S0F 4G 3.3V 8-bit", { .id = {0x98, 0xdc, 0x90, 0x26, 0x76, 0x15, 0x01, 0x08} }, SZ_4K, SZ_512, SZ_256K, 0, 8, 224, NAND_ECC_INFO(4, SZ_512) }, diff --git a/drivers/mtd/nand/raw/nand_toshiba.c b/drivers/mtd/nand/raw/nand_toshiba.c index d3d34d71921f..32062b26a1f6 100644 --- a/drivers/mtd/nand/raw/nand_toshiba.c +++ b/drivers/mtd/nand/raw/nand_toshiba.c @@ -287,7 +287,9 @@ static int toshiba_nand_init(struct nand_chip *chip) if (!strncmp("TC58NVG0S3E", chip->parameters.model, sizeof("TC58NVG0S3E") - 1)) tc58nvg0s3e_init(chip); - if ((!strncmp("TH58NVG2S3HBAI4", chip->parameters.model, + if ((!strncmp("TC58NVG1S3H", chip->parameters.model, + sizeof("TC58NVG1S3H") - 1)) || + (!strncmp("TH58NVG2S3HBAI4", chip->parameters.model, sizeof("TH58NVG2S3HBAI4") - 1)) || (!strncmp("TH58NVG3S0HBAI4", chip->parameters.model, sizeof("TH58NVG3S0HBAI4") - 1))) From f97bdc8ec1dc7b33781a702eeba55326c206be56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uwe=20Kleine-K=C3=B6nig=20=28The=20Capable=20Hub=29?= Date: Thu, 16 Jul 2026 21:25:23 +0200 Subject: [PATCH 14/21] mtd: nand-omap2: Move omap_nand_ids[] to raw nand driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defining a static array in a header results in each .c file that includes the header (here: drivers/memory/omap-gpmc.c and drivers/mtd/nand/raw/omap2.c) to contain a copy of that array when compiled to an object file. With sizeof(struct of_device_id[3]) ≥ 588 having omap_nand_ids[] twice just to do two string comparisons is quite some bloat. So move omap_nand_ids[] to the nand driver which actually needs that array for its module meta data and do the compatible check by hand. bloat-o-meter reports for drivers/memory/omap-gpmc.o (ARCH=arm): add/remove: 1/2 grow/shrink: 1/0 up/down: 28/-588 (-560) Function old new delta gpmc_probe_generic_child 2108 2136 +28 omap_nand_ids 588 - -588 Total: Before=18114, After=17554, chg -3.09% (drivers/mtd/nand/raw/omap2.o doesn't change). This allows to drop from include/linux/platform_data/mtd-nand-omap2.h (which is my original motivation for this change). Note that this header isn't needed in the two drivers because omap-gpmc.c doesn't use any device id struct and for the nand driver omap2.c of_device_id is already provided via . Signed-off-by: Uwe Kleine-König (The Capable Hub) Signed-off-by: Miquel Raynal --- drivers/memory/omap-gpmc.c | 9 ++++++++- drivers/mtd/nand/raw/omap2.c | 6 +++++- include/linux/platform_data/mtd-nand-omap2.h | 7 ------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/drivers/memory/omap-gpmc.c b/drivers/memory/omap-gpmc.c index 958d2b0ea54a..48af31a54b55 100644 --- a/drivers/memory/omap-gpmc.c +++ b/drivers/memory/omap-gpmc.c @@ -2129,6 +2129,13 @@ static void __maybe_unused gpmc_read_timings_dt(struct device_node *np, of_property_read_bool(np, "gpmc,time-para-granularity"); } +static int gpmc_child_is_nand(struct device_node *child) +{ + /* This has to match drivers/mtd/nand/raw/omap2.c's omap_nand_ids[] */ + return of_device_is_compatible(child, "ti,omap2-nand") || + of_device_is_compatible(child, "ti,am64-nand"); +} + /** * gpmc_probe_generic_child - configures the gpmc for a child device * @pdev: pointer to gpmc platform device @@ -2220,7 +2227,7 @@ static int gpmc_probe_generic_child(struct platform_device *pdev, goto err; } - if (of_match_node(omap_nand_ids, child)) { + if (gpmc_child_is_nand(child)) { /* NAND specific setup */ val = 8; of_property_read_u32(child, "nand-bus-width", &val); diff --git a/drivers/mtd/nand/raw/omap2.c b/drivers/mtd/nand/raw/omap2.c index 39e297486721..552f36da79fc 100644 --- a/drivers/mtd/nand/raw/omap2.c +++ b/drivers/mtd/nand/raw/omap2.c @@ -2318,7 +2318,11 @@ static void omap_nand_remove(struct platform_device *pdev) nand_cleanup(nand_chip); } -/* omap_nand_ids defined in linux/platform_data/mtd-nand-omap2.h */ +static const struct of_device_id omap_nand_ids[] = { + { .compatible = "ti,omap2-nand" }, + { .compatible = "ti,am64-nand" }, + { } +}; MODULE_DEVICE_TABLE(of, omap_nand_ids); static struct platform_driver omap_nand_driver = { diff --git a/include/linux/platform_data/mtd-nand-omap2.h b/include/linux/platform_data/mtd-nand-omap2.h index 8c2f1f185353..2ddb624b97fd 100644 --- a/include/linux/platform_data/mtd-nand-omap2.h +++ b/include/linux/platform_data/mtd-nand-omap2.h @@ -7,7 +7,6 @@ #define _MTD_NAND_OMAP2_H #include -#include #define GPMC_BCH_NUM_REMAINDER 8 @@ -63,10 +62,4 @@ struct gpmc_nand_regs { void __iomem *gpmc_bch_result6[GPMC_BCH_NUM_REMAINDER]; }; -static const struct of_device_id omap_nand_ids[] = { - { .compatible = "ti,omap2-nand", }, - { .compatible = "ti,am64-nand", }, - {}, -}; - #endif /* _MTD_NAND_OMAP2_H */ From 8211f2d74b356a02fe38aeea5b74030ac156461f Mon Sep 17 00:00:00 2001 From: Aleksandr Mineev Date: Fri, 17 Jul 2026 18:50:50 +0300 Subject: [PATCH 15/21] mtd: spinand: fmsh: fix FM25G01B/FM25G02B Quad I/O read dummy cycles The FM25G01B/FM25G02B datasheets specify a single dummy byte for the 0xEB Quad I/O read-from-cache operation, but the generic read_cache_variants set uses two dummy bytes for the 1S-4S-4S variant. The extra dummy byte shifts the data phase and returns corrupted data with no ECC error, breaking boot on boards using these chips. Use a dedicated read-from-cache variant set with ndummy=1 for the 1S-4S-4S (0xEB) operation. FM25G01B datasheet: https://www.fmsh.com/nvm/FM25G01B_ds_eng.pdf FM25G02B datasheet: https://www.fmsh.com/nvm/FM25G02B_ds_eng.pdf Fixes: d5a5c9eb2ee9 ("mtd: spinand: fmsh: add support for FM25G{01,02}B") Cc: stable@vger.kernel.org Signed-off-by: Aleksandr Mineev Signed-off-by: Miquel Raynal --- drivers/mtd/nand/spi/fmsh.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/mtd/nand/spi/fmsh.c b/drivers/mtd/nand/spi/fmsh.c index be5db9f9502b..6352e9c9e502 100644 --- a/drivers/mtd/nand/spi/fmsh.c +++ b/drivers/mtd/nand/spi/fmsh.c @@ -44,6 +44,14 @@ static SPINAND_OP_VARIANTS(update_cache_variants, SPINAND_PROG_LOAD_1S_1S_4S_OP(false, 0, NULL, 0), SPINAND_PROG_LOAD_1S_1S_1S_OP(false, 0, NULL, 0)); +static SPINAND_OP_VARIANTS(fm25g_read_cache_variants, + SPINAND_PAGE_READ_FROM_CACHE_1S_4S_4S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_1S_4S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_2S_2S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_1S_2S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_FAST_1S_1S_1S_OP(0, 1, NULL, 0, 0), + SPINAND_PAGE_READ_FROM_CACHE_1S_1S_1S_OP(0, 1, NULL, 0, 0)); + static int fm25g01b_ooblayout_ecc(struct mtd_info *mtd, int section, struct mtd_oob_region *region) { @@ -192,7 +200,7 @@ static const struct spinand_info fmsh_spinand_table[] = { SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xd1), NAND_MEMORG(1, 2048, 128, 64, 1024, 21, 1, 1, 1), NAND_ECCREQ(8, 528), - SPINAND_INFO_OP_VARIANTS(&read_cache_variants, + SPINAND_INFO_OP_VARIANTS(&fm25g_read_cache_variants, &write_cache_variants, &update_cache_variants), SPINAND_HAS_QE_BIT, @@ -202,7 +210,7 @@ static const struct spinand_info fmsh_spinand_table[] = { SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xd2), NAND_MEMORG(1, 2048, 128, 64, 2048, 41, 1, 1, 1), NAND_ECCREQ(8, 528), - SPINAND_INFO_OP_VARIANTS(&read_cache_variants, + SPINAND_INFO_OP_VARIANTS(&fm25g_read_cache_variants, &write_cache_variants, &update_cache_variants), SPINAND_HAS_QE_BIT, From 5ee37db34637ca6e33ce01d7062cb31275484404 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 17 Jul 2026 10:41:58 -0600 Subject: [PATCH 16/21] mtd: rawnand: sunxi: add H616 MBUS DMA support The H616 NAND controller uses a descriptor-based internal MBUS DMA engine instead of the direct address and count registers used by the A23/A33 controller. Since the driver does not support these descriptors, it currently attempts to request an external rxtx DMA channel and falls back to PIO when none is provided. Add a single-descriptor backend to the existing ECC page DMA paths. Allocate the descriptor coherently, constrain data mappings to the controller's 32-bit address range, program the H6-style data block mask, and request an interrupt for both command and DMA completion. Keep the existing external DMA and legacy MBUS DMA paths unchanged, and fall back to PIO if the descriptor cannot be allocated. With identical kernels except for this patch, running flash_speed -d -b 1906 -c 100 /dev/mtd6 on an H616 board with 2 KiB-page SLC NAND reported: PIO descriptor DMA eraseblock write 3365 KiB/s 4192 KiB/s eraseblock read 6454 KiB/s 16040 KiB/s page write 3254 KiB/s 4021 KiB/s page read 6419 KiB/s 15686 KiB/s Signed-off-by: James Hilliard Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/sunxi_nand.c | 120 ++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 16 deletions(-) diff --git a/drivers/mtd/nand/raw/sunxi_nand.c b/drivers/mtd/nand/raw/sunxi_nand.c index 02647565c8ba..6e22d82a77ea 100644 --- a/drivers/mtd/nand/raw/sunxi_nand.c +++ b/drivers/mtd/nand/raw/sunxi_nand.c @@ -79,6 +79,12 @@ #define NFC_REG_H6_MDMA_BUF_ADDR 0x0210 #define NFC_REG_H6_MDMA_CNT 0x0214 +#define NFC_H6_MDMA_STA_DESC0_COMPLETE BIT(0) + +#define NFC_MDMA_DESC_LAST BIT(2) +#define NFC_MDMA_DESC_FIRST BIT(3) +#define NFC_MDMA_DESC_SIZE_MASK GENMASK(15, 0) + #define NFC_RAM0_BASE 0x0400 #define NFC_RAM1_BASE 0x0800 @@ -267,12 +273,19 @@ static inline struct sunxi_nand_chip *to_sunxi_nand(struct nand_chip *nand) return container_of(nand, struct sunxi_nand_chip, nand); } +struct sunxi_nfc_mdma_desc { + __le32 config; + __le32 size; + __le32 buf; + __le32 next; +} __packed __aligned(4); + /* * NAND Controller capabilities structure: stores NAND controller capabilities * for distinction between compatible strings. * - * @has_mdma: Use mbus dma mode, otherwise general dma - * through MBUS on A23/A33 needs extra configuration. + * @has_mdma: Use A23/A33-style MBUS DMA registers + * @has_mdma_desc: MBUS DMA uses H6-style descriptors * @has_ecc_block_512: If the ECC can handle 512B or only 1024B chunks * @has_ecc_clk: If the controller needs an ECC clock. * @has_mbus_clk: If the controller needs a mbus clock. @@ -304,6 +317,7 @@ static inline struct sunxi_nand_chip *to_sunxi_nand(struct nand_chip *nand) */ struct sunxi_nfc_caps { bool has_mdma; + bool has_mdma_desc; bool has_ecc_block_512; bool has_ecc_clk; bool has_mbus_clk; @@ -346,6 +360,9 @@ struct sunxi_nfc_caps { * controller * @complete: a completion object used to wait for NAND controller events * @dmac: the DMA channel attached to the NAND controller + * @use_mdma: use an internal MBUS DMA backend + * @mdma_desc: H6-style MBUS DMA descriptor + * @mdma_desc_dma: DMA address of @mdma_desc * @caps: NAND Controller capabilities */ struct sunxi_nfc { @@ -362,6 +379,9 @@ struct sunxi_nfc { struct list_head chips; struct completion complete; struct dma_chan *dmac; + bool use_mdma; + struct sunxi_nfc_mdma_desc *mdma_desc; + dma_addr_t mdma_desc_dma; const struct sunxi_nfc_caps *caps; }; @@ -466,7 +486,10 @@ static int sunxi_nfc_dma_op_prepare(struct sunxi_nfc *nfc, const void *buf, { struct dma_async_tx_descriptor *dmad; enum dma_transfer_direction tdir; + dma_addr_t buf_dma; dma_cookie_t dmat; + int len = chunksize * nchunks; + u32 data_blocks = nchunks; int ret; if (ddir == DMA_FROM_DEVICE) @@ -474,12 +497,21 @@ static int sunxi_nfc_dma_op_prepare(struct sunxi_nfc *nfc, const void *buf, else tdir = DMA_MEM_TO_DEV; - sg_init_one(sg, buf, nchunks * chunksize); + sg_init_one(sg, buf, len); ret = dma_map_sg(nfc->dev, sg, 1, ddir); if (!ret) return -ENOMEM; - if (!nfc->caps->has_mdma) { + buf_dma = sg_dma_address(sg); + + if (nfc->mdma_desc && + (len > NFC_MDMA_DESC_SIZE_MASK || !IS_ALIGNED(len, 8) || + !IS_ALIGNED(buf_dma, 4))) { + ret = -EINVAL; + goto err_unmap_buf; + } + + if (!nfc->use_mdma) { dmad = dmaengine_prep_slave_sg(nfc->dmac, sg, 1, tdir, DMA_CTRL_ACK); if (!dmad) { ret = -EINVAL; @@ -489,14 +521,35 @@ static int sunxi_nfc_dma_op_prepare(struct sunxi_nfc *nfc, const void *buf, writel(readl(nfc->regs + NFC_REG_CTL) | NFC_RAM_METHOD, nfc->regs + NFC_REG_CTL); - writel(nchunks, nfc->regs + NFC_REG_SECTOR_NUM); + + /* H6/H616 use one enable bit per ECC data block. */ + if (nfc->caps->has_mdma_desc) + data_blocks = GENMASK(nchunks - 1, 0); + writel(data_blocks, nfc->regs + NFC_REG_SECTOR_NUM); writel(chunksize, nfc->regs + NFC_REG_CNT); - if (nfc->caps->has_mdma) { + if (nfc->use_mdma) writel(readl(nfc->regs + NFC_REG_CTL) & ~NFC_DMA_TYPE_NORMAL, nfc->regs + NFC_REG_CTL); - writel(chunksize * nchunks, nfc->regs + NFC_REG_MDMA_CNT); - writel(sg_dma_address(sg), nfc->regs + NFC_REG_MDMA_ADDR); + + if (nfc->mdma_desc) { + struct sunxi_nfc_mdma_desc *desc = nfc->mdma_desc; + + desc->config = cpu_to_le32(NFC_MDMA_DESC_FIRST | + NFC_MDMA_DESC_LAST); + desc->size = cpu_to_le32(len); + /* Descriptor words are little-endian DMA memory, not MMIO. */ + desc->buf = cpu_to_le32(buf_dma); + desc->next = cpu_to_le32(nfc->mdma_desc_dma); + + writel(NFC_H6_MDMA_STA_DESC0_COMPLETE, + nfc->regs + NFC_REG_H6_MDMA_STA); + dma_wmb(); + writel(nfc->mdma_desc_dma, + nfc->regs + NFC_REG_H6_MDMA_DLBA_REG); + } else if (nfc->caps->has_mdma) { + writel(len, nfc->regs + NFC_REG_MDMA_CNT); + writel(buf_dma, nfc->regs + NFC_REG_MDMA_ADDR); } else { dmat = dmaengine_submit(dmad); @@ -525,6 +578,14 @@ static void sunxi_nfc_dma_op_cleanup(struct sunxi_nfc *nfc, nfc->regs + NFC_REG_CTL); } +static void sunxi_nfc_dma_op_abort(struct sunxi_nfc *nfc) +{ + if (nfc->use_mdma) + sunxi_nfc_rst(nfc); + else + dmaengine_terminate_all(nfc->dmac); +} + static void sunxi_nfc_select_chip(struct nand_chip *nand, unsigned int cs) { struct mtd_info *mtd = nand_to_mtd(nand); @@ -1202,7 +1263,7 @@ static int sunxi_nfc_hw_ecc_read_chunks_dma(struct nand_chip *nand, uint8_t *buf wait = NFC_CMD_INT_FLAG; - if (nfc->caps->has_mdma) + if (nfc->use_mdma) wait |= NFC_DMA_INT_FLAG; else dma_async_issue_pending(nfc->dmac); @@ -1211,8 +1272,8 @@ static int sunxi_nfc_hw_ecc_read_chunks_dma(struct nand_chip *nand, uint8_t *buf nfc->regs + NFC_REG_CMD); ret = sunxi_nfc_wait_events(nfc, wait, false, 0); - if (ret && !nfc->caps->has_mdma) - dmaengine_terminate_all(nfc->dmac); + if (ret) + sunxi_nfc_dma_op_abort(nfc); sunxi_nfc_randomizer_disable(nand); sunxi_nfc_hw_ecc_disable(nand); @@ -1613,7 +1674,7 @@ static int sunxi_nfc_hw_ecc_write_page_dma(struct nand_chip *nand, wait = NFC_CMD_INT_FLAG; - if (nfc->caps->has_mdma) + if (nfc->use_mdma) wait |= NFC_DMA_INT_FLAG; else dma_async_issue_pending(nfc->dmac); @@ -1623,8 +1684,8 @@ static int sunxi_nfc_hw_ecc_write_page_dma(struct nand_chip *nand, nfc->regs + NFC_REG_CMD); ret = sunxi_nfc_wait_events(nfc, wait, false, 0); - if (ret && !nfc->caps->has_mdma) - dmaengine_terminate_all(nfc->dmac); + if (ret) + sunxi_nfc_dma_op_abort(nfc); sunxi_nfc_randomizer_disable(nand); sunxi_nfc_hw_ecc_disable(nand); @@ -2073,11 +2134,13 @@ static int sunxi_nand_hw_ecc_ctrl_init(struct nand_chip *nand, ecc->write_oob = sunxi_nfc_hw_ecc_write_oob; mtd_set_ooblayout(mtd, &sunxi_nand_ooblayout_ops); - if (nfc->dmac || nfc->caps->has_mdma) { + if (nfc->dmac || nfc->use_mdma) { ecc->read_page = sunxi_nfc_hw_ecc_read_page_dma; ecc->read_subpage = sunxi_nfc_hw_ecc_read_subpage_dma; ecc->write_page = sunxi_nfc_hw_ecc_write_page_dma; nand->options |= NAND_USES_DMA; + if (nfc->mdma_desc) + nand->buf_align = 4; } else { ecc->read_page = sunxi_nfc_hw_ecc_read_page; ecc->read_subpage = sunxi_nfc_hw_ecc_read_subpage; @@ -2426,8 +2489,32 @@ static int sunxi_nfc_dma_init(struct sunxi_nfc *nfc, struct resource *r) { int ret; - if (nfc->caps->has_mdma) + if (nfc->caps->has_mdma_desc) { + ret = dma_set_mask_and_coherent(nfc->dev, DMA_BIT_MASK(32)); + if (ret) { + dev_warn(nfc->dev, + "failed to set MBUS DMA mask, using PIO: %d\n", + ret); + return 0; + } + + nfc->mdma_desc = + dmam_alloc_coherent(nfc->dev, sizeof(*nfc->mdma_desc), + &nfc->mdma_desc_dma, GFP_KERNEL); + if (!nfc->mdma_desc) { + dev_warn(nfc->dev, + "failed to allocate MBUS DMA descriptor, using PIO\n"); + return 0; + } + + nfc->use_mdma = true; return 0; + } + + if (nfc->caps->has_mdma) { + nfc->use_mdma = true; + return 0; + } nfc->dmac = dma_request_chan(nfc->dev, "rxtx"); if (IS_ERR(nfc->dmac)) { @@ -2620,6 +2707,7 @@ static const struct sunxi_nfc_caps sunxi_nfc_a23_caps = { }; static const struct sunxi_nfc_caps sunxi_nfc_h616_caps = { + .has_mdma_desc = true, .has_ecc_clk = true, .has_mbus_clk = true, .reg_io_data = NFC_REG_A23_IO_DATA, From e5e415262330bd70f983e091d8919d9dcd99e475 Mon Sep 17 00:00:00 2001 From: Pengpeng Hou Date: Mon, 20 Jul 2026 19:57:25 +0800 Subject: [PATCH 17/21] mtd: rawnand: validate ONFI extended parameter page sections nand_flash_detect_ext_param_page() allocates the length declared by the ONFI parameter page, then treats the data as a fixed header followed by variable-length sections. It reads that header and advances over sections without first proving that the fixed page and each current section fit in the allocation. Reject pages shorter than the fixed header, track the remaining variable area while walking sections, and require the ECC section to contain every field read from struct onfi_ext_ecc_info. Use device-scoped diagnostics that identify the malformed ONFI section. Fixes: 6dcbe0cdd83f ("mtd: get the ECC info from the Extended Parameter Page") Cc: stable@vger.kernel.org Signed-off-by: Pengpeng Hou Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/nand_onfi.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/drivers/mtd/nand/raw/nand_onfi.c b/drivers/mtd/nand/raw/nand_onfi.c index cd3ad373883e..b5e304c35738 100644 --- a/drivers/mtd/nand/raw/nand_onfi.c +++ b/drivers/mtd/nand/raw/nand_onfi.c @@ -35,16 +35,21 @@ static int nand_flash_detect_ext_param_page(struct nand_chip *chip, struct nand_onfi_params *p) { struct nand_device *base = &chip->base; + struct mtd_info *mtd = nand_to_mtd(chip); struct nand_ecc_props requirements; struct onfi_ext_param_page *ep; struct onfi_ext_section *s; struct onfi_ext_ecc_info *ecc; + size_t remaining, section_len; uint8_t *cursor; int ret; int len; int i; len = le16_to_cpu(p->ext_param_page_length) * 16; + if (len < sizeof(*ep)) + return -EINVAL; + ep = kmalloc(len, GFP_KERNEL); if (!ep) return -ENOMEM; @@ -77,11 +82,29 @@ static int nand_flash_detect_ext_param_page(struct nand_chip *chip, /* find the ECC section. */ cursor = (uint8_t *)(ep + 1); + remaining = len - sizeof(*ep); for (i = 0; i < ONFI_EXT_SECTION_MAX; i++) { s = ep->sections + i; - if (s->type == ONFI_SECTION_TYPE_2) + section_len = s->length * 16; + if (section_len > remaining) { + dev_dbg(&mtd->dev, + "ONFI extended parameter section %d exceeds page\n", + i); + goto ext_out; + } + + if (s->type == ONFI_SECTION_TYPE_2) { + if (section_len < sizeof(*ecc)) { + dev_dbg(&mtd->dev, + "ONFI extended parameter ECC section %d is too short\n", + i); + goto ext_out; + } break; - cursor += s->length * 16; + } + + cursor += section_len; + remaining -= section_len; } if (i == ONFI_EXT_SECTION_MAX) { pr_debug("We can not find the ECC section.\n"); From 113f62225e98febabba0fee00afd29d4eaf65abb Mon Sep 17 00:00:00 2001 From: Han Xu Date: Wed, 29 Jul 2026 14:22:20 -0500 Subject: [PATCH 18/21] mtd: rawnand: gpmi: add debugfs entry for BCH geometry Export the BCH geometry parameters via debugfs to aid debugging and provide the necessary information for legacy kobs-ng tool. The debugfs directory "gpmi-nand" is created under the root debugfs tree, exposing: bch_geometry - blob containing the struct bch_geometry fields raw_mode - flag indicating raw mode status The implementation is guarded with #ifdef CONFIG_DEBUG_FS to avoid build failures when debugfs is disabled. Signed-off-by: Han Xu Reviewed-by: Frank Li Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.c | 29 ++++++++++++++++++++++ drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.h | 6 +++++ 2 files changed, 35 insertions(+) diff --git a/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.c b/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.c index c1f766cb225a..527165ccc839 100644 --- a/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.c +++ b/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.c @@ -7,6 +7,7 @@ */ #include #include +#include #include #include #include @@ -732,6 +733,31 @@ static int common_nfc_set_geometry(struct gpmi_nand_data *this) return err; } +#ifdef CONFIG_DEBUG_FS +static void bch_remove_debugfs(void *data) +{ + struct gpmi_nand_data *this = data; + + debugfs_remove_recursive(this->dbg_root); + this->dbg_root = NULL; +} + +static void bch_create_debugfs(struct gpmi_nand_data *this) +{ + struct bch_geometry *bch_geo = &this->bch_geometry; + + this->dbg_root = debugfs_create_dir("gpmi-nand", NULL); + this->dbg_bch_geo.data = (void *)bch_geo; + this->dbg_bch_geo.size = sizeof(struct bch_geometry); + this->raw_mode = true; + debugfs_create_blob("bch_geometry", 0444, this->dbg_root, &this->dbg_bch_geo); + debugfs_create_bool("raw_mode", 0444, this->dbg_root, &this->raw_mode); + devm_add_action_or_reset(this->dev, bch_remove_debugfs, this); +} +#else +static void bch_create_debugfs(struct gpmi_nand_data *this) {} +#endif /* CONFIG_DEBUG_FS */ + /* Configures the geometry for BCH. */ static int bch_set_geometry(struct gpmi_nand_data *this) { @@ -2282,6 +2308,9 @@ static int gpmi_init_last(struct gpmi_nand_data *this) if (ret) return ret; + /* save BCH geometry to debugfs if CONFIG_DEBUG_FS is enabled */ + bch_create_debugfs(this); + /* Init the nand_ecc_ctrl{} */ ecc->read_page = gpmi_ecc_read_page; ecc->write_page = gpmi_ecc_write_page; diff --git a/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.h b/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.h index 3e9bc985e44a..80c32efbaef8 100644 --- a/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.h +++ b/drivers/mtd/nand/raw/gpmi-nand/gpmi-nand.h @@ -161,6 +161,12 @@ struct gpmi_nand_data { #define DMA_CHANS 8 struct dma_chan *dma_chans[DMA_CHANS]; struct completion dma_done; + +#ifdef CONFIG_DEBUG_FS + struct dentry *dbg_root; + struct debugfs_blob_wrapper dbg_bch_geo; + bool raw_mode; +#endif }; /* BCH : Status Block Completion Codes */ From 9f2e033754c6c023150f184b4e72bbcc5d6eea13 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 7 Aug 2026 09:38:59 -0600 Subject: [PATCH 19/21] mtd: rawnand: sunxi: group controller delay tables The tWB and tRHW timing field encodings are controller properties, but they currently live in standalone lookup tables. Group them in a timing descriptor selected through the controller capability data. Point every existing controller at the legacy values so this is a pure preparation change. Fixes: 88fd4e4deae8 ("mtd: rawnand: sunxi: Add support for H616 nand controller") Cc: stable@vger.kernel.org Signed-off-by: James Hilliard Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/sunxi_nand.c | 36 ++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/drivers/mtd/nand/raw/sunxi_nand.c b/drivers/mtd/nand/raw/sunxi_nand.c index 6e22d82a77ea..4434221ad841 100644 --- a/drivers/mtd/nand/raw/sunxi_nand.c +++ b/drivers/mtd/nand/raw/sunxi_nand.c @@ -243,6 +243,14 @@ struct sunxi_nand_hw_ecc { u32 ecc_ctl; }; +#define SUNXI_NFC_TIMING_STEPS 4 + +/* Delay arrays contain internal NDFC clock cycles for field values 0 to 3. */ +struct sunxi_nfc_timings { + s32 tWB[SUNXI_NFC_TIMING_STEPS]; + s32 tRHW[SUNXI_NFC_TIMING_STEPS]; +}; + /** * struct sunxi_nand_chip - stores NAND chip device related information * @@ -314,6 +322,7 @@ struct sunxi_nfc_mdma_desc { * bytes to write * @nuser_data_tab: Size of @user_data_len_tab * @sram_size: Size of the NAND controller SRAM + * @timings: Controller timing characteristics */ struct sunxi_nfc_caps { bool has_mdma; @@ -341,6 +350,7 @@ struct sunxi_nfc_caps { unsigned int nuser_data_tab; unsigned int max_ecc_steps; int sram_size; + const struct sunxi_nfc_timings *timings; }; /** @@ -1728,8 +1738,10 @@ static int sunxi_nfc_hw_ecc_write_oob(struct nand_chip *nand, int page) return nand_prog_page_end_op(nand); } -static const s32 tWB_lut[] = {6, 12, 16, 20}; -static const s32 tRHW_lut[] = {4, 8, 12, 20}; +static const struct sunxi_nfc_timings sun4i_a10_nfc_timings = { + .tWB = { 6, 12, 16, 20 }, + .tRHW = { 4, 8, 12, 20 }, +}; static int _sunxi_nand_lookup_timing(const s32 *lut, int lut_size, u32 duration, u32 clk_period) @@ -1754,6 +1766,7 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, { struct sunxi_nand_chip *sunxi_nand = to_sunxi_nand(nand); struct sunxi_nfc *nfc = to_sunxi_nfc(sunxi_nand->nand.controller); + const struct sunxi_nfc_timings *nfc_timings = nfc->caps->timings; const struct nand_sdr_timings *timings; u32 min_clk_period = 0; s32 tWB, tADL, tWHR, tRHW, tCAD; @@ -1824,8 +1837,10 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, min_clk_period = DIV_ROUND_UP(timings->tWC_min, 2); /* T16 - T19 + tCAD */ - if (timings->tWB_max > (min_clk_period * 20)) - min_clk_period = DIV_ROUND_UP(timings->tWB_max, 20); + if (timings->tWB_max > + (min_clk_period * nfc_timings->tWB[SUNXI_NFC_TIMING_STEPS - 1])) + min_clk_period = DIV_ROUND_UP(timings->tWB_max, + nfc_timings->tWB[SUNXI_NFC_TIMING_STEPS - 1]); if (timings->tADL_min > (min_clk_period * 32)) min_clk_period = DIV_ROUND_UP(timings->tADL_min, 32); @@ -1833,8 +1848,10 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, if (timings->tWHR_min > (min_clk_period * 32)) min_clk_period = DIV_ROUND_UP(timings->tWHR_min, 32); - if (timings->tRHW_min > (min_clk_period * 20)) - min_clk_period = DIV_ROUND_UP(timings->tRHW_min, 20); + if (timings->tRHW_min > + (min_clk_period * nfc_timings->tRHW[SUNXI_NFC_TIMING_STEPS - 1])) + min_clk_period = DIV_ROUND_UP(timings->tRHW_min, + nfc_timings->tRHW[SUNXI_NFC_TIMING_STEPS - 1]); /* * In non-EDO, tREA should be less than tRP to guarantee that the @@ -1850,7 +1867,7 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, if (timings->tREA_max > min_clk_period && !timings->tRLOH_min) min_clk_period = timings->tREA_max; - tWB = sunxi_nand_lookup_timing(tWB_lut, timings->tWB_max, + tWB = sunxi_nand_lookup_timing(nfc_timings->tWB, timings->tWB_max, min_clk_period); if (tWB < 0) { dev_err(nfc->dev, "unsupported tWB\n"); @@ -1869,7 +1886,7 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, return -EINVAL; } - tRHW = sunxi_nand_lookup_timing(tRHW_lut, timings->tRHW_min, + tRHW = sunxi_nand_lookup_timing(nfc_timings->tRHW, timings->tRHW_min, min_clk_period); if (tRHW < 0) { dev_err(nfc->dev, "unsupported tRHW\n"); @@ -2682,6 +2699,7 @@ static const struct sunxi_nfc_caps sunxi_nfc_a10_caps = { .nstrengths = ARRAY_SIZE(sunxi_ecc_strengths_a10), .max_ecc_steps = 16, .sram_size = 1024, + .timings = &sun4i_a10_nfc_timings, }; static const struct sunxi_nfc_caps sunxi_nfc_a23_caps = { @@ -2704,6 +2722,7 @@ static const struct sunxi_nfc_caps sunxi_nfc_a23_caps = { .nstrengths = ARRAY_SIZE(sunxi_ecc_strengths_a10), .max_ecc_steps = 16, .sram_size = 1024, + .timings = &sun4i_a10_nfc_timings, }; static const struct sunxi_nfc_caps sunxi_nfc_h616_caps = { @@ -2729,6 +2748,7 @@ static const struct sunxi_nfc_caps sunxi_nfc_h616_caps = { .nuser_data_tab = ARRAY_SIZE(sunxi_user_data_len_h6), .max_ecc_steps = 32, .sram_size = 8192, + .timings = &sun4i_a10_nfc_timings, }; static const struct of_device_id sunxi_nfc_ids[] = { From 147f2a5743f8864bfc265654b1856af11c8c0031 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 7 Aug 2026 09:39:00 -0600 Subject: [PATCH 20/21] mtd: rawnand: sunxi: describe tADL and tWHR delays The tADL and tWHR timing fields use four encoded delays, but the driver currently derives their values with a shift. This hides the actual controller timing characteristics and lets the clock solver select a 32-cycle delay that the fields cannot encode. Describe the legacy 7, 15, 23 and 31 cycle thresholds explicitly and use the tables for both clock selection and field lookup. This prepares the driver for controllers with different encodings. Fixes: 88fd4e4deae8 ("mtd: rawnand: sunxi: Add support for H616 nand controller") Cc: stable@vger.kernel.org Signed-off-by: James Hilliard Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/sunxi_nand.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/drivers/mtd/nand/raw/sunxi_nand.c b/drivers/mtd/nand/raw/sunxi_nand.c index 4434221ad841..c37d5a280361 100644 --- a/drivers/mtd/nand/raw/sunxi_nand.c +++ b/drivers/mtd/nand/raw/sunxi_nand.c @@ -248,6 +248,8 @@ struct sunxi_nand_hw_ecc { /* Delay arrays contain internal NDFC clock cycles for field values 0 to 3. */ struct sunxi_nfc_timings { s32 tWB[SUNXI_NFC_TIMING_STEPS]; + s32 tADL[SUNXI_NFC_TIMING_STEPS]; + s32 tWHR[SUNXI_NFC_TIMING_STEPS]; s32 tRHW[SUNXI_NFC_TIMING_STEPS]; }; @@ -1740,6 +1742,8 @@ static int sunxi_nfc_hw_ecc_write_oob(struct nand_chip *nand, int page) static const struct sunxi_nfc_timings sun4i_a10_nfc_timings = { .tWB = { 6, 12, 16, 20 }, + .tADL = { 7, 15, 23, 31 }, + .tWHR = { 7, 15, 23, 31 }, .tRHW = { 4, 8, 12, 20 }, }; @@ -1842,11 +1846,15 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, min_clk_period = DIV_ROUND_UP(timings->tWB_max, nfc_timings->tWB[SUNXI_NFC_TIMING_STEPS - 1]); - if (timings->tADL_min > (min_clk_period * 32)) - min_clk_period = DIV_ROUND_UP(timings->tADL_min, 32); + if (timings->tADL_min > + (min_clk_period * nfc_timings->tADL[SUNXI_NFC_TIMING_STEPS - 1])) + min_clk_period = DIV_ROUND_UP(timings->tADL_min, + nfc_timings->tADL[SUNXI_NFC_TIMING_STEPS - 1]); - if (timings->tWHR_min > (min_clk_period * 32)) - min_clk_period = DIV_ROUND_UP(timings->tWHR_min, 32); + if (timings->tWHR_min > + (min_clk_period * nfc_timings->tWHR[SUNXI_NFC_TIMING_STEPS - 1])) + min_clk_period = DIV_ROUND_UP(timings->tWHR_min, + nfc_timings->tWHR[SUNXI_NFC_TIMING_STEPS - 1]); if (timings->tRHW_min > (min_clk_period * nfc_timings->tRHW[SUNXI_NFC_TIMING_STEPS - 1])) @@ -1874,16 +1882,18 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, return tWB; } - tADL = DIV_ROUND_UP(timings->tADL_min, min_clk_period) >> 3; - if (tADL > 3) { + tADL = sunxi_nand_lookup_timing(nfc_timings->tADL, + timings->tADL_min, min_clk_period); + if (tADL < 0) { dev_err(nfc->dev, "unsupported tADL\n"); - return -EINVAL; + return tADL; } - tWHR = DIV_ROUND_UP(timings->tWHR_min, min_clk_period) >> 3; - if (tWHR > 3) { + tWHR = sunxi_nand_lookup_timing(nfc_timings->tWHR, + timings->tWHR_min, min_clk_period); + if (tWHR < 0) { dev_err(nfc->dev, "unsupported tWHR\n"); - return -EINVAL; + return tWHR; } tRHW = sunxi_nand_lookup_timing(nfc_timings->tRHW, timings->tRHW_min, From 15a3cbce32994141252bb4ecfe3ff3a5d22d0b4f Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 7 Aug 2026 09:39:01 -0600 Subject: [PATCH 21/21] mtd: rawnand: sunxi: fix H6/H616 controller timings The NAND timing calculation assumes that command and address setup and hold intervals T1-T4, T7 and T11 each take one controller clock. It also uses the original A10 delay encodings for tWB, tADL, tWHR and tRHW. The H6/H616 NDFC defines the setup and hold intervals as two internal clock cycles and uses different delay encodings. Add the H616 timing characteristics and select them through the controller capability data so the clock solver and timing fields match the hardware. Fixes: 88fd4e4deae8 ("mtd: rawnand: sunxi: Add support for H616 nand controller") Cc: stable@vger.kernel.org Signed-off-by: James Hilliard Signed-off-by: Miquel Raynal --- drivers/mtd/nand/raw/sunxi_nand.c | 49 +++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/drivers/mtd/nand/raw/sunxi_nand.c b/drivers/mtd/nand/raw/sunxi_nand.c index c37d5a280361..45ccbce91551 100644 --- a/drivers/mtd/nand/raw/sunxi_nand.c +++ b/drivers/mtd/nand/raw/sunxi_nand.c @@ -247,6 +247,8 @@ struct sunxi_nand_hw_ecc { /* Delay arrays contain internal NDFC clock cycles for field values 0 to 3. */ struct sunxi_nfc_timings { + /* Internal clock cycles used by T1-T4, T7 and T11. */ + u8 setup_cycles; s32 tWB[SUNXI_NFC_TIMING_STEPS]; s32 tADL[SUNXI_NFC_TIMING_STEPS]; s32 tWHR[SUNXI_NFC_TIMING_STEPS]; @@ -1741,12 +1743,21 @@ static int sunxi_nfc_hw_ecc_write_oob(struct nand_chip *nand, int page) } static const struct sunxi_nfc_timings sun4i_a10_nfc_timings = { + .setup_cycles = 1, .tWB = { 6, 12, 16, 20 }, .tADL = { 7, 15, 23, 31 }, .tWHR = { 7, 15, 23, 31 }, .tRHW = { 4, 8, 12, 20 }, }; +static const struct sunxi_nfc_timings sun50i_h616_nfc_timings = { + .setup_cycles = 2, + .tWB = { 28, 44, 60, 76 }, + .tADL = { 0, 12, 28, 44 }, + .tWHR = { 0, 12, 28, 44 }, + .tRHW = { 8, 24, 40, 56 }, +}; + static int _sunxi_nand_lookup_timing(const s32 *lut, int lut_size, u32 duration, u32 clk_period) { @@ -1781,20 +1792,28 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, return -ENOTSUPP; /* T1 <=> tCLS */ - if (timings->tCLS_min > min_clk_period) - min_clk_period = timings->tCLS_min; + if (timings->tCLS_min > + min_clk_period * nfc_timings->setup_cycles) + min_clk_period = DIV_ROUND_UP(timings->tCLS_min, + nfc_timings->setup_cycles); /* T2 <=> tCLH */ - if (timings->tCLH_min > min_clk_period) - min_clk_period = timings->tCLH_min; + if (timings->tCLH_min > + min_clk_period * nfc_timings->setup_cycles) + min_clk_period = DIV_ROUND_UP(timings->tCLH_min, + nfc_timings->setup_cycles); /* T3 <=> tCS */ - if (timings->tCS_min > min_clk_period) - min_clk_period = timings->tCS_min; + if (timings->tCS_min > + min_clk_period * nfc_timings->setup_cycles) + min_clk_period = DIV_ROUND_UP(timings->tCS_min, + nfc_timings->setup_cycles); /* T4 <=> tCH */ - if (timings->tCH_min > min_clk_period) - min_clk_period = timings->tCH_min; + if (timings->tCH_min > + min_clk_period * nfc_timings->setup_cycles) + min_clk_period = DIV_ROUND_UP(timings->tCH_min, + nfc_timings->setup_cycles); /* T5 <=> tWP */ if (timings->tWP_min > min_clk_period) @@ -1805,8 +1824,10 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, min_clk_period = timings->tWH_min; /* T7 <=> tALS */ - if (timings->tALS_min > min_clk_period) - min_clk_period = timings->tALS_min; + if (timings->tALS_min > + min_clk_period * nfc_timings->setup_cycles) + min_clk_period = DIV_ROUND_UP(timings->tALS_min, + nfc_timings->setup_cycles); /* T8 <=> tDS */ if (timings->tDS_min > min_clk_period) @@ -1821,8 +1842,10 @@ static int sunxi_nfc_setup_interface(struct nand_chip *nand, int csline, min_clk_period = DIV_ROUND_UP(timings->tRR_min, 3); /* T11 <=> tALH */ - if (timings->tALH_min > min_clk_period) - min_clk_period = timings->tALH_min; + if (timings->tALH_min > + min_clk_period * nfc_timings->setup_cycles) + min_clk_period = DIV_ROUND_UP(timings->tALH_min, + nfc_timings->setup_cycles); /* T12 <=> tRP */ if (timings->tRP_min > min_clk_period) @@ -2758,7 +2781,7 @@ static const struct sunxi_nfc_caps sunxi_nfc_h616_caps = { .nuser_data_tab = ARRAY_SIZE(sunxi_user_data_len_h6), .max_ecc_steps = 32, .sram_size = 8192, - .timings = &sun4i_a10_nfc_timings, + .timings = &sun50i_h616_nfc_timings, }; static const struct of_device_id sunxi_nfc_ids[] = {