/*
 * Copyright 2004 Freescale Semiconductor, Inc. All Rights Reserved.
 */

/*
 * The code contained herein is licensed under the GNU General Public
 * License. You may obtain a copy of the GNU General Public License
 * Version 2 or later at the following locations:
 *
 * http://www.opensource.org/licenses/gpl-license.html
 * http://www.gnu.org/copyleft/gpl.html
 */

/*!
 * @file mxc_pm_argonplus.c
 *
 * @brief This file provides all the kernel level and user level API
 * definitions for the CRM_MCU and DPLL in ArgonPlus.
 *
 * @ingroup LPMD
 */

/*
 * Include Files
 */
#include <linux/config.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kdev_t.h>
#include <linux/major.h>
#include <linux/mm.h>
#include <linux/devfs_fs_kernel.h>
#include <linux/circ_buf.h>
#include <asm/arch/platform.h>
#include <asm/arch/mxc_pm.h>
#include <asm/arch/system.h>
#include <asm/arch/clock.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>
#include <asm/system.h>
#include "mxc_pm_argonplus.h"

#define LO_INTR_EN              IO_ADDRESS(AVIC_BASE_ADDR + 0x14)

/*!
 * To find the BRM divider which supports the next lowest ARM frequency
 * supported using Integer Scaling
 *
 * @param   armfreq    The desired ARM frequency
 *
 * @return             Returns BRM divider value
 */
static int mxc_pm_findnext(long armfreq)
{
        unsigned long mpdr, mcu_main_clk, max_clk, t_pll, t_max, t_arm;
        unsigned int brmm = 0, max_pdf;

        mcu_main_clk = (mxc_pll_clock(MCUPLL) / 1000000) + 1;
        mpdr = readl(CRM_MCU_MPDR0);
        max_pdf = (mpdr & MXC_CCM_MPDR0_MAX_PDF_MASK) >> MXC_CCM_MPDR0_MAX_PDF_OFFSET;
        max_clk = mcu_main_clk / (max_pdf + 1);
        t_pll = 1000000 / mcu_main_clk;
        t_max = 1000000 / max_clk;
        t_arm = 1000000 / armfreq;
        if ((t_arm >= t_pll) && (t_arm <= (((2 * t_pll) + (t_max)) / 3))) {
                brmm = 1;
        } else if ((t_arm >= (((2 * t_pll) + (t_max)) / 3)) &&
                   (t_arm <= ((t_pll + t_max) / 2))) {
                brmm = 2;
        } else if ((t_arm >= ((t_pll + t_max) / 2)) &&
                   (t_arm <= ((t_pll + (2 * t_max)) / 3))) {
                brmm = 3;
        } else if ((t_arm >= ((t_pll + (2 * t_max)) / 3)) && (t_arm <= t_max)) {
                brmm = 4;
        }
        return brmm;
}

/*!
 * Calculating possible divider values for MAX and IPG
 *
 * @param   armfreq     The ARM frequency desired or changed as a result
 *                     of PLL or Integer scaling
 * @param   ahbfreq    The desired AHB frequency
 * @param   ipfreq     The desired IP frequency
 *
 * @return             Returns -FREQ_OUT_OF_RANGE if AHB freq is
 *                     greater than 133MHz or if IPG freq is greater than
 *                     66.5MHz and 0 on success
 */
static int mxc_pm_div_adjust(long armfreq, long ahbfreq, long ipfreq, int intorpll)
{
        unsigned long mpdr, max_clk;
        unsigned int max_pdf, ipg_pdf;

        if (ahbfreq > AHB_FREQ || ipfreq > IPG_FREQ) {
                return -FREQ_OUT_OF_RANGE;
        }
        if (intorpll > 0) {
                armfreq = ((mxc_pll_clock(MCUPLL) / 1000000) + 1) * 1000000;
        }
        armfreq = armfreq / 1000000;
        ahbfreq = ahbfreq / 1000000;
        ipfreq = ipfreq / 1000000;
        mpdr = readl(CRM_MCU_MPDR0);
        max_pdf = (armfreq / ahbfreq) - 1;
        max_clk = armfreq / (max_pdf + 1);
        ipg_pdf = (max_clk / ipfreq) - 1;
        if (max_clk > ahbfreq) {
                max_pdf++;
        }
        if ((max_clk / (ipg_pdf + 1)) > ipfreq) {
                ipg_pdf++;
        }
        writel((((readl(CRM_MCU_MPDR0) & 0xFFFFFF07) | (max_pdf << MXC_CCM_MPDR0_MAX_PDF_OFFSET))
                         | (ipg_pdf << MXC_CCM_MPDR0_IPG_PDF_OFFSET)), CRM_MCU_MPDR0);
        return 0;
}

/*!
 * Implementing steps required to set Integer Scaling
 *
 * @param   armfreq    The desired ARM frequency. AHB and IP
 *                     frequency are changed depending on ARM
 *                     frequency and the divider values.
 * @param   ahbfreq    The desired AHB frequency
 * @param   ipfreq     The desired IP frequency
 *
 * @return             Returns 0 on success or
 *                     Returns -PLL_LESS_ARM_ERR if pllfreq is less than
 *                     desired core freq
 */
