/* Copyright 2016 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/* Clocks and power management settings */

#include "chipset.h"
#include "clock.h"
#include "common.h"
#include "console.h"
#include "cpu.h"
#include "hooks.h"
#include "registers.h"
#include "util.h"

/* High-speed oscillator is 16 MHz */
#define STM32_HSI_CLOCK 16000000
/* Multi-speed oscillator is 4 MHz by default */
#define STM32_MSI_CLOCK 4000000

enum clock_osc {
	OSC_INIT = 0,	/* Uninitialized */
	OSC_HSI,	/* High-speed internal oscillator */
	OSC_MSI,	/* Multi-speed internal oscillator */
#ifdef STM32_HSE_CLOCK	/* Allows us to catch absence of HSE at comiple time */
	OSC_HSE,	/* High-speed external oscillator */
#endif
	OSC_PLL,	/* PLL */
};

static int freq = STM32_MSI_CLOCK;
static int current_osc;

int clock_get_freq(void)
{
	return freq;
}

void clock_wait_bus_cycles(enum bus_type bus, uint32_t cycles)
{
	volatile uint32_t dummy __attribute__((unused));

	if (bus == BUS_AHB) {
		while (cycles--)
			dummy = STM32_DMA1_REGS->isr;
	} else { /* APB */
		while (cycles--)
			dummy = STM32_USART_BRR(STM32_USART1_BASE);
	}
}

static void clock_enable_osc(enum clock_osc osc)
{
	uint32_t ready;
	uint32_t on;

	switch (osc) {
	case OSC_HSI:
		ready = STM32_RCC_CR_HSIRDY;
		on = STM32_RCC_CR_HSION;
		break;
	case OSC_MSI:
		ready = STM32_RCC_CR_MSIRDY;
		on = STM32_RCC_CR_MSION;
		break;
#ifdef STM32_HSE_CLOCK
	case OSC_HSE:
		ready = STM32_RCC_CR_HSERDY;
		on = STM32_RCC_CR_HSEON;
		break;
#endif
	case OSC_PLL:
		ready = STM32_RCC_CR_PLLRDY;
		on = STM32_RCC_CR_PLLON;
		break;
	default:
		return;
	}

	if (!(STM32_RCC_CR & ready)) {
		/* Enable HSI */
		STM32_RCC_CR |= on;
		/* Wait for HSI to be ready */
		while (!(STM32_RCC_CR & ready))
			;
	}
}

/* Switch system clock oscillator */
static void clock_switch_osc(enum clock_osc osc)
{
	uint32_t sw;
	uint32_t sws;

	switch (osc) {
	case OSC_HSI:
		sw = STM32_RCC_CFGR_SW_HSI;
		sws = STM32_RCC_CFGR_SWS_HSI;
		break;
	case OSC_MSI:
		sw = STM32_RCC_CFGR_SW_MSI;
		sws = STM32_RCC_CFGR_SWS_MSI;
		break;
#ifdef STM32_HSE_CLOCK
	case OSC_HSE:
		sw = STM32_RCC_CFGR_SW_HSE;
		sws = STM32_RCC_CFGR_SWS_HSE;
		break;
#endif
	case OSC_PLL:
		sw = STM32_RCC_CFGR_SW_PLL;
		sws = STM32_RCC_CFGR_SWS_PLL;
		break;
	default:
		return;
	}

	STM32_RCC_CFGR = sw;
	while ((STM32_RCC_CFGR & STM32_RCC_CFGR_SWS_MASK) != sws)
		;
}

/*
 * Configure PLL for HSE
 *
 * 1. Disable the PLL by setting PLLON to 0 in RCC_CR.
 * 2. Wait until PLLRDY is cleared. The PLL is now fully stopped.
 * 3. Change the desired parameter.
 * 4. Enable the PLL again by setting PLLON to 1.
 * 5. Enable the desired PLL outputs by configuring PLLPEN, PLLQEN, PLLREN
 *    in RCC_PLLCFGR.
 */
