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

#include <amdblocks/lpc.h>
#include <amdblocks/spi.h>
#include <assert.h>
#include <boot_device.h>
#include <cbfs.h>
#include <commonlib/bsd/helpers.h>
#include <commonlib/region.h>
#include <console/console.h>
#include <delay.h>
#include <device/pci_ops.h>
#include <soc/pci_devs.h>
#include <spi_flash.h>
#include <string.h>
#include <thread.h>
#include <types.h>

/* The ROM is memory mapped just below 4GiB. Form a pointer for the base. */
#define rom_base ((void *)(uintptr_t)(0x100000000ULL - CONFIG_ROM_SIZE))

struct spi_dma_transaction {
	uint8_t *destination;
	size_t source;
	size_t size;
	size_t transfer_size;
	size_t remaining;
};

static ssize_t spi_dma_readat_mmap(const struct region_device *rd, void *b, size_t offset,
				   size_t size)
{
	const struct mem_region_device *mdev;

	mdev = container_of(rd, __typeof__(*mdev), rdev);

	memcpy(b, &mdev->base[offset], size);

	return size;
}

static bool spi_dma_is_busy(void)
{
	return pci_read_config32(SOC_LPC_DEV, LPC_ROM_DMA_EC_HOST_CONTROL)
	       & LPC_ROM_DMA_CTRL_START;
}

static bool spi_dma_has_error(void)
{
	return pci_read_config32(SOC_LPC_DEV, LPC_ROM_DMA_EC_HOST_CONTROL)
	       & LPC_ROM_DMA_CTRL_ERROR;
}

static bool can_use_dma(void *destination, size_t source, size_t size)
{
	/*
	 * Print a notice if reading more than 1024 bytes using mmap. This makes
	 * it easier to debug why the SPI DMA wasn't used.
	 */
	const size_t warning_size = 1024;

	if (size < LPC_ROM_DMA_MIN_ALIGNMENT)
		return false;

	if (!IS_ALIGNED((uintptr_t)destination, LPC_ROM_DMA_MIN_ALIGNMENT)) {
		if (size > warning_size)
			printk(BIOS_DEBUG, "Target %p is unaligned\n", destination);
		return false;
	}

	if (!IS_ALIGNED(source, LPC_ROM_DMA_MIN_ALIGNMENT)) {
		if (size > warning_size)
			printk(BIOS_DEBUG, "Source %#zx is unaligned\n", source);
		return false;
	}

	return true;
}

static void start_spi_dma_transaction(struct spi_dma_transaction *transaction)
{
	uint32_t ctrl;

	printk(BIOS_SPEW, "%s: dest: %p, source: %#zx, remaining: %zu\n", __func__,
	       transaction->destination, transaction->source, transaction->remaining);

	/*
	 * We should have complete control over the DMA controller, so there shouldn't
	 * be any outstanding transactions.
	 */
	assert(!spi_dma_is_busy());
	assert(IS_ALIGNED((uintptr_t)transaction->destination, LPC_ROM_DMA_MIN_ALIGNMENT));
	assert(IS_ALIGNED(transaction->source, LPC_ROM_DMA_MIN_ALIGNMENT));
	assert(transaction->remaining >= LPC_ROM_DMA_MIN_ALIGNMENT);

	pci_write_config32(SOC_LPC_DEV, LPC_ROM_DMA_SRC_ADDR, transaction->source);
	pci_write_config32(SOC_LPC_DEV, LPC_ROM_DMA_DST_ADDR,
			   (uintptr_t)transaction->destination);

	ctrl = pci_read_config32(SOC_LPC_DEV, LPC_ROM_DMA_EC_HOST_CONTROL);
	ctrl &= ~LPC_ROM_DMA_CTRL_DW_COUNT_MASK;

	transaction->transfer_size =
		MIN(LPC_ROM_DMA_CTRL_MAX_BYTES,
		    ALIGN_DOWN(transaction->remaining, LPC_ROM_DMA_MIN_ALIGNMENT));

	ctrl |= LPC_ROM_DMA_CTRL_DW_COUNT(transaction->transfer_size);
	ctrl |= LPC_ROM_DMA_CTRL_ERROR; /* Clear error */
	ctrl |= LPC_ROM_DMA_CTRL_START;

	/*
	 * Ensure we have exclusive access to the SPI controller before starting the LPC SPI DMA
	 * transaction.
	 */
	thread_mutex_lock(&spi_hw_mutex);

	pci_write_config32(SOC_LPC_DEV, LPC_ROM_DMA_EC_HOST_CONTROL, ctrl);
}

/* Returns true if transaction is still in progress. */
static bool continue_spi_dma_transaction(const struct region_device *rd,
					 struct spi_dma_transaction *transaction)
{
	/* Verify we are looking at the correct transaction */
	assert(pci_read_config32(SOC_LPC_DEV, LPC_ROM_DMA_SRC_ADDR) == transaction->source);

	if (spi_dma_is_busy())
		return true;

