Add an SPL loader that talks to SPI NAND the same way the Allwinner
V3s BROM does, so U-Boot proper can be fetched after BROM has loaded
SPL. BROM still reports BOOT_DEVICE_SPI for SPI NAND, so this path is
mutually exclusive with SPL_SPI_SUNXI.

Also avoid selecting SPI NOR for the environment in that case; prefer
FAT when available.

Signed-off-by: Zunyi Hu <[email protected]>
---
 arch/arm/mach-sunxi/Kconfig             |  22 ++
 arch/arm/mach-sunxi/Makefile            |   1 +
 arch/arm/mach-sunxi/spl_spinand_sunxi.c | 449 ++++++++++++++++++++++++
 board/sunxi/board.c                     |   9 +
 4 files changed, 481 insertions(+)
 create mode 100644 arch/arm/mach-sunxi/spl_spinand_sunxi.c

diff --git a/arch/arm/mach-sunxi/Kconfig b/arch/arm/mach-sunxi/Kconfig
index b0c5d3cd903..b6c68698555 100644
--- a/arch/arm/mach-sunxi/Kconfig
+++ b/arch/arm/mach-sunxi/Kconfig
@@ -1208,6 +1208,28 @@ config SPL_SPI_SUNXI
          sunxi SPI Flash. It uses the same method as the boot ROM, so does
          not need any extra configuration.
 
+config SPL_SPINAND_SUNXI
+       bool "Support for SPI NAND Flash on Allwinner V3s in SPL"
+       depends on MACH_SUN8I_V3S
+       depends on !SPL_SPI_SUNXI
+       select SPL_CRC32
+       select SPL_LEGACY_IMAGE_FORMAT
+       help
+         Enable SPI NAND boot support in SPL. When the BROM loads SPL from
+         SPI NAND, it still reports BOOT_DEVICE_SPI. This driver then loads
+         U-Boot proper using the same BROM-compatible page layout (SPL
+         copies at a 32-page stride, U-Boot on an erase-block boundary).
+         Mutually exclusive with SPL_SPI_SUNXI (same boot device id).
+         Selects SPL_CRC32 for legacy uImage header CRC checks.
+
+config SPL_SPINAND_SUNXI_GENERIC_2K
+       bool "Use generic 2K-page profile for unknown SPI NAND chips"
+       depends on SPL_SPINAND_SUNXI
+       default y
+       help
+         Fall back to a generic 2K-page SPI NAND profile when the JEDEC ID
+         is not in the known chip table.
+
 config PINE64_DT_SELECTION
        bool "Enable Pine64 device tree selection code"
        depends on MACH_SUN50I
diff --git a/arch/arm/mach-sunxi/Makefile b/arch/arm/mach-sunxi/Makefile
index ff475d8fff5..f8c0dfc2a7d 100644
--- a/arch/arm/mach-sunxi/Makefile
+++ b/arch/arm/mach-sunxi/Makefile
@@ -40,6 +40,7 @@ obj-$(CONFIG_DRAM_SUN8I_A33)  += dram_sun8i_a33.o
 obj-$(CONFIG_DRAM_SUN8I_A83T)  += dram_sun8i_a83t.o
 obj-$(CONFIG_DRAM_SUN9I)       += dram_sun9i.o
 obj-$(CONFIG_SPL_SPI_SUNXI)    += spl_spi_sunxi.o
+obj-$(CONFIG_SPL_SPINAND_SUNXI)        += spl_spinand_sunxi.o
 obj-$(CONFIG_SUNXI_DRAM_DW)    += dram_sunxi_dw.o
 obj-$(CONFIG_SUNXI_DRAM_DW)    += dram_timings/
 obj-$(CONFIG_DRAM_SUN50I_H6)   += dram_sun50i_h6.o dram_dw_helpers.o
