Commit 9c823fa4 authored by Patrick Mochel's avatar Patrick Mochel

[power] Improve device suspend/resume sequence.

- Split calls into 
  - device_pm_suspend() [ Saving device state. ]
  - device_pm_power_down() [ Powering devices down. ]
  - device_pm_power_up() [ Powering devices up. ]
  - device_pm_resume() [ Restoring device state. ]

- Walk local dpm_active list when suspending devices, and move devices to 
  dpm_suspended list when ->suspend() is called. 
- Walk dpm_suspended list to power down devices (with interrrupts enabled.)
  - Try to power down devices with IRQs on. 
  - If they succeed, move them to dpm_off list.
  - If they return -EAGAIN, move them to dpm_off_irq list. 
- Disable interrupts and suspend devices needed interrupts off. 

- Do converse on resume 
  - power on devices that need interrupts off and move them to 
    dpm_suspended.
  - Enable interrupts.
  - Power on all other devices and move them to dpm_suspended.
  - Restore state of all devices and move them to dpm_active. 
parent 44a245c3
...@@ -24,10 +24,13 @@ ...@@ -24,10 +24,13 @@
#include <linux/device.h> #include <linux/device.h>
static LIST_HEAD(dpm_active_list); LIST_HEAD(dpm_active);
LIST_HEAD(dpm_suspended);
LIST_HEAD(dpm_off);
LIST_HEAD(dpm_off_irq);
static spinlock_t dpm_lock = SPIN_LOCK_UNLOCKED; spinlock_t dpm_lock = SPIN_LOCK_UNLOCKED;
static DECLARE_MUTEX(dpm_sem); DECLARE_MUTEX(dpm_sem);
static struct attribute power_attrs[] = { static struct attribute power_attrs[] = {
{ .name = NULL }, { .name = NULL },
...@@ -45,7 +48,7 @@ int device_pm_add(struct device * dev) ...@@ -45,7 +48,7 @@ int device_pm_add(struct device * dev)
dev->bus ? dev->bus->name : "No Bus", dev->kobj.name); dev->bus ? dev->bus->name : "No Bus", dev->kobj.name);
down(&dpm_sem); down(&dpm_sem);
spin_lock(&dpm_lock); spin_lock(&dpm_lock);
list_add_tail(&dev->power.entry,&dpm_active_list); list_add_tail(&dev->power.entry,&dpm_active);
spin_unlock(&dpm_lock); spin_unlock(&dpm_lock);
error = sysfs_create_group(&dev->kobj,&pm_attr_group); error = sysfs_create_group(&dev->kobj,&pm_attr_group);
up(&dpm_sem); up(&dpm_sem);
......
/*
* Used to synchronize global power management operations.
*/
extern struct semaphore dpm_sem;
/*
* Used to protect PM lists.
*/
extern spinlock_t dpm_lock;
/*
* The PM lists.
*/
extern struct list_head dpm_active;
extern struct list_head dpm_suspended;
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);
}
/*
* resume.c
*/
extern int dpm_resume(void);
extern void dpm_power_up(void);
extern void dpm_power_up_irq(void);
...@@ -9,46 +9,153 @@ ...@@ -9,46 +9,153 @@
*/ */
#include <linux/device.h> #include <linux/device.h>
#include "power.h"
#define to_dev(node) container_of(node,struct device,kobj.entry) extern int sysdev_resume(void);
extern int sysdev_restore(void);
extern struct subsystem devices_subsys;
/**
* resume_device - Restore state for one device.
* @dev: Device.
*
*/
extern int sysdev_resume(void); static int resume_device(struct device * dev)
extern int sysdev_restore(void); {
struct device_driver * drv = dev->driver;
if (drv && drv->resume)
return drv->resume(dev,RESUME_RESTORE_STATE);
return 0;
}
/**
* dpm_resume - Restore all device state.
*
* Walk the dpm_suspended list and restore each device. As they are
* resumed, move the devices to the dpm_active list.
*/
int dpm_resume(void)
{
spin_lock(&dpm_lock);
while(!list_empty(&dpm_suspended)) {
struct list_head * entry = dpm_suspended.next;
struct device * dev = to_device(entry);
list_del_init(entry);
spin_unlock(&dpm_lock);
resume_device(dev);
spin_lock(&dpm_lock);
list_add_tail(entry,&dpm_active);
}
spin_unlock(&dpm_lock);
return 0;
}
/**
* device_pm_resume - Restore state of each device in system.
*
* Restore system device state, then common device state. Finally,
* release dpm_sem, as we're done with device PM.
*/
void device_pm_resume(void)
{
sysdev_restore();
dpm_resume();
up(&dpm_sem);
}
/**
* power_up_device - Power one device on.
* @dev: Device.
*/
static void power_up_device(struct device * dev)
{
struct device_driver * drv = dev->driver;
if (drv && drv->resume)
drv->resume(dev,RESUME_POWER_ON);
}
/**
* 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_irq(void)
{
spin_lock_irq(&dpm_lock);
while(!list_empty(&dpm_off_irq)) {
struct list_head * entry = dpm_off_irq.next;
list_del_init(entry);
power_up_device(to_device(entry));
list_add_tail(entry,&dpm_suspended);
}
spin_unlock_irq(&dpm_lock);
}
/**
* dpm_power_up - Power on most devices.
*
* Walk the dpm_off list and power each device up. This is used
* to power on devices that were able to power down with interrupts
* enabled.
*/
void dpm_power_up(void)
{
spin_lock(&dpm_lock);
while (!list_empty(&dpm_off)) {
struct list_head * entry = dpm_off.next;
list_del_init(entry);
power_up_device(to_device(entry));
list_add_tail(entry,&dpm_suspended);
}
spin_unlock(&dpm_lock);
}
/**
* device_pm_power_up - Turn on all devices.
*
* First, power on system devices, which must happen with interrupts
* disbled. Then, power on devices that also require interrupts disabled.
* Turn interrupts back on, and finally power up the rest of the normal
* devices.
*/
void device_pm_power_up(void)
{
sysdev_resume();
dpm_power_up_irq();
local_irq_enable();
dpm_power_up();
}
/** /**
* device_resume - resume all the devices in the system * device_resume - resume all the devices in the system
* @level: stage of resume process we're at * @level: stage of resume process we're at
* *
* Similar to device_suspend above, though we want to do a breadth-first * This function is deprecated, and should be replaced with appropriate
* walk of the tree to make sure we wake up parents before children. * calls to device_pm_power_up() and device_pm_resume() above.
* So, we iterate over the list backward.
*/ */
void device_resume(u32 level) void device_resume(u32 level)
{ {
struct device * dev;
switch (level) {
case RESUME_POWER_ON:
sysdev_resume();
break;
case RESUME_RESTORE_STATE:
sysdev_restore();
break;
default:
break;
}
down_write(&devices_subsys.rwsem); printk("%s is deprecated. Called from:\n",__FUNCTION__);
list_for_each_entry(dev,&devices_subsys.kset.list,kobj.entry) { dump_stack();
if (dev->driver && dev->driver->resume) {
pr_debug("resuming device %s\n",dev->name);
dev->driver->resume(dev,level);
}
}
up_write(&devices_subsys.rwsem);
} }
...@@ -9,56 +9,234 @@ ...@@ -9,56 +9,234 @@
*/ */
#include <linux/device.h> #include <linux/device.h>
#include "power.h"
#define to_dev(node) container_of(node,struct device,kobj.entry)
extern struct subsystem devices_subsys;
extern int sysdev_save(u32 state); extern int sysdev_save(u32 state);
extern int sysdev_suspend(u32 state); extern int sysdev_suspend(u32 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.
*/
/** /**
* device_suspend - suspend/remove all devices on the device ree * suspend_device - Save state of one device.
* @state: state we're entering * @dev: Device.
* @level: what stage of the suspend process we're at * @state: Power state device is entering.
* (emb: it seems that these two arguments are described backwards of what */
* they actually mean .. is this correct?)
static int suspend_device(struct device * dev, u32 state)
{
struct device_driver * drv = dev->driver;
if (drv && drv->suspend)
return drv->suspend(dev,state,SUSPEND_SAVE_STATE);
return 0;
}
/**
* device_pm_suspend - Save state and stop all devices in system.
* @state: Power state to put each device in.
* *
* The entries in the global device list are inserted such that they're in a * Walk the dpm_active list, call ->suspend() for each device, and move
* depth-first ordering. So, simply interate over the list, and call the * it to dpm_suspended. If we hit a failure with any of the devices, call
* driver's suspend or remove callback for each device. * dpm_resume() above to bring the suspended devices back to life.
*
* Have system devices save state last.
*
* Note this function leaves dpm_sem held to
* a) block other devices from registering.
* b) prevent other PM operations from happening after we've begun.
* c) make sure we're exclusive when we disable interrupts.
*
* device_pm_resume() will release dpm_sem after restoring state to
* all devices (as will this on error). You must call it once you've
* called device_pm_suspend().
*/ */
int device_suspend(u32 state, u32 level)
int device_pm_suspend(u32 state)
{ {
struct device * dev;
int error = 0; int error = 0;
down_write(&devices_subsys.rwsem); down(&dpm_sem);
list_for_each_entry_reverse(dev,&devices_subsys.kset.list,kobj.entry) { spin_lock(&dpm_lock);
if (dev->driver && dev->driver->suspend) { while(!list_empty(&dpm_active)) {
pr_debug("suspending device %s\n",dev->name); struct list_head * entry = dpm_active.prev;
error = dev->driver->suspend(dev,state,level); struct device * dev = to_device(entry);
if (error) list_del_init(entry);
printk(KERN_ERR "%s: suspend returned %d\n", spin_unlock(&dpm_lock);
dev->name,error); error = suspend_device(dev,state);
spin_lock(&dpm_lock);
if (!error)
list_add(entry,&dpm_suspended);
else {
list_add_tail(entry,&dpm_active);
goto Error;
} }
} }
up_write(&devices_subsys.rwsem); spin_unlock(&dpm_lock);
/* if ((error = sysdev_save(state)))
* Make sure system devices are suspended. goto Error;
*/ Done:
switch(level) { return error;
case SUSPEND_SAVE_STATE: Error:
sysdev_save(state); dpm_resume();
break; up(&dpm_sem);
case SUSPEND_POWER_DOWN: goto Done;
sysdev_suspend(state); }
break;
default:
break; /**
* power_down_device - Put one device in low power state.
* @dev: Device.
* @state: Power state to enter.
*/
static int power_down_device(struct device * dev, u32 state)
{
struct device_driver * drv = dev->driver;
if (drv && drv->suspend)
return drv->suspend(dev,state,SUSPEND_POWER_DOWN);
return 0;
}
/**
* dpm_power_down - Put all devices in low power state.
* @state: Power state to enter.
*
* Walk the dpm_suspended list (with interrupts enabled) and try
* to power down each each. If any fail with -EAGAIN, they require
* the call to be done with interrupts disabled. So, we move them to
* the dpm_off_irq list.
*
* If the call succeeds, we move each device to the dpm_off list.
*/
static int dpm_power_down(u32 state)
{
spin_lock(&dpm_lock);
while(!list_empty(&dpm_suspended)) {
struct list_head * entry = dpm_suspended.prev;
int error;
list_del_init(entry);
spin_unlock(&dpm_lock);
error = power_down_device(to_device(entry),state);
spin_lock(&dpm_lock);
if (!error)
list_add(entry,&dpm_off);
else if (error == -EAGAIN)
list_add(entry,&dpm_off_irq);
else {
list_add_tail(entry,&dpm_suspended);
return error;
}
} }
spin_unlock(&dpm_lock);
return 0;
}
/**
* dpm_power_down_irq - Power down devices without interrupts.
* @state: State to enter.
*
* Walk the dpm_off_irq list (built by dpm_power_down) and power
* down each device that requires the call to be made with interrupts
* disabled.
*/
static int dpm_power_down_irq(u32 state)
{
struct device * dev;
int error = 0;
spin_lock_irq(&dpm_lock);
list_for_each_entry_reverse(dev,&dpm_off_irq,power.entry) {
if ((error = power_down_device(dev,state)))
break;
}
spin_unlock_irq(&dpm_lock);
return error; return error;
} }
/**
* device_pm_power_down - Put all devices in low power state.
* @state: Power state to enter.
*
* Walk the dpm_suspended list, calling ->power_down() for each device.
* 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.
*
* dpm_irq_off is for devices that require interrupts to be disabled to
* either to power down the device or power it back on.
*
* When we're done, we disable interrrupts (!!) and walk the dpm_off_irq
* list to shut down the devices that need interrupts disabled.
*
* This function leaves interrupts disabled on exit, since powering down
* devices should be the very last thing before the system is put into a
* low-power state.
*
* device_pm_power_on() should be called to re-enable interrupts and power
* the devices back on.
*/
int device_pm_power_down(u32 state)
{
int error = 0;
if ((error = dpm_power_down(state)))
goto ErrorIRQOn;
local_irq_disable();
if ((error = dpm_power_down_irq(state)))
goto ErrorIRQOff;
sysdev_suspend(state);
Done:
return error;
ErrorIRQOff:
dpm_power_up_irq();
local_irq_enable();
ErrorIRQOn:
dpm_power_up();
goto Done;
}
/**
* device_suspend - suspend all devices on the device ree
* @state: state we're entering
* @level: Stage of suspend sequence we're in.
*
*
* This function is deprecated. Calls should be replaced with
* appropriate calls to device_pm_suspend() and device_pm_power_down().
*/
int device_suspend(u32 state, u32 level)
{
printk("%s Called from:\n",__FUNCTION__);
dump_stack();
return -EFAULT;
}
...@@ -197,6 +197,12 @@ struct dev_pm_info { ...@@ -197,6 +197,12 @@ struct dev_pm_info {
#endif #endif
}; };
extern int device_pm_suspend(u32 state);
extern int device_pm_power_down(u32 state);
extern void device_pm_power_up(void);
extern void device_pm_resume(void);
#endif /* __KERNEL__ */ #endif /* __KERNEL__ */
#endif /* _LINUX_PM_H */ #endif /* _LINUX_PM_H */
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment