diff --git a/Documentation/devicetree/bindings/arm/msm/msm-spm.txt b/Documentation/devicetree/bindings/arm/msm/msm-spm.txt new file mode 100644 index 000000000000..194059c39c68 --- /dev/null +++ b/Documentation/devicetree/bindings/arm/msm/msm-spm.txt @@ -0,0 +1,169 @@ +* MSM Subsystem Power Manager (spm-v2) + +S4 generation of MSMs have SPM hardware blocks to control the Application +Processor Sub-System power. These SPM blocks run individual state machine +to determine what the core (L2 or Krait/Scorpion) would do when the WFI +instruction is executed by the core. The SAW hardware block handles SPM and +AVS functionality for the cores. + +The devicetree representation of the SPM block should be: + +Required properties + +- compatible: "qcom,spm-v2" +- reg: The physical address and the size of the SPM's memory mapped registers +- qcom,cpu: phandle for the CPU that the SPM block is attached to. This field +is required on only for SPMs that control the CPU. This field is not required +for SPMs that control L2/CCI/L3 +- qcom,saw2-ver-reg: The location of the version register +- qcom,name: The name with which a SPM device is identified by the power +management code. + +---------------------------------------------------- +Non-PSCI targets should follow the rules shown below +---------------------------------------------------- +Required properties for only Non-PSCI targets: + +- qcom,saw2-cfg: SAW2 configuration register +- qcom,saw2-spm-ctl: The SPM control register +- qcom,saw2-spm-dly: Provides the values for the SPM delay command in the SPM + sequence + +Optional properties for only Non-PSCI targets +- reg-names: Register names for the physical address required if spm device + has more than one physical addressed to be mapped. Allowed register + names are: "saw-base", "q2s", "hw-flush", "slpreq" +- qcom,saw2-avs-ctl: The AVS control register +- qcom,saw2-avs-hysterisis: The AVS hysterisis register to delay the AVS + controller requests +- qcom,vctl-timeout-us: The timeout value in us to wait for voltage to change + after sending the voltage command to the PMIC +- qcom,saw2-avs-limit: The AVS limit register +- qcom,saw2-avs-dly: The AVS delay register is used to specify the delay values + between AVS controller requests +- qcom,saw2-pmic-data0..7: Specify the pmic data value and the associated FTS + index to send the PMIC data to +- qcom,vctl-port: The PVC (PMIC Virtual Channel) port used for changing + voltage +- qcom,phase-port: The PVC port used for changing the number of phases +- qcom,pfm-port: The PVC port used for enabling PWM/PFM modes +- qcom,cpu-vctl-mask: Mask of cpus, whose voltage the spm device can control. + Depricated: Replaced with cpu-vctl-list when cpu phandles are available. +- qcom,cpu-vctl-list: List of cpu node phandles, whose voltage the spm device + can control. +- qcom,use-qchannel-for-pc: Boolean property to specify if qchannel should be + ignored when entering power collapse. If this property is set qchannel + will not be ignored in power collapse. +- qcom,supports-rpm-hs: Indicates that this SPM instance allow handshake with +RPM processor when executing the sleep command in the SPM sequence. Supported +only on SAW2 v3.0 and above. +- qcom,use-spm-clock-gating: This boolean property is used to indicate that + the SPM needs to be used for clock gating. Using the SPM for clock + gating would result in auto clock gating being disabled. Use this on + targets that do not support or do not use auto clock gating. +- qcom,use-qchannel-for-wfi: This boolean property is used to indicate + that the SPM gets triggerd by the qchannel and not by means of + wfi. So a wfe could trigger a spm for clock gating as well. +- modes: Lists all the available low power modes for the device + +Second level properties for modes + +Required properties (if modes node is available) +- qcom,label: Specifies the mode name such as: + qcom,saw2-spm-cmd-wfi: WFI mode + qcom,saw2-spm-cmd-ret: Retention mode + qcom,saw2-spm-cmd-spc: Standalone PC mode + qcom,saw2-spm-cmd-pc: Power Collapse mode + qcom,saw2-spm-cmd-gdhs: GDHS mode +- qcom,sequence: Specifies sequence for the low power mode +Optional properties +- qcom,pc_mode: Specifies pc_mode bit should be set in the SPM control register +- qcom,ret_mode: Specifies ret_mode bit should be set in the SPM control register +- qcom,spm_en: Specifies spm_en bit should be set in the SPM control register +- qcom,isar: Specifies isar bit should be set in the SPM control register + Specify this property only if SPM should retain its start address at + the end of the program. +- qcom,slp_cmd_mode: Specifies slp_cmd_mode bit should be set in SPM control register. + Adding this property results in SPM handshaking with RPM. Please remove + the RPM handshake command from the sleep sequence, replace that with + Sleep without RPM handshake command. +- qcom,event_sync: Specifies event_sync byte should be set in SPM control + register. + +---------------------------------------------------- +PSCI targets should follow the rules shown below +---------------------------------------------------- +Optional properties for only PSCI targets: + +- qcom,saw2-avs-ctl: The AVS control register +- qcom,saw2-avs-hysterisis: The AVS hysterisis register to delay the AVS + controller requests +- qcom,vctl-timeout-us: The timeout value in us to wait for voltage to change + after sending the voltage command to the PMIC +- qcom,saw2-avs-limit: The AVS limit register +- qcom,saw2-avs-dly: The AVS delay register is used to specify the delay values + between AVS controller requests +- qcom,vctl-port: The PVC (PMIC Virtual Channel) port used for changing + voltage +- qcom,phase-port: The PVC port used for changing the number of phases +- qcom,pfm-port: The PVC port used for enabling PWM/PFM modes +- qcom,cpu-vctl-list: List of cpu node phandles, whose voltage the spm device + can control. + + +Example 1: + qcom,spm@f9089000 { + compatible = "qcom,spm-v2"; + #address-cells = <1>; + #size-cells = <1>; + reg = <0xf9089000 0x1000>; + qcom,cpu = <&CPU0>; + qcom,saw2-ver-reg = <0xfd0>; + qcom,saw2-cfg = <0x1b>; + qcom,saw2-avs-ctl = <0>; + qcom,saw2-avs-hysteresis = <0>; + qcom,saw2-avs-limit = <0>; + qcom,saw2-avs-dly= <0>; + qcom,saw2-spm-dly= <0x20000400>; + qcom,saw2-spm-ctl = <0x1>; + qcom,cpu-vctl-list = <&CPU0 &CPU1 &CPU2 &CPU3>; + qcom,mode0 { + qcom,label = "qcom,saw2-spm-cmd-wfi"; + qcom,sequence = [03 0b 0f]; + qcom,spm_en; + }; + + qcom,mode1 { + qcom,label = "qcom,saw2-spm-cmd-spc"; + qcom,sequence = [00 20 50 80 60 70 10 92 + a0 b0 03 68 70 3b 92 a0 b0 + 82 2b 50 10 30 02 22 30 0f]; + qcom,spm_en; + qcom,pc_mode; + }; + + qcom,mode2 { + qcom,label = "qcom,saw2-spm-cmd-pc"; + qcom,sequence = [00 20 10 92 a0 b0 07 3b 92 + a0 b0 82 10 30 02 22 30 0f]; + qcom,spm_en; + qcom,pc_mode; + }; + }; + +Example 2: + qcom,spm@9A10000 { + compatible = "qcom,spm-v2"; + #address-cells = <1>; + #size-cells = <1>; + reg = <0x9A10000 0x1000>; + qcom,name = "system-cbf"; /* CBF SAW */ + qcom,saw2-ver-reg = <0xFD0>; + qcom,cpu-vctl-list = <&CPU0 &CPU1 &CPU2 &CPU3>; + qcom,vctl-timeout-us = <50>; + qcom,vctl-port = <0x0>; + qcom,phase-port = <0x1>; + qcom,saw2-avs-ctl = <0x1100>; + qcom,pfm-port = <0x2>; +}; + diff --git a/arch/arm64/configs/msm_defconfig b/arch/arm64/configs/msm_defconfig index 5d229d82849a..d16d4888b29a 100644 --- a/arch/arm64/configs/msm_defconfig +++ b/arch/arm64/configs/msm_defconfig @@ -156,6 +156,7 @@ CONFIG_MSM_GLINK_LOOPBACK_SERVER=y CONFIG_MSM_GLINK_SMD_XPRT=y CONFIG_MSM_GLINK_SMEM_NATIVE_XPRT=y CONFIG_MSM_RPM_SMD=y +CONFIG_MSM_SPM=y CONFIG_QCOM_SCM_XPU=y CONFIG_QCOM_SCM=y CONFIG_QCOM_WATCHDOG_V2=y diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig index 9458a2ff8ba7..b022f07e4165 100644 --- a/drivers/soc/qcom/Kconfig +++ b/drivers/soc/qcom/Kconfig @@ -128,6 +128,14 @@ config QCOM_SMD_RPM Say M here if you want to include support for the Qualcomm RPM as a module. This will build a module called "qcom-smd-rpm". +config MSM_SPM + bool "Driver support for SPM and AVS wrapper hardware" + help + Enables the support SAW and AVS wrapper hardware on MSMs SPM + hardware is used to manage the processor power during sleep. The + driver allows configuring SPM to allow different low power modes for + both core and L2. + config QCOM_SCM bool "Secure Channel Manager (SCM) support" default n diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile index b18c03ae8bf9..353a9dabac57 100644 --- a/drivers/soc/qcom/Makefile +++ b/drivers/soc/qcom/Makefile @@ -13,6 +13,7 @@ obj-$(CONFIG_QCOM_PM) += spm.o obj-$(CONFIG_QCOM_SMD) += smd.o obj-$(CONFIG_QCOM_SMD_RPM) += smd-rpm.o obj-$(CONFIG_QCOM_SMEM) += smem.o +obj-$(CONFIG_MSM_SPM) += msm-spm.o spm_devices.o CFLAGS_scm.o :=$(call as-instr,.arch_extension sec,-DREQUIRES_SEC=1) obj-$(CONFIG_QCOM_SCM_ERRATA) += scm-errata.o diff --git a/drivers/soc/qcom/msm-spm.c b/drivers/soc/qcom/msm-spm.c new file mode 100644 index 000000000000..4a42d9a213f9 --- /dev/null +++ b/drivers/soc/qcom/msm-spm.c @@ -0,0 +1,682 @@ +/* Copyright (c) 2011-2016, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only 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. + * + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/slab.h> + +#include "spm_driver.h" + +#define MSM_SPM_PMIC_STATE_IDLE 0 + +enum { + MSM_SPM_DEBUG_SHADOW = 1U << 0, + MSM_SPM_DEBUG_VCTL = 1U << 1, +}; + +static int msm_spm_debug_mask; +module_param_named( + debug_mask, msm_spm_debug_mask, int, S_IRUGO | S_IWUSR | S_IWGRP +); + +struct saw2_data { + const char *ver_name; + uint32_t major; + uint32_t minor; + uint32_t *spm_reg_offset_ptr; +}; + +static uint32_t msm_spm_reg_offsets_saw2_v2_1[MSM_SPM_REG_NR] = { + [MSM_SPM_REG_SAW_SECURE] = 0x00, + [MSM_SPM_REG_SAW_ID] = 0x04, + [MSM_SPM_REG_SAW_CFG] = 0x08, + [MSM_SPM_REG_SAW_SPM_STS] = 0x0C, + [MSM_SPM_REG_SAW_AVS_STS] = 0x10, + [MSM_SPM_REG_SAW_PMIC_STS] = 0x14, + [MSM_SPM_REG_SAW_RST] = 0x18, + [MSM_SPM_REG_SAW_VCTL] = 0x1C, + [MSM_SPM_REG_SAW_AVS_CTL] = 0x20, + [MSM_SPM_REG_SAW_AVS_LIMIT] = 0x24, + [MSM_SPM_REG_SAW_AVS_DLY] = 0x28, + [MSM_SPM_REG_SAW_AVS_HYSTERESIS] = 0x2C, + [MSM_SPM_REG_SAW_SPM_CTL] = 0x30, + [MSM_SPM_REG_SAW_SPM_DLY] = 0x34, + [MSM_SPM_REG_SAW_PMIC_DATA_0] = 0x40, + [MSM_SPM_REG_SAW_PMIC_DATA_1] = 0x44, + [MSM_SPM_REG_SAW_PMIC_DATA_2] = 0x48, + [MSM_SPM_REG_SAW_PMIC_DATA_3] = 0x4C, + [MSM_SPM_REG_SAW_PMIC_DATA_4] = 0x50, + [MSM_SPM_REG_SAW_PMIC_DATA_5] = 0x54, + [MSM_SPM_REG_SAW_PMIC_DATA_6] = 0x58, + [MSM_SPM_REG_SAW_PMIC_DATA_7] = 0x5C, + [MSM_SPM_REG_SAW_SEQ_ENTRY] = 0x80, + [MSM_SPM_REG_SAW_VERSION] = 0xFD0, +}; + +static uint32_t msm_spm_reg_offsets_saw2_v3_0[MSM_SPM_REG_NR] = { + [MSM_SPM_REG_SAW_SECURE] = 0x00, + [MSM_SPM_REG_SAW_ID] = 0x04, + [MSM_SPM_REG_SAW_CFG] = 0x08, + [MSM_SPM_REG_SAW_SPM_STS] = 0x0C, + [MSM_SPM_REG_SAW_AVS_STS] = 0x10, + [MSM_SPM_REG_SAW_PMIC_STS] = 0x14, + [MSM_SPM_REG_SAW_RST] = 0x18, + [MSM_SPM_REG_SAW_VCTL] = 0x1C, + [MSM_SPM_REG_SAW_AVS_CTL] = 0x20, + [MSM_SPM_REG_SAW_AVS_LIMIT] = 0x24, + [MSM_SPM_REG_SAW_AVS_DLY] = 0x28, + [MSM_SPM_REG_SAW_AVS_HYSTERESIS] = 0x2C, + [MSM_SPM_REG_SAW_SPM_CTL] = 0x30, + [MSM_SPM_REG_SAW_SPM_DLY] = 0x34, + [MSM_SPM_REG_SAW_STS2] = 0x38, + [MSM_SPM_REG_SAW_PMIC_DATA_0] = 0x40, + [MSM_SPM_REG_SAW_PMIC_DATA_1] = 0x44, + [MSM_SPM_REG_SAW_PMIC_DATA_2] = 0x48, + [MSM_SPM_REG_SAW_PMIC_DATA_3] = 0x4C, + [MSM_SPM_REG_SAW_PMIC_DATA_4] = 0x50, + [MSM_SPM_REG_SAW_PMIC_DATA_5] = 0x54, + [MSM_SPM_REG_SAW_PMIC_DATA_6] = 0x58, + [MSM_SPM_REG_SAW_PMIC_DATA_7] = 0x5C, + [MSM_SPM_REG_SAW_SEQ_ENTRY] = 0x400, + [MSM_SPM_REG_SAW_VERSION] = 0xFD0, +}; + +static struct saw2_data saw2_info[] = { + [0] = { + "SAW_v2.1", + 0x2, + 0x1, + msm_spm_reg_offsets_saw2_v2_1, + }, + [1] = { + "SAW_v2.3", + 0x3, + 0x0, + msm_spm_reg_offsets_saw2_v3_0, + }, + [2] = { + "SAW_v3.0", + 0x1, + 0x0, + msm_spm_reg_offsets_saw2_v3_0, + }, +}; + +static uint32_t num_pmic_data; + +static void msm_spm_drv_flush_shadow(struct msm_spm_driver_data *dev, + unsigned int reg_index) +{ + BUG_ON(!dev); + + BUG_ON(!dev->reg_shadow); + + __raw_writel(dev->reg_shadow[reg_index], + dev->reg_base_addr + dev->reg_offsets[reg_index]); +} + +static void msm_spm_drv_load_shadow(struct msm_spm_driver_data *dev, + unsigned int reg_index) +{ + dev->reg_shadow[reg_index] = + __raw_readl(dev->reg_base_addr + + dev->reg_offsets[reg_index]); +} + +static inline uint32_t msm_spm_drv_get_num_spm_entry( + struct msm_spm_driver_data *dev) +{ + BUG_ON(!dev); + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_ID); + return (dev->reg_shadow[MSM_SPM_REG_SAW_ID] >> 24) & 0xFF; +} + +static inline void msm_spm_drv_set_start_addr( + struct msm_spm_driver_data *dev, uint32_t ctl) +{ + dev->reg_shadow[MSM_SPM_REG_SAW_SPM_CTL] = ctl; +} + +static inline bool msm_spm_pmic_arb_present(struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_ID); + return (dev->reg_shadow[MSM_SPM_REG_SAW_ID] >> 2) & 0x1; +} + +static inline void msm_spm_drv_set_vctl2(struct msm_spm_driver_data *dev, + uint32_t vlevel) +{ + unsigned int pmic_data = 0; + + /** + * VCTL_PORT has to be 0, for PMIC_STS register to be updated. + * Ensure that vctl_port is always set to 0. + */ + WARN_ON(dev->vctl_port); + + pmic_data |= vlevel; + pmic_data |= (dev->vctl_port & 0x7) << 16; + + dev->reg_shadow[MSM_SPM_REG_SAW_VCTL] &= ~0x700FF; + dev->reg_shadow[MSM_SPM_REG_SAW_VCTL] |= pmic_data; + + dev->reg_shadow[MSM_SPM_REG_SAW_PMIC_DATA_3] &= ~0x700FF; + dev->reg_shadow[MSM_SPM_REG_SAW_PMIC_DATA_3] |= pmic_data; + + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_VCTL); + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_PMIC_DATA_3); +} + +static inline uint32_t msm_spm_drv_get_num_pmic_data( + struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_ID); + mb(); + return (dev->reg_shadow[MSM_SPM_REG_SAW_ID] >> 4) & 0x7; +} + +static inline uint32_t msm_spm_drv_get_sts_pmic_state( + struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_PMIC_STS); + return (dev->reg_shadow[MSM_SPM_REG_SAW_PMIC_STS] >> 16) & + 0x03; +} + +uint32_t msm_spm_drv_get_sts_curr_pmic_data( + struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_PMIC_STS); + return dev->reg_shadow[MSM_SPM_REG_SAW_PMIC_STS] & 0xFF; +} + +static inline void msm_spm_drv_get_saw2_ver(struct msm_spm_driver_data *dev, + uint32_t *major, uint32_t *minor) +{ + uint32_t val = 0; + + dev->reg_shadow[MSM_SPM_REG_SAW_VERSION] = + __raw_readl(dev->reg_base_addr + dev->ver_reg); + + val = dev->reg_shadow[MSM_SPM_REG_SAW_VERSION]; + + *major = (val >> 28) & 0xF; + *minor = (val >> 16) & 0xFFF; +} + +inline int msm_spm_drv_set_spm_enable( + struct msm_spm_driver_data *dev, bool enable) +{ + uint32_t value = enable ? 0x01 : 0x00; + + if (!dev) + return -EINVAL; + + if ((dev->reg_shadow[MSM_SPM_REG_SAW_SPM_CTL] & 0x01) ^ value) { + + dev->reg_shadow[MSM_SPM_REG_SAW_SPM_CTL] &= ~0x1; + dev->reg_shadow[MSM_SPM_REG_SAW_SPM_CTL] |= value; + + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_SPM_CTL); + wmb(); + } + return 0; +} + +int msm_spm_drv_get_avs_enable(struct msm_spm_driver_data *dev) +{ + if (!dev) + return -EINVAL; + + return dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] & 0x01; +} + +int msm_spm_drv_set_avs_enable(struct msm_spm_driver_data *dev, + bool enable) +{ + uint32_t value = enable ? 0x1 : 0x0; + + if (!dev) + return -EINVAL; + + if ((dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] & 0x1) ^ value) { + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] &= ~0x1; + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] |= value; + + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); + } + + return 0; +} + +int msm_spm_drv_set_avs_limit(struct msm_spm_driver_data *dev, + uint32_t min_lvl, uint32_t max_lvl) +{ + uint32_t value = (max_lvl & 0xff) << 16 | (min_lvl & 0xff); + + if (!dev) + return -EINVAL; + + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_LIMIT] = value; + + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_LIMIT); + + return 0; +} + +static int msm_spm_drv_avs_irq_mask(enum msm_spm_avs_irq irq) +{ + switch (irq) { + case MSM_SPM_AVS_IRQ_MIN: + return BIT(1); + case MSM_SPM_AVS_IRQ_MAX: + return BIT(2); + default: + return -EINVAL; + } +} + +int msm_spm_drv_set_avs_irq_enable(struct msm_spm_driver_data *dev, + enum msm_spm_avs_irq irq, bool enable) +{ + int mask = msm_spm_drv_avs_irq_mask(irq); + uint32_t value; + + if (!dev) + return -EINVAL; + else if (mask < 0) + return mask; + + value = enable ? mask : 0; + + if ((dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] & mask) ^ value) { + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] &= ~mask; + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] |= value; + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); + } + + return 0; +} + +int msm_spm_drv_avs_clear_irq(struct msm_spm_driver_data *dev, + enum msm_spm_avs_irq irq) +{ + int mask = msm_spm_drv_avs_irq_mask(irq); + + if (!dev) + return -EINVAL; + else if (mask < 0) + return mask; + + if (dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] & mask) { + /* + * The interrupt status is cleared by disabling and then + * re-enabling the interrupt. + */ + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] &= ~mask; + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] |= mask; + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); + } + + return 0; +} + +void msm_spm_drv_flush_seq_entry(struct msm_spm_driver_data *dev) +{ + int i; + int num_spm_entry = msm_spm_drv_get_num_spm_entry(dev); + + if (!dev) { + __WARN(); + return; + } + + for (i = 0; i < num_spm_entry; i++) { + __raw_writel(dev->reg_seq_entry_shadow[i], + dev->reg_base_addr + + dev->reg_offsets[MSM_SPM_REG_SAW_SEQ_ENTRY] + + 4 * i); + } + mb(); +} + +void dump_regs(struct msm_spm_driver_data *dev, int cpu) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_SPM_STS); + mb(); + pr_err("CPU%d: spm register MSM_SPM_REG_SAW_SPM_STS: 0x%x\n", cpu, + dev->reg_shadow[MSM_SPM_REG_SAW_SPM_STS]); + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_SPM_CTL); + mb(); + pr_err("CPU%d: spm register MSM_SPM_REG_SAW_SPM_CTL: 0x%x\n", cpu, + dev->reg_shadow[MSM_SPM_REG_SAW_SPM_CTL]); +} + +int msm_spm_drv_write_seq_data(struct msm_spm_driver_data *dev, + uint8_t *cmd, uint32_t *offset) +{ + uint32_t cmd_w; + uint32_t offset_w = *offset / 4; + uint8_t last_cmd; + + if (!cmd) + return -EINVAL; + + while (1) { + int i; + cmd_w = 0; + last_cmd = 0; + cmd_w = dev->reg_seq_entry_shadow[offset_w]; + + for (i = (*offset % 4); i < 4; i++) { + last_cmd = *(cmd++); + cmd_w |= last_cmd << (i * 8); + (*offset)++; + if (last_cmd == 0x0f) + break; + } + + dev->reg_seq_entry_shadow[offset_w++] = cmd_w; + if (last_cmd == 0x0f) + break; + } + + return 0; +} + +int msm_spm_drv_set_low_power_mode(struct msm_spm_driver_data *dev, + uint32_t ctl) +{ + + /* SPM is configured to reset start address to zero after end of Program + */ + if (!dev) + return -EINVAL; + + msm_spm_drv_set_start_addr(dev, ctl); + + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_SPM_CTL); + wmb(); + + if (msm_spm_debug_mask & MSM_SPM_DEBUG_SHADOW) { + int i; + for (i = 0; i < MSM_SPM_REG_NR; i++) + pr_info("%s: reg %02x = 0x%08x\n", __func__, + dev->reg_offsets[i], dev->reg_shadow[i]); + } + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_SPM_STS); + + return 0; +} + +uint32_t msm_spm_drv_get_vdd(struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_PMIC_STS); + return dev->reg_shadow[MSM_SPM_REG_SAW_PMIC_STS] & 0xFF; +} + +#ifdef CONFIG_MSM_AVS_HW +static bool msm_spm_drv_is_avs_enabled(struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); + return dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] & BIT(0); +} + +static void msm_spm_drv_disable_avs(struct msm_spm_driver_data *dev) +{ + msm_spm_drv_load_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] &= ~BIT(27); + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); +} + +static void msm_spm_drv_enable_avs(struct msm_spm_driver_data *dev) +{ + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] |= BIT(27); + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); +} + +static void msm_spm_drv_set_avs_vlevel(struct msm_spm_driver_data *dev, + unsigned int vlevel) +{ + vlevel &= 0x3f; + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] &= ~0x7efc00; + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] |= ((vlevel - 4) << 10); + dev->reg_shadow[MSM_SPM_REG_SAW_AVS_CTL] |= (vlevel << 17); + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_AVS_CTL); +} + +#else +static bool msm_spm_drv_is_avs_enabled(struct msm_spm_driver_data *dev) +{ + return false; +} + +static void msm_spm_drv_disable_avs(struct msm_spm_driver_data *dev) { } + +static void msm_spm_drv_enable_avs(struct msm_spm_driver_data *dev) { } + +static void msm_spm_drv_set_avs_vlevel(struct msm_spm_driver_data *dev, + unsigned int vlevel) { } +#endif + +int msm_spm_drv_set_vdd(struct msm_spm_driver_data *dev, unsigned int vlevel) +{ + uint32_t timeout_us, new_level; + bool avs_enabled; + + if (!dev) + return -EINVAL; + + avs_enabled = msm_spm_drv_is_avs_enabled(dev); + + if (!msm_spm_pmic_arb_present(dev)) + return -ENOSYS; + + if (msm_spm_debug_mask & MSM_SPM_DEBUG_VCTL) + pr_info("%s: requesting vlevel %#x\n", __func__, vlevel); + + if (avs_enabled) + msm_spm_drv_disable_avs(dev); + + /* Kick the state machine back to idle */ + dev->reg_shadow[MSM_SPM_REG_SAW_RST] = 1; + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_RST); + + msm_spm_drv_set_vctl2(dev, vlevel); + + timeout_us = dev->vctl_timeout_us; + /* Confirm the voltage we set was what hardware sent */ + do { + new_level = msm_spm_drv_get_sts_curr_pmic_data(dev); + if (new_level == vlevel) + break; + udelay(1); + } while (--timeout_us); + if (!timeout_us) { + pr_info("Wrong level %#x\n", new_level); + goto set_vdd_bail; + } + + if (msm_spm_debug_mask & MSM_SPM_DEBUG_VCTL) + pr_info("%s: done, remaining timeout %u us\n", + __func__, timeout_us); + + /* Set AVS min/max */ + if (avs_enabled) { + msm_spm_drv_set_avs_vlevel(dev, vlevel); + msm_spm_drv_enable_avs(dev); + } + + return 0; + +set_vdd_bail: + if (avs_enabled) + msm_spm_drv_enable_avs(dev); + + pr_err("%s: failed %#x, remaining timeout %uus, vlevel %#x\n", + __func__, vlevel, timeout_us, new_level); + return -EIO; +} + +static int msm_spm_drv_get_pmic_port(struct msm_spm_driver_data *dev, + enum msm_spm_pmic_port port) +{ + int index = -1; + + switch (port) { + case MSM_SPM_PMIC_VCTL_PORT: + index = dev->vctl_port; + break; + case MSM_SPM_PMIC_PHASE_PORT: + index = dev->phase_port; + break; + case MSM_SPM_PMIC_PFM_PORT: + index = dev->pfm_port; + break; + default: + break; + } + + return index; +} + +int msm_spm_drv_set_pmic_data(struct msm_spm_driver_data *dev, + enum msm_spm_pmic_port port, unsigned int data) +{ + unsigned int pmic_data = 0; + unsigned int timeout_us = 0; + int index = 0; + + if (!msm_spm_pmic_arb_present(dev)) + return -ENOSYS; + + index = msm_spm_drv_get_pmic_port(dev, port); + if (index < 0) + return -ENODEV; + + pmic_data |= data & 0xFF; + pmic_data |= (index & 0x7) << 16; + + dev->reg_shadow[MSM_SPM_REG_SAW_VCTL] &= ~0x700FF; + dev->reg_shadow[MSM_SPM_REG_SAW_VCTL] |= pmic_data; + msm_spm_drv_flush_shadow(dev, MSM_SPM_REG_SAW_VCTL); + mb(); + + timeout_us = dev->vctl_timeout_us; + /** + * Confirm the pmic data set was what hardware sent by + * checking the PMIC FSM state. + * We cannot use the sts_pmic_data and check it against + * the value like we do fot set_vdd, since the PMIC_STS + * is only updated for SAW_VCTL sent with port index 0. + */ + do { + if (msm_spm_drv_get_sts_pmic_state(dev) == + MSM_SPM_PMIC_STATE_IDLE) + break; + udelay(1); + } while (--timeout_us); + + if (!timeout_us) { + pr_err("%s: failed, remaining timeout %u us, data %d\n", + __func__, timeout_us, data); + return -EIO; + } + + return 0; +} + +void msm_spm_drv_reinit(struct msm_spm_driver_data *dev, bool seq_write) +{ + int i; + + if (seq_write) + msm_spm_drv_flush_seq_entry(dev); + + for (i = 0; i < MSM_SPM_REG_SAW_PMIC_DATA_0 + num_pmic_data; i++) + msm_spm_drv_load_shadow(dev, i); + + for (i = MSM_SPM_REG_NR_INITIALIZE + 1; i < MSM_SPM_REG_NR; i++) + msm_spm_drv_load_shadow(dev, i); +} + +int msm_spm_drv_reg_init(struct msm_spm_driver_data *dev, + struct msm_spm_platform_data *data) +{ + int i; + bool found = false; + + dev->ver_reg = data->ver_reg; + dev->reg_base_addr = data->reg_base_addr; + msm_spm_drv_get_saw2_ver(dev, &dev->major, &dev->minor); + for (i = 0; i < ARRAY_SIZE(saw2_info); i++) + if (dev->major == saw2_info[i].major && + dev->minor == saw2_info[i].minor) { + pr_debug("%s: Version found\n", + saw2_info[i].ver_name); + dev->reg_offsets = saw2_info[i].spm_reg_offset_ptr; + found = true; + break; + } + + if (!found) { + pr_err("%s: No SAW version found\n", __func__); + BUG_ON(!found); + } + return 0; +} + +void msm_spm_drv_upd_reg_shadow(struct msm_spm_driver_data *dev, int id, + int val) +{ + dev->reg_shadow[id] = val; + msm_spm_drv_flush_shadow(dev, id); + /* Complete the above writes before other accesses */ + mb(); +} + +int msm_spm_drv_init(struct msm_spm_driver_data *dev, + struct msm_spm_platform_data *data) +{ + int num_spm_entry; + + BUG_ON(!dev || !data); + + dev->vctl_port = data->vctl_port; + dev->phase_port = data->phase_port; + dev->pfm_port = data->pfm_port; + dev->reg_base_addr = data->reg_base_addr; + memcpy(dev->reg_shadow, data->reg_init_values, + sizeof(data->reg_init_values)); + + dev->vctl_timeout_us = data->vctl_timeout_us; + + + if (!num_pmic_data) + num_pmic_data = msm_spm_drv_get_num_pmic_data(dev); + + num_spm_entry = msm_spm_drv_get_num_spm_entry(dev); + + dev->reg_seq_entry_shadow = + kzalloc(sizeof(*dev->reg_seq_entry_shadow) * num_spm_entry, + GFP_KERNEL); + + if (!dev->reg_seq_entry_shadow) + return -ENOMEM; + + return 0; +} diff --git a/drivers/soc/qcom/spm_devices.c b/drivers/soc/qcom/spm_devices.c new file mode 100644 index 000000000000..31f015106bf9 --- /dev/null +++ b/drivers/soc/qcom/spm_devices.c @@ -0,0 +1,985 @@ +/* Copyright (c) 2011-2016, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only 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. + * + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/slab.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/cpu.h> +#include <soc/qcom/spm.h> +#include "spm_driver.h" + +#define VDD_DEFAULT 0xDEADF00D +#define SLP_CMD_BIT 17 +#define PC_MODE_BIT 16 +#define RET_MODE_BIT 15 +#define EVENT_SYNC_BIT 24 +#define ISAR_BIT 3 +#define SPM_EN_BIT 0 + +struct msm_spm_power_modes { + uint32_t mode; + uint32_t ctl; +}; + +struct msm_spm_device { + struct list_head list; + bool initialized; + const char *name; + struct msm_spm_driver_data reg_data; + struct msm_spm_power_modes *modes; + uint32_t num_modes; + uint32_t cpu_vdd; + struct cpumask mask; + void __iomem *q2s_reg; + bool qchannel_ignore; + bool allow_rpm_hs; + bool use_spm_clk_gating; + bool use_qchannel_for_wfi; + void __iomem *flush_base_addr; + void __iomem *slpreq_base_addr; +}; + +struct msm_spm_vdd_info { + struct msm_spm_device *vctl_dev; + uint32_t vlevel; + int err; +}; + +static LIST_HEAD(spm_list); +static DEFINE_PER_CPU_SHARED_ALIGNED(struct msm_spm_device, msm_cpu_spm_device); +static DEFINE_PER_CPU(struct msm_spm_device *, cpu_vctl_device); + +static void msm_spm_smp_set_vdd(void *data) +{ + struct msm_spm_vdd_info *info = (struct msm_spm_vdd_info *)data; + struct msm_spm_device *dev = info->vctl_dev; + + dev->cpu_vdd = info->vlevel; + info->err = msm_spm_drv_set_vdd(&dev->reg_data, info->vlevel); +} + +/** + * msm_spm_probe_done(): Verify and return the status of the cpu(s) and l2 + * probe. + * Return: 0 if all spm devices have been probed, else return -EPROBE_DEFER. + * if probe failed, then return the err number for that failure. + */ +int msm_spm_probe_done(void) +{ + struct msm_spm_device *dev; + int cpu; + int ret = 0; + + for_each_possible_cpu(cpu) { + dev = per_cpu(cpu_vctl_device, cpu); + if (!dev) + return -EPROBE_DEFER; + + ret = IS_ERR(dev); + if (ret) + return ret; + } + + return 0; +} +EXPORT_SYMBOL(msm_spm_probe_done); + +void msm_spm_dump_regs(unsigned int cpu) +{ + dump_regs(&per_cpu(msm_cpu_spm_device, cpu).reg_data, cpu); +} + +/** + * msm_spm_set_vdd(): Set core voltage + * @cpu: core id + * @vlevel: Encoded PMIC data. + * + * Return: 0 on success or -(ERRNO) on failure. + */ +int msm_spm_set_vdd(unsigned int cpu, unsigned int vlevel) +{ + struct msm_spm_vdd_info info; + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + int ret; + + if (!dev) + return -EPROBE_DEFER; + + ret = IS_ERR(dev); + if (ret) + return ret; + + info.vctl_dev = dev; + info.vlevel = vlevel; + + ret = smp_call_function_any(&dev->mask, msm_spm_smp_set_vdd, &info, + true); + if (ret) + return ret; + + return info.err; +} +EXPORT_SYMBOL(msm_spm_set_vdd); + +/** + * msm_spm_get_vdd(): Get core voltage + * @cpu: core id + * @return: Returns encoded PMIC data. + */ +int msm_spm_get_vdd(unsigned int cpu) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -EPROBE_DEFER; + + return msm_spm_drv_get_vdd(&dev->reg_data) ? : -EINVAL; +} +EXPORT_SYMBOL(msm_spm_get_vdd); + +static void msm_spm_config_q2s(struct msm_spm_device *dev, unsigned int mode) +{ + uint32_t spm_legacy_mode = 0; + uint32_t qchannel_ignore = 0; + uint32_t val = 0; + + if (!dev->q2s_reg) + return; + + switch (mode) { + case MSM_SPM_MODE_DISABLED: + case MSM_SPM_MODE_CLOCK_GATING: + qchannel_ignore = !dev->use_qchannel_for_wfi; + spm_legacy_mode = 0; + break; + case MSM_SPM_MODE_RETENTION: + qchannel_ignore = 0; + spm_legacy_mode = 0; + break; + case MSM_SPM_MODE_GDHS: + case MSM_SPM_MODE_STANDALONE_POWER_COLLAPSE: + case MSM_SPM_MODE_POWER_COLLAPSE: + qchannel_ignore = dev->qchannel_ignore; + spm_legacy_mode = 1; + break; + default: + break; + } + + val = spm_legacy_mode << 2 | qchannel_ignore << 1; + __raw_writel(val, dev->q2s_reg); + mb(); +} + +static void msm_spm_config_hw_flush(struct msm_spm_device *dev, + unsigned int mode) +{ + uint32_t val = 0; + + if (!dev->flush_base_addr) + return; + + switch (mode) { + case MSM_SPM_MODE_FASTPC: + case MSM_SPM_MODE_STANDALONE_POWER_COLLAPSE: + case MSM_SPM_MODE_POWER_COLLAPSE: + val = BIT(0); + break; + default: + break; + } + + __raw_writel(val, dev->flush_base_addr); +} + +static void msm_spm_config_slpreq(struct msm_spm_device *dev, + unsigned int mode) +{ + uint32_t val = 0; + + if (!dev->slpreq_base_addr) + return; + + switch (mode) { + case MSM_SPM_MODE_FASTPC: + case MSM_SPM_MODE_GDHS: + case MSM_SPM_MODE_STANDALONE_POWER_COLLAPSE: + case MSM_SPM_MODE_POWER_COLLAPSE: + val = BIT(4); + break; + default: + break; + } + + val = (__raw_readl(dev->slpreq_base_addr) & ~BIT(4)) | val; + __raw_writel(val, dev->slpreq_base_addr); +} + +static int msm_spm_dev_set_low_power_mode(struct msm_spm_device *dev, + unsigned int mode, bool notify_rpm) +{ + uint32_t i; + int ret = -EINVAL; + uint32_t ctl; + + if (!dev) { + pr_err("dev is NULL\n"); + return -ENODEV; + } + + if (!dev->initialized) + return -ENXIO; + + if (!dev->num_modes) + return 0; + + if (mode == MSM_SPM_MODE_DISABLED) { + ret = msm_spm_drv_set_spm_enable(&dev->reg_data, false); + } else if (!msm_spm_drv_set_spm_enable(&dev->reg_data, true)) { + for (i = 0; i < dev->num_modes; i++) { + if (dev->modes[i].mode != mode) + continue; + + ctl = dev->modes[i].ctl; + if (!dev->allow_rpm_hs && notify_rpm) + ctl &= ~BIT(SLP_CMD_BIT); + + break; + } + ret = msm_spm_drv_set_low_power_mode(&dev->reg_data, ctl); + } + + msm_spm_config_q2s(dev, mode); + msm_spm_config_hw_flush(dev, mode); + msm_spm_config_slpreq(dev, mode); + + return ret; +} + +static int msm_spm_dev_init(struct msm_spm_device *dev, + struct msm_spm_platform_data *data) +{ + int i, ret = -ENOMEM; + uint32_t offset = 0; + + dev->cpu_vdd = VDD_DEFAULT; + dev->num_modes = data->num_modes; + dev->modes = kmalloc( + sizeof(struct msm_spm_power_modes) * dev->num_modes, + GFP_KERNEL); + + if (!dev->modes) + goto spm_failed_malloc; + + ret = msm_spm_drv_init(&dev->reg_data, data); + + if (ret) + goto spm_failed_init; + + for (i = 0; i < dev->num_modes; i++) { + + /* Default offset is 0 and gets updated as we write more + * sequences into SPM + */ + dev->modes[i].ctl = data->modes[i].ctl | ((offset & 0x1FF) + << 4); + ret = msm_spm_drv_write_seq_data(&dev->reg_data, + data->modes[i].cmd, &offset); + if (ret < 0) + goto spm_failed_init; + + dev->modes[i].mode = data->modes[i].mode; + } + + msm_spm_drv_reinit(&dev->reg_data, dev->num_modes ? true : false); + + dev->initialized = true; + + return 0; + +spm_failed_init: + kfree(dev->modes); +spm_failed_malloc: + return ret; +} + +/** + * msm_spm_turn_on_cpu_rail(): Power on cpu rail before turning on core + * @node: The SPM node that controls the voltage for the CPU + * @val: The value to be set on the rail + * @cpu: The cpu for this with rail is being powered on + */ +int msm_spm_turn_on_cpu_rail(struct device_node *vctl_node, + unsigned int val, int cpu, int vctl_offset) +{ + uint32_t timeout = 2000; /* delay for voltage to settle on the core */ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + void __iomem *base; + + base = of_iomap(vctl_node, 1); + if (base) { + /* + * Program Q2S to disable SPM legacy mode and ignore Q2S + * channel requests. + * bit[1] = qchannel_ignore = 1 + * bit[2] = spm_legacy_mode = 0 + */ + writel_relaxed(0x2, base); + mb(); + iounmap(base); + } + + base = of_iomap(vctl_node, 0); + if (!base) + return -ENOMEM; + + if (dev && (dev->cpu_vdd != VDD_DEFAULT)) + return 0; + + /* Set the CPU supply regulator voltage */ + val = (val & 0xFF); + writel_relaxed(val, base + vctl_offset); + mb(); + udelay(timeout); + + /* Enable the CPU supply regulator*/ + val = 0x30080; + writel_relaxed(val, base + vctl_offset); + mb(); + udelay(timeout); + + iounmap(base); + + return 0; +} +EXPORT_SYMBOL(msm_spm_turn_on_cpu_rail); + +void msm_spm_reinit(void) +{ + unsigned int cpu; + for_each_possible_cpu(cpu) + msm_spm_drv_reinit( + &per_cpu(msm_cpu_spm_device.reg_data, cpu), true); +} +EXPORT_SYMBOL(msm_spm_reinit); + +/* + * msm_spm_is_mode_avail() - Specifies if a mode is available for the cpu + * It should only be used to decide a mode before lpm driver is probed. + * @mode: SPM LPM mode to be selected + */ +bool msm_spm_is_mode_avail(unsigned int mode) +{ + struct msm_spm_device *dev = &__get_cpu_var(msm_cpu_spm_device); + int i; + + for (i = 0; i < dev->num_modes; i++) { + if (dev->modes[i].mode == mode) + return true; + } + + return false; +} + +/** + * msm_spm_is_avs_enabled() - Functions returns 1 if AVS is enabled and + * 0 if it is not. + * @cpu: specifies cpu's avs should be read + * + * Returns errno in case of failure or AVS enable state otherwise + */ +int msm_spm_is_avs_enabled(unsigned int cpu) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_get_avs_enable(&dev->reg_data); +} +EXPORT_SYMBOL(msm_spm_is_avs_enabled); + +/** + * msm_spm_avs_enable() - Enables AVS on the SAW that controls this cpu's + * voltage. + * @cpu: specifies which cpu's avs should be enabled + * + * Returns errno in case of failure or 0 if successful + */ +int msm_spm_avs_enable(unsigned int cpu) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_avs_enable(&dev->reg_data, true); +} +EXPORT_SYMBOL(msm_spm_avs_enable); + +/** + * msm_spm_avs_disable() - Disables AVS on the SAW that controls this cpu's + * voltage. + * @cpu: specifies which cpu's avs should be enabled + * + * Returns errno in case of failure or 0 if successful + */ +int msm_spm_avs_disable(unsigned int cpu) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_avs_enable(&dev->reg_data, false); +} +EXPORT_SYMBOL(msm_spm_avs_disable); + +/** + * msm_spm_avs_set_limit() - Set maximum and minimum AVS limits on the + * SAW that controls this cpu's voltage. + * @cpu: specify which cpu's avs should be configured + * @min_lvl: specifies the minimum PMIC output voltage control register + * value that may be sent to the PMIC + * @max_lvl: specifies the maximum PMIC output voltage control register + * value that may be sent to the PMIC + * Returns errno in case of failure or 0 if successful + */ +int msm_spm_avs_set_limit(unsigned int cpu, + uint32_t min_lvl, uint32_t max_lvl) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_avs_limit(&dev->reg_data, min_lvl, max_lvl); +} +EXPORT_SYMBOL(msm_spm_avs_set_limit); + +/** + * msm_spm_avs_enable_irq() - Enable an AVS interrupt + * @cpu: specifies which CPU's AVS should be configured + * @irq: specifies which interrupt to enable + * + * Returns errno in case of failure or 0 if successful. + */ +int msm_spm_avs_enable_irq(unsigned int cpu, enum msm_spm_avs_irq irq) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_avs_irq_enable(&dev->reg_data, irq, true); +} +EXPORT_SYMBOL(msm_spm_avs_enable_irq); + +/** + * msm_spm_avs_disable_irq() - Disable an AVS interrupt + * @cpu: specifies which CPU's AVS should be configured + * @irq: specifies which interrupt to disable + * + * Returns errno in case of failure or 0 if successful. + */ +int msm_spm_avs_disable_irq(unsigned int cpu, enum msm_spm_avs_irq irq) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_avs_irq_enable(&dev->reg_data, irq, false); +} +EXPORT_SYMBOL(msm_spm_avs_disable_irq); + +/** + * msm_spm_avs_clear_irq() - Clear a latched AVS interrupt + * @cpu: specifies which CPU's AVS should be configured + * @irq: specifies which interrupt to clear + * + * Returns errno in case of failure or 0 if successful. + */ +int msm_spm_avs_clear_irq(unsigned int cpu, enum msm_spm_avs_irq irq) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_avs_clear_irq(&dev->reg_data, irq); +} +EXPORT_SYMBOL(msm_spm_avs_clear_irq); + +/** + * msm_spm_set_low_power_mode() - Configure SPM start address for low power mode + * @mode: SPM LPM mode to enter + * @notify_rpm: Notify RPM in this mode + */ +int msm_spm_set_low_power_mode(unsigned int mode, bool notify_rpm) +{ + struct msm_spm_device *dev = &__get_cpu_var(msm_cpu_spm_device); + return msm_spm_dev_set_low_power_mode(dev, mode, notify_rpm); +} +EXPORT_SYMBOL(msm_spm_set_low_power_mode); + +/** + * msm_spm_init(): Board initalization function + * @data: platform specific SPM register configuration data + * @nr_devs: Number of SPM devices being initialized + */ +int __init msm_spm_init(struct msm_spm_platform_data *data, int nr_devs) +{ + unsigned int cpu; + int ret = 0; + + BUG_ON((nr_devs < num_possible_cpus()) || !data); + + for_each_possible_cpu(cpu) { + struct msm_spm_device *dev = &per_cpu(msm_cpu_spm_device, cpu); + ret = msm_spm_dev_init(dev, &data[cpu]); + if (ret < 0) { + pr_warn("%s():failed CPU:%u ret:%d\n", __func__, + cpu, ret); + break; + } + } + + return ret; +} + +struct msm_spm_device *msm_spm_get_device_by_name(const char *name) +{ + struct list_head *list; + + list_for_each(list, &spm_list) { + struct msm_spm_device *dev + = list_entry(list, typeof(*dev), list); + if (dev->name && !strcmp(dev->name, name)) + return dev; + } + return ERR_PTR(-ENODEV); +} + +int msm_spm_config_low_power_mode(struct msm_spm_device *dev, + unsigned int mode, bool notify_rpm) +{ + return msm_spm_dev_set_low_power_mode(dev, mode, notify_rpm); +} +#ifdef CONFIG_MSM_L2_SPM + +/** + * msm_spm_apcs_set_phase(): Set number of SMPS phases. + * @cpu: cpu which is requesting the change in number of phases. + * @phase_cnt: Number of phases to be set active + */ +int msm_spm_apcs_set_phase(int cpu, unsigned int phase_cnt) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_pmic_data(&dev->reg_data, + MSM_SPM_PMIC_PHASE_PORT, phase_cnt); +} +EXPORT_SYMBOL(msm_spm_apcs_set_phase); + +/** msm_spm_enable_fts_lpm() : Enable FTS to switch to low power + * when the cores are in low power modes + * @cpu: cpu that is entering low power mode. + * @mode: The mode configuration for FTS + */ +int msm_spm_enable_fts_lpm(int cpu, uint32_t mode) +{ + struct msm_spm_device *dev = per_cpu(cpu_vctl_device, cpu); + + if (!dev) + return -ENXIO; + + return msm_spm_drv_set_pmic_data(&dev->reg_data, + MSM_SPM_PMIC_PFM_PORT, mode); +} +EXPORT_SYMBOL(msm_spm_enable_fts_lpm); + +#endif + +static int get_cpu_id(struct device_node *node) +{ + struct device_node *cpu_node; + u32 cpu; + char *key = "qcom,cpu"; + + cpu_node = of_parse_phandle(node, key, 0); + if (cpu_node) { + for_each_possible_cpu(cpu) { + if (of_get_cpu_node(cpu, NULL) == cpu_node) + return cpu; + } + } else + return num_possible_cpus(); + + return -EINVAL; +} + +static struct msm_spm_device *msm_spm_get_device(struct platform_device *pdev) +{ + struct msm_spm_device *dev = NULL; + const char *val = NULL; + char *key = "qcom,name"; + int cpu = get_cpu_id(pdev->dev.of_node); + + if ((cpu >= 0) && cpu < num_possible_cpus()) + dev = &per_cpu(msm_cpu_spm_device, cpu); + else if (cpu == num_possible_cpus()) + dev = devm_kzalloc(&pdev->dev, sizeof(struct msm_spm_device), + GFP_KERNEL); + + if (!dev) + return NULL; + + if (of_property_read_string(pdev->dev.of_node, key, &val)) { + pr_err("%s(): Cannot find a required node key:%s\n", + __func__, key); + return NULL; + } + dev->name = val; + list_add(&dev->list, &spm_list); + + return dev; +} + +static void get_cpumask(struct device_node *node, struct cpumask *mask) +{ + unsigned c; + int idx = 0; + struct device_node *cpu_node; + char *key = "qcom,cpu-vctl-list"; + + cpu_node = of_parse_phandle(node, key, idx++); + while (cpu_node) { + for_each_possible_cpu(c) { + if (of_get_cpu_node(c, NULL) == cpu_node) + cpumask_set_cpu(c, mask); + } + cpu_node = of_parse_phandle(node, key, idx++); + }; +} + +static int msm_spm_dev_probe(struct platform_device *pdev) +{ + int ret = 0; + int cpu = 0; + int i = 0; + struct device_node *node = pdev->dev.of_node; + struct device_node *n = NULL; + struct msm_spm_platform_data spm_data; + char *key = NULL; + uint32_t val = 0; + struct msm_spm_seq_entry modes[MSM_SPM_MODE_NR]; + int len = 0; + struct msm_spm_device *dev = NULL; + struct resource *res = NULL; + uint32_t mode_count = 0; + + struct spm_of { + char *key; + uint32_t id; + }; + + struct spm_of spm_of_data[] = { + {"qcom,saw2-cfg", MSM_SPM_REG_SAW_CFG}, + {"qcom,saw2-avs-ctl", MSM_SPM_REG_SAW_AVS_CTL}, + {"qcom,saw2-avs-hysteresis", MSM_SPM_REG_SAW_AVS_HYSTERESIS}, + {"qcom,saw2-avs-limit", MSM_SPM_REG_SAW_AVS_LIMIT}, + {"qcom,saw2-avs-dly", MSM_SPM_REG_SAW_AVS_DLY}, + {"qcom,saw2-spm-dly", MSM_SPM_REG_SAW_SPM_DLY}, + {"qcom,saw2-spm-ctl", MSM_SPM_REG_SAW_SPM_CTL}, + {"qcom,saw2-pmic-data0", MSM_SPM_REG_SAW_PMIC_DATA_0}, + {"qcom,saw2-pmic-data1", MSM_SPM_REG_SAW_PMIC_DATA_1}, + {"qcom,saw2-pmic-data2", MSM_SPM_REG_SAW_PMIC_DATA_2}, + {"qcom,saw2-pmic-data3", MSM_SPM_REG_SAW_PMIC_DATA_3}, + {"qcom,saw2-pmic-data4", MSM_SPM_REG_SAW_PMIC_DATA_4}, + {"qcom,saw2-pmic-data5", MSM_SPM_REG_SAW_PMIC_DATA_5}, + {"qcom,saw2-pmic-data6", MSM_SPM_REG_SAW_PMIC_DATA_6}, + {"qcom,saw2-pmic-data7", MSM_SPM_REG_SAW_PMIC_DATA_7}, + }; + + struct mode_of { + char *key; + uint32_t id; + }; + + struct mode_of mode_of_data[] = { + {"qcom,saw2-spm-cmd-wfi", MSM_SPM_MODE_CLOCK_GATING}, + {"qcom,saw2-spm-cmd-ret", MSM_SPM_MODE_RETENTION}, + {"qcom,saw2-spm-cmd-gdhs", MSM_SPM_MODE_GDHS}, + {"qcom,saw2-spm-cmd-spc", + MSM_SPM_MODE_STANDALONE_POWER_COLLAPSE}, + {"qcom,saw2-spm-cmd-pc", MSM_SPM_MODE_POWER_COLLAPSE}, + {"qcom,saw2-spm-cmd-fpc", MSM_SPM_MODE_FASTPC}, + }; + + dev = msm_spm_get_device(pdev); + if (!dev) { + /* + * For partial goods support some CPUs might not be available + * in which case, shouldn't throw an error + */ + return 0; + } + get_cpumask(node, &dev->mask); + + memset(&spm_data, 0, sizeof(struct msm_spm_platform_data)); + memset(&modes, 0, + (MSM_SPM_MODE_NR - 2) * sizeof(struct msm_spm_seq_entry)); + + key = "qcom,saw2-ver-reg"; + ret = of_property_read_u32(node, key, &val); + if (ret) + goto fail; + spm_data.ver_reg = val; + + key = "qcom,vctl-timeout-us"; + ret = of_property_read_u32(node, key, &val); + if (!ret) + spm_data.vctl_timeout_us = val; + + /* SAW start address */ + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + ret = -EFAULT; + goto fail; + } + + spm_data.reg_base_addr = devm_ioremap(&pdev->dev, res->start, + resource_size(res)); + if (!spm_data.reg_base_addr) { + ret = -ENOMEM; + goto fail; + } + + spm_data.vctl_port = -1; + spm_data.phase_port = -1; + spm_data.pfm_port = -1; + + key = "qcom,vctl-port"; + of_property_read_u32(node, key, &spm_data.vctl_port); + + key = "qcom,phase-port"; + of_property_read_u32(node, key, &spm_data.phase_port); + + key = "qcom,pfm-port"; + of_property_read_u32(node, key, &spm_data.pfm_port); + + /* Q2S (QChannel-2-SPM) register */ + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "q2s"); + if (res) { + dev->q2s_reg = devm_ioremap(&pdev->dev, res->start, + resource_size(res)); + if (!dev->q2s_reg) { + pr_err("%s(): Unable to iomap Q2S register\n", + __func__); + ret = -EADDRNOTAVAIL; + goto fail; + } + } + + key = "qcom,use-qchannel-for-pc"; + dev->qchannel_ignore = !of_property_read_bool(node, key); + + key = "qcom,use-spm-clock-gating"; + dev->use_spm_clk_gating = of_property_read_bool(node, key); + + key = "qcom,use-qchannel-for-wfi"; + dev->use_qchannel_for_wfi = of_property_read_bool(node, key); + + /* HW flush address */ + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "hw-flush"); + if (res) { + dev->flush_base_addr = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(dev->flush_base_addr)) { + ret = PTR_ERR(dev->flush_base_addr); + pr_err("%s(): Unable to iomap hw flush register %d\n", + __func__, ret); + goto fail; + } + } + + /* Sleep req address */ + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "slpreq"); + if (res) { + dev->slpreq_base_addr = devm_ioremap(&pdev->dev, res->start, + resource_size(res)); + if (!dev->slpreq_base_addr) { + ret = -ENOMEM; + pr_err("%s(): Unable to iomap slpreq register\n", + __func__); + ret = -EADDRNOTAVAIL; + goto fail; + } + } + + /* + * At system boot, cpus and or clusters can remain in reset. CCI SPM + * will not be triggered unless SPM_LEGACY_MODE bit is set for the + * cluster in reset. Initialize q2s registers and set the + * SPM_LEGACY_MODE bit. + */ + msm_spm_config_q2s(dev, MSM_SPM_MODE_POWER_COLLAPSE); + msm_spm_drv_reg_init(&dev->reg_data, &spm_data); + + for (i = 0; i < ARRAY_SIZE(spm_of_data); i++) { + ret = of_property_read_u32(node, spm_of_data[i].key, &val); + if (ret) + continue; + msm_spm_drv_upd_reg_shadow(&dev->reg_data, spm_of_data[i].id, + val); + } + + for_each_child_of_node(node, n) { + const char *name; + bool bit_set; + int sync; + + if (!n->name) + continue; + + ret = of_property_read_string(n, "qcom,label", &name); + if (ret) + continue; + + for (i = 0; i < ARRAY_SIZE(mode_of_data); i++) + if (!strcmp(name, mode_of_data[i].key)) + break; + + if (i == ARRAY_SIZE(mode_of_data)) { + pr_err("Mode name invalid %s\n", name); + break; + } + + modes[mode_count].mode = mode_of_data[i].id; + modes[mode_count].cmd = + (uint8_t *)of_get_property(n, "qcom,sequence", &len); + if (!modes[mode_count].cmd) { + pr_err("cmd is empty\n"); + continue; + } + + bit_set = of_property_read_bool(n, "qcom,pc_mode"); + modes[mode_count].ctl |= bit_set ? BIT(PC_MODE_BIT) : 0; + + bit_set = of_property_read_bool(n, "qcom,ret_mode"); + modes[mode_count].ctl |= bit_set ? BIT(RET_MODE_BIT) : 0; + + bit_set = of_property_read_bool(n, "qcom,slp_cmd_mode"); + modes[mode_count].ctl |= bit_set ? BIT(SLP_CMD_BIT) : 0; + + bit_set = of_property_read_bool(n, "qcom,isar"); + modes[mode_count].ctl |= bit_set ? BIT(ISAR_BIT) : 0; + + bit_set = of_property_read_bool(n, "qcom,spm_en"); + modes[mode_count].ctl |= bit_set ? BIT(SPM_EN_BIT) : 0; + + ret = of_property_read_u32(n, "qcom,event_sync", &sync); + if (!ret) + modes[mode_count].ctl |= sync << EVENT_SYNC_BIT; + + mode_count++; + } + + spm_data.modes = modes; + spm_data.num_modes = mode_count; + + key = "qcom,supports-rpm-hs"; + dev->allow_rpm_hs = of_property_read_bool(pdev->dev.of_node, key); + + ret = msm_spm_dev_init(dev, &spm_data); + if (ret) + pr_err("SPM modes programming is not available from HLOS\n"); + + platform_set_drvdata(pdev, dev); + + for_each_cpu(cpu, &dev->mask) + per_cpu(cpu_vctl_device, cpu) = dev; + + if (!spm_data.num_modes) + return 0; + + cpu = get_cpu_id(pdev->dev.of_node); + + /* For CPUs that are online, the SPM has to be programmed for + * clockgating mode to ensure that it can use SPM for entering these + * low power modes. + */ + get_online_cpus(); + if ((cpu >= 0) && (cpu < num_possible_cpus()) && (cpu_online(cpu))) + msm_spm_config_low_power_mode(dev, MSM_SPM_MODE_CLOCK_GATING, + false); + put_online_cpus(); + return ret; + +fail: + cpu = get_cpu_id(pdev->dev.of_node); + if (dev && (cpu >= num_possible_cpus() || (cpu < 0))) { + for_each_cpu(cpu, &dev->mask) + per_cpu(cpu_vctl_device, cpu) = ERR_PTR(ret); + } + + pr_err("%s: CPU%d SPM device probe failed: %d\n", __func__, cpu, ret); + + return ret; +} + +static int msm_spm_dev_remove(struct platform_device *pdev) +{ + struct msm_spm_device *dev = platform_get_drvdata(pdev); + list_del(&dev->list); + return 0; +} + +static struct of_device_id msm_spm_match_table[] = { + {.compatible = "qcom,spm-v2"}, + {}, +}; + +static struct platform_driver msm_spm_device_driver = { + .probe = msm_spm_dev_probe, + .remove = msm_spm_dev_remove, + .driver = { + .name = "spm-v2", + .owner = THIS_MODULE, + .of_match_table = msm_spm_match_table, + }, +}; + +/** + * msm_spm_device_init(): Device tree initialization function + */ +int __init msm_spm_device_init(void) +{ + static bool registered; + if (registered) + return 0; + registered = true; + return platform_driver_register(&msm_spm_device_driver); +} +arch_initcall(msm_spm_device_init); diff --git a/drivers/soc/qcom/spm_driver.h b/drivers/soc/qcom/spm_driver.h new file mode 100644 index 000000000000..ff55a3517b0c --- /dev/null +++ b/drivers/soc/qcom/spm_driver.h @@ -0,0 +1,134 @@ +/* Copyright (c) 2011-2016, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only 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. + */ +#ifndef __ARCH_ARM_MACH_MSM_SPM_DEVICES_H +#define __ARCH_ARM_MACH_MSM_SPM_DEVICES_H + +#include <soc/qcom/spm.h> + +enum { + MSM_SPM_REG_SAW_CFG, + MSM_SPM_REG_SAW_AVS_CTL, + MSM_SPM_REG_SAW_AVS_HYSTERESIS, + MSM_SPM_REG_SAW_SPM_CTL, + MSM_SPM_REG_SAW_PMIC_DLY, + MSM_SPM_REG_SAW_AVS_LIMIT, + MSM_SPM_REG_SAW_AVS_DLY, + MSM_SPM_REG_SAW_SPM_DLY, + MSM_SPM_REG_SAW_PMIC_DATA_0, + MSM_SPM_REG_SAW_PMIC_DATA_1, + MSM_SPM_REG_SAW_PMIC_DATA_2, + MSM_SPM_REG_SAW_PMIC_DATA_3, + MSM_SPM_REG_SAW_PMIC_DATA_4, + MSM_SPM_REG_SAW_PMIC_DATA_5, + MSM_SPM_REG_SAW_PMIC_DATA_6, + MSM_SPM_REG_SAW_PMIC_DATA_7, + MSM_SPM_REG_SAW_RST, + + MSM_SPM_REG_NR_INITIALIZE = MSM_SPM_REG_SAW_RST, + + MSM_SPM_REG_SAW_ID, + MSM_SPM_REG_SAW_SECURE, + MSM_SPM_REG_SAW_STS0, + MSM_SPM_REG_SAW_STS1, + MSM_SPM_REG_SAW_STS2, + MSM_SPM_REG_SAW_VCTL, + MSM_SPM_REG_SAW_SEQ_ENTRY, + MSM_SPM_REG_SAW_SPM_STS, + MSM_SPM_REG_SAW_AVS_STS, + MSM_SPM_REG_SAW_PMIC_STS, + MSM_SPM_REG_SAW_VERSION, + + MSM_SPM_REG_NR, +}; + +struct msm_spm_seq_entry { + uint32_t mode; + uint8_t *cmd; + uint32_t ctl; +}; + +struct msm_spm_platform_data { + void __iomem *reg_base_addr; + uint32_t reg_init_values[MSM_SPM_REG_NR_INITIALIZE]; + + uint32_t ver_reg; + uint32_t vctl_port; + uint32_t phase_port; + uint32_t pfm_port; + + uint8_t awake_vlevel; + uint32_t vctl_timeout_us; + uint32_t avs_timeout_us; + + uint32_t num_modes; + struct msm_spm_seq_entry *modes; +}; + +enum msm_spm_pmic_port { + MSM_SPM_PMIC_VCTL_PORT, + MSM_SPM_PMIC_PHASE_PORT, + MSM_SPM_PMIC_PFM_PORT, +}; + +struct msm_spm_driver_data { + uint32_t major; + uint32_t minor; + uint32_t ver_reg; + uint32_t vctl_port; + uint32_t phase_port; + uint32_t pfm_port; + void __iomem *reg_base_addr; + uint32_t vctl_timeout_us; + uint32_t avs_timeout_us; + uint32_t reg_shadow[MSM_SPM_REG_NR]; + uint32_t *reg_seq_entry_shadow; + uint32_t *reg_offsets; +}; + +int msm_spm_drv_init(struct msm_spm_driver_data *dev, + struct msm_spm_platform_data *data); +int msm_spm_drv_reg_init(struct msm_spm_driver_data *dev, + struct msm_spm_platform_data *data); +void msm_spm_drv_reinit(struct msm_spm_driver_data *dev, bool seq); +int msm_spm_drv_set_low_power_mode(struct msm_spm_driver_data *dev, + uint32_t ctl); +int msm_spm_drv_set_vdd(struct msm_spm_driver_data *dev, + unsigned int vlevel); +void dump_regs(struct msm_spm_driver_data *dev, int cpu); +uint32_t msm_spm_drv_get_sts_curr_pmic_data( + struct msm_spm_driver_data *dev); +int msm_spm_drv_write_seq_data(struct msm_spm_driver_data *dev, + uint8_t *cmd, uint32_t *offset); +void msm_spm_drv_flush_seq_entry(struct msm_spm_driver_data *dev); +int msm_spm_drv_set_spm_enable(struct msm_spm_driver_data *dev, + bool enable); +int msm_spm_drv_set_pmic_data(struct msm_spm_driver_data *dev, + enum msm_spm_pmic_port port, unsigned int data); + +int msm_spm_drv_set_avs_limit(struct msm_spm_driver_data *dev, + uint32_t min_lvl, uint32_t max_lvl); + +int msm_spm_drv_set_avs_enable(struct msm_spm_driver_data *dev, + bool enable); +int msm_spm_drv_get_avs_enable(struct msm_spm_driver_data *dev); + +int msm_spm_drv_set_avs_irq_enable(struct msm_spm_driver_data *dev, + enum msm_spm_avs_irq irq, bool enable); +int msm_spm_drv_avs_clear_irq(struct msm_spm_driver_data *dev, + enum msm_spm_avs_irq irq); + +void msm_spm_reinit(void); +int msm_spm_init(struct msm_spm_platform_data *data, int nr_devs); +void msm_spm_drv_upd_reg_shadow(struct msm_spm_driver_data *dev, int id, + int val); +uint32_t msm_spm_drv_get_vdd(struct msm_spm_driver_data *dev); +#endif diff --git a/include/soc/qcom/spm.h b/include/soc/qcom/spm.h index 3900864dc8d1..0d199d027258 100644 --- a/include/soc/qcom/spm.h +++ b/include/soc/qcom/spm.h @@ -1,4 +1,4 @@ -/* Copyright (c) 2010-2015, The Linux Foundation. All rights reserved. +/* Copyright (c) 2010-2016, The Linux Foundation. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and