| /* |
| * 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) { |
| hwblk_enable(hwblk_info, hwblk); |
| ret = 0; |
| |
| if (test_bit(PDEV_ARCHDATA_FLAG_SUSP, &ad->flags)) { |
| if (d->driver->pm && d->driver->pm->runtime_resume) |
| 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) { |
| BUG_ON(!test_bit(PDEV_ARCHDATA_FLAG_IDLE, &ad->flags)); |
| ret = 0; |
| |
| if (d->driver->pm && d->driver->pm->runtime_suspend) { |
| 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); |