304 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			304 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
/*
 | 
						|
 * 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);
 |