/**
 * \file
 *
 * \brief Chip-specific system clock management functions
 *
 * Copyright (c) 2012-2013 Atmel Corporation. All rights reserved.
 *
 * \asf_license_start
 *
 * \page License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. The name of Atmel may not be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * 4. This software may only be redistributed and used in connection with an
 *    Atmel microcontroller product.
 *
 * THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * EXPRESSLY AND SPECIFICALLY DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * \asf_license_stop
 *
 */

#include <compiler.h>
#include <stdbool.h>
#include <sysclk.h>
#include <flashcalw.h>
#include <bpm.h>
#include <osc.h>

/**
 * \weakgroup sysclk_group
 * @{
 */

#if ((CONFIG_SYSCLK_CPU_DIV > CONFIG_SYSCLK_PBA_DIV) || \
     (CONFIG_SYSCLK_CPU_DIV > CONFIG_SYSCLK_PBB_DIV) || \
     (CONFIG_SYSCLK_CPU_DIV > CONFIG_SYSCLK_PBC_DIV) || \
     (CONFIG_SYSCLK_CPU_DIV > CONFIG_SYSCLK_PBD_DIV))
# error CONFIG_SYSCLK_PBx_DIV must be equal to or more than CONFIG_SYSCLK_CPU_DIV.
#endif

/**
 * \internal
 * \defgroup sysclk_internals_group System Clock internals
 *
 * System clock management is fairly straightforward apart from one
 * thing: Enabling and disabling bus bridges. When all peripherals on a
 * given bus are disabled, the bridge to the bus may be disabled. Only
 * the PBA and PBB busses support this, and it is not practical to
 * disable the PBA bridge as it includes the PM and SCIF modules, so turning
 * it off would make it impossible to turn anything back on again.
 *
 * The system clock implementation keeps track of a reference count for
 * PBB. When the reference count is zero, the bus bridge is disabled, otherwise
 * it is enabled.
 *
 * @{
 */

/**
 * \internal
 * \name Initial module clock masks
 *
 * These are the mask values written to the xxxMASK registers during
 * initialization if the user has overridden the default behavior of all clocks
 * left enabled. These values assume that:
 *   - Debugging should be possible
 *   - The program may be running from flash
 *   - The PM should be available to unmask other clocks
 *   - All on-chip RAM should be available
 *   - SCIF, BPM, BSCIF and GPIO are made permanently available for now; this
 *     may change in the future.
 */
//@{
//! \internal
//! \brief Initial value of CPUMASK
#define SYSCLK_INIT_MINIMAL_CPUMASK       0

//! \internal
//! \brief Initial value of HSBMASK
#define SYSCLK_INIT_MINIMAL_HSBMASK                                    \
	((1 << SYSCLK_HFLASHC_DATA)                                        \
		| (1 << SYSCLK_PBB_BRIDGE)                                     \
		| (1 << SYSCLK_PBC_BRIDGE)                                     \
		| (1 << SYSCLK_PBD_BRIDGE))

//! \internal
//! \brief Initial value of PBAMASK
#define SYSCLK_INIT_MINIMAL_PBAMASK       0

//! \internal
//! \brief Initial value of PBBMASK
#define SYSCLK_INIT_MINIMAL_PBBMASK       (1 << SYSCLK_HFLASHC_REGS)

//! \internal
//! \brief Initial value of PBCMASK
#define SYSCLK_INIT_MINIMAL_PBCMASK                                    \
	((1 << SYSCLK_PM)                                                  \
		| (1 << SYSCLK_GPIO)                                           \
		| (1 << SYSCLK_SCIF))

//! \internal
//! \brief Initial value of PBDMASK
#define SYSCLK_INIT_MINIMAL_PBDMASK                                    \
	((1 << SYSCLK_BPM)                                                 \
		| (1 << SYSCLK_BSCIF))
//@}

#if defined(CONFIG_SYSCLK_DEFAULT_RETURNS_SLOW_OSC)
/**
 * \brief boolean signalling that the sysclk_init is done.
 */
bool sysclk_initialized = false;
#endif

/**
 * \internal
 * \brief Enable a maskable module clock.
 * \param bus_id Bus index, given by the \c PM_CLK_GRP_xxx definitions.
 * \param module_index Index of the module to be enabled. This is the
 * bit number in the corresponding xxxMASK register.
 */
