linux/drivers/power/pda_power.c
Paul Parsons ec60ea5cd6 pda_power: Fix ac_draw usage before it being set
When I reboot my iPAQ hx4700 in its cradle, the battery will not begin
to charge even though the AC supply is connected. Charging will start
only after the PDA power driver is tickled by some other power event,
such as reseating the iPAQ in its cradle or connecting the USB cable.

The problem lies in pda_power_probe(), where ac_draw is used by the call
to update_charger() before being set by the call to regulator_get().

Moving the regulator_get() call to before the update_charger() call
fixes the problem.

Signed-off-by: Paul Parsons <lost.distance@yahoo.com>
Cc: Philipp Zabel <philipp.zabel@gmail.com>
Signed-off-by: Anton Vorontsov <anton.vorontsov@linaro.org>
2012-09-20 14:27:34 -07:00

514 lines
12 KiB
C

/*
* Common power driver for PDAs and phones with one or two external
* power supplies (AC/USB) connected to main and backup batteries,
* and optional builtin charger.
*
* Copyright © 2007 Anton Vorontsov <cbou@mail.ru>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/notifier.h>
#include <linux/power_supply.h>
#include <linux/pda_power.h>
#include <linux/regulator/consumer.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/usb/otg.h>
static inline unsigned int get_irq_flags(struct resource *res)
{
return IRQF_SHARED | (res->flags & IRQF_TRIGGER_MASK);
}
static struct device *dev;
static struct pda_power_pdata *pdata;
static struct resource *ac_irq, *usb_irq;
static struct timer_list charger_timer;
static struct timer_list supply_timer;
static struct timer_list polling_timer;
static int polling;
#ifdef CONFIG_USB_OTG_UTILS
static struct usb_phy *transceiver;
static struct notifier_block otg_nb;
#endif
static struct regulator *ac_draw;
enum {
PDA_PSY_OFFLINE = 0,
PDA_PSY_ONLINE = 1,
PDA_PSY_TO_CHANGE,
};
static int new_ac_status = -1;
static int new_usb_status = -1;
static int ac_status = -1;
static int usb_status = -1;
static int pda_power_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
switch (psp) {
case POWER_SUPPLY_PROP_ONLINE:
if (psy->type == POWER_SUPPLY_TYPE_MAINS)
val->intval = pdata->is_ac_online ?
pdata->is_ac_online() : 0;
else
val->intval = pdata->is_usb_online ?
pdata->is_usb_online() : 0;
break;
default:
return -EINVAL;
}
return 0;
}
static enum power_supply_property pda_power_props[] = {
POWER_SUPPLY_PROP_ONLINE,
};
static char *pda_power_supplied_to[] = {
"main-battery",
"backup-battery",
};
static struct power_supply pda_psy_ac = {
.name = "ac",
.type = POWER_SUPPLY_TYPE_MAINS,
.supplied_to = pda_power_supplied_to,
.num_supplicants = ARRAY_SIZE(pda_power_supplied_to),
.properties = pda_power_props,
.num_properties = ARRAY_SIZE(pda_power_props),
.get_property = pda_power_get_property,
};
static struct power_supply pda_psy_usb = {
.name = "usb",
.type = POWER_SUPPLY_TYPE_USB,
.supplied_to = pda_power_supplied_to,
.num_supplicants = ARRAY_SIZE(pda_power_supplied_to),
.properties = pda_power_props,
.num_properties = ARRAY_SIZE(pda_power_props),
.get_property = pda_power_get_property,
};
static void update_status(void)
{
if (pdata->is_ac_online)
new_ac_status = !!pdata->is_ac_online();
if (pdata->is_usb_online)
new_usb_status = !!pdata->is_usb_online();
}
static void update_charger(void)
{
static int regulator_enabled;
int max_uA = pdata->ac_max_uA;
if (pdata->set_charge) {
if (new_ac_status > 0) {
dev_dbg(dev, "charger on (AC)\n");
pdata->set_charge(PDA_POWER_CHARGE_AC);
} else if (new_usb_status > 0) {
dev_dbg(dev, "charger on (USB)\n");
pdata->set_charge(PDA_POWER_CHARGE_USB);
} else {
dev_dbg(dev, "charger off\n");
pdata->set_charge(0);
}
} else if (ac_draw) {
if (new_ac_status > 0) {
regulator_set_current_limit(ac_draw, max_uA, max_uA);
if (!regulator_enabled) {
dev_dbg(dev, "charger on (AC)\n");
WARN_ON(regulator_enable(ac_draw));
regulator_enabled = 1;
}
} else {
if (regulator_enabled) {
dev_dbg(dev, "charger off\n");
WARN_ON(regulator_disable(ac_draw));
regulator_enabled = 0;
}
}
}
}
static void supply_timer_func(unsigned long unused)
{
if (ac_status == PDA_PSY_TO_CHANGE) {
ac_status = new_ac_status;
power_supply_changed(&pda_psy_ac);
}
if (usb_status == PDA_PSY_TO_CHANGE) {
usb_status = new_usb_status;
power_supply_changed(&pda_psy_usb);
}
}
static void psy_changed(void)
{
update_charger();
/*
* Okay, charger set. Now wait a bit before notifying supplicants,
* charge power should stabilize.
*/
mod_timer(&supply_timer,
jiffies + msecs_to_jiffies(pdata->wait_for_charger));
}
static void charger_timer_func(unsigned long unused)
{
update_status();
psy_changed();
}
static irqreturn_t power_changed_isr(int irq, void *power_supply)
{
if (power_supply == &pda_psy_ac)
ac_status = PDA_PSY_TO_CHANGE;
else if (power_supply == &pda_psy_usb)
usb_status = PDA_PSY_TO_CHANGE;
else
return IRQ_NONE;
/*
* Wait a bit before reading ac/usb line status and setting charger,
* because ac/usb status readings may lag from irq.
*/
mod_timer(&charger_timer,
jiffies + msecs_to_jiffies(pdata->wait_for_status));
return IRQ_HANDLED;
}
static void polling_timer_func(unsigned long unused)
{
int changed = 0;
dev_dbg(dev, "polling...\n");
update_status();
if (!ac_irq && new_ac_status != ac_status) {
ac_status = PDA_PSY_TO_CHANGE;
changed = 1;
}
if (!usb_irq && new_usb_status != usb_status) {
usb_status = PDA_PSY_TO_CHANGE;
changed = 1;
}
if (changed)
psy_changed();
mod_timer(&polling_timer,
jiffies + msecs_to_jiffies(pdata->polling_interval));
}
#ifdef CONFIG_USB_OTG_UTILS
static int otg_is_usb_online(void)
{
return (transceiver->last_event == USB_EVENT_VBUS ||
transceiver->last_event == USB_EVENT_ENUMERATED);
}
static int otg_is_ac_online(void)
{
return (transceiver->last_event == USB_EVENT_CHARGER);
}
static int otg_handle_notification(struct notifier_block *nb,
unsigned long event, void *unused)
{
switch (event) {
case USB_EVENT_CHARGER:
ac_status = PDA_PSY_TO_CHANGE;
break;
case USB_EVENT_VBUS:
case USB_EVENT_ENUMERATED:
usb_status = PDA_PSY_TO_CHANGE;
break;
case USB_EVENT_NONE:
ac_status = PDA_PSY_TO_CHANGE;
usb_status = PDA_PSY_TO_CHANGE;
break;
default:
return NOTIFY_OK;
}
/*
* Wait a bit before reading ac/usb line status and setting charger,
* because ac/usb status readings may lag from irq.
*/
mod_timer(&charger_timer,
jiffies + msecs_to_jiffies(pdata->wait_for_status));
return NOTIFY_OK;
}
#endif
static int pda_power_probe(struct platform_device *pdev)
{
int ret = 0;
dev = &pdev->dev;
if (pdev->id != -1) {
dev_err(dev, "it's meaningless to register several "
"pda_powers; use id = -1\n");
ret = -EINVAL;
goto wrongid;
}
pdata = pdev->dev.platform_data;
if (pdata->init) {
ret = pdata->init(dev);
if (ret < 0)
goto init_failed;
}
ac_draw = regulator_get(dev, "ac_draw");
if (IS_ERR(ac_draw)) {
dev_dbg(dev, "couldn't get ac_draw regulator\n");
ac_draw = NULL;
ret = PTR_ERR(ac_draw);
goto ac_draw_failed;
}
update_status();
update_charger();
if (!pdata->wait_for_status)
pdata->wait_for_status = 500;
if (!pdata->wait_for_charger)
pdata->wait_for_charger = 500;
if (!pdata->polling_interval)
pdata->polling_interval = 2000;
if (!pdata->ac_max_uA)
pdata->ac_max_uA = 500000;
setup_timer(&charger_timer, charger_timer_func, 0);
setup_timer(&supply_timer, supply_timer_func, 0);
ac_irq = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "ac");
usb_irq = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "usb");
if (pdata->supplied_to) {
pda_psy_ac.supplied_to = pdata->supplied_to;
pda_psy_ac.num_supplicants = pdata->num_supplicants;
pda_psy_usb.supplied_to = pdata->supplied_to;
pda_psy_usb.num_supplicants = pdata->num_supplicants;
}
#ifdef CONFIG_USB_OTG_UTILS
transceiver = usb_get_phy(USB_PHY_TYPE_USB2);
if (!IS_ERR_OR_NULL(transceiver)) {
if (!pdata->is_usb_online)
pdata->is_usb_online = otg_is_usb_online;
if (!pdata->is_ac_online)
pdata->is_ac_online = otg_is_ac_online;
}
#endif
if (pdata->is_ac_online) {
ret = power_supply_register(&pdev->dev, &pda_psy_ac);
if (ret) {
dev_err(dev, "failed to register %s power supply\n",
pda_psy_ac.name);
goto ac_supply_failed;
}
if (ac_irq) {
ret = request_irq(ac_irq->start, power_changed_isr,
get_irq_flags(ac_irq), ac_irq->name,
&pda_psy_ac);
if (ret) {
dev_err(dev, "request ac irq failed\n");
goto ac_irq_failed;
}
} else {
polling = 1;
}
}
if (pdata->is_usb_online) {
ret = power_supply_register(&pdev->dev, &pda_psy_usb);
if (ret) {
dev_err(dev, "failed to register %s power supply\n",
pda_psy_usb.name);
goto usb_supply_failed;
}
if (usb_irq) {
ret = request_irq(usb_irq->start, power_changed_isr,
get_irq_flags(usb_irq),
usb_irq->name, &pda_psy_usb);
if (ret) {
dev_err(dev, "request usb irq failed\n");
goto usb_irq_failed;
}
} else {
polling = 1;
}
}
#ifdef CONFIG_USB_OTG_UTILS
if (!IS_ERR_OR_NULL(transceiver) && pdata->use_otg_notifier) {
otg_nb.notifier_call = otg_handle_notification;
ret = usb_register_notifier(transceiver, &otg_nb);
if (ret) {
dev_err(dev, "failure to register otg notifier\n");
goto otg_reg_notifier_failed;
}
polling = 0;
}
#endif
if (polling) {
dev_dbg(dev, "will poll for status\n");
setup_timer(&polling_timer, polling_timer_func, 0);
mod_timer(&polling_timer,
jiffies + msecs_to_jiffies(pdata->polling_interval));
}
if (ac_irq || usb_irq)
device_init_wakeup(&pdev->dev, 1);
return 0;
#ifdef CONFIG_USB_OTG_UTILS
otg_reg_notifier_failed:
if (pdata->is_usb_online && usb_irq)
free_irq(usb_irq->start, &pda_psy_usb);
#endif
usb_irq_failed:
if (pdata->is_usb_online)
power_supply_unregister(&pda_psy_usb);
usb_supply_failed:
if (pdata->is_ac_online && ac_irq)
free_irq(ac_irq->start, &pda_psy_ac);
#ifdef CONFIG_USB_OTG_UTILS
if (!IS_ERR_OR_NULL(transceiver))
usb_put_phy(transceiver);
#endif
ac_irq_failed:
if (pdata->is_ac_online)
power_supply_unregister(&pda_psy_ac);
ac_supply_failed:
if (ac_draw) {
regulator_put(ac_draw);
ac_draw = NULL;
}
ac_draw_failed:
if (pdata->exit)
pdata->exit(dev);
init_failed:
wrongid:
return ret;
}
static int pda_power_remove(struct platform_device *pdev)
{
if (pdata->is_usb_online && usb_irq)
free_irq(usb_irq->start, &pda_psy_usb);
if (pdata->is_ac_online && ac_irq)
free_irq(ac_irq->start, &pda_psy_ac);
if (polling)
del_timer_sync(&polling_timer);
del_timer_sync(&charger_timer);
del_timer_sync(&supply_timer);
if (pdata->is_usb_online)
power_supply_unregister(&pda_psy_usb);
if (pdata->is_ac_online)
power_supply_unregister(&pda_psy_ac);
#ifdef CONFIG_USB_OTG_UTILS
if (!IS_ERR_OR_NULL(transceiver))
usb_put_phy(transceiver);
#endif
if (ac_draw) {
regulator_put(ac_draw);
ac_draw = NULL;
}
if (pdata->exit)
pdata->exit(dev);
return 0;
}
#ifdef CONFIG_PM
static int ac_wakeup_enabled;
static int usb_wakeup_enabled;
static int pda_power_suspend(struct platform_device *pdev, pm_message_t state)
{
if (pdata->suspend) {
int ret = pdata->suspend(state);
if (ret)
return ret;
}
if (device_may_wakeup(&pdev->dev)) {
if (ac_irq)
ac_wakeup_enabled = !enable_irq_wake(ac_irq->start);
if (usb_irq)
usb_wakeup_enabled = !enable_irq_wake(usb_irq->start);
}
return 0;
}
static int pda_power_resume(struct platform_device *pdev)
{
if (device_may_wakeup(&pdev->dev)) {
if (usb_irq && usb_wakeup_enabled)
disable_irq_wake(usb_irq->start);
if (ac_irq && ac_wakeup_enabled)
disable_irq_wake(ac_irq->start);
}
if (pdata->resume)
return pdata->resume();
return 0;
}
#else
#define pda_power_suspend NULL
#define pda_power_resume NULL
#endif /* CONFIG_PM */
static struct platform_driver pda_power_pdrv = {
.driver = {
.name = "pda-power",
},
.probe = pda_power_probe,
.remove = pda_power_remove,
.suspend = pda_power_suspend,
.resume = pda_power_resume,
};
module_platform_driver(pda_power_pdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anton Vorontsov <cbou@mail.ru>");
MODULE_ALIAS("platform:pda-power");