nicamd: Introduce new programmer driver for AMD NICs

On a PCnet32 FAST card with a nice PLCC32 socket, only 18 of the 20 ad-
dress lines were connected. It's not easy to detect this, but we'll do
our best: Check the ROM contents for repeating patterns, and allow the
user to override the guessed size. We use the ROM BAR here, though not
for the actual flash reading / writing. According to the datasheet the
ROM BAR interface always reads four bytes, and doesn't seem to support
writes.

Otherwise tested successfully with the first 256KiB of an SST39VF040.

Datasheet used:
https://www.amd.com/content/dam/amd/en/documents/archived-tech-docs/datasheets/20550.pdf

Change-Id: I3bddfcd1e242d28790a0916197ebd8b8bba5a70a
Signed-off-by: Nico Huber <nico.h@gmx.de>
Reviewed-on: https://review.sourcearcade.org/c/flashprog/+/530
Reviewed-by: Arthur Heymans <arthur@aheymans.xyz>
diff --git a/Makefile b/Makefile
index ac73011..a9f2429 100644
--- a/Makefile
+++ b/Makefile
@@ -132,6 +132,7 @@
 	CONFIG_ATAPROMISE \
 	CONFIG_INTERNAL_X86 \
 	CONFIG_NIC3COM \
+	CONFIG_NICAMD \
 	CONFIG_NICNATSEMI \
 	CONFIG_NICREALTEK \
 	CONFIG_RAYER_SPI \
@@ -146,6 +147,7 @@
 	CONFIG_INTERNAL \
 	CONFIG_IT8212 \
 	CONFIG_NIC3COM \
+	CONFIG_NICAMD \
 	CONFIG_NICINTEL \
 	CONFIG_NICINTEL_EEPROM \
 	CONFIG_NICINTEL_SPI \
@@ -435,6 +437,9 @@
 # Always enable 3Com NICs for now.
 CONFIG_NIC3COM ?= yes
 
+# Always enable AMD NICs for now.
+CONFIG_NICAMD ?= yes
+
 # Enable NVIDIA graphics cards. Note: write and erase do not work properly.
 CONFIG_GFXNVIDIA ?= yes
 
@@ -647,6 +652,11 @@
 PROGRAMMER_OBJS += nic3com.o
 endif
 
+ifeq ($(CONFIG_NICAMD), yes)
+FEATURE_FLAGS += -D'CONFIG_NICAMD=1'
+PROGRAMMER_OBJS += nicamd.o
+endif
+
 ifeq ($(CONFIG_GFXNVIDIA), yes)
 FEATURE_FLAGS += -D'CONFIG_GFXNVIDIA=1'
 PROGRAMMER_OBJS += gfxnvidia.o
diff --git a/flashprog.8.tmpl b/flashprog.8.tmpl
index 2048da7..d771333 100644
--- a/flashprog.8.tmpl
+++ b/flashprog.8.tmpl
@@ -308,6 +308,8 @@
 .sp
 .BR "* nicintel" " (for parallel flash ROMs on Intel 10/100Mbit network cards)
 .sp
+.BR "* nicamd" " (for parallel flash ROMs on AMD PCnet network cards)
+.sp
 .BR "* gfxnvidia" " (for flash ROMs on NVIDIA graphics cards)"
 .sp
 .BR "* drkaiser" " (for flash ROMs on Dr. Kaiser PC-Waechter PCI cards)"
@@ -761,9 +763,9 @@
 not here).
 .SS
 .BR "nic3com" , " nicrealtek" , " nicnatsemi" , " nicintel", " nicintel_eeprom"\
-, " nicintel_spi" , " gfxnvidia" , " ogp_spi" , " drkaiser" , " satasii"\
-, " scsilsi", " satamv" , " atahpt", " atavia ", " atapromise " and " it8212 "\
-programmers
+, " nicintel_spi" , " nicamd" , " gfxnvidia" , " ogp_spi" , " drkaiser"\
+, " satasii" , " scsilsi", " satamv" , " atahpt", " atavia ", " atapromise "\
+and " it8212 " programmers
 .IP
 These programmers have an option to specify the PCI address of the card
 your want to use, which must be specified if more than one card supported
@@ -812,6 +814,15 @@
 unprogrammed EEPROM/card is detected. Intel specifies following EEPROMs to be compatible:
 Atmel AT25128, AT25256, Micron (ST) M95128, M95256 and OnSemi (Catalyst) CAT25CS128.
 .SS
+.BR "nicamd " programmer
+.IP
+This programmer module supports parallel flash up to 1MiB, attached to AMD PCnet cards. As it can't be said
+for sure if all 20 parallel address lines are connected on a given card, flashprog will try to probe the
+addressable size. If this doesn't work reliably, one can override the probing logic with the
+.BR "max_decode_kb " parameter.
+.sp
+.B "  flashprog \-p nicamd:max_decode_kb=128"
+.SS
 .BR "ft2232_spi " programmer
 .IP
 This module supports various programmers based on FTDI FT2232/FT4232H/FT4233H/FT232H chips including the DLP Design
@@ -1565,7 +1576,7 @@
 .BR satasii ", " scsilsi ", " nicintel ", " nicintel_eeprom " and " nicintel_spi
 need PCI configuration space read access and raw memory access.
 .sp
-.BR satamv " and " atapromise
+.BR nicamd ", " satamv " and " atapromise
 need PCI configuration space read access, raw I/O port access and raw memory
 access.
 .sp
@@ -1584,7 +1595,7 @@
 .B dummy
 needs no access permissions at all.
 .sp
-.BR internal ", " nic3com ", " nicrealtek ", " nicnatsemi ", "
+.BR internal ", " nicamd ", " nic3com ", " nicrealtek ", " nicnatsemi ", "
 .BR gfxnvidia ", " drkaiser ", " satamv ", " satasii ", " scsilsi ", "
 .BR atahpt ", " atavia " and " atapromise
 have to be run as superuser/root, and need additional raw access permission.
diff --git a/include/programmer.h b/include/programmer.h
index a2de74e..1aa57b4 100644
--- a/include/programmer.h
+++ b/include/programmer.h
@@ -82,6 +82,7 @@
 extern const struct programmer_entry programmer_mstarddc_spi;
 extern const struct programmer_entry programmer_ni845x_spi;
 extern const struct programmer_entry programmer_nic3com;
+extern const struct programmer_entry programmer_nicamd;
 extern const struct programmer_entry programmer_nicintel;
 extern const struct programmer_entry programmer_nicintel_eeprom;
 extern const struct programmer_entry programmer_nicintel_spi;
diff --git a/meson.build b/meson.build
index d7865d4..b94d951 100644
--- a/meson.build
+++ b/meson.build
@@ -347,6 +347,14 @@
     'srcs'    : files('nic3com.c', 'pcidev.c'),
     'flags'   : [ '-DCONFIG_NIC3COM=1' ],
   },
+  'nicamd' : {
+    'systems' : systems_hwaccess,
+    'cpu_families' : [ cpus_port_io ],
+    'deps'    : [ libpci ],
+    'groups'  : [ group_pci, group_internal ],
+    'srcs'    : files('nicamd.c', 'pcidev.c'),
+    'flags'   : [ '-DCONFIG_NICAMD=1' ],
+  },
   'nicintel' : {
     'systems' : systems_hwaccess,
     'cpu_families' : [ cpus_raw_mem ],
diff --git a/meson_options.txt b/meson_options.txt
index d466bd4..85a8752 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -12,8 +12,9 @@
         'atahpt', 'atapromise', 'atavia', 'buspirate_spi', 'ch341a_spi', 'ch347_spi', 'dediprog', 'developerbox_spi',
         'digilent_spi', 'dirtyjtag_spi', 'drkaiser', 'dummy', 'ft2232_spi', 'ft4222_spi',
         'gfxnvidia', 'internal', 'it8212',
-        'jlink_spi', 'linux_gpio_spi', 'linux_mtd', 'linux_spi', 'mediatek_i2c_spi', 'mstarddc_spi', 'nic3com',
-        'nicintel', 'nicintel_eeprom', 'nicintel_spi', 'nicnatsemi', 'nicrealtek', 'ogp_spi', 'parade_lspcon',
+        'jlink_spi', 'linux_gpio_spi', 'linux_mtd', 'linux_spi', 'mediatek_i2c_spi', 'mstarddc_spi',
+        'nic3com', 'nicamd', 'nicintel', 'nicintel_eeprom', 'nicintel_spi', 'nicnatsemi', 'nicrealtek',
+        'ogp_spi', 'parade_lspcon',
         'pickit2_spi', 'pony_spi', 'raiden_debug_spi', 'rayer_spi', 'realtek_mst_i2c_spi', 'satamv',
         'satasii', 'scsilsi', 'serprog', 'stlinkv3_spi', 'usbblaster_spi',
 ], description: 'Active programmers')
