diff options
Diffstat (limited to 'drivers/base/power')
-rw-r--r-- | drivers/base/power/Makefile | 6 | ||||
-rw-r--r-- | drivers/base/power/main.c | 99 | ||||
-rw-r--r-- | drivers/base/power/power.h | 106 | ||||
-rw-r--r-- | drivers/base/power/resume.c | 112 | ||||
-rw-r--r-- | drivers/base/power/runtime.c | 81 | ||||
-rw-r--r-- | drivers/base/power/shutdown.c | 67 | ||||
-rw-r--r-- | drivers/base/power/suspend.c | 144 | ||||
-rw-r--r-- | drivers/base/power/sysfs.c | 68 |
8 files changed, 683 insertions, 0 deletions
diff --git a/drivers/base/power/Makefile b/drivers/base/power/Makefile new file mode 100644 index 00000000000..c0219ad94ac --- /dev/null +++ b/drivers/base/power/Makefile @@ -0,0 +1,6 @@ +obj-y := shutdown.o +obj-$(CONFIG_PM) += main.o suspend.o resume.o runtime.o sysfs.o + +ifeq ($(CONFIG_DEBUG_DRIVER),y) +EXTRA_CFLAGS += -DDEBUG +endif diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c new file mode 100644 index 00000000000..15e6a8f951f --- /dev/null +++ b/drivers/base/power/main.c @@ -0,0 +1,99 @@ +/* + * drivers/base/power/main.c - Where the driver meets power management. + * + * Copyright (c) 2003 Patrick Mochel + * Copyright (c) 2003 Open Source Development Lab + * + * This file is released under the GPLv2 + * + * + * The driver model core calls device_pm_add() when a device is registered. + * This will intialize the embedded device_pm_info object in the device + * and add it to the list of power-controlled devices. sysfs entries for + * controlling device power management will also be added. + * + * A different set of lists than the global subsystem list are used to + * keep track of power info because we use different lists to hold + * devices based on what stage of the power management process they + * are in. The power domain dependencies may also differ from the + * ancestral dependencies that the subsystem list maintains. + */ + +#include <linux/config.h> +#include <linux/device.h> +#include "power.h" + +LIST_HEAD(dpm_active); +LIST_HEAD(dpm_off); +LIST_HEAD(dpm_off_irq); + +DECLARE_MUTEX(dpm_sem); +DECLARE_MUTEX(dpm_list_sem); + +/* + * PM Reference Counting. + */ + +static inline void device_pm_hold(struct device * dev) +{ + if (dev) + atomic_inc(&dev->power.pm_users); +} + +static inline void device_pm_release(struct device * dev) +{ + if (dev) + atomic_dec(&dev->power.pm_users); +} + + +/** + * device_pm_set_parent - Specify power dependency. + * @dev: Device who needs power. + * @parent: Device that supplies power. + * + * This function is used to manually describe a power-dependency + * relationship. It may be used to specify a transversal relationship + * (where the power supplier is not the physical (or electrical) + * ancestor of a specific device. + * The effect of this is that the supplier will not be powered down + * before the power dependent. + */ + +void device_pm_set_parent(struct device * dev, struct device * parent) +{ + struct device * old_parent = dev->power.pm_parent; + device_pm_release(old_parent); + dev->power.pm_parent = parent; + device_pm_hold(parent); +} +EXPORT_SYMBOL_GPL(device_pm_set_parent); + +int device_pm_add(struct device * dev) +{ + int error; + + pr_debug("PM: Adding info for %s:%s\n", + dev->bus ? dev->bus->name : "No Bus", dev->kobj.name); + atomic_set(&dev->power.pm_users, 0); + down(&dpm_list_sem); + list_add_tail(&dev->power.entry, &dpm_active); + device_pm_set_parent(dev, dev->parent); + if ((error = dpm_sysfs_add(dev))) + list_del(&dev->power.entry); + up(&dpm_list_sem); + return error; +} + +void device_pm_remove(struct device * dev) +{ + pr_debug("PM: Removing info for %s:%s\n", + dev->bus ? dev->bus->name : "No Bus", dev->kobj.name); + down(&dpm_list_sem); + dpm_sysfs_remove(dev); + device_pm_release(dev->power.pm_parent); + list_del_init(&dev->power.entry); + up(&dpm_list_sem); +} + + diff --git a/drivers/base/power/power.h b/drivers/base/power/power.h new file mode 100644 index 00000000000..e5eda746f2a --- /dev/null +++ b/drivers/base/power/power.h @@ -0,0 +1,106 @@ + + +enum { + DEVICE_PM_ON, + DEVICE_PM1, + DEVICE_PM2, + DEVICE_PM3, + DEVICE_PM_OFF, +}; + +/* + * shutdown.c + */ + +extern int device_detach_shutdown(struct device *); +extern void device_shutdown(void); + + +#ifdef CONFIG_PM + +/* + * main.c + */ + +/* + * Used to synchronize global power management operations. + */ +extern struct semaphore dpm_sem; + +/* + * Used to serialize changes to the dpm_* lists. + */ +extern struct semaphore dpm_list_sem; + +/* + * The PM lists. + */ +extern struct list_head dpm_active; +extern struct list_head dpm_off; +extern struct list_head dpm_off_irq; + + +static inline struct dev_pm_info * to_pm_info(struct list_head * entry) +{ + return container_of(entry, struct dev_pm_info, entry); +} + +static inline struct device * to_device(struct list_head * entry) +{ + return container_of(to_pm_info(entry), struct device, power); +} + +extern int device_pm_add(struct device *); +extern void device_pm_remove(struct device *); + +/* + * sysfs.c + */ + +extern int dpm_sysfs_add(struct device *); +extern void dpm_sysfs_remove(struct device *); + +/* + * resume.c + */ + +extern void dpm_resume(void); +extern void dpm_power_up(void); +extern int resume_device(struct device *); + +/* + * suspend.c + */ +extern int suspend_device(struct device *, pm_message_t); + + +/* + * runtime.c + */ + +extern int dpm_runtime_suspend(struct device *, pm_message_t); +extern void dpm_runtime_resume(struct device *); + +#else /* CONFIG_PM */ + + +static inline int device_pm_add(struct device * dev) +{ + return 0; +} +static inline void device_pm_remove(struct device * dev) +{ + +} + +static inline int dpm_runtime_suspend(struct device * dev, pm_message_t state) +{ + return 0; +} + +static inline void dpm_runtime_resume(struct device * dev) +{ + +} + +#endif diff --git a/drivers/base/power/resume.c b/drivers/base/power/resume.c new file mode 100644 index 00000000000..f8f5055754d --- /dev/null +++ b/drivers/base/power/resume.c @@ -0,0 +1,112 @@ +/* + * resume.c - Functions for waking devices up. + * + * Copyright (c) 2003 Patrick Mochel + * Copyright (c) 2003 Open Source Development Labs + * + * This file is released under the GPLv2 + * + */ + +#include <linux/device.h> +#include "power.h" + +extern int sysdev_resume(void); + + +/** + * resume_device - Restore state for one device. + * @dev: Device. + * + */ + +int resume_device(struct device * dev) +{ + if (dev->bus && dev->bus->resume) + return dev->bus->resume(dev); + return 0; +} + + + +void dpm_resume(void) +{ + down(&dpm_list_sem); + while(!list_empty(&dpm_off)) { + struct list_head * entry = dpm_off.next; + struct device * dev = to_device(entry); + + get_device(dev); + list_del_init(entry); + list_add_tail(entry, &dpm_active); + + up(&dpm_list_sem); + if (!dev->power.prev_state) + resume_device(dev); + down(&dpm_list_sem); + put_device(dev); + } + up(&dpm_list_sem); +} + + +/** + * device_resume - Restore state of each device in system. + * + * Walk the dpm_off list, remove each entry, resume the device, + * then add it to the dpm_active list. + */ + +void device_resume(void) +{ + down(&dpm_sem); + dpm_resume(); + up(&dpm_sem); +} + +EXPORT_SYMBOL_GPL(device_resume); + + +/** + * device_power_up_irq - Power on some devices. + * + * Walk the dpm_off_irq list and power each device up. This + * is used for devices that required they be powered down with + * interrupts disabled. As devices are powered on, they are moved to + * the dpm_suspended list. + * + * Interrupts must be disabled when calling this. + */ + +void dpm_power_up(void) +{ + while(!list_empty(&dpm_off_irq)) { + struct list_head * entry = dpm_off_irq.next; + struct device * dev = to_device(entry); + + get_device(dev); + list_del_init(entry); + list_add_tail(entry, &dpm_active); + resume_device(dev); + put_device(dev); + } +} + + +/** + * device_pm_power_up - Turn on all devices that need special attention. + * + * Power on system devices then devices that required we shut them down + * with interrupts disabled. + * Called with interrupts disabled. + */ + +void device_power_up(void) +{ + sysdev_resume(); + dpm_power_up(); +} + +EXPORT_SYMBOL_GPL(device_power_up); + + diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c new file mode 100644 index 00000000000..325962d8019 --- /dev/null +++ b/drivers/base/power/runtime.c @@ -0,0 +1,81 @@ +/* + * drivers/base/power/runtime.c - Handling dynamic device power management. + * + * Copyright (c) 2003 Patrick Mochel + * Copyright (c) 2003 Open Source Development Lab + * + */ + +#include <linux/device.h> +#include "power.h" + + +static void runtime_resume(struct device * dev) +{ + dev_dbg(dev, "resuming\n"); + if (!dev->power.power_state) + return; + if (!resume_device(dev)) + dev->power.power_state = 0; +} + + +/** + * dpm_runtime_resume - Power one device back on. + * @dev: Device. + * + * Bring one device back to the on state by first powering it + * on, then restoring state. We only operate on devices that aren't + * already on. + * FIXME: We need to handle devices that are in an unknown state. + */ + +void dpm_runtime_resume(struct device * dev) +{ + down(&dpm_sem); + runtime_resume(dev); + up(&dpm_sem); +} + + +/** + * dpm_runtime_suspend - Put one device in low-power state. + * @dev: Device. + * @state: State to enter. + */ + +int dpm_runtime_suspend(struct device * dev, pm_message_t state) +{ + int error = 0; + + down(&dpm_sem); + if (dev->power.power_state == state) + goto Done; + + if (dev->power.power_state) + runtime_resume(dev); + + if (!(error = suspend_device(dev, state))) + dev->power.power_state = state; + Done: + up(&dpm_sem); + return error; +} + + +/** + * dpm_set_power_state - Update power_state field. + * @dev: Device. + * @state: Power state device is in. + * + * This is an update mechanism for drivers to notify the core + * what power state a device is in. Device probing code may not + * always be able to tell, but we need accurate information to + * work reliably. + */ +void dpm_set_power_state(struct device * dev, pm_message_t state) +{ + down(&dpm_sem); + dev->power.power_state = state; + up(&dpm_sem); +} diff --git a/drivers/base/power/shutdown.c b/drivers/base/power/shutdown.c new file mode 100644 index 00000000000..d1e023fbe16 --- /dev/null +++ b/drivers/base/power/shutdown.c @@ -0,0 +1,67 @@ +/* + * shutdown.c - power management functions for the device tree. + * + * Copyright (c) 2002-3 Patrick Mochel + * 2002-3 Open Source Development Lab + * + * This file is released under the GPLv2 + * + */ + +#include <linux/config.h> +#include <linux/device.h> +#include <asm/semaphore.h> + +#include "power.h" + +#define to_dev(node) container_of(node, struct device, kobj.entry) + +extern struct subsystem devices_subsys; + + +int device_detach_shutdown(struct device * dev) +{ + if (!dev->detach_state) + return 0; + + if (dev->detach_state == DEVICE_PM_OFF) { + if (dev->driver && dev->driver->shutdown) + dev->driver->shutdown(dev); + return 0; + } + return dpm_runtime_suspend(dev, dev->detach_state); +} + + +/** + * We handle system devices differently - we suspend and shut them + * down last and resume them first. That way, we don't do anything stupid like + * shutting down the interrupt controller before any devices.. + * + * Note that there are not different stages for power management calls - + * they only get one called once when interrupts are disabled. + */ + +extern int sysdev_shutdown(void); + +/** + * device_shutdown - call ->shutdown() on each device to shutdown. + */ +void device_shutdown(void) +{ + struct device * dev; + + down_write(&devices_subsys.rwsem); + list_for_each_entry_reverse(dev, &devices_subsys.kset.list, kobj.entry) { + pr_debug("shutting down %s: ", dev->bus_id); + if (dev->driver && dev->driver->shutdown) { + pr_debug("Ok\n"); + dev->driver->shutdown(dev); + } else + pr_debug("Ignored.\n"); + } + up_write(&devices_subsys.rwsem); + + sysdev_shutdown(); +} + diff --git a/drivers/base/power/suspend.c b/drivers/base/power/suspend.c new file mode 100644 index 00000000000..a0b5cf689e6 --- /dev/null +++ b/drivers/base/power/suspend.c @@ -0,0 +1,144 @@ +/* + * suspend.c - Functions for putting devices to sleep. + * + * Copyright (c) 2003 Patrick Mochel + * Copyright (c) 2003 Open Source Development Labs + * + * This file is released under the GPLv2 + * + */ + +#include <linux/device.h> +#include "power.h" + +extern int sysdev_suspend(pm_message_t state); + +/* + * The entries in the dpm_active list are in a depth first order, simply + * because children are guaranteed to be discovered after parents, and + * are inserted at the back of the list on discovery. + * + * All list on the suspend path are done in reverse order, so we operate + * on the leaves of the device tree (or forests, depending on how you want + * to look at it ;) first. As nodes are removed from the back of the list, + * they are inserted into the front of their destintation lists. + * + * Things are the reverse on the resume path - iterations are done in + * forward order, and nodes are inserted at the back of their destination + * lists. This way, the ancestors will be accessed before their descendents. + */ + + +/** + * suspend_device - Save state of one device. + * @dev: Device. + * @state: Power state device is entering. + */ + +int suspend_device(struct device * dev, pm_message_t state) +{ + int error = 0; + + dev_dbg(dev, "suspending\n"); + + dev->power.prev_state = dev->power.power_state; + + if (dev->bus && dev->bus->suspend && !dev->power.power_state) + error = dev->bus->suspend(dev, state); + + return error; +} + + +/** + * device_suspend - Save state and stop all devices in system. + * @state: Power state to put each device in. + * + * Walk the dpm_active list, call ->suspend() for each device, and move + * it to dpm_off. + * Check the return value for each. If it returns 0, then we move the + * the device to the dpm_off list. If it returns -EAGAIN, we move it to + * the dpm_off_irq list. If we get a different error, try and back out. + * + * If we hit a failure with any of the devices, call device_resume() + * above to bring the suspended devices back to life. + * + */ + +int device_suspend(pm_message_t state) +{ + int error = 0; + + down(&dpm_sem); + down(&dpm_list_sem); + while (!list_empty(&dpm_active) && error == 0) { + struct list_head * entry = dpm_active.prev; + struct device * dev = to_device(entry); + + get_device(dev); + up(&dpm_list_sem); + + error = suspend_device(dev, state); + + down(&dpm_list_sem); + + /* Check if the device got removed */ + if (!list_empty(&dev->power.entry)) { + /* Move it to the dpm_off or dpm_off_irq list */ + if (!error) { + list_del(&dev->power.entry); + list_add(&dev->power.entry, &dpm_off); + } else if (error == -EAGAIN) { + list_del(&dev->power.entry); + list_add(&dev->power.entry, &dpm_off_irq); + error = 0; + } + } + if (error) + printk(KERN_ERR "Could not suspend device %s: " + "error %d\n", kobject_name(&dev->kobj), error); + put_device(dev); + } + up(&dpm_list_sem); + if (error) + dpm_resume(); + up(&dpm_sem); + return error; +} + +EXPORT_SYMBOL_GPL(device_suspend); + + +/** + * device_power_down - Shut down special devices. + * @state: Power state to enter. + * + * Walk the dpm_off_irq list, calling ->power_down() for each device that + * couldn't power down the device with interrupts enabled. When we're + * done, power down system devices. + */ + +int device_power_down(pm_message_t state) +{ + int error = 0; + struct device * dev; + + list_for_each_entry_reverse(dev, &dpm_off_irq, power.entry) { + if ((error = suspend_device(dev, state))) + break; + } + if (error) + goto Error; + if ((error = sysdev_suspend(state))) + goto Error; + Done: + return error; + Error: + printk(KERN_ERR "Could not power down device %s: " + "error %d\n", kobject_name(&dev->kobj), error); + dpm_power_up(); + goto Done; +} + +EXPORT_SYMBOL_GPL(device_power_down); + diff --git a/drivers/base/power/sysfs.c b/drivers/base/power/sysfs.c new file mode 100644 index 00000000000..6ac96349a8e --- /dev/null +++ b/drivers/base/power/sysfs.c @@ -0,0 +1,68 @@ +/* + * drivers/base/power/sysfs.c - sysfs entries for device PM + */ + +#include <linux/device.h> +#include "power.h" + + +/** + * state - Control current power state of device + * + * show() returns the current power state of the device. '0' indicates + * the device is on. Other values (1-3) indicate the device is in a low + * power state. + * + * store() sets the current power state, which is an integer value + * between 0-3. If the device is on ('0'), and the value written is + * greater than 0, then the device is placed directly into the low-power + * state (via its driver's ->suspend() method). + * If the device is currently in a low-power state, and the value is 0, + * the device is powered back on (via the ->resume() method). + * If the device is in a low-power state, and a different low-power state + * is requested, the device is first resumed, then suspended into the new + * low-power state. + */ + +static ssize_t state_show(struct device * dev, char * buf) +{ + return sprintf(buf, "%u\n", dev->power.power_state); +} + +static ssize_t state_store(struct device * dev, const char * buf, size_t n) +{ + u32 state; + char * rest; + int error = 0; + + state = simple_strtoul(buf, &rest, 10); + if (*rest) + return -EINVAL; + if (state) + error = dpm_runtime_suspend(dev, state); + else + dpm_runtime_resume(dev); + return error ? error : n; +} + +static DEVICE_ATTR(state, 0644, state_show, state_store); + + +static struct attribute * power_attrs[] = { + &dev_attr_state.attr, + NULL, +}; +static struct attribute_group pm_attr_group = { + .name = "power", + .attrs = power_attrs, +}; + +int dpm_sysfs_add(struct device * dev) +{ + return sysfs_create_group(&dev->kobj, &pm_attr_group); +} + +void dpm_sysfs_remove(struct device * dev) +{ + sysfs_remove_group(&dev->kobj, &pm_attr_group); +} |