	/*
	 * Unlock the SPI mutex between DMA transactions to allow other users of the SPI
	 * controller to interleave their transactions.
	 */
	thread_mutex_unlock(&spi_hw_mutex);

	if (spi_dma_has_error()) {
		printk(BIOS_ERR, "SPI DMA failure: dest: %p, source: %#zx, size: %zu\n",
		       transaction->destination, transaction->source,
		       transaction->transfer_size);
		return false;
	}

	transaction->destination += transaction->transfer_size;
	transaction->source += transaction->transfer_size;
	transaction->remaining -= transaction->transfer_size;

	if (transaction->remaining >= LPC_ROM_DMA_MIN_ALIGNMENT) {
		start_spi_dma_transaction(transaction);
		return true;
	}

	if (transaction->remaining > 0) {
		/* Use mmap to finish off the transfer */
		spi_dma_readat_mmap(rd, transaction->destination, transaction->source,
				    transaction->remaining);

		transaction->destination += transaction->remaining;
		transaction->source += transaction->remaining;
		transaction->remaining -= transaction->remaining;
	}

	return false;
}

static struct thread_mutex spi_dma_hw_mutex;

static ssize_t spi_dma_readat_dma(const struct region_device *rd, void *destination,
				  size_t source, size_t size)
{
	struct spi_dma_transaction transaction = {
		.destination = destination,
		.source = source,
		.size = size,
		.remaining = size,
	};

	printk(BIOS_SPEW, "%s: start: dest: %p, source: %#zx, size: %zu\n", __func__,
	       destination, source, size);

	thread_mutex_lock(&spi_dma_hw_mutex);

	start_spi_dma_transaction(&transaction);

	do {
		udelay(2);
	} while (continue_spi_dma_transaction(rd, &transaction));

	thread_mutex_unlock(&spi_dma_hw_mutex);

	printk(BIOS_SPEW, "%s: end: dest: %p, source: %#zx, remaining: %zu\n",
	       __func__, destination, source, transaction.remaining);

	/* Allow queued up transaction to continue */
	thread_yield();

	if (transaction.remaining)
		return -1;

	return transaction.size;
}

static ssize_t spi_dma_readat(const struct region_device *rd, void *b, size_t offset,
			      size_t size)
{
	if (can_use_dma(b, offset, size))
		return spi_dma_readat_dma(rd, b, offset, size);
	else
		return spi_dma_readat_mmap(rd, b, offset, size);
}

static void *spi_dma_mmap(const struct region_device *rd, size_t offset, size_t size)
{
	const struct mem_region_device *mdev;
	void *mapping;

	mdev = container_of(rd, __typeof__(*mdev), rdev);

	if (!CONFIG_CBFS_CACHE_SIZE)
		return &mdev->base[offset];

	mapping = mem_pool_alloc(&cbfs_cache, size);
	if (!mapping) {
		printk(BIOS_INFO, "%s: Could not allocate %zu bytes from memory pool\n",
		       __func__, size);
		/* Fall-back to memory map */
		return &mdev->base[offset];
	}

	if (spi_dma_readat(rd, mapping, offset, size) != size) {
		printk(BIOS_ERR, "%s: Error reading into mmap buffer\n", __func__);
		mem_pool_free(&cbfs_cache, mapping);
		/* Fall-back to memory mapped read - not expected to fail atleast for now */
		spi_dma_readat_mmap(rd, mapping, offset, size);
	}

	return mapping;
}

static int spi_dma_munmap(const struct region_device *rd __always_unused, void *mapping)
{
	if (CONFIG_CBFS_CACHE_SIZE)
		mem_pool_free(&cbfs_cache, mapping);
	return 0;
}

const struct region_device_ops spi_dma_rdev_ro_ops = {
	.mmap = spi_dma_mmap,
	.munmap = spi_dma_munmap,
	.readat = spi_dma_readat,
};

static const struct mem_region_device boot_dev = {
	.base = rom_base,
	.rdev = REGION_DEV_INIT(&spi_dma_rdev_ro_ops, 0, CONFIG_ROM_SIZE),
};

const struct region_device *boot_device_ro(void)
{
	return &boot_dev.rdev;
}

uint32_t spi_flash_get_mmap_windows(struct flash_mmap_window *table)
{
	table->flash_base = 0;
	table->host_base = (uint32_t)(uintptr_t)rom_base;
	table->size = CONFIG_ROM_SIZE;

	return 1;
}

/*
 * Without this magic bit, the SPI DMA controller will write 0s into the destination if an MMAP
 * read happens while a DMA transaction is in progress. i.e., PSP is reading from SPI. The bit
 * that fixes this was added to Cezanne, Renoir and later SoCs. So the SPI DMA controller is not
 * reliable on any prior generations.
 */
static void spi_dma_fix(void)
{
	/* Internal only registers */
	uint8_t val = spi_read8(SPI_MISC_CNTRL);
	val |= BIT(6);
	spi_write8(SPI_MISC_CNTRL, val);
}

void boot_device_init(void)
{
	spi_dma_fix();
}