void sysclk_priv_enable_module(uint32_t bus_id, uint32_t module_index)
{
	irqflags_t flags;
	uint32_t   mask;

	flags = cpu_irq_save();

	/* Enable the clock */
	mask = *(&PM->PM_CPUMASK + bus_id);
	mask |= 1U << module_index;
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu) |
		BPM_UNLOCK_ADDR(((uint32_t)&PM->PM_CPUMASK - (uint32_t)PM) + (4 * bus_id));
	*(&PM->PM_CPUMASK + bus_id) = mask;

	cpu_irq_restore(flags);
}

/**
 * \internal
 * \brief Disable a maskable module clock.
 * \param bus_id Bus index, given by the \c PM_CLK_GRP_xxx definitions.
 * \param module_index Index of the module to be disabled. This is the
 * bit number in the corresponding xxxMASK register.
 */
void sysclk_priv_disable_module(uint32_t bus_id, uint32_t module_index)
{
	irqflags_t flags;
	uint32_t   mask;

	flags = cpu_irq_save();

	/* Disable the clock */
	mask = *(&PM->PM_CPUMASK + bus_id);
	mask &= ~(1U << module_index);
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu) |
		BPM_UNLOCK_ADDR(((uint32_t)&PM->PM_CPUMASK - (uint32_t)PM) + (4 * bus_id));
	*(&PM->PM_CPUMASK + bus_id) = mask;

	cpu_irq_restore(flags);
}

//! @}

/**
 * \brief Enable a module clock derived from the PBA clock
 * \param index Index of the module clock in the PBAMASK register
 */
void sysclk_enable_pba_module(uint32_t index)
{
	irqflags_t flags;

	/* Enable the bridge if necessary */
	flags = cpu_irq_save();

	if (PM->PM_PBAMASK == 0) {
		sysclk_enable_hsb_module(SYSCLK_PBA_BRIDGE);
	}

	cpu_irq_restore(flags);

	/* Enable the module */
	sysclk_priv_enable_module(PM_CLK_GRP_PBA, index);
}

/**
 * \brief Disable a module clock derived from the PBA clock
 * \param index Index of the module clock in the PBAMASK register
 */
void sysclk_disable_pba_module(uint32_t index)
{
	irqflags_t flags;

	/* Disable the module */
	sysclk_priv_disable_module(PM_CLK_GRP_PBA, index);

	/* Disable the bridge if possible */
	flags = cpu_irq_save();

	if (PM->PM_PBAMASK == 0) {
		sysclk_disable_hsb_module(SYSCLK_PBA_BRIDGE);
	}

	cpu_irq_restore(flags);
}

/**
 * \brief Enable a module clock derived from the PBB clock
 * \param index Index of the module clock in the PBBMASK register
 */
void sysclk_enable_pbb_module(uint32_t index)
{
	irqflags_t flags;

	/* Enable the bridge if necessary */
	flags = cpu_irq_save();

	if (PM->PM_PBBMASK == 0) {
		sysclk_enable_hsb_module(SYSCLK_PBB_BRIDGE);
	}

	cpu_irq_restore(flags);

	/* Enable the module */
	sysclk_priv_enable_module(PM_CLK_GRP_PBB, index);
}

/**
 * \brief Disable a module clock derived from the PBB clock
 * \param index Index of the module clock in the PBBMASK register
 */
void sysclk_disable_pbb_module(uint32_t index)
{
	irqflags_t flags;

	/* Disable the module */
	sysclk_priv_disable_module(PM_CLK_GRP_PBB, index);

	/* Disable the bridge if possible */
	flags = cpu_irq_save();

	if (PM->PM_PBBMASK == 0) {
		sysclk_disable_hsb_module(SYSCLK_PBB_BRIDGE);
	}

	cpu_irq_restore(flags);
}

/**
 * \brief Retrieves the current rate in Hz of the Peripheral Bus clock attached
 *        to the specified peripheral.
 *
 * \param module Pointer to the module's base address.
 *
 * \return Frequency of the bus attached to the specified peripheral, in Hz.
 */
