/** @file rcc.c
 * Module handling Reset and Clocks Control (RCC).
 *
 * The module provides functions to configure clocks according to presets as
 * well as to enable/disable/reset peripherals.
 */

//--includes--------------------------------------------------------------------

#include "rcc.h"
#include "rcc_regs.h"
#include "flash.h"


//--local definitions-----------------------------------------------------------

#define AHB_MASK  0x00000557
#define APB1_MASK 0x3afec9ff
#define APB2_MASK 0x0038fffd

static void apply_default_preset(void);
static void apply_speed_preset(void);


//--local variables-------------------------------------------------------------

static volatile struct RCC* regs = (struct RCC*)RCC_BASE_ADDRESS;
static enum RccPreset current_preset = RCC_PRESET_DEFAULT;


//--public functions------------------------------------------------------------

/**
 * Configures the clock sources, PLL, prescalers, etc. All peripherals are
 * disabled prior to any modifications to avoid unwanted side effects.
 */
void rcc_configure(enum RccPreset preset)
{
	//disable all peripherals during clock configuration
	union RCC_APB1ENR apb1_enr = regs->APB1ENR;
	regs->APB1ENR.word = 0x0;
	union RCC_APB2ENR apb2_enr = regs->APB2ENR;
	regs->APB2ENR.word = 0x0;

	switch (preset) {
		case RCC_PRESET_DEFAULT:
			apply_default_preset();
			current_preset = RCC_PRESET_DEFAULT;
			break;
		case RCC_PRESET_SPEED:
			apply_speed_preset();
			current_preset = RCC_PRESET_SPEED;
			break;
		default:
			break;
	}

	//re-enable peripherals
	regs->APB1ENR = apb1_enr;
	regs->APB2ENR = apb2_enr;
}

void rcc_configure_lsi(bool enable)
{
	regs->CSR.LSION = enable;

	//ensure LSI is enabled
	if (enable) {
		while (regs->CSR.LSIRDY != 0x1) {}
	}
}

void rcc_enable(enum RccAhb ahb_mask, enum RccApb1 apb1_mask,
		enum RccApb2 apb2_mask)
{
	regs->AHBENR.word |= ahb_mask & AHB_MASK;
	regs->APB1ENR.word |= apb1_mask & APB1_MASK;
	regs->APB2ENR.word |= apb2_mask & APB2_MASK;
}

void rcc_disable(enum RccAhb ahb_mask, enum RccApb1 apb1_mask,
		enum RccApb2 apb2_mask)
{
	regs->AHBENR.word &= !(ahb_mask & AHB_MASK);
	regs->APB1ENR.word &= !(apb1_mask & APB1_MASK);
	regs->APB2ENR.word &= !(apb2_mask & APB2_MASK);
}

void rcc_reset(enum RccApb1 apb1_mask, enum RccApb2 apb2_mask)
{
	regs->APB1RSTR.word &= !(apb1_mask & APB1_MASK);
	regs->APB2RSTR.word &= !(apb2_mask & APB2_MASK);
}

void rcc_get_clocks(struct RccClocks* clocks)
{
	clocks->ahb_freq = 0;
	switch (current_preset)
	{
		case RCC_PRESET_DEFAULT:
			clocks->ahb_freq = 8000000;
			clocks->apb1_freq = 8000000;
			clocks->apb2_freq = 8000000;
			clocks->tim_freq = 8000000;
			break;
		case RCC_PRESET_SPEED:
			clocks->ahb_freq = 72000000;
			clocks->apb1_freq = 36000000;
			clocks->apb2_freq = 72000000;
			clocks->tim_freq = 72000000;
			break;
		default:
			//TODO hardfault
			break;
	}
}


//--local functions-------------------------------------------------------------

/**
 * Apply the default clock preset, using HSI whithout PLL or prescalers. This is
 * similar to the reset configuration. Most buses run at 8Mhz, except ADCs wich
 * run at 4Mhz, and Systick wich only reaches 1Mhz.
 */
static void apply_default_preset(void)
{
	//ensures HSI is enabled
	regs->CR.HSION = 1;
	while (regs->CR.HSIRDY != 0x1);

	//set HSI as main clock source and disable prescalers
	regs->CFGR.word &= ~0x077fff3;

	//disable all options
	regs->CR.HSITRIM = 0;
	regs->CR.HSITRIM = 0x10;
	regs->CR.HSEON = 0;
	regs->CR.HSEBYP = 0;
	regs->CR.CCSON = 0;
	regs->CR.PLLON = 0;

	//disable all interrupts
	regs->CIR.LSIRDYIE = 0;
	regs->CIR.LSERDYIE = 0;
	regs->CIR.HSIRDYIE = 0;
	regs->CIR.HSERDYIE = 0;
	regs->CIR.PLLRDYIE = 0;

	//reconfigure flash
	flash_configure(FLASH_PRESET_LOW_CLOCK_SPEED);
}

/**
 * Apply the speed preset, using HSE with PLL to achieve 72MHz on AHB, APB2 and
 * timer buses. APB1 is limited to its maximum clock of 36Mhz and the ADCs run
 * at 12MHz. Systick runs at 9Mhz.
 */
static void apply_speed_preset(void)
{
	//restore sane values
	apply_default_preset();

	//enable HSE
	regs->CR.HSEON = 1;
	for (uint32_t i=0; i<10000; ++i) {
		__asm("nop");
	}
	if (regs->CR.HSERDY == 0x1) {
		regs->CFGR.PLLSCR = 1;
	} else {
		apply_default_preset();
		while (true);
	}

	//configure PLL
	regs->CFGR.PLLMUL = 0x7; //PLL x9
	regs->CR.PLLON = 1;
	for (uint32_t i=0; i<10000; ++i) {
		__asm("nop");
	}
	if (regs->CR.PLLRDY != 0x1) {
		apply_default_preset();
		while (true);
	}

	//configure prescalers
	regs->CFGR.PPRE1 = 0x4;  // /2
	regs->CFGR.ADCPRE = 0x2; // /6

	//reconfigure flash
	flash_configure(FLASH_PRESET_HIGH_CLOCK_SPEED);

	//switch to PLL output
	regs->CFGR.SW = 0x2;
}

