/*
 * Copyright (c) 2023 Antmicro <www.antmicro.com>
 *
 * Based on:
 *  sam0_rtc_timer.c Copyright (c) 2018 omSquare s.r.o.
 *  intel_adsp_timer.c Copyright (c) 2020 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT silabs_gecko_burtc

/**
 * @file
 * @brief SiLabs Gecko BURTC-based sys_clock driver
 *
 */

#include <zephyr/init.h>
#include <soc.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/timer/system_timer.h>
#include <zephyr/sys_clock.h>
#include <zephyr/irq.h>
#include <zephyr/spinlock.h>
#include <zephyr/logging/log.h>

#include "em_device.h"
#include "em_cmu.h"
#include "em_burtc.h"


LOG_MODULE_REGISTER(gecko_burtc_timer);


/* Maximum time interval between timer interrupts (in hw_cycles) */
#define MAX_TIMEOUT_CYC (UINT32_MAX >> 1)

/*
 * Mininum time interval between now and IRQ firing that can be scheduled.
 * The main cause for this is LFSYNC register update, which requires several
 * LF clk cycles for synchronization.
 * Seee e.g. "4.2.4.4.4 LFSYNC Registers" in "EFR32xG22 Reference Manual"
 */
#define MIN_DELAY_CYC (6u)

#define TIMER_IRQ (DT_INST_IRQN(0))

#if defined(CONFIG_TEST)
/* See tests/kernel/context */
const int32_t z_sys_timer_irq_for_test = TIMER_IRQ;
#endif

/* With CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME, that's where we
 * should write hw_cycles timer clock frequency upon init
 */
extern int z_clock_hw_cycles_per_sec;

/* Number of hw_cycles clocks per 1 kernel tick */
static uint32_t g_cyc_per_tick;

/* MAX_TIMEOUT_CYC expressed as ticks */
static uint32_t g_max_timeout_ticks;

/* Value of BURTC counter when the previous kernel tick was announced */
static atomic_t g_last_count;

/* Spinlock to sync between Compare ISR and update of Compare register */
static struct k_spinlock g_lock;

/* Set to true when timer is initialized */
static bool g_init;

static void burtc_isr(const void *arg)
{
	ARG_UNUSED(arg);

	/* Clear the interrupt */
	BURTC_IntClear(BURTC_IF_COMP);

	uint32_t curr = BURTC_CounterGet();

	/* NOTE: this is the only place where g_last_count is modified,
	 * so we don't need to do make the whole read-and-modify atomic, just
	 * writing it behind the memory barrier is enough
	 */
	uint32_t prev = atomic_get(&g_last_count);

	/* How many ticks have we not announced since the last announcement */
	uint32_t unannounced = (curr - prev) / g_cyc_per_tick;

	atomic_set(&g_last_count, prev + unannounced * g_cyc_per_tick);

	if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
		/* Counter value on which announcement should be made */
		uint32_t next = prev + g_cyc_per_tick;

		/* `next` can be too close in the future since we're trying to
		 * announce the very next tick - in that case we skip one and
		 * announce the one after it instead
		 */
		if ((next - curr) < MIN_DELAY_CYC) {
			next += g_cyc_per_tick;
		}

		BURTC_CompareSet(0, next);
	}

	sys_clock_announce(unannounced);
}

