[PATCH v1 05/12] mtd: spi-nand: add EN75 BMT bad-block management support

AK Sharma <[email protected]>
Newsgroups org.u-boot-project.lists.u-boot
Message-ID <[email protected]>
Add read support for the EcoNet/Airoha BMT/BBT (bad-block management
table) layout used by the EN75xx vendor bootloaders, so U-Boot maps the
SPI-NAND logical blocks the same way as the vendor firmware.

Signed-off-by: AK Sharma <[email protected]>
---
 drivers/mtd/nand/spi/Kconfig    |   8 +
 drivers/mtd/nand/spi/Makefile   |   1 +
 drivers/mtd/nand/spi/core.c     |   9 +-
 drivers/mtd/nand/spi/en75_bmt.c | 665 ++++++++++++++++++++++++++++++++
 drivers/mtd/nand/spi/en75_bmt.h |  24 ++
 5 files changed, 706 insertions(+), 1 deletion(-)
 create mode 100644 drivers/mtd/nand/spi/en75_bmt.c
 create mode 100644 drivers/mtd/nand/spi/en75_bmt.h

diff --git a/drivers/mtd/nand/spi/Kconfig b/drivers/mtd/nand/spi/Kconfig
index 1124dada..09531914 100644
--- a/drivers/mtd/nand/spi/Kconfig
+++ b/drivers/mtd/nand/spi/Kconfig
@@ -6,3 +6,11 @@ menuconfig MTD_SPI_NAND
 	select SPI_MEM
 	help
 	  This is the framework for the SPI NAND device drivers.
+
+config MTD_EN75_BMT
+	bool "EcoNet EN75xx vendor BMT/BBT translation (read-only)"
+	depends on MTD_SPI_NAND
+	help
+	  Honour the EcoNet/Airoha factory BBT and BMT so logical NAND
+	  addresses match the bootbase and Linux. Read-only: never rewrite
+	  the tables.
diff --git a/drivers/mtd/nand/spi/Makefile b/drivers/mtd/nand/spi/Makefile
index a7a0b2cb..20aa718d 100644
--- a/drivers/mtd/nand/spi/Makefile
+++ b/drivers/mtd/nand/spi/Makefile
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 
 spinand-objs := core.o otp.o
+spinand-objs += $(if $(CONFIG_MTD_EN75_BMT),en75_bmt.o)
 spinand-objs += alliancememory.o ato.o esmt.o fmsh.o foresee.o gigadevice.o macronix.o
 spinand-objs += 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 14af4264..29fdefe2 100644
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -33,6 +33,7 @@
 #include <linux/mtd/spinand.h>
 #include <linux/printk.h>
 #include <linux/delay.h>