uint32_t sysclk_get_peripheral_bus_hz(const volatile void *module)
{
	/* Fallthroughs intended for modules sharing the same peripheral bus. */
	switch ((uintptr_t)module) {
	case IISC_ADDR:
	case SPI_ADDR:
	case TC0_ADDR:
	case TC1_ADDR:
	case TWIM0_ADDR:
	case TWIS0_ADDR:
	case TWIM1_ADDR:
	case TWIS1_ADDR:
	case USART0_ADDR:
	case USART1_ADDR:
	case USART2_ADDR:
	case USART3_ADDR:
	case ADCIFE_ADDR:
	case DACC_ADDR:
	case ACIFC_ADDR:
	case GLOC_ADDR:
	case ABDACB_ADDR:
	case TRNG_ADDR:
	case PARC_ADDR:
	case CATB_ADDR:
	case TWIM2_ADDR:
	case TWIM3_ADDR:
	case LCDCA_ADDR:
		return sysclk_get_pba_hz();

	case HFLASHC_ADDR:
	case HCACHE_ADDR:
	case HMATRIX_ADDR:
	case PDCA_ADDR:
	case CRCCU_ADDR:
	case USBC_ADDR:
	case PEVC_ADDR:
		return sysclk_get_pbb_hz();

	case PM_ADDR:
	case CHIPID_ADDR:
	case SCIF_ADDR:
	case FREQM_ADDR:
	case GPIO_ADDR:
		return sysclk_get_pbc_hz();

	case BPM_ADDR:
	case BSCIF_ADDR:
	case AST_ADDR:
	case WDT_ADDR:
	case EIC_ADDR:
	case PICOUART_ADDR:
		return sysclk_get_pbd_hz();

	default:
		Assert(false);
		return 0;
	}
}

/**
 * \brief Enable a peripheral's clock from its base address.
 *
 *  Enables the clock to a peripheral, given its base address. If the peripheral
 *  has an associated clock on the HSB bus, this will be enabled also.
 *
 * \param module Pointer to the module's base address.
 */
