/*
 * Copyright (c) 2022 ASPEED Technology Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/kernel.h>
#include <zephyr/drivers/syscon.h>
#include <zephyr/sys/barrier.h>

/*
 * cache area control: each bit controls 32KB cache area
 *	1: cacheable
 *	0: no-cache
 *
 *	bit[0]: 1st 32KB from 0x0000_0000 to 0x0000_7fff
 *	bit[1]: 2nd 32KB from 0x0000_8000 to 0x0000_ffff
 *	...
 *	bit[22]: 23th 32KB from 0x000a_8000 to 0x000a_ffff
 *	bit[23]: 24th 32KB from 0x000b_0000 to 0x000b_ffff
 */
#define CACHE_AREA_CTRL_REG	0xa50
#define CACHE_INVALID_REG	0xa54
#define CACHE_FUNC_CTRL_REG	0xa58

#define CACHED_SRAM_ADDR	CONFIG_SRAM_BASE_ADDRESS
#define CACHED_SRAM_SIZE	KB(CONFIG_SRAM_SIZE)
#define CACHED_SRAM_END		(CACHED_SRAM_ADDR + CACHED_SRAM_SIZE - 1)

#define CACHE_AREA_SIZE_LOG2	15
#define CACHE_AREA_SIZE		(1 << CACHE_AREA_SIZE_LOG2)

#define DCACHE_INVALID(addr)	(BIT(31) | ((addr & GENMASK(10, 0)) << 16))
#define ICACHE_INVALID(addr)	(BIT(15) | ((addr & GENMASK(10, 0)) << 0))

#define ICACHE_CLEAN		BIT(2)
#define DCACHE_CLEAN		BIT(1)
#define CACHE_ENABLE		BIT(0)

/* cache size = 32B * 128 = 4KB */
#define CACHE_LINE_SIZE_LOG2	5
#define CACHE_LINE_SIZE		(1 << CACHE_LINE_SIZE_LOG2)
#define N_CACHE_LINE		128
#define CACHE_ALIGNED_ADDR(addr) \
	((addr >> CACHE_LINE_SIZE_LOG2) << CACHE_LINE_SIZE_LOG2)

/* prefetch buffer */
#define PREFETCH_BUF_SIZE	CACHE_LINE_SIZE

static void aspeed_cache_init(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	uint32_t start_bit, end_bit, max_bit;

	/* set all cache areas to no-cache by default */
	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, 0);

	/* calculate how many areas need to be set */
	max_bit = 8 * sizeof(uint32_t) - 1;
	start_bit = MIN(max_bit, CACHED_SRAM_ADDR >> CACHE_AREA_SIZE_LOG2);
	end_bit = MIN(max_bit, CACHED_SRAM_END >> CACHE_AREA_SIZE_LOG2);
	syscon_write_reg(dev, CACHE_AREA_CTRL_REG, GENMASK(end_bit, start_bit));

	/* enable cache */
	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, CACHE_ENABLE);
}

/**
 * @brief get aligned address and the number of cachline to be invalied
 * @param [IN] addr - start address to be invalidated
 * @param [IN] size - size in byte
 * @param [OUT] p_aligned_addr - pointer to the cacheline aligned address variable
 * @return number of cacheline to be invalidated
 *
 *  * addr
 *   |--------size-------------|
 * |-----|-----|-----|-----|-----|
 *  \                             \
 *   head                          tail
 *
 * example 1:
 * addr = 0x100 (cacheline aligned), size = 64
 * then head = 0x100, number of cache line to be invalidated = 64 / 32 = 2
 * which means range [0x100, 0x140) will be invalidated
 *
 * example 2:
 * addr = 0x104 (cacheline unaligned), size = 64
 * then head = 0x100, number of cache line to be invalidated = 1 + 64 / 32 = 3
 * which means range [0x100, 0x160) will be invalidated
 */
static uint32_t get_n_cacheline(uint32_t addr, uint32_t size, uint32_t *p_head)
{
	uint32_t n = 0;
	uint32_t tail;

	/* head */
	*p_head = CACHE_ALIGNED_ADDR(addr);

	/* roundup the tail address */
	tail = addr + size + (CACHE_LINE_SIZE - 1);
	tail = CACHE_ALIGNED_ADDR(tail);

	n = (tail - *p_head) >> CACHE_LINE_SIZE_LOG2;

	return n;
}

void cache_data_enable(void)
{
	aspeed_cache_init();
}

void cache_data_disable(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));

	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, 0);
}

void cache_instr_enable(void)
{
	aspeed_cache_init();
}

void cache_instr_disable(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));

	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, 0);
}

