/* Enable interrupts and we're off */
        snd_soc_update_bits(codec, WM8996_INTERRUPT_STATUS_2_MASK,
-                           WM8996_IM_MICD_EINT, 0);
+                           WM8996_IM_MICD_EINT | WM8996_HP_DONE_EINT, 0);
 
        return 0;
 }
 EXPORT_SYMBOL_GPL(wm8996_detect);
 
+static void wm8996_hpdet_irq(struct snd_soc_codec *codec)
+{
+       struct wm8996_priv *wm8996 = snd_soc_codec_get_drvdata(codec);
+       int val, reg, report;
+
+       /* Assume headphone in error conditions; we need to report
+        * something or we stall our state machine.
+        */
+       report = SND_JACK_HEADPHONE;
+
+       reg = snd_soc_read(codec, WM8996_HEADPHONE_DETECT_2);
+       if (reg < 0) {
+               dev_err(codec->dev, "Failed to read HPDET status\n");
+               goto out;
+       }
+
+       if (!(reg & WM8996_HP_DONE)) {
+               dev_err(codec->dev, "Got HPDET IRQ but HPDET is busy\n");
+               goto out;
+       }
+
+       val = reg & WM8996_HP_LVL_MASK;
+
+       dev_dbg(codec->dev, "HPDET measured %d ohms\n", val);
+
+       /* If we've got high enough impedence then report as line,
+        * otherwise assume headphone.
+        */
+       if (val >= 126)
+               report = SND_JACK_LINEOUT;
+       else
+               report = SND_JACK_HEADPHONE;
+
+out:
+       if (wm8996->jack_mic)
+               report |= SND_JACK_MICROPHONE;
+
+       snd_soc_jack_report(wm8996->jack, report,
+                           SND_JACK_LINEOUT | SND_JACK_HEADSET);
+
+       wm8996->detecting = false;
+
+       /* If the output isn't running re-clamp it */
+       if (!(snd_soc_read(codec, WM8996_POWER_MANAGEMENT_1) &
+             (WM8996_HPOUT1L_ENA | WM8996_HPOUT1R_RMV_SHORT)))
+               snd_soc_update_bits(codec, WM8996_ANALOGUE_HP_1,
+                                   WM8996_HPOUT1L_RMV_SHORT |
+                                   WM8996_HPOUT1R_RMV_SHORT, 0);
+
+       /* Go back to looking at the microphone */
+       snd_soc_update_bits(codec, WM8996_ACCESSORY_DETECT_MODE_1,
+                           WM8996_JD_MODE_MASK, 0);
+       snd_soc_update_bits(codec, WM8996_MIC_DETECT_1, WM8996_MICD_ENA,
+                           WM8996_MICD_ENA);
+
+       snd_soc_dapm_disable_pin(&codec->dapm, "Bandgap");
+       snd_soc_dapm_sync(&codec->dapm);
+}
+
+static void wm8996_hpdet_start(struct snd_soc_codec *codec)
+{
+       /* Unclamp the output, we can't measure while we're shorting it */
+       snd_soc_update_bits(codec, WM8996_ANALOGUE_HP_1,
+                           WM8996_HPOUT1L_RMV_SHORT |
+                           WM8996_HPOUT1R_RMV_SHORT,
+                           WM8996_HPOUT1L_RMV_SHORT |
+                           WM8996_HPOUT1R_RMV_SHORT);
+
+       /* We need bandgap for HPDET */
+       snd_soc_dapm_force_enable_pin(&codec->dapm, "Bandgap");
+       snd_soc_dapm_sync(&codec->dapm);
+
+       /* Go into headphone detect left mode */
+       snd_soc_update_bits(codec, WM8996_MIC_DETECT_1, WM8996_MICD_ENA, 0);
+       snd_soc_update_bits(codec, WM8996_ACCESSORY_DETECT_MODE_1,
+                           WM8996_JD_MODE_MASK, 1);
+
+       /* Trigger a measurement */
+       snd_soc_update_bits(codec, WM8996_HEADPHONE_DETECT_1,
+                           WM8996_HP_POLL, WM8996_HP_POLL);
+}
+
 static void wm8996_micd(struct snd_soc_codec *codec)
 {
        struct wm8996_priv *wm8996 = snd_soc_codec_get_drvdata(codec);
                wm8996->jack_mic = false;
                wm8996->detecting = true;
                snd_soc_jack_report(wm8996->jack, 0,
-                                   SND_JACK_HEADSET | SND_JACK_BTN_0);
+                                   SND_JACK_LINEOUT | SND_JACK_HEADSET |
+                                   SND_JACK_BTN_0);
+
                snd_soc_update_bits(codec, WM8996_MIC_DETECT_1,
                                    WM8996_MICD_RATE_MASK,
                                    WM8996_MICD_RATE_MASK);
                return;
        }
 