void sysclk_enable_peripheral_clock(const volatile void *module)
{
	switch ((uintptr_t)module) {
	case AESA_ADDR:
		sysclk_enable_hsb_module(SYSCLK_AESA_HSB);
		break;

	case IISC_ADDR:
		sysclk_enable_pba_module(SYSCLK_IISC);
		break;

	case SPI_ADDR:
		sysclk_enable_pba_module(SYSCLK_SPI);
		break;

	case TC0_ADDR:
		sysclk_enable_pba_module(SYSCLK_TC0);
		sysclk_enable_pba_divmask(PBA_DIVMASK_TIMER_CLOCK2
			| PBA_DIVMASK_TIMER_CLOCK3
			| PBA_DIVMASK_TIMER_CLOCK4
			| PBA_DIVMASK_TIMER_CLOCK5
			);
		break;

	case TC1_ADDR:
		sysclk_enable_pba_module(SYSCLK_TC1);
		sysclk_enable_pba_divmask(PBA_DIVMASK_TIMER_CLOCK2
			| PBA_DIVMASK_TIMER_CLOCK3
			| PBA_DIVMASK_TIMER_CLOCK4
			| PBA_DIVMASK_TIMER_CLOCK5
			);
		break;

	case TWIM0_ADDR:
		sysclk_enable_pba_module(SYSCLK_TWIM0);
		break;

	case TWIS0_ADDR:
		sysclk_enable_pba_module(SYSCLK_TWIS0);
		break;

	case TWIM1_ADDR:
		sysclk_enable_pba_module(SYSCLK_TWIM1);
		break;

	case TWIS1_ADDR:
		sysclk_enable_pba_module(SYSCLK_TWIS1);
		break;

	case USART0_ADDR:
		sysclk_enable_pba_module(SYSCLK_USART0);
		sysclk_enable_pba_divmask(PBA_DIVMASK_CLK_USART);
		break;

	case USART1_ADDR:
		sysclk_enable_pba_module(SYSCLK_USART1);
		sysclk_enable_pba_divmask(PBA_DIVMASK_CLK_USART);
		break;

	case USART2_ADDR:
		sysclk_enable_pba_module(SYSCLK_USART2);
		sysclk_enable_pba_divmask(PBA_DIVMASK_CLK_USART);
		break;

	case USART3_ADDR:
		sysclk_enable_pba_module(SYSCLK_USART3);
		sysclk_enable_pba_divmask(PBA_DIVMASK_CLK_USART);
		break;

	case ADCIFE_ADDR:
		sysclk_enable_pba_module(SYSCLK_ADCIFE);
		break;

	case DACC_ADDR:
		sysclk_enable_pba_module(SYSCLK_DACC);
		break;

	case ACIFC_ADDR:
		sysclk_enable_pba_module(SYSCLK_ACIFC);
		break;

	case GLOC_ADDR:
		sysclk_enable_pba_module(SYSCLK_GLOC);
		break;

	case ABDACB_ADDR:
		sysclk_enable_pba_module(SYSCLK_ABDACB);
		break;

	case TRNG_ADDR:
		sysclk_enable_pba_module(SYSCLK_TRNG);
		break;

	case PARC_ADDR:
		sysclk_enable_pba_module(SYSCLK_PARC);
		break;

	case CATB_ADDR:
		sysclk_enable_pba_module(SYSCLK_CATB);
		break;

	case TWIM2_ADDR:
		sysclk_enable_pba_module(SYSCLK_TWIM2);
		break;

	case TWIM3_ADDR:
		sysclk_enable_pba_module(SYSCLK_TWIM3);
		break;

	case LCDCA_ADDR:
		sysclk_enable_pba_module(SYSCLK_LCDCA);
		break;

	case HFLASHC_ADDR:
		sysclk_enable_hsb_module(SYSCLK_HFLASHC_DATA);
		sysclk_enable_pbb_module(SYSCLK_HFLASHC_REGS);
		break;

	case HCACHE_ADDR:
		sysclk_enable_hsb_module(SYSCLK_HRAMC1_DATA);
		sysclk_enable_pbb_module(SYSCLK_HRAMC1_REGS);
		break;

	case HMATRIX_ADDR:
		sysclk_enable_pbb_module(SYSCLK_HMATRIX);
		break;

	case PDCA_ADDR:
		sysclk_enable_hsb_module(SYSCLK_PDCA_HSB);
		sysclk_enable_pbb_module(SYSCLK_PDCA_PB);
		break;

	case CRCCU_ADDR:
		sysclk_enable_hsb_module(SYSCLK_CRCCU_DATA);
		sysclk_enable_pbb_module(SYSCLK_CRCCU_REGS);
		break;

	case USBC_ADDR:
		sysclk_enable_hsb_module(SYSCLK_USBC_DATA);
		sysclk_enable_pbb_module(SYSCLK_USBC_REGS);
		break;

	case PEVC_ADDR:
		sysclk_enable_pbb_module(SYSCLK_PEVC);
		break;

	case PM_ADDR:
		sysclk_enable_pbc_module(SYSCLK_PM);
		break;

	case CHIPID_ADDR:
		sysclk_enable_pbc_module(SYSCLK_CHIPID);
		break;

	case SCIF_ADDR:
		sysclk_enable_pbc_module(SYSCLK_SCIF);
		break;

	case FREQM_ADDR:
		sysclk_enable_pbc_module(SYSCLK_FREQM);
		break;

	case GPIO_ADDR:
		sysclk_enable_pbc_module(SYSCLK_GPIO);
		break;

	case BPM_ADDR:
		sysclk_enable_pbd_module(SYSCLK_BPM);
		break;

	case BSCIF_ADDR:
		sysclk_enable_pbd_module(SYSCLK_BSCIF);
		break;

	case AST_ADDR:
		sysclk_enable_pbd_module(SYSCLK_AST);
		break;

	case WDT_ADDR:
		sysclk_enable_pbd_module(SYSCLK_WDT);
		break;

	case EIC_ADDR:
		sysclk_enable_pbd_module(SYSCLK_EIC);
		break;

	case PICOUART_ADDR:
		sysclk_enable_pbd_module(SYSCLK_PICOUART);
		break;

	default:
		Assert(false);
		return;
	}
}

/**
 * \brief Disable a peripheral's clock from its base address.
 *
 *  Disables the clock to a peripheral, given its base address. If the peripheral
 *  has an associated clock on the HSB bus, this will be disabled also.
 *
 * \param module Pointer to the module's base address.
 */