int mxc_pm_intscale(long armfreq, long ahbfreq, long ipfreq)
{
        unsigned long mpdr, mcu_main_clk, max_clk, t_pll, t_max, t_arm;
        unsigned int brmm = 0, max_pdf, ipg_pdf;

        mcu_main_clk = (mxc_pll_clock(MCUPLL) / 1000000) + 1;
        armfreq = armfreq / 1000000;
        ahbfreq = ahbfreq / 1000000;
        ipfreq = ipfreq / 1000000;
        if ((armfreq / 1000000) > ((mcu_main_clk / 1000000) + 1)) {
                return -PLL_LESS_ARM_ERR;
        }
        mpdr = readl(CRM_MCU_MPDR0);
        max_pdf = (mpdr & MXC_CCM_MPDR0_MAX_PDF_MASK) >> MXC_CCM_MPDR0_MAX_PDF_OFFSET;
        ipg_pdf = (mpdr & MXC_CCM_MPDR0_IPG_PDF_MASK) >> MXC_CCM_MPDR0_IPG_PDF_OFFSET;
        max_clk = mcu_main_clk / (max_pdf + 1);
        if (((ahbfreq * 1000000) <= AHB_FREQ) && (max_clk != ahbfreq)) {
                mxc_pm_div_adjust((armfreq * 1000000), (ahbfreq * 1000000), (ipfreq * 1000000), 1);
        }
        mpdr = readl(CRM_MCU_MPDR0);
        max_pdf = (mpdr & MXC_CCM_MPDR0_MAX_PDF_MASK) >> MXC_CCM_MPDR0_MAX_PDF_OFFSET;
        ipg_pdf = (mpdr & MXC_CCM_MPDR0_IPG_PDF_MASK) >> MXC_CCM_MPDR0_IPG_PDF_OFFSET;
        max_clk = mcu_main_clk / (max_pdf + 1);
        t_pll = 10000 / mcu_main_clk;
        t_max = 10000 / max_clk;
        t_arm = 10000 / armfreq;
        if (t_arm == t_pll) {
                brmm = 0;
        } else if (t_arm == (((2 * t_pll) + (t_max)) / 3)) {
                brmm = 1;
        } else if (t_arm == ((t_pll + t_max) / 2)) {
                brmm = 2;
        } else if (t_arm == ((t_pll + (2 * t_max)) / 3)) {
                brmm = 3;
        } else if (t_arm == t_max) {
                brmm = 4;
        } else {
                brmm = mxc_pm_findnext(armfreq);
        }
        mpdr = readl(CRM_MCU_MPDR0);
        writel(((mpdr & 0xFFFFFFF8) | brmm), CRM_MCU_MPDR0);
        return 0;
}

/*!
 * To calculate MFI, MFN, MFD values. Using this the output frequency
 * whose value is calculated using,
 * 2 * REF_FREQ * (MF / PDF), where
 * REF_FREQ is 26 Mhz
 * MF = MFI + (MFN + MFD)
 * PDF is assumed to be 1
 *
 * @param   armfreq    The desired ARM frequency
 * @param   ahbfreq    The desired AHB frequency
 * @param   ipfreq     The desired IP frequency
 *
 * @return             Returns 0 on success or
 *                     Returns -1 on error
 */
int mxc_pm_pllscale(long armfreq, long ahbfreq, long ipfreq)
{
        signed long mf, mfi, mfn, mfd, pdf, ref_freq, freq_out, pllfreq;
        int brmm, ret_val = 0;
        unsigned long mpctl = readl(CRM_MCU_MPCTL);

        pdf = (signed long) ((mpctl & MXC_CCM_PCTL_PDF_MASK) >> MXC_CCM_PCTL_PDF_OFFSET);
        ref_freq = REF_FREQ;
        pllfreq = mxc_pll_clock(MCUPLL);
        mf = (armfreq * (pdf + 1)) / (2 * ref_freq);
        mfi = (int)mf;
        mfn = (armfreq - (2 * mf * ref_freq)) / 1000000;
        mfd = (2 * REF_FREQ) / 1000000;
        mfi = (mfi <= 5) ? 5: mfi;
        mfn = (mfn <= 0x400) ? mfn: (mfn - 0x800);
        /*
         * Calculate freq_out to see whether required freq is
         * less than the calculated freq. If calc freq is greater then
         * the actual output freq value should be minimum of current
         * PLL freq or total possible min freq
         */
        freq_out = (2 * ref_freq * mfi + ((2 * ref_freq / (mfd + 1)) * mfn)) /
                   (pdf + 1);
        if (freq_out > armfreq) {
                if (pllfreq <= freq_out) {
                        /* retain mfd and mfn values to old ones */
                        mfn = mpctl & 0xFFFFF800;
                } else if (mfi == 0x5) {
                        /*
                         * Reset mfn and mfd to 0, so we get minimun
                         * possible frequency obtainable using
                         * PLL scaling which is 260MHz
                         */
                        mfn = mfd = 0;
                }
        }
        if (armfreq > pllfreq) {
                ret_val = mxc_pm_div_adjust(armfreq, ahbfreq, ipfreq, -1);
        }
        /* Check if BRMM is 1 */
        brmm = readl(CRM_MCU_MPDR0) & 0x00000007;
        mpctl = (mpctl & 0xFC000000) | (mfd << 16) | (mfi << 11) | mfn;
        writel(mpctl, CRM_MCU_MPCTL);
        if (brmm > 0) {
                writel((readl(CRM_MCU_MPDR0) & 0xFFFFFFF8), CRM_MCU_MPDR0);
        }
        if (armfreq < pllfreq) {
                ret_val = mxc_pm_div_adjust(armfreq, ahbfreq, ipfreq, -1);
        }
        return ret_val;
}

