@@ -14,8 +14,16 @@ int platform_resource_setup_memory(struc
void plat_early_device_setup(void);
+#define PDEV_ARCHDATA_FLAG_INIT 0
+#define PDEV_ARCHDATA_FLAG_LIST 1
+#define PDEV_ARCHDATA_FLAG_SUSP 2
+#define PDEV_ARCHDATA_FLAG_WORK 3
+
struct pdev_archdata {
#ifdef CONFIG_ARCH_SHMOBILE
int hwblk_id;
+ unsigned long flags;
+ struct list_head entry;
+ struct work_struct work;
#endif
};
@@ -5,7 +5,9 @@
#include <asm/io.h>
#define HWBLK_CNT_USAGE 0
-#define HWBLK_CNT_NR 1
+#define HWBLK_CNT_IDLE 1
+#define HWBLK_CNT_DEVICES 2
+#define HWBLK_CNT_NR 3
#define HWBLK_AREA_FLAG_PARENT (1 << 0) /* valid parent */
@@ -5,3 +5,4 @@
# Power Management & Sleep mode
obj-$(CONFIG_PM) += pm.o sleep.o
obj-$(CONFIG_CPU_IDLE) += cpuidle.o
+obj-$(CONFIG_PM_RUNTIME) += pm_runtime.o
@@ -0,0 +1,244 @@
+/*
+ * 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 <asm/hwblk.h>
+
+static DEFINE_SPINLOCK(hwblk_lock);
+static LIST_HEAD(hwblk_idle_list);
+
+extern struct hwblk_info *hwblk_info;
+
+static void platform_pm_runtime_work(struct work_struct *work)
+{
+ struct platform_device *pdev = container_of(work,
+ struct platform_device,
+ archdata.work);
+ struct device *dev = &pdev->dev;
+ unsigned long flags;
+ int hwblk = pdev->archdata.hwblk_id;
+ int ret = 0;
+
+ dev_dbg(dev, "platform_pm_runtime_work() [%d]\n", hwblk);
+
+ if (dev->driver) {
+ hwblk_enable(hwblk_info, hwblk);
+ ret = pm_runtime_ops_suspend(dev, dev->driver->pm);
+ hwblk_disable(hwblk_info, hwblk);
+ }
+
+ if (ret)
+ return;
+
+ /* remove device from idle list and decrease idle count */
+ spin_lock_irqsave(&hwblk_lock, flags);
+ if (test_bit(PDEV_ARCHDATA_FLAG_LIST, &pdev->archdata.flags)) {
+ list_del(&pdev->archdata.entry);
+ __clear_bit(PDEV_ARCHDATA_FLAG_LIST, &pdev->archdata.flags);
+ }
+ __set_bit(PDEV_ARCHDATA_FLAG_SUSP, &pdev->archdata.flags);
+ __clear_bit(PDEV_ARCHDATA_FLAG_WORK, &pdev->archdata.flags);
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+
+ hwblk_cnt_dec(hwblk_info, hwblk, HWBLK_CNT_IDLE);
+
+ dev_dbg(dev, "platform_pm_runtime_work() suspended [%d]\n", hwblk);
+}
+
+/* this function gets called from cpuidle context when all devices in
+ * a power domain are unused but some are coutned as idle, ie the hwblk
+ * counter values are (HWBLK_CNT_USAGE == 0) && (HWBLK_CNT_IDLE != 0)
+ */
+void platform_pm_runtime_suspend_idle(void)
+{
+ struct platform_device *pdev;
+ struct pdev_archdata *ad;
+ unsigned long flags;
+
+ /* schedule suspend for all devices on the idle list */
+ spin_lock_irqsave(&hwblk_lock, flags);
+ list_for_each_entry(pdev, &hwblk_idle_list, archdata.entry) {
+ ad = &pdev->archdata;
+ if (!test_and_set_bit(PDEV_ARCHDATA_FLAG_WORK, &ad->flags))
+ schedule_work(&ad->work);
+ }
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+}
+
+int platform_pm_runtime_suspend(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ unsigned long flags;
+ int hwblk = pdev->archdata.hwblk_id;
+ int ret = 0;
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ return 0;
+
+ dev_dbg(dev, "platform_pm_runtime_suspend() [%d]\n", hwblk);
+
+ /* 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;
+ }
+
+ /* 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_LIST, &pdev->archdata.flags);
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+
+ /* increase idle count */
+ hwblk_cnt_inc(hwblk_info, hwblk, HWBLK_CNT_IDLE);
+
+ 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);
+ unsigned long flags;
+ int hwblk = pdev->archdata.hwblk_id;
+ int ret = 0;
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ return 0;
+
+ dev_dbg(dev, "platform_pm_runtime_resume() [%d]\n", hwblk);
+
+ /* interrupt context not allowed */
+ might_sleep();
+
+ /* make sure device is removed from idle list */
+ spin_lock_irqsave(&hwblk_lock, flags);
+ if (test_bit(PDEV_ARCHDATA_FLAG_LIST, &pdev->archdata.flags)) {
+ list_del(&pdev->archdata.entry);
+ __clear_bit(PDEV_ARCHDATA_FLAG_LIST, &pdev->archdata.flags);
+ }
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+
+ /* decrease idle count if needed */
+ 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);
+
+ /* enable clock */
+ hwblk_enable(hwblk_info, hwblk);
+
+ /* resume the device if needed */
+ if (test_bit(PDEV_ARCHDATA_FLAG_SUSP, &pdev->archdata.flags)) {
+ if (dev->driver)
+ ret = pm_runtime_ops_resume(dev, dev->driver->pm);
+ }
+
+ spin_lock_irqsave(&hwblk_lock, flags);
+
+ /* the driver has been initialized now, so clear the init flag */
+ __clear_bit(PDEV_ARCHDATA_FLAG_INIT, &pdev->archdata.flags);
+
+ /* clear susp flag if driver runtime resume was successful */
+ if (!ret)
+ __clear_bit(PDEV_ARCHDATA_FLAG_SUSP, &pdev->archdata.flags);
+
+ spin_unlock_irqrestore(&hwblk_lock, flags);
+
+ /* disable clock again if driver runtime resume failed */
+ if (ret)
+ hwblk_disable(hwblk_info, hwblk);
+
+ dev_dbg(dev, "platform_pm_runtime_resume() [%d] returns %d\n",
+ hwblk, ret);
+
+ return ret;
+};
+
+void platform_pm_runtime_idle(struct device *dev)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ int hwblk = pdev->archdata.hwblk_id;
+
+ /* ignore off-chip platform devices */
+ if (!hwblk)
+ return;
+
+ /* interrupt context not allowed, use pm_runtime_put()! */
+ might_sleep();
+
+ /* suspend synchronously to disable clocks immediately */
+ pm_runtime_suspend(dev);
+};
+
+static int __devinit 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);
+ INIT_WORK(&pdev->archdata.work, platform_pm_runtime_work);
+ /* 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)
+{
+ bus_register_notifier(&platform_bus_type, &platform_bus_notifier);
+ return 0;
+}
+
+core_initcall(sh_pm_runtime_init);