void sysclk_disable_peripheral_clock(const volatile void *module)
{
	switch ((uintptr_t)module) {
	case AESA_ADDR:
		sysclk_disable_hsb_module(SYSCLK_AESA_HSB);
		break;

	case IISC_ADDR:
		sysclk_disable_pba_module(SYSCLK_IISC);
		break;

	case SPI_ADDR:
		sysclk_disable_pba_module(SYSCLK_SPI);
		break;

	case TC0_ADDR:
		sysclk_disable_pba_module(SYSCLK_TC0);
		break;

	case TC1_ADDR:
		sysclk_disable_pba_module(SYSCLK_TC1);
		break;

	case TWIM0_ADDR:
		sysclk_disable_pba_module(SYSCLK_TWIM0);
		break;

	case TWIS0_ADDR:
		sysclk_disable_pba_module(SYSCLK_TWIS0);
		break;

	case TWIM1_ADDR:
		sysclk_disable_pba_module(SYSCLK_TWIM1);
		break;

	case TWIS1_ADDR:
		sysclk_disable_pba_module(SYSCLK_TWIS1);
		break;

	case USART0_ADDR:
		sysclk_disable_pba_module(SYSCLK_USART0);
		break;

	case USART1_ADDR:
		sysclk_disable_pba_module(SYSCLK_USART1);
		break;

	case USART2_ADDR:
		sysclk_disable_pba_module(SYSCLK_USART2);
		break;

	case USART3_ADDR:
		sysclk_disable_pba_module(SYSCLK_USART3);
		break;

	case ADCIFE_ADDR:
		sysclk_disable_pba_module(SYSCLK_ADCIFE);
		break;

	case DACC_ADDR:
		sysclk_disable_pba_module(SYSCLK_DACC);
		break;

	case ACIFC_ADDR:
		sysclk_disable_pba_module(SYSCLK_ACIFC);
		break;

	case GLOC_ADDR:
		sysclk_disable_pba_module(SYSCLK_GLOC);
		break;

	case ABDACB_ADDR:
		sysclk_disable_pba_module(SYSCLK_ABDACB);
		break;

	case TRNG_ADDR:
		sysclk_disable_pba_module(SYSCLK_TRNG);
		break;

	case PARC_ADDR:
		sysclk_disable_pba_module(SYSCLK_PARC);
		break;

	case CATB_ADDR:
		sysclk_disable_pba_module(SYSCLK_CATB);
		break;

	case TWIM2_ADDR:
		sysclk_disable_pba_module(SYSCLK_TWIM2);
		break;

	case TWIM3_ADDR:
		sysclk_disable_pba_module(SYSCLK_TWIM3);
		break;

	case LCDCA_ADDR:
		sysclk_disable_pba_module(SYSCLK_LCDCA);
		break;

	case HFLASHC_ADDR:
		sysclk_disable_pbb_module(SYSCLK_HFLASHC_REGS);
		break;

	case HCACHE_ADDR:
		sysclk_disable_hsb_module(SYSCLK_HRAMC1_DATA);
		sysclk_disable_pbb_module(SYSCLK_HRAMC1_REGS);
		break;

	case HMATRIX_ADDR:
		sysclk_disable_pbb_module(SYSCLK_HMATRIX);
		break;

	case PDCA_ADDR:
		sysclk_disable_hsb_module(SYSCLK_PDCA_HSB);
		sysclk_disable_pbb_module(SYSCLK_PDCA_PB);
		break;

	case CRCCU_ADDR:
		sysclk_disable_hsb_module(SYSCLK_CRCCU_DATA);
		sysclk_disable_pbb_module(SYSCLK_CRCCU_REGS);
		break;

	case USBC_ADDR:
		sysclk_disable_hsb_module(SYSCLK_USBC_DATA);
		sysclk_disable_pbb_module(SYSCLK_USBC_REGS);
		break;

	case PEVC_ADDR:
		sysclk_disable_pbb_module(SYSCLK_PEVC);
		break;

	case PM_ADDR:
		sysclk_disable_pbc_module(SYSCLK_PM);
		break;

	case CHIPID_ADDR:
		sysclk_disable_pbc_module(SYSCLK_CHIPID);
		break;

	case SCIF_ADDR:
		sysclk_disable_pbc_module(SYSCLK_SCIF);
		break;

	case FREQM_ADDR:
		sysclk_disable_pbc_module(SYSCLK_FREQM);
		break;

	case GPIO_ADDR:
		sysclk_disable_pbc_module(SYSCLK_GPIO);
		break;

	case BPM_ADDR:
		sysclk_disable_pbd_module(SYSCLK_BPM);
		break;

	case BSCIF_ADDR:
		sysclk_disable_pbd_module(SYSCLK_BSCIF);
		break;

	case AST_ADDR:
		sysclk_disable_pbd_module(SYSCLK_AST);
		break;

	case WDT_ADDR:
		sysclk_disable_pbd_module(SYSCLK_WDT);
		break;

	case EIC_ADDR:
		sysclk_disable_pbd_module(SYSCLK_EIC);
		break;

	case PICOUART_ADDR:
		sysclk_disable_pbd_module(SYSCLK_PICOUART);
		break;

	default:
		Assert(false);
		return;
	}

	// Disable PBA divided clock if possible.
#define PBADIV_CLKSRC_MASK (SYSCLK_TC0 | SYSCLK_TC1 \
		| SYSCLK_USART0 | SYSCLK_USART1 | SYSCLK_USART2 | SYSCLK_USART3)
	if ((PM->PM_PBAMASK & PBADIV_CLKSRC_MASK) == 0) {
		sysclk_disable_pba_divmask(PBA_DIVMASK_Msk);
	}
}