int cache_data_invd_all(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	uint32_t ctrl;
	unsigned int key = 0;

	syscon_read_reg(dev, CACHE_FUNC_CTRL_REG, &ctrl);

	/* enter critical section */
	if (!k_is_in_isr()) {
		key = irq_lock();
	}

	ctrl &= ~DCACHE_CLEAN;
	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, ctrl);

	barrier_dsync_fence_full();
	ctrl |= DCACHE_CLEAN;
	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, ctrl);
	barrier_dsync_fence_full();

	/* exit critical section */
	if (!k_is_in_isr()) {
		irq_unlock(key);
	}

	return 0;
}

int cache_data_invd_range(void *addr, size_t size)
{
	uint32_t aligned_addr, i, n;
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	unsigned int key = 0;

	if (((uint32_t)addr < CACHED_SRAM_ADDR) ||
	    ((uint32_t)addr > CACHED_SRAM_END)) {
		return 0;
	}

	/* enter critical section */
	if (!k_is_in_isr()) {
		key = irq_lock();
	}

	n = get_n_cacheline((uint32_t)addr, size, &aligned_addr);

	for (i = 0; i < n; i++) {
		syscon_write_reg(dev, CACHE_INVALID_REG, 0);
		syscon_write_reg(dev, CACHE_INVALID_REG, DCACHE_INVALID(aligned_addr));
		aligned_addr += CACHE_LINE_SIZE;
	}
	barrier_dsync_fence_full();

	/* exit critical section */
	if (!k_is_in_isr()) {
		irq_unlock(key);
	}

	return 0;
}

int cache_instr_invd_all(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	uint32_t ctrl;
	unsigned int key = 0;

	syscon_read_reg(dev, CACHE_FUNC_CTRL_REG, &ctrl);

	/* enter critical section */
	if (!k_is_in_isr()) {
		key = irq_lock();
	}

	ctrl &= ~ICACHE_CLEAN;
	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, ctrl);
	barrier_isync_fence_full();
	ctrl |= ICACHE_CLEAN;
	syscon_write_reg(dev, CACHE_FUNC_CTRL_REG, ctrl);
	barrier_isync_fence_full();

	/* exit critical section */
	if (!k_is_in_isr()) {
		irq_unlock(key);
	}

	return 0;
}

int cache_instr_invd_range(void *addr, size_t size)
{
	uint32_t aligned_addr, i, n;
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	unsigned int key = 0;

	if (((uint32_t)addr < CACHED_SRAM_ADDR) ||
	    ((uint32_t)addr > CACHED_SRAM_END)) {
		return 0;
	}

	n = get_n_cacheline((uint32_t)addr, size, &aligned_addr);

	/* enter critical section */
	if (!k_is_in_isr()) {
		key = irq_lock();
	}

	for (i = 0; i < n; i++) {
		syscon_write_reg(dev, CACHE_INVALID_REG, 0);
		syscon_write_reg(dev, CACHE_INVALID_REG, ICACHE_INVALID(aligned_addr));
		aligned_addr += CACHE_LINE_SIZE;
	}
	barrier_dsync_fence_full();

	/* exit critical section */
	if (!k_is_in_isr()) {
		irq_unlock(key);
	}

	return 0;
}

int cache_data_flush_all(void)
{
	return -ENOTSUP;
}

int cache_data_flush_and_invd_all(void)
{
	return -ENOTSUP;
}

int cache_data_flush_range(void *addr, size_t size)
{
	ARG_UNUSED(addr);
	ARG_UNUSED(size);

	return -ENOTSUP;
}

int cache_data_flush_and_invd_range(void *addr, size_t size)
{
	ARG_UNUSED(addr);
	ARG_UNUSED(size);

	return -ENOTSUP;
}

int cache_instr_flush_all(void)
{
	return -ENOTSUP;
}

int cache_instr_flush_and_invd_all(void)
{
	return -ENOTSUP;
}

int cache_instr_flush_range(void *addr, size_t size)
{
	ARG_UNUSED(addr);
	ARG_UNUSED(size);

	return -ENOTSUP;
}

int cache_instr_flush_and_invd_range(void *addr, size_t size)
{
	ARG_UNUSED(addr);
	ARG_UNUSED(size);

	return -ENOTSUP;
}


#ifdef CONFIG_DCACHE_LINE_SIZE_DETECT
size_t cache_data_line_size_get(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	uint32_t ctrl;

	syscon_read_reg(dev, CACHE_FUNC_CTRL_REG, &ctrl);

	return (ctrl & CACHE_ENABLE) ? CACHE_LINE_SIZE : 0;
}
#endif /* CONFIG_DCACHE_LINE_SIZE_DETECT */

#ifdef CONFIG_ICACHE_LINE_SIZE_DETECT
size_t cache_instr_line_size_get(void)
{
	const struct device *const dev = DEVICE_DT_GET(DT_NODELABEL(syscon));
	uint32_t ctrl;

	syscon_read_reg(dev, CACHE_FUNC_CTRL_REG, &ctrl);

	return (ctrl & CACHE_ENABLE) ? CACHE_LINE_SIZE : 0;
}
#endif /* CONFIG_ICACHE_LINE_SIZE_DETECT */
