[PATCH v4 10/11] ARM: OMAP2+: AM33XX: Basic suspend resume support

Dave Gerlach
2014-07-11T02:57:52Z

AM335x supports various low power modes as documented
in section 8.1.4.3 of the AM335x Technical Reference Manual.

DeepSleep0 mode offers the lowest power mode with limited
wakeup sources without a system reboot and is mapped as
the suspend state in the kernel. In this state, MPU and
PER domains are turned off with the internal RAM held in
retention to facilitate the resume process. As part of
the boot process, the assembly code is copied over to OCMCRAM
using the OMAP SRAM code so it can be executed to turn of the
EMIF.

AM335x has a Cortex-M3 (WKUP_M3) which assists the MPU
in DeepSleep0 and Standby entry and exit. WKUP_M3 takes care
of the clockdomain and powerdomain transitions based on the
intended low power state. MPU needs to load the appropriate
WKUP_M3 binary onto the WKUP_M3 memory space before it can
leverage any of the PM features like DeepSleep.

The WKUP_M3 is managed by a remoteproc driver. The PM code hooks
into the remoteproc driver through an rproc_ready callback to
allow PM features to become available once the firmware for the
wkup_m3 has been loaded. This code maintains its own state machine
for the wkup_m3 so that it can be used in the manner intended for
low power modes.

In the current implementation when the suspend process
is initiated, MPU interrupts the WKUP_M3 to let it know about
the intent of entering DeepSleep0 and waits for an ACK. When
the ACK is received MPU continues with its suspend process
to suspend all the drivers and then jumps to assembly in
OCMC RAM. The assembly code puts the external RAM in self-refresh
mode, gates the MPU clock, and then finally executes the WFI
instruction. Execution of the WFI instruction with MPU clock gated
triggers another interrupt to the WKUP_M3 which then continues
with the power down sequence wherein the clockdomain and
powerdomain transition takes place. As part of the sleep sequence,
WKUP_M3 unmasks the interrupt lines for the wakeup sources. WFI
execution on WKUP_M3 causes the hardware to disable the main
oscillator of the SoC and from here system remains in sleep state
until a wake source brings the system into resume path.

When a wakeup event occurs, WKUP_M3 starts the power-up
sequence by switching on the power domains and finally
enabling the clock to MPU. Since the MPU gets powered down
as part of the sleep sequence in the resume path ROM code
starts executing. The ROM code detects a wakeup from sleep
and then jumps to the resume location in OCMC which was
populated in one of the IPC registers as part of the suspend
sequence.

Code is based on work by Vaibhav Bedia.

Signed-off-by: Dave Gerlach <d-gerlach@ti.com>
---
v3->v4:
Remove all direct wkup_m3 usage and moved to rproc driver introduced
in previous patch.

arch/arm/mach-omap2/pm33xx.c | 346 +++++++++++++++++++++++++++++++++++++++++++
arch/arm/mach-omap2/pm33xx.h | 64 ++++++++
2 files changed, 410 insertions(+)
create mode 100644 arch/arm/mach-omap2/pm33xx.c
create mode 100644 arch/arm/mach-omap2/pm33xx.h

