Add M28F parallel flash driver

Add just enough code to erase & write STMicro M28F chips. Similar
chip series that should be compatible include CAT28F and TK28F.

Probing should be fully compatible to the already supported Intel
28F series (82802AB probing).

Datasheets used:
https://www.mouser.com/catalog/specsheets/STMicroelectronics_M28F256.pdf
https://tekmos.com/docmandocuments/products/flash-memory/100-tk28f010
https://www.onsemi.com/download/data-sheet/pdf/cat28f010-d.pdf

Change-Id: Iab4860558a8ddc48b529bb3d8392d48473191769
Signed-off-by: Nico Huber <nico.h@gmx.de>
Reviewed-on: https://review.sourcearcade.org/c/flashprog/+/521
Reviewed-by: Arthur Heymans <arthur@aheymans.xyz>
diff --git a/m28f.c b/m28f.c
new file mode 100644
index 0000000..97224ed
--- /dev/null
+++ b/m28f.c
@@ -0,0 +1,98 @@
+/*
+ * This file is part of the flashprog project.
+ *
+ * Copyright (C) 2026 Nico Huber <nico.h@gmx.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <stdint.h>
+
+#include "flash.h"
+#include "chipdrivers/memory_bus.h"
+
+static int write_m28f_1(struct flashctx *flash, unsigned int pos, uint8_t val)
+{
+	const chipaddr base = flash->virtual_memory;
+	unsigned int tries;
+
+	for (tries = 25; tries > 0; --tries) {
+		chip_writeb(flash, 0x40, base);
+		chip_writeb(flash, val, base + pos);
+		programmer_delay(10);
+
+		chip_writeb(flash, 0xc0, base);
+		programmer_delay(6);
+
+		if (chip_readb(flash, base) == val)
+			return 0;
+	}
+
+	msg_cerr("Write failed at 0x%06x.\n", pos);
+	return 1;
+}
+
+int write_m28f(struct flashctx *flash, const uint8_t *src, unsigned int pos, unsigned int len)
+{
+	const chipaddr base = flash->virtual_memory;
+	const unsigned int limit = pos + len;
+	int ret;
+
+	for (; pos < limit; ++pos, ++src) {
+		if (*src == 0xff) /* skip no-op writes */
+			continue;
+
+		ret = write_m28f_1(flash, pos, *src);
+		if (ret)
+			goto return_to_read_mode;
+	}
+
+return_to_read_mode:
+	chip_writeb(flash, 0x00, base);
+	return ret;
+}
+
+int erase_m28f(struct flashctx *flash, unsigned int addr, unsigned int blocksize)
+{
+	const chipaddr base = flash->virtual_memory;
+	unsigned int tries, pos;
+	int ret;
+
+	/* Programming everything to 0 is required. */
+	for (pos = 0; pos < blocksize; ++pos) {
+		ret = write_m28f_1(flash, pos, 0x00);
+		if (ret)
+			goto return_to_read_mode;
+	}
+
+	for (pos = 0, tries = 1000; tries > 0; --tries) {
+		chip_writeb(flash, 0x20, base);
+		chip_writeb(flash, 0x20, base);
+		programmer_delay(10*1000);
+
+		for (; pos < blocksize; ++pos) {
+			chip_writeb(flash, 0xa0, base + pos);
+			programmer_delay(6);
+
+			if (chip_readb(flash, base) != 0xff)
+				break;
+		}
+		if (pos == blocksize)
+			goto return_to_read_mode;
+	}
+
+	msg_cerr("Erase failed at 0x%06x.\n", pos);
+	ret = 1;
+
+return_to_read_mode:
+	chip_writeb(flash, 0x00, base);
+	return ret;
+}