4160 lines
117 KiB
C
4160 lines
117 KiB
C
/*
|
|
* clk-t124-dfll.c - clock provider support for the Tegra124 DFLL clock source
|
|
*
|
|
* Copyright (C) 2012-2013 NVIDIA Corporation. All rights reserved.
|
|
*
|
|
* Aleksandr Frid <afrid@nvidia.com>
|
|
* Paul Walmsley <pwalmsley@nvidia.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* ...
|
|
*
|
|
* DFLL states:
|
|
*
|
|
* - DISABLED: control logic mode - DISABLED, output interface disabled,
|
|
* dfll in reset
|
|
* - OPEN_LOOP: control logic mode - OPEN_LOOP, output interface disabled,
|
|
* dfll is running "unlocked"
|
|
* - CLOSED_LOOP: control logic mode - CLOSED_LOOP, output interface enabled,
|
|
* dfll is running "locked"
|
|
*
|
|
* In the following code, 'ol' is used to abbreviate 'open loop', and
|
|
* 'cl' is used to abbreviate 'closed loop'.
|
|
*
|
|
* The IP block controlled by this driver is also known as "CL_DVFS".
|
|
*/
|
|
|
|
#include <linux/kernel.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/module.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/err.h>
|
|
#include <linux/io.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/clk-provider.h>
|
|
#include <linux/suspend.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/seq_file.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/of.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/thermal.h>
|
|
|
|
#include <linux/clk/tegra124-dfll.h>
|
|
/*
|
|
* XXX The fuse reading functions should be moved into a driver underneath
|
|
* drivers/, and their symbols exported via EXPORT_SYMBOL(), rather than
|
|
* using functions in arch/arm/mach-tegra/.
|
|
*/
|
|
#include <linux/tegra-soc.h>
|
|
|
|
#include "clk.h"
|
|
|
|
#define DRIVER_NAME "t124_dfll"
|
|
|
|
#define to_tegra_dfll_clk_hw(_hw) container_of(_hw, struct tegra_dfll_clk_hw, \
|
|
hw)
|
|
|
|
/*
|
|
* DFLL register offset & bitfield macros
|
|
*/
|
|
|
|
/* DFLL_CTRL: DFLL control register */
|
|
#define DFLL_CTRL 0x00
|
|
#define DFLL_CTRL_MODE_MASK 0x03
|
|
|
|
/* DFLL_CONFIG: DFLL sample rate control */
|
|
#define DFLL_CONFIG 0x04
|
|
#define DFLL_CONFIG_DIV_MASK 0xff
|
|
#define DFLL_CONFIG_DIV_PRESCALE 32
|
|
|
|
/* DFLL_PARAMS: Loop control register */
|
|
#define DFLL_PARAMS 0x08
|
|
#define DFLL_PARAMS_CG_SCALE (0x1 << 24)
|
|
#define DFLL_PARAMS_CG_SCALE_SHIFT 24
|
|
#define DFLL_PARAMS_FORCE_MODE_SHIFT 22
|
|
#define DFLL_PARAMS_FORCE_MODE_MASK (0x3 << DFLL_PARAMS_FORCE_MODE_SHIFT)
|
|
#define DFLL_PARAMS_CF_PARAM_SHIFT 16
|
|
#define DFLL_PARAMS_CF_PARAM_MASK (0x3f << DFLL_PARAMS_CF_PARAM_SHIFT)
|
|
#define DFLL_PARAMS_CI_PARAM_SHIFT 8
|
|
#define DFLL_PARAMS_CI_PARAM_MASK (0x7 << DFLL_PARAMS_CI_PARAM_SHIFT)
|
|
#define DFLL_PARAMS_CG_PARAM_SHIFT 0
|
|
#define DFLL_PARAMS_CG_PARAM_MASK (0xff << DFLL_PARAMS_CG_PARAM_SHIFT)
|
|
|
|
/* DFLL_TUNE0: delay line configuration register 0 */
|
|
#define DFLL_TUNE0 0x0c
|
|
#define DFLL_TUNE0_DLY_STK_SHIFT 16
|
|
#define DFLL_TUNE0_DLY_STK_MASK (0xff << DFLL_TUNE0_DLY_STK_SHIFT)
|
|
#define DFLL_TUNE0_DLY_SRAM_SHIFT 8
|
|
#define DFLL_TUNE0_DLY_SRAM_MASK (0xff << DFLL_TUNE0_DLY_SRAM_SHIFT)
|
|
#define DFLL_TUNE0_DLY_INV_SHIFT 0
|
|
#define DFLL_TUNE0_DLY_INV_MASK (0xff << DFLL_TUNE0_DLY_INV_SHIFT)
|
|
|
|
/* DFLL_TUNE1: delay line configuration register 1 */
|
|
#define DFLL_TUNE1 0x10
|
|
#define DFLL_TUNE1_DLY_FINE_SHIFT 11
|
|
#define DFLL_TUNE1_DLY_FINE_MASK (0x1ff << DFLL_TUNE1_DLY_FINE_SHIFT)
|
|
#define DFLL_TUNE1_DLY_WIRE_SHIFT 0
|
|
#define DFLL_TUNE1_DLY_WIRE_MASK (0x7ff << DFLL_TUNE1_DLY_WIRE_SHIFT)
|
|
|
|
/* DFLL_FREQ_REQ: target DFLL frequency control */
|
|
#define DFLL_FREQ_REQ 0x14
|
|
#define DFLL_FREQ_REQ_FORCE_ENABLE (0x1 << 28)
|
|
#define DFLL_FREQ_REQ_FORCE_SHIFT 16
|
|
#define DFLL_FREQ_REQ_FORCE_MASK (0xfff << DFLL_FREQ_REQ_FORCE_SHIFT)
|
|
#define FORCE_MAX 2047
|
|
#define FORCE_MIN -2048
|
|
#define DFLL_FREQ_REQ_SCALE_SHIFT 8
|
|
#define DFLL_FREQ_REQ_SCALE_MASK (0xff << DFLL_FREQ_REQ_SCALE_SHIFT)
|
|
#define SCALE_MAX 256
|
|
#define DFLL_FREQ_REQ_FREQ_VALID (0x1 << 7)
|
|
#define DFLL_FREQ_REQ_FREQ_SHIFT 0
|
|
#define DFLL_FREQ_REQ_FREQ_MASK (0x7f << DFLL_FREQ_REQ_FREQ_SHIFT)
|
|
#define FREQ_MAX 127
|
|
|
|
/* DFLL_SCALE_RAMP: slope control for output frequency ramp */
|
|
#define DFLL_SCALE_RAMP 0x18
|
|
#define DFLL_SCALE_RAMP_RATE_MASK 0xf
|
|
|
|
/* DFLL_DROOP_CTRL: droop prevention control */
|
|
#define DFLL_DROOP_CTRL 0x1c
|
|
#define DFLL_DROOP_CTRL_MIN_FREQ_SHIFT 16
|
|
#define DFLL_DROOP_CTRL_MIN_FREQ_MASK (0xff << DFLL_DROOP_CTRL_MIN_FREQ_SHIFT)
|
|
#define DFLL_DROOP_CTRL_CUT_SHIFT 8
|
|
#define DFLL_DROOP_CTRL_CUT_MASK (0xf << DFLL_DROOP_CTRL_CUT_SHIFT)
|
|
#define DFLL_DROOP_CTRL_RAMP_SHIFT 0
|
|
#define DFLL_DROOP_CTRL_RAMP_MASK (0xff << DFLL_DROOP_CTRL_RAMP_SHIFT)
|
|
|
|
/* DFLL_OUTPUT_CFG: PMIC interface control */
|
|
#define DFLL_OUTPUT_CFG 0x20
|
|
#define DFLL_OUTPUT_CFG_I2C_ENABLE (0x1 << 30)
|
|
#define OUT_MASK 0x3f
|
|
#define DFLL_OUTPUT_CFG_SAFE_SHIFT 24
|
|
#define DFLL_OUTPUT_CFG_SAFE_MASK (OUT_MASK << DFLL_OUTPUT_CFG_SAFE_SHIFT)
|
|
#define DFLL_OUTPUT_CFG_MAX_SHIFT 16
|
|
#define DFLL_OUTPUT_CFG_MAX_MASK (OUT_MASK << DFLL_OUTPUT_CFG_MAX_SHIFT)
|
|
#define DFLL_OUTPUT_CFG_MIN_SHIFT 8
|
|
#define DFLL_OUTPUT_CFG_MIN_MASK (OUT_MASK << DFLL_OUTPUT_CFG_MIN_SHIFT)
|
|
|
|
/* DFLL_OUTPUT_FORCE: output forcing control */
|
|
#define DFLL_OUTPUT_FORCE 0x24
|
|
#define DFLL_OUTPUT_FORCE_ENABLE_SHIFT 6
|
|
#define DFLL_OUTPUT_FORCE_ENABLE_MASK (0x1 << DFLL_OUTPUT_FORCE_ENABLE_SHIFT)
|
|
#define DFLL_OUTPUT_FORCE_DEBOUNCE_CNT_SHIFT 0
|
|
#define DFLL_OUTPUT_FORCE_DEBOUNCE_CNT_MASK \
|
|
(0x3f << DFLL_OUTPUT_FORCE_DEBOUNCE_CNT_SHIFT)
|
|
|
|
/* DFLL_MONITOR_CTRL: loop data source control */
|
|
#define DFLL_MONITOR_CTRL 0x28
|
|
#define DFLL_MONITOR_CTRL_SRC_SHIFT 0
|
|
#define DFLL_MONITOR_CTRL_SRC_MASK (0x7 << DFLL_MONITOR_CTRL_SRC_SHIFT)
|
|
#define DFLL_MONITOR_CTRL_DISABLE 0
|
|
#define DFLL_MONITOR_CTRL_FREQ 6
|
|
|
|
/* DFLL_MONITOR_DATA: internal monitoring */
|
|
#define DFLL_MONITOR_DATA 0x2c
|
|
#define DFLL_MONITOR_DATA_NEW_MASK (0x1 << 16)
|
|
#define DFLL_MONITOR_DATA_VAL_MASK 0xFFFF
|
|
|
|
/* DFLL_I2C_CFG: I2C controller configuration register */
|
|
#define DFLL_I2C_CFG 0x40
|
|
#define DFLL_I2C_CFG_ARB_ENABLE (0x1 << 20)
|
|
#define DFLL_I2C_CFG_HS_CODE_SHIFT 16
|
|
#define DFLL_I2C_CFG_HS_CODE_MASK (0x7 << DFLL_I2C_CFG_HS_CODE_SHIFT)
|
|
#define DFLL_I2C_CFG_PACKET_ENABLE (0x1 << 15)
|
|
#define DFLL_I2C_CFG_SIZE_SHIFT 12
|
|
#define DFLL_I2C_CFG_SIZE_MASK (0x7 << DFLL_I2C_CFG_SIZE_SHIFT)
|
|
#define DFLL_I2C_CFG_SLAVE_ADDR_10 (0x1 << 10)
|
|
#define DFLL_I2C_CFG_SLAVE_ADDR_SHIFT 0
|
|
#define DFLL_I2C_CFG_SLAVE_ADDR_MASK (0x3ff << DFLL_I2C_CFG_SLAVE_ADDR_SHIFT)
|
|
|
|
/* DFLL_I2C_VDD_REG_ADDR: PMIC internal register controlling VDD */
|
|
#define DFLL_I2C_VDD_REG_ADDR 0x44
|
|
#define DFLL_I2C_DEFAULT_DATA_SHIFT 8
|
|
#define DFLL_I2C_DEFAULT_DATA_MASK (0xff << DFLL_I2C_DEFAULT_DATA_SHIFT)
|
|
#define DFLL_I2C_ADDR_DATA_SHIFT 0
|
|
#define DFLL_I2C_ADDR_DATA_MASK (0xff << DFLL_I2C_ADDR_DATA_SHIFT)
|
|
|
|
/* DFLL_I2C_STS: I2C controller status */
|
|
#define DFLL_I2C_STS 0x48
|
|
#define DFLL_I2C_STS_I2C_LAST_SHIFT 1
|
|
#define DFLL_I2C_STS_I2C_REQ_PENDING 0x1
|
|
|
|
/* DFLL_INTR_STS: DFLL interrupt status register */
|
|
#define DFLL_INTR_STS 0x5c
|
|
|
|
/* DFLL_INTR_EN: DFLL interrupt enable register */
|
|
#define DFLL_INTR_EN 0x60
|
|
#define DFLL_INTR_MIN_MASK 0x1
|
|
#define DFLL_INTR_MAX_MASK 0x2
|
|
|
|
/* DFLL_I2C_CLK_DIVISOR: I2C controller clock divisor */
|
|
#define DFLL_I2C_CLK_DIVISOR 0x16c
|
|
#define DFLL_I2C_CLK_DIVISOR_MASK 0xffff
|
|
#define DFLL_I2C_CLK_DIVISOR_FS_SHIFT 16
|
|
#define DFLL_I2C_CLK_DIVISOR_HS_SHIFT 0
|
|
#define DFLL_I2C_CLK_DIVISOR_PREDIV 8
|
|
#define DFLL_I2C_CLK_DIVISOR_HSMODE_PREDIV 12
|
|
|
|
/*
|
|
* DFLL_OUTPUT_LUT: Offset to the start of the 33x8-bit voltage lookup
|
|
* table. 32-bit aligned. Used in I2C mode only.
|
|
*/
|
|
#define DFLL_OUTPUT_LUT 0x200
|
|
|
|
|
|
/*
|
|
* Other constants
|
|
*/
|
|
|
|
/*
|
|
* DFLL_CALIBR_TIME: minimum time interval between DFLL calibration
|
|
* attempts, in microseconds
|
|
*/
|
|
#define DFLL_CALIBR_TIME 40000
|
|
|
|
/*
|
|
* DFLL_OUTPUT_PENDING_TIMEOUT: if the code waits for longer than
|
|
* DFLL_OUTPUT_PENDING_TIMEOUT microseconds for in-progress I2C
|
|
* commands to the PMIC to complete, then indicate a timeout error.
|
|
*/
|
|
#define DFLL_OUTPUT_PENDING_TIMEOUT 1000
|
|
|
|
/*
|
|
* DFLL_OUTPUT_RAMP_DELAY: after the DFLL's I2C PMIC output starts
|
|
* requesting the minimum TUNE_HIGH voltage, the minimum number of
|
|
* microseconds to wait for the PMIC's voltage output to finish
|
|
* slewing
|
|
*/
|
|
#define DFLL_OUTPUT_RAMP_DELAY 100
|
|
|
|
/*
|
|
* DFLL_TUNE_HIGH_DELAY: number of microseconds to wait between tests
|
|
* to see if the high voltage has been reached yet, during a
|
|
* transition from the low-voltage range to the high-voltage range
|
|
*/
|
|
#define DFLL_TUNE_HIGH_DELAY 2000
|
|
|
|
/*
|
|
* DFLL_TUNE_HIGH_MARGIN_STEPS: attempt to initially program the DFLL
|
|
* voltage target to a (DFLL_TUNE_HIGH_MARGIN_STEPS * 10 millivolt)
|
|
* margin above the high voltage floor, in closed-loop mode in the
|
|
* high-voltage range
|
|
*/
|
|
#define DFLL_TUNE_HIGH_MARGIN_STEPS 2
|
|
|
|
/* MAX_DFLL_VOLTAGES: number of LUT entries */
|
|
#define MAX_DFLL_VOLTAGES 33
|
|
|
|
/*
|
|
* MAX_THERMAL_CAPS: maximum number of thermal cap (temperature,
|
|
* voltage) tuples that can be specified in the driver data.
|
|
*/
|
|
#define MAX_THERMAL_CAPS 8
|
|
|
|
/*
|
|
* MAX_THERMAL_FLOORS: maximum number of thermal floor (temperature,
|
|
* voltage) tuples that can be specified in the driver data.
|
|
*/
|
|
#define MAX_THERMAL_FLOORS 8
|
|
|
|
/*
|
|
* MAX_DVFS_FREQS: maximum number of DFLL output frequencies that this
|
|
* driver supports.
|
|
*/
|
|
#define MAX_DVFS_FREQS 40
|
|
|
|
/*
|
|
* CVB_ROW_WIDTH: in the DT data, this represents the number of cells
|
|
* in each CVB table entry
|
|
*/
|
|
#define CVB_ROW_WIDTH 4
|
|
|
|
/*
|
|
* THERM_CAPS_ROW_WIDTH: in the DT data, this represents the number
|
|
* of cells in each thermal cap table entry
|
|
*/
|
|
#define THERM_CAPS_ROW_WIDTH 2
|
|
|
|
/*
|
|
* THERM_FLOORS_ROW_WIDTH: in the DT data, this represents the number
|
|
* of cells in each thermal floor table entry
|
|
*/
|
|
#define THERM_FLOORS_ROW_WIDTH 2
|
|
|
|
/*
|
|
* DFLL_OUTPUT_CLOCK_NAME: the output clock name representing the DVCO
|
|
* output rate -- registered with the clock framework.
|
|
*/
|
|
#define DFLL_OUTPUT_CLOCK_NAME "dfllCPU_out"
|
|
|
|
/*
|
|
* I2C_OUTPUT_ACTIVE_TEST_US: mandatory minimum interval (in
|
|
* microseconds) between testing whether the I2C controller is
|
|
* currently sending a voltage-set command. Some comments list this
|
|
* as being a worst-case margin for "disable propagation."
|
|
*/
|
|
#define I2C_OUTPUT_ACTIVE_TEST_US 2
|
|
|
|
/**
|
|
* enum tegra_dfll_force_mode - output voltage forcing mode during freq change
|
|
* %TEGRA_DFLL_FORCE_NONE: no I2C output forcing
|
|
* %TEGRA_DFLL_FORCE_FIXED: force for a fixed number of sample periods
|
|
* %TEGRA_DFLL_FORCE_AUTO: force for a calculated number of sample periods
|
|
*
|
|
* Controls whether the output voltage index that the DFLL
|
|
* communicates to the PMIC is forced to a certain value during
|
|
* frequency changes, and if so, how long the forced value persists.
|
|
* Only applies to I2C control.
|
|
*/
|
|
enum tegra_dfll_force_mode {
|
|
TEGRA_DFLL_FORCE_NONE = 0,
|
|
TEGRA_DFLL_FORCE_FIXED = 1,
|
|
TEGRA_DFLL_FORCE_AUTO = 2,
|
|
};
|
|
|
|
/**
|
|
* struct tegra_dfll_voltage_reg_map - map of voltages to PMIC register values
|
|
* @reg_value: PMIC voltage control register value that produces @reg_uV
|
|
* @reg_uv: microvolts DC that the PMIC will generate when @reg_value is used
|
|
*
|
|
* Maps a voltage @reg_uv to the PMIC voltage-set register value @reg_value.
|
|
*/
|
|
struct tegra_dfll_voltage_reg_map {
|
|
u8 reg_value;
|
|
int reg_uv;
|
|
};
|
|
|
|
/**
|
|
* enum tegra_dfll_ctrl_mode - DFLL hardware operating mode
|
|
* @TEGRA_DFLL_UNINITIALIZED: (uninitialized state - not in hardware bitfield)
|
|
* @TEGRA_DFLL_DISABLED: DFLL not generating an output clock
|
|
* @TEGRA_DFLL_OPEN_LOOP: DVCO running, but DFLL not adjusting voltage
|
|
* @TEGRA_DFLL_CLOSED_LOOP: DVCO running and DFLL adjusting PMIC voltage
|
|
*
|
|
* The integer corresponding to the last three states, minus one, is
|
|
* written to the DFLL hardware to change operating modes.
|
|
*/
|
|
enum tegra_dfll_ctrl_mode {
|
|
TEGRA_DFLL_UNINITIALIZED = 0,
|
|
TEGRA_DFLL_DISABLED = 1,
|
|
TEGRA_DFLL_OPEN_LOOP = 2,
|
|
TEGRA_DFLL_CLOSED_LOOP = 3,
|
|
};
|
|
|
|
/**
|
|
* enum tegra_dfll_tune_state - state of the voltage-regime switching code
|
|
* @TEGRA_DFLL_TUNE_LOW: DFLL in the low-voltage range (or open-loop mode)
|
|
* @TEGRA_DFLL_TUNE_WAIT_DFLL: waiting for DFLL voltage output to reach high
|
|
* @TEGRA_DFLL_TUNE_WAIT_PMIC: waiting for PMIC to react to DFLL output
|
|
* @TEGRA_DFLL_TUNE_HIGH: DFLL in the high-voltage range
|
|
*
|
|
* These are software states; these values are never written into
|
|
* registers.
|
|
*/
|
|
enum tegra_dfll_tune_state {
|
|
TEGRA_DFLL_TUNE_LOW = 0,
|
|
TEGRA_DFLL_TUNE_WAIT_DFLL,
|
|
TEGRA_DFLL_TUNE_WAIT_PMIC,
|
|
TEGRA_DFLL_TUNE_HIGH,
|
|
};
|
|
|
|
/**
|
|
* struct dfll_rate_req - target DFLL rate request data
|
|
* @freq: value to program to the FREQ bitfield of the DFLL_FREQ_REQ register
|
|
* @scale: value to program to the SCALE bitfield of DFLL_FREQ_REQ
|
|
* @output: desired voltage to force the PMIC voltage output to (LUT index)
|
|
* @cap: unconditionally safe voltage for this frequency
|
|
* @rate: frequency in Hz corresponding to @freq
|
|
*
|
|
* When in closed-loop mode, there is guaranteed to be some voltage
|
|
* margin below @cap for the DFLL to adjust down to.
|
|
*/
|
|
struct dfll_rate_req {
|
|
u8 freq;
|
|
u8 scale;
|
|
u8 output;
|
|
u8 cap;
|
|
unsigned long rate;
|
|
};
|
|
|
|
/*
|
|
* Possible values for struct tegra_dfll.flags:
|
|
*
|
|
* TEGRA_DFLL_FLAGS_I2C_FORCE_QUIET: disable the DFLL's PMIC voltage
|
|
* control output before disabling the DFLL IP block. Set when the
|
|
* 'i2c-quiet-output-workaround' is present in the DT data for the
|
|
* DFLL IP block.
|
|
*/
|
|
#define TEGRA_DFLL_FLAGS_I2C_FORCE_QUIET BIT(0)
|
|
|
|
/**
|
|
* struct tegra_dfll_cvb - CVB table
|
|
* @freq: (DT) target DVCO frequency
|
|
* @c0: (DT) DFLL calibration constant 0
|
|
* @c1: (DT) DFLL calibration constant 1
|
|
* @c2: (DT) DFLL calibration constant 2
|
|
*
|
|
* Voltage curve data, indexed by @freq. This data comes from DT.
|
|
*/
|
|
struct tegra_dfll_cvb {
|
|
unsigned long freq;
|
|
int c0;
|
|
int c1;
|
|
int c2;
|
|
};
|
|
|
|
/**
|
|
* struct tegra_dfll_therm - thermally-sensitive voltage caps
|
|
* @temp: maximum trip temperature
|
|
* @output: PMIC voltage output at @temp or below
|
|
* @mv: minimum voltage output, in millivolts, at @temp or below
|
|
*/
|
|
struct tegra_dfll_therm {
|
|
u8 temp;
|
|
u8 output;
|
|
u16 mv;
|
|
};
|
|
|
|
/**
|
|
* struct tegra_dfll_clk_hw - DFLL clk_hw wrapper for the clock framework
|
|
* @pdev: DFLL instance
|
|
* @hw: struct clk_hw - for use by the clock framework
|
|
*
|
|
* The @pdev is used by the DFLL driver's clock framework interface
|
|
* functions, to retrieve the DFLL context.
|
|
*/
|
|
struct tegra_dfll_clk_hw {
|
|
struct platform_device *pdev;
|
|
struct clk_hw hw;
|
|
};
|
|
|
|
/**
|
|
* struct tegra_dfll_dvfs_info - all frequency and voltage related table
|
|
*
|
|
* @num_freqs: number of total entries in @freqs
|
|
* @num_voltages: number of entries in out_map
|
|
* @cvb_table_len: number of CVB table entries (see @cvb_table)
|
|
* @freqs: map from CVB table index to target output clock frequency
|
|
* @cpu_dfll_millivolts: map from CVB table index to output voltage (in mV)
|
|
* @clk_dvfs_map: map from CVB table index to output LUT index
|
|
* @cvb_table: CVB table data, from DT
|
|
*
|
|
* @clk_dvfs_map: output voltage mapping: legacy dvfs table index -to-
|
|
* cl_dvfs output LUT index; cl_dvfs output LUT index -to- PMU
|
|
* value/voltage pair ptr
|
|
*/
|
|
struct tegra_dfll_dvfs_info {
|
|
u8 num_freqs;
|
|
u8 num_voltages;
|
|
u8 cvb_table_len;
|
|
unsigned long freqs[MAX_DVFS_FREQS];
|
|
int cpu_dfll_millivolts[MAX_DVFS_FREQS];
|
|
u8 clk_dvfs_map[MAX_DVFS_FREQS];
|
|
struct tegra_dfll_cvb *cvb_table;
|
|
};
|
|
|
|
/**
|
|
* struct tegra_dfll - context for a DFLL instance
|
|
* @base: virtual address that the DFLL IP block MMIO space is mapped to
|
|
* @soc_clk: DFLL logic clock input - 51MHz
|
|
* @ref_clk: Reference clock input - 51MHz
|
|
* @i2c_clk: I2C5 controller clock input
|
|
* @dfll_clk: our output clock - registered in dfll_init()
|
|
* @ref_rate: clock rate of @ref_clk. Does not change once set
|
|
* @vdd_map_size: number of entries in @vdd_map
|
|
* @vdd_map: list of all possible voltages & corresponding PMIC VSELs
|
|
* @out_map: map from LUT index to voltage & PMIC VSEL - before clamping
|
|
* @safe_output: LUT index of the minimum safe voltage (OPEN_LOOP or DISABLED)
|
|
* @tune_high_out_start: LUT index to start with when tuning to high range
|
|
* @tune_high_out_min: LUT index of the minimum high tuning range voltage
|
|
* @minimax_output: LUT index of the minimum level of the maximum CL voltage
|
|
* @dvco_rate_min: @out_rate_min, rounded to (@ref_rate / 2)
|
|
* @lut_min: LUT index of the minimum voltage to send to the PMIC
|
|
* @lut_max: LUT index of the maximum voltage to send to the PMIC
|
|
* @therm_caps_idx: LUT index of the maximum volatage
|
|
* @therm_floors_idx: LUT index of the minimum voltage (due to cold die effects)
|
|
* @last_req: most recent closed-loop output frequency request
|
|
* @tune_state: in closed-loop mode: is the DFLL in low- or high-voltage regime?
|
|
* @mode: DFLL hardware operating mode: disabled, open-loop, or closed-loop
|
|
* @resume_mode: DFLL mode to be set to when resuming
|
|
* @tune_timer: timer used to wait during TUNE_HIGH_REQUEST for high Vdd
|
|
* @tune_delay: interval between tune_timer_cb calls during TUNE_HIGH_REQUEST
|
|
* @flags: (see "Possible values for struct tegra_dfll.flags" above)
|
|
* @tune0_low_voltage_range: DFLL_TUNE0 reg value in the low-voltage regime
|
|
* @tune0_high_voltage_range: DFLL_TUNE0 reg value in the high-voltage regime
|
|
* @tune1: DFLL_TUNE1 reg value (valid for both voltage regimes)
|
|
* @droop_rate_min: min ring osc freq before voltage droop control is enabled
|
|
* @tune_high_min_mv: starting voltage (in mV) of the high-voltage tuning range
|
|
* @dent: dentry for DFLL debugfs (at /DRIVER_NAME)
|
|
* @sample_rate: control loop sample rate
|
|
* @force_mode: I2C: force PMIC voltage during a freq change? if so, how?
|
|
* @cf: I2C: duration to force the PMIC voltage after frequency change
|
|
* @cg: loop gain (signed)
|
|
* @cg_scale: set to 1 to divide loop gain by 8
|
|
* @ci: loop integral gain selector
|
|
* @droop_cut_value: control output clock scaler at minimum ring osc freq
|
|
* @droop_restore_ramp: clock recovery rate after a voltage droop event
|
|
* @out_rate_min: "FmaxAtVmin": DFLL max operating freq @ minimum voltage
|
|
* @scale_out_ramp: voltage output ramp rate
|
|
* @min_millivolts: minimum voltage (in millivolts)
|
|
* @cvb_max_millivolts: CVB maximum voltage limit (in millivolts)
|
|
* @cvb_speedo_scale: silicon characterization constant - from fuses
|
|
* @cvb_voltage_scale: CVB voltage scale factor (to microvolts)
|
|
* @speedo_id: CPU Speedo ID - from process characterization
|
|
* @process_id: CPU process ID - from process characterization
|
|
* @speedo_value: CPU Speedo value - from process characterization
|
|
* @pmic_i2c_addr: PMIC I2C address (when I2C interface is active)
|
|
* @pmic_i2c_voltage_reg: PMIC I2C voltage control register
|
|
* @pmic_i2c_fs_rate: I2C FS bus rate
|
|
* @pmic_i2c_hs_rate: I2C HS bus rate - set to talk to PMIC in HS mode
|
|
* @pmic_i2c_hs_master_code: PMIC I2C HS master code (only needed for HS mode)
|
|
* @pmic_i2c_ten_bit_addrs: use 10-bit I2C address to address PMIC
|
|
* @vdd: regulator controlling the DFLL's voltage rail
|
|
* @vdd_step: linear step size between VSEL values (from regulator framework)
|
|
* @dfll_min_microvolt: lowest voltage the DFLL should try to program (from DT)
|
|
* @dfll_max_microvolt: highest voltage the DFLL should try to program (from DT)
|
|
* @therm_caps_num: number of entries in @therm_caps
|
|
* @therm_caps: maximum voltage caps at various temperatures
|
|
* @therm_floors_num: number of entries in @therm_floors
|
|
* @therm_floors: minimum voltage floors at various cold die temperatures
|
|
* @calibration_timer: timer to periodically recalibrate dvco_min_rate
|
|
* @calibration_delay: time interval between calibrations - see DFLL_CALIBR_TIME
|
|
* @last_calibration: ktime_t that the last calibration started
|
|
* @calibration_range_min: absolute minimum rate of the DFLL calibration range
|
|
* @calibration_range_max: absolute maximum rate of the DFLL calibration range
|
|
* @cdev: pointer to registered DFLL thermal device (if loaded)
|
|
* @lock: spinlock to protect accesses to DFLL registers and state
|
|
*
|
|
* @dfll_min_microvolt must be greater than or equal to the PMIC
|
|
* regulator's low voltage limit. @dfll_max_microvolt must be lesser
|
|
* than or equal to the PMIC's high voltage limit.
|
|
*/
|
|
struct tegra_dfll {
|
|
void __iomem *base;
|
|
|
|
struct clk *soc_clk;
|
|
struct clk *ref_clk;
|
|
struct clk *i2c_clk;
|
|
struct clk *dfll_clk;
|
|
unsigned long ref_rate;
|
|
|
|
u8 safe_output;
|
|
u8 tune_high_out_start;
|
|
u8 tune_high_out_min;
|
|
u8 minimax_output;
|
|
u8 lut_min;
|
|
u8 lut_max;
|
|
u8 therm_caps_idx;
|
|
u8 therm_caps_num;
|
|
u8 therm_floors_idx;
|
|
u8 therm_floors_num;
|
|
u8 flags;
|
|
u8 vdd_map_size;
|
|
u8 cg_scale;
|
|
struct tegra_dfll_voltage_reg_map *vdd_map;
|
|
struct tegra_dfll_voltage_reg_map *out_map[MAX_DFLL_VOLTAGES];
|
|
struct tegra_dfll_dvfs_info *dvfs_info;
|
|
unsigned long dvco_rate_min;
|
|
|
|
struct tegra_dfll_therm therm_caps[MAX_THERMAL_CAPS];
|
|
struct tegra_dfll_therm therm_floors[MAX_THERMAL_FLOORS];
|
|
struct dfll_rate_req last_req;
|
|
enum tegra_dfll_tune_state tune_state;
|
|
enum tegra_dfll_ctrl_mode mode;
|
|
enum tegra_dfll_ctrl_mode resume_mode;
|
|
|
|
struct timer_list tune_timer;
|
|
unsigned long tune_delay;
|
|
|
|
u32 tune0_low_voltage_range;
|
|
u32 tune0_high_voltage_range;
|
|
u32 tune1;
|
|
u32 droop_rate_min;
|
|
u32 tune_high_min_mv;
|
|
|
|
struct dentry *dent;
|
|
|
|
u32 sample_rate;
|
|
u32 force_mode;
|
|
u32 cf;
|
|
u32 ci;
|
|
u32 cg;
|
|
u32 droop_cut_value;
|
|
u32 droop_restore_ramp;
|
|
unsigned long out_rate_min;
|
|
u32 scale_out_ramp;
|
|
|
|
u32 min_millivolts;
|
|
u32 cvb_max_millivolts;
|
|
|
|
int cvb_speedo_scale; /* must be signed */
|
|
int cvb_voltage_scale; /* must be signed */
|
|
|
|
int speedo_id;
|
|
int process_id;
|
|
|
|
int speedo_value;
|
|
|
|
u32 pmic_i2c_addr;
|
|
u32 pmic_i2c_voltage_reg;
|
|
u32 pmic_i2c_fs_rate;
|
|
u32 pmic_i2c_hs_rate;
|
|
u32 pmic_i2c_hs_master_code;
|
|
u32 pmic_i2c_ten_bit_addrs;
|
|
|
|
struct regulator *vdd;
|
|
int vdd_step; /* must be signed */
|
|
|
|
u32 dfll_min_microvolt;
|
|
u32 dfll_max_microvolt;
|
|
|
|
struct timer_list calibration_timer;
|
|
unsigned long calibration_delay;
|
|
ktime_t last_calibration;
|
|
unsigned long calibration_range_min;
|
|
unsigned long calibration_range_max;
|
|
|
|
struct tegra_dfll_clk_hw dfll_clk_hw;
|
|
struct thermal_cooling_device *cdev_floor;
|
|
struct thermal_cooling_device *cdev_cap;
|
|
spinlock_t lock; /* see kerneldoc above */
|
|
};
|
|
|
|
/*
|
|
* Conversion macros (different scales for frequency request, and
|
|
* monitored rate is not a typo)
|
|
*/
|
|
#define RATE_STEP(td) ((td)->ref_rate / 2)
|
|
#define GET_REQUEST_FREQ(rate, ref_rate) ((rate) / ((ref_rate) / 2))
|
|
#define GET_REQUEST_RATE(freq, ref_rate) ((freq) * ((ref_rate) / 2))
|
|
#define GET_MONITORED_RATE(freq, ref_rate) ((freq) * ((ref_rate) / 4))
|
|
#define GET_DROOP_FREQ(rate, ref_rate) ((rate) / ((ref_rate) / 4))
|
|
#define ROUND_MIN_RATE(rate, ref_rate) \
|
|
(DIV_ROUND_UP(rate, (ref_rate) / 2) * ((ref_rate) / 2))
|
|
#define GET_DIV(ref_rate, out_rate, scale) \
|
|
DIV_ROUND_UP((ref_rate), (out_rate) * (scale))
|
|
|
|
/* fcpu_dfll_pdev: the DFLL platform device instance */
|
|
static struct platform_device *fcpu_dfll_pdev;
|
|
|
|
/* mode_name: map numeric DFLL modes to names for friendly console messages */
|
|
static const char * const mode_name[] = {
|
|
[TEGRA_DFLL_UNINITIALIZED] = "uninitialized",
|
|
[TEGRA_DFLL_DISABLED] = "disabled",
|
|
[TEGRA_DFLL_OPEN_LOOP] = "open_loop",
|
|
[TEGRA_DFLL_CLOSED_LOOP] = "closed_loop",
|
|
};
|
|
|
|
/* Static functions */
|
|
|
|
static inline u32 dfll_readl(struct tegra_dfll *td, u32 offs)
|
|
{
|
|
return __raw_readl(td->base + offs);
|
|
}
|
|
|
|
static inline void dfll_writel(struct tegra_dfll *td, u32 val, u32 offs)
|
|
{
|
|
__raw_writel(val, td->base + offs);
|
|
}
|
|
|
|
/**
|
|
* dfll_wmb - ensure all MMIO writes from the CPU to the DFLL have completed
|
|
* @td: struct tegra_dfll * device context
|
|
*
|
|
* Ensure that all writes from the CPU to the memory-mapped I/O space
|
|
* of the DFLL IP block have completed. Assumes that the CPU that
|
|
* this code is currently running on has excluded other CPUs on the
|
|
* system from accessing the DFLL IP block MMIO space.
|
|
*/
|
|
static inline void dfll_wmb(struct tegra_dfll *td)
|
|
{
|
|
dfll_readl(td, DFLL_CTRL);
|
|
}
|
|
|
|
/**
|
|
* i2c_output_enable - enable generation of voltage adjustment I2C commands
|
|
* @pdev: DFLL instance
|
|
* @val: ptr to the value to write to DFLL_OUTPUT_CFG register
|
|
*
|
|
* Tell the DFLL-I2C interface to start transmitting voltage-set
|
|
* commands to the PMIC. The variable pointed to by @v must be loaded
|
|
* from DFLL_OUTPUT_CFG, or manually set, before calling this code.
|
|
* No return value.
|
|
*/
|
|
static void i2c_output_enable(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
|
|
val = dfll_readl(td, DFLL_OUTPUT_CFG);
|
|
val |= DFLL_OUTPUT_CFG_I2C_ENABLE;
|
|
dfll_writel(td, val, DFLL_OUTPUT_CFG);
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* i2c_output_disable - stop generating voltage adjustment I2C commands
|
|
* @pdev: DFLL instance
|
|
* @val: ptr to the value of the DFLL_OUTPUT_CFG register
|
|
*
|
|
* Tell the DFLL-I2C interface to stop transmitting voltage-set
|
|
* commands to the PMIC. The variable pointed to by @v must be loaded
|
|
* from DFLL_OUTPUT_CFG, or manually set, before calling this code.
|
|
* Most code should not call this directly - instead call
|
|
* i2c_output_disable_flush(). No return value.
|
|
*/
|
|
static void i2c_output_disable(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
|
|
val = dfll_readl(td, DFLL_OUTPUT_CFG);
|
|
val &= ~DFLL_OUTPUT_CFG_I2C_ENABLE;
|
|
dfll_writel(td, val, DFLL_OUTPUT_CFG);
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* is_output_i2c_req_pending - is an I2C voltage-set command in progress?
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Returns 1 if an I2C request is in progress, or 0 if not. The DFLL
|
|
* IP block requires two back-to-back reads of the I2C_REQ_PENDING
|
|
* field to return 0 before the software can be sure that no I2C
|
|
* request is currently pending. Also, a minimum time interval
|
|
* between DFLL_I2C_STS reads is required by the IP block.
|
|
*/
|
|
static int is_output_i2c_req_pending(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 sts;
|
|
|
|
sts = dfll_readl(td, DFLL_I2C_STS);
|
|
if (sts & DFLL_I2C_STS_I2C_REQ_PENDING)
|
|
return 1;
|
|
|
|
udelay(I2C_OUTPUT_ACTIVE_TEST_US);
|
|
|
|
sts = dfll_readl(td, DFLL_I2C_STS);
|
|
if (sts & DFLL_I2C_STS_I2C_REQ_PENDING)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* i2c_output_disable_flush - disable I2C output and wait for I2C command to finish
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Prevent the DFLL I2C controller from sending any further
|
|
* voltage-set commands, then wait for any in-progress commands to
|
|
* complete. This is the normal (non-workaround) path. Returns 0 if
|
|
* the flush completed within the timeout interval, or -ETIMEDOUT
|
|
* otherwise.
|
|
*/
|
|
static int i2c_output_disable_flush(struct platform_device *pdev)
|
|
{
|
|
int i, t;
|
|
|
|
t = DFLL_OUTPUT_PENDING_TIMEOUT / I2C_OUTPUT_ACTIVE_TEST_US;
|
|
|
|
i2c_output_disable(pdev);
|
|
|
|
for (i = 0; i < t; i++) {
|
|
if (!is_output_i2c_req_pending(pdev))
|
|
return 0;
|
|
|
|
udelay(I2C_OUTPUT_ACTIVE_TEST_US);
|
|
}
|
|
|
|
/* I2C request is still pending - report error */
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
/**
|
|
* set_mode - change the DFLL control mode
|
|
* @pdev: DFLL instance
|
|
* @mode: DFLL control mode (see enum tegra_dfll_ctrl_mode)
|
|
*
|
|
* Change the DFLL's operating mode from open-loop mode to closed-loop
|
|
* mode, or vice versa. No return value.
|
|
*/
|
|
static void set_mode(struct platform_device *pdev,
|
|
enum tegra_dfll_ctrl_mode mode)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
td->mode = mode;
|
|
dfll_writel(td, mode - 1, DFLL_CTRL);
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* get_output_cap - return LUT index of the maximum non-DFLL safe voltage
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Return the LUT index of the maximum non-DFLL safe voltage, given
|
|
* the current temperature of the SoC.
|
|
*/
|
|
static u8 get_output_cap(struct platform_device *pdev,
|
|
struct dfll_rate_req *req)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 thermal_cap = td->dvfs_info->num_voltages - 1;
|
|
|
|
if (td->therm_caps_idx && (td->therm_caps_idx <= td->therm_caps_num))
|
|
thermal_cap = td->therm_caps[td->therm_caps_idx - 1].output;
|
|
if (req && (req->cap < thermal_cap))
|
|
return req->cap;
|
|
return thermal_cap;
|
|
}
|
|
|
|
/**
|
|
* get_output_min - return LUT index of the minimum non-DFLL safe voltage
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Return the LUT index of the minimum non-DFLL safe voltage, given
|
|
* the DFLL's current tuning state and the current temperature of the
|
|
* SoC.
|
|
*/
|
|
static u8 get_output_min(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 tune_min, thermal_min;
|
|
|
|
tune_min = (td->tune_state == TEGRA_DFLL_TUNE_LOW) ?
|
|
0 : td->tune_high_out_min;
|
|
|
|
thermal_min = 0;
|
|
if (td->therm_floors_idx < td->therm_floors_num)
|
|
thermal_min = td->therm_floors[td->therm_floors_idx].output;
|
|
|
|
return max(tune_min, thermal_min);
|
|
}
|
|
|
|
/*
|
|
* Voltage lookup table operations
|
|
*/
|
|
|
|
/**
|
|
* _load_lut - load voltage lookup table into DFLL RAM
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Load the voltage-to-PMIC register value lookup table into the DFLL
|
|
* IP block LUT memory. td->lut_min and td->lut_max are used to cap
|
|
* the minimum and maximum voltage requested. This function shouldn't
|
|
* be called directly by code other than dfll_load_lut(), since this
|
|
* function doesn't handle the necessary pre- and post-requisites. No
|
|
* return value.
|
|
*/
|
|
static void _load_lut(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
u32 val;
|
|
|
|
val = td->out_map[td->lut_min]->reg_value;
|
|
for (i = 0; i <= td->lut_min; i++)
|
|
dfll_writel(td, val, DFLL_OUTPUT_LUT + i * 4);
|
|
|
|
for (; i < td->lut_max; i++) {
|
|
val = td->out_map[i]->reg_value;
|
|
dfll_writel(td, val, DFLL_OUTPUT_LUT + i * 4);
|
|
}
|
|
|
|
val = td->out_map[td->lut_max]->reg_value;
|
|
for (; i < td->dvfs_info->num_voltages; i++)
|
|
dfll_writel(td, val, DFLL_OUTPUT_LUT + i * 4);
|
|
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* dfll_load_lut - load the voltage lookup table, disabling output if needed
|
|
* @td: struct tegra_dfll *
|
|
*
|
|
* Load the voltage-to-PMIC register value lookup table into the DFLL
|
|
* IP block memory, disabling and re-enabling the I2C output if
|
|
* needed. Look-up tables can be loaded at any time. No return
|
|
* value.
|
|
*/
|
|
static void dfll_load_lut(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val = dfll_readl(td, DFLL_OUTPUT_CFG);
|
|
int output_quiet_before_disable;
|
|
bool disable_out_for_load;
|
|
|
|
output_quiet_before_disable = (td->flags &
|
|
TEGRA_DFLL_FLAGS_I2C_FORCE_QUIET);
|
|
|
|
/*
|
|
* If the I2C output can be disabled at any time, and it's currently
|
|
* enabled, then disable it.
|
|
*/
|
|
disable_out_for_load = !output_quiet_before_disable &&
|
|
(val & DFLL_OUTPUT_CFG_I2C_ENABLE);
|
|
if (disable_out_for_load) {
|
|
i2c_output_disable(pdev);
|
|
udelay(I2C_OUTPUT_ACTIVE_TEST_US);
|
|
}
|
|
|
|
_load_lut(pdev);
|
|
|
|
/* Re-enable the I2C output if we disabled it previously. */
|
|
if (disable_out_for_load)
|
|
i2c_output_enable(pdev);
|
|
}
|
|
|
|
/**
|
|
* set_tune_state - set the internal tune state variable and log some debug
|
|
* @pdev: DFLL instance
|
|
* @state: tune_state to change the internal state variable to
|
|
*
|
|
* Set the internal tune_state variable to @state, and optionally output some
|
|
* debug. Does not affect the hardware. No return value.
|
|
*/
|
|
static void set_tune_state(struct platform_device *pdev,
|
|
enum tegra_dfll_tune_state state)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
td->tune_state = state;
|
|
pr_debug("%s: set tune state %d\n", __func__, state);
|
|
}
|
|
|
|
/**
|
|
* tune_low - fine-tune DFLL and CPU clock shaper for low voltages
|
|
* @pdev: DFLL instance
|
|
* @state: tune_state to change the internal state variable to
|
|
*
|
|
* Fine-tune the DFLL oscillator parameters and the CPU clock shaper for
|
|
* the low-voltage range. The top end of the low-voltage range is
|
|
* represented by the index (td->tune_high_out_start - 1). No return value.
|
|
*/
|
|
static void tune_low(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
dfll_writel(td, td->tune0_low_voltage_range, DFLL_TUNE0);
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* tune_high - fine-tune DFLL and CPU clock shaper for high voltages
|
|
* @pdev: DFLL instance
|
|
* @state: tune_state to change the internal state variable to
|
|
*
|
|
* Fine-tune the DFLL oscillator parameters and the CPU clock shaper
|
|
* for the high-voltage range. The bottom end of the high-voltage
|
|
* range is represented by the index td->tune_high_out_start.
|
|
* Used in closed-loop mode. No return value.
|
|
*/
|
|
static void tune_high(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
dfll_writel(td, td->tune0_high_voltage_range, DFLL_TUNE0);
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* tune_to_low_state - switch to the minimum safe non-DFLL voltage
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Set the DFLL's voltage output floor to the minimum safe non-DFLL
|
|
* voltage, and tune the DFLL for the low-voltage range. Used before
|
|
* switching to open-loop mode. This prevents the DFLL from entering
|
|
* open-loop mode with an output voltage below the minimum safe
|
|
* non-DFLL voltage.
|
|
*/
|
|
static void tune_to_low_state(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 out_min;
|
|
|
|
set_tune_state(pdev, TEGRA_DFLL_TUNE_LOW);
|
|
tune_low(pdev);
|
|
|
|
out_min = get_output_min(pdev);
|
|
if (td->lut_min != out_min) {
|
|
td->lut_min = out_min;
|
|
dfll_load_lut(pdev);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* set_ol_config - prepare to switch to open-loop mode
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Prepare to switch the DFLL to open-loop mode. This involves ensuring
|
|
* that the DFLL is in the low-voltage tuning regime, and disabling
|
|
* output frequency scaling. No return value.
|
|
*/
|
|
static void set_ol_config(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
|
|
/* always tune low (safe) in open loop */
|
|
if (td->tune_state != TEGRA_DFLL_TUNE_LOW)
|
|
tune_to_low_state(pdev);
|
|
|
|
/* 1:1 scaling in open loop */
|
|
val = dfll_readl(td, DFLL_FREQ_REQ);
|
|
val |= (SCALE_MAX - 1) << DFLL_FREQ_REQ_SCALE_SHIFT;
|
|
val &= ~DFLL_FREQ_REQ_FORCE_ENABLE;
|
|
dfll_writel(td, val, DFLL_FREQ_REQ);
|
|
}
|
|
|
|
/**
|
|
* set_cl_config - prepare to switch to closed-loop mode
|
|
* @pdev: DFLL instance
|
|
* @req: requested output rate
|
|
*
|
|
* Prepare to switch the DFLL to closed-loop mode. This involves
|
|
* switching the DFLL's tuning voltage regime (if necessary), and
|
|
* rewriting the LUT to restrict the minimum and maximum voltages. No
|
|
* return value.
|
|
*/
|
|
static void set_cl_config(struct platform_device *pdev,
|
|
struct dfll_rate_req *req)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 out_max, out_min;
|
|
u8 out_cap = get_output_cap(pdev, req);
|
|
|
|
switch (td->tune_state) {
|
|
case TEGRA_DFLL_TUNE_LOW:
|
|
if (out_cap > td->tune_high_out_start) {
|
|
set_tune_state(pdev, TEGRA_DFLL_TUNE_WAIT_DFLL);
|
|
mod_timer(&td->tune_timer, jiffies + td->tune_delay);
|
|
}
|
|
break;
|
|
|
|
case TEGRA_DFLL_TUNE_HIGH:
|
|
case TEGRA_DFLL_TUNE_WAIT_DFLL:
|
|
case TEGRA_DFLL_TUNE_WAIT_PMIC:
|
|
if (out_cap <= td->tune_high_out_start) {
|
|
set_tune_state(pdev, TEGRA_DFLL_TUNE_LOW);
|
|
tune_low(pdev);
|
|
}
|
|
break;
|
|
default:
|
|
BUG();
|
|
}
|
|
|
|
out_min = get_output_min(pdev);
|
|
if (out_cap > out_min + 1)
|
|
req->output = out_cap - 1;
|
|
else
|
|
req->output = out_min + 1;
|
|
if (req->output == td->safe_output)
|
|
req->output++;
|
|
out_max = max((u8)(req->output + 1), td->minimax_output);
|
|
|
|
if ((td->lut_min != out_min) || (td->lut_max != out_max)) {
|
|
td->lut_min = out_min;
|
|
td->lut_max = out_max;
|
|
dfll_load_lut(pdev);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* tune_timer_cb - timer callback during TUNE_HIGH_REQUEST
|
|
* @data: struct platform_device * of the DFLL instance
|
|
*
|
|
* Timer callback, used when switching from TUNE_LOW to TUNE_HIGH in
|
|
* closed-loop mode. Waits for DFLL I2C voltage command output to
|
|
* reach tune_high_out_min, then waits for the PMIC to react to the
|
|
* command. No return value.
|
|
*/
|
|
static void tune_timer_cb(unsigned long data)
|
|
{
|
|
struct platform_device *pdev = (struct platform_device *)data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val, out_min, out_last;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (td->tune_state == TEGRA_DFLL_TUNE_WAIT_DFLL) {
|
|
out_min = td->lut_min;
|
|
|
|
val = dfll_readl(td, DFLL_I2C_STS);
|
|
out_last = (val >> DFLL_I2C_STS_I2C_LAST_SHIFT) & OUT_MASK;
|
|
|
|
if (!is_output_i2c_req_pending(pdev) &&
|
|
(out_last >= td->tune_high_out_min) &&
|
|
(out_min >= td->tune_high_out_min)) {
|
|
set_tune_state(pdev, TEGRA_DFLL_TUNE_WAIT_PMIC);
|
|
mod_timer(&td->tune_timer, jiffies +
|
|
usecs_to_jiffies(DFLL_OUTPUT_RAMP_DELAY));
|
|
} else {
|
|
mod_timer(&td->tune_timer, jiffies + td->tune_delay);
|
|
}
|
|
} else if (td->tune_state == TEGRA_DFLL_TUNE_WAIT_PMIC) {
|
|
set_tune_state(pdev, TEGRA_DFLL_TUNE_HIGH);
|
|
tune_high(pdev);
|
|
}
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_calibrate - recalibrate dvco_rate_min to match reality
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Adjust our estimate of the DVCO minimum rate based on the measured
|
|
* rate. This is only done if the DFLL is in closed loop mode, the
|
|
* last request engaged the clock skipper, a minimum amount of time
|
|
* has passed since the last calibration attempt, and there is no I2C
|
|
* transaction pending. No return value.
|
|
*/
|
|
static void tegra_dfll_calibrate(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
ktime_t now;
|
|
unsigned long data;
|
|
u8 out_min;
|
|
|
|
if ((td->mode != TEGRA_DFLL_CLOSED_LOOP) ||
|
|
(td->last_req.rate > td->dvco_rate_min))
|
|
return;
|
|
|
|
now = ktime_get();
|
|
if (ktime_us_delta(now, td->last_calibration) < DFLL_CALIBR_TIME)
|
|
return;
|
|
|
|
out_min = get_output_min(pdev);
|
|
|
|
/* Skip calibration this time if I2C transaction is pending */
|
|
val = dfll_readl(td, DFLL_I2C_STS);
|
|
if (is_output_i2c_req_pending(pdev)) {
|
|
mod_timer(&td->calibration_timer,
|
|
jiffies + td->calibration_delay);
|
|
return;
|
|
}
|
|
|
|
/* XXX Cache DFLL_MONITOR_CTRL to avoid an unnecessary DFLL read */
|
|
if (dfll_readl(td, DFLL_MONITOR_CTRL) != DFLL_MONITOR_CTRL_FREQ)
|
|
dfll_writel(td, DFLL_MONITOR_CTRL_FREQ, DFLL_MONITOR_CTRL);
|
|
|
|
/* Synchronize with sample period, and get rate measurements */
|
|
data = dfll_readl(td, DFLL_MONITOR_DATA);
|
|
do {
|
|
data = dfll_readl(td, DFLL_MONITOR_DATA);
|
|
} while (!(data & DFLL_MONITOR_DATA_NEW_MASK));
|
|
do {
|
|
data = dfll_readl(td, DFLL_MONITOR_DATA);
|
|
} while (!(data & DFLL_MONITOR_DATA_NEW_MASK));
|
|
|
|
td->last_calibration = now;
|
|
|
|
/* Adjust minimum rate */
|
|
data &= DFLL_MONITOR_DATA_VAL_MASK;
|
|
data = GET_MONITORED_RATE(data, td->ref_rate);
|
|
if ((val > out_min) || (data < (td->dvco_rate_min - RATE_STEP(td))))
|
|
td->dvco_rate_min -= RATE_STEP(td);
|
|
else if (data > (td->dvco_rate_min + RATE_STEP(td)))
|
|
td->dvco_rate_min += RATE_STEP(td);
|
|
else
|
|
return;
|
|
|
|
td->dvco_rate_min = clamp(td->dvco_rate_min, td->calibration_range_min,
|
|
td->calibration_range_max);
|
|
mod_timer(&td->calibration_timer, jiffies + td->calibration_delay);
|
|
dev_dbg(&pdev->dev, "calibrated dvco_rate_min %lu\n",
|
|
td->dvco_rate_min);
|
|
}
|
|
|
|
/**
|
|
* calibration_timer_cb - calibrate the DFLL; called from a timer
|
|
* @data: struct platform_device * of the DFLL instance
|
|
*
|
|
* Take the DFLL lock and run the calibration process. Called as a
|
|
* timer callback. No return value.
|
|
*/
|
|
static void calibration_timer_cb(unsigned long data)
|
|
{
|
|
struct platform_device *pdev = (struct platform_device *)data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
tegra_dfll_calibrate(pdev);
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
}
|
|
|
|
/**
|
|
* set_request - ask the DFLL to tune to a different frequency
|
|
* @pdev: DFLL instance
|
|
* @req: target frequency request
|
|
*
|
|
* Tell the DFLL to try to change its output frequency to the
|
|
* frequency represented by @req. DFLL must be in closed-loop mode.
|
|
* No return value.
|
|
*/
|
|
static void set_request(struct platform_device *pdev, struct dfll_rate_req *req)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
int force_val = req->output - td->safe_output;
|
|
int coef = 128; /* FIXME: td->cg_scale? */;
|
|
|
|
force_val = force_val * coef / td->cg;
|
|
force_val = clamp(force_val, FORCE_MIN, FORCE_MAX);
|
|
|
|
val = req->freq << DFLL_FREQ_REQ_FREQ_SHIFT;
|
|
val |= req->scale << DFLL_FREQ_REQ_SCALE_SHIFT;
|
|
val |= ((u32)force_val << DFLL_FREQ_REQ_FORCE_SHIFT) &
|
|
DFLL_FREQ_REQ_FORCE_MASK;
|
|
val |= DFLL_FREQ_REQ_FREQ_VALID | DFLL_FREQ_REQ_FORCE_ENABLE;
|
|
|
|
dfll_writel(td, val, DFLL_FREQ_REQ);
|
|
dfll_wmb(td);
|
|
}
|
|
|
|
/**
|
|
* find_mv_out_cap - find the out_map index with voltage >= @mv
|
|
* @pdev: DFLL instance
|
|
* @mv: millivolts
|
|
*
|
|
* Find the out_map index with voltage greater than or equal to @mv,
|
|
* and return it. If all of the voltages in out_map are less than
|
|
* @mv, then return the out_map index * corresponding to the highest
|
|
* possible voltage, even though it's less than @mv.
|
|
*/
|
|
static u8 find_mv_out_cap(struct platform_device *pdev, int mv)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u8 cap;
|
|
|
|
for (cap = 0; cap < td->dvfs_info->num_voltages; cap++)
|
|
if (td->out_map[cap]->reg_uv >= mv * 1000)
|
|
return cap;
|
|
|
|
return cap - 1; /* maximum possible output */
|
|
}
|
|
|
|
/**
|
|
* find_mv_out_floor - find the largest out_map index with voltage < @mv
|
|
* @pdev: DFLL instance
|
|
* @mv: millivolts
|
|
*
|
|
* Find the largest out_map index with voltage lesser to @mv,
|
|
* and return it. If all of the voltages in out_map are greater than
|
|
* @mv, then return the out_map index * corresponding to the minimum
|
|
* possible voltage, even though it's greater than @mv.
|
|
*/
|
|
static u8 find_mv_out_floor(struct platform_device *pdev, int mv)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u8 floor;
|
|
|
|
for (floor = 0; floor < td->dvfs_info->num_voltages; floor++) {
|
|
if (td->out_map[floor]->reg_uv > mv * 1000) {
|
|
if (!floor)
|
|
/* minimum possible output */
|
|
return 0;
|
|
break;
|
|
}
|
|
}
|
|
return floor - 1;
|
|
}
|
|
|
|
/**
|
|
* find_safe_output - find minimum safe voltage for a desired clock frequency
|
|
* @pdev: DFLL instance
|
|
* @rate: desired clock frequency
|
|
* @safe_output: ptr to a variable to return the minimum safe voltage index
|
|
*
|
|
* For a desired clock frequency @rate, finds the voltage @safe_output
|
|
* that is unconditionally safe to run at for any PVT point. There is
|
|
* guaranteed to be some voltage below @safe_output that the DFLL can
|
|
* adjust down to. Returns 0 if found, or -ENOENT if no safe voltage
|
|
* could be found for @rate.
|
|
*/
|
|
static int find_safe_output(struct platform_device *pdev, unsigned long rate,
|
|
u8 *safe_output)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
|
|
for (i = 0; i < td->dvfs_info->num_freqs; i++) {
|
|
if (td->dvfs_info->freqs[i] >= rate) {
|
|
*safe_output = td->dvfs_info->clk_dvfs_map[i];
|
|
return 0;
|
|
}
|
|
}
|
|
return -ENOENT;
|
|
}
|
|
|
|
/**
|
|
* find_dvco_rate_min - find min output freq for an output voltage LUT index
|
|
* @pdev: DFLL instance
|
|
* @out_min: an output voltage LUT index to find the minimum frequency for
|
|
*
|
|
* Given an output voltage LUT index @out_min, return the "minimum" clock
|
|
* frequency that the DVCO will generate (based on the CVB table data).
|
|
* Note that this frequency may not really be a strict minimum, due to
|
|
* calibration/characterization errors.
|
|
*/
|
|
static unsigned long find_dvco_rate_min(struct platform_device *pdev,
|
|
u8 out_min)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
|
|
for (i = 0; i < td->dvfs_info->num_freqs; i++)
|
|
if (td->dvfs_info->clk_dvfs_map[i] > out_min)
|
|
break;
|
|
|
|
i = i ? i-1 : 0;
|
|
return td->dvfs_info->freqs[i];
|
|
}
|
|
|
|
/**
|
|
* set_dvco_rate_min - set the minimum DVCO output rate & interval
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Find and cache the "minimum" DVCO output frequency, as well as the
|
|
* lower and upper calibration boundaries around this frequency. No
|
|
* return value.
|
|
*/
|
|
static void set_dvco_rate_min(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long rate = td->out_rate_min;
|
|
|
|
if (td->therm_floors_idx < td->therm_floors_num)
|
|
rate = find_dvco_rate_min(
|
|
pdev, td->therm_floors[td->therm_floors_idx].output);
|
|
|
|
/* round minimum rate to request unit (ref_rate/2) boundary */
|
|
td->dvco_rate_min = ROUND_MIN_RATE(rate, td->ref_rate);
|
|
|
|
/* dvco min rate is under-estimated - skewed range up */
|
|
td->calibration_range_min = td->dvco_rate_min - 2 * RATE_STEP(td);
|
|
td->calibration_range_max = td->dvco_rate_min + 8 * RATE_STEP(td);
|
|
}
|
|
|
|
/**
|
|
* get_cvb_voltage - returns the CVB voltage based on frequency & silicon
|
|
* @pdev: DFLL instance
|
|
* @c0: calibration data 0 (see below)
|
|
* @c1: calibration data 1 (see below)
|
|
* @c2: calibration data 2 (see below)
|
|
*
|
|
* Return the CVB voltage (in millivolts) for a particular set of
|
|
* frequency and silicon parameters. The formula used to compute the
|
|
* voltage is:
|
|
*
|
|
* cvb_mv = ((c2 * speedo / s_scale + c1) * speedo / s_scale + c0) / v_scale
|
|
*/
|
|
static int get_cvb_voltage(struct platform_device *pdev, int c0, int c1,
|
|
int c2)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
/* apply only speedo scale: output mv = cvb_mv * v_scale */
|
|
int mv;
|
|
|
|
/* combined: apply voltage scale and round to cvb alignment step */
|
|
mv = DIV_ROUND_CLOSEST(c2 * td->speedo_value, td->cvb_speedo_scale);
|
|
mv = DIV_ROUND_CLOSEST((mv + c1) * td->speedo_value,
|
|
td->cvb_speedo_scale) + c0;
|
|
|
|
return DIV_ROUND_UP(mv * 1000,
|
|
td->cvb_voltage_scale * td->vdd_step) *
|
|
td->vdd_step / 1000;
|
|
}
|
|
|
|
/**
|
|
* find_vdd_map_entry_exact - find vdd_map entry with voltage equal to @mv
|
|
* @pdev: DFLL instance
|
|
* @mv: millivolts to look up in vdd_map[]
|
|
*
|
|
* Attempt to find the entry in the vdd_map with the voltage is equal
|
|
* to @mv. Returns a pointer to the matching vdd_map entry if found;
|
|
* otherwise, returns NULL.
|
|
*/
|
|
static struct tegra_dfll_voltage_reg_map *find_vdd_map_entry_exact(
|
|
struct platform_device *pdev, int mv)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
|
|
for (i = 0; i < td->vdd_map_size; i++)
|
|
if (mv == td->vdd_map[i].reg_uv / 1000)
|
|
return &td->vdd_map[i];
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* find_vdd_map_entry_min - find first vdd_map entry with voltage >= @mv
|
|
* @pdev: DFLL instance
|
|
* @mv: millivolts to look up in vdd_map[]
|
|
*
|
|
* Attempt to find the lowest-voltage entry in the vdd_map with a
|
|
* voltage equal to or greater than @mv. Returns a pointer to the
|
|
* matching vdd_map entry if found; otherwise, returns NULL.
|
|
*/
|
|
static struct tegra_dfll_voltage_reg_map *find_vdd_map_entry_min(
|
|
struct platform_device *pdev, int mv)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
|
|
for (i = 0; i < td->vdd_map_size; i++)
|
|
if (mv <= td->vdd_map[i].reg_uv / 1000)
|
|
return &td->vdd_map[i];
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* prepare_volt_freq_table - analyze the CVB table and build indexes
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Loop through the CVB table data, generating the starting output
|
|
* voltage for each frequency point. (Each CVB entry specifies CPU
|
|
* frequency and CVB coefficients to calculate the respective voltage
|
|
* when DFLL is used as the CPU clock source.)
|
|
*
|
|
* Minimum voltage limit is applied only to DFLL source. Maximum
|
|
* voltage limit is applied by directly clipping voltage for DFLL.
|
|
*
|
|
* Returns 0 upon success, or -EINVAL if the CVB table or DFLL configuration
|
|
* data is invalid.
|
|
*/
|
|
static int prepare_volt_freq_table(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct tegra_dfll_cvb *tdc;
|
|
u32 dfll_mv;
|
|
int i;
|
|
unsigned long fmax_at_vmin = 0;
|
|
|
|
for (i = 0; i < td->dvfs_info->cvb_table_len; i++) {
|
|
tdc = &td->dvfs_info->cvb_table[i];
|
|
|
|
dfll_mv = get_cvb_voltage(pdev, tdc->c0, tdc->c1, tdc->c2);
|
|
|
|
/* Check maximum frequency at minimum voltage */
|
|
if (dfll_mv > td->min_millivolts) {
|
|
if (!i)
|
|
break; /* 1st entry already above Vmin */
|
|
if (!fmax_at_vmin)
|
|
fmax_at_vmin = td->dvfs_info->freqs[i - 1];
|
|
}
|
|
|
|
dfll_mv = max(dfll_mv, td->min_millivolts);
|
|
|
|
td->dvfs_info->freqs[i] = tdc->freq;
|
|
td->dvfs_info->cpu_dfll_millivolts[i] = min(dfll_mv,
|
|
td->cvb_max_millivolts);
|
|
td->dvfs_info->num_freqs = i + 1;
|
|
|
|
/*
|
|
* "Round-up" frequency list cut-off (keep first entry that
|
|
* exceeds max voltage - the voltage limit will be enforced
|
|
* anyway, so when requested this frequency dfll will settle
|
|
* at whatever high frequency it can on the particular chip)
|
|
*/
|
|
if (dfll_mv > td->cvb_max_millivolts)
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Table must not be empty and must have at least
|
|
* 1. one entry <= Vmin and
|
|
* 2. one entry >= Vmin
|
|
*/
|
|
if (!i || !fmax_at_vmin) {
|
|
dev_err(&pdev->dev, "invalid cpu dvfs table\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->out_rate_min = fmax_at_vmin;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* dfll_init_maps - build output voltage-related maps
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Build the LUT index-to-output PMIC voltage value map (out_map), and
|
|
* the CVB table index-to-LUT index map (clk_dvfs_map). Returns 0
|
|
* upon success or -EINVAL, -ERANGE, or -ENOENT upon error.
|
|
*/
|
|
static int __must_check dfll_init_maps(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i, j, v, v_max, n, r;
|
|
struct tegra_dfll_voltage_reg_map *m;
|
|
|
|
BUILD_BUG_ON(MAX_DFLL_VOLTAGES > OUT_MASK + 1);
|
|
|
|
r = prepare_volt_freq_table(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "unable to prepare voltage table\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
n = td->dvfs_info->num_freqs;
|
|
if (n >= MAX_DFLL_VOLTAGES) {
|
|
dev_err(&pdev->dev, "too many frequencies (%d)\n", n);
|
|
return -EINVAL;
|
|
}
|
|
|
|
v_max = td->dvfs_info->cpu_dfll_millivolts[n - 1];
|
|
|
|
v = td->min_millivolts;
|
|
if (v > td->dvfs_info->cpu_dfll_millivolts[0]) {
|
|
dev_err(&pdev->dev,
|
|
"min voltage %d > lowest voltage in map %d\n",
|
|
td->min_millivolts,
|
|
td->dvfs_info->cpu_dfll_millivolts[0]);
|
|
return -ERANGE;
|
|
}
|
|
|
|
td->out_map[0] = find_vdd_map_entry_exact(pdev, v);
|
|
if (!td->out_map[0]) {
|
|
dev_err(&pdev->dev, "no vdd_map entry for min voltage %d\n", v);
|
|
return -ENOENT;
|
|
}
|
|
|
|
for (i = 0, j = 1; i < n; i++) {
|
|
for (;;) {
|
|
v += max(1, (v_max - v) / (MAX_DFLL_VOLTAGES - j));
|
|
if (v >= td->dvfs_info->cpu_dfll_millivolts[i])
|
|
break;
|
|
|
|
m = find_vdd_map_entry_min(pdev, v);
|
|
if (!m) {
|
|
dev_err(&pdev->dev,
|
|
"no vdd_map entry for map voltage %d\n",
|
|
v);
|
|
return -ENOENT;
|
|
}
|
|
if (m != td->out_map[j - 1])
|
|
td->out_map[j++] = m;
|
|
}
|
|
|
|
v = td->dvfs_info->cpu_dfll_millivolts[i];
|
|
m = find_vdd_map_entry_exact(pdev, v);
|
|
if (!m) {
|
|
dev_err(&pdev->dev,
|
|
"no exact vdd_map entry for voltage %d\n", v);
|
|
return -ENOENT;
|
|
}
|
|
if (m != td->out_map[j - 1])
|
|
td->out_map[j++] = m;
|
|
td->dvfs_info->clk_dvfs_map[i] = j - 1;
|
|
if (j > MAX_DFLL_VOLTAGES) {
|
|
dev_err(&pdev->dev, "too many out_map entries\n");
|
|
return -ENOMEM;
|
|
}
|
|
}
|
|
td->dvfs_info->num_voltages = j;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* dfll_init_tuning_thresholds - set up the high voltage range, if possible
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Determine whether the DFLL tuning parameters need to be
|
|
* reprogrammed when the DFLL voltage reaches a certain minimum
|
|
* threshold. No return value.
|
|
*/
|
|
static void dfll_init_tuning_thresholds(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u8 out_min, out_start, max_voltage_index;
|
|
|
|
max_voltage_index = td->dvfs_info->num_voltages - 1;
|
|
|
|
/*
|
|
* Convert high tuning voltage threshold into output LUT
|
|
* index, and add necessary margin. If voltage threshold is
|
|
* outside operating range set it at maximum output level to
|
|
* effectively disable tuning parameters adjustment.
|
|
*/
|
|
td->tune_high_out_min = max_voltage_index;
|
|
td->tune_high_out_start = max_voltage_index;
|
|
if (td->tune_high_min_mv < td->min_millivolts)
|
|
return; /* no difference between low & high voltage range */
|
|
|
|
out_min = find_mv_out_cap(pdev, td->tune_high_min_mv);
|
|
if ((out_min + 2) > max_voltage_index)
|
|
return;
|
|
|
|
out_start = out_min + DFLL_TUNE_HIGH_MARGIN_STEPS;
|
|
if (out_start > max_voltage_index)
|
|
return;
|
|
|
|
td->tune_high_out_min = out_min;
|
|
td->tune_high_out_start = out_start;
|
|
if (td->minimax_output <= out_min)
|
|
td->minimax_output = out_min + 1;
|
|
}
|
|
|
|
/**
|
|
* dfll_init_hot_output_cap - set a thermally-sensitive maximum voltage cap
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization, map the temperature-dependent
|
|
* voltage caps from millivolts to output LUT indexes, and make sure
|
|
* there is room for regulation below the minimum thermal cap. Must
|
|
* be called after the therm-caps property has been parsed from DT.
|
|
* Must be called after td->safe_output is first set by taking
|
|
* td->therm_caps[0] into consideration. The voltage cap must
|
|
* monotonically decrease as temperatures monotonically increase.
|
|
* Returns 0 upon success, -EINVAL if the voltage cap data is in an
|
|
* invalid format, or -ENOSPC if there's no space to adjust the
|
|
* voltage above the minimum.
|
|
*/
|
|
static int __must_check dfll_init_hot_output_cap(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
u8 output;
|
|
|
|
if (td->therm_caps_num == 0)
|
|
return 0;
|
|
|
|
for (i = 0; i < td->therm_caps_num; i++) {
|
|
if (i > 0 &&
|
|
(td->therm_caps[i].mv >= td->therm_caps[i-1].mv ||
|
|
td->therm_caps[i].temp <= td->therm_caps[i-1].temp)) {
|
|
dev_err(&pdev->dev,
|
|
"format error in thermal cap data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->therm_caps[i].output =
|
|
find_mv_out_floor(pdev, td->therm_caps[i].mv);
|
|
}
|
|
|
|
output = td->therm_caps[td->therm_caps_num - 1].output;
|
|
if (output < td->minimax_output) {
|
|
dev_err(&pdev->dev,
|
|
"no space available for regulation below max\n");
|
|
return -ENOSPC;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* dfll_init_cold_output_floor - set a thermally-sensitive minimum voltage floor
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization, map the temperature-dependent
|
|
* voltage floors from millivolts to output LUT indexes, and make sure
|
|
* there is room for regulation above the maximum thermal floor. Must
|
|
* be called after the therm-floors property has been parsed from DT.
|
|
* Must be called before td->safe_output is first set by taking
|
|
* td->therm_floors[0] into consideration. The voltage floor must
|
|
* monotonically decrease as temperatures monotonically increase.
|
|
* Returns 0 upon success, -EINVAL if the voltage floor data is in an
|
|
* invalid format, or -ENOSPC if there's no space to adjust the
|
|
* voltage above the minimum.
|
|
*/
|
|
static int __must_check dfll_init_cold_output_floor(
|
|
struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int i;
|
|
|
|
if (td->therm_floors_num == 0)
|
|
return 0;
|
|
|
|
for (i = 0; i < td->therm_floors_num; i++) {
|
|
if (i > 0 &&
|
|
(td->therm_floors[i].mv >= td->therm_floors[i-1].mv ||
|
|
td->therm_floors[i].temp <= td->therm_floors[i-1].temp)) {
|
|
dev_err(&pdev->dev,
|
|
"format error in thermal floor data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->therm_floors[i].output =
|
|
find_mv_out_cap(pdev, td->therm_floors[i].mv);
|
|
}
|
|
|
|
if (td->therm_floors[0].output + 2 >= td->dvfs_info->num_voltages) {
|
|
dev_err(&pdev->dev,
|
|
"no space available for regulation above min\n");
|
|
return -ENOSPC;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* dfll_init_output_thresholds - set various DFLL min/max voltage thresholds
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization, set the minimum safe voltage
|
|
* output LUT index (safe_output), along with the maximum voltage that
|
|
* the DFLL can request in closed-loop mode (minimax_output). Returns
|
|
* 0 upon success, or passes along the return value from
|
|
* dfll_init_cold_output_floor() upon error.
|
|
*/
|
|
static int __must_check dfll_init_output_thresholds(
|
|
struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int r;
|
|
|
|
td->minimax_output = 0;
|
|
td->tune_state = TEGRA_DFLL_TUNE_LOW;
|
|
|
|
dfll_init_tuning_thresholds(pdev);
|
|
|
|
r = dfll_init_cold_output_floor(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "could not set cold output floor: %d\n", r);
|
|
return r;
|
|
}
|
|
|
|
/* make sure safe output is safe at any temperature */
|
|
td->safe_output = td->therm_floors[0].output ? : 1;
|
|
if (td->minimax_output <= td->safe_output)
|
|
td->minimax_output = td->safe_output + 1;
|
|
|
|
/* init caps after minimax output is determined */
|
|
r = dfll_init_hot_output_cap(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "could not set hot output cap: %d\n", r);
|
|
return r;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* DFLL PMIC I2C controller
|
|
*/
|
|
|
|
/**
|
|
* dfll_init_i2c_clk - initialize clock for the DFLL's internal I2C controller
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization, set up the DFLL's internal I2C
|
|
* controller. Only used if the DFLL is using I2C to communicate with
|
|
* the PMIC (which is the only method currently supported in this
|
|
* driver). td->i2c_clk must be enabled before calling. Returns 0
|
|
* upon success or -ERANGE if the computed I2C divisor is out of
|
|
* range.
|
|
*/
|
|
static int dfll_init_i2c_clk(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
u32 div = 2; /* default hs divisor just in case */
|
|
unsigned long i2c_clk_rate;
|
|
|
|
i2c_clk_rate = clk_get_rate(td->i2c_clk);
|
|
|
|
val = GET_DIV(i2c_clk_rate, td->pmic_i2c_fs_rate,
|
|
DFLL_I2C_CLK_DIVISOR_PREDIV);
|
|
if (val == 0 || val > DFLL_I2C_CLK_DIVISOR_MASK) {
|
|
dev_err(&pdev->dev, "I2C FS clock divisor out of range\n");
|
|
return -ERANGE;
|
|
}
|
|
val = (val - 1) << DFLL_I2C_CLK_DIVISOR_FS_SHIFT;
|
|
if (td->pmic_i2c_hs_rate) {
|
|
div = GET_DIV(i2c_clk_rate, td->pmic_i2c_hs_rate,
|
|
DFLL_I2C_CLK_DIVISOR_HSMODE_PREDIV);
|
|
if (div == 0 || div > DFLL_I2C_CLK_DIVISOR_MASK) {
|
|
dev_err(&pdev->dev,
|
|
"I2C HS clock divisor out of range\n");
|
|
return -ERANGE;
|
|
}
|
|
}
|
|
val |= (div - 1) << DFLL_I2C_CLK_DIVISOR_HS_SHIFT;
|
|
dfll_writel(td, val, DFLL_I2C_CLK_DIVISOR);
|
|
dfll_wmb(td);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* DFLL-to-I2C controller interface
|
|
*/
|
|
|
|
/**
|
|
* dfll_init_i2c_if - set up the DFLL's DFLL-I2C interface
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization, program the DFLL-I2C interfae
|
|
* with the PMU slave address, vdd register offset, and transfer mode.
|
|
* This data is used by the DFLL to automatically construct I2C
|
|
* voltage-set commands, which are then passed to the DFLL's internal
|
|
* I2C controller. No return value.
|
|
*/
|
|
static void dfll_init_i2c_if(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
|
|
val = td->pmic_i2c_addr << DFLL_I2C_CFG_SLAVE_ADDR_SHIFT;
|
|
|
|
if (td->pmic_i2c_ten_bit_addrs)
|
|
val |= DFLL_I2C_CFG_SLAVE_ADDR_10;
|
|
if (td->pmic_i2c_hs_rate) {
|
|
val |= (td->pmic_i2c_hs_master_code <<
|
|
DFLL_I2C_CFG_HS_CODE_SHIFT);
|
|
val |= DFLL_I2C_CFG_PACKET_ENABLE;
|
|
}
|
|
val |= DFLL_I2C_CFG_SIZE_MASK;
|
|
val |= DFLL_I2C_CFG_ARB_ENABLE;
|
|
dfll_writel(td, val, DFLL_I2C_CFG);
|
|
dfll_writel(td, td->pmic_i2c_voltage_reg, DFLL_I2C_VDD_REG_ADDR);
|
|
}
|
|
|
|
/**
|
|
* dfll_init_out_if - prepare DFLL-to-PMIC interface
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization or resume from context loss,
|
|
* disable the I2C command output to the PMIC, set safe voltage and
|
|
* output limits, and disable and clear limit interrupts. No return
|
|
* value.
|
|
*/
|
|
static void dfll_init_out_if(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val, out_min, out_max;
|
|
|
|
/* Necessary for the call to get_output_min() */
|
|
td->tune_state = TEGRA_DFLL_TUNE_LOW;
|
|
td->therm_caps_idx = td->therm_caps_num;
|
|
td->therm_floors_idx = 0;
|
|
set_dvco_rate_min(pdev);
|
|
|
|
/*
|
|
* Allow the entire range of LUT indexes, but limit output
|
|
* voltage in LUT mapping (this "indirect" application of
|
|
* limits is used, because h/w does not support dynamic change
|
|
* of index limits, but dynamic reload of LUT is fine).
|
|
*/
|
|
out_min = 0;
|
|
out_max = td->dvfs_info->num_voltages - 1;
|
|
td->lut_min = get_output_min(pdev);
|
|
td->lut_max = get_output_cap(pdev, NULL);
|
|
|
|
val = (td->safe_output << DFLL_OUTPUT_CFG_SAFE_SHIFT) |
|
|
(out_max << DFLL_OUTPUT_CFG_MAX_SHIFT) |
|
|
(out_min << DFLL_OUTPUT_CFG_MIN_SHIFT);
|
|
dfll_writel(td, val, DFLL_OUTPUT_CFG);
|
|
dfll_wmb(td);
|
|
|
|
dfll_writel(td, 0, DFLL_OUTPUT_FORCE);
|
|
dfll_writel(td, 0, DFLL_INTR_EN);
|
|
dfll_writel(td, DFLL_INTR_MAX_MASK | DFLL_INTR_MIN_MASK, DFLL_INTR_STS);
|
|
|
|
dfll_load_lut(pdev);
|
|
|
|
dfll_init_i2c_if(pdev);
|
|
dfll_init_i2c_clk(pdev);
|
|
}
|
|
|
|
/**
|
|
* dfll_init_cntrl_logic - set up some of the DFLL registers
|
|
* @pdev: DFLL instance
|
|
*
|
|
* During DFLL driver initialization or resume from context loss, set
|
|
* up many of the DFLL IP block registers, including the DFLL mode,
|
|
* control loop parameters, and tuning. Returns 0 upon success or
|
|
* -ERANGE if a value destined for a register bitfield will exceed
|
|
* that bitfield (which indicates a problem with the DT data).
|
|
*/
|
|
static int __must_check dfll_init_cntrl_logic(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
|
|
set_mode(pdev, TEGRA_DFLL_DISABLED);
|
|
|
|
val = GET_DIV(td->ref_rate, td->sample_rate, DFLL_CONFIG_DIV_PRESCALE);
|
|
if (val > DFLL_CONFIG_DIV_MASK) {
|
|
dev_err(&pdev->dev, "sample-rate exceeds bitfield width\n");
|
|
return -ERANGE;
|
|
}
|
|
dfll_writel(td, val, DFLL_CONFIG);
|
|
|
|
val = (td->force_mode << DFLL_PARAMS_FORCE_MODE_SHIFT) |
|
|
(td->cf << DFLL_PARAMS_CF_PARAM_SHIFT) |
|
|
(td->ci << DFLL_PARAMS_CI_PARAM_SHIFT) |
|
|
((u8)td->cg << DFLL_PARAMS_CG_PARAM_SHIFT) |
|
|
(td->cg_scale << DFLL_PARAMS_CG_SCALE_SHIFT);
|
|
dfll_writel(td, val, DFLL_PARAMS);
|
|
|
|
tune_low(pdev);
|
|
dfll_writel(td, td->tune1, DFLL_TUNE1);
|
|
|
|
/* configure droop (skipper 1) and scale (skipper 2) */
|
|
val = GET_DROOP_FREQ(td->droop_rate_min, td->ref_rate);
|
|
val <<= DFLL_DROOP_CTRL_MIN_FREQ_SHIFT;
|
|
if (val > DFLL_DROOP_CTRL_MIN_FREQ_MASK) {
|
|
dev_err(&pdev->dev, "droop-rate-min exceeds bitfield width\n");
|
|
return -ERANGE;
|
|
}
|
|
|
|
val |= (td->droop_cut_value << DFLL_DROOP_CTRL_CUT_SHIFT);
|
|
val |= (td->droop_restore_ramp << DFLL_DROOP_CTRL_RAMP_SHIFT);
|
|
dfll_writel(td, val, DFLL_DROOP_CTRL);
|
|
|
|
td->last_req.cap = 0;
|
|
td->last_req.freq = 0;
|
|
td->last_req.output = 0;
|
|
td->last_req.scale = SCALE_MAX - 1;
|
|
dfll_writel(td, DFLL_FREQ_REQ_SCALE_MASK, DFLL_FREQ_REQ);
|
|
dfll_writel(td, td->scale_out_ramp, DFLL_SCALE_RAMP);
|
|
|
|
/* select frequency for monitoring */
|
|
dfll_writel(td, DFLL_MONITOR_CTRL_FREQ, DFLL_MONITOR_CTRL);
|
|
dfll_wmb(td);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* dfll_enable_clocks - enable all clocks needed by the DFLL
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Enable all clocks needed by the DFLL. Assumes that clk_prepare()
|
|
* has already been called on all the clocks. Assumes that the DFLL
|
|
* communicates with the PMIC via the DFLL I2C interface, which is
|
|
* currently the only option supported by this driver. No return
|
|
* value.
|
|
*/
|
|
static void dfll_enable_clocks(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
clk_enable(td->i2c_clk);
|
|
clk_enable(td->ref_clk);
|
|
clk_enable(td->soc_clk);
|
|
}
|
|
|
|
/**
|
|
* dfll_disable_clocks - disable all clocks needed by the DFLL
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Disable all clocks needed by the DFLL. Assumes that other code
|
|
* will later call clk_unprepare(). Assumes that the DFLL
|
|
* communicates with the PMIC via the DFLL I2C interface, which is
|
|
* currently the only option supported by this driver. No return
|
|
* value.
|
|
*/
|
|
/* XXX: Shouldn't the clock ordering be reversed from the enable? */
|
|
static void dfll_disable_clocks(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
clk_disable(td->i2c_clk);
|
|
clk_disable(td->ref_clk);
|
|
clk_disable(td->soc_clk);
|
|
}
|
|
|
|
/**
|
|
* dfll_init_timers - prepare the TUNE_HIGH and calibration timers
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Fill in the required fields for the TUNE_HIGH and calibration
|
|
* timers. The TUNE_HIGH timer is used in DFLL configurations with
|
|
* both low and high voltage tuning ranges. The timer callback is
|
|
* activated upon a request to switch to the high voltage tuning range
|
|
* - see set_cl_config(). The calibration timer is used to adjust
|
|
* the DVCO minimum rate. No return value.
|
|
*/
|
|
static void dfll_init_timers(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
init_timer(&td->tune_timer);
|
|
td->tune_timer.function = tune_timer_cb;
|
|
td->tune_timer.data = (unsigned long)pdev;
|
|
td->tune_delay = usecs_to_jiffies(DFLL_TUNE_HIGH_DELAY);
|
|
|
|
init_timer_deferrable(&td->calibration_timer);
|
|
td->calibration_timer.function = calibration_timer_cb;
|
|
td->calibration_timer.data = (unsigned long)pdev;
|
|
td->calibration_delay = usecs_to_jiffies(DFLL_CALIBR_TIME);
|
|
}
|
|
|
|
/**
|
|
* dfll_init - Prepare the DFLL IP block for use
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Do everything necessary to prepare the DFLL IP block for use.
|
|
* The DFLL will be left in DISABLED state. Called by
|
|
* tegra_dfll_probe(). Returns 0 upon success, or passes along the
|
|
* error from whatever function returned it.
|
|
*/
|
|
static int dfll_init(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int ret;
|
|
|
|
tegra124_clock_deassert_dfll_dvco_reset();
|
|
|
|
ret = clk_prepare_enable(td->i2c_clk);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "failed to enable i2c_clk\n");
|
|
return ret;
|
|
}
|
|
/* Enable module clocks, release control logic reset */
|
|
ret = clk_prepare_enable(td->ref_clk);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "failed to enable ref_clk\n");
|
|
goto di_err1;
|
|
}
|
|
ret = clk_prepare_enable(td->soc_clk);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "failed to enable soc_clk\n");
|
|
goto di_err2;
|
|
}
|
|
|
|
td->ref_rate = clk_get_rate(td->ref_clk);
|
|
if (td->ref_rate == 0) {
|
|
dev_err(&pdev->dev, "ref_clk rate cannot be 0\n");
|
|
goto di_err3;
|
|
}
|
|
|
|
dfll_init_timers(pdev);
|
|
|
|
ret = dfll_init_maps(pdev);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "could not init the voltage map\n");
|
|
goto di_err3;
|
|
}
|
|
|
|
ret = dfll_init_output_thresholds(pdev);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "could not init the voltage thresholds\n");
|
|
goto di_err3;
|
|
}
|
|
|
|
dfll_init_out_if(pdev);
|
|
|
|
/* Configure control registers in disabled mode */
|
|
ret = dfll_init_cntrl_logic(pdev);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "could not init the control logic\n");
|
|
goto di_err3;
|
|
}
|
|
|
|
spin_lock_init(&td->lock);
|
|
|
|
dfll_disable_clocks(pdev);
|
|
|
|
return 0;
|
|
di_err3:
|
|
clk_disable_unprepare(td->soc_clk);
|
|
di_err2:
|
|
clk_disable_unprepare(td->ref_clk);
|
|
di_err1:
|
|
clk_disable_unprepare(td->i2c_clk);
|
|
|
|
tegra124_clock_assert_dfll_dvco_reset();
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* DFLL enable/disable & open-loop <-> closed-loop transitions
|
|
*/
|
|
|
|
/**
|
|
* tegra_dfll_disable - switch from open-loop mode to disabled mode
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Switch from OPEN_LOOP state to DISABLED state. No return value.
|
|
*/
|
|
static void tegra_dfll_disable(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (td->mode != TEGRA_DFLL_OPEN_LOOP) {
|
|
dev_err(&pdev->dev, "cannot disable DFLL in %s mode\n",
|
|
mode_name[td->mode]);
|
|
goto tdd_exit;
|
|
}
|
|
|
|
set_mode(pdev, TEGRA_DFLL_DISABLED);
|
|
|
|
tdd_exit:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_enable - switch a disabled DFLL to open-loop mode
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Switch from DISABLED state to OPEN_LOOP state. Returns 0 upon success
|
|
* or -EPERM if the DFLL is not currently in open-loop mode.
|
|
*/
|
|
static int tegra_dfll_enable(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long flags;
|
|
int ret = 0;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (td->mode != TEGRA_DFLL_DISABLED) {
|
|
dev_err(&pdev->dev, "cannot enable DFLL in %s mode\n",
|
|
mode_name[td->mode]);
|
|
ret = -EPERM;
|
|
goto tde_exit;
|
|
}
|
|
|
|
set_mode(pdev, TEGRA_DFLL_OPEN_LOOP);
|
|
|
|
tde_exit:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_lock - switch from open-loop to closed-loop mode
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Switch from OPEN_LOOP state to CLOSED_LOOP state. Returns 0 upon success,
|
|
* -EINVAL if the DFLL's target rate hasn't been set yet, or -EPERM if the
|
|
* DFLL is not currently in open-loop mode.
|
|
*/
|
|
static int tegra_dfll_lock(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct dfll_rate_req *req = &td->last_req;
|
|
unsigned long flags;
|
|
int ret = 0;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (td->mode != TEGRA_DFLL_OPEN_LOOP) {
|
|
WARN(1, "DFLL: cannot lock in %s mode\n", mode_name[td->mode]);
|
|
ret = -EPERM;
|
|
goto tdl_exit;
|
|
}
|
|
|
|
if (req->freq == 0) {
|
|
dev_err(&pdev->dev, "cannot lock DFLL at rate 0\n");
|
|
ret = -EINVAL;
|
|
goto tdl_exit;
|
|
}
|
|
|
|
/*
|
|
* Update control logic setting with last rate request; sync
|
|
* output limits with current tuning and thermal state, enable
|
|
* output and switch to closed loop mode.
|
|
*/
|
|
set_cl_config(pdev, req);
|
|
i2c_output_enable(pdev);
|
|
set_mode(pdev, TEGRA_DFLL_CLOSED_LOOP);
|
|
set_request(pdev, req);
|
|
mod_timer(&td->calibration_timer, jiffies + td->calibration_delay);
|
|
|
|
tdl_exit:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_unlock - switch from closed-loop to open-loop mode
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Switch from CLOSED_LOOP state to OPEN_LOOP state. Passes along the
|
|
* return value from i2c_output_disable_flush(), or -EPERM if the
|
|
* DFLL is not currently in closed-loop mode.
|
|
*/
|
|
static int tegra_dfll_unlock(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (td->mode != TEGRA_DFLL_CLOSED_LOOP) {
|
|
WARN(1, "DFLL: cannot unlock in %s mode\n",
|
|
mode_name[td->mode]);
|
|
ret = -EPERM;
|
|
goto tdu_exit;
|
|
}
|
|
|
|
set_ol_config(pdev);
|
|
ret = i2c_output_disable_flush(pdev);
|
|
if (ret)
|
|
dev_warn(&pdev->dev, "I2C pending timeout\n");
|
|
set_mode(pdev, TEGRA_DFLL_OPEN_LOOP);
|
|
|
|
tdu_exit:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Set/get the DFLL's targeted output clock rate
|
|
*/
|
|
|
|
/**
|
|
* reprogram_last_request - reprogram the last frequency request in CL mode
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Reprogram the last frequency request when the DFLL is in
|
|
* closed-loop mode, changing voltage regimes if necessary. No return
|
|
* value.
|
|
*/
|
|
static void reprogram_last_request(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
if (td->mode != TEGRA_DFLL_CLOSED_LOOP)
|
|
return;
|
|
|
|
set_cl_config(pdev, &td->last_req);
|
|
set_request(pdev, &td->last_req);
|
|
}
|
|
|
|
/**
|
|
* calc_req_scale_mult - calculate DFLL parameters for a given rate
|
|
* @pdev: DFLL instance
|
|
* @req: DFLL-rate-request structure record pointer
|
|
* @dvco_rate: ptr to the unsigned long containing the DFLL DVCO rate
|
|
*
|
|
* Populate the DFLL-rate-request record @req fields with the scale
|
|
* and freq parameters, based on the target input rate, based in by
|
|
* the caller in req->rate. Pass back the approximate rate that the
|
|
* DVCO will generate in @dvco_rate. Returns 0 upon success, or
|
|
* -EINVAL if the requested rate in req->rate is too high or low for
|
|
* the DFLL to generate.
|
|
*/
|
|
static int calc_req_scale_mult(struct platform_device *pdev,
|
|
struct dfll_rate_req *req,
|
|
unsigned long *dvco_rate)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
u32 val;
|
|
int scale;
|
|
unsigned long rate;
|
|
|
|
rate = req->rate;
|
|
|
|
/* Determine DFLL output scale */
|
|
req->scale = SCALE_MAX - 1;
|
|
if (rate < td->dvco_rate_min) {
|
|
scale = DIV_ROUND_CLOSEST((rate / 1000 * SCALE_MAX),
|
|
(td->dvco_rate_min / 1000));
|
|
if (!scale) {
|
|
dev_err(&pdev->dev,
|
|
"rate %lu is below scalable range\n",
|
|
rate);
|
|
return -EINVAL;
|
|
}
|
|
req->scale = scale - 1;
|
|
rate = td->dvco_rate_min;
|
|
}
|
|
|
|
/* Convert requested rate into frequency request and scale settings */
|
|
val = GET_REQUEST_FREQ(rate, td->ref_rate);
|
|
if (val > FREQ_MAX) {
|
|
dev_err(&pdev->dev, "rate %lu is above dfll range\n", rate);
|
|
return -EINVAL;
|
|
}
|
|
req->freq = val;
|
|
|
|
*dvco_rate = GET_REQUEST_RATE(val, td->ref_rate);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_request_rate - set the next rate for the DFLL to tune to
|
|
* @pdev: DFLL instance
|
|
* @rate: clock rate to target
|
|
*
|
|
* Convert the requested clock rate @rate into the DFLL control logic
|
|
* settings. In closed-loop mode, update new settings immediately to
|
|
* adjust DFLL output rate accordingly. Otherwise, just save them
|
|
* until the next switch to closed loop. Returns 0 upon success,
|
|
* -EPERM if the DFLL driver has not yet been initialized, or -EINVAL
|
|
* if @rate is outside the DFLL's tunable range or if there is no safe
|
|
* output voltage for @rate.
|
|
*/
|
|
static int tegra_dfll_request_rate(struct platform_device *pdev,
|
|
unsigned long rate)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct dfll_rate_req req;
|
|
unsigned long flags, dvco_rate;
|
|
int r;
|
|
int ret = 0;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
req.rate = rate;
|
|
|
|
if (td->mode == TEGRA_DFLL_UNINITIALIZED) {
|
|
dev_err(&pdev->dev, "cannot set DFLL rate in %s mode\n",
|
|
mode_name[td->mode]);
|
|
ret = -EPERM;
|
|
goto tdrr_exit;
|
|
}
|
|
|
|
/* Calibrate dfll minimum rate */
|
|
tegra_dfll_calibrate(pdev);
|
|
|
|
r = calc_req_scale_mult(pdev, &req, &dvco_rate);
|
|
if (r)
|
|
goto tdrr_exit;
|
|
|
|
/* Find safe voltage for requested rate */
|
|
if (find_safe_output(pdev, dvco_rate, &req.output)) {
|
|
dev_err(&pdev->dev, "failed to find safe output for rate %lu\n",
|
|
dvco_rate);
|
|
ret = -EINVAL;
|
|
goto tdrr_exit;
|
|
}
|
|
req.cap = req.output;
|
|
|
|
/*
|
|
* Save validated request, and in CLOSED_LOOP mode actually update
|
|
* control logic settings; use request output to set maximum voltage
|
|
* limit, but keep one LUT step room above safe voltage
|
|
*/
|
|
td->last_req = req;
|
|
|
|
if (td->mode == TEGRA_DFLL_CLOSED_LOOP)
|
|
reprogram_last_request(pdev);
|
|
|
|
tdrr_exit:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* calc_dfll_request_rate - return the approximate DFLL output clock rate
|
|
* @pdev: DFLL instance
|
|
* @req: ptr to a DFLL-rate-request structure record
|
|
*
|
|
* Return the approximate DFLL output clock rate that @req would
|
|
* generate, after the scaling skipper.
|
|
*/
|
|
static unsigned long calc_dfll_request_rate(struct platform_device *pdev,
|
|
struct dfll_rate_req *req)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long rate;
|
|
|
|
if ((req->scale + 1) < SCALE_MAX)
|
|
rate = (req->rate / 1000) * 1000;
|
|
else
|
|
rate = GET_REQUEST_RATE(req->freq, td->ref_rate);
|
|
|
|
return rate;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_request_get - return the DFLL's target rate from the last request
|
|
* @pdev: DFLL instance
|
|
*
|
|
* If the DFLL is currently in closed-loop mode, return the clock rate
|
|
* that the DFLL is targeting. If the DFLL is currently in open-loop
|
|
* mode or is disabled, return the clock rate that the DFLL will
|
|
* target when it next enters closed-loop mode. The rates come from
|
|
* the driver's cache of the last request, not the hardware. Note
|
|
* that the actual DFLL output rate may not exactly match the targeted rate,
|
|
* and may vary. Returns the rate in Hz.
|
|
*/
|
|
static unsigned long tegra_dfll_request_get(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct dfll_rate_req *req = &td->last_req;
|
|
unsigned long flags;
|
|
u32 rate;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
rate = calc_dfll_request_rate(pdev, req);
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return rate;
|
|
}
|
|
|
|
/*
|
|
* Clock framework integration
|
|
*/
|
|
|
|
static int tegra_dfll_clk_is_enabled(struct clk_hw *hw)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct tegra_dfll *td = dev_get_drvdata(&tdc->pdev->dev);
|
|
|
|
return (td->mode == TEGRA_DFLL_OPEN_LOOP ||
|
|
td->mode == TEGRA_DFLL_CLOSED_LOOP);
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_clk_prepare - enable the DFLL clocks
|
|
* @hw: DFLL instance struct clk_hw *
|
|
*
|
|
* Enable the DFLL clocks. XXX This is a temporary workaround until
|
|
* the clock framework gets multiple-parent or re-entrancy support.
|
|
*/
|
|
static int tegra_dfll_clk_prepare(struct clk_hw *hw)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
|
|
dfll_enable_clocks(pdev);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_clk_unprepare - disable the DFLL clocks
|
|
* @hw: DFLL instance struct clk_hw *
|
|
*
|
|
* Disable the DFLL clocks. XXX This is a temporary workaround until
|
|
* the clock framework gets multiple-parent or re-entrancy support.
|
|
*/
|
|
static void tegra_dfll_clk_unprepare(struct clk_hw *hw)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
|
|
/* XXX Validate the current state of the DFLL first */
|
|
|
|
dfll_disable_clocks(pdev);
|
|
}
|
|
|
|
static int tegra_dfll_clk_enable(struct clk_hw *hw)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
|
|
/* XXX Validate the current state of the DFLL first */
|
|
|
|
return tegra_dfll_enable(pdev);
|
|
}
|
|
|
|
static void tegra_dfll_clk_disable(struct clk_hw *hw)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
|
|
/* XXX Validate the current state of the DFLL first */
|
|
|
|
tegra_dfll_disable(pdev);
|
|
}
|
|
|
|
static unsigned long tegra_dfll_clk_recalc_rate(struct clk_hw *hw,
|
|
unsigned long parent_rate)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
|
|
/* XXX Validate the current state of the DFLL first */
|
|
|
|
return tegra_dfll_request_get(pdev);
|
|
}
|
|
|
|
static long tegra_dfll_clk_round_rate(struct clk_hw *hw, unsigned long rate,
|
|
unsigned long *parent_rate)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
struct dfll_rate_req req;
|
|
int r;
|
|
unsigned long dvco_rate;
|
|
|
|
req.rate = rate;
|
|
r = calc_req_scale_mult(pdev, &req, &dvco_rate);
|
|
if (r)
|
|
return r;
|
|
|
|
return calc_dfll_request_rate(pdev, &req);
|
|
}
|
|
|
|
static int tegra_dfll_clk_set_rate(struct clk_hw *hw, unsigned long rate,
|
|
unsigned long parent_rate)
|
|
{
|
|
struct tegra_dfll_clk_hw *tdc = to_tegra_dfll_clk_hw(hw);
|
|
struct platform_device *pdev = tdc->pdev;
|
|
|
|
return tegra_dfll_request_rate(pdev, rate);
|
|
}
|
|
|
|
static const struct clk_ops tegra_dfll_clk_ops = {
|
|
.is_enabled = tegra_dfll_clk_is_enabled,
|
|
.prepare = tegra_dfll_clk_prepare,
|
|
.unprepare = tegra_dfll_clk_unprepare,
|
|
.enable = tegra_dfll_clk_enable,
|
|
.disable = tegra_dfll_clk_disable,
|
|
.recalc_rate = tegra_dfll_clk_recalc_rate,
|
|
.round_rate = tegra_dfll_clk_round_rate,
|
|
.set_rate = tegra_dfll_clk_set_rate,
|
|
};
|
|
|
|
static const char *const dfll_clk_parents[] = { "dfll_soc" };
|
|
|
|
static const struct clk_init_data dfll_clk_init_data = {
|
|
.name = DFLL_OUTPUT_CLOCK_NAME,
|
|
.ops = &tegra_dfll_clk_ops,
|
|
.parent_names = (const char **)dfll_clk_parents,
|
|
.num_parents = ARRAY_SIZE(dfll_clk_parents),
|
|
};
|
|
|
|
/**
|
|
* register_dfll_clk - register the DFLL output clock with the clock framework
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Register the DFLL's output clock (i.e., the clock source that the
|
|
* DVCO generates) with the Linux clock framework. Returns 0
|
|
* upon success or -EINVAL upon failure.
|
|
*/
|
|
static int register_dfll_clk(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int ret;
|
|
|
|
td->dfll_clk_hw.pdev = pdev;
|
|
td->dfll_clk_hw.hw.init = &dfll_clk_init_data;
|
|
|
|
td->dfll_clk = clk_register(&pdev->dev, &td->dfll_clk_hw.hw);
|
|
if (IS_ERR(td->dfll_clk)) {
|
|
dev_err(&pdev->dev, "DFLL clock registration error\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = clk_register_clkdev(td->dfll_clk, DFLL_OUTPUT_CLOCK_NAME, NULL);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "DFLL clkdev registration error\n");
|
|
goto rdc_err;
|
|
}
|
|
|
|
return 0;
|
|
rdc_err:
|
|
clk_unregister(td->dfll_clk);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Debugfs interface
|
|
*/
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
|
|
static int enable_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
*val = (td->mode == TEGRA_DFLL_CLOSED_LOOP ||
|
|
td->mode == TEGRA_DFLL_OPEN_LOOP);
|
|
return 0;
|
|
}
|
|
static int enable_set(void *data, u64 val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
if (val)
|
|
clk_prepare_enable(td->dfll_clk);
|
|
else
|
|
clk_disable_unprepare(td->dfll_clk);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(enable_fops, enable_get, enable_set, "%llu\n");
|
|
|
|
static int lock_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
*val = td->mode == TEGRA_DFLL_CLOSED_LOOP;
|
|
return 0;
|
|
}
|
|
static int lock_set(void *data, u64 val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
int r;
|
|
|
|
if (val)
|
|
r = tegra_dfll_lock(pdev);
|
|
else
|
|
r = tegra_dfll_unlock(pdev);
|
|
|
|
if (r)
|
|
dev_err(&pdev->dev, "lock_set %llu returned %d\n", val, r);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(lock_fops, lock_get, lock_set, "%llu\n");
|
|
|
|
static int monitor_get(void *data, u64 *val)
|
|
{
|
|
u32 v, s;
|
|
unsigned long flags;
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
clk_enable(td->soc_clk);
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
v = dfll_readl(td, DFLL_MONITOR_DATA) & DFLL_MONITOR_DATA_VAL_MASK;
|
|
|
|
if (dfll_readl(td, DFLL_MONITOR_CTRL) == DFLL_MONITOR_CTRL_FREQ) {
|
|
v = GET_MONITORED_RATE(v, td->ref_rate);
|
|
s = dfll_readl(td, DFLL_FREQ_REQ);
|
|
s = (s & DFLL_FREQ_REQ_SCALE_MASK) >> DFLL_FREQ_REQ_SCALE_SHIFT;
|
|
*val = (u64)v * (s + 1) / 256;
|
|
} else {
|
|
*val = v;
|
|
}
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
clk_disable(td->soc_clk);
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(monitor_fops, monitor_get, NULL, "%llu\n");
|
|
|
|
static int target_rate_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
|
|
*val = tegra_dfll_request_get(pdev);
|
|
return 0;
|
|
}
|
|
static int target_rate_set(void *data, u64 val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
return clk_set_rate(td->dfll_clk, val);
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(target_rate_fops, target_rate_get, target_rate_set,
|
|
"%llu\n");
|
|
|
|
static int vmax_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
*val = td->out_map[td->lut_max]->reg_uv / 1000;
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(vmax_fops, vmax_get, NULL, "%llu\n");
|
|
|
|
static int vmin_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
*val = td->out_map[td->lut_min]->reg_uv / 1000;
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(vmin_fops, vmin_get, NULL, "%llu\n");
|
|
|
|
static int tune_high_mv_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
*val = td->tune_high_min_mv;
|
|
return 0;
|
|
}
|
|
static int tune_high_mv_set(void *data, u64 val)
|
|
{
|
|
unsigned long flags;
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int r, ret;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
td->tune_high_min_mv = val;
|
|
r = dfll_init_output_thresholds(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "could not set output thresholds\n");
|
|
ret = -EINVAL;
|
|
goto thms_err;
|
|
}
|
|
|
|
if (td->mode == TEGRA_DFLL_CLOSED_LOOP)
|
|
reprogram_last_request(pdev);
|
|
|
|
ret = 0;
|
|
thms_err:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(tune_high_mv_fops, tune_high_mv_get, tune_high_mv_set,
|
|
"%llu\n");
|
|
|
|
static int fmin_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
*val = td->dvco_rate_min;
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(dvco_rate_min_fops, fmin_get, NULL, "%llu\n");
|
|
|
|
static int calibr_delay_get(void *data, u64 *val)
|
|
{
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
*val = jiffies_to_msecs(td->calibration_delay);
|
|
return 0;
|
|
}
|
|
static int calibr_delay_set(void *data, u64 val)
|
|
{
|
|
unsigned long flags;
|
|
struct platform_device *pdev = data;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
td->calibration_delay = msecs_to_jiffies(val);
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
return 0;
|
|
}
|
|
DEFINE_SIMPLE_ATTRIBUTE(calibr_delay_fops, calibr_delay_get, calibr_delay_set,
|
|
"%llu\n");
|
|
|
|
static int cl_register_show(struct seq_file *s, void *data)
|
|
{
|
|
unsigned long flags;
|
|
u32 offs;
|
|
struct platform_device *pdev = s->private;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
clk_enable(td->soc_clk);
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
seq_puts(s, "CONTROL REGISTERS:\n");
|
|
for (offs = 0; offs <= DFLL_MONITOR_DATA; offs += 4)
|
|
seq_printf(s, "[0x%02x] = 0x%08x\n",
|
|
offs, dfll_readl(td, offs));
|
|
|
|
seq_puts(s, "\nI2C and INTR REGISTERS:\n");
|
|
for (offs = DFLL_I2C_CFG; offs <= DFLL_I2C_STS; offs += 4)
|
|
seq_printf(s, "[0x%02x] = 0x%08x\n",
|
|
offs, dfll_readl(td, offs));
|
|
|
|
offs = DFLL_INTR_STS;
|
|
seq_printf(s, "[0x%02x] = 0x%08x\n", offs, dfll_readl(td, offs));
|
|
offs = DFLL_INTR_EN;
|
|
seq_printf(s, "[0x%02x] = 0x%08x\n", offs, dfll_readl(td, offs));
|
|
offs = DFLL_I2C_CLK_DIVISOR;
|
|
seq_printf(s, "[0x%02x] = 0x%08x\n", offs, dfll_readl(td, offs));
|
|
|
|
seq_puts(s, "\nLUT:\n");
|
|
for (offs = DFLL_OUTPUT_LUT;
|
|
offs < DFLL_OUTPUT_LUT + 4 * MAX_DFLL_VOLTAGES;
|
|
offs += 4)
|
|
seq_printf(s, "[0x%02x] = 0x%08x\n",
|
|
offs, dfll_readl(td, offs));
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
clk_disable(td->soc_clk);
|
|
return 0;
|
|
}
|
|
|
|
static int cl_register_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, cl_register_show, inode->i_private);
|
|
}
|
|
|
|
static ssize_t cl_register_write(struct file *file,
|
|
const char __user *userbuf, size_t count,
|
|
loff_t *ppos)
|
|
{
|
|
struct platform_device *pdev = file->f_path.dentry->d_inode->i_private;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
char buf[80];
|
|
u32 offs, val;
|
|
|
|
if (sizeof(buf) <= count)
|
|
return -EINVAL;
|
|
|
|
if (copy_from_user(buf, userbuf, count))
|
|
return -EFAULT;
|
|
|
|
/*
|
|
* terminate buffer and trim - white spaces may be appended at
|
|
* the end when invoked from shell command line
|
|
*/
|
|
buf[count] = '\0';
|
|
strim(buf);
|
|
|
|
if (sscanf(buf, "[0x%x] = 0x%x", &offs, &val) != 2)
|
|
return -1;
|
|
|
|
clk_enable(td->soc_clk);
|
|
dfll_writel(td, val, offs & (~0x3));
|
|
clk_disable(td->soc_clk);
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations cl_register_fops = {
|
|
.open = cl_register_open,
|
|
.read = seq_read,
|
|
.write = cl_register_write,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
};
|
|
|
|
static int tegra_dfll_debug_init(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int ret;
|
|
|
|
if (!td || (td->mode == TEGRA_DFLL_UNINITIALIZED))
|
|
return 0;
|
|
|
|
td->dent = debugfs_create_dir(DRIVER_NAME, NULL);
|
|
if (!td->dent)
|
|
return -ENOMEM;
|
|
|
|
ret = -ENOMEM;
|
|
|
|
if (!debugfs_create_file("enable", S_IRUGO | S_IWUSR,
|
|
td->dent, pdev, &enable_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("lock", S_IRUGO | S_IWUSR,
|
|
td->dent, pdev, &lock_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("monitor", S_IRUGO,
|
|
td->dent, pdev, &monitor_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("target_rate", S_IRUGO,
|
|
td->dent, pdev, &target_rate_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("vmax_mv", S_IRUGO,
|
|
td->dent, pdev, &vmax_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("vmin_mv", S_IRUGO,
|
|
td->dent, pdev, &vmin_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("tune_high_mv", S_IRUGO, td->dent, pdev,
|
|
&tune_high_mv_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("dvco_min", S_IRUGO, td->dent, pdev,
|
|
&dvco_rate_min_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("calibr_delay", S_IRUGO, td->dent, pdev,
|
|
&calibr_delay_fops))
|
|
goto err_out;
|
|
|
|
if (!debugfs_create_file("registers", S_IRUGO | S_IWUSR,
|
|
td->dent, pdev, &cl_register_fops))
|
|
goto err_out;
|
|
|
|
return 0;
|
|
|
|
err_out:
|
|
debugfs_remove_recursive(td->dent);
|
|
return ret;
|
|
}
|
|
|
|
#endif /* CONFIG_DEBUG_FS */
|
|
|
|
/*
|
|
* Interface for the thermal reaction driver and thermal zone
|
|
*/
|
|
|
|
/**
|
|
* tegra124_dfll_update_thermal_index - tell the DFLL how hot it is
|
|
* @pdev: DFLL instance
|
|
* @type: type of thermal floor or cap
|
|
* @new_idx: current DFLL temperature index (into therm_floors)
|
|
*
|
|
* Update the DFLL driver's sense of what temperature the DFLL is
|
|
* running at. @new_idx is an index into td->therm_floors - provided
|
|
* earlier to the thermal cooling driver. Intended to be called by
|
|
* the function supplied to the struct
|
|
* thermal_cooling_device_ops.set_cur_state function pointer. Returns
|
|
* 0 upon success or -ERANGE if @new_idx is out of range.
|
|
*/
|
|
int tegra124_dfll_update_thermal_index(struct platform_device *pdev,
|
|
enum tegra_dfll_therm_type type,
|
|
unsigned long new_idx)
|
|
{
|
|
unsigned long flags;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
if (type == TEGRA_DFLL_THERM_FLOOR) {
|
|
if (new_idx > td->therm_floors_num)
|
|
return -ERANGE;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
td->therm_floors_idx = new_idx;
|
|
set_dvco_rate_min(pdev);
|
|
if (td->mode == TEGRA_DFLL_CLOSED_LOOP)
|
|
reprogram_last_request(pdev);
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
} else if (type == TEGRA_DFLL_THERM_CAP) {
|
|
if (new_idx > td->therm_caps_num)
|
|
return -ERANGE;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
td->therm_caps_idx = new_idx;
|
|
if (td->mode == TEGRA_DFLL_CLOSED_LOOP)
|
|
reprogram_last_request(pdev);
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_update_thermal_index);
|
|
|
|
/**
|
|
* tegra124_dfll_get_thermal_index - return the DFLL's current thermal state
|
|
* @pdev: DFLL instance
|
|
* @type: type of thermal floor or cap
|
|
*
|
|
* Return the DFLL driver's copy of the DFLL's current temperature
|
|
* index, set by tegra124_dfll_update_thermal_index(). Intended to be
|
|
* called by the function supplied to the struct
|
|
* thermal_cooling_device_ops.get_cur_state function pointer.
|
|
*/
|
|
int tegra124_dfll_get_thermal_index(struct platform_device *pdev,
|
|
enum tegra_dfll_therm_type type)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
if (type == TEGRA_DFLL_THERM_FLOOR)
|
|
return td->therm_floors_idx;
|
|
else if (type == TEGRA_DFLL_THERM_CAP)
|
|
return td->therm_caps_idx;
|
|
else
|
|
return -EINVAL;
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_get_thermal_index);
|
|
|
|
/**
|
|
* tegra124_dfll_count_therm_states - return the number of thermal states
|
|
* @pdev: DFLL instance
|
|
* @type: type of thermal floor or cap
|
|
*
|
|
* Return the number of thermal states passed into the DFLL driver
|
|
* from the DT data. Intended to be called by the function supplied
|
|
* to the struct thermal_cooling_device_ops.get_max_state function
|
|
* pointer, and by the integration code that binds a thermal zone to
|
|
* the DFLL thermal reaction driver.
|
|
*/
|
|
int tegra124_dfll_count_therm_states(struct platform_device *pdev,
|
|
enum tegra_dfll_therm_type type)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
if (type == TEGRA_DFLL_THERM_FLOOR)
|
|
return td->therm_floors_num;
|
|
else if (type == TEGRA_DFLL_THERM_CAP)
|
|
return td->therm_caps_num;
|
|
else
|
|
return -EINVAL;
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_count_therm_states);
|
|
|
|
/**
|
|
* tegra124_dfll_get_therm_state_temp - return the state temp at @index
|
|
* @pdev: DFLL instance
|
|
* @type: type of thermal floor or cap
|
|
* @index: index into the therm_floors or therm_caps array (zero-based)
|
|
*
|
|
* Return the temperature corresponding to @index from the therm_floors or
|
|
* therm_caps array. Intended to be called by integration code
|
|
* binding a thermal zone to the DFLL thermal reaction driver.
|
|
* Returns -ERANGE if @index would result in an access off the end of
|
|
* the array, or returns the temperature corresponding to @index in
|
|
* degrees Celsius.
|
|
*/
|
|
int tegra124_dfll_get_therm_state_temp(struct platform_device *pdev,
|
|
enum tegra_dfll_therm_type type,
|
|
unsigned long index)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
if (type == TEGRA_DFLL_THERM_FLOOR) {
|
|
if (index >= td->therm_floors_num)
|
|
return -ERANGE;
|
|
return td->therm_floors[index].temp;
|
|
} else if (type == TEGRA_DFLL_THERM_CAP) {
|
|
if (index >= td->therm_caps_num)
|
|
return -ERANGE;
|
|
return td->therm_caps[index].temp;
|
|
} else {
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_get_therm_state_temp);
|
|
|
|
/**
|
|
* tegra124_dfll_attach_thermal - attach the "cooling device" @cdev to @pdev
|
|
* @pdev: DFLL instance
|
|
* @type: type of thermal floor or cap
|
|
* @cdev: DFLL "cooling device" instance
|
|
*
|
|
* Attach a thermal reaction "cooling device" to the DFLL instance
|
|
* @pdev. As long as a thermal reaction driver is attached, the DFLL
|
|
* driver can't be unbound from the DFLL device. Returns -EINVAL if
|
|
* the DFLL instance hasn't been initialized yet, -EBUSY if a thermal
|
|
* driver is already associated with @pdev, or 0 upon success.
|
|
*/
|
|
int tegra124_dfll_attach_thermal(struct platform_device *pdev,
|
|
enum tegra_dfll_therm_type type,
|
|
struct thermal_cooling_device *cdev)
|
|
{
|
|
unsigned long flags;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int ret = -EINVAL;
|
|
|
|
if (!td)
|
|
return ret;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (td->mode == TEGRA_DFLL_UNINITIALIZED) {
|
|
dev_err(&pdev->dev, "DFLL not yet initialized\n");
|
|
ret = -EINVAL;
|
|
goto tdat_out;
|
|
}
|
|
|
|
if (type == TEGRA_DFLL_THERM_FLOOR) {
|
|
if (td->cdev_floor) {
|
|
dev_err(&pdev->dev,
|
|
"DFLL floor already bound to thermal driver\n");
|
|
ret = -EBUSY;
|
|
goto tdat_out;
|
|
}
|
|
|
|
td->cdev_floor = cdev;
|
|
ret = 0;
|
|
} else if (type == TEGRA_DFLL_THERM_CAP) {
|
|
if (td->cdev_cap) {
|
|
dev_err(&pdev->dev,
|
|
"DFLL cap already bound to thermal driver\n");
|
|
ret = -EBUSY;
|
|
goto tdat_out;
|
|
}
|
|
|
|
td->cdev_cap = cdev;
|
|
ret = 0;
|
|
} else {
|
|
ret = -EINVAL;
|
|
}
|
|
|
|
tdat_out:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_attach_thermal);
|
|
|
|
/**
|
|
* tegra124_dfll_detach_thermal - detach the "cooling device" @cdev from @pdev
|
|
* @pdev: DFLL instance
|
|
* @type: type of thermal floor or cap
|
|
* @cdev: DFLL "cooling device" instance
|
|
*
|
|
* Detach a thermal reaction "cooling device" from the DFLL instance
|
|
* @pdev. Returns -EINVAL if @cdev isn't currently associated with
|
|
* the DFLL instance, or 0 upon success.
|
|
*/
|
|
int tegra124_dfll_detach_thermal(struct platform_device *pdev,
|
|
enum tegra_dfll_therm_type type,
|
|
struct thermal_cooling_device *cdev)
|
|
{
|
|
unsigned long flags;
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int ret = -EINVAL;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
if (type == TEGRA_DFLL_THERM_FLOOR) {
|
|
if (td->cdev_floor != cdev) {
|
|
dev_err(&pdev->dev, "floor cooling device mismatch\n");
|
|
ret = -EINVAL;
|
|
goto tddt_out;
|
|
}
|
|
|
|
td->cdev_floor = NULL;
|
|
td->therm_floors_idx = 0;
|
|
ret = 0;
|
|
} else if (type == TEGRA_DFLL_THERM_CAP) {
|
|
if (td->cdev_cap != cdev) {
|
|
dev_err(&pdev->dev, "cap cooling device mismatch\n");
|
|
ret = -EINVAL;
|
|
goto tddt_out;
|
|
}
|
|
|
|
td->cdev_cap = NULL;
|
|
td->therm_caps_idx = 0;
|
|
ret = 0;
|
|
} else {
|
|
ret = -EINVAL;
|
|
}
|
|
|
|
tddt_out:
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_detach_thermal);
|
|
|
|
/*
|
|
* Interface to lock and unlock DFLL loop
|
|
*/
|
|
|
|
/**
|
|
* tegra124_dfll_lock_loop - enable closed loop
|
|
*
|
|
* The wrapper of tegra_dfll_lock(). Return -EPERM if the static DFLL
|
|
* platform device instance does not get initialized.
|
|
*/
|
|
int tegra124_dfll_lock_loop(void)
|
|
{
|
|
if (!fcpu_dfll_pdev) {
|
|
WARN(1, "Tegra124 DFLL is not initialized\n");
|
|
return -EPERM;
|
|
}
|
|
|
|
return tegra_dfll_lock(fcpu_dfll_pdev);
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_lock_loop);
|
|
|
|
/**
|
|
* tegra124_dfll_unlock_loop - disable closed loop
|
|
*
|
|
* The wrapper of tegra_dfll_lock(). Return -EPERM if the static DFLL
|
|
* platform device instance does not get initialized.
|
|
*/
|
|
int tegra124_dfll_unlock_loop(void)
|
|
{
|
|
if (!fcpu_dfll_pdev) {
|
|
WARN(1, "Tegra124 DFLL is not initialized\n");
|
|
return -EPERM;
|
|
}
|
|
|
|
return tegra_dfll_unlock(fcpu_dfll_pdev);
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_unlock_loop);
|
|
|
|
/**
|
|
* tegra124_dfll_get_fv_table - get freq/volt table for cpug
|
|
*
|
|
* @num_freqs: number of frequencies
|
|
* @freqs: the array of frequencies
|
|
* @millivolts: the array of voltages
|
|
*/
|
|
int tegra124_dfll_get_fv_table(int *num_freqs, unsigned long **freqs,
|
|
int **millivolts)
|
|
{
|
|
struct tegra_dfll *td;
|
|
|
|
if (!fcpu_dfll_pdev)
|
|
return -EPERM;
|
|
|
|
if (!num_freqs || !freqs || !millivolts)
|
|
return -EINVAL;
|
|
|
|
td = dev_get_drvdata(&fcpu_dfll_pdev->dev);
|
|
*num_freqs = td->dvfs_info->num_freqs;
|
|
*freqs = td->dvfs_info->freqs;
|
|
*millivolts = td->dvfs_info->cpu_dfll_millivolts;
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(tegra124_dfll_get_fv_table);
|
|
|
|
/*
|
|
* DFLL initialization
|
|
*/
|
|
|
|
/**
|
|
* tegra_dfll_init_clks - clk_get() the DFLL source clocks
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Call clk_get() on the DFLL source clocks and save the pointers for later
|
|
* use. Returns 0 upon success or -ENODEV if one or more of the clocks
|
|
* couldn't be looked up.
|
|
*/
|
|
static int tegra_dfll_init_clks(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int ret;
|
|
|
|
td->ref_clk = devm_clk_get(&pdev->dev, "ref");
|
|
if (IS_ERR(td->ref_clk)) {
|
|
dev_err(&pdev->dev, "missing ref clock\n");
|
|
ret = -ENODEV;
|
|
goto tdic_err;
|
|
}
|
|
|
|
td->soc_clk = devm_clk_get(&pdev->dev, "soc");
|
|
if (IS_ERR(td->soc_clk)) {
|
|
dev_err(&pdev->dev, "missing soc clock\n");
|
|
ret = -ENODEV;
|
|
goto tdic_err;
|
|
}
|
|
|
|
td->i2c_clk = devm_clk_get(&pdev->dev, "i2c");
|
|
if (IS_ERR(td->i2c_clk)) {
|
|
dev_err(&pdev->dev, "missing i2c clock\n");
|
|
ret = -ENODEV;
|
|
goto tdic_err;
|
|
}
|
|
|
|
return 0;
|
|
|
|
tdic_err:
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_regulator_probe_voltages - build vdd_map[] from the regulator
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Build the vdd_map from regulator framework and DFLL DT data.
|
|
* Returns 0 upon success, or -ENOSPC on a memory allocation failure.
|
|
*/
|
|
static int tegra_dfll_regulator_probe_voltages(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int c, i, j, vdd_uv;
|
|
struct tegra_dfll_voltage_reg_map *vdd_map;
|
|
|
|
c = regulator_count_voltages(td->vdd);
|
|
if (c < 0)
|
|
return c;
|
|
|
|
vdd_map = devm_kzalloc(&pdev->dev,
|
|
sizeof(struct tegra_dfll_voltage_reg_map) * c,
|
|
GFP_KERNEL);
|
|
if (!vdd_map)
|
|
return -ENOMEM;
|
|
|
|
j = 0;
|
|
for (i = 0; i < c; i++) {
|
|
vdd_uv = regulator_list_voltage(td->vdd, i);
|
|
if (vdd_uv <= 0)
|
|
continue;
|
|
|
|
if (vdd_uv < td->dfll_min_microvolt)
|
|
continue;
|
|
|
|
if (vdd_uv > td->dfll_max_microvolt)
|
|
break;
|
|
|
|
vdd_map[j].reg_value = i;
|
|
vdd_map[j].reg_uv = vdd_uv;
|
|
j++;
|
|
}
|
|
|
|
td->vdd_map_size = j;
|
|
td->vdd_map = vdd_map;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* DT data fetch
|
|
*/
|
|
|
|
/**
|
|
* parse_of_dfll_pmic_integration - find and extract the DT DFLL-PMIC data
|
|
* @pdev: DFLL instance
|
|
* @dn: DT node of the DFLL instance data
|
|
*
|
|
* Read the DFLL PMIC integration data from the DT node with the
|
|
* "nvidia,tegra124-dfll,pmic-integration" property. This data
|
|
* includes the PMIC I2C address, the I2C bus speed, the I2C voltage
|
|
* register address. Returns 0 upon success or -EINVAL upon error.
|
|
*/
|
|
static int parse_of_dfll_pmic_integration(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
const __be32 *prop;
|
|
struct device_node *p_dn;
|
|
|
|
prop = of_get_property(dn, "pmic-integration", NULL);
|
|
if (!prop) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"pmic-integration");
|
|
return -EINVAL;
|
|
}
|
|
|
|
p_dn = of_find_node_by_phandle(be32_to_cpup(prop));
|
|
if (!p_dn) {
|
|
dev_err(&pdev->dev, "missing DFLL PMIC integration DT data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(p_dn, "pmic-i2c-address",
|
|
&td->pmic_i2c_addr)) {
|
|
dev_err(&pdev->dev, "missing PMIC I2C address\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(p_dn, "pmic-i2c-voltage-register",
|
|
&td->pmic_i2c_voltage_reg)) {
|
|
dev_err(&pdev->dev,
|
|
"missing PMIC I2C voltage register address\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(p_dn, "dfll-min-microvolt",
|
|
&td->dfll_min_microvolt)) {
|
|
dev_err(&pdev->dev,
|
|
"missing DFLL regulator minimum voltage\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(p_dn, "dfll-max-microvolt",
|
|
&td->dfll_max_microvolt)) {
|
|
dev_err(&pdev->dev,
|
|
"missing DFLL regulator maximum voltage\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
of_property_read_u32(p_dn, "i2c-hs-rate", &td->pmic_i2c_hs_rate);
|
|
|
|
if ((td->pmic_i2c_hs_rate > 0) &&
|
|
of_property_read_u32(p_dn, "i2c-hs-master-code",
|
|
&td->pmic_i2c_hs_master_code)) {
|
|
dev_err(&pdev->dev, "missing I2C HS master code\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->pmic_i2c_ten_bit_addrs = !!of_get_property(p_dn,
|
|
"i2c-10-bit-addresses",
|
|
NULL);
|
|
|
|
if (of_property_read_u32(p_dn, "i2c-fs-rate", &td->pmic_i2c_fs_rate)) {
|
|
dev_err(&pdev->dev, "missing I2C FS bus rate\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* parse_of_dfll_board_data - find and extract the DFLL per-board parameters
|
|
* @pdev: DFLL instance
|
|
* @dn: DT node of the DFLL instance data
|
|
*
|
|
* Read the DFLL per-board data from the DT node with the
|
|
* "nvidia,tegra124-dfll,board-data" compatible property. This data
|
|
* is the result of the DFLL characterization process for a particular
|
|
* board design. It includes loop and voltage droop avoidance
|
|
* parameters. It does not include PMIC integration; see
|
|
* parse_of_dfll_pmic_integration() for that. Returns 0 upon success
|
|
* or -EINVAL upon error.
|
|
*/
|
|
static int parse_of_dfll_board_data(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct device_node *b_dn;
|
|
int fixed_forcing, auto_forcing, no_forcing;
|
|
int i = 0, ret = 0;
|
|
const __be32 *prop;
|
|
u32 dtp;
|
|
|
|
prop = of_get_property(dn, "board-params", NULL);
|
|
if (!prop) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "board-params");
|
|
return -EINVAL;
|
|
}
|
|
|
|
b_dn = of_find_node_by_phandle(be32_to_cpup(prop));
|
|
if (!b_dn) {
|
|
dev_err(&pdev->dev, "missing DFLL board DT data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(b_dn, "sample-rate", &td->sample_rate)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "sample-rate");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(b_dn, "cf", &td->cf)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "cf");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(b_dn, "ci", &td->ci)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "ci");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(b_dn, "cg", &td->cg)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "cg");
|
|
return -EINVAL;
|
|
}
|
|
|
|
dtp = !!of_get_property(b_dn, "cg-scale", NULL);
|
|
if (dtp)
|
|
td->cg_scale = 1;
|
|
|
|
if (of_property_read_u32(b_dn, "droop-cut-value",
|
|
&td->droop_cut_value)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"droop-cut-value");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(b_dn, "droop-restore-ramp",
|
|
&td->droop_restore_ramp)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"droop-restore-ramp");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(b_dn, "scale-out-ramp", &td->scale_out_ramp)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"scale-out-ramp");
|
|
return -EINVAL;
|
|
}
|
|
|
|
fixed_forcing = !!of_get_property(b_dn, "fixed-output-forcing", NULL);
|
|
auto_forcing = !!of_get_property(b_dn, "auto-output-forcing", NULL);
|
|
no_forcing = !!of_get_property(b_dn, "no-output-forcing", NULL);
|
|
if (fixed_forcing) {
|
|
td->force_mode = TEGRA_DFLL_FORCE_FIXED;
|
|
i++;
|
|
}
|
|
if (auto_forcing) {
|
|
td->force_mode = TEGRA_DFLL_FORCE_AUTO;
|
|
i++;
|
|
}
|
|
if (no_forcing) {
|
|
td->force_mode = TEGRA_DFLL_FORCE_NONE;
|
|
i++;
|
|
}
|
|
if (i != 1) {
|
|
dev_err(&pdev->dev,
|
|
"must specify one and only one forcing param in DT\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* parse_of_dfll_tuning - read the DFLL tuning data from DT
|
|
* @pdev: DFLL instance
|
|
* @dn: DT node of the DFLL instance data
|
|
*
|
|
* Read the DFLL tuning data from DT. Returns 0 upon success, -EINVAL
|
|
* if required data fields are missing, or -ENOENT if no DFLL tuning
|
|
* node could be found that matches the current chip's Speedo ID and
|
|
* process id.
|
|
*/
|
|
static int parse_of_dfll_tuning(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct device_node *t_dn, *tc_dn;
|
|
const __be32 *prop;
|
|
u32 speedo_id, process_id;
|
|
|
|
prop = of_get_property(dn, "tuning", NULL);
|
|
if (!prop) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "tuning");
|
|
return -EINVAL;
|
|
}
|
|
|
|
t_dn = of_find_node_by_phandle(be32_to_cpup(prop));
|
|
if (!t_dn) {
|
|
dev_err(&pdev->dev, "missing DFLL tuning DT data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Read the loop params */
|
|
for_each_child_of_node(t_dn, tc_dn) {
|
|
if (of_property_read_u32(tc_dn, "speedo-id", &speedo_id)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"speedo-id");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (speedo_id != td->speedo_id)
|
|
continue;
|
|
|
|
if (!of_property_read_u32(tc_dn, "process-id", &process_id))
|
|
if (process_id != td->process_id)
|
|
continue;
|
|
|
|
if (of_property_read_u32(tc_dn, "tune0-low-voltage-range",
|
|
&td->tune0_low_voltage_range)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"tune0-low-voltage-range");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(tc_dn, "tune0-high-voltage-range",
|
|
&td->tune0_high_voltage_range)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"tune0-high-voltage-range");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(tc_dn, "tune1", &td->tune1)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "tune1");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(tc_dn, "droop-rate-min",
|
|
&td->droop_rate_min)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"droop-rate-min");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(tc_dn, "min-millivolts",
|
|
&td->min_millivolts)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"min-millivolts");
|
|
return -EINVAL;
|
|
}
|
|
|
|
of_property_read_u32(tc_dn, "tune-high-min-millivolts",
|
|
&td->tune_high_min_mv);
|
|
|
|
return 0;
|
|
}
|
|
|
|
return -ENOENT;
|
|
}
|
|
|
|
/**
|
|
* parse_of_cvbs - parse the per-frequency voltage curve data
|
|
* @pdev: DFLL instance
|
|
* @dn: struct device_node * to the "cvb" DT node
|
|
*
|
|
* Read the CVB per-frequency data from DT. Other code walks this
|
|
* table and builds a "cooked" table, used during runtime. Returns 0
|
|
* upon success or -EINVAL upon error.
|
|
*/
|
|
static int parse_of_cvbs(struct platform_device *pdev, struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct tegra_dfll_cvb *tdc;
|
|
struct property *prop;
|
|
const __be32 *p;
|
|
u32 u;
|
|
int i, j, l, rem;
|
|
|
|
prop = of_find_property(dn, "cvb-voltage-curves", &l);
|
|
if (!prop || (l == 0)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "cvb");
|
|
return -EINVAL;
|
|
}
|
|
l /= sizeof(__be32);
|
|
|
|
rem = l % CVB_ROW_WIDTH;
|
|
if (rem > 0) {
|
|
dev_err(&pdev->dev, "CVB data format error\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
l /= CVB_ROW_WIDTH;
|
|
tdc = devm_kzalloc(&pdev->dev, l * sizeof(struct tegra_dfll_cvb),
|
|
GFP_KERNEL);
|
|
if (!tdc)
|
|
goto poct_err;
|
|
|
|
i = 0;
|
|
of_property_for_each_u32(dn, "cvb-voltage-curves", prop, p, u) {
|
|
j = i / CVB_ROW_WIDTH;
|
|
|
|
switch (i % CVB_ROW_WIDTH) {
|
|
case 0:
|
|
tdc[j].freq = u;
|
|
break;
|
|
case 1:
|
|
tdc[j].c0 = u;
|
|
break;
|
|
case 2:
|
|
tdc[j].c1 = u;
|
|
break;
|
|
case 3:
|
|
tdc[j].c2 = u;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
td->dvfs_info->cvb_table = tdc;
|
|
td->dvfs_info->cvb_table_len = i / CVB_ROW_WIDTH;
|
|
|
|
return 0;
|
|
|
|
poct_err:
|
|
return -EINVAL;
|
|
}
|
|
|
|
/**
|
|
* parse_of_cvb_table_meta - read the CVB table metadata from DT
|
|
* @pdev: DFLL instance
|
|
* @lpc_dn: 'characterization' DT node
|
|
*
|
|
* Read the CVB table metadata from the DeviceTree data. Returns 0
|
|
* upon success, or -EINVAL upon failure.
|
|
*/
|
|
static int parse_of_cvb_table_meta(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
if (of_property_read_u32(dn, "cvb-max-millivolts",
|
|
&td->cvb_max_millivolts)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"cvb-max-millivolts");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(dn, "cvb-speedo-scale",
|
|
&td->cvb_speedo_scale)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"cvb-speedo-scale");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (of_property_read_u32(dn, "cvb-voltage-scale",
|
|
&td->cvb_voltage_scale)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"cvb-voltage-scale");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* parse_of_therm_caps - parse the thermal voltage caps
|
|
* @pdev: DFLL instance
|
|
* @dn: struct device_node * of the "characterization" DT node
|
|
*
|
|
* Read the array of u32s from the 'therm-caps' property, under the
|
|
* DT device node @dn. The first of the two values represents the
|
|
* trip point temperature in degrees Celsius. The second value
|
|
* represents the maximum voltage cap for that temperature, and, if
|
|
* there are no lower temperatures specified, any temperatures below
|
|
* it. Returns 0 if the 'therm-caps' property doesn't exist (it's
|
|
* optional) or if the data was read successfully, or -EINVAL if
|
|
* something is wrong with the DT data.
|
|
*/
|
|
static int parse_of_therm_caps(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct property *prop;
|
|
const __be32 *p;
|
|
u32 u;
|
|
int i, j;
|
|
|
|
i = 0;
|
|
of_property_for_each_u32(dn, "therm-caps", prop, p, u) {
|
|
j = i / THERM_CAPS_ROW_WIDTH;
|
|
if (j >= MAX_THERMAL_CAPS)
|
|
break;
|
|
|
|
if (!(i % THERM_CAPS_ROW_WIDTH)) {
|
|
/* XXX Test u32 -> u8 overflow */
|
|
td->therm_caps[j].temp = u;
|
|
} else {
|
|
td->therm_caps[j].mv = u;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
if (i % THERM_CAPS_ROW_WIDTH) {
|
|
dev_err(&pdev->dev, "therm-caps data format error\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->therm_caps_num = i / THERM_CAPS_ROW_WIDTH;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* parse_of_therm_floors - parse the thermal voltage floors
|
|
* @pdev: DFLL instance
|
|
* @dn: struct device_node * of the "characterization" DT node
|
|
*
|
|
* Read the array of u32s from the 'therm-floors' property, under the
|
|
* DT device node @dn. The first of the two values represents the
|
|
* trip point temperature in degrees Celsius. The second value
|
|
* represents the minimum voltage floor for that temperature, and, if
|
|
* there are no lower temperatures specified, any temperatures below
|
|
* it. Returns 0 if the 'therm-floors' property doesn't exist (it's
|
|
* optional) or if the data was read successfully, or -EINVAL if
|
|
* something is wrong with the DT data.
|
|
*/
|
|
static int parse_of_therm_floors(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct property *prop;
|
|
const __be32 *p;
|
|
u32 u;
|
|
int i, j;
|
|
|
|
i = 0;
|
|
of_property_for_each_u32(dn, "therm-floors", prop, p, u) {
|
|
j = i / THERM_FLOORS_ROW_WIDTH;
|
|
if (j >= MAX_THERMAL_FLOORS)
|
|
break;
|
|
|
|
if (!(i % THERM_FLOORS_ROW_WIDTH)) {
|
|
/* XXX Test u32 -> u8 overflow */
|
|
td->therm_floors[j].temp = u;
|
|
} else {
|
|
td->therm_floors[j].mv = u;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
if (i % THERM_FLOORS_ROW_WIDTH) {
|
|
dev_err(&pdev->dev, "therm-floors data format error\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->therm_floors_num = i / THERM_FLOORS_ROW_WIDTH;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* parse_of_cvb_table - read CVB table data from DeviceTree
|
|
* @pdev: DFLL instance
|
|
* @dn: DT node of the DFLL instance data
|
|
*
|
|
* Reads the CVB table data and CVB metadata that corresponds to the
|
|
* current chip's Speedo and process ID. Returns 0 upon success,
|
|
* -EINVAL upon error, or -ENOENT if no CVB table node could be found
|
|
* that matches the current chip's Speedo ID and process id.
|
|
*/
|
|
static int parse_of_cvb_table(struct platform_device *pdev,
|
|
struct device_node *dn)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
struct device_node *d_dn, *dc_dn;
|
|
const __be32 *prop;
|
|
u32 speedo_id, process_id;
|
|
int r;
|
|
|
|
prop = of_get_property(dn, "cvb-table", NULL);
|
|
if (!prop) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n", "cvb-table");
|
|
return -EINVAL;
|
|
}
|
|
|
|
d_dn = of_find_node_by_phandle(be32_to_cpup(prop));
|
|
if (!d_dn) {
|
|
dev_err(&pdev->dev, "missing DFLL cvb table DT data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
for_each_child_of_node(d_dn, dc_dn) {
|
|
if (of_property_read_u32(dc_dn, "speedo-id", &speedo_id)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"speedo-id");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (speedo_id != td->speedo_id)
|
|
continue;
|
|
|
|
if (!of_property_read_u32(dc_dn, "process-id", &process_id))
|
|
if (process_id != td->process_id)
|
|
continue;
|
|
|
|
if (parse_of_therm_caps(pdev, dc_dn)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"therm-caps");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (parse_of_therm_floors(pdev, dc_dn)) {
|
|
dev_err(&pdev->dev, "missing %s in DT data\n",
|
|
"therm-floors");
|
|
return -EINVAL;
|
|
}
|
|
|
|
r = parse_of_cvb_table_meta(pdev, dc_dn);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "couldn't parse CVB table meta\n");
|
|
return r;
|
|
}
|
|
|
|
r = parse_of_cvbs(pdev, dc_dn);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "couldn't parse CVB table\n");
|
|
return r;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
return -ENOENT;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_init_pmic_data - initialize PMIC regulator data
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Read the PMIC integration data, including regulator data, from DT
|
|
* and the the regulator framework. Build the voltage map from
|
|
* regulator data. Returns 0 upon success or -EINVAL upon error.
|
|
*/
|
|
static int __must_check tegra_dfll_init_pmic_data(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
int r;
|
|
|
|
td->vdd = devm_regulator_get(&pdev->dev, "vdd");
|
|
if (IS_ERR(td->vdd)) {
|
|
dev_err(&pdev->dev, "couldn't locate regulator\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
td->vdd_step = regulator_get_linear_step(td->vdd);
|
|
if (!td->vdd_step) {
|
|
dev_err(&pdev->dev, "only linear map regulators supported\n");
|
|
goto tdipd_err;
|
|
}
|
|
|
|
r = parse_of_dfll_pmic_integration(pdev, pdev->dev.of_node);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "DFLL PMIC integration parse error\n");
|
|
goto tdipd_err;
|
|
}
|
|
|
|
r = tegra_dfll_regulator_probe_voltages(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "couldn't probe regulator voltages\n");
|
|
goto tdipd_err;
|
|
}
|
|
|
|
return 0;
|
|
|
|
tdipd_err:
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* platform_device integration
|
|
*/
|
|
|
|
/**
|
|
* tegra_dfll_probe - probe the instance of the DFLL IP block
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Called when a DFLL device is bound to this driver by the driver
|
|
* core. Registers the DFLL output clock with the clock framework.
|
|
* Returns 0 upon success, or -ENOMEM if memory couldn't be allocated,
|
|
* -EINVAL if various calls fail, or can pass along the error returned
|
|
* by several other functions.
|
|
*/
|
|
static int tegra_dfll_probe(struct platform_device *pdev)
|
|
{
|
|
struct device_node *dn = pdev->dev.of_node;
|
|
struct resource *mem;
|
|
struct tegra_dfll *td;
|
|
struct tegra_dfll_dvfs_info *dvfs_info;
|
|
int ret = -EINVAL;
|
|
int r;
|
|
|
|
td = devm_kzalloc(&pdev->dev, sizeof(*td), GFP_KERNEL);
|
|
if (!td)
|
|
return -ENOMEM;
|
|
|
|
dvfs_info = devm_kzalloc(&pdev->dev, sizeof(*dvfs_info), GFP_KERNEL);
|
|
if (!dvfs_info)
|
|
return -ENOMEM;
|
|
|
|
td->dvfs_info = dvfs_info;
|
|
|
|
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
|
if (!mem) {
|
|
dev_err(&pdev->dev, "missing register MMIO resource\n");
|
|
ret = -EINVAL;
|
|
goto tdp_err;
|
|
}
|
|
|
|
td->base = ioremap(mem->start, resource_size(mem));
|
|
if (!td->base) {
|
|
dev_err(&pdev->dev, "couldn't ioremap() register MMIO area\n");
|
|
ret = -EINVAL;
|
|
goto tdp_err;
|
|
}
|
|
|
|
td->speedo_id = tegra_get_cpu_speedo_id();
|
|
td->process_id = tegra_get_cpu_process_id();
|
|
td->speedo_value = tegra_get_cpu_speedo_value();
|
|
|
|
platform_set_drvdata(pdev, td);
|
|
|
|
r = tegra_dfll_init_clks(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "DFLL clock init error\n");
|
|
ret = r;
|
|
goto tdp_err;
|
|
}
|
|
|
|
/* Set some parameters from the IP block's DT data */
|
|
r = parse_of_cvb_table(pdev, dn);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "DFLL CVB data parse error\n");
|
|
ret = r;
|
|
goto tdp_err;
|
|
}
|
|
|
|
r = parse_of_dfll_tuning(pdev, dn);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "DFLL tuning data parse error\n");
|
|
ret = r;
|
|
goto tdp_err;
|
|
}
|
|
|
|
r = parse_of_dfll_board_data(pdev, dn);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "DFLL board data parse error\n");
|
|
ret = -EINVAL;
|
|
goto tdp_err;
|
|
}
|
|
|
|
r = tegra_dfll_init_pmic_data(pdev);
|
|
if (r) {
|
|
dev_err(&pdev->dev, "DFLL PMIC data parse error\n");
|
|
ret = r;
|
|
goto tdp_err;
|
|
}
|
|
|
|
/* Enable the clocks and set the device up */
|
|
ret = dfll_init(pdev);
|
|
if (ret)
|
|
goto tdp_err;
|
|
|
|
fcpu_dfll_pdev = pdev;
|
|
|
|
ret = register_dfll_clk(pdev);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "DFLL clk registration failed\n");
|
|
goto tdp_err;
|
|
}
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
tegra_dfll_debug_init(pdev);
|
|
#endif
|
|
|
|
dev_info(&pdev->dev, "Tegra T124 DFLL clock driver initialized\n");
|
|
|
|
return 0;
|
|
|
|
tdp_err:
|
|
fcpu_dfll_pdev = NULL;
|
|
iounmap(td->base);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_remove - unbind the DFLL driver from the DFLL device
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Releases the DFLL platform_driver from the DFLL platform_device
|
|
* @pdev. The DFLL must be in the DISABLED state before this can be
|
|
* successful. Returns 0 upon success, or -EBUSY if the DFLL is still
|
|
* generating a clock.
|
|
*/
|
|
static int tegra_dfll_remove(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
|
|
/* Try to prevent removal while the DFLL is active */
|
|
if (td->mode != TEGRA_DFLL_DISABLED) {
|
|
dev_err(&pdev->dev,
|
|
"must disable DFLL before removing driver\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
if (td->cdev_floor || td->cdev_cap) {
|
|
dev_err(&pdev->dev,
|
|
"must unload thermal driver before removing DFLL\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
clk_unregister(td->dfll_clk);
|
|
tegra124_clock_assert_dfll_dvco_reset();
|
|
regulator_put(td->vdd);
|
|
iounmap(td->base);
|
|
kfree(td);
|
|
fcpu_dfll_pdev = NULL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* dfll controls clock/voltage to other devices, including CPU. Therefore,
|
|
* dfll driver pm suspend callback does not stop cl-dvfs operations. It is
|
|
* only used to enforce cold voltage limit, since SoC may cool down during
|
|
* suspend without waking up. The correct temperature zone after supend will
|
|
* be updated via dfll cooling device interface during resume of temperature
|
|
* sensor.
|
|
*/
|
|
int tegra124_dfll_suspend(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
td->therm_caps_idx = td->therm_caps_num;
|
|
td->therm_floors_idx = 0;
|
|
set_dvco_rate_min(pdev);
|
|
if (td->mode == TEGRA_DFLL_CLOSED_LOOP) {
|
|
set_cl_config(pdev, &td->last_req);
|
|
set_request(pdev, &td->last_req);
|
|
}
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
if (td->mode == TEGRA_DFLL_CLOSED_LOOP) {
|
|
tegra_dfll_unlock(pdev);
|
|
td->resume_mode = TEGRA_DFLL_CLOSED_LOOP;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* tegra_dfll_resume - reprogram the DFLL after context-loss
|
|
* @pdev: DFLL instance
|
|
*
|
|
* Re-initialize and enable target device clock in open loop mode. Called
|
|
* directly from SoC clock resume syscore operation. Closed loop will be
|
|
* re-entered in platform syscore ops as well.
|
|
*/
|
|
void tegra124_dfll_resume(struct platform_device *pdev)
|
|
{
|
|
struct tegra_dfll *td = dev_get_drvdata(&pdev->dev);
|
|
enum tegra_dfll_ctrl_mode mode = td->mode;
|
|
struct dfll_rate_req req = td->last_req;
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
spin_lock_irqsave(&td->lock, flags);
|
|
|
|
tegra124_clock_deassert_dfll_dvco_reset();
|
|
|
|
dfll_enable_clocks(pdev);
|
|
|
|
/* Setup PMU interface, and configure controls in disabled mode */
|
|
dfll_init_out_if(pdev);
|
|
ret = dfll_init_cntrl_logic(pdev);
|
|
if (ret)
|
|
dev_err(&pdev->dev, "could not init the control logic\n");
|
|
|
|
dfll_disable_clocks(pdev);
|
|
|
|
/* Restore last request and mode */
|
|
td->last_req = req;
|
|
if (mode != TEGRA_DFLL_DISABLED) {
|
|
set_mode(pdev, TEGRA_DFLL_OPEN_LOOP);
|
|
WARN(mode > TEGRA_DFLL_OPEN_LOOP,
|
|
"DFLL was left locked in suspend\n");
|
|
}
|
|
|
|
spin_unlock_irqrestore(&td->lock, flags);
|
|
|
|
if (td->resume_mode == TEGRA_DFLL_CLOSED_LOOP) {
|
|
tegra_dfll_lock(pdev);
|
|
td->resume_mode = TEGRA_DFLL_UNINITIALIZED;
|
|
}
|
|
}
|
|
|
|
/* Match table for OF platform binding */
|
|
static struct of_device_id tegra_dfll_of_match[] = {
|
|
{ .compatible = "nvidia,tegra124-dfll", },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(of, tegra_dfll_of_match);
|
|
|
|
static struct platform_driver tegra_dfll_driver = {
|
|
.probe = tegra_dfll_probe,
|
|
.remove = tegra_dfll_remove,
|
|
.driver = {
|
|
.name = DRIVER_NAME,
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = of_match_ptr(tegra_dfll_of_match),
|
|
},
|
|
};
|
|
|
|
static int __init tegra_dfll_driver_init(void)
|
|
{
|
|
return platform_driver_register(&tegra_dfll_driver);
|
|
}
|
|
|
|
static void __exit tegra_dfll_driver_exit(void)
|
|
{
|
|
platform_driver_unregister(&tegra_dfll_driver);
|
|
}
|
|
|
|
fs_initcall(tegra_dfll_driver_init);
|
|
module_exit(tegra_dfll_driver_exit);
|
|
|
|
MODULE_DESCRIPTION("Tegra DFLL clock source driver");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS("platform:" DRIVER_NAME);
|
|
MODULE_AUTHOR("Aleksandr Frid <afrid@nvidia.com>");
|
|
MODULE_AUTHOR("Paul Walmsley <pwalmsley@nvidia.com>");
|