diff options
author | Srinivas Kandagatla <srinivas.kandagatla@linaro.org> | 2015-02-13 07:41:27 +0000 |
---|---|---|
committer | Srinivas Kandagatla <srinivas.kandagatla@linaro.org> | 2015-02-13 07:41:27 +0000 |
commit | 1fb9ceef7e6c945c7a030b58e68df215073190f8 (patch) | |
tree | f8f3e39be9643291f544272a7d9593fcca52632c | |
parent | d9bfb5b1438be0f4f7938da48f47b45dc4c87aaf (diff) | |
parent | 918ac8c541f891bd31047fa08623cf7d878531cf (diff) |
Merge branch 'tracking-qcomlt-cpuidle' into integration-linux-qcomlt
* tracking-qcomlt-cpuidle:
WIP: soc: qcom: scm-pass: Add support to scm pas
msm: scm: Add API to query for service/command availability.
ARM: qcom: scm: make error reporting more useful.
qcom: spm: ignore scm return value in power down path.
qcom: cpuidle: Add cpuidle driver for QCOM cpus
qcom: spm: Add Subsystem Power Manager driver
qcom: scm: Add SCM warmboot support for quad core SoCs
qcom: scm: Move scm-boot files to drivers/soc/qcom/ and include/soc/qcom
ARM: qcom: scm: Add atomic SCM APIs
ARM: qcom: scm: Move the scm driver to drivers/soc/qcom
ARM: qcom: scm: Add logging of actual return code from scm call
ARM: qcom: scm: Flush the command buffer only instead of the entire cache
ARM: qcom: scm: Get cacheline size from CTR
ARM: qcom: scm: Fix incorrect cache invalidation
-rw-r--r-- | Documentation/devicetree/bindings/arm/msm/qcom,idle-state.txt | 81 | ||||
-rw-r--r-- | Documentation/devicetree/bindings/arm/msm/qcom,saw2.txt | 31 | ||||
-rw-r--r-- | arch/arm/mach-qcom/Kconfig | 3 | ||||
-rw-r--r-- | arch/arm/mach-qcom/Makefile | 3 | ||||
-rw-r--r-- | arch/arm/mach-qcom/platsmp.c | 2 | ||||
-rw-r--r-- | drivers/cpuidle/Kconfig.arm | 7 | ||||
-rw-r--r-- | drivers/cpuidle/Makefile | 1 | ||||
-rw-r--r-- | drivers/cpuidle/cpuidle-qcom.c | 99 | ||||
-rw-r--r-- | drivers/soc/qcom/Kconfig | 10 | ||||
-rw-r--r-- | drivers/soc/qcom/Makefile | 3 | ||||
-rw-r--r-- | drivers/soc/qcom/scm-boot.c (renamed from arch/arm/mach-qcom/scm-boot.c) | 39 | ||||
-rw-r--r-- | drivers/soc/qcom/scm-pas.c | 112 | ||||
-rw-r--r-- | drivers/soc/qcom/scm-pas.h | 32 | ||||
-rw-r--r-- | drivers/soc/qcom/scm.c (renamed from arch/arm/mach-qcom/scm.c) | 156 | ||||
-rw-r--r-- | drivers/soc/qcom/spm.c | 339 | ||||
-rw-r--r-- | include/soc/qcom/pm.h | 31 | ||||
-rw-r--r-- | include/soc/qcom/scm-boot.h (renamed from arch/arm/mach-qcom/scm-boot.h) | 3 | ||||
-rw-r--r-- | include/soc/qcom/scm.h (renamed from arch/arm/mach-qcom/scm.h) | 16 |
18 files changed, 932 insertions, 36 deletions
diff --git a/Documentation/devicetree/bindings/arm/msm/qcom,idle-state.txt b/Documentation/devicetree/bindings/arm/msm/qcom,idle-state.txt new file mode 100644 index 000000000000..ae1b07f852c6 --- /dev/null +++ b/Documentation/devicetree/bindings/arm/msm/qcom,idle-state.txt @@ -0,0 +1,81 @@ +QCOM Idle States for cpuidle driver + +ARM provides idle-state node to define the cpuidle states, as defined in [1]. +cpuidle-qcom is the cpuidle driver for Qualcomm SoCs and uses these idle +states. Idle states have different enter/exit latency and residency values. +The idle states supported by the QCOM SoC are defined as - + + * Standby + * Retention + * Standalone Power Collapse (Standalone PC or SPC) + * Power Collapse (PC) + +Standby: Standby does a little more in addition to architectural clock gating. +When the WFI instruction is executed the ARM core would gate its internal +clocks. In addition to gating the clocks, QCOM cpus use this instruction as a +trigger to execute the SPM state machine. The SPM state machine waits for the +interrupt to trigger the core back in to active. This triggers the cache +hierarchy to enter standby states, when all cpus are idle. An interrupt brings +the SPM state machine out of its wait, the next step is to ensure that the +cache hierarchy is also out of standby, and then the cpu is allowed to resume +execution. + +Retention: Retention is a low power state where the core is clock gated and +the memory and the registers associated with the core are retained. The +voltage may be reduced to the minimum value needed to keep the processor +registers active. The SPM should be configured to execute the retention +sequence and would wait for interrupt, before restoring the cpu to execution +state. Retention may have a slightly higher latency than Standby. + +Standalone PC: A cpu can power down and warmboot if there is a sufficient time +between the time it enters idle and the next known wake up. SPC mode is used +to indicate a core entering a power down state without consulting any other +cpu or the system resources. This helps save power only on that core. The SPM +sequence for this idle state is programmed to power down the supply to the +core, wait for the interrupt, restore power to the core, and ensure the +system state including cache hierarchy is ready before allowing core to +resume. Applying power and resetting the core causes the core to warmboot +back into Elevation Level (EL) which trampolines the control back to the +kernel. Entering a power down state for the cpu, needs to be done by trapping +into a EL. Failing to do so, would result in a crash enforced by the warm boot +code in the EL for the SoC. On SoCs with write-back L1 cache, the cache has to +be flushed in s/w, before powering down the core. + +Power Collapse: This state is similar to the SPC mode, but distinguishes +itself in that the cpu acknowledges and permits the SoC to enter deeper sleep +modes. In a hierarchical power domain SoC, this means L2 and other caches can +be flushed, system bus, clocks - lowered, and SoC main XO clock gated and +voltages reduced, provided all cpus enter this state. Since the span of low +power modes possible at this state is vast, the exit latency and the residency +of this low power mode would be considered high even though at a cpu level, +this essentially is cpu power down. The SPM in this state also may handshake +with the Resource power manager processor in the SoC to indicate a complete +application processor subsystem shut down. + +The idle-state for QCOM SoCs are distinguished by the compatible property of +the idle-states device node. +The devicetree representation of the idle state should be - + +Required properties: + +- compatible: Must be one of - + "qcom,idle-state-stby", + "qcom,idle-state-ret", + "qcom,idle-state-spc", + "qcom,idle-state-pc", + and "arm,idle-state". + +Other required and optional properties are specified in [1]. + +Example: + + idle-states { + CPU_SPC: spc { + compatible = "qcom,idle-state-spc", "arm,idle-state"; + entry-latency-us = <150>; + exit-latency-us = <200>; + min-residency-us = <2000>; + }; + }; + +[1]. Documentation/devicetree/bindings/arm/idle-states.txt diff --git a/Documentation/devicetree/bindings/arm/msm/qcom,saw2.txt b/Documentation/devicetree/bindings/arm/msm/qcom,saw2.txt index 1505fb8e131a..690c3c002ad2 100644 --- a/Documentation/devicetree/bindings/arm/msm/qcom,saw2.txt +++ b/Documentation/devicetree/bindings/arm/msm/qcom,saw2.txt @@ -2,11 +2,20 @@ SPM AVS Wrapper 2 (SAW2) The SAW2 is a wrapper around the Subsystem Power Manager (SPM) and the Adaptive Voltage Scaling (AVS) hardware. The SPM is a programmable -micro-controller that transitions a piece of hardware (like a processor or +power-controller that transitions a piece of hardware (like a processor or subsystem) into and out of low power modes via a direct connection to the PMIC. It can also be wired up to interact with other processors in the system, notifying them when a low power state is entered or exited. +Multiple revisions of the SAW hardware are supported using these Device Nodes. +SAW2 revisions differ in the register offset and configuration data. Also, the +same revision of the SAW in different SoCs may have different configuration +data due the the differences in hardware capabilities. Hence the SoC name, the +version of the SAW hardware in that SoC and the distinction between cpu (big +or Little) or cache, may be needed to uniquely identify the SAW register +configuration and initialization data. The compatible string is used to +indicate this parameter. + PROPERTIES - compatible: @@ -14,10 +23,13 @@ PROPERTIES Value type: <string> Definition: shall contain "qcom,saw2". A more specific value should be one of: - "qcom,saw2-v1" - "qcom,saw2-v1.1" - "qcom,saw2-v2" - "qcom,saw2-v2.1" + "qcom,saw2-v1" + "qcom,saw2-v1.1" + "qcom,saw2-v2" + "qcom,saw2-v2.1" + "qcom,apq8064-saw2-v1.1-cpu" + "qcom,msm8974-saw2-v2.1-cpu" + "qcom,apq8084-saw2-v2.1-cpu" - reg: Usage: required @@ -26,10 +38,17 @@ PROPERTIES the register region. An optional second element specifies the base address and size of the alias register region. +- regulator: + Usage: optional + Value type: boolean + Definition: Indicates that this SPM device acts as a regulator device + device for the core (CPU or Cache) the SPM is attached + to. Example: - regulator@2099000 { + power-controller@2099000 { compatible = "qcom,saw2"; reg = <0x02099000 0x1000>, <0x02009000 0x1000>; + regulator; }; diff --git a/arch/arm/mach-qcom/Kconfig b/arch/arm/mach-qcom/Kconfig index 3b5539cc907b..891442eb01b1 100644 --- a/arch/arm/mach-qcom/Kconfig +++ b/arch/arm/mach-qcom/Kconfig @@ -25,7 +25,4 @@ config ARCH_MSM8974 bool "Enable support for MSM8974" select HAVE_ARM_ARCH_TIMER -config QCOM_SCM - bool - endif diff --git a/arch/arm/mach-qcom/Makefile b/arch/arm/mach-qcom/Makefile index 8f756ae1ae31..e324375fa919 100644 --- a/arch/arm/mach-qcom/Makefile +++ b/arch/arm/mach-qcom/Makefile @@ -1,5 +1,2 @@ obj-y := board.o obj-$(CONFIG_SMP) += platsmp.o -obj-$(CONFIG_QCOM_SCM) += scm.o scm-boot.o - -CFLAGS_scm.o :=$(call as-instr,.arch_extension sec,-DREQUIRES_SEC=1) diff --git a/arch/arm/mach-qcom/platsmp.c b/arch/arm/mach-qcom/platsmp.c index d6908569ecaf..a692bcb7e7f5 100644 --- a/arch/arm/mach-qcom/platsmp.c +++ b/arch/arm/mach-qcom/platsmp.c @@ -20,7 +20,7 @@ #include <asm/smp_plat.h> -#include "scm-boot.h" +#include <soc/qcom/scm-boot.h> #define VDD_SC1_ARRAY_CLAMP_GFS_CTL 0x35a0 #define SCSS_CPU1CORE_RESET 0x2d80 diff --git a/drivers/cpuidle/Kconfig.arm b/drivers/cpuidle/Kconfig.arm index 8c16ab20fb15..e98993c872a2 100644 --- a/drivers/cpuidle/Kconfig.arm +++ b/drivers/cpuidle/Kconfig.arm @@ -63,3 +63,10 @@ config ARM_MVEBU_V7_CPUIDLE depends on ARCH_MVEBU help Select this to enable cpuidle on Armada 370, 38x and XP processors. + +config ARM_QCOM_CPUIDLE + bool "CPU Idle drivers for Qualcomm processors" + depends on ARCH_QCOM + select DT_IDLE_STATES + help + Select this to enable cpuidle for QCOM processors diff --git a/drivers/cpuidle/Makefile b/drivers/cpuidle/Makefile index 4d177b916f75..6c222d536005 100644 --- a/drivers/cpuidle/Makefile +++ b/drivers/cpuidle/Makefile @@ -17,6 +17,7 @@ obj-$(CONFIG_ARM_ZYNQ_CPUIDLE) += cpuidle-zynq.o obj-$(CONFIG_ARM_U8500_CPUIDLE) += cpuidle-ux500.o obj-$(CONFIG_ARM_AT91_CPUIDLE) += cpuidle-at91.o obj-$(CONFIG_ARM_EXYNOS_CPUIDLE) += cpuidle-exynos.o +obj-$(CONFIG_ARM_QCOM_CPUIDLE) += cpuidle-qcom.o ############################################################################### # MIPS drivers diff --git a/drivers/cpuidle/cpuidle-qcom.c b/drivers/cpuidle/cpuidle-qcom.c new file mode 100644 index 000000000000..aa6a6c9a3590 --- /dev/null +++ b/drivers/cpuidle/cpuidle-qcom.c @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2014, Linaro Limited. + * + * 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/cpuidle.h> +#include <linux/module.h> +#include <linux/platform_device.h> + +#include <soc/qcom/pm.h> +#include "dt_idle_states.h" + +static struct qcom_cpu_pm_ops *lpm_ops; + +static int qcom_cpu_stby(struct cpuidle_device *dev, + struct cpuidle_driver *drv, int index) +{ + int ret; + + ret = lpm_ops->standby(NULL); + if (ret) + return ret; + + return index; +} + +static int qcom_cpu_spc(struct cpuidle_device *dev, + struct cpuidle_driver *drv, int index) +{ + int ret; + + ret = lpm_ops->spc(NULL); + if (ret) + return ret; + + return index; +} + +static struct cpuidle_driver qcom_cpuidle_driver = { + .name = "qcom_cpuidle", +}; + +static const struct of_device_id qcom_idle_state_match[] = { + { .compatible = "qcom,idle-state-stby", .data = qcom_cpu_stby }, + { .compatible = "qcom,idle-state-spc", .data = qcom_cpu_spc }, + { }, +}; + +static int qcom_cpuidle_probe(struct platform_device *pdev) +{ + struct cpuidle_driver *drv = &qcom_cpuidle_driver; + int ret; + + lpm_ops = pdev->dev.platform_data; + + /* Probe for other states, including standby */ + ret = dt_init_idle_driver(drv, qcom_idle_state_match, 0); + if (ret < 0) + return ret; + + /* + * We will not register for cpu's cpuidle device here, + * they will be registered as and when their power controllers + * are ready. + */ + return cpuidle_register_driver(drv); +} + +static struct platform_driver qcom_cpuidle = { + .probe = qcom_cpuidle_probe, + .driver = { + .name = "qcom_cpuidle", + }, +}; + +/* + * Register the driver early so the we have a successul registration + * when the device shows up. + * This way the cpuidle driver could be registered before the cpuidle + * devices are registered. + */ +static int __init qcom_cpuidle_driver_init(void) +{ + return platform_driver_register(&qcom_cpuidle); +} +core_initcall(qcom_cpuidle_driver_init); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("CPUIDLE driver for QCOM SoC"); +MODULE_ALIAS("platform:qcom-cpuidle"); diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig index 7bd2c94f54a4..012fb37b3ba9 100644 --- a/drivers/soc/qcom/Kconfig +++ b/drivers/soc/qcom/Kconfig @@ -9,3 +9,13 @@ config QCOM_GSBI functions for connecting the underlying serial UART, SPI, and I2C devices to the output pins. +config QCOM_SCM + bool + +config QCOM_PM + bool "Qualcomm Power Management" + depends on ARCH_QCOM + help + QCOM Platform specific power driver to manage cores and L2 low power + modes. It interface with various system drivers to put the cores in + low power modes. diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile index 438901257ac1..4d4ff4a09b7d 100644 --- a/drivers/soc/qcom/Makefile +++ b/drivers/soc/qcom/Makefile @@ -1 +1,4 @@ obj-$(CONFIG_QCOM_GSBI) += qcom_gsbi.o +obj-$(CONFIG_QCOM_PM) += spm.o +CFLAGS_scm.o :=$(call as-instr,.arch_extension sec,-DREQUIRES_SEC=1) +obj-$(CONFIG_QCOM_SCM) += scm.o scm-boot.o scm-pas.o diff --git a/arch/arm/mach-qcom/scm-boot.c b/drivers/soc/qcom/scm-boot.c index 45cee3e469a5..f653217ae442 100644 --- a/arch/arm/mach-qcom/scm-boot.c +++ b/drivers/soc/qcom/scm-boot.c @@ -18,8 +18,25 @@ #include <linux/module.h> #include <linux/slab.h> -#include "scm.h" -#include "scm-boot.h" +#include <soc/qcom/scm.h> +#include <soc/qcom/scm-boot.h> + +#define SCM_FLAG_WARMBOOT_CPU0 0x04 +#define SCM_FLAG_WARMBOOT_CPU1 0x02 +#define SCM_FLAG_WARMBOOT_CPU2 0x10 +#define SCM_FLAG_WARMBOOT_CPU3 0x40 + +struct scm_warmboot { + int flag; + void *entry; +}; + +static struct scm_warmboot scm_flags[] = { + { .flag = SCM_FLAG_WARMBOOT_CPU0 }, + { .flag = SCM_FLAG_WARMBOOT_CPU1 }, + { .flag = SCM_FLAG_WARMBOOT_CPU2 }, + { .flag = SCM_FLAG_WARMBOOT_CPU3 }, +}; /* * Set the cold/warm boot address for one of the CPU cores. @@ -37,3 +54,21 @@ int scm_set_boot_addr(phys_addr_t addr, int flags) &cmd, sizeof(cmd), NULL, 0); } EXPORT_SYMBOL(scm_set_boot_addr); + +int scm_set_warm_boot_addr(void *entry, int cpu) +{ + int ret; + + /* + * Reassign only if we are switching from hotplug entry point + * to cpuidle entry point or vice versa. + */ + if (entry == scm_flags[cpu].entry) + return 0; + + ret = scm_set_boot_addr(virt_to_phys(entry), scm_flags[cpu].flag); + if (!ret) + scm_flags[cpu].entry = entry; + + return ret; +} diff --git a/drivers/soc/qcom/scm-pas.c b/drivers/soc/qcom/scm-pas.c new file mode 100644 index 000000000000..9b32cc478434 --- /dev/null +++ b/drivers/soc/qcom/scm-pas.c @@ -0,0 +1,112 @@ +/* Copyright (c) 2010-2012, 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. + */ + +#define pr_fmt(fmt) "scm-pas: " fmt + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/clk.h> +#include <linux/err.h> +#include <linux/dma-mapping.h> + +#include <soc/qcom/scm.h> +#include <asm/cacheflush.h> +#include "scm-pas.h" + +#define PAS_INIT_IMAGE_CMD 1 +#define PAS_MEM_SETUP_CMD 2 +#define PAS_AUTH_AND_RESET_CMD 5 +#define PAS_SHUTDOWN_CMD 6 +#define PAS_IS_SUPPORTED_CMD 7 + + +int pas_init_image(enum pas_id id, const u8 *metadata, size_t size) +{ + int ret; + struct pas_init_image_req { + u32 proc; + u32 image_addr; + } request; + u32 scm_ret = 0; + void *mdata_buf; + dma_addr_t mdata_phys; + DEFINE_DMA_ATTRS(attrs); + + + dma_set_attr(DMA_ATTR_WRITE_BARRIER, &attrs); + mdata_buf = dma_alloc_attrs(NULL, size, &mdata_phys, GFP_KERNEL, + &attrs); + if (!mdata_buf) { + pr_err("Allocation for metadata failed.\n"); + return -ENOMEM; + } + + memcpy(mdata_buf, metadata, size); + + request.proc = id; + request.image_addr = mdata_phys; + + ret = scm_call(SCM_SVC_PIL, PAS_INIT_IMAGE_CMD, &request, + sizeof(request), &scm_ret, sizeof(scm_ret)); + + dma_free_attrs(NULL, size, mdata_buf, mdata_phys, &attrs); + + if (ret) + return ret; + return scm_ret; +} +EXPORT_SYMBOL(pas_init_image); + +int pas_auth_and_reset(enum pas_id id) +{ + int ret; + u32 proc = id, scm_ret = 0; + ret = scm_call(SCM_SVC_PIL, PAS_AUTH_AND_RESET_CMD, &proc, + sizeof(proc), &scm_ret, sizeof(scm_ret)); + if (ret) + scm_ret = ret; + + return scm_ret; +} +EXPORT_SYMBOL(pas_auth_and_reset); + +int pas_shutdown(enum pas_id id) +{ + int ret; + u32 proc = id, scm_ret = 0; + + ret = scm_call(SCM_SVC_PIL, PAS_SHUTDOWN_CMD, &proc, sizeof(proc), + &scm_ret, sizeof(scm_ret)); + if (ret) + return ret; + + return scm_ret; +} +EXPORT_SYMBOL(pas_shutdown); + +int pas_supported(enum pas_id id) +{ + int ret; + u32 periph = id, ret_val = 0; + + if (scm_is_call_available(SCM_SVC_PIL, PAS_IS_SUPPORTED_CMD) <= 0) + return 0; + + ret = scm_call(SCM_SVC_PIL, PAS_IS_SUPPORTED_CMD, &periph, + sizeof(periph), &ret_val, sizeof(ret_val)); + if (ret) + return ret; + + return ret_val; +} +EXPORT_SYMBOL(pas_supported); diff --git a/drivers/soc/qcom/scm-pas.h b/drivers/soc/qcom/scm-pas.h new file mode 100644 index 000000000000..86b5b30f5bea --- /dev/null +++ b/drivers/soc/qcom/scm-pas.h @@ -0,0 +1,32 @@ +/* Copyright (c) 2010-2012, 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 __MSM_SCM_PAS_H +#define __MSM_SCM_PAS_H + +enum pas_id { + PAS_MODEM, + PAS_Q6, + PAS_DSPS, + PAS_TZAPPS, + PAS_MODEM_SW, + PAS_MODEM_FW, + PAS_WCNSS, + PAS_SECAPP, + PAS_GSS, + PAS_VIDC, +}; + +extern int pas_init_image(enum pas_id id, const u8 *metadata, size_t size); +extern int pas_auth_and_reset(enum pas_id id); +extern int pas_shutdown(enum pas_id id); +extern int pas_supported(enum pas_id id); +#endif diff --git a/arch/arm/mach-qcom/scm.c b/drivers/soc/qcom/scm.c index c536fd6bf827..cdcaccc5dec2 100644 --- a/arch/arm/mach-qcom/scm.c +++ b/drivers/soc/qcom/scm.c @@ -22,12 +22,11 @@ #include <linux/errno.h> #include <linux/err.h> -#include <asm/cacheflush.h> +#include <soc/qcom/scm.h> -#include "scm.h" +#include <asm/outercache.h> +#include <asm/cacheflush.h> -/* Cache line size for msm8x60 */ -#define CACHELINESIZE 32 #define SCM_ENOMEM -5 #define SCM_EOPNOTSUPP -4 @@ -152,8 +151,13 @@ static inline void *scm_get_response_buffer(const struct scm_response *rsp) return (void *)rsp + rsp->buf_offset; } -static int scm_remap_error(int err) +static int scm_remap_error(const struct scm_command *cmd, int err) { + u32 svc_id = cmd->id >> 10; + u32 cmd_id = cmd->id & GENMASK(10, 0); + + pr_err("scm_call for svc_id %d & cmd_id %d failed with error code %d\n", + svc_id, cmd_id, err); switch (err) { case SCM_ERROR: return -EIO; @@ -198,18 +202,38 @@ static int __scm_call(const struct scm_command *cmd) u32 cmd_addr = virt_to_phys(cmd); /* - * Flush the entire cache here so callers don't have to remember - * to flush the cache when passing physical addresses to the secure - * side in the buffer. + * Flush the command buffer so that the secure world sees + * the correct data. */ - flush_cache_all(); + __cpuc_flush_dcache_area((void *)cmd, cmd->len); + outer_flush_range(cmd_addr, cmd_addr + cmd->len); + ret = smc(cmd_addr); if (ret < 0) - ret = scm_remap_error(ret); + ret = scm_remap_error(cmd, ret); return ret; } +static void scm_inv_range(unsigned long start, unsigned long end) +{ + u32 cacheline_size, ctr; + + asm volatile("mrc p15, 0, %0, c0, c0, 1" : "=r" (ctr)); + cacheline_size = 4 << ((ctr >> 16) & 0xf); + + start = round_down(start, cacheline_size); + end = round_up(end, cacheline_size); + outer_inv_range(start, end); + while (start < end) { + asm ("mcr p15, 0, %0, c7, c6, 1" : : "r" (start) + : "memory"); + start += cacheline_size; + } + dsb(); + isb(); +} + /** * scm_call() - Send an SCM command * @svc_id: service identifier @@ -220,6 +244,13 @@ static int __scm_call(const struct scm_command *cmd) * @resp_len: length of the response buffer * * Sends a command to the SCM and waits for the command to finish processing. + * + * A note on cache maintenance: + * Note that any buffers that are expected to be accessed by the secure world + * must be flushed before invoking scm_call and invalidated in the cache + * immediately after scm_call returns. Cache maintenance on the command and + * response buffers is taken care of by scm_call; however, callers are + * responsible for any other cached buffers passed over to the secure world. */ int scm_call(u32 svc_id, u32 cmd_id, const void *cmd_buf, size_t cmd_len, void *resp_buf, size_t resp_len) @@ -227,6 +258,7 @@ int scm_call(u32 svc_id, u32 cmd_id, const void *cmd_buf, size_t cmd_len, int ret; struct scm_command *cmd; struct scm_response *rsp; + unsigned long start, end; cmd = alloc_scm_command(cmd_len, resp_len); if (!cmd) @@ -243,17 +275,15 @@ int scm_call(u32 svc_id, u32 cmd_id, const void *cmd_buf, size_t cmd_len, goto out; rsp = scm_command_to_response(cmd); + start = (unsigned long)rsp; + do { - u32 start = (u32)rsp; - u32 end = (u32)scm_get_response_buffer(rsp) + resp_len; - start &= ~(CACHELINESIZE - 1); - while (start < end) { - asm ("mcr p15, 0, %0, c7, c6, 1" : : "r" (start) - : "memory"); - start += CACHELINESIZE; - } + scm_inv_range(start, start + sizeof(*rsp)); } while (!rsp->is_complete); + end = (unsigned long)scm_get_response_buffer(rsp) + resp_len; + scm_inv_range(start, end); + if (resp_buf) memcpy(resp_buf, scm_get_response_buffer(rsp), resp_len); out: @@ -262,6 +292,79 @@ out: } EXPORT_SYMBOL(scm_call); +#define SCM_CLASS_REGISTER (0x2 << 8) +#define SCM_MASK_IRQS BIT(5) +#define SCM_ATOMIC(svc, cmd, n) (((((svc) << 10)|((cmd) & 0x3ff)) << 12) | \ + SCM_CLASS_REGISTER | \ + SCM_MASK_IRQS | \ + (n & 0xf)) + +/** + * scm_call_atomic1() - Send an atomic SCM command with one argument + * @svc_id: service identifier + * @cmd_id: command identifier + * @arg1: first argument + * + * This shall only be used with commands that are guaranteed to be + * uninterruptable, atomic and SMP safe. + */ +s32 scm_call_atomic1(u32 svc, u32 cmd, u32 arg1) +{ + int context_id; + register u32 r0 asm("r0") = SCM_ATOMIC(svc, cmd, 1); + register u32 r1 asm("r1") = (u32)&context_id; + register u32 r2 asm("r2") = arg1; + + asm volatile( + __asmeq("%0", "r0") + __asmeq("%1", "r0") + __asmeq("%2", "r1") + __asmeq("%3", "r2") +#ifdef REQUIRES_SEC + ".arch_extension sec\n" +#endif + "smc #0 @ switch to secure world\n" + : "=r" (r0) + : "r" (r0), "r" (r1), "r" (r2) + : "r3"); + return r0; +} +EXPORT_SYMBOL(scm_call_atomic1); + +/** + * scm_call_atomic2() - Send an atomic SCM command with two arguments + * @svc_id: service identifier + * @cmd_id: command identifier + * @arg1: first argument + * @arg2: second argument + * + * This shall only be used with commands that are guaranteed to be + * uninterruptable, atomic and SMP safe. + */ +s32 scm_call_atomic2(u32 svc, u32 cmd, u32 arg1, u32 arg2) +{ + int context_id; + register u32 r0 asm("r0") = SCM_ATOMIC(svc, cmd, 2); + register u32 r1 asm("r1") = (u32)&context_id; + register u32 r2 asm("r2") = arg1; + register u32 r3 asm("r3") = arg2; + + asm volatile( + __asmeq("%0", "r0") + __asmeq("%1", "r0") + __asmeq("%2", "r1") + __asmeq("%3", "r2") + __asmeq("%4", "r3") +#ifdef REQUIRES_SEC + ".arch_extension sec\n" +#endif + "smc #0 @ switch to secure world\n" + : "=r" (r0) + : "r" (r0), "r" (r1), "r" (r2), "r" (r3)); + return r0; +} +EXPORT_SYMBOL(scm_call_atomic2); + u32 scm_get_version(void) { int context_id; @@ -297,3 +400,20 @@ u32 scm_get_version(void) return version; } EXPORT_SYMBOL(scm_get_version); + +#define IS_CALL_AVAIL_CMD 1 +int scm_is_call_available(u32 svc_id, u32 cmd_id) +{ + int ret; + u32 svc_cmd = (svc_id << 10) | cmd_id; + u32 ret_val = 0; + + ret = scm_call(SCM_SVC_INFO, IS_CALL_AVAIL_CMD, &svc_cmd, + sizeof(svc_cmd), &ret_val, sizeof(ret_val)); + if (ret) + return ret; + + return ret_val; +} +EXPORT_SYMBOL(scm_is_call_available); + diff --git a/drivers/soc/qcom/spm.c b/drivers/soc/qcom/spm.c new file mode 100644 index 000000000000..e6fbed081b47 --- /dev/null +++ b/drivers/soc/qcom/spm.c @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2011-2014, 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/init.h> +#include <linux/io.h> +#include <linux/slab.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/cpuidle.h> +#include <linux/cpu_pm.h> + +#include <asm/proc-fns.h> +#include <asm/suspend.h> + +#include <soc/qcom/pm.h> +#include <soc/qcom/pm.h> +#include <soc/qcom/scm.h> +#include <soc/qcom/scm-boot.h> + + +#define SCM_CMD_TERMINATE_PC 0x2 +#define SCM_FLUSH_FLAG_MASK 0x3 +#define SCM_L2_ON 0x0 +#define SCM_L2_OFF 0x1 +#define MAX_PMIC_DATA 2 +#define MAX_SEQ_DATA 64 +#define SPM_CTL_INDEX 0x7f +#define SPM_CTL_INDEX_SHIFT 4 +#define SPM_CTL_EN BIT(0) + +enum spm_reg { + SPM_REG_CFG, + SPM_REG_SPM_CTL, + SPM_REG_DLY, + SPM_REG_PMIC_DLY, + SPM_REG_PMIC_DATA_0, + SPM_REG_PMIC_DATA_1, + SPM_REG_VCTL, + SPM_REG_SEQ_ENTRY, + SPM_REG_SPM_STS, + SPM_REG_PMIC_STS, + SPM_REG_NR, +}; + +struct spm_reg_data { + const u8 *reg_offset; + u32 spm_cfg; + u32 spm_dly; + u32 pmic_dly; + u32 pmic_data[MAX_PMIC_DATA]; + u8 seq[MAX_SEQ_DATA]; + u8 start_index[PM_SLEEP_MODE_NR]; +}; + +struct spm_driver_data { + void __iomem *reg_base; + const struct spm_reg_data *reg_data; +}; + +static const u8 spm_reg_offset_v2_1[SPM_REG_NR] = { + [SPM_REG_CFG] = 0x08, + [SPM_REG_SPM_CTL] = 0x30, + [SPM_REG_DLY] = 0x34, + [SPM_REG_SEQ_ENTRY] = 0x80, +}; + +/* SPM register data for 8974, 8084 */ +static const struct spm_reg_data spm_reg_8974_8084_cpu = { + .reg_offset = spm_reg_offset_v2_1, + .spm_cfg = 0x1, + .spm_dly = 0x3C102800, + .seq = { 0x03, 0x0B, 0x0F, 0x00, 0x20, 0x80, 0x10, 0xE8, 0x5B, 0x03, + 0x3B, 0xE8, 0x5B, 0x82, 0x10, 0x0B, 0x30, 0x06, 0x26, 0x30, + 0x0F }, + .start_index[PM_SLEEP_MODE_STBY] = 0, + .start_index[PM_SLEEP_MODE_SPC] = 3, +}; + +static const u8 spm_reg_offset_v1_1[SPM_REG_NR] = { + [SPM_REG_CFG] = 0x08, + [SPM_REG_SPM_CTL] = 0x20, + [SPM_REG_PMIC_DLY] = 0x24, + [SPM_REG_PMIC_DATA_0] = 0x28, + [SPM_REG_PMIC_DATA_1] = 0x2C, + [SPM_REG_SEQ_ENTRY] = 0x80, +}; + +/* SPM register data for 8064 */ +static const struct spm_reg_data spm_reg_8064_cpu = { + .reg_offset = spm_reg_offset_v1_1, + .spm_cfg = 0x1F, + .pmic_dly = 0x02020004, + .pmic_data[0] = 0x0084009C, + .pmic_data[1] = 0x00A4001C, + .seq = { 0x03, 0x0F, 0x00, 0x24, 0x54, 0x10, 0x09, 0x03, 0x01, + 0x10, 0x54, 0x30, 0x0C, 0x24, 0x30, 0x0F }, + .start_index[PM_SLEEP_MODE_STBY] = 0, + .start_index[PM_SLEEP_MODE_SPC] = 2, +}; + +static DEFINE_PER_CPU_SHARED_ALIGNED(struct spm_driver_data *, cpu_spm_drv); + +static inline void spm_register_write(struct spm_driver_data *drv, + enum spm_reg reg, u32 val) +{ + if (drv->reg_data->reg_offset[reg]) + writel_relaxed(val, drv->reg_base + + drv->reg_data->reg_offset[reg]); +} + +/* Ensure a guaranteed write, before return */ +static inline void spm_register_write_sync(struct spm_driver_data *drv, + enum spm_reg reg, u32 val) +{ + u32 ret; + + if (!drv->reg_data->reg_offset[reg]) + return; + + do { + writel_relaxed(val, drv->reg_base + + drv->reg_data->reg_offset[reg]); + ret = readl_relaxed(drv->reg_base + + drv->reg_data->reg_offset[reg]); + if (ret == val) + break; + cpu_relax(); + } while (1); +} + +static inline u32 spm_register_read(struct spm_driver_data *drv, + enum spm_reg reg) +{ + return readl_relaxed(drv->reg_base + drv->reg_data->reg_offset[reg]); +} + +static void spm_set_low_power_mode(enum pm_sleep_mode mode) +{ + struct spm_driver_data *drv = per_cpu(cpu_spm_drv, + smp_processor_id()); + u32 start_index; + u32 ctl_val; + + start_index = drv->reg_data->start_index[mode]; + + ctl_val = spm_register_read(drv, SPM_REG_SPM_CTL); + ctl_val &= ~(SPM_CTL_INDEX << SPM_CTL_INDEX_SHIFT); + ctl_val |= start_index << SPM_CTL_INDEX_SHIFT; + ctl_val |= SPM_CTL_EN; + spm_register_write_sync(drv, SPM_REG_SPM_CTL, ctl_val); +} + +static int qcom_pm_collapse(unsigned long int unused) +{ + int ret; + u32 flag; + int cpu = smp_processor_id(); + + ret = scm_set_warm_boot_addr(cpu_resume, cpu); + if (ret) + return ret; + + flag = SCM_L2_ON & SCM_FLUSH_FLAG_MASK; + ret = scm_call_atomic1(SCM_SVC_BOOT, SCM_CMD_TERMINATE_PC, flag); + + /* + * Returns here only if there was a pending interrupt and we did not + * power down as a result. + */ + /* Hack:: Ignore scm call return values in power down path */ + return 0; +} + +static int qcom_cpu_standby(void *unused) +{ + spm_set_low_power_mode(PM_SLEEP_MODE_STBY); + cpu_do_idle(); + + return 0; +} + +static int qcom_cpu_spc(void *unused) +{ + int ret; + + spm_set_low_power_mode(PM_SLEEP_MODE_STBY); + cpu_pm_enter(); + ret = cpu_suspend(0, qcom_pm_collapse); + cpu_pm_exit(); + + return ret; +} + +static struct spm_driver_data *spm_get_drv(struct platform_device *pdev, + int *spm_cpu) +{ + struct spm_driver_data *drv = NULL; + struct device_node *cpu_node, *saw_node; + int cpu; + bool found; + + for_each_possible_cpu(cpu) { + cpu_node = of_cpu_device_node_get(cpu); + if (!cpu_node) + continue; + saw_node = of_parse_phandle(cpu_node, "qcom,saw", 0); + found = (saw_node == pdev->dev.of_node); + of_node_put(saw_node); + of_node_put(cpu_node); + if (found) + break; + } + + if (found) { + drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL); + if (drv) + *spm_cpu = cpu; + } + + return drv; +} + +static const struct of_device_id spm_match_table[] = { + { .compatible = "qcom,msm8974-saw2-v2.1-cpu", + .data = &spm_reg_8974_8084_cpu }, + { .compatible = "qcom,apq8084-saw2-v2.1-cpu", + .data = &spm_reg_8974_8084_cpu }, + { .compatible = "qcom,apq8064-saw2-v1.1-cpu", + .data = &spm_reg_8064_cpu }, + { }, +}; + +static int spm_dev_probe(struct platform_device *pdev) +{ + struct spm_driver_data *drv; + struct resource *res; + const struct of_device_id *match_id; + void __iomem *addr; + int cpu; + struct cpuidle_device *dev; + + drv = spm_get_drv(pdev, &cpu); + if (!drv) + return -EINVAL; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + drv->reg_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(drv->reg_base)) + return PTR_ERR(drv->reg_base); + + match_id = of_match_node(spm_match_table, pdev->dev.of_node); + if (!match_id) + return -ENODEV; + + drv->reg_data = match_id->data; + + /* Write the SPM sequences first.. */ + addr = drv->reg_base + drv->reg_data->reg_offset[SPM_REG_SEQ_ENTRY]; + __iowrite32_copy(addr, drv->reg_data->seq, + ARRAY_SIZE(drv->reg_data->seq) / 4); + + /* + * ..and then the control registers. + * On some SoC if the control registers are written first and if the + * CPU was held in reset, the reset signal could trigger the SPM state + * machine, before the sequences are completely written. + */ + spm_register_write(drv, SPM_REG_CFG, drv->reg_data->spm_cfg); + spm_register_write(drv, SPM_REG_DLY, drv->reg_data->spm_dly); + spm_register_write(drv, SPM_REG_PMIC_DLY, drv->reg_data->pmic_dly); + spm_register_write(drv, SPM_REG_PMIC_DATA_0, + drv->reg_data->pmic_data[0]); + spm_register_write(drv, SPM_REG_PMIC_DATA_1, + drv->reg_data->pmic_data[1]); + + per_cpu(cpu_spm_drv, cpu) = drv; + + /* Register the cpuidle device for the cpu, we are ready for cpuidle */ + dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + dev->cpu = cpu; + return cpuidle_register_device(dev); +} + +static struct qcom_cpu_pm_ops lpm_ops = { + .standby = qcom_cpu_standby, + .spc = qcom_cpu_spc, +}; + +static struct platform_device qcom_cpuidle_drv = { + .name = "qcom_cpuidle", + .id = -1, + .dev.platform_data = &lpm_ops, +}; + +static struct platform_driver spm_driver = { + .probe = spm_dev_probe, + .driver = { + .name = "saw", + .of_match_table = spm_match_table, + }, +}; + +static int __init qcom_spm_init(void) +{ + int ret; + + /* + * cpuidle driver need to registered before the cpuidle device + * for any cpu. Register the device for the the cpuidle driver. + */ + ret = platform_device_register(&qcom_cpuidle_drv); + if (ret) + return ret; + + return platform_driver_register(&spm_driver); +} +module_init(qcom_spm_init); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("SAW power controller driver"); +MODULE_ALIAS("platform:saw"); diff --git a/include/soc/qcom/pm.h b/include/soc/qcom/pm.h new file mode 100644 index 000000000000..d9a56d7c0206 --- /dev/null +++ b/include/soc/qcom/pm.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2009-2014, The Linux Foundation. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * 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 __QCOM_PM_H +#define __QCOM_PM_H + +enum pm_sleep_mode { + PM_SLEEP_MODE_STBY, + PM_SLEEP_MODE_RET, + PM_SLEEP_MODE_SPC, + PM_SLEEP_MODE_PC, + PM_SLEEP_MODE_NR, +}; + +struct qcom_cpu_pm_ops { + int (*standby)(void *data); + int (*spc)(void *data); +}; + +#endif /* __QCOM_PM_H */ diff --git a/arch/arm/mach-qcom/scm-boot.h b/include/soc/qcom/scm-boot.h index 6aabb2428176..529f55ae07f7 100644 --- a/arch/arm/mach-qcom/scm-boot.h +++ b/include/soc/qcom/scm-boot.h @@ -16,9 +16,8 @@ #define SCM_FLAG_COLDBOOT_CPU1 0x01 #define SCM_FLAG_COLDBOOT_CPU2 0x08 #define SCM_FLAG_COLDBOOT_CPU3 0x20 -#define SCM_FLAG_WARMBOOT_CPU0 0x04 -#define SCM_FLAG_WARMBOOT_CPU1 0x02 int scm_set_boot_addr(phys_addr_t addr, int flags); +int scm_set_warm_boot_addr(void *entry, int cpu); #endif diff --git a/arch/arm/mach-qcom/scm.h b/include/soc/qcom/scm.h index 00b31ea58f29..e486266c2009 100644 --- a/arch/arm/mach-qcom/scm.h +++ b/include/soc/qcom/scm.h @@ -1,4 +1,4 @@ -/* Copyright (c) 2010, Code Aurora Forum. All rights reserved. +/* Copyright (c) 2010-2014, 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 @@ -14,12 +14,26 @@ #define SCM_SVC_BOOT 0x1 #define SCM_SVC_PIL 0x2 +#define SCM_SVC_UTIL 0x3 +#define SCM_SVC_TZ 0x4 +#define SCM_SVC_IO 0x5 +#define SCM_SVC_INFO 0x6 +#define SCM_SVC_SSD 0x7 +#define SCM_SVC_FUSE 0x8 +#define SCM_SVC_PWR 0x9 +#define SCM_SVC_CP 0xC +#define SCM_SVC_DCVS 0xD +#define SCM_SVC_TZSCHEDULER 0xFC extern int scm_call(u32 svc_id, u32 cmd_id, const void *cmd_buf, size_t cmd_len, void *resp_buf, size_t resp_len); +extern s32 scm_call_atomic1(u32 svc, u32 cmd, u32 arg1); +extern s32 scm_call_atomic2(u32 svc, u32 cmd, u32 arg1, u32 arg2); + #define SCM_VERSION(major, minor) (((major) << 16) | ((minor) & 0xFF)) extern u32 scm_get_version(void); +extern int scm_is_call_available(u32 svc_id, u32 cmd_id); #endif |