diff options
Diffstat (limited to 'drivers/i2c/chips/pcf50606.c')
-rw-r--r-- | drivers/i2c/chips/pcf50606.c | 148 |
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", |