void sys_clock_set_timeout(int32_t ticks, bool idle)
{
	ARG_UNUSED(idle);

	if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
		return;
	}

	/*
	 * calculate 'ticks' value that specifies which tick to announce,
	 * beginning from the closest upcoming one:
	 * 0 - announce upcoming tick itself
	 * 1 - skip upcoming one, but announce the one after it, etc.
	 */
	ticks = (ticks == K_TICKS_FOREVER) ? g_max_timeout_ticks : ticks;
	ticks = CLAMP(ticks - 1, 0, g_max_timeout_ticks);

	k_spinlock_key_t key = k_spin_lock(&g_lock);

	uint32_t curr = BURTC_CounterGet();
	uint32_t prev = atomic_get(&g_last_count);

	/* How many ticks have we not announced since the last announcement */
	uint32_t unannounced = (curr - prev) / g_cyc_per_tick;

	/* Which tick to announce (counting from the last announced one) */
	uint32_t to_announce = unannounced + ticks + 1;

	/* Force maximum interval between announcements. If we sit without
	 * announcements for too long, counter will roll over and we'll lose
	 * track of unannounced ticks.
	 */
	to_announce = MIN(to_announce, g_max_timeout_ticks);

	/* Counter value on which announcement should be made */
	uint32_t next = prev + to_announce * g_cyc_per_tick;

	/* `next` can be too close in the future if we're trying to announce
	 * the very next tick - in that case we skip one and announce the one
	 * after it instead
	 */
	if ((next - curr) < MIN_DELAY_CYC) {
		next += g_cyc_per_tick;
	}

	BURTC_CompareSet(0, next);
	k_spin_unlock(&g_lock, key);
}

uint32_t sys_clock_elapsed(void)
{
	if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL) || !g_init) {
		return 0;
	} else {
		return (BURTC_CounterGet() - g_last_count) / g_cyc_per_tick;
	}
}

uint32_t sys_clock_cycle_get_32(void)
{
	/* API note: this function is unrelated to kernel ticks, it returns
	 * a value of some 32-bit hw_cycles counter which counts with
	 * z_clock_hw_cycles_per_sec frequency
	 */
	if (!g_init) {
		return 0;
	} else {
		return BURTC_CounterGet();
	}
}

static int burtc_init(void)
{
	uint32_t hw_clock_freq;
	BURTC_Init_TypeDef init = BURTC_INIT_DEFAULT;

	/* Enable clock for BURTC CSRs on APB */
	CMU_ClockEnable(cmuClock_BURTC, true);

	/* Calculate timing constants and init BURTC */
	hw_clock_freq = CMU_ClockFreqGet(cmuClock_BURTC);
	z_clock_hw_cycles_per_sec = hw_clock_freq;

	BUILD_ASSERT(CONFIG_SYS_CLOCK_TICKS_PER_SEC > 0,
			"Invalid CONFIG_SYS_CLOCK_TICKS_PER_SEC value");
	g_cyc_per_tick = hw_clock_freq / CONFIG_SYS_CLOCK_TICKS_PER_SEC;

	__ASSERT(g_cyc_per_tick >= MIN_DELAY_CYC,
		"%u cycle-long tick is too short to be scheduled "
		"(min is %u). Config: SYS_CLOCK_TICKS_PER_SEC is "
		"%d and timer frequency is %u",
		g_cyc_per_tick, MIN_DELAY_CYC, CONFIG_SYS_CLOCK_TICKS_PER_SEC,
		hw_clock_freq);

	g_max_timeout_ticks = MAX_TIMEOUT_CYC / g_cyc_per_tick;

	init.clkDiv = 1;
	init.start = false;
	BURTC_Init(&init);
	g_init = true;

	/* Enable compare match interrupt */
	BURTC_IntClear(BURTC_IF_COMP);
	BURTC_IntEnable(BURTC_IF_COMP);
	NVIC_ClearPendingIRQ(TIMER_IRQ);
	IRQ_CONNECT(TIMER_IRQ, DT_INST_IRQ(0, priority), burtc_isr, 0, 0);
	irq_enable(TIMER_IRQ);

	/* Start the timer and announce 1 kernel tick */
	atomic_set(&g_last_count, 0);
	BURTC_CompareSet(0, g_cyc_per_tick);

	BURTC_SyncWait();
	BURTC->CNT = 0;
	BURTC_Start();

	return 0;
}

SYS_INIT(burtc_init, PRE_KERNEL_2,
	 CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);
