diff options
Diffstat (limited to 'arch/arm/kernel/psci.c')
| -rw-r--r-- | arch/arm/kernel/psci.c | 211 | 
1 files changed, 211 insertions, 0 deletions
| diff --git a/arch/arm/kernel/psci.c b/arch/arm/kernel/psci.c new file mode 100644 index 00000000000..36531643cc2 --- /dev/null +++ b/arch/arm/kernel/psci.c @@ -0,0 +1,211 @@ +/* + * 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); | 
