/*
 * Copyright (c) 2022, STMicroelectronics - All Rights Reserved
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <assert.h>
#include <errno.h>
#include <stdbool.h>

#include <arch_helpers.h>
#include <drivers/clk.h>
#include <drivers/delay_timer.h>
#include <drivers/st/stm32_rng.h>
#include <drivers/st/stm32mp_reset.h>
#include <lib/mmio.h>
#include <libfdt.h>

#include <platform_def.h>

#if STM32_RNG_VER == 2
#define DT_RNG_COMPAT		"st,stm32-rng"
#endif
#if STM32_RNG_VER == 4
#define DT_RNG_COMPAT		"st,stm32mp13-rng"
#endif
#define RNG_CR			0x00U
#define RNG_SR			0x04U
#define RNG_DR			0x08U

#define RNG_CR_RNGEN		BIT(2)
#define RNG_CR_IE		BIT(3)
#define RNG_CR_CED		BIT(5)
#define RNG_CR_CLKDIV		GENMASK(19, 16)
#define RNG_CR_CLKDIV_SHIFT	16U
#define RNG_CR_CONDRST		BIT(30)

#define RNG_SR_DRDY		BIT(0)
#define RNG_SR_CECS		BIT(1)
#define RNG_SR_SECS		BIT(2)
#define RNG_SR_CEIS		BIT(5)
#define RNG_SR_SEIS		BIT(6)

#define RNG_TIMEOUT_US		100000U
#define RNG_TIMEOUT_STEP_US	10U

#define TIMEOUT_US_1MS		1000U

#define RNG_NIST_CONFIG_A	0x00F40F00U
#define RNG_NIST_CONFIG_B	0x01801000U
#define RNG_NIST_CONFIG_C	0x00F00D00U
#define RNG_NIST_CONFIG_MASK	GENMASK(25, 8)

#define RNG_MAX_NOISE_CLK_FREQ	48000000U

struct stm32_rng_instance {
	uintptr_t base;
	unsigned long clock;
};

static struct stm32_rng_instance stm32_rng;

static void seed_error_recovery(void)
{
	uint8_t i __maybe_unused;

	/* Recommended by the SoC reference manual */
	mmio_clrbits_32(stm32_rng.base + RNG_SR, RNG_SR_SEIS);
	dmbsy();

#if STM32_RNG_VER == 2
	/* No Auto-reset on version 2, need to clean FIFO */
	for (i = 12U; i != 0U; i--) {
		(void)mmio_read_32(stm32_rng.base + RNG_DR);
	}

	dmbsy();
#endif

	if ((mmio_read_32(stm32_rng.base + RNG_SR) & RNG_SR_SEIS) != 0U) {
		ERROR("RNG noise\n");
		panic();
	}
}

static uint32_t stm32_rng_clock_freq_restrain(void)
{
	unsigned long clock_rate;
	uint32_t clock_div = 0U;

	clock_rate = clk_get_rate(stm32_rng.clock);

	/*
	 * Get the exponent to apply on the CLKDIV field in RNG_CR register
	 * No need to handle the case when clock-div > 0xF as it is physically
	 * impossible
	 */
	while ((clock_rate >> clock_div) > RNG_MAX_NOISE_CLK_FREQ) {
		clock_div++;
	}

	VERBOSE("RNG clk rate : %lu\n", clk_get_rate(stm32_rng.clock) >> clock_div);

	return clock_div;
}