/**
 * \brief Set system clock prescaler configuration
 *
 * This function will change the system clock prescaler configuration to
 * match the parameters.
 *
 * \note The parameters to this function are device-specific.
 *
 * \param cpu_shift The CPU clock will be divided by \f$2^{cpu\_shift}\f$
 * \param pba_shift The PBA clock will be divided by \f$2^{pba\_shift}\f$
 * \param pbb_shift The PBB clock will be divided by \f$2^{pbb\_shift}\f$
 * \param pbc_shift The PBC clock will be divided by \f$2^{pbc\_shift}\f$
 * \param pbd_shift The PBD clock will be divided by \f$2^{pbd\_shift}\f$
 */
void sysclk_set_prescalers(uint32_t cpu_shift,
		uint32_t pba_shift, uint32_t pbb_shift,
		uint32_t pbc_shift, uint32_t pbd_shift)
{
	irqflags_t flags;
	uint32_t   cpu_cksel = 0;
	uint32_t   pba_cksel = 0;
	uint32_t   pbb_cksel = 0;
	uint32_t   pbc_cksel = 0;
	uint32_t   pbd_cksel = 0;

	Assert(cpu_shift <= pba_shift);
	Assert(cpu_shift <= pbb_shift);
	Assert(cpu_shift <= pbc_shift);
	Assert(cpu_shift <= pbd_shift);

	if (cpu_shift > 0) {
		cpu_cksel = (PM_CPUSEL_CPUSEL(cpu_shift - 1))
				| PM_CPUSEL_CPUDIV;
	}

	if (pba_shift > 0) {
		pba_cksel = (PM_PBASEL_PBSEL(pba_shift - 1))
				| PM_PBASEL_PBDIV;
	}

	if (pbb_shift > 0) {
		pbb_cksel = (PM_PBBSEL_PBSEL(pbb_shift - 1))
				| PM_PBBSEL_PBDIV;
	}

	if (pbc_shift > 0) {
		pbc_cksel = (PM_PBCSEL_PBSEL(pbc_shift - 1))
				| PM_PBCSEL_PBDIV;
	}

	if (pbd_shift > 0) {
		pbd_cksel = (PM_PBDSEL_PBSEL(pbd_shift - 1))
				| PM_PBDSEL_PBDIV;
	}

	flags = cpu_irq_save();

	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_CPUSEL - (uint32_t)PM);
	PM->PM_CPUSEL = cpu_cksel;

	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBASEL - (uint32_t)PM);
	PM->PM_PBASEL = pba_cksel;

	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBBSEL - (uint32_t)PM);
	PM->PM_PBBSEL = pbb_cksel;

	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBCSEL - (uint32_t)PM);
	PM->PM_PBCSEL = pbc_cksel;

	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBDSEL - (uint32_t)PM);
	PM->PM_PBDSEL = pbd_cksel;

	cpu_irq_restore(flags);
}

