aboutsummaryrefslogtreecommitdiff
path: root/drivers/i2c/chips/pcf50606.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/i2c/chips/pcf50606.c')
-rw-r--r--drivers/i2c/chips/pcf50606.c148
1 files changed, 142 insertions, 6 deletions
diff --git a/drivers/i2c/chips/pcf50606.c b/drivers/i2c/chips/pcf50606.c
index c58fd3f7a8e..97089da7d05 100644
--- a/drivers/i2c/chips/pcf50606.c
+++ b/drivers/i2c/chips/pcf50606.c
@@ -38,6 +38,7 @@
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/workqueue.h>
+#include <linux/delay.h>
#include <linux/rtc.h>
#include <linux/bcd.h>
#include <linux/watchdog.h>
@@ -95,6 +96,15 @@ enum close_state {
CLOSE_STATE_ALLOW = 0x2342,
};
+enum pcf50606_suspend_states {
+ PCF50606_SS_RUNNING,
+ PCF50606_SS_STARTING_SUSPEND,
+ PCF50606_SS_COMPLETED_SUSPEND,
+ PCF50606_SS_RESUMING_BUT_NOT_US_YET,
+ PCF50606_SS_STARTING_RESUME,
+ PCF50606_SS_COMPLETED_RESUME,
+};
+
struct pcf50606_data {
struct i2c_client client;
struct pcf50606_platform_data *pdata;
@@ -110,6 +120,7 @@ struct pcf50606_data {
int onkey_seconds;
int irq;
int coldplug_done;
+ enum pcf50606_suspend_states suspend_state;
#ifdef CONFIG_PM
struct {
u_int8_t dcdc1, dcdc2;
@@ -160,6 +171,10 @@ static const u_int16_t ntc_table_10k_3370B[] = {
static inline int __reg_write(struct pcf50606_data *pcf, u_int8_t reg,
u_int8_t val)
{
+ if (pcf->suspend_state == PCF50606_SS_COMPLETED_SUSPEND) {
+ dev_err(&pcf->client.dev, "__reg_write while suspended.\n");
+ dump_stack();
+ }
return i2c_smbus_write_byte_data(&pcf->client, reg, val);
}
@@ -178,6 +193,10 @@ static inline int32_t __reg_read(struct pcf50606_data *pcf, u_int8_t reg)
{
int32_t ret;
+ if (pcf->suspend_state == PCF50606_SS_COMPLETED_SUSPEND) {
+ dev_err(&pcf->client.dev, "__reg_read while suspended.\n");
+ dump_stack();
+ }
ret = i2c_smbus_read_byte_data(&pcf->client, reg);
return ret;
@@ -569,6 +588,48 @@ static void pcf50606_work(struct work_struct *work)
mutex_lock(&pcf->working_lock);
pcf->working = 1;
+
+ /* sanity */
+ if (!&pcf->client.dev)
+ goto bail;
+
+ /*
+ * if we are presently suspending, we are not in a position to deal
+ * with pcf50606 interrupts at all.
+ *
+ * Because we didn't clear the int pending registers, there will be
+ * no edge / interrupt waiting for us when we wake. But it is OK
+ * because at the end of our resume, we call this workqueue function
+ * gratuitously, clearing the pending register and re-enabling
+ * servicing this interrupt.
+ */
+
+ if ((pcf->suspend_state == PCF50606_SS_STARTING_SUSPEND) ||
+ (pcf->suspend_state == PCF50606_SS_COMPLETED_SUSPEND))
+ goto bail;
+
+ /*
+ * If we are inside suspend -> resume completion time we don't attempt
+ * service until we have fully resumed. Although we could talk to the
+ * device as soon as I2C is up, the regs in the device which we might
+ * choose to modify as part of the service action have not been
+ * reloaded with their pre-suspend states yet. Therefore we will
+ * defer our service if we are called like that until our resume has
+ * completed.
+ *
+ * This shouldn't happen any more because we disable servicing this
+ * interrupt in suspend and don't re-enable it until resume is
+ * completed.
+ */
+
+ if (pcf->suspend_state &&
+ (pcf->suspend_state != PCF50606_SS_COMPLETED_RESUME))
+ goto reschedule;
+
+ /* this is the case early in resume! Sanity check! */
+ if (i2c_get_clientdata(&pcf->client) == NULL)
+ goto reschedule;
+
/*
* p35 pcf50606 datasheet rev 2.2:
* ''The system controller shall read all interrupt registers in
@@ -576,10 +637,27 @@ static void pcf50606_work(struct work_struct *work)
* because if you don't INT# gets stuck asserted forever after a
* while
*/
- ret = i2c_smbus_read_i2c_block_data(&pcf->client, PCF50606_REG_INT1, 3,
- pcfirq);
- if (ret != 3)
+ ret = i2c_smbus_read_i2c_block_data(&pcf->client, PCF50606_REG_INT1,
+ sizeof(pcfirq), pcfirq);
+ if (ret != sizeof(pcfirq)) {
DEBUGPC("Oh crap PMU IRQ register read failed %d\n", ret);
+ /*
+ * it shouldn't fail, we no longer attempt to use
+ * I2C while it can be suspended. But we don't have
+ * much option but to retry if if it ever did fail,
+ * because if we don't service the interrupt to clear
+ * it, we will never see another PMU interrupt edge.
+ */
+ goto reschedule;
+ }
+
+ /* hey did we just resume? (because we don't get here unless we are
+ * running normally or the first call after resumption)
+ *
+ * pcf50606 resume is really really over now then.
+ */
+ if (pcf->suspend_state != PCF50606_SS_RUNNING)
+ pcf->suspend_state = PCF50606_SS_RUNNING;
if (!pcf->coldplug_done) {
DEBUGPC("PMU Coldplug init\n");
@@ -815,10 +893,26 @@ static void pcf50606_work(struct work_struct *work)
DEBUGPC("\n");
+bail:
pcf->working = 0;
input_sync(pcf->input_dev);
put_device(&pcf->client.dev);
mutex_unlock(&pcf->working_lock);
+
+ return;
+
+reschedule:
+
+ if ((pcf->suspend_state != PCF50606_SS_STARTING_SUSPEND) &&
+ (pcf->suspend_state != PCF50606_SS_COMPLETED_SUSPEND)) {
+ msleep(10);
+ dev_info(&pcf->client.dev, "rescheduling interrupt service\n");
+ }
+ if (!schedule_work(&pcf->work))
+ dev_err(&pcf->client.dev, "int service reschedule failed\n");
+
+ /* we don't put the device here, hold it for next time */
+ mutex_unlock(&pcf->working_lock);
}
static irqreturn_t pcf50606_irq(int irq, void *_pcf)
@@ -829,7 +923,7 @@ static irqreturn_t pcf50606_irq(int irq, void *_pcf)
irq, _pcf);
get_device(&pcf->client.dev);
if (!schedule_work(&pcf->work) && !pcf->working)
- dev_dbg(&pcf->client.dev, "work item may be lost\n");
+ dev_err(&pcf->client.dev, "pcf irq work already queued.\n");
return IRQ_HANDLED;
}
@@ -1882,12 +1976,27 @@ static int pcf50606_suspend(struct device *dev, pm_message_t state)
struct pcf50606_data *pcf = i2c_get_clientdata(client);
int i;
+ /* we suspend once (!) as late as possible in the suspend sequencing */
+
+ if ((state.event != PM_EVENT_SUSPEND) ||
+ (pcf->suspend_state != PCF50606_SS_RUNNING))
+ return -EBUSY;
+
/* The general idea is to power down all unused power supplies,
* and then mask all PCF50606 interrup sources but EXTONR, ONKEYF
* and ALARM */
mutex_lock(&pcf->lock);
+ pcf->suspend_state = PCF50606_SS_STARTING_SUSPEND;
+
+ /* we are not going to service any further interrupts until we
+ * resume. If the IRQ workqueue is still pending in the background,
+ * it will bail when it sees we set suspend state above.
+ */
+
+ disable_irq(pcf->irq);
+
/* Save all registers that don't "survive" standby state */
pcf->standby_regs.dcdc1 = __reg_read(pcf, PCF50606_REG_DCDC1);
pcf->standby_regs.dcdc2 = __reg_read(pcf, PCF50606_REG_DCDC2);
@@ -1928,6 +2037,8 @@ static int pcf50606_suspend(struct device *dev, pm_message_t state)
__reg_write(pcf, PCF50606_REG_INT2M, ~INT2M_RESUMERS & 0xff);
__reg_write(pcf, PCF50606_REG_INT3M, ~INT3M_RESUMERS & 0xff);
+ pcf->suspend_state = PCF50606_SS_COMPLETED_SUSPEND;
+
mutex_unlock(&pcf->lock);
return 0;
@@ -1940,6 +2051,8 @@ static int pcf50606_resume(struct device *dev)
mutex_lock(&pcf->lock);
+ pcf->suspend_state = PCF50606_SS_STARTING_RESUME;
+
/* Resume all saved registers that don't "survive" standby state */
__reg_write(pcf, PCF50606_REG_INT1M, pcf->standby_regs.int1m);
__reg_write(pcf, PCF50606_REG_INT2M, pcf->standby_regs.int2m);
@@ -1958,10 +2071,17 @@ static int pcf50606_resume(struct device *dev)
__reg_write(pcf, PCF50606_REG_ADCC2, pcf->standby_regs.adcc2);
__reg_write(pcf, PCF50606_REG_PWMC1, pcf->standby_regs.pwmc1);
+ pcf->suspend_state = PCF50606_SS_COMPLETED_RESUME;
+
+ enable_irq(pcf->irq);
+
mutex_unlock(&pcf->lock);
- /* Hack to fix the gta01 power button problem on resume */
- pcf50606_irq(0, pcf);
+ /* Call PCF work function; this fixes an issue on the gta01 where
+ * the power button "goes away" if it is used to wake the device.
+ */
+ get_device(&pcf->client.dev);
+ pcf50606_work(&pcf->work);
return 0;
}
@@ -1999,9 +2119,25 @@ static int pcf50606_plat_remove(struct platform_device *pdev)
return 0;
}
+/* We have this purely to capture an early indication that we are coming out
+ * of suspend, before our device resume got called; async interrupt service is
+ * interested in this.
+ */
+
+static int pcf50606_plat_resume(struct platform_device *pdev)
+{
+ /* i2c_get_clientdata(to_i2c_client(&pdev->dev)) returns NULL at this
+ * early resume time so we have to use pcf50606_global
+ */
+ pcf50606_global->suspend_state = PCF50606_SS_RESUMING_BUT_NOT_US_YET;
+
+ return 0;
+}
+
static struct platform_driver pcf50606_plat_driver = {
.probe = pcf50606_plat_probe,
.remove = pcf50606_plat_remove,
+ .resume_early = pcf50606_plat_resume,
.driver = {
.owner = THIS_MODULE,
.name = "pcf50606",