diff --git a/arch/arm/mach-sunxi/spl_spinand_sunxi.c 
b/arch/arm/mach-sunxi/spl_spinand_sunxi.c
new file mode 100644
index 00000000000..ef7a9a171e2
--- /dev/null
+++ b/arch/arm/mach-sunxi/spl_spinand_sunxi.c
@@ -0,0 +1,449 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * SPL loader for Allwinner SPI NAND (BROM-compatible layout).
+ *
+ * Copyright (C) 2019 Benedikt-Alexander Mokroß
+ * Copyright (C) 2026 Zunyi Hu <[email protected]>
+ *
+ * Mimics the Allwinner BROM SPI NAND read path (page load + cache read)
+ * so SPL can fetch U-Boot proper after the BROM has loaded SPL itself.
+ *
+ * Flash layout (BROM-compatible): multiple SPL copies at a 32-page stride,
+ * then U-Boot proper starting on an erase-block boundary after those copies.
+ */
+
+#include <image.h>
+#include <log.h>
+#include <mapmem.h>
+#include <spl.h>
+#include <asm/arch/spl.h>
+#include <asm/io.h>
+#include <linux/bitops.h>
+#include <linux/delay.h>
+#include <linux/kernel.h>
+#include <sunxi_gpio.h>
+
+#ifdef CONFIG_SPL_OS_BOOT
+#error CONFIG_SPL_OS_BOOT is not supported yet
+#endif
+
+#define SUN6I_SPI0_CCTL                0x24
+#define SUN6I_SPI0_GCR         0x04
+#define SUN6I_SPI0_TCR         0x08
+#define SUN6I_SPI0_FIFO_STA    0x1C
+#define SUN6I_SPI0_WCR         0x20
+#define SUN6I_SPI0_MBC         0x30
+#define SUN6I_SPI0_MTC         0x34
+#define SUN6I_SPI0_BCC         0x38
+#define SUN6I_SPI0_TXD         0x200
+#define SUN6I_SPI0_RXD         0x300
+
+#define SUN6I_CTL_ENABLE       BIT(0)
+#define SUN6I_CTL_MASTER       BIT(1)
+#define SUN6I_CTL_SRST         BIT(31)
+#define SUN6I_TCR_XCH          BIT(31)
+
+#define CCM_AHB_GATING0                (0x01C20000 + 0x60)
+#define CCM_SPI0_CLK           (0x01C20000 + 0xA0)
+#define SUN6I_BUS_SOFT_RST_REG0        (0x01C20000 + 0x2C0)
+
+#define AHB_RESET_SPI0_SHIFT   20
+#define AHB_GATE_OFFSET_SPI0   20
+
+#define SPI0_CLK_DIV_BY_4      0x1001
+#define DUMMY_BURST_BYTE       0x00
+#define EGON_BT0_HEADER_SIZE   40
+#define SPI_READ_MAX_SIZE      60
+/* Bound U-Boot search so a blank device cannot hang forever. */
+#define SPINAND_UBOOT_MAX_TRIES        64
+
+struct sunxi_nand_config {
+       const char *name;
+       u32 jedec_id;
+       u32 page_mask;
+       u8 page_shift;
+       u32 addr_mask;
+       u8 addr_shift;
+       u32 block_size;
+       u32 page_size;
+};
+
+struct egon_bt0_header {
+       u32 jump;
+       u8 magic[8];
+       u32 checksum;
+       u32 length;
+       u32 header_size;
+       u8 header_version[4];
+       u8 boot_vsn[4];
+       u8 egon_vsn[4];
+       u8 platform[8];
+};
+
+static const u8 egon_bt0_magic[8] = {
+       'e', 'G', 'O', 'N', '.', 'B', 'T', '0'
+};
+
+static struct sunxi_nand_config sunxi_known_nands[] = {
+       {
+               .name = "Macronix MX35LF1GE4AB",
+               .jedec_id = 0x00c212c2,
+               .page_mask = 0x00ffffff,
+               .page_shift = 11,
+               .addr_mask = 0x7ff,
+               .addr_shift = 0,
+               .block_size = 128 * 1024,
+               .page_size = 2048,
+       },
+       {
+               .name = "Winbond W25N01GVxxIG",
+               .jedec_id = 0x00efaa21,
+               .page_mask = 0x0000ffff,
+               .page_shift = 11,
+               .addr_mask = 0x7ff,
+               .addr_shift = 0,
+               .block_size = 128 * 1024,
+               .page_size = 2048,
+       },
+       {
+               /* Some W25N01G samples report device id byte 0x20. */
+               .name = "Winbond W25N01G",
+               .jedec_id = 0x00efaa20,
+               .page_mask = 0x0000ffff,
+               .page_shift = 11,
+               .addr_mask = 0x7ff,
+               .addr_shift = 0,
+               .block_size = 128 * 1024,
+               .page_size = 2048,
+       },
+       {
+               .name = "GigaDevice GD5F1GQ4",
+               .jedec_id = 0x00c8b148,
+               .page_mask = 0x00ffffff,
+               .page_shift = 11,
+               .addr_mask = 0x7ff,
+               .addr_shift = 0,
+               .block_size = 128 * 1024,
+               .page_size = 2048,
+       },
+       { /* sentinel */ }
+};
+
+#ifdef CONFIG_SPL_SPINAND_SUNXI_GENERIC_2K
+static struct sunxi_nand_config sunxi_generic_nand_config = {
+       .name = "Generic 2K SPI-NAND",
+       .jedec_id = 0xffffffff,
+       .page_mask = 0x00ffffff,
+       .page_shift = 11,
+       .addr_mask = 0x7ff,
+       .addr_shift = 0,
+       .block_size = 128 * 1024,
+       .page_size = 2048,
+};
+#endif
+
+static struct sunxi_nand_config *sunxi_spinand_enumerate(u32 jedec_id)
+{
+       struct sunxi_nand_config *ptr = sunxi_known_nands;
+
+       while (ptr->name) {
+               if (jedec_id == ptr->jedec_id)
+                       return ptr;
+               ptr++;
+       }
+#ifdef CONFIG_SPL_SPINAND_SUNXI_GENERIC_2K
+       return &sunxi_generic_nand_config;
+#else
+       return NULL;
+#endif
+}
+
+static uintptr_t spi0_base_address(void)
+{
+       /* V3s / sun6i-gen SPI0 */
+       return 0x01c68000;
+}
+
+static void spi0_pinmux_setup(unsigned int pin_function)
+{
+       unsigned int pin;
+
+       for (pin = SUNXI_GPC(0); pin <= SUNXI_GPC(2); pin++)
+               sunxi_gpio_set_cfgpin(pin, pin_function);
+       sunxi_gpio_set_cfgpin(SUNXI_GPC(3), pin_function);
+}
+
+static void spi0_enable_clock(void)
+{
+       uintptr_t base = spi0_base_address();
+
+       setbits_le32(SUN6I_BUS_SOFT_RST_REG0, BIT(AHB_RESET_SPI0_SHIFT));
+       setbits_le32(CCM_AHB_GATING0, BIT(AHB_GATE_OFFSET_SPI0));
+       writel(SPI0_CLK_DIV_BY_4, base + SUN6I_SPI0_CCTL);
+       writel(BIT(31), CCM_SPI0_CLK);
+       setbits_le32(base + SUN6I_SPI0_GCR,
+                    SUN6I_CTL_MASTER | SUN6I_CTL_ENABLE | SUN6I_CTL_SRST);
+       while (readl(base + SUN6I_SPI0_GCR) & SUN6I_CTL_SRST)
+               ;
+}
+
+static void spi0_disable_clock(void)
+{
+       uintptr_t base = spi0_base_address();
+
+       clrbits_le32(base + SUN6I_SPI0_GCR, SUN6I_CTL_MASTER | 
SUN6I_CTL_ENABLE);
+       writel(0, CCM_SPI0_CLK);
+       clrbits_le32(CCM_AHB_GATING0, BIT(AHB_GATE_OFFSET_SPI0));
+       clrbits_le32(SUN6I_BUS_SOFT_RST_REG0, BIT(AHB_RESET_SPI0_SHIFT));
+}
+
+static void spi0_init(void)
+{
+       uintptr_t base = spi0_base_address();
+
+       spi0_pinmux_setup(SUNXI_GPC_SPI0);
+       spi0_enable_clock();
+       writel(0x01, base + SUN6I_SPI0_WCR);
+}
+
+static void spi0_deinit(void)
+{
+       spi0_disable_clock();
+       spi0_pinmux_setup(SUNXI_GPIO_DISABLE);
+}
+
+static void sunxi_spi0_load_page(struct sunxi_nand_config *config, u32 addr)
+{
+       uintptr_t base = spi0_base_address();
+       u8 status = 0x01;
+
+       addr = (addr >> config->page_shift) & config->page_mask;
+
+       writel(4, base + SUN6I_SPI0_MBC);
+       writel(4, base + SUN6I_SPI0_MTC);
+       writel(4, base + SUN6I_SPI0_BCC);
+
+       writeb(0x13, base + SUN6I_SPI0_TXD);
+       writeb((u8)(addr >> 16), base + SUN6I_SPI0_TXD);
+       writeb((u8)(addr >> 8), base + SUN6I_SPI0_TXD);
+       writeb((u8)addr, base + SUN6I_SPI0_TXD);
+       setbits_le32(base + SUN6I_SPI0_TCR, SUN6I_TCR_XCH);
+
+       while ((readl(base + SUN6I_SPI0_FIFO_STA) & 0x7f0000) > 0)
+               ;
+       while ((readl(base + SUN6I_SPI0_FIFO_STA) & 0x7f) < 4)
+               ;
+       readl(base + SUN6I_SPI0_RXD);
+
+       do {
+               ndelay(200);
+               writel(3, base + SUN6I_SPI0_MBC);
+               writel(2, base + SUN6I_SPI0_MTC);
+               writel(2, base + SUN6I_SPI0_BCC);
+               writeb(0x0f, base + SUN6I_SPI0_TXD);
+               writeb(0xc0, base + SUN6I_SPI0_TXD);
+               setbits_le32(base + SUN6I_SPI0_TCR, SUN6I_TCR_XCH);
+               while ((readl(base + SUN6I_SPI0_FIFO_STA) & 0x7f) < 3)
+                       ;
+               readb(base + SUN6I_SPI0_RXD);
+               readb(base + SUN6I_SPI0_RXD);
+               status = readb(base + SUN6I_SPI0_RXD);
+       } while (status & 0x01);
+}
+
+static void sunxi_spi0_read_cache(struct sunxi_nand_config *config,
+                                 u8 *buf, u32 addr, u32 len)
+{
+       uintptr_t base = spi0_base_address();
+
+       addr = (addr >> config->addr_shift) & config->addr_mask;
+
+       writel(4 + len, base + SUN6I_SPI0_MBC);
+       writel(4, base + SUN6I_SPI0_MTC);
+       writel(4, base + SUN6I_SPI0_BCC);
+
+       writeb(0x0b, base + SUN6I_SPI0_TXD);
+       writeb((u8)(addr >> 8), base + SUN6I_SPI0_TXD);
+       writeb((u8)addr, base + SUN6I_SPI0_TXD);
+       writeb(DUMMY_BURST_BYTE, base + SUN6I_SPI0_TXD);
+       setbits_le32(base + SUN6I_SPI0_TCR, SUN6I_TCR_XCH);
+
+       while ((readl(base + SUN6I_SPI0_FIFO_STA) & 0x7f) < 4 + len)
+               ;
+       readl(base + SUN6I_SPI0_RXD);
+
+       while (len-- > 0)
+               *buf++ = readb(base + SUN6I_SPI0_RXD);
+
+       ndelay(100);
+}
+
+static void spi0_read_data(struct sunxi_nand_config *config, void *buf,
+                          u32 addr, u32 len)
+{
+       u8 *buf8 = buf;
+       u32 chunk_len;
+       u32 curr_page;
+       u32 last_page = (addr >> config->page_shift) & config->page_mask;
+
+       sunxi_spi0_load_page(config, addr);
+
+       while (len > 0) {
+               curr_page = (addr >> config->page_shift) & config->page_mask;
+               if (curr_page > last_page) {
+                       sunxi_spi0_load_page(config, addr);
+                       last_page = curr_page;
+               }
+
+               chunk_len = min(len, (u32)SPI_READ_MAX_SIZE);
+               if ((((addr + chunk_len) >> config->page_shift) &
+                    config->page_mask) > curr_page)
+                       chunk_len = ((curr_page + 1) << config->page_shift) -
+                                   addr;
+
+               sunxi_spi0_read_cache(config, buf8, addr, chunk_len);
+               len -= chunk_len;
+               buf8 += chunk_len;
+               addr += chunk_len;
+       }
+}
+
+static u32 spi0_read_id(void)
+{
+       uintptr_t base = spi0_base_address();
+       u8 idbuf[3];
+
+       writel(5, base + SUN6I_SPI0_MBC);
+       writel(2, base + SUN6I_SPI0_MTC);
+       writel(2, base + SUN6I_SPI0_BCC);
+       writeb(0x9f, base + SUN6I_SPI0_TXD);
+       writeb(DUMMY_BURST_BYTE, base + SUN6I_SPI0_TXD);
+       setbits_le32(base + SUN6I_SPI0_TCR, SUN6I_TCR_XCH);
+       while ((readl(base + SUN6I_SPI0_FIFO_STA) & 0x7f) < 5)
+               ;
+       readb(base + SUN6I_SPI0_RXD);
+       readb(base + SUN6I_SPI0_RXD);
+       idbuf[0] = readb(base + SUN6I_SPI0_RXD);
+       idbuf[1] = readb(base + SUN6I_SPI0_RXD);
+       idbuf[2] = readb(base + SUN6I_SPI0_RXD);
+       ndelay(100);
+
+       return idbuf[2] | (idbuf[1] << 8) | (idbuf[0] << 16);
+}
+
+static u32 sunxi_spi0_count_spls(struct sunxi_nand_config *config,
+                                struct egon_bt0_header *self)
+{
+       u8 buffer[EGON_BT0_HEADER_SIZE];
+       struct egon_bt0_header *egon = (struct egon_bt0_header *)buffer;
+       u32 entry_pages[8] = { 0, 32, 64, 96, 128, 160, 192, 224 };
+       u32 spls = 0;
+       int i;
+
+       for (i = 0; i < 8; i++) {
+               u32 address = entry_pages[i] * config->page_size;
+               int hits = 0;
+
+               sunxi_spi0_load_page(config, address);
+               sunxi_spi0_read_cache(config, buffer, address,
+                                     EGON_BT0_HEADER_SIZE);
+
+               if (!memcmp(egon->magic, egon_bt0_magic, 8)) {
+                       spls++;
+                       continue;
+               }
+
+               if (egon->jump == self->jump)
+                       hits++;
+               if (egon->checksum == self->checksum)
+                       hits++;
+               if (egon->length == self->length)
+                       hits++;
+               if (egon->header_size == self->header_size)
+                       hits++;
+               if (!memcmp(egon->header_version, self->header_version, 4))
+                       hits++;
+               if (!memcmp(egon->boot_vsn, self->boot_vsn, 4))
+                       hits++;
+               if (!memcmp(egon->egon_vsn, self->egon_vsn, 4))
+                       hits++;
+               if (!memcmp(egon->platform, self->platform, 8))
+                       hits++;
+               if (hits > 4)
+                       spls++;
+       }
+
+       return spls;
+}
+
+static int spl_spinand_load_image(struct spl_image_info *spl_image,
+                                 struct spl_boot_device *bootdev)
+{
+       struct legacy_img_hdr *header;
+       struct sunxi_nand_config *config;
+       struct egon_bt0_header *egon;
+       u32 id, uboot_offset, total_spl_size, spls;
+       int ret = -EINVAL;
+       int tries = 0;
+
+       header = (struct legacy_img_hdr *)CONFIG_TEXT_BASE;
+
+       spi0_init();
+       id = spi0_read_id();
+       if (!id) {
+               spi0_deinit();
+               printf("sunxi SPI-NAND: no device\n");
+               return -ENODEV;
+       }
+
+       config = sunxi_spinand_enumerate(id);
+       if (!config) {
+               spi0_deinit();
+               printf("sunxi SPI-NAND: unknown chip %06x\n", id);
+               return -ENODEV;
+       }
+
+       debug("sunxi SPI-NAND: %s (%06x)\n", config->name, id);
+
+       egon = map_sysmem(0x00000000, EGON_BT0_HEADER_SIZE);
+       spls = sunxi_spi0_count_spls(config, egon);
+       if (spls < 1)
+               spls = 1;
+       total_spl_size = ((config->page_size * 32) * (spls - 1)) +
+                        ((config->page_size / 1024) * egon->length);
+       /* Align U-Boot proper to an erase-block boundary after the SPL copies 
*/
+       uboot_offset = round_up(total_spl_size, config->block_size);
+       unmap_sysmem(egon);
+
+       do {
+               debug("sunxi SPI-NAND: try U-Boot @ 0x%x\n", uboot_offset);
+               spi0_read_data(config, (void *)header, uboot_offset, 0x40);
+               if (image_check_hcrc(header)) {
+                       ret = spl_parse_image_header(spl_image, bootdev, 
header);
+                       if (!ret) {
+                               spi0_read_data(config,
+                                              (void 
*)(uintptr_t)spl_image->load_addr,
+                                              uboot_offset, spl_image->size);
+                               break;
+                       }
+                       uboot_offset += round_up(image_get_image_size(header),
+                                                config->block_size);
+               } else {
+                       uboot_offset += config->block_size;
+               }
+       } while (++tries < SPINAND_UBOOT_MAX_TRIES);
+
+       spi0_deinit();
+
+       if (ret)
+               printf("sunxi SPI-NAND: U-Boot not found\n");
+
+       return ret;
+}
+
+/*
+ * BROM reports SPI for SPI NAND on V3s; register on BOOT_DEVICE_SPI.
+ * Do not enable together with SPL_SPI_SUNXI (same boot device).
+ */
+SPL_LOAD_IMAGE_METHOD("sunxi SPI-NAND", 0, BOOT_DEVICE_SPI,
+                     spl_spinand_load_image);
diff --git a/board/sunxi/board.c b/board/sunxi/board.c
index 3d1afec7c66..3e2fb48d981 100644
--- a/board/sunxi/board.c
+++ b/board/sunxi/board.c
@@ -160,6 +160,15 @@ enum env_location env_get_location(enum env_operation op, 
int prio)
                        return ENVL_NAND;
                break;
        case BOOT_DEVICE_SPI:
+               /*
+                * On V3s, SPI NAND boot is still reported as BOOT_DEVICE_SPI.
+                * Do not treat that as SPI NOR for the environment.
+                */
+               if (IS_ENABLED(CONFIG_SPL_SPINAND_SUNXI)) {
+                       if (IS_ENABLED(CONFIG_ENV_IS_IN_FAT))
+                               return ENVL_FAT;
+                       break;
+               }
                if (prio == 0 && IS_ENABLED(CONFIG_ENV_IS_IN_SPI_FLASH))
                        return ENVL_SPI_FLASH;
                if (IS_ENABLED(CONFIG_ENV_IS_IN_FAT))
-- 
2.17.1

Reply via email to