diff --git a/arch/arm/mach-omap2/pm33xx.c b/arch/arm/mach-omap2/pm33xx.c
new file mode 100644
index 0000000..30d0f7d
--- /dev/null
+++ b/arch/arm/mach-omap2/pm33xx.c
@@ -0,0 +1,346 @@
+/*
+ * AM33XX Power Management Routines
+ *
+ * Copyright (C) 2012-2014 Texas Instruments Incorporated - http://www.ti.com/
+ * Vaibhav Bedia, Dave Gerlach
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation version 2.
+ *
+ * This program is distributed "as is" WITHOUT ANY WARRANTY of any
+ * kind, whether express or implied; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/cpu.h>
+#include <linux/err.h>
+#include <linux/firmware.h>
+#include <linux/io.h>
+#include <linux/platform_device.h>
+#include <linux/sched.h>
+#include <linux/suspend.h>
+#include <linux/completion.h>
+#include <linux/module.h>
+#include <linux/interrupt.h>
+#include <linux/ti_emif.h>
+#include <linux/omap-mailbox.h>
+#include <linux/sizes.h>
+
+#include <asm/suspend.h>
+#include <asm/proc-fns.h>
+#include <asm/fncpy.h>
+#include <asm/system_misc.h>
+
+#include "pm.h"
+#include "cm33xx.h"
+#include "pm33xx.h"
+#include "common.h"
+#include "clockdomain.h"
+#include "powerdomain.h"
+#include "soc.h"
+#include "sram.h"
+
+static void __iomem *am33xx_emif_base;
+static struct powerdomain *cefuse_pwrdm, *gfx_pwrdm, *per_pwrdm, *mpu_pwrdm;
+static struct clockdomain *gfx_l4ls_clkdm;
+
+static struct am33xx_pm_context *am33xx_pm;
+
+static DECLARE_COMPLETION(am33xx_pm_sync);
+
+static void (*am33xx_do_wfi_sram)(struct am33xx_suspend_params *);
+
+static struct am33xx_suspend_params susp_params;
+
+#ifdef CONFIG_SUSPEND
+
+static int am33xx_do_sram_idle(long unsigned int unused)
+{
+ am33xx_do_wfi_sram(&susp_params);
+ return 0;
+}
+
+static int am33xx_pm_suspend(void)
+{
+ int i, ret = 0;
+ int status = 0;
+ struct wkup_m3_wakeup_src wakeup_src = {.irq_nr = 0,
+ .src = "Unknown",};
+
+ /* Try to put GFX to sleep */
+ omap_set_pwrdm_state(gfx_pwrdm, PWRDM_POWER_OFF);
+
+ ret = cpu_suspend(0, am33xx_do_sram_idle);
+
+ status = pwrdm_read_pwrst(gfx_pwrdm);
+ if (status != PWRDM_POWER_OFF)
+ pr_err("PM: GFX domain did not transition\n");
+
+ /*
+ * BUG: GFX_L4LS clock domain needs to be woken up to
+ * ensure thet L4LS clock domain does not get stuck in transition
+ * If that happens L3 module does not get disabled, thereby leading
+ * to PER power domain transition failing
+ */
+ clkdm_wakeup(gfx_l4ls_clkdm);
+ clkdm_sleep(gfx_l4ls_clkdm);
+
+ if (ret) {
+ pr_err("PM: Kernel suspend failure\n");
+ } else {
+ i = wkup_m3_pm_status();
+
+ switch (i) {
+ case 0:
+ pr_info("PM: Successfully put all powerdomains to target state\n");
+
+ /*
+ * The PRCM registers on AM335x do not contain
+ * previous state information like those present on
+ * OMAP4 so we must manually indicate transition so
+ * state counters are properly incremented
+ */
+ pwrdm_post_transition(mpu_pwrdm);
+ pwrdm_post_transition(per_pwrdm);
+ break;
+ case 1:
+ pr_err("PM: Could not transition all powerdomains to target state\n");
+ ret = -1;
+ break;
+ default:
+ pr_err("PM: CM3 returned unknown result = %d\n", i);
+ ret = -1;
+ }
+ /* print the wakeup reason */
+ wkup_m3_wake_src(&wakeup_src);
+
+ pr_info("PM: Wakeup source %s\n", wakeup_src.src);
+ }
+
+ return ret;
+}
+
+static int am33xx_pm_enter(suspend_state_t suspend_state)
+{
+ int ret = 0;
+
+ switch (suspend_state) {
+ case PM_SUSPEND_MEM:
+ ret = am33xx_pm_suspend();
+ break;
+ default:
+ ret = -EINVAL;
+ }
+
+ return ret;
+}
+
+
+static void am33xx_m3_state_machine_reset(void)
+{
+ int i;
+
+ am33xx_pm->ipc.reg1 = IPC_CMD_RESET;
+
+ wkup_m3_set_cmd(&am33xx_pm->ipc);
+
+ am33xx_pm->state = M3_STATE_MSG_FOR_RESET;
+
+ if (!wkup_m3_ping()) {
+ i = wait_for_completion_timeout(&am33xx_pm_sync,
+ msecs_to_jiffies(500));
+ if (!i) {
+ WARN(1, "PM: MPU<->CM3 sync failure\n");
+ am33xx_pm->state = M3_STATE_UNKNOWN;
+ }
+ } else {
+ pr_warn("PM: Unable to ping CM3\n");
+ }
+}
+
+static int am33xx_pm_begin(suspend_state_t state)
+{
+ int i;
+
+
+ switch (state) {
+ case PM_SUSPEND_MEM:
+ am33xx_pm->ipc.reg1 = IPC_CMD_DS0;
+ break;
+ }
+
+ am33xx_pm->ipc.reg2 = DS_IPC_DEFAULT;
+ am33xx_pm->ipc.reg3 = DS_IPC_DEFAULT;
+
+ wkup_m3_set_cmd(&am33xx_pm->ipc);
+
+ am33xx_pm->state = M3_STATE_MSG_FOR_LP;
+
+ if (!wkup_m3_ping()) {
+ i = wait_for_completion_timeout(&am33xx_pm_sync,
+ msecs_to_jiffies(500));
+ if (!i) {
+ WARN(1, "PM: MPU<->CM3 sync failure\n");
+ return -1;
+ }
+ } else {
+ pr_warn("PM: Unable to ping CM3\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+static void am33xx_pm_end(void)
+{
+ am33xx_m3_state_machine_reset();
+}
+
+static int am33xx_pm_valid(suspend_state_t state)
+{
+ switch (state) {
+ case PM_SUSPEND_MEM:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static const struct platform_suspend_ops am33xx_pm_ops = {
+ .begin = am33xx_pm_begin,
+ .end = am33xx_pm_end,
+ .enter = am33xx_pm_enter,
+ .valid = am33xx_pm_valid,
+};
+#endif /* CONFIG_SUSPEND */
+
+static void am33xx_txev_handler(void)
+{
+ switch (am33xx_pm->state) {
+ case M3_STATE_RESET:
+ am33xx_pm->state = M3_STATE_INITED;
+ complete(&am33xx_pm_sync);
+ break;
+ case M3_STATE_MSG_FOR_RESET:
+ am33xx_pm->state = M3_STATE_INITED;
+ complete(&am33xx_pm_sync);
+ break;
+ case M3_STATE_MSG_FOR_LP:
+ complete(&am33xx_pm_sync);
+ break;
+ case M3_STATE_UNKNOWN:
+ pr_warn("PM: Unknown CM3 State\n");
+ }
+}
+
+static void am33xx_m3_ready_cb(void)
+{
+ am33xx_pm->ver = wkup_m3_fw_version_read();
+
+ if (am33xx_pm->ver == M3_VERSION_UNKNOWN ||
+ am33xx_pm->ver < M3_BASELINE_VERSION) {
+ pr_warn("PM: CM3 Firmware Version %x not supported\n",
+ am33xx_pm->ver);
+ return;
+ } else {
+ pr_info("PM: CM3 Firmware Version = 0x%x\n",
+ am33xx_pm->ver);
+ }
+
+#ifdef CONFIG_SUSPEND
+ suspend_set_ops(&am33xx_pm_ops);
+#endif /* CONFIG_SUSPEND */
+}
+
+static struct wkup_m3_ops am33xx_wkup_m3_ops = {
+ .txev_handler = am33xx_txev_handler,
+ .rproc_ready = am33xx_m3_ready_cb,
+};
+
+/*
+ * Push the minimal suspend-resume code to SRAM
+ */
+void am33xx_push_sram_idle(void)
+{
+ am33xx_do_wfi_sram = (void *)omap_sram_push
+ (am33xx_do_wfi, am33xx_do_wfi_sz);
+}
+
+static int __init am33xx_map_emif(void)
+{
+ am33xx_emif_base = ioremap(AM33XX_EMIF_BASE, SZ_32K);
+
+ if (!am33xx_emif_base)
+ return -ENOMEM;
+
+ return 0;
+}
+
+int __init am33xx_pm_init(void)
+{
+ int ret;
+ u32 temp;
+
+ if (!soc_is_am33xx())
+ return -ENODEV;
+
+ gfx_pwrdm = pwrdm_lookup("gfx_pwrdm");
+ per_pwrdm = pwrdm_lookup("per_pwrdm");
+ mpu_pwrdm = pwrdm_lookup("mpu_pwrdm");
+
+ gfx_l4ls_clkdm = clkdm_lookup("gfx_l4ls_gfx_clkdm");
+
+ if ((!gfx_pwrdm) || (!per_pwrdm) || (!mpu_pwrdm) || (!gfx_l4ls_clkdm)) {
+ ret = -ENODEV;
+ goto err;
+ }
+
+ am33xx_pm = kzalloc(sizeof(*am33xx_pm), GFP_KERNEL);
+ if (!am33xx_pm) {
+ pr_err("Memory allocation failed\n");
+ ret = -ENOMEM;
+ return ret;
+ }
+
+ ret = am33xx_map_emif();
+ if (ret) {
+ pr_err("PM: Could not ioremap EMIF\n");
+ goto err;
+ }
+
+ /* Determine Memory Type */
+ temp = readl(am33xx_emif_base + EMIF_SDRAM_CONFIG);
+ temp = (temp & SDRAM_TYPE_MASK) >> SDRAM_TYPE_SHIFT;
+ /* Parameters to pass to aseembly code */
+ susp_params.emif_addr_virt = am33xx_emif_base;
+ susp_params.dram_sync = am33xx_dram_sync;
+ susp_params.mem_type = temp;
+ am33xx_pm->ipc.reg4 = temp;
+
+ (void) clkdm_for_each(omap_pm_clkdms_setup, NULL);
+
+ /* CEFUSE domain can be turned off post bootup */
+ cefuse_pwrdm = pwrdm_lookup("cefuse_pwrdm");
+ if (cefuse_pwrdm)
+ omap_set_pwrdm_state(cefuse_pwrdm, PWRDM_POWER_OFF);
+ else
+ pr_err("PM: Failed to get cefuse_pwrdm\n");
+
+ am33xx_pm->state = M3_STATE_RESET;
+
+ wkup_m3_set_ops(&am33xx_wkup_m3_ops);
+
+ /* Physical resume address to be used by ROM code */
+ am33xx_pm->ipc.reg0 = (AM33XX_OCMC_END -
+ am33xx_do_wfi_sz + am33xx_resume_offset + 0x4);
+
+ return 0;
+
+err:
+ kfree(am33xx_pm);
+ return ret;
+}
diff --git a/arch/arm/mach-omap2/pm33xx.h b/arch/arm/mach-omap2/pm33xx.h
new file mode 100644
index 0000000..309e65a
--- /dev/null
+++ b/arch/arm/mach-omap2/pm33xx.h
@@ -0,0 +1,64 @@
+/*
+ * AM33XX Power Management Routines
+ *
+ * Copyright (C) 2012-2014 Texas Instruments Inc.
+ * Vaibhav Bedia, Dave Gerlach
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation version 2.
+ *
+ * This program is distributed "as is" WITHOUT ANY WARRANTY of any
+ * kind, whether express or implied; 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_OMAP2_PM33XX_H
+#define __ARCH_ARM_MACH_OMAP2_PM33XX_H
+
+#ifndef __ASSEMBLER__
+
+#include <linux/wkup_m3.h>
+
+struct am33xx_pm_context {
+ struct wkup_m3_ipc_regs ipc;
+ struct firmware *firmware;
+ struct omap_mbox *mbox;
+ u8 state;
+ u32 ver;
+};
+
+/*
+ * Params passed to suspend routine
+ *
+ * These are used to load into registers by suspend code,
+ * entries here must always be in sync with the suspend code
+ * in arm/mach-omap2/sleep33xx.S
+ */
+struct am33xx_suspend_params {
+ void __iomem *emif_addr_virt;
+ u32 mem_type;
+ void __iomem *dram_sync;
+};
+
+#endif /*__ASSEMBLER__ */
+
+#define IPC_CMD_DS0 0x4
+#define IPC_CMD_STANDBY 0xc
+#define IPC_CMD_RESET 0xe
+#define DS_IPC_DEFAULT 0xffffffff
+#define M3_VERSION_UNKNOWN 0x0000ffff
+#define M3_BASELINE_VERSION 0x187
+
+#define M3_STATE_UNKNOWN 0
+#define M3_STATE_RESET 1
+#define M3_STATE_INITED 2
+#define M3_STATE_MSG_FOR_LP 3
+#define M3_STATE_MSG_FOR_RESET 4
+
+#define AM33XX_OCMC_END 0x40310000
+#define AM33XX_EMIF_BASE 0x4C000000
+
+#define MEM_TYPE_DDR2 2
+
+#endif /* __ARCH_ARM_MACH_OMAP2_PM33XX_H */
--
1.9.0