static int stm32_rng_enable(void)
{
	uint32_t sr;
	uint64_t timeout;
	uint32_t clock_div __maybe_unused;

#if STM32_RNG_VER == 2
	mmio_write_32(stm32_rng.base + RNG_CR, RNG_CR_RNGEN | RNG_CR_CED);
#endif
#if STM32_RNG_VER == 4
	/* Reset internal block and disable CED bit */
	clock_div = stm32_rng_clock_freq_restrain();

	/* Update configuration fields */
	mmio_clrsetbits_32(stm32_rng.base + RNG_CR, RNG_NIST_CONFIG_MASK,
			   RNG_NIST_CONFIG_A | RNG_CR_CONDRST | RNG_CR_CED);

	mmio_clrsetbits_32(stm32_rng.base + RNG_CR, RNG_CR_CLKDIV,
			   (clock_div << RNG_CR_CLKDIV_SHIFT));

	mmio_clrsetbits_32(stm32_rng.base + RNG_CR, RNG_CR_CONDRST, RNG_CR_RNGEN);
#endif
	timeout = timeout_init_us(RNG_TIMEOUT_US);
	sr = mmio_read_32(stm32_rng.base + RNG_SR);
	while ((sr & RNG_SR_DRDY) == 0U) {
		if (timeout_elapsed(timeout)) {
			WARN("Timeout waiting\n");
			return -ETIMEDOUT;
		}

		if ((sr & (RNG_SR_SECS | RNG_SR_SEIS)) != 0U) {
			seed_error_recovery();
			timeout = timeout_init_us(RNG_TIMEOUT_US);
		}

		udelay(RNG_TIMEOUT_STEP_US);
		sr = mmio_read_32(stm32_rng.base + RNG_SR);
	}

	VERBOSE("Init RNG done\n");

	return 0;
}

/*
 * stm32_rng_read - Read a number of random bytes from RNG
 * out: pointer to the output buffer
 * size: number of bytes to be read
 * Return 0 on success, non-0 on failure
 */
int stm32_rng_read(uint8_t *out, uint32_t size)
{
	uint8_t *buf = out;
	size_t len = size;
	int nb_tries;
	uint32_t data32;
	int rc = 0;
	unsigned int count;

	if (stm32_rng.base == 0U) {
		return -EPERM;
	}

	while (len != 0U) {
		nb_tries = RNG_TIMEOUT_US / RNG_TIMEOUT_STEP_US;
		do {
			uint32_t status = mmio_read_32(stm32_rng.base + RNG_SR);

			if ((status & (RNG_SR_SECS | RNG_SR_SEIS)) != 0U) {
				seed_error_recovery();
			}

			udelay(RNG_TIMEOUT_STEP_US);
			nb_tries--;
			if (nb_tries == 0) {
				rc = -ETIMEDOUT;
				goto bail;
			}
		} while ((mmio_read_32(stm32_rng.base + RNG_SR) &
			  RNG_SR_DRDY) == 0U);

		count = 4U;
		while (len != 0U) {
			if ((mmio_read_32(stm32_rng.base + RNG_SR) & RNG_SR_DRDY) == 0U) {
				break;
			}

			data32 = mmio_read_32(stm32_rng.base + RNG_DR);
			count--;

			memcpy(buf, &data32, MIN(len, sizeof(uint32_t)));
			buf += MIN(len, sizeof(uint32_t));
			len -= MIN(len, sizeof(uint32_t));

			if (count == 0U) {
				break;
			}
		}
	}

bail:
	if (rc != 0) {
		memset(out, 0, buf - out);
	}

	return rc;
}

/*
 * stm32_rng_init: Initialize rng from DT
 * return 0 on success, negative value on failure
 */
int stm32_rng_init(void)
{
	void *fdt;
	struct dt_node_info dt_rng;
	int node;

	if (stm32_rng.base != 0U) {
		/* Driver is already initialized */
		return 0;
	}

	if (fdt_get_address(&fdt) == 0) {
		panic();
	}

	node = dt_get_node(&dt_rng, -1, DT_RNG_COMPAT);
	if (node < 0) {
		return 0;
	}

	if (dt_rng.status == DT_DISABLED) {
		return 0;
	}

	assert(dt_rng.base != 0U);

	stm32_rng.base = dt_rng.base;

	if (dt_rng.clock < 0) {
		panic();
	}

	stm32_rng.clock = (unsigned long)dt_rng.clock;
	clk_enable(stm32_rng.clock);

	if (dt_rng.reset >= 0) {
		int ret;

		ret = stm32mp_reset_assert((unsigned long)dt_rng.reset,
					   TIMEOUT_US_1MS);
		if (ret != 0) {
			panic();
		}

		udelay(20);

		ret = stm32mp_reset_deassert((unsigned long)dt_rng.reset,
					     TIMEOUT_US_1MS);
		if (ret != 0) {
			panic();
		}
	}

	return stm32_rng_enable();
}
