| /* | |
| * This program is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License version 2 as | |
| * published by the Free Software Foundation. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU General Public License for more details. | |
| * | |
| * Copyright (C) 2012 ARM Limited | |
| * | |
| * Author: Will Deacon <will.deacon@arm.com> | |
| */ | |
| #define pr_fmt(fmt) "psci: " fmt | |
| #include <linux/init.h> | |
| #include <linux/of.h> | |
| #include <asm/compiler.h> | |
| #include <asm/errno.h> | |
| #include <asm/opcodes-sec.h> | |
| #include <asm/opcodes-virt.h> | |
| #include <asm/psci.h> | |
| struct psci_operations psci_ops; | |
| static int (*invoke_psci_fn)(u32, u32, u32, u32); | |
| enum psci_function { | |
| PSCI_FN_CPU_SUSPEND, | |
| PSCI_FN_CPU_ON, | |
| PSCI_FN_CPU_OFF, | |
| PSCI_FN_MIGRATE, | |
| PSCI_FN_MAX, | |
| }; | |
| static u32 psci_function_id[PSCI_FN_MAX]; | |
| #define PSCI_RET_SUCCESS 0 | |
| #define PSCI_RET_EOPNOTSUPP -1 | |
| #define PSCI_RET_EINVAL -2 | |
| #define PSCI_RET_EPERM -3 | |
| static int psci_to_linux_errno(int errno) | |
| { | |
| switch (errno) { | |
| case PSCI_RET_SUCCESS: | |
| return 0; | |
| case PSCI_RET_EOPNOTSUPP: | |
| return -EOPNOTSUPP; | |
| case PSCI_RET_EINVAL: | |
| return -EINVAL; | |
| case PSCI_RET_EPERM: | |
| return -EPERM; | |
| }; | |
| return -EINVAL; | |
| } | |
| #define PSCI_POWER_STATE_ID_MASK 0xffff | |
| #define PSCI_POWER_STATE_ID_SHIFT 0 | |
| #define PSCI_POWER_STATE_TYPE_MASK 0x1 | |
| #define PSCI_POWER_STATE_TYPE_SHIFT 16 | |
| #define PSCI_POWER_STATE_AFFL_MASK 0x3 | |
| #define PSCI_POWER_STATE_AFFL_SHIFT 24 | |
| static u32 psci_power_state_pack(struct psci_power_state state) | |
| { | |
| return ((state.id & PSCI_POWER_STATE_ID_MASK) | |
| << PSCI_POWER_STATE_ID_SHIFT) | | |
| ((state.type & PSCI_POWER_STATE_TYPE_MASK) | |
| << PSCI_POWER_STATE_TYPE_SHIFT) | | |
| ((state.affinity_level & PSCI_POWER_STATE_AFFL_MASK) | |
| << PSCI_POWER_STATE_AFFL_SHIFT); | |
| } | |
| /* | |
| * The following two functions are invoked via the invoke_psci_fn pointer | |
| * and will not be inlined, allowing us to piggyback on the AAPCS. | |
| */ | |
| static noinline int __invoke_psci_fn_hvc(u32 function_id, u32 arg0, u32 arg1, | |
| u32 arg2) | |
| { | |
| asm volatile( | |
| __asmeq("%0", "r0") | |
| __asmeq("%1", "r1") | |
| __asmeq("%2", "r2") | |
| __asmeq("%3", "r3") | |
| __HVC(0) | |
| : "+r" (function_id) | |
| : "r" (arg0), "r" (arg1), "r" (arg2)); | |
| return function_id; | |
| } | |
| static noinline int __invoke_psci_fn_smc(u32 function_id, u32 arg0, u32 arg1, | |
| u32 arg2) | |
| { | |
| asm volatile( | |
| __asmeq("%0", "r0") | |
| __asmeq("%1", "r1") | |
| __asmeq("%2", "r2") | |
| __asmeq("%3", "r3") | |
| __SMC(0) | |
| : "+r" (function_id) | |
| : "r" (arg0), "r" (arg1), "r" (arg2)); | |
| return function_id; | |
| } | |
| static int psci_cpu_suspend(struct psci_power_state state, | |
| unsigned long entry_point) | |
| { | |
| int err; | |
| u32 fn, power_state; | |
| fn = psci_function_id[PSCI_FN_CPU_SUSPEND]; | |
| power_state = psci_power_state_pack(state); | |
| err = invoke_psci_fn(fn, power_state, entry_point, 0); | |
| return psci_to_linux_errno(err); | |
| } | |
| static int psci_cpu_off(struct psci_power_state state) | |
| { | |
| int err; | |
| u32 fn, power_state; | |
| fn = psci_function_id[PSCI_FN_CPU_OFF]; | |
| power_state = psci_power_state_pack(state); | |
| err = invoke_psci_fn(fn, power_state, 0, 0); | |
| return psci_to_linux_errno(err); | |
| } | |
| static int psci_cpu_on(unsigned long cpuid, unsigned long entry_point) | |
| { | |
| int err; | |
| u32 fn; | |
| fn = psci_function_id[PSCI_FN_CPU_ON]; | |
| err = invoke_psci_fn(fn, cpuid, entry_point, 0); | |
| return psci_to_linux_errno(err); | |
| } | |
| static int psci_migrate(unsigned long cpuid) | |
| { | |
| int err; | |
| u32 fn; | |
| fn = psci_function_id[PSCI_FN_MIGRATE]; | |
| err = invoke_psci_fn(fn, cpuid, 0, 0); | |
| return psci_to_linux_errno(err); | |
| } | |
| static const struct of_device_id psci_of_match[] __initconst = { | |
| { .compatible = "arm,psci", }, | |
| {}, | |
| }; | |
| static int __init psci_init(void) | |
| { | |
| struct device_node *np; | |
| const char *method; | |
| u32 id; | |
| np = of_find_matching_node(NULL, psci_of_match); | |
| if (!np) | |
| return 0; | |
| pr_info("probing function IDs from device-tree\n"); | |
| if (of_property_read_string(np, "method", &method)) { | |
| pr_warning("missing \"method\" property\n"); | |
| goto out_put_node; | |
| } | |
| if (!strcmp("hvc", method)) { | |
| invoke_psci_fn = __invoke_psci_fn_hvc; | |
| } else if (!strcmp("smc", method)) { | |
| invoke_psci_fn = __invoke_psci_fn_smc; | |
| } else { | |
| pr_warning("invalid \"method\" property: %s\n", method); | |
| goto out_put_node; | |
| } | |
| if (!of_property_read_u32(np, "cpu_suspend", &id)) { | |
| psci_function_id[PSCI_FN_CPU_SUSPEND] = id; | |
| psci_ops.cpu_suspend = psci_cpu_suspend; | |
| } | |
| if (!of_property_read_u32(np, "cpu_off", &id)) { | |
| psci_function_id[PSCI_FN_CPU_OFF] = id; | |
| psci_ops.cpu_off = psci_cpu_off; | |
| } | |
| if (!of_property_read_u32(np, "cpu_on", &id)) { | |
| psci_function_id[PSCI_FN_CPU_ON] = id; | |
| psci_ops.cpu_on = psci_cpu_on; | |
| } | |
| if (!of_property_read_u32(np, "migrate", &id)) { | |
| psci_function_id[PSCI_FN_MIGRATE] = id; | |
| psci_ops.migrate = psci_migrate; | |
| } | |
| out_put_node: | |
| of_node_put(np); | |
| return 0; | |
| } | |
| early_initcall(psci_init); |