// SPDX-License-Identifier: BSD-2-Clause
/*
 * Copyright (c) 2015 Atmel Corporation,
 *                    Nicolas Ferre <nicolas.ferre@atmel.com>
 * Copyright (c) 2021, Microchip
 */

#include <drivers/atmel_shdwc.h>
#include <drivers/sam/at91_ddr.h>
#include <drivers/pm/sam/atmel_pm.h>
#include <io.h>
#include <kernel/dt.h>
#include <kernel/dt_driver.h>
#include <kernel/thread.h>
#include <libfdt.h>
#include <matrix.h>
#include <platform_config.h>
#include <stdbool.h>
#include <tee_api_defines.h>
#include <tee_api_types.h>
#include <trace.h>
#include <types_ext.h>
#include <util.h>

#include "at91_clk.h"

#define SHDW_WK_PIN(reg, cfg)	((reg) & \
					AT91_SHDW_WKUPIS((cfg)->wkup_pin_input))
#define SHDW_RTCWK(reg, cfg)	(((reg) >> ((cfg)->sr_rtcwk_shift)) & 0x1)
#define SHDW_RTTWK(reg, cfg)	(((reg) >> ((cfg)->sr_rttwk_shift)) & 0x1)
#define SHDW_RTCWKEN(cfg)	BIT((cfg)->mr_rtcwk_shift)
#define SHDW_RTTWKEN(cfg)	BIT((cfg)->mr_rttwk_shift)

#define SLOW_CLK_FREQ		32768ULL
#define DBC_PERIOD_US(x)	DIV_ROUND_UP((1000000ULL * (x)), SLOW_CLK_FREQ)

/*
 * @type_offset	offset of Memory Device Register
 * @type_mask	mask of Memory Device Type in Memory Device Register
 * @compatible	the compatible string in the device tree
 */
struct ddrc_reg_config {
	uint32_t type_offset;
	uint32_t type_mask;
	const char *compatible;
};

/*
 * @shdwc_always_secure	Is peripheral SHDWC always secured?
 * @ddrc		DDR controller configurations
 */
struct shdwc_compat {
	bool shdwc_always_secure;
	struct ddrc_reg_config ddrc;
};

static vaddr_t shdwc_base;
static vaddr_t mpddrc_base;

bool atmel_shdwc_available(void)
{
	return shdwc_base != 0;
}

void __noreturn atmel_shdwc_shutdown(void)
{
	vaddr_t pmc_base = at91_pmc_get_base();

	/*
	 * Mask exception before entering assembly which does not expect to be
	 * interrupted.
	 */
	thread_mask_exceptions(THREAD_EXCP_ALL);

	__atmel_shdwc_shutdown(mpddrc_base, shdwc_base, pmc_base);

	/* We are going to shutdown the CPU so we will never hit this loop */
	while (true)
		;
}

static const unsigned long long sdwc_dbc_period[] = {
	0, 3, 32, 512, 4096, 32768,
};

static uint32_t at91_shdwc_debouncer_value(uint32_t in_period_us)
{
	int i = 0;
	int max_idx = ARRAY_SIZE(sdwc_dbc_period) - 1;
	uint64_t period_us = 0;
	uint64_t max_period_us = DBC_PERIOD_US(sdwc_dbc_period[max_idx]);

	if (in_period_us > max_period_us) {
		DMSG("debouncer period %"PRIu32" too big, using %"PRIu64" us",
		     in_period_us, max_period_us);
		return max_idx;
	}

	for (i = max_idx - 1; i > 0; i--) {
		period_us = DBC_PERIOD_US(sdwc_dbc_period[i]);
		if (in_period_us > period_us)
			break;
	}

	return i + 1;
}

static uint32_t at91_shdwc_get_wakeup_input(const void *fdt, int np)
{
	const uint32_t *prop = NULL;
	uint32_t wk_input_mask = 0;
	uint32_t wuir = 0;
	uint32_t wk_input = 0;
	int child = 0;
	int len = 0;

	fdt_for_each_subnode(child, fdt, np) {
		prop = fdt_getprop(fdt, child, "reg", &len);
		if (!prop || len != sizeof(uint32_t)) {
			DMSG("reg property is missing for node %s",
			     fdt_get_name(fdt, child, NULL));
			continue;
		}
		wk_input = fdt32_to_cpu(*prop);
		wk_input_mask = BIT32(wk_input);
		if (!(wk_input_mask & AT91_SHDW_WKUPEN_MASK)) {
			DMSG("wake-up input %"PRId32" out of bounds ignore",
			     wk_input);
			continue;
		}
		wuir |= wk_input_mask;

		if (fdt_getprop(fdt, child, "atmel,wakeup-active-high", NULL))
			wuir |= AT91_SHDW_WKUPT(wk_input);
	}

	return wuir;
}

