aboutsummaryrefslogtreecommitdiffstats
path: root/arch/sh/kernel/cpu/shmobile/pm_runtime.c
diff options
context:
space:
mode:
Diffstat (limited to 'arch/sh/kernel/cpu/shmobile/pm_runtime.c')
-rw-r--r--arch/sh/kernel/cpu/shmobile/pm_runtime.c303
1 files changed, 303 insertions, 0 deletions
diff --git a/arch/sh/kernel/cpu/shmobile/pm_runtime.c b/arch/sh/kernel/cpu/shmobile/pm_runtime.c
new file mode 100644
index 000000000000..7c615b17e209
--- /dev/null
+++ b/arch/sh/kernel/cpu/shmobile/pm_runtime.c
@@ -0,0 +1,303 @@
+/*
+ * arch/sh/kernel/cpu/shmobile/pm_runtime.c
+ *
+ * Runtime PM support code for SuperH Mobile
+ *
+ * Copyright (C) 2009 Magnus Damm
+ *
+ * This file is subject to the terms and conditions of the GNU General Public
+ * License. See the file "COPYING" in the main directory of this archive
+ * for more details.
+ */
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/io.h>
+#include <linux/pm_runtime.h>
+#include <linux/platform_device.h>
+#include <linux/mutex.h>
+#include <asm/hwblk.h>
+
+static DEFINE_SPINLOCK(hwblk_lock);
+static LIST_HEAD(hwblk_idle_list);
+static struct work_struct hwblk_work;
+
+extern struct hwblk_info *hwblk_info;
+
+static void platform_pm_runtime_not_idle(struct platform_device *pdev)
+{
+ unsigned long flags;
+
+ /* remove device from idle list */
+ spin_lock_irqsave(&hwblk_lock, flags);
+ if (test_bit(PDEV_ARCHDATA_FLAG_IDLE, &pdev->archdata.flags)) {
+ list_del(&pdev->archdata.entry);
+ __clear_bit(PDEV_ARCHDATA_FLAG_IDLE, &pdev->archdata.flags);
+ }
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+}
+
+static int __platform_pm_runtime_resume(struct platform_device *pdev)
+{
+ struct device *d = &pdev->dev;
+ struct pdev_archdata *ad = &pdev->archdata;
+ int hwblk = ad->hwblk_id;
+ int ret = -ENOSYS;
+
+ dev_dbg(d, "__platform_pm_runtime_resume() [%d]\n", hwblk);
+
+ if (d->driver && d->driver->pm && d->driver->pm->runtime_resume) {
+ hwblk_enable(hwblk_info, hwblk);
+ ret = 0;
+
+ if (test_bit(PDEV_ARCHDATA_FLAG_SUSP, &ad->flags)) {
+ ret = d->driver->pm->runtime_resume(d);
+ if (!ret)
+ clear_bit(PDEV_ARCHDATA_FLAG_SUSP, &ad->flags);
+ else
+ hwblk_disable(hwblk_info, hwblk);
+ }
+ }
+
+ dev_dbg(d, "__platform_pm_runtime_resume() [%d] - returns %d\n",
+ hwblk, ret);
+
+ return ret;
+}
+
+static int __platform_pm_runtime_suspend(struct platform_device *pdev)
+{
+ struct device *d = &pdev->dev;
+ struct pdev_archdata *ad = &pdev->archdata;
+ int hwblk = ad->hwblk_id;
+ int ret = -ENOSYS;
+
+ dev_dbg(d, "__platform_pm_runtime_suspend() [%d]\n", hwblk);
+
+ if (d->driver && d->driver->pm && d->driver->pm->runtime_suspend) {
+ BUG_ON(!test_bit(PDEV_ARCHDATA_FLAG_IDLE, &ad->flags));
+
+ hwblk_enable(hwblk_info, hwblk);
+ ret = d->driver->pm->runtime_suspend(d);
+ hwblk_disable(hwblk_info, hwblk);
+
+ if (!ret) {
+ set_bit(PDEV_ARCHDATA_FLAG_SUSP, &ad->flags);
+ platform_pm_runtime_not_idle(pdev);
+ hwblk_cnt_dec(hwblk_info, hwblk, HWBLK_CNT_IDLE);
+ }
+ }
+
+ dev_dbg(d, "__platform_pm_runtime_suspend() [%d] - returns %d\n",
+ hwblk, ret);
+
+ return ret;
+}
+
+static void platform_pm_runtime_work(struct work_struct *work)
+{
+ struct platform_device *pdev;
+ unsigned long flags;
+ int ret;
+
+ /* go through the idle list and suspend one device at a time */
+ do {
+ spin_lock_irqsave(&hwblk_lock, flags);
+ if (list_empty(&hwblk_idle_list))
+ pdev = NULL;
+ else
+ pdev = list_first_entry(&hwblk_idle_list,
+ struct platform_device,
+ archdata.entry);
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+
+ if (pdev) {
+ mutex_lock(&pdev->archdata.mutex);
+ ret = __platform_pm_runtime_suspend(pdev);
+
+ /* at this point the platform device may be:
+ * suspended: ret = 0, FLAG_SUSP set, clock stopped
+ * failed: ret < 0, FLAG_IDLE set, clock stopped
+ */
+ mutex_unlock(&pdev->archdata.mutex);
+ } else {
+ ret = -ENODEV;
+ }
+ } while (!ret);
+}
+
+/* this function gets called from cpuidle context when all devices in the
+ * main power domain are unused but some are counted as idle, ie the hwblk
+ * counter values are (HWBLK_CNT_USAGE == 0) && (HWBLK_CNT_IDLE != 0)
+ */
+void platform_pm_runtime_suspend_idle(void)
+{
+ queue_work(pm_wq, &hwblk_work);
+}
+
+int platform_pm_runtime_suspend(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ struct pdev_archdata *ad = &pdev->archdata;
+ unsigned long flags;
+ int hwblk = ad->hwblk_id;
+ int ret = 0;
+
+ dev_dbg(dev, "platform_pm_runtime_suspend() [%d]\n", hwblk);
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ goto out;
+
+ /* interrupt context not allowed */
+ might_sleep();
+
+ /* catch misconfigured drivers not starting with resume */
+ if (test_bit(PDEV_ARCHDATA_FLAG_INIT, &pdev->archdata.flags)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* serialize */
+ mutex_lock(&ad->mutex);
+
+ /* disable clock */
+ hwblk_disable(hwblk_info, hwblk);
+
+ /* put device on idle list */
+ spin_lock_irqsave(&hwblk_lock, flags);
+ list_add_tail(&pdev->archdata.entry, &hwblk_idle_list);
+ __set_bit(PDEV_ARCHDATA_FLAG_IDLE, &pdev->archdata.flags);
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+
+ /* increase idle count */
+ hwblk_cnt_inc(hwblk_info, hwblk, HWBLK_CNT_IDLE);
+
+ /* at this point the platform device is:
+ * idle: ret = 0, FLAG_IDLE set, clock stopped
+ */
+ mutex_unlock(&ad->mutex);
+
+out:
+ dev_dbg(dev, "platform_pm_runtime_suspend() [%d] returns %d\n",
+ hwblk, ret);
+
+ return ret;
+}
+
+int platform_pm_runtime_resume(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ struct pdev_archdata *ad = &pdev->archdata;
+ int hwblk = ad->hwblk_id;
+ int ret = 0;
+
+ dev_dbg(dev, "platform_pm_runtime_resume() [%d]\n", hwblk);
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ goto out;
+
+ /* interrupt context not allowed */
+ might_sleep();
+
+ /* serialize */
+ mutex_lock(&ad->mutex);
+
+ /* make sure device is removed from idle list */
+ platform_pm_runtime_not_idle(pdev);
+
+ /* decrease idle count */
+ if (!test_bit(PDEV_ARCHDATA_FLAG_INIT, &pdev->archdata.flags) &&
+ !test_bit(PDEV_ARCHDATA_FLAG_SUSP, &pdev->archdata.flags))
+ hwblk_cnt_dec(hwblk_info, hwblk, HWBLK_CNT_IDLE);
+
+ /* resume the device if needed */
+ ret = __platform_pm_runtime_resume(pdev);
+
+ /* the driver has been initialized now, so clear the init flag */
+ clear_bit(PDEV_ARCHDATA_FLAG_INIT, &pdev->archdata.flags);
+
+ /* at this point the platform device may be:
+ * resumed: ret = 0, flags = 0, clock started
+ * failed: ret < 0, FLAG_SUSP set, clock stopped
+ */
+ mutex_unlock(&ad->mutex);
+out:
+ dev_dbg(dev, "platform_pm_runtime_resume() [%d] returns %d\n",
+ hwblk, ret);
+
+ return ret;
+}
+
+int platform_pm_runtime_idle(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ int hwblk = pdev->archdata.hwblk_id;
+ int ret = 0;
+
+ dev_dbg(dev, "platform_pm_runtime_idle() [%d]\n", hwblk);
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ goto out;
+
+ /* interrupt context not allowed, use pm_runtime_put()! */
+ might_sleep();
+
+ /* suspend synchronously to disable clocks immediately */
+ ret = pm_runtime_suspend(dev);
+out:
+ dev_dbg(dev, "platform_pm_runtime_idle() [%d] done!\n", hwblk);
+ return ret;
+}
+
+static int platform_bus_notify(struct notifier_block *nb,
+ unsigned long action, void *data)
+{
+ struct device *dev = data;
+ struct platform_device *pdev = to_platform_device(dev);
+ int hwblk = pdev->archdata.hwblk_id;
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ return 0;
+
+ switch (action) {
+ case BUS_NOTIFY_ADD_DEVICE:
+ INIT_LIST_HEAD(&pdev->archdata.entry);
+ mutex_init(&pdev->archdata.mutex);
+ /* platform devices without drivers should be disabled */
+ hwblk_enable(hwblk_info, hwblk);
+ hwblk_disable(hwblk_info, hwblk);
+ /* make sure driver re-inits itself once */
+ __set_bit(PDEV_ARCHDATA_FLAG_INIT, &pdev->archdata.flags);
+ break;
+ /* TODO: add BUS_NOTIFY_BIND_DRIVER and increase idle count */
+ case BUS_NOTIFY_BOUND_DRIVER:
+ /* keep track of number of devices in use per hwblk */
+ hwblk_cnt_inc(hwblk_info, hwblk, HWBLK_CNT_DEVICES);
+ break;
+ case BUS_NOTIFY_UNBOUND_DRIVER:
+ /* keep track of number of devices in use per hwblk */
+ hwblk_cnt_dec(hwblk_info, hwblk, HWBLK_CNT_DEVICES);
+ /* make sure driver re-inits itself once */
+ __set_bit(PDEV_ARCHDATA_FLAG_INIT, &pdev->archdata.flags);
+ break;
+ case BUS_NOTIFY_DEL_DEVICE:
+ break;
+ }
+ return 0;
+}
+
+static struct notifier_block platform_bus_notifier = {
+ .notifier_call = platform_bus_notify
+};
+
+static int __init sh_pm_runtime_init(void)
+{
+ INIT_WORK(&hwblk_work, platform_pm_runtime_work);
+
+ bus_register_notifier(&platform_bus_type, &platform_bus_notifier);
+ return 0;
+}
+core_initcall(sh_pm_runtime_init);