/*!
 * Configuring CKOH pins to measure changed frequency
 *
 * @param   opt     The desired clock needed to measure.
 *
 */
void mxc_pm_ckohsel(unsigned int opt)
{
        switch(opt) {
        case 1:
                /* To select AP clock */
                writel(((readl(CRM_MCU_COSR) & 0x00000000) | 0x00037000), CRM_MCU_COSR);
                break;
        case 2:
                /* To select AHB clock */
                writel(((readl(CRM_MCU_COSR) & 0x00000000) | 0x00026C00), CRM_MCU_COSR);
                break;
        case 3:
                /* To select IP clock */
                writel(((readl(CRM_MCU_COSR) & 0x00000000) | 0x0002EC00), CRM_MCU_COSR);
                break;
        }
}

/*!
 * Implementing steps required to transition to low-power modes
 *
 * @param   mode    The desired low-power mode. Possible values are,
 *                  WAIT_MODE, DOZE_MODE, STOP_MODE or DSM_MODE
 *
 */
void mxc_pm_lowpower(int mode)
{
        unsigned int crm_ctrl;

        local_irq_disable();
        switch(mode) {
        case WAIT_MODE:
                /*
                 * Handled by kernel using arch_idle()
                 * There are no LPM bits for WAIT mode which
                 * are set by software
                 */
                break;
        case DOZE_MODE:
                /* Writing '00' to CRM_lpmd bits */
                /* Bypass all acknowledgements */
                /* Disable Timer interrupt source */
                writel((readl(LO_INTR_EN) & 0xC7FFFFFF), LO_INTR_EN);
                crm_ctrl = ((readl(CRM_MCU_MCR) & 0x9FFFFFFF) | (MCR_LPMD1)) & (~MCR_LPMD0);
                writel(crm_ctrl, CRM_MCU_MCR);
                break;
        case STOP_MODE:
                /* Disable Timer interrupt source */
                writel((readl(LO_INTR_EN) & 0xC7FFFFFF), LO_INTR_EN);
                writel((readl(CRM_MCU_MCR) & (~MCR_DPE)), CRM_MCU_MCR);
                crm_ctrl = ((readl(CRM_MCU_MCR) & 0x9FFFFFFF) | (MCR_LPMD1)) | (MCR_LPMD0);
                writel(crm_ctrl, CRM_MCU_MCR);
                break;
        case DSM_MODE:
                writel((readl(CRM_MCU_MCR) | (MCR_WBEN)), CRM_MCU_MCR);
                writel((readl(CRM_MCU_MCR) | (MCR_SBYOS)), CRM_MCU_MCR);
                mxc_pm_lowpower(STOP_MODE);
                break;
        }
        /* Executing CP15 (Wait-for-Interrupt) Instruction */
        arch_idle();
        /* Enable timer interrupt */
        writel((readl(LO_INTR_EN) | 0x38000000), LO_INTR_EN);
        local_irq_enable();
}

/*!
 * This function is used to load the module.
 *
 * @return   Returns an Integer on success
 */
int __init mxc_pm_init_module(void)
{
        unsigned long cosr;

        cosr = readl(CRM_MCU_COSR);
        writel(((cosr & 0xFFFC3FFF) | (0x00002000)), CRM_MCU_COSR);
        printk("Low-Level PM Driver module loaded\n");
        return 0;
}

/*!
 * This function is used to unload the module
 */
void __exit mxc_pm_cleanup_module(void)
{
        printk("Low-Level PM Driver module Unloaded\n");
}

module_init(mxc_pm_init_module);
module_exit(mxc_pm_cleanup_module);

EXPORT_SYMBOL(mxc_pm_intscale);
EXPORT_SYMBOL(mxc_pm_pllscale);
EXPORT_SYMBOL(mxc_pm_ckohsel);
EXPORT_SYMBOL(mxc_pm_lowpower);

MODULE_AUTHOR("Freescale Semiconductor");
MODULE_DESCRIPTION("SCMA11 Low-level PM Driver");
MODULE_LICENSE("GPL");