static void at91_shdwc_dt_configure(const void *fdt, int np)
{
	const uint32_t *prop = NULL;
	uint32_t mode = 0;
	uint32_t tmp = 0;
	uint32_t input = 0;
	int len = 0;

	prop = fdt_getprop(fdt, np, "debounce-delay-us", &len);
	if (prop && len == sizeof(uint32_t)) {
		tmp = fdt32_to_cpu(*prop);
		mode |= AT91_SHDW_WKUPDBC(at91_shdwc_debouncer_value(tmp));
	}

	if (fdt_getprop(fdt, np, "atmel,wakeup-rtc-timer", &len))
		mode |= AT91_SHDW_RTCWKEN;

	if (fdt_getprop(fdt, np, "atmel,wakeup-rtt-timer", &len))
		mode |= AT91_SHDW_RTTWKEN;

	io_write32(shdwc_base + AT91_SHDW_MR, mode);

	input = at91_shdwc_get_wakeup_input(fdt, np);
	io_write32(shdwc_base + AT91_SHDW_WUIR, input);
}

static TEE_Result atmel_shdwc_probe(const void *fdt, int node,
				    const void *compat_data)
{
	int ddr_node = 0;
	size_t size = 0;
	uint32_t ddr = AT91_DDRSDRC_MD_LPDDR2;
	struct shdwc_compat *compat = (struct shdwc_compat *)compat_data;

	/*
	 * Assembly code relies on the fact that there is only one CPU to avoid
	 * any other one to invalidate TLB/I-Cache.
	 */
	COMPILE_TIME_ASSERT(CFG_TEE_CORE_NB_CORE == 1);

	if (fdt_get_status(fdt, node) != DT_STATUS_OK_SEC)
		return TEE_ERROR_BAD_PARAMETERS;

	if (!compat->shdwc_always_secure)
		matrix_configure_periph_secure(AT91C_ID_SYS);

	if (dt_map_dev(fdt, node, &shdwc_base, &size, DT_MAP_AUTO) < 0)
		return TEE_ERROR_GENERIC;

	ddr_node = fdt_node_offset_by_compatible(fdt, -1,
						 compat->ddrc.compatible);
	if (ddr_node < 0)
		return TEE_ERROR_GENERIC;

	if (dt_map_dev(fdt, ddr_node, &mpddrc_base, &size, DT_MAP_AUTO) < 0)
		return TEE_ERROR_GENERIC;

	if (!compat->ddrc.type_mask) {
		ddr = io_read32(mpddrc_base + compat->ddrc.type_offset);
		ddr &= compat->ddrc.type_mask;
		if (ddr != AT91_DDRSDRC_MD_LPDDR2 &&
		    ddr != AT91_DDRSDRC_MD_LPDDR3)
			mpddrc_base = 0;
	} else {
		/*
		 * Set the base to 0 as the code of DRAM controller for power
		 * down is not implemented yet.
		 */
		mpddrc_base = 0;
	}

	at91_shdwc_dt_configure(fdt, node);

	return sam_pm_init(fdt, shdwc_base);
}

static const struct shdwc_compat sama5d2_compat = {
	.shdwc_always_secure = false,
	.ddrc = {
		.type_offset = AT91_DDRSDRC_MDR,
		.type_mask = AT91_DDRSDRC_MD,
		.compatible = "atmel,sama5d3-ddramc",
	}
};

static const struct shdwc_compat sama7g5_compat = {
	.shdwc_always_secure = true,
	.ddrc = {
		.compatible = "microchip,sama7g5-uddrc",
	}
};

static const struct dt_device_match atmel_shdwc_match_table[] = {
	{
		.compatible = "atmel,sama5d2-shdwc",
		.compat_data = &sama5d2_compat
	},
	{
		.compatible = "microchip,sama7g5-shdwc",
		.compat_data = &sama7g5_compat,
	},
	{ }
};

DEFINE_DT_DRIVER(atmel_shdwc_dt_driver) = {
	.name = "atmel_shdwc",
	.type = DT_DRIVER_NOTYPE,
	.match_table = atmel_shdwc_match_table,
	.probe = atmel_shdwc_probe,
};