/**
 * \brief Change the source of the main system clock.
 *
 * \param src The new system clock source. Must be one of the constants
 * from the <em>System Clock Sources</em> section.
 */
void sysclk_set_source(uint32_t src)
{
	irqflags_t flags;
	Assert(src <= SYSCLK_SRC_RC1M);

	flags = cpu_irq_save();
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_MCCTRL - (uint32_t)PM);
	PM->PM_MCCTRL = src;
	cpu_irq_restore(flags);
}

#if defined(CONFIG_USBCLK_SOURCE) || defined(__DOXYGEN__)
/**
 * \brief Enable the USB generic clock
 *
 * \pre The USB generic clock must be configured to 48MHz.
 * CONFIG_USBCLK_SOURCE and CONFIG_USBCLK_DIV must be defined with proper
 * configuration. The selected clock source must also be configured.
 */
void sysclk_enable_usb(void)
{
	// Note: the SYSCLK_PBB_BRIDGE clock is enabled by
	// sysclk_enable_pbb_module().
	sysclk_enable_pbb_module(SYSCLK_USBC_REGS);
	sysclk_enable_hsb_module(SYSCLK_USBC_DATA);

	genclk_enable_config(7, CONFIG_USBCLK_SOURCE, CONFIG_USBCLK_DIV);
}

/**
 * \brief Disable the USB generic clock
 */
void sysclk_disable_usb(void)
{
   genclk_disable(7);
}
#endif // CONFIG_USBCLK_SOURCE

void sysclk_init(void)
{
	uint32_t ps_value = 0;
	bool is_fwu_enabled = false;

#if CONFIG_HCACHE_ENABLE == 1
	/* Enable HCACHE */
	sysclk_enable_peripheral_clock(HCACHE);
	HCACHE->HCACHE_CTRL = HCACHE_CTRL_CEN_YES;
	while (!(HCACHE->HCACHE_SR & HCACHE_SR_CSTS_EN));
#endif

	/* Set up system clock dividers if different from defaults */
	if ((CONFIG_SYSCLK_CPU_DIV > 0) || (CONFIG_SYSCLK_PBA_DIV > 0) ||
			(CONFIG_SYSCLK_PBB_DIV > 0) || (CONFIG_SYSCLK_PBC_DIV > 0) ||
			(CONFIG_SYSCLK_PBD_DIV > 0)) {
		sysclk_set_prescalers(CONFIG_SYSCLK_CPU_DIV,
				CONFIG_SYSCLK_PBA_DIV,
				CONFIG_SYSCLK_PBB_DIV,
				CONFIG_SYSCLK_PBC_DIV,
				CONFIG_SYSCLK_PBD_DIV
				);
	}

	/* Automatically select best power scaling mode */
#ifdef CONFIG_FLASH_READ_MODE_HIGH_SPEED_ENABLE
	ps_value = BPM_PS_2;
	is_fwu_enabled = false;
#else
	if (sysclk_get_cpu_hz() <= FLASH_FREQ_PS1_FWS_1_MAX_FREQ) {
		ps_value = BPM_PS_1;
		if (sysclk_get_cpu_hz() > FLASH_FREQ_PS1_FWS_0_MAX_FREQ) {
			bpm_enable_fast_wakeup(BPM);
			is_fwu_enabled = true;
		}
	} else {
		ps_value = BPM_PS_0;
	}
#endif

	/* Switch to system clock selected by user */
	if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_RCSYS) {
		/* Already running from RCSYS */
	}
#ifdef BOARD_OSC0_HZ
	else if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_OSC0) {
		osc_enable(OSC_ID_OSC0);
		osc_wait_ready(OSC_ID_OSC0);
		// Set a flash wait state depending on the new cpu frequency.
		flash_set_bus_freq(sysclk_get_cpu_hz(), ps_value, is_fwu_enabled);
		sysclk_set_source(SYSCLK_SRC_OSC0);
	}
#endif
#ifdef CONFIG_DFLL0_SOURCE
	else if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_DFLL) {
		dfll_enable_config_defaults(0);
		// Set a flash wait state depending on the new cpu frequency.
		flash_set_bus_freq(sysclk_get_cpu_hz(), ps_value, is_fwu_enabled);
		sysclk_set_source(SYSCLK_SRC_DFLL);
	}