static int stm32_configure_pll(enum clock_osc osc,
			       uint8_t m, uint8_t n, uint8_t r)
{
	uint32_t val;
	int f;

	/* 1 */
	STM32_RCC_CR &= ~STM32_RCC_CR_PLLON;

	/* 2 */
	while (STM32_RCC_CR & STM32_RCC_CR_PLLRDY)
		;

	/* 3 */
	val = STM32_RCC_PLLCFGR;

	val &= ~STM32_RCC_PLLCFGR_PLLSRC_MASK;
	switch (osc) {
	case OSC_HSI:
		val |= STM32_RCC_PLLCFGR_PLLSRC_HSI;
		f = STM32_HSI_CLOCK;
		break;
	case OSC_MSI:
		val |= STM32_RCC_PLLCFGR_PLLSRC_MSI;
		f = STM32_MSI_CLOCK;
		break;
#ifdef STM32_HSE_CLOCK
	case OSC_HSE:
		val |= STM32_RCC_PLLCFGR_PLLSRC_HSE;
		f = STM32_HSE_CLOCK;
		break;
#endif
	default:
		return -1;
	}

	ASSERT(m > 0 && m < 9);
	val &= ~STM32_RCC_PLLCFGR_PLLM_MASK;
	val |= (m  - 1) << STM32_RCC_PLLCFGR_PLLM_SHIFT;

	/* Max and min values are from TRM */
	ASSERT(n > 7 && n < 87);
	val &= ~STM32_RCC_PLLCFGR_PLLN_MASK;
	val |= n << STM32_RCC_PLLCFGR_PLLN_SHIFT;

	val &= ~STM32_RCC_PLLCFGR_PLLR_MASK;
	switch (r) {
	case 2:
		val |= 0 << STM32_RCC_PLLCFGR_PLLR_SHIFT;
		break;
	case 4:
		val |= 1 << STM32_RCC_PLLCFGR_PLLR_SHIFT;
		break;
	case 6:
		val |= 2 << STM32_RCC_PLLCFGR_PLLR_SHIFT;
		break;
	case 8:
		val |= 3 << STM32_RCC_PLLCFGR_PLLR_SHIFT;
		break;
	default:
		return -1;
	}

	STM32_RCC_PLLCFGR = val;

	/* 4 */
	clock_enable_osc(OSC_PLL);

	/* 5 */
	val = STM32_RCC_PLLCFGR;
	val |= 1 << STM32_RCC_PLLCFGR_PLLREN_SHIFT;
	STM32_RCC_PLLCFGR = val;

	/* (f * n) shouldn't overflow based on their max values */
	return (f * n / m / r);
}

/**
 * Set system clock oscillator
 *
 * @param osc		Oscillator to use
 * @param pll_osc	Source oscillator for PLL. Ignored if osc is not PLL.
 */
