/* SPDX-License-Identifier: GPL-2.0-only */

#include <arch/cache.h>
#include <boot_device.h>
#include <console/console.h>
#include <soc/alternate_cbfs.h>
#include <soc/power.h>
#include <soc/spi.h>
#include <symbols.h>

/* This allows USB A-A firmware upload from a compatible host in four parts:
 * The first two are the bare BL1 and the coreboot boot block, which are just
 * written to their respective loading addresses. These transfers are initiated
 * by the IROM / BL1, so this code has nothing to do with them.
 *
 * The third transfer is a valid CBFS image that contains only the romstage,
 * and must be small enough to fit into the PRE_RAM CBFS cache in
 * IRAM. It is loaded when this function gets called in the boot block, and
 * the normal CBFS code extracts the romstage from it.
 *
 * The fourth transfer is also a CBFS image, but can be of arbitrary size and
 * should contain all available stages/payloads/etc. It is loaded when this
 * function is called a second time at the end of the romstage, and copied to
 * the romstage/ramstage CBFS cache in DRAM. It will reside there for the
 * rest of the firmware's lifetime and all subsequent stages can just directly
 * reference it there.
 */
static int usb_cbfs_open(void)
{
	if (!ENV_ROMSTAGE_OR_BEFORE)
		return 0;

	static int first_run = 1;
	int (*irom_load_usb)(void) = *irom_load_image_from_usb_ptr;

	if (!first_run)
		return 0;

	dcache_mmu_disable();
	if (!irom_load_usb()) {
		dcache_mmu_enable();
		printk(BIOS_EMERG, "Unable to load CBFS image via USB!\n");
		return -1;
	}
	dcache_mmu_enable();

	/*
	 * We need to trust the host/irom to copy the image to our
	 * _cbfs_cache address... there is no way to control or even
	 * check the transfer size or target address from our side.
	 */

	printk(BIOS_DEBUG, "USB A-A transfer successful, CBFS image should now"
		" be at %p\n", _cbfs_cache);
	first_run = 0;
	return 0;
}

/*
 * SDMMC works very similar to USB A-A: we copy the CBFS image into memory
 * and read it from there. While SDMMC would also allow direct block by block
 * on-demand reading, we might run into problems if we call back into the IROM
 * in very late boot stages (e.g. after initializing/changing MMC clocks)... so
 * this seems like a safer approach. It also makes it easy to pass our image
 * down to payloads.
 */
static int sdmmc_cbfs_open(void)
{
	if (!ENV_ROMSTAGE_OR_BEFORE)
		return 0;

	/*
	 * In the bootblock, we just copy the small part that fits in the buffer
	 * and hope that it's enough (since the romstage is currently always the
	 * first component in the image, this should work out). In the romstage,
	 * we copy until our cache is full (currently 12M) to avoid the pain of
	 * figuring out the true image size from in here. Since this is mainly a
	 * developer/debug boot mode, those shortcomings should be bearable.
	 */
	const u32 count = REGION_SIZE(cbfs_cache) / 512;
	static int first_run = 1;
	int (*irom_load_sdmmc)(u32 start, u32 count, void *dst) =
		*irom_sdmmc_read_blocks_ptr;

	if (!first_run)
		return 0;

	dcache_mmu_disable();
	if (!irom_load_sdmmc(1, count, _cbfs_cache)) {
		dcache_mmu_enable();
		printk(BIOS_EMERG, "Unable to load CBFS image from SDMMC!\n");
		return -1;
	}
	dcache_mmu_enable();

	printk(BIOS_DEBUG, "SDMMC read successful, CBFS image should now be"
		" at %p\n", _cbfs_cache);
	first_run = 0;
	return 0;
}

static struct mem_region_device alternate_rdev =
	MEM_REGION_DEV_RO_INIT(_cbfs_cache, REGION_SIZE(cbfs_cache));

const struct region_device *boot_device_ro(void)
{
	if (*iram_secondary_base == SECONDARY_BASE_BOOT_USB)
		return &alternate_rdev.rdev;

	switch (exynos_power->om_stat & OM_STAT_MASK) {
	case OM_STAT_SDMMC:
		return &alternate_rdev.rdev;
	case OM_STAT_SPI:
		return exynos_spi_boot_device();
	default:
		printk(BIOS_EMERG, "Exynos OM_STAT value 0x%x not supported!\n",
			exynos_power->om_stat);
		return NULL;
	}
}

void boot_device_init(void)
{
	if (*iram_secondary_base == SECONDARY_BASE_BOOT_USB) {
		printk(BIOS_DEBUG, "Using Exynos alternate boot mode USB A-A\n");
		usb_cbfs_open();
		return;
	}

	switch (exynos_power->om_stat & OM_STAT_MASK) {
	case OM_STAT_SDMMC:
		printk(BIOS_DEBUG, "Using Exynos alternate boot mode SDMMC\n");
		sdmmc_cbfs_open();
		break;
	case OM_STAT_SPI:
		exynos_init_spi_boot_device();
		break;
	default:
		printk(BIOS_EMERG, "Exynos OM_STAT value 0x%x not supported!\n",
			exynos_power->om_stat);
	}
}