diff --git a/nicamd.c b/nicamd.c
new file mode 100644
index 0000000..b447a94
--- /dev/null
+++ b/nicamd.c
@@ -0,0 +1,212 @@
+/*
+ * This file is part of the flashprog project.
+ *
+ * Copyright (C) 2024 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 <stdlib.h>
+#include <flash.h>
+#include <hwaccess_x86_io.h>
+#include <hwaccess_physmap.h>
+#include <platform/pci.h>
+#include <programmer.h>
+
+#define PCI_VENDOR_ID_AMD	0x1022
+
+static const struct dev_entry nics_amd[] = {
+	{PCI_VENDOR_ID_AMD, 0x2000, OK, "AMD", "79C97x [PCnet32 LANCE]"},
+
+	{0},
+};
+
+static uint8_t readb(const struct par_master *, chipaddr);
+static void writeb(const struct par_master *, uint8_t val, chipaddr);
+static int shutdown(void *);
+
+static const struct par_master par_master = {
+	.chip_readb	= readb,
+	.chip_readw	= fallback_chip_readw,
+	.chip_readl	= fallback_chip_readl,
+	.chip_readn	= fallback_chip_readn,
+	.chip_writeb	= writeb,
+	.chip_writew	= fallback_chip_writew,
+	.chip_writel	= fallback_chip_writel,
+	.chip_writen	= fallback_chip_writen,
+	.shutdown	= shutdown,
+};
+
+#define ROM_BAR_SIZE	(1*MiB)
+
+/* ----- i/o  space ----- */
+
+#define ANY_IO_RDP	0x10	/* Register Data Port */
+#define WORD_IO_RAP	0x12
+#define WORD_IO_RESET	0x14
+#define WORD_IO_BDP	0x16
+
+#define DWORD_IO_RAP	0x14	/* Register Address Port */
+#define DWORD_IO_RESET	0x18
+#define DWORD_IO_BDP	0x1c	/* BCR (Bus Configuration Register) Data Port */
+
+/* ----- BCR  space ----- */
+
+#define EXP_BUS_ADDR_LOWER	28
+#define EXP_BUS_ADDR_UPPER	29
+#define  EBADDRU_FLASH		(1 << 15)
+#define EXP_BUS_DATA_PORT	30
+
+/* all BCR registers are 16-bit wide */
+
+static uint16_t bcr_read16(unsigned int io_base, unsigned int reg)
+{
+	OUTL(reg, io_base + DWORD_IO_RAP);
+	return INL(io_base + DWORD_IO_BDP);
+}
+
+static void bcr_write16(unsigned int io_base, unsigned int reg, uint16_t val)
+{
+	OUTL(reg, io_base + DWORD_IO_RAP);
+	OUTL(val, io_base + DWORD_IO_BDP);
+}
+
+static int init(struct flashprog_programmer *const prog)
+{
+	size_t max_decode = 0;
+	char *const decode_override = extract_programmer_param("max_decode_kb");
+	if (decode_override) {
+		char *endptr;
+		max_decode = strtoul(decode_override, &endptr, 10);
+		if (*endptr || max_decode < 16 || max_decode > 1024 || (max_decode & (max_decode - 1)))  {
+			msg_perr("Error: Invalid ROM decode override: \"%s\".\n"
+				 "Valid values are powers of 2 from 16 through 1024 (KiB).\n",
+				 decode_override);
+			free(decode_override);
+			return 1;
+		}
+		max_decode *= KiB;
+	}
+	free(decode_override);
+
+	if (rget_io_perms())
+		return 1;
+
+	struct pci_dev *const dev = pcidev_init(nics_amd, PCI_BASE_ADDRESS_0);
+	if (!dev)
+		return 1;
+
+	uintptr_t io_base = pcidev_readbar(dev, PCI_BASE_ADDRESS_0);
+	if (!io_base)
+		return 1;
+
+	if (!max_decode) {
+		msg_pinfo("Trying to guess addressable size based on read contents and patterns. If this\n"
+			  "doesn't work, you can override probing with `-p nicamd:max_decode_kb=<size>`.\n");
+
+		const uint16_t command = pci_read_word(dev, PCI_COMMAND);
+		if (!(command & PCI_COMMAND_MEMORY)) {
+			msg_perr("Error: PCI memory access is disabled.\n");
+			return 1;
+		}
+
+		const uint32_t rom_base = pci_read_long(dev, PCI_ROM_ADDRESS);
+		if (!rom_base) {
+			msg_perr("Error: PCI ROM base is not configured.\n");
+			return 1;
+		}
+
+		void *physmap(const char *descr, uintptr_t phys_addr, size_t len);
+		void *const rom = physmap("ROM BAR", rom_base & ~1u, ROM_BAR_SIZE);
+		if (rom == ERROR_PTR)
+			return 1;
+
+		if (!(rom_base & 1u))
+			pci_write_long(dev, PCI_ROM_ADDRESS, rom_base | 1);
+
+		max_decode = estimate_addressable_size(rom, ROM_BAR_SIZE);
+
+		if (!(rom_base & 1u))
+			pci_write_long(dev, PCI_ROM_ADDRESS, rom_base);
+
+		physunmap(rom, ROM_BAR_SIZE);
+
+		if (!max_decode) {
+			max_decode = 256*KiB;
+			msg_pwarn("Estimating addressable size failed. Using default of %zuKiB.\n",
+				  max_decode / KiB);
+		}
+	}
+
+	/*
+	 * Now comes the fun part: This controller knows two i/o modes,
+	 * word and dword. Writing with the wrong width is illegal, and
+	 * it's impossible to tell the current mode without writing.
+	 *
+	 * The datasheet was reasoned with and we concluded:
+	 *   * A soft reset can be performed with read requests only.
+	 *   * Trying a dword-i/o reset in word i/o mode is only a read
+	 *     from a reserved register and shouldn't hurt.
+	 *   * Trying a word-i/o reset in dword i/o mode is only a read
+	 *     with the wrong length.
+	 *   * After reset we still don't know the mode  (it is decided
+	 *     by EEPROM bits), though we can switch to dword i/o mode,
+	 *     blindly.
+	 */
+	INW(io_base + WORD_IO_RESET);
+	INL(io_base + DWORD_IO_RESET);
+	OUTL(0, io_base + ANY_IO_RDP); /* supposed to enable dword i/o mode */
+
+	OUTL(18, io_base + DWORD_IO_RAP);
+	msg_pdbg2("BBCR: 0x%04x\n", INL(io_base + DWORD_IO_BDP) & 0xffff);
+	OUTL(0x98e1, io_base + DWORD_IO_BDP);
+
+	msg_pdbg2("BCR25: 0x%04x\n", bcr_read16(io_base, 25));
+	bcr_write16(io_base, 25, 0);
+
+	return register_par_master(&par_master, BUS_PARALLEL, 0, max_decode, (void *)io_base);
+}
+
+static uint8_t readb(const struct par_master *par, chipaddr addr)
+{
+	uintptr_t io_base = (uintptr_t)par->data;
+
+	bcr_write16(io_base, EXP_BUS_ADDR_UPPER, EBADDRU_FLASH | addr >> 16);
+	bcr_write16(io_base, EXP_BUS_ADDR_LOWER, addr);
+	return bcr_read16(io_base, EXP_BUS_DATA_PORT);
+}
+
+static void writeb(const struct par_master *par, uint8_t val, chipaddr addr)
+{
+	uintptr_t io_base = (uintptr_t)par->data;
+
+	bcr_write16(io_base, EXP_BUS_ADDR_UPPER, EBADDRU_FLASH | addr >> 16);
+	bcr_write16(io_base, EXP_BUS_ADDR_LOWER, addr);
+	bcr_write16(io_base, EXP_BUS_DATA_PORT, val);
+}
+
+static int shutdown(void *data)
+{
+	uintptr_t io_base = (uintptr_t)data;
+
+	/* Reset just in case. */
+	INL(io_base + DWORD_IO_RESET);
+
+	return 0;
+}
+
+const struct programmer_entry programmer_nicamd = {
+	.name			= "nicamd",
+	.type			= PCI,
+	.devs.dev		= nics_amd,
+	.init			= init,
+};
diff --git a/programmer_table.c b/programmer_table.c
index b4b8a4d..b2ab826 100644
--- a/programmer_table.c
+++ b/programmer_table.c
@@ -28,6 +28,10 @@
     &programmer_nic3com,
 #endif
 
+#if CONFIG_NICAMD == 1
+    &programmer_nicamd,
+#endif
+
 #if CONFIG_NICREALTEK == 1
     &programmer_nicrealtek,
 #endif