/* * 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 #include #include #include #include #include #include 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);