+#include "en75_bmt.h"
 #endif
 
 struct spinand_plat {
@@ -1703,15 +1704,21 @@ static int spinand_probe(struct udevice *dev)
 #ifndef __UBOOT__
 	ret = mtd_device_register(mtd, NULL, 0);
 #else
+	ret = en75_bmt_attach(mtd);
+	if (ret)
+		goto err_spinand_cleanup;
+
 	ret = add_mtd_device(mtd);
 #endif
 	if (ret)
-		goto err_spinand_cleanup;
+		goto err_bmt_detach;
 
 	plat->mtd = mtd;
 
 	return 0;
 
+err_bmt_detach:
+	en75_bmt_detach(mtd);
 err_spinand_cleanup:
 	spinand_cleanup(spinand);
 
diff --git a/drivers/mtd/nand/spi/en75_bmt.c b/drivers/mtd/nand/spi/en75_bmt.c
new file mode 100644
index 00000000..1cb3ca01
--- /dev/null
+++ b/drivers/mtd/nand/spi/en75_bmt.c
@@ -0,0 +1,665 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * EcoNet/Airoha EN75xx vendor BMT/BBT translation layer (READ-ONLY).
+ *
+ * The vendor bootbase and the Linux en75_bmt driver do not "skip" bad NAND
+ * blocks the way stock MTD does - they *remap* them into a reserve pool kept at
+ * the top of the chip, so that a logical block keeps a stable address after a
+ * block goes bad. A bootloader that ignores this reads the wrong data on any
+ * device that has ever remapped a block.
+ *
+ * This is a deliberately read-only port of that scheme:
+ *
+ *   - the factory Bad Block Table ("RAWB") and the Block Mapping Table ("BMT")
+ *     are located, checksum-verified and parsed,
+ *   - every MTD access is translated logical -> physical exactly as the vendor
+ *     does (BBT shift-up, then BMT lookup),
+ *   - mtd->size is reduced to the user area so partitions land where Linux and
+ *     the bootbase expect them,
+ *   - *nothing is ever written to the tables*. If a block turns bad we report
+ *     the error upward instead of allocating a replacement; leave repair to
+ *     Linux, which implements the full scheme.
+ *
+ * Deliberately NOT ported: BBT reconstruction / factory-BBT writing. Rewriting
+ * the factory BBT shifts the physical address of every block above the first
+ * bad one, which in a bootloader means bricking the board. Omission is the
+ * safest form of "disabled".
+ *
+ * Derived from the Linux drivers by Caleb James DeLisle:
+ *   target/linux/econet/files/drivers/mtd/nand/en75_bmt.c   (back end)
+ *   target/linux/generic/files/drivers/mtd/nand/mtk_bmt.c   (MTD glue)
+ */
+
+#include <malloc.h>
+#include <asm/cache.h>
+#include <dm/ofnode.h>
+#include <linux/bitops.h>
+#include <linux/bug.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/mtd/mtd.h>
+#include <linux/printk.h>
+#include <linux/sizes.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include "en75_bmt.h"
+
+#define LOG_PFX				"en75_bmt"
+
+/* Vendor firmware calls this POOL_GOOD_BLOCK_PERCENT. Must agree with the
+ * bootbase, which prints the resulting figure as "bmt pool size: N".
+ */
+#define REQUIRED_GOOD_BLOCKS(total)	((total) * 8 / 100)
+
+#define MAX_BMT_SIZE			256
+#define MAX_BBT_SIZE			1000
+#define DEFAULT_BBT_TABLE_SIZE		1000
+#define INITIAL_READ_TRIES		3
+
+/* Marked bad by the Linux driver (the bootbase uses 0x00 for everything). */
+#define BLOCK_WORN_MARK			0x55
+
+/*
+ * On-disk structures. All multi-byte fields are host-order little-endian.
+ */
+
+struct bmt_table_header {
+	char signature[3];	/* "BMT" */
+	u8 version;		/* 1 */
+	u8 bad_count;		/* unused */
+	u8 size;		/* number of mappings */
+	u8 checksum;		/* bmt_checksum() */
+	u8 reserved[13];
+};
+
+static_assert(sizeof(struct bmt_table_header) == 20);
+
+struct bmt_entry {
+	u16 from;		/* physical (post-BBT-shift) block being replaced */
+	u16 to;			/* reserve block standing in for it */
+};
+
+struct bmt_table {
+	struct bmt_table_header header;
+	struct bmt_entry table[MAX_BMT_SIZE];
+};
+
+struct bbt_table_header {
+	char signature[4];	/* "RAWB" */
+	u32 checksum;		/* bbt_checksum(), only 16 bits used */
+	u8 version;		/* 1 */
+	u8 size;		/* number of bad blocks */
+	u8 reserved[2];		/* 0xffff */
+};
+
+static_assert(sizeof(struct bbt_table_header) == 12);
+
+struct bbt_table {
+	struct bbt_table_header header;
+	u16 table[MAX_BBT_SIZE];	/* ascending order required */
+};
+
+static_assert(sizeof(struct bbt_table) == 2012);
+
+/*
+ * Runtime state. Single instance: there is one NAND chip on these boards, and
+ * the Linux driver makes the same assumption.
+ */
+struct en75_bmt {
+	struct mtd_info *mtd;
+
+	/* saved raw ops - internal I/O must bypass our own translation */
+	int (*_read_oob)(struct mtd_info *mtd, loff_t from, struct mtd_oob_ops *ops);
+	int (*_write_oob)(struct mtd_info *mtd, loff_t to, struct mtd_oob_ops *ops);
+	int (*_erase)(struct mtd_info *mtd, struct erase_info *instr);
+	int (*_block_isbad)(struct mtd_info *mtd, loff_t ofs);
+	int (*_block_markbad)(struct mtd_info *mtd, loff_t ofs);
+
+	struct bbt_table bbt;
+	struct bmt_table bmt;
+
+	u16 bbt_table_size;
+	u16 total_blks;
+	u16 reserve_area_begin;
+	u32 blk_size;
+	u32 pg_size;
+	u16 blk_shift;
+	u16 pg_shift;
+
+	u8 *data_buf;
+	bool attached;
+};
+
+static struct en75_bmt bmtd;
+
+static inline u32 blk_pg(u16 block)
+{
+	return (u32)(block << (bmtd.blk_shift - bmtd.pg_shift));
+}
+
+/* Read via the *saved* op: reserve-area offsets sit above the shrunken
+ * mtd->size and would be rejected by the bounds-checked wrappers, and going
+ * through our own translation here would recurse.
+ */
+static int bbt_nand_read(u32 page, u8 *dat, int dat_len, u8 *fdm, int fdm_len)
+{
+	struct mtd_oob_ops ops = {
+		.mode = MTD_OPS_PLACE_OOB,
+		.ooboffs = 0,
+		.oobbuf = fdm,
+		.ooblen = fdm_len,
+		.datbuf = dat,
+		.len = dat_len,
+	};
+	int ret;
+
+	ret = bmtd._read_oob(bmtd.mtd, (loff_t)page << bmtd.pg_shift, &ops);
+	if (ret < 0)
+		return ret;
+	return 0;
+}
+
+/*
+ * In-memory helpers (no I/O)
+ */
+
+static u16 bbt_checksum(const struct bbt_table *bbt, u16 table_size)
+{
+	const u8 *data = (const u8 *)bbt->table;
+	u16 checksum = bbt->header.version + bbt->header.size;
+	int i;
+
+	for (i = 0; i < table_size * (int)sizeof(bbt->table[0]); i++)
+		checksum += data[i];
+
+	return checksum;
+}
+
+static u8 bmt_checksum(const struct bmt_table *bmt, int check_entries)
+{
+	const u8 *data = (const u8 *)&bmt->table;
+	u8 checksum = bmt->header.version + bmt->header.size;
+	int length, i;
+
+	if (check_entries > MAX_BMT_SIZE)
+		check_entries = MAX_BMT_SIZE;
+	length = check_entries * (int)sizeof(bmt->table[0]);
+	for (i = 0; i < length; i++)
+		checksum += data[i];
+
+	return checksum;
+}
+
+static void sort_bbt(struct bbt_table *bbt)
+{
+	int i, j;
+
+	/* Insertion sort: at most a few hundred u16s, and it avoids pulling in
+	 * qsort() for something this small. Ascending order is required both by
+	 * the shift algorithm below and by the vendor firmware.
+	 */
+	for (i = 1; i < bbt->header.size; i++) {
+		u16 key = bbt->table[i];
+
+		for (j = i - 1; j >= 0 && bbt->table[j] > key; j--)
+			bbt->table[j + 1] = bbt->table[j];
+		bbt->table[j + 1] = key;
+	}
+}
+
+/*
+ * Stage 1: each factory-bad block shifts everything above it up by one. This
+ * is a shift, not a skip-list, which is why the BBT must be sorted and why
+ * changing it after the fact moves every block above the change.
+ */
+static int get_mapping_block_bbt(int block)
+{
+	int size = bmtd.bbt.header.size;
+	int i;
+
+	for (i = 0; i < size; i++)
+		if (bmtd.bbt.table[i] <= block)
+			block++;
+
+	if (block >= bmtd.reserve_area_begin || block < 0)
+		return -EINVAL;
+
+	return block;
+}
+
+/* Stage 2: BMT lookup, scanned backwards so a later duplicate 'from' wins. */
+static int get_mapping_block(int block)
+{
+	int i;
+
+	block = get_mapping_block_bbt(block);
+	if (block < 0)
+		return block;
+
+	for (i = bmtd.bmt.header.size - 1; i >= 0; i--)
+		if (bmtd.bmt.table[i].from == block)
+			return bmtd.bmt.table[i].to;
+
+	return block;
+}
+
+enum block_is_bad {
+	BB_GOOD,
+	BB_FACTORY_BAD,
+	BB_WORN,
+	BB_UNKNOWN_BAD,
+};
+
+static enum block_is_bad fdm_is_bad(const u8 fdm[4])
+{
+	if (fdm[0] == 0xff && fdm[1] == 0xff)
+		return BB_GOOD;
+	if (fdm[0] == BLOCK_WORN_MARK)
+		return BB_WORN;
+	if (fdm[0] == 0x00 || fdm[1] == 0x00)
+		return BB_FACTORY_BAD;
+	return BB_UNKNOWN_BAD;
+}
+
+static int try_parse_bbt(struct bbt_table *out, const u8 *buf, int len, u16 table_size)
+{
+	static struct bbt_table workspace;
+	size_t bbt_size = sizeof(workspace.header) +
+			  table_size * sizeof(workspace.table[0]);
+
+	if (len < (int)bbt_size)
+		return -EINVAL;
+
+	memcpy(&workspace, buf, bbt_size);
+
+	if (strncmp(workspace.header.signature, "RAWB", 4))
+		return -EINVAL;
+
+	if ((u16)workspace.header.checksum != bbt_checksum(&workspace, table_size))
+		return -EINVAL;
+
+	sort_bbt(&workspace);
+	memcpy(out, &workspace, bbt_size);
+
+	return 0;
+}
+
+static int try_parse_bmt(struct bmt_table *out, const u8 *buf, int len)
+{
+	static struct bmt_table workspace;
+
+	if (len < (int)sizeof(*out))
+		return -EINVAL;
+
+	memcpy(&workspace, buf, sizeof(workspace));
+
+	if (strncmp(workspace.header.signature, "BMT", 3))
+		return -EINVAL;
+
+	if (workspace.header.checksum !=
+	    bmt_checksum(&workspace, workspace.header.size))
+		return -EINVAL;
+
+	memcpy(out, &workspace, sizeof(workspace));
+
+	return 0;
+}
+
+/*
+ * Walk down from the last block until REQUIRED_GOOD_BLOCKS good ones have been
+ * seen; that boundary is where the reserve pool starts. The BBT and BMT are
+ * found by signature+checksum along the way, not at fixed addresses.
+ */
+static int scan_reserve(void)
+{
+	u16 total_blks = bmtd.total_blks;
+	int required = REQUIRED_GOOD_BLOCKS(total_blks);
+	int good_blocks = 0;
+	bool found_bbt = false, found_bmt = false;
+	int cursor;
+
+	for (cursor = total_blks - 1; cursor > 0; cursor--) {
+		u8 fdm[4] = { 0xff, 0xff, 0xff, 0xff };
+		int ret = -EIO;
+		int i;
+
+		for (i = 0; i < INITIAL_READ_TRIES; i++) {
+			ret = bbt_nand_read(blk_pg(cursor), bmtd.data_buf,
+					    bmtd.pg_size, fdm, sizeof(fdm));
+			if (!ret)
+				break;
+		}
+
+		if (ret || fdm_is_bad(fdm)) {
+			pr_info("%s: skipping bad block %d in reserve area\n",
+				LOG_PFX, cursor);
+		} else {
+			good_blocks++;
+			if (!found_bbt &&
+			    !try_parse_bbt(&bmtd.bbt, bmtd.data_buf,
+					   bmtd.pg_size, bmtd.bbt_table_size)) {
+				printf("%s: found BBT in block %d\n", LOG_PFX, cursor);
+				found_bbt = true;
+			} else if (!found_bmt &&
+				   !try_parse_bmt(&bmtd.bmt, bmtd.data_buf,
+						  bmtd.pg_size)) {
+				printf("%s: found BMT in block %d\n", LOG_PFX, cursor);
+				found_bmt = true;
+			}
+		}
+
+		if (good_blocks >= required)
+			break;
+	}
+
+	if (!cursor) {
+		pr_err("%s: not enough valid blocks found, need %d got %d\n",
+		       LOG_PFX, required, good_blocks);
+		return -ENOSPC;
+	}
+
+	bmtd.reserve_area_begin = cursor;
+
+	/*
+	 * Refuse to run without a valid BBT. The alternative - reconstructing
+	 * it - is exactly the operation that can shift every block on the chip,
+	 * so a bootloader must not attempt it.
+	 */
+	if (!found_bbt) {
+		pr_err("%s: no valid BBT found, refusing to translate\n", LOG_PFX);
+		return -ENOENT;
+	}
+	if (!found_bmt)
+		pr_info("%s: no BMT found, assuming no remapped blocks\n", LOG_PFX);
+
+	return 0;
+}
+
+/*
+ * MTD operation wrappers. Each translates then defers to the saved raw op.
+ * Unlike the Linux driver these never allocate a replacement block: a failure
+ * is reported to the caller instead.
+ */
+
+static int en75_bmt_read(struct mtd_info *mtd, loff_t from, struct mtd_oob_ops *ops)
+{
+	struct mtd_oob_ops cur_ops = *ops;
+	int max_bitflips = 0;
+	int ret = 0;
+
+	ops->retlen = 0;
+	ops->oobretlen = 0;
+
+	while (ops->retlen < ops->len || ops->oobretlen < ops->ooblen) {
+		u32 offset = from & (bmtd.blk_size - 1);
+		u32 block = from >> bmtd.blk_shift;
+		int cur_block, cur_ret;
+		loff_t cur_from;
+
+		cur_block = get_mapping_block(block);
+		if (cur_block < 0)
+			return -EIO;
+
+		cur_from = ((loff_t)cur_block << bmtd.blk_shift) + offset;
+
+		cur_ops.oobretlen = 0;
+		cur_ops.retlen = 0;
+		cur_ops.len = min_t(u32, mtd->erasesize - offset,
+				    ops->len - ops->retlen);
+
+		cur_ret = bmtd._read_oob(mtd, cur_from, &cur_ops);
+		if (cur_ret < 0) {
+			ret = cur_ret;
+			if (!mtd_is_bitflip(cur_ret))
+				goto out;
+		} else {
+			max_bitflips = max_t(int, max_bitflips, cur_ret);
+		}
+
+		ops->retlen += cur_ops.retlen;
+		ops->oobretlen += cur_ops.oobretlen;
+
+		cur_ops.ooboffs = 0;
+		cur_ops.datbuf += cur_ops.retlen;
+		cur_ops.oobbuf += cur_ops.oobretlen;
+		cur_ops.ooblen -= cur_ops.oobretlen;
+
+		if (!cur_ops.len)
+			cur_ops.len = mtd->erasesize - offset;
+
+		from += cur_ops.len;
+	}
+
+out:
+	if (ret < 0 && !mtd_is_bitflip(ret))
+		return ret;
+
+	return max_bitflips;
+}
+
+static int en75_bmt_write(struct mtd_info *mtd, loff_t to, struct mtd_oob_ops *ops)
+{
+	struct mtd_oob_ops cur_ops = *ops;
+	int ret;
+
+	ops->retlen = 0;
+	ops->oobretlen = 0;
+
+	while (ops->retlen < ops->len || ops->oobretlen < ops->ooblen) {
+		u32 offset = to & (bmtd.blk_size - 1);
+		u32 block = to >> bmtd.blk_shift;
+		int cur_block;
+		loff_t cur_to;
+
+		cur_block = get_mapping_block(block);
+		if (cur_block < 0)
+			return -EIO;
+
+		cur_to = ((loff_t)cur_block << bmtd.blk_shift) + offset;
+
+		cur_ops.oobretlen = 0;
+		cur_ops.retlen = 0;
+		cur_ops.len = min_t(u32, bmtd.blk_size - offset,
+				    ops->len - ops->retlen);
+
+		ret = bmtd._write_oob(mtd, cur_to, &cur_ops);
+		if (ret < 0) {
+			/* No remap-on-failure here: the block stays bad and the
+			 * caller finds out. Repairing it is Linux's job.
+			 */
+			pr_err("%s: write failed at block %u (physical %d); "
+			       "not remapping (read-only BMT)\n",
+			       LOG_PFX, block, cur_block);
+			return ret;
+		}
+
+		ops->retlen += cur_ops.retlen;
+		ops->oobretlen += cur_ops.oobretlen;
+
+		cur_ops.ooboffs = 0;
+		cur_ops.datbuf += cur_ops.retlen;
+		cur_ops.oobbuf += cur_ops.oobretlen;
+		cur_ops.ooblen -= cur_ops.oobretlen;
+
+		if (!cur_ops.len)
+			cur_ops.len = mtd->erasesize - offset;
+
+		to += cur_ops.len;
+	}
+
+	return 0;
+}
+
+static int en75_bmt_erase(struct mtd_info *mtd, struct erase_info *instr)
+{
+	struct erase_info mapped_instr = {
+		.len = bmtd.blk_size,
+	};
+	u64 start_addr, end_addr;
+	int ret = 0;
+
+	start_addr = instr->addr & (~mtd->erasesize_mask);
+	end_addr = instr->addr + instr->len;
+
+	while (start_addr < end_addr) {
+		u16 orig_block = start_addr >> bmtd.blk_shift;
+		int block = get_mapping_block(orig_block);
+
+		if (block < 0)
+			return -EIO;
+
+		mapped_instr.addr = (loff_t)block << bmtd.blk_shift;
+		ret = bmtd._erase(mtd, &mapped_instr);
+		if (ret) {
+			pr_err("%s: erase failed at block %u (physical %d); "
+			       "not remapping (read-only BMT)\n",
+			       LOG_PFX, orig_block, block);
+			instr->fail_addr = start_addr;
+			break;
+		}
+		start_addr += mtd->erasesize;
+	}
+
+	return ret;
+}
+
+static int en75_bmt_block_isbad(struct mtd_info *mtd, loff_t ofs)
+{
+	u16 orig_block = ofs >> bmtd.blk_shift;
+	int block = get_mapping_block(orig_block);
+
+	if (block < 0)
+		return -EIO;
+
+	return bmtd._block_isbad(mtd, (loff_t)block << bmtd.blk_shift);
+}
+
+static int en75_bmt_block_markbad(struct mtd_info *mtd, loff_t ofs)
+{
+	u16 orig_block = ofs >> bmtd.blk_shift;
+	int block = get_mapping_block(orig_block);
+
+	if (block < 0)
+		return -EIO;
+
+	return bmtd._block_markbad(mtd, (loff_t)block << bmtd.blk_shift);
+}
+
+static void en75_bmt_replace_ops(struct mtd_info *mtd)
+{
+	bmtd._read_oob		= mtd->_read_oob;
+	bmtd._write_oob		= mtd->_write_oob;
+	bmtd._erase		= mtd->_erase;
+	bmtd._block_isbad	= mtd->_block_isbad;
+	bmtd._block_markbad	= mtd->_block_markbad;
+
+	mtd->_read_oob		= en75_bmt_read;
+	mtd->_write_oob		= en75_bmt_write;
+	mtd->_erase		= en75_bmt_erase;
+	mtd->_block_isbad	= en75_bmt_block_isbad;
+	mtd->_block_markbad	= en75_bmt_block_markbad;
+}
+
+static void en75_bmt_restore_ops(struct mtd_info *mtd)
+{
+	mtd->_read_oob		= bmtd._read_oob;
+	mtd->_write_oob		= bmtd._write_oob;
+	mtd->_erase		= bmtd._erase;
+	mtd->_block_isbad	= bmtd._block_isbad;
+	mtd->_block_markbad	= bmtd._block_markbad;
+}
+
+int en75_bmt_attach(struct mtd_info *mtd)
+{
+	ofnode node = mtd_get_ofnode(mtd);
+	u32 assert_reserve = 0;
+	u32 val;
+	int ret;
+
+	if (!ofnode_valid(node) || !ofnode_read_bool(node, "econet,bmt"))
+		return 0;
+
+	if (bmtd.attached) {
+		pr_err("%s: already attached to an MTD device\n", LOG_PFX);
+		return -EBUSY;
+	}
+
+	memset(&bmtd, 0, sizeof(bmtd));
+	bmtd.mtd = mtd;
+	bmtd.blk_size = mtd->erasesize;
+	bmtd.pg_size = mtd->writesize;
+	bmtd.blk_shift = ffs(mtd->erasesize) - 1;
+	bmtd.pg_shift = ffs(mtd->writesize) - 1;
+	/* Must be taken before the size is shrunk below. */
+	bmtd.total_blks = mtd->size >> bmtd.blk_shift;
+
+	bmtd.bbt_table_size = DEFAULT_BBT_TABLE_SIZE;
+	if (!ofnode_read_u32(node, "econet,bbt-table-size", &val)) {
+		if (val == 0 || val > MAX_BBT_SIZE) {
+			pr_err("%s: econet,bbt-table-size %u out of range\n",
+			       LOG_PFX, val);
+			return -EINVAL;
+		}
+		bmtd.bbt_table_size = val;
+	}
+	ofnode_read_u32(node, "econet,assert-reserve-size", &assert_reserve);
+
+	bmtd.data_buf = memalign(ARCH_DMA_MINALIGN, bmtd.pg_size);
+	if (!bmtd.data_buf)
+		return -ENOMEM;
+
+	en75_bmt_replace_ops(mtd);
+
+	ret = scan_reserve();
+	if (ret)
+		goto err_restore;
+
+	if (assert_reserve &&
+	    assert_reserve != bmtd.total_blks - bmtd.reserve_area_begin) {
+		/* Mismatch means our idea of the pool boundary differs from the
+		 * bootbase's, so every translation would be wrong.
+		 */
+		pr_err("%s: reserve size %d != asserted %u, refusing\n", LOG_PFX,
+		       bmtd.total_blks - bmtd.reserve_area_begin, assert_reserve);
+		ret = -EINVAL;
+		goto err_restore;
+	}
+
+	/* Hide the reserve area and the factory-bad shift from everything
+	 * above, so DT partitions are validated against the same size Linux
+	 * sees. Must happen before partitions are parsed.
+	 */
+	mtd->size = (loff_t)(bmtd.reserve_area_begin - bmtd.bbt.header.size)
+		    << bmtd.blk_shift;
+
+	bmtd.attached = true;
+
+	printf("%s: blocks: total: %d, user: %d, factory_bad: %d, worn: %d reserve: %d\n",
+	       LOG_PFX, bmtd.total_blks, bmtd.reserve_area_begin,
+	       bmtd.bbt.header.size, bmtd.bmt.header.size,
+	       bmtd.total_blks - bmtd.reserve_area_begin);
+	printf("%s: %llu MiB usable space (read-only BMT: no remapping)\n",
+	       LOG_PFX, (u64)mtd->size >> 20);
+
+	return 0;
+
+err_restore:
+	en75_bmt_restore_ops(mtd);
+	free(bmtd.data_buf);
+	bmtd.data_buf = NULL;
+	bmtd.mtd = NULL;
+	return ret;
+}
+
+void en75_bmt_detach(struct mtd_info *mtd)
+{
+	if (!bmtd.attached || bmtd.mtd != mtd)
+		return;
+
+	en75_bmt_restore_ops(mtd);
+	free(bmtd.data_buf);
+	bmtd.data_buf = NULL;
+	bmtd.mtd = NULL;
+	bmtd.attached = false;
+}
diff --git a/drivers/mtd/nand/spi/en75_bmt.h b/drivers/mtd/nand/spi/en75_bmt.h
new file mode 100644
index 00000000..e9d50b8d
--- /dev/null
+++ b/drivers/mtd/nand/spi/en75_bmt.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * EcoNet/Airoha EN75xx vendor BMT/BBT translation layer (read-only).
+ */
+#ifndef __EN75_BMT_H
+#define __EN75_BMT_H
+
+struct mtd_info;
+
+#if CONFIG_IS_ENABLED(MTD_EN75_BMT)
+/*
+ * Install the translation layer on a NAND whose DT node carries "econet,bmt".
+ * Must be called before add_mtd_device(), because it shrinks mtd->size to the
+ * user area and partitions are validated against that.
+ * Returns 0 when attached *or* when the node opts out; negative on error.
+ */
+int en75_bmt_attach(struct mtd_info *mtd);
+void en75_bmt_detach(struct mtd_info *mtd);
+#else
+static inline int en75_bmt_attach(struct mtd_info *mtd) { return 0; }
+static inline void en75_bmt_detach(struct mtd_info *mtd) { }
+#endif
+
+#endif /* __EN75_BMT_H */
-- 
2.53.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.