#endif
#ifdef CONFIG_PLL0_SOURCE
	else if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_PLL0) {
		pll_enable_config_defaults(0);
		// Set a flash wait state depending on the new cpu frequency.
		flash_set_bus_freq(sysclk_get_cpu_hz(), ps_value, is_fwu_enabled);
		sysclk_set_source(SYSCLK_SRC_PLL0);
	}
#endif
	else if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_RC80M) {
		osc_enable(OSC_ID_RC80M);
		osc_wait_ready(OSC_ID_RC80M);
		// Set a flash wait state depending on the new cpu frequency.
		flash_set_bus_freq(sysclk_get_cpu_hz(), ps_value, is_fwu_enabled);
		sysclk_set_source(SYSCLK_SRC_RC80M);
	}
	else if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_RCFAST) {
		osc_enable(OSC_ID_RCFAST);
		osc_wait_ready(OSC_ID_RCFAST);
		// Set a flash wait state depending on the new cpu frequency.
		flash_set_bus_freq(sysclk_get_cpu_hz(), ps_value, is_fwu_enabled);
		sysclk_set_source(SYSCLK_SRC_RCFAST);
	}
	else if (CONFIG_SYSCLK_SOURCE == SYSCLK_SRC_RC1M) {
		osc_enable(OSC_ID_RC1M);
		osc_wait_ready(OSC_ID_RC1M);
		// Set a flash wait state depending on the new cpu frequency.
		flash_set_bus_freq(sysclk_get_cpu_hz(), ps_value, is_fwu_enabled);
		sysclk_set_source(SYSCLK_SRC_RC1M);
	}
	else {
		Assert(false);
	}

	/* Automatically switch to low power mode */
	bpm_configure_power_scaling(BPM, ps_value, BPM_PSCM_CPU_NOT_HALT);
	while ((bpm_get_status(BPM) & BPM_SR_PSOK) == 0);

	/* If the user has specified clock masks, enable only requested clocks */
	irqflags_t const flags = cpu_irq_save();
#if defined(CONFIG_SYSCLK_INIT_CPUMASK)
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_CPUMASK - (uint32_t)PM);
	PM->PM_CPUMASK = SYSCLK_INIT_MINIMAL_CPUMASK | CONFIG_SYSCLK_INIT_CPUMASK;
#endif

#if defined(CONFIG_SYSCLK_INIT_HSBMASK)
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_HSBMASK - (uint32_t)PM);
	PM->PM_HSBMASK = SYSCLK_INIT_MINIMAL_HSBMASK | CONFIG_SYSCLK_INIT_HSBMASK;
#endif

#if defined(CONFIG_SYSCLK_INIT_PBAMASK)
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBAMASK - (uint32_t)PM);
	PM->PM_PBAMASK = SYSCLK_INIT_MINIMAL_PBAMASK | CONFIG_SYSCLK_INIT_PBAMASK;
#endif

#if defined(CONFIG_SYSCLK_INIT_PBBMASK)
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBBMASK - (uint32_t)PM);
	PM->PM_PBBMASK = SYSCLK_INIT_MINIMAL_PBBMASK | CONFIG_SYSCLK_INIT_PBBMASK;
#endif

#if defined(CONFIG_SYSCLK_INIT_PBCMASK)
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBCMASK - (uint32_t)PM);
	PM->PM_PBCMASK = SYSCLK_INIT_MINIMAL_PBCMASK | CONFIG_SYSCLK_INIT_PBCMASK;
#endif

#if defined(CONFIG_SYSCLK_INIT_PBDMASK)
	PM->PM_UNLOCK = PM_UNLOCK_KEY(0xAAu)
		| PM_UNLOCK_ADDR((uint32_t)&PM->PM_PBDMASK - (uint32_t)PM);
	PM->PM_PBDMASK = SYSCLK_INIT_MINIMAL_PBDMASK | CONFIG_SYSCLK_INIT_PBDMASK;
#endif

	cpu_irq_restore(flags);

#if (defined CONFIG_SYSCLK_DEFAULT_RETURNS_SLOW_OSC)
	/* Signal that the internal frequencies are setup */
	sysclk_initialized = true;
#endif
}

//! @}