static void clock_set_osc(enum clock_osc osc, enum clock_osc pll_osc)
{
	uint32_t val;

	if (osc == current_osc)
		return;

	if (current_osc != OSC_INIT)
		hook_notify(HOOK_PRE_FREQ_CHANGE);

	switch (osc) {
	case OSC_HSI:
		/* Ensure that HSI is ON */
		clock_enable_osc(osc);

		/* Disable LPSDSR */
		STM32_PWR_CR &= ~STM32_PWR_CR_LPSDSR;

		/* Switch to HSI */
		clock_switch_osc(osc);

		/* Disable MSI */
		STM32_RCC_CR &= ~STM32_RCC_CR_MSION;

		freq = STM32_HSI_CLOCK;
		break;

	case OSC_MSI:
		/* Switch to MSI @ 1MHz */
		STM32_RCC_ICSCR =
			(STM32_RCC_ICSCR & ~STM32_RCC_ICSCR_MSIRANGE_MASK) |
			STM32_RCC_ICSCR_MSIRANGE_1MHZ;
		/* Ensure that MSI is ON */
		clock_enable_osc(osc);

		/* Switch to MSI */
		clock_switch_osc(osc);

		/* Disable HSI */
		STM32_RCC_CR &= ~STM32_RCC_CR_HSION;

		/* Enable LPSDSR */
		STM32_PWR_CR |= STM32_PWR_CR_LPSDSR;

		freq = STM32_MSI_CLOCK;
		break;

#ifdef STM32_HSE_CLOCK
	case OSC_HSE:
		/* Ensure that HSE is stable */
		clock_enable_osc(osc);

		/* Switch to HSE */
		clock_switch_osc(osc);

		/* Disable other clock sources */
		STM32_RCC_CR &= ~(STM32_RCC_CR_MSION | STM32_RCC_CR_HSION |
				STM32_RCC_CR_PLLON);

		freq = STM32_HSE_CLOCK;

		break;
#endif
	case OSC_PLL:
		/* Ensure that source clock is stable */
		clock_enable_osc(pll_osc);

		/* Configure PLLCFGR */
		freq = stm32_configure_pll(pll_osc, STM32_PLLM,
					   STM32_PLLN, STM32_PLLR);
		ASSERT(freq > 0);

		/* Adjust flash latency as instructed in TRM */
		val = STM32_FLASH_ACR;
		val &= ~STM32_FLASH_ACR_LATENCY_MASK;
		/* Flash 4 wait state. TODO: Should depend on freq. */
		val |= 4 << STM32_FLASH_ACR_LATENCY_SHIFT;
		STM32_FLASH_ACR = val;
		while (STM32_FLASH_ACR != val)
			;

		/* Switch to PLL */
		clock_switch_osc(osc);

		/* TODO: Disable other sources */
		break;
	default:
		break;
	}

	/* Notify modules of frequency change unless we're initializing */
	if (current_osc != OSC_INIT) {
		current_osc = osc;
		hook_notify(HOOK_FREQ_CHANGE);
	} else {
		current_osc = osc;
	}
}

void clock_enable_module(enum module_id module, int enable)
{
	static uint32_t clock_mask;
	int new_mask;

	if (enable)
		new_mask = clock_mask | (1 << module);
	else
		new_mask = clock_mask & ~(1 << module);

	/* Only change clock if needed */
	if ((!!new_mask) != (!!clock_mask)) {

		/* Flush UART before switching clock speed */
		cflush();

		clock_set_osc(new_mask ? OSC_HSI : OSC_MSI, OSC_INIT);
	}

	clock_mask = new_mask;
}

void clock_init(void)
{
#ifdef STM32_HSE_CLOCK
	clock_set_osc(OSC_PLL, OSC_HSE);
#else
	clock_set_osc(OSC_HSI, OSC_INIT);
#endif
}

static void clock_chipset_startup(void)
{
	/* Return to full speed */
	clock_enable_module(MODULE_CHIPSET, 1);
}
DECLARE_HOOK(HOOK_CHIPSET_STARTUP, clock_chipset_startup, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_RESUME, clock_chipset_startup, HOOK_PRIO_DEFAULT);

static void clock_chipset_shutdown(void)
{
	/* Drop to lower clock speed if no other module requires full speed */
	clock_enable_module(MODULE_CHIPSET, 0);
}
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, clock_chipset_shutdown, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, clock_chipset_shutdown, HOOK_PRIO_DEFAULT);

static int command_clock(int argc, char **argv)
{
	if (argc >= 2) {
		if (!strcasecmp(argv[1], "hsi"))
			clock_set_osc(OSC_HSI, OSC_INIT);
		else if (!strcasecmp(argv[1], "msi"))
			clock_set_osc(OSC_MSI, OSC_INIT);
#ifdef STM32_HSE_CLOCK
		else if (!strcasecmp(argv[1], "hse"))
			clock_set_osc(OSC_HSE, OSC_INIT);
		else if (!strcasecmp(argv[1], "pll"))
			clock_set_osc(OSC_PLL, OSC_HSE);
#endif
		else
			return EC_ERROR_PARAM1;
	}

	ccprintf("Clock frequency is now %d Hz\n", freq);
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(clock, command_clock,
			"hsi | msi"
#ifdef STM32_HSE_CLOCK
			" | hse | pll"
#endif
			,
			"Set clock frequency");