-       /* If the measurement is very high we've got a microphone but
-        * do a little debounce to account for mechanical issues.
+       /* If the measurement is very high we've got a microphone,
+        * either we just detected one or if we already reported then
+        * we've got a button release event.
         */
        if (val & 0x400) {
-               dev_dbg(codec->dev, "Microphone detected\n");
-               snd_soc_jack_report(wm8996->jack, SND_JACK_HEADSET,
-                                   SND_JACK_HEADSET | SND_JACK_BTN_0);
-               wm8996->jack_mic = true;
-               wm8996->detecting = false;
-
-               /* Increase poll rate to give better responsiveness
-                * for buttons */
-               snd_soc_update_bits(codec, WM8996_MIC_DETECT_1,
-                                   WM8996_MICD_RATE_MASK,
-                                   5 << WM8996_MICD_RATE_SHIFT);
+               if (wm8996->detecting) {
+                       dev_dbg(codec->dev, "Microphone detected\n");
+                       wm8996->jack_mic = true;
+                       wm8996_hpdet_start(codec);
+
+                       /* Increase poll rate to give better responsiveness
+                        * for buttons */
+                       snd_soc_update_bits(codec, WM8996_MIC_DETECT_1,
+                                           WM8996_MICD_RATE_MASK,
+                                           5 << WM8996_MICD_RATE_SHIFT);
+               } else {
+                       dev_dbg(codec->dev, "Mic button up\n");
+                       snd_soc_jack_report(wm8996->jack, 0, SND_JACK_BTN_0);
+               }
+
+               return;
        }
 
        /* If we detected a lower impedence during initial startup
        if (val & 0x3fc) {
                if (wm8996->jack_mic) {
                        dev_dbg(codec->dev, "Mic button detected\n");
-                       snd_soc_jack_report(wm8996->jack,
-                                           SND_JACK_HEADSET | SND_JACK_BTN_0,
-                                           SND_JACK_HEADSET | SND_JACK_BTN_0);
-               } else {
-                       dev_dbg(codec->dev, "Headphone detected\n");
-                       snd_soc_jack_report(wm8996->jack,
-                                           SND_JACK_HEADPHONE,
-                                           SND_JACK_HEADSET |
+                       snd_soc_jack_report(wm8996->jack, SND_JACK_BTN_0,
                                            SND_JACK_BTN_0);
+               } else if (wm8996->detecting) {
+                       dev_dbg(codec->dev, "Headphone detected\n");
+                       wm8996_hpdet_start(codec);
 
                        /* Increase the detection rate a bit for
                         * responsiveness.
                        snd_soc_update_bits(codec, WM8996_MIC_DETECT_1,
                                            WM8996_MICD_RATE_MASK,
                                            7 << WM8996_MICD_RATE_SHIFT);
-
-                       wm8996->detecting = false;
                }
        }
 }
        if (irq_val & WM8996_MICD_EINT)
                wm8996_micd(codec);
 
+       if (irq_val & WM8996_HP_DONE_EINT)
+               wm8996_hpdet_irq(codec);
+
        return IRQ_HANDLED;
 }