diff options
Diffstat (limited to 'arch/arm/mach-mvebu/pmsu.c')
-rw-r--r-- | arch/arm/mach-mvebu/pmsu.c | 606 |
1 files changed, 606 insertions, 0 deletions
diff --git a/arch/arm/mach-mvebu/pmsu.c b/arch/arm/mach-mvebu/pmsu.c new file mode 100644 index 000000000..af27a7156 --- /dev/null +++ b/arch/arm/mach-mvebu/pmsu.c @@ -0,0 +1,606 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Power Management Service Unit(PMSU) support for Armada 370/XP platforms. + * + * Copyright (C) 2012 Marvell + * + * Yehuda Yitschak <yehuday@marvell.com> + * Gregory Clement <gregory.clement@free-electrons.com> + * Thomas Petazzoni <thomas.petazzoni@free-electrons.com> + * + * The Armada 370 and Armada XP SOCs have a power management service + * unit which is responsible for powering down and waking up CPUs and + * other SOC units + */ + +#define pr_fmt(fmt) "mvebu-pmsu: " fmt + +#include <linux/clk.h> +#include <linux/cpu_pm.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/mbus.h> +#include <linux/mvebu-pmsu.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/resource.h> +#include <linux/slab.h> +#include <linux/smp.h> +#include <asm/cacheflush.h> +#include <asm/cp15.h> +#include <asm/smp_scu.h> +#include <asm/smp_plat.h> +#include <asm/suspend.h> +#include <asm/tlbflush.h> +#include "common.h" +#include "pmsu.h" + +#define PMSU_BASE_OFFSET 0x100 +#define PMSU_REG_SIZE 0x1000 + +/* PMSU MP registers */ +#define PMSU_CONTROL_AND_CONFIG(cpu) ((cpu * 0x100) + 0x104) +#define PMSU_CONTROL_AND_CONFIG_DFS_REQ BIT(18) +#define PMSU_CONTROL_AND_CONFIG_PWDDN_REQ BIT(16) +#define PMSU_CONTROL_AND_CONFIG_L2_PWDDN BIT(20) + +#define PMSU_CPU_POWER_DOWN_CONTROL(cpu) ((cpu * 0x100) + 0x108) + +#define PMSU_CPU_POWER_DOWN_DIS_SNP_Q_SKIP BIT(0) + +#define PMSU_STATUS_AND_MASK(cpu) ((cpu * 0x100) + 0x10c) +#define PMSU_STATUS_AND_MASK_CPU_IDLE_WAIT BIT(16) +#define PMSU_STATUS_AND_MASK_SNP_Q_EMPTY_WAIT BIT(17) +#define PMSU_STATUS_AND_MASK_IRQ_WAKEUP BIT(20) +#define PMSU_STATUS_AND_MASK_FIQ_WAKEUP BIT(21) +#define PMSU_STATUS_AND_MASK_DBG_WAKEUP BIT(22) +#define PMSU_STATUS_AND_MASK_IRQ_MASK BIT(24) +#define PMSU_STATUS_AND_MASK_FIQ_MASK BIT(25) + +#define PMSU_EVENT_STATUS_AND_MASK(cpu) ((cpu * 0x100) + 0x120) +#define PMSU_EVENT_STATUS_AND_MASK_DFS_DONE BIT(1) +#define PMSU_EVENT_STATUS_AND_MASK_DFS_DONE_MASK BIT(17) + +#define PMSU_BOOT_ADDR_REDIRECT_OFFSET(cpu) ((cpu * 0x100) + 0x124) + +/* PMSU fabric registers */ +#define L2C_NFABRIC_PM_CTL 0x4 +#define L2C_NFABRIC_PM_CTL_PWR_DOWN BIT(20) + +/* PMSU delay registers */ +#define PMSU_POWERDOWN_DELAY 0xF04 +#define PMSU_POWERDOWN_DELAY_PMU BIT(1) +#define PMSU_POWERDOWN_DELAY_MASK 0xFFFE +#define PMSU_DFLT_ARMADA38X_DELAY 0x64 + +/* CA9 MPcore SoC Control registers */ + +#define MPCORE_RESET_CTL 0x64 +#define MPCORE_RESET_CTL_L2 BIT(0) +#define MPCORE_RESET_CTL_DEBUG BIT(16) + +#define SRAM_PHYS_BASE 0xFFFF0000 +#define BOOTROM_BASE 0xFFF00000 +#define BOOTROM_SIZE 0x100000 + +#define ARMADA_370_CRYPT0_ENG_TARGET 0x9 +#define ARMADA_370_CRYPT0_ENG_ATTR 0x1 + +extern void ll_disable_coherency(void); +extern void ll_enable_coherency(void); + +extern void armada_370_xp_cpu_resume(void); +extern void armada_38x_cpu_resume(void); + +static phys_addr_t pmsu_mp_phys_base; +static void __iomem *pmsu_mp_base; + +static void *mvebu_cpu_resume; + +static const struct of_device_id of_pmsu_table[] = { + { .compatible = "marvell,armada-370-pmsu", }, + { .compatible = "marvell,armada-370-xp-pmsu", }, + { .compatible = "marvell,armada-380-pmsu", }, + { /* end of list */ }, +}; + +void mvebu_pmsu_set_cpu_boot_addr(int hw_cpu, void *boot_addr) +{ + writel(__pa_symbol(boot_addr), pmsu_mp_base + + PMSU_BOOT_ADDR_REDIRECT_OFFSET(hw_cpu)); +} + +extern unsigned char mvebu_boot_wa_start[]; +extern unsigned char mvebu_boot_wa_end[]; + +/* + * This function sets up the boot address workaround needed for SMP + * boot on Armada 375 Z1 and cpuidle on Armada 370. It unmaps the + * BootROM Mbus window, and instead remaps a crypto SRAM into which a + * custom piece of code is copied to replace the problematic BootROM. + */ +int mvebu_setup_boot_addr_wa(unsigned int crypto_eng_target, + unsigned int crypto_eng_attribute, + phys_addr_t resume_addr_reg) +{ + void __iomem *sram_virt_base; + u32 code_len = mvebu_boot_wa_end - mvebu_boot_wa_start; + + mvebu_mbus_del_window(BOOTROM_BASE, BOOTROM_SIZE); + mvebu_mbus_add_window_by_id(crypto_eng_target, crypto_eng_attribute, + SRAM_PHYS_BASE, SZ_64K); + + sram_virt_base = ioremap(SRAM_PHYS_BASE, SZ_64K); + if (!sram_virt_base) { + pr_err("Unable to map SRAM to setup the boot address WA\n"); + return -ENOMEM; + } + + memcpy(sram_virt_base, &mvebu_boot_wa_start, code_len); + + /* + * The last word of the code copied in SRAM must contain the + * physical base address of the PMSU register. We + * intentionally store this address in the native endianness + * of the system. + */ + __raw_writel((unsigned long)resume_addr_reg, + sram_virt_base + code_len - 4); + + iounmap(sram_virt_base); + + return 0; +} + +static int __init mvebu_v7_pmsu_init(void) +{ + struct device_node *np; + struct resource res; + int ret = 0; + + np = of_find_matching_node(NULL, of_pmsu_table); + if (!np) + return 0; + + pr_info("Initializing Power Management Service Unit\n"); + + if (of_address_to_resource(np, 0, &res)) { + pr_err("unable to get resource\n"); + ret = -ENOENT; + goto out; + } + + if (of_device_is_compatible(np, "marvell,armada-370-xp-pmsu")) { + pr_warn(FW_WARN "deprecated pmsu binding\n"); + res.start = res.start - PMSU_BASE_OFFSET; + res.end = res.start + PMSU_REG_SIZE - 1; + } + + if (!request_mem_region(res.start, resource_size(&res), + np->full_name)) { + pr_err("unable to request region\n"); + ret = -EBUSY; + goto out; + } + + pmsu_mp_phys_base = res.start; + + pmsu_mp_base = ioremap(res.start, resource_size(&res)); + if (!pmsu_mp_base) { + pr_err("unable to map registers\n"); + release_mem_region(res.start, resource_size(&res)); + ret = -ENOMEM; + goto out; + } + + out: + of_node_put(np); + return ret; +} + +static void mvebu_v7_pmsu_enable_l2_powerdown_onidle(void) +{ + u32 reg; + + if (pmsu_mp_base == NULL) + return; + + /* Enable L2 & Fabric powerdown in Deep-Idle mode - Fabric */ + reg = readl(pmsu_mp_base + L2C_NFABRIC_PM_CTL); + reg |= L2C_NFABRIC_PM_CTL_PWR_DOWN; + writel(reg, pmsu_mp_base + L2C_NFABRIC_PM_CTL); +} + +enum pmsu_idle_prepare_flags { + PMSU_PREPARE_NORMAL = 0, + PMSU_PREPARE_DEEP_IDLE = BIT(0), + PMSU_PREPARE_SNOOP_DISABLE = BIT(1), +}; + +/* No locking is needed because we only access per-CPU registers */ +static int mvebu_v7_pmsu_idle_prepare(unsigned long flags) +{ + unsigned int hw_cpu = cpu_logical_map(smp_processor_id()); + u32 reg; + + if (pmsu_mp_base == NULL) + return -EINVAL; + + /* + * Adjust the PMSU configuration to wait for WFI signal, enable + * IRQ and FIQ as wakeup events, set wait for snoop queue empty + * indication and mask IRQ and FIQ from CPU + */ + reg = readl(pmsu_mp_base + PMSU_STATUS_AND_MASK(hw_cpu)); + reg |= PMSU_STATUS_AND_MASK_CPU_IDLE_WAIT | + PMSU_STATUS_AND_MASK_IRQ_WAKEUP | + PMSU_STATUS_AND_MASK_FIQ_WAKEUP | + PMSU_STATUS_AND_MASK_SNP_Q_EMPTY_WAIT | + PMSU_STATUS_AND_MASK_IRQ_MASK | + PMSU_STATUS_AND_MASK_FIQ_MASK; + writel(reg, pmsu_mp_base + PMSU_STATUS_AND_MASK(hw_cpu)); + + reg = readl(pmsu_mp_base + PMSU_CONTROL_AND_CONFIG(hw_cpu)); + /* ask HW to power down the L2 Cache if needed */ + if (flags & PMSU_PREPARE_DEEP_IDLE) + reg |= PMSU_CONTROL_AND_CONFIG_L2_PWDDN; + + /* request power down */ + reg |= PMSU_CONTROL_AND_CONFIG_PWDDN_REQ; + writel(reg, pmsu_mp_base + PMSU_CONTROL_AND_CONFIG(hw_cpu)); + + if (flags & PMSU_PREPARE_SNOOP_DISABLE) { + /* Disable snoop disable by HW - SW is taking care of it */ + reg = readl(pmsu_mp_base + PMSU_CPU_POWER_DOWN_CONTROL(hw_cpu)); + reg |= PMSU_CPU_POWER_DOWN_DIS_SNP_Q_SKIP; + writel(reg, pmsu_mp_base + PMSU_CPU_POWER_DOWN_CONTROL(hw_cpu)); + } + + return 0; +} + +int armada_370_xp_pmsu_idle_enter(unsigned long deepidle) +{ + unsigned long flags = PMSU_PREPARE_SNOOP_DISABLE; + int ret; + + if (deepidle) + flags |= PMSU_PREPARE_DEEP_IDLE; + + ret = mvebu_v7_pmsu_idle_prepare(flags); + if (ret) + return ret; + + v7_exit_coherency_flush(all); + + ll_disable_coherency(); + + dsb(); + + wfi(); + + /* If we are here, wfi failed. As processors run out of + * coherency for some time, tlbs might be stale, so flush them + */ + local_flush_tlb_all(); + + ll_enable_coherency(); + + /* Test the CR_C bit and set it if it was cleared */ + asm volatile( + "mrc p15, 0, r0, c1, c0, 0 \n\t" + "tst r0, %0 \n\t" + "orreq r0, r0, #(1 << 2) \n\t" + "mcreq p15, 0, r0, c1, c0, 0 \n\t" + "isb " + : : "Ir" (CR_C) : "r0"); + + pr_debug("Failed to suspend the system\n"); + + return 0; +} + +static int armada_370_xp_cpu_suspend(unsigned long deepidle) +{ + return cpu_suspend(deepidle, armada_370_xp_pmsu_idle_enter); +} + +int armada_38x_do_cpu_suspend(unsigned long deepidle) +{ + unsigned long flags = 0; + + if (deepidle) + flags |= PMSU_PREPARE_DEEP_IDLE; + + mvebu_v7_pmsu_idle_prepare(flags); + /* + * Already flushed cache, but do it again as the outer cache + * functions dirty the cache with spinlocks + */ + v7_exit_coherency_flush(louis); + + scu_power_mode(mvebu_get_scu_base(), SCU_PM_POWEROFF); + + cpu_do_idle(); + + return 1; +} + +static int armada_38x_cpu_suspend(unsigned long deepidle) +{ + return cpu_suspend(false, armada_38x_do_cpu_suspend); +} + +/* No locking is needed because we only access per-CPU registers */ +void mvebu_v7_pmsu_idle_exit(void) +{ + unsigned int hw_cpu = cpu_logical_map(smp_processor_id()); + u32 reg; + + if (pmsu_mp_base == NULL) + return; + /* cancel ask HW to power down the L2 Cache if possible */ + reg = readl(pmsu_mp_base + PMSU_CONTROL_AND_CONFIG(hw_cpu)); + reg &= ~PMSU_CONTROL_AND_CONFIG_L2_PWDDN; + writel(reg, pmsu_mp_base + PMSU_CONTROL_AND_CONFIG(hw_cpu)); + + /* cancel Enable wakeup events and mask interrupts */ + reg = readl(pmsu_mp_base + PMSU_STATUS_AND_MASK(hw_cpu)); + reg &= ~(PMSU_STATUS_AND_MASK_IRQ_WAKEUP | PMSU_STATUS_AND_MASK_FIQ_WAKEUP); + reg &= ~PMSU_STATUS_AND_MASK_CPU_IDLE_WAIT; + reg &= ~PMSU_STATUS_AND_MASK_SNP_Q_EMPTY_WAIT; + reg &= ~(PMSU_STATUS_AND_MASK_IRQ_MASK | PMSU_STATUS_AND_MASK_FIQ_MASK); + writel(reg, pmsu_mp_base + PMSU_STATUS_AND_MASK(hw_cpu)); +} + +static int mvebu_v7_cpu_pm_notify(struct notifier_block *self, + unsigned long action, void *hcpu) +{ + if (action == CPU_PM_ENTER) { + unsigned int hw_cpu = cpu_logical_map(smp_processor_id()); + mvebu_pmsu_set_cpu_boot_addr(hw_cpu, mvebu_cpu_resume); + } else if (action == CPU_PM_EXIT) { + mvebu_v7_pmsu_idle_exit(); + } + + return NOTIFY_OK; +} + +static struct notifier_block mvebu_v7_cpu_pm_notifier = { + .notifier_call = mvebu_v7_cpu_pm_notify, +}; + +static struct platform_device mvebu_v7_cpuidle_device; + +static int broken_idle(struct device_node *np) +{ + if (of_property_read_bool(np, "broken-idle")) { + pr_warn("CPU idle is currently broken: disabling\n"); + return 1; + } + + return 0; +} + +static __init int armada_370_cpuidle_init(void) +{ + struct device_node *np; + phys_addr_t redirect_reg; + + np = of_find_compatible_node(NULL, NULL, "marvell,coherency-fabric"); + if (!np) + return -ENODEV; + + if (broken_idle(np)) + goto end; + + /* + * On Armada 370, there is "a slow exit process from the deep + * idle state due to heavy L1/L2 cache cleanup operations + * performed by the BootROM software". To avoid this, we + * replace the restart code of the bootrom by a a simple jump + * to the boot address. Then the code located at this boot + * address will take care of the initialization. + */ + redirect_reg = pmsu_mp_phys_base + PMSU_BOOT_ADDR_REDIRECT_OFFSET(0); + mvebu_setup_boot_addr_wa(ARMADA_370_CRYPT0_ENG_TARGET, + ARMADA_370_CRYPT0_ENG_ATTR, + redirect_reg); + + mvebu_cpu_resume = armada_370_xp_cpu_resume; + mvebu_v7_cpuidle_device.dev.platform_data = armada_370_xp_cpu_suspend; + mvebu_v7_cpuidle_device.name = "cpuidle-armada-370"; + +end: + of_node_put(np); + return 0; +} + +static __init int armada_38x_cpuidle_init(void) +{ + struct device_node *np; + void __iomem *mpsoc_base; + u32 reg; + + pr_warn("CPU idle is currently broken on Armada 38x: disabling\n"); + return 0; + + np = of_find_compatible_node(NULL, NULL, + "marvell,armada-380-coherency-fabric"); + if (!np) + return -ENODEV; + + if (broken_idle(np)) + goto end; + + of_node_put(np); + + np = of_find_compatible_node(NULL, NULL, + "marvell,armada-380-mpcore-soc-ctrl"); + if (!np) + return -ENODEV; + mpsoc_base = of_iomap(np, 0); + BUG_ON(!mpsoc_base); + + /* Set up reset mask when powering down the cpus */ + reg = readl(mpsoc_base + MPCORE_RESET_CTL); + reg |= MPCORE_RESET_CTL_L2; + reg |= MPCORE_RESET_CTL_DEBUG; + writel(reg, mpsoc_base + MPCORE_RESET_CTL); + iounmap(mpsoc_base); + + /* Set up delay */ + reg = readl(pmsu_mp_base + PMSU_POWERDOWN_DELAY); + reg &= ~PMSU_POWERDOWN_DELAY_MASK; + reg |= PMSU_DFLT_ARMADA38X_DELAY; + reg |= PMSU_POWERDOWN_DELAY_PMU; + writel(reg, pmsu_mp_base + PMSU_POWERDOWN_DELAY); + + mvebu_cpu_resume = armada_38x_cpu_resume; + mvebu_v7_cpuidle_device.dev.platform_data = armada_38x_cpu_suspend; + mvebu_v7_cpuidle_device.name = "cpuidle-armada-38x"; + +end: + of_node_put(np); + return 0; +} + +static __init int armada_xp_cpuidle_init(void) +{ + struct device_node *np; + + np = of_find_compatible_node(NULL, NULL, "marvell,coherency-fabric"); + if (!np) + return -ENODEV; + + if (broken_idle(np)) + goto end; + + mvebu_cpu_resume = armada_370_xp_cpu_resume; + mvebu_v7_cpuidle_device.dev.platform_data = armada_370_xp_cpu_suspend; + mvebu_v7_cpuidle_device.name = "cpuidle-armada-xp"; + +end: + of_node_put(np); + return 0; +} + +static int __init mvebu_v7_cpu_pm_init(void) +{ + struct device_node *np; + int ret; + + np = of_find_matching_node(NULL, of_pmsu_table); + if (!np) + return 0; + of_node_put(np); + + /* + * Currently the CPU idle support for Armada 38x is broken, as + * the CPU hotplug uses some of the CPU idle functions it is + * broken too, so let's disable it + */ + if (of_machine_is_compatible("marvell,armada380")) { + cpu_hotplug_disable(); + pr_warn("CPU hotplug support is currently broken on Armada 38x: disabling\n"); + } + + if (of_machine_is_compatible("marvell,armadaxp")) + ret = armada_xp_cpuidle_init(); + else if (of_machine_is_compatible("marvell,armada370")) + ret = armada_370_cpuidle_init(); + else if (of_machine_is_compatible("marvell,armada380")) + ret = armada_38x_cpuidle_init(); + else + return 0; + + if (ret) + return ret; + + mvebu_v7_pmsu_enable_l2_powerdown_onidle(); + if (mvebu_v7_cpuidle_device.name) + platform_device_register(&mvebu_v7_cpuidle_device); + cpu_pm_register_notifier(&mvebu_v7_cpu_pm_notifier); + + return 0; +} + +arch_initcall(mvebu_v7_cpu_pm_init); +early_initcall(mvebu_v7_pmsu_init); + +static void mvebu_pmsu_dfs_request_local(void *data) +{ + u32 reg; + u32 cpu = smp_processor_id(); + unsigned long flags; + + local_irq_save(flags); + + /* Prepare to enter idle */ + reg = readl(pmsu_mp_base + PMSU_STATUS_AND_MASK(cpu)); + reg |= PMSU_STATUS_AND_MASK_CPU_IDLE_WAIT | + PMSU_STATUS_AND_MASK_IRQ_MASK | + PMSU_STATUS_AND_MASK_FIQ_MASK; + writel(reg, pmsu_mp_base + PMSU_STATUS_AND_MASK(cpu)); + + /* Request the DFS transition */ + reg = readl(pmsu_mp_base + PMSU_CONTROL_AND_CONFIG(cpu)); + reg |= PMSU_CONTROL_AND_CONFIG_DFS_REQ; + writel(reg, pmsu_mp_base + PMSU_CONTROL_AND_CONFIG(cpu)); + + /* The fact of entering idle will trigger the DFS transition */ + wfi(); + + /* + * We're back from idle, the DFS transition has completed, + * clear the idle wait indication. + */ + reg = readl(pmsu_mp_base + PMSU_STATUS_AND_MASK(cpu)); + reg &= ~PMSU_STATUS_AND_MASK_CPU_IDLE_WAIT; + writel(reg, pmsu_mp_base + PMSU_STATUS_AND_MASK(cpu)); + + local_irq_restore(flags); +} + +int mvebu_pmsu_dfs_request(int cpu) +{ + unsigned long timeout; + int hwcpu = cpu_logical_map(cpu); + u32 reg; + + /* Clear any previous DFS DONE event */ + reg = readl(pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + reg &= ~PMSU_EVENT_STATUS_AND_MASK_DFS_DONE; + writel(reg, pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + + /* Mask the DFS done interrupt, since we are going to poll */ + reg = readl(pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + reg |= PMSU_EVENT_STATUS_AND_MASK_DFS_DONE_MASK; + writel(reg, pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + + /* Trigger the DFS on the appropriate CPU */ + smp_call_function_single(cpu, mvebu_pmsu_dfs_request_local, + NULL, false); + + /* Poll until the DFS done event is generated */ + timeout = jiffies + HZ; + while (time_before(jiffies, timeout)) { + reg = readl(pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + if (reg & PMSU_EVENT_STATUS_AND_MASK_DFS_DONE) + break; + udelay(10); + } + + if (time_after(jiffies, timeout)) + return -ETIME; + + /* Restore the DFS mask to its original state */ + reg = readl(pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + reg &= ~PMSU_EVENT_STATUS_AND_MASK_DFS_DONE_MASK; + writel(reg, pmsu_mp_base + PMSU_EVENT_STATUS_AND_MASK(hwcpu)); + + return 0; +} |