]> Pileus Git - ~andy/linux/blobdiff - drivers/extcon/extcon-arizona.c
Merge tag 'iio-fixes-for-3.9a' of git://git.kernel.org/pub/scm/linux/kernel/git/jic23...
[~andy/linux] / drivers / extcon / extcon-arizona.c
index 414aed50b1bc0aa52c32a49019eef746f7d63617..dc357a4051f6d79aa5497ee0883c903d0baf97ac 100644 (file)
 #include <linux/regulator/consumer.h>
 #include <linux/extcon.h>
 
+#include <sound/soc.h>
+
 #include <linux/mfd/arizona/core.h>
 #include <linux/mfd/arizona/pdata.h>
 #include <linux/mfd/arizona/registers.h>
 
 #define ARIZONA_NUM_BUTTONS 6
 
+#define ARIZONA_ACCDET_MODE_MIC 0
+#define ARIZONA_ACCDET_MODE_HPL 1
+#define ARIZONA_ACCDET_MODE_HPR 2
+
 struct arizona_extcon_info {
        struct device *dev;
        struct arizona *arizona;
@@ -45,17 +51,28 @@ struct arizona_extcon_info {
        int micd_num_modes;
 
        bool micd_reva;
+       bool micd_clamp;
+
+       struct delayed_work hpdet_work;
+
+       bool hpdet_active;
+       bool hpdet_done;
+
+       int num_hpdet_res;
+       unsigned int hpdet_res[3];
 
        bool mic;
        bool detecting;
        int jack_flips;
 
+       int hpdet_ip;
+
        struct extcon_dev edev;
 };
 
 static const struct arizona_micd_config micd_default_modes[] = {
-       { ARIZONA_ACCDET_SRC, 1 << ARIZONA_MICD_BIAS_SRC_SHIFT, 0 },
        { 0,                  2 << ARIZONA_MICD_BIAS_SRC_SHIFT, 1 },
+       { ARIZONA_ACCDET_SRC, 1 << ARIZONA_MICD_BIAS_SRC_SHIFT, 0 },
 };
 
 static struct {
@@ -73,11 +90,13 @@ static struct {
 #define ARIZONA_CABLE_MECHANICAL 0
 #define ARIZONA_CABLE_MICROPHONE 1
 #define ARIZONA_CABLE_HEADPHONE  2
+#define ARIZONA_CABLE_LINEOUT    3
 
 static const char *arizona_cable[] = {
        "Mechanical",
        "Microphone",
        "Headphone",
+       "Line-out",
        NULL,
 };
 
@@ -85,8 +104,9 @@ static void arizona_extcon_set_mode(struct arizona_extcon_info *info, int mode)
 {
        struct arizona *arizona = info->arizona;
 
-       gpio_set_value_cansleep(arizona->pdata.micd_pol_gpio,
-                               info->micd_modes[mode].gpio);
+       if (arizona->pdata.micd_pol_gpio > 0)
+               gpio_set_value_cansleep(arizona->pdata.micd_pol_gpio,
+                                       info->micd_modes[mode].gpio);
        regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1,
                           ARIZONA_MICD_BIAS_SRC_MASK,
                           info->micd_modes[mode].bias);
@@ -98,19 +118,70 @@ static void arizona_extcon_set_mode(struct arizona_extcon_info *info, int mode)
        dev_dbg(arizona->dev, "Set jack polarity to %d\n", mode);
 }
 
+static const char *arizona_extcon_get_micbias(struct arizona_extcon_info *info)
+{
+       switch (info->micd_modes[0].bias >> ARIZONA_MICD_BIAS_SRC_SHIFT) {
+       case 1:
+               return "MICBIAS1";
+       case 2:
+               return "MICBIAS2";
+       case 3:
+               return "MICBIAS3";
+       default:
+               return "MICVDD";
+       }
+}
+
+static void arizona_extcon_pulse_micbias(struct arizona_extcon_info *info)
+{
+       struct arizona *arizona = info->arizona;
+       const char *widget = arizona_extcon_get_micbias(info);
+       struct snd_soc_dapm_context *dapm = arizona->dapm;
+       int ret;
+
+       mutex_lock(&dapm->card->dapm_mutex);
+
+       ret = snd_soc_dapm_force_enable_pin(dapm, widget);
+       if (ret != 0)
+               dev_warn(arizona->dev, "Failed to enable %s: %d\n",
+                        widget, ret);
+
+       mutex_unlock(&dapm->card->dapm_mutex);
+
+       snd_soc_dapm_sync(dapm);
+
+       if (!arizona->pdata.micd_force_micbias) {
+               mutex_lock(&dapm->card->dapm_mutex);
+
+               ret = snd_soc_dapm_disable_pin(arizona->dapm, widget);
+               if (ret != 0)
+                       dev_warn(arizona->dev, "Failed to disable %s: %d\n",
+                                widget, ret);
+
+               mutex_unlock(&dapm->card->dapm_mutex);
+
+               snd_soc_dapm_sync(dapm);
+       }
+}
+
 static void arizona_start_mic(struct arizona_extcon_info *info)
 {
        struct arizona *arizona = info->arizona;
        bool change;
        int ret;
 
-       info->detecting = true;
-       info->mic = false;
-       info->jack_flips = 0;
-
        /* Microphone detection can't use idle mode */
        pm_runtime_get(info->dev);
 
+       if (info->detecting) {
+               ret = regulator_allow_bypass(info->micvdd, false);
+               if (ret != 0) {
+                       dev_err(arizona->dev,
+                               "Failed to regulate MICVDD: %d\n",
+                               ret);
+               }
+       }
+
        ret = regulator_enable(info->micvdd);
        if (ret != 0) {
                dev_err(arizona->dev, "Failed to enable MICVDD: %d\n",
@@ -123,6 +194,12 @@ static void arizona_start_mic(struct arizona_extcon_info *info)
                regmap_write(arizona->regmap, 0x80, 0x0);
        }
 
+       regmap_update_bits(arizona->regmap,
+                          ARIZONA_ACCESSORY_DETECT_MODE_1,
+                          ARIZONA_ACCDET_MODE_MASK, ARIZONA_ACCDET_MODE_MIC);
+
+       arizona_extcon_pulse_micbias(info);
+
        regmap_update_bits_check(arizona->regmap, ARIZONA_MIC_DETECT_1,
                                 ARIZONA_MICD_ENA, ARIZONA_MICD_ENA,
                                 &change);
@@ -135,18 +212,39 @@ static void arizona_start_mic(struct arizona_extcon_info *info)
 static void arizona_stop_mic(struct arizona_extcon_info *info)
 {
        struct arizona *arizona = info->arizona;
+       const char *widget = arizona_extcon_get_micbias(info);
+       struct snd_soc_dapm_context *dapm = arizona->dapm;
        bool change;
+       int ret;
 
        regmap_update_bits_check(arizona->regmap, ARIZONA_MIC_DETECT_1,
                                 ARIZONA_MICD_ENA, 0,
                                 &change);
 
+       mutex_lock(&dapm->card->dapm_mutex);
+
+       ret = snd_soc_dapm_disable_pin(dapm, widget);
+       if (ret != 0)
+               dev_warn(arizona->dev,
+                        "Failed to disable %s: %d\n",
+                        widget, ret);
+
+       mutex_unlock(&dapm->card->dapm_mutex);
+
+       snd_soc_dapm_sync(dapm);
+
        if (info->micd_reva) {
                regmap_write(arizona->regmap, 0x80, 0x3);
                regmap_write(arizona->regmap, 0x294, 2);
                regmap_write(arizona->regmap, 0x80, 0x0);
        }
 
+       ret = regulator_allow_bypass(info->micvdd, true);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Failed to bypass MICVDD: %d\n",
+                       ret);
+       }
+
        if (change) {
                regulator_disable(info->micvdd);
                pm_runtime_mark_last_busy(info->dev);
@@ -154,6 +252,478 @@ static void arizona_stop_mic(struct arizona_extcon_info *info)
        }
 }
 
+static struct {
+       unsigned int factor_a;
+       unsigned int factor_b;
+} arizona_hpdet_b_ranges[] = {
+       {  5528,   362464 },
+       { 11084,  6186851 },
+       { 11065, 65460395 },
+};
+
+static struct {
+       int min;
+       int max;
+} arizona_hpdet_c_ranges[] = {
+       { 0,       30 },
+       { 8,      100 },
+       { 100,   1000 },
+       { 1000, 10000 },
+};
+
+static int arizona_hpdet_read(struct arizona_extcon_info *info)
+{
+       struct arizona *arizona = info->arizona;
+       unsigned int val, range;
+       int ret;
+
+       ret = regmap_read(arizona->regmap, ARIZONA_HEADPHONE_DETECT_2, &val);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Failed to read HPDET status: %d\n",
+                       ret);
+               return ret;
+       }
+
+       switch (info->hpdet_ip) {
+       case 0:
+               if (!(val & ARIZONA_HP_DONE)) {
+                       dev_err(arizona->dev, "HPDET did not complete: %x\n",
+                               val);
+                       return -EAGAIN;
+               }
+
+               val &= ARIZONA_HP_LVL_MASK;
+               break;
+
+       case 1:
+               if (!(val & ARIZONA_HP_DONE_B)) {
+                       dev_err(arizona->dev, "HPDET did not complete: %x\n",
+                               val);
+                       return -EAGAIN;
+               }
+
+               ret = regmap_read(arizona->regmap, ARIZONA_HP_DACVAL, &val);
+               if (ret != 0) {
+                       dev_err(arizona->dev, "Failed to read HP value: %d\n",
+                               ret);
+                       return -EAGAIN;
+               }
+
+               regmap_read(arizona->regmap, ARIZONA_HEADPHONE_DETECT_1,
+                           &range);
+               range = (range & ARIZONA_HP_IMPEDANCE_RANGE_MASK)
+                          >> ARIZONA_HP_IMPEDANCE_RANGE_SHIFT;
+
+               if (range < ARRAY_SIZE(arizona_hpdet_b_ranges) - 1 &&
+                   (val < 100 || val > 0x3fb)) {
+                       range++;
+                       dev_dbg(arizona->dev, "Moving to HPDET range %d\n",
+                               range);
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_HEADPHONE_DETECT_1,
+                                          ARIZONA_HP_IMPEDANCE_RANGE_MASK,
+                                          range <<
+                                          ARIZONA_HP_IMPEDANCE_RANGE_SHIFT);
+                       return -EAGAIN;
+               }
+
+               /* If we go out of range report top of range */
+               if (val < 100 || val > 0x3fb) {
+                       dev_dbg(arizona->dev, "Measurement out of range\n");
+                       return 10000;
+               }
+
+               dev_dbg(arizona->dev, "HPDET read %d in range %d\n",
+                       val, range);
+
+               val = arizona_hpdet_b_ranges[range].factor_b
+                       / ((val * 100) -
+                          arizona_hpdet_b_ranges[range].factor_a);
+               break;
+
+       default:
+               dev_warn(arizona->dev, "Unknown HPDET IP revision %d\n",
+                        info->hpdet_ip);
+       case 2:
+               if (!(val & ARIZONA_HP_DONE_B)) {
+                       dev_err(arizona->dev, "HPDET did not complete: %x\n",
+                               val);
+                       return -EAGAIN;
+               }
+
+               val &= ARIZONA_HP_LVL_B_MASK;
+
+               regmap_read(arizona->regmap, ARIZONA_HEADPHONE_DETECT_1,
+                           &range);
+               range = (range & ARIZONA_HP_IMPEDANCE_RANGE_MASK)
+                          >> ARIZONA_HP_IMPEDANCE_RANGE_SHIFT;
+
+               /* Skip up or down a range? */
+               if (range && (val < arizona_hpdet_c_ranges[range].min)) {
+                       range--;
+                       dev_dbg(arizona->dev, "Moving to HPDET range %d-%d\n",
+                               arizona_hpdet_c_ranges[range].min,
+                               arizona_hpdet_c_ranges[range].max);
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_HEADPHONE_DETECT_1,
+                                          ARIZONA_HP_IMPEDANCE_RANGE_MASK,
+                                          range <<
+                                          ARIZONA_HP_IMPEDANCE_RANGE_SHIFT);
+                       return -EAGAIN;
+               }
+
+               if (range < ARRAY_SIZE(arizona_hpdet_c_ranges) - 1 &&
+                   (val >= arizona_hpdet_c_ranges[range].max)) {
+                       range++;
+                       dev_dbg(arizona->dev, "Moving to HPDET range %d-%d\n",
+                               arizona_hpdet_c_ranges[range].min,
+                               arizona_hpdet_c_ranges[range].max);
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_HEADPHONE_DETECT_1,
+                                          ARIZONA_HP_IMPEDANCE_RANGE_MASK,
+                                          range <<
+                                          ARIZONA_HP_IMPEDANCE_RANGE_SHIFT);
+                       return -EAGAIN;
+               }
+       }
+
+       dev_dbg(arizona->dev, "HP impedance %d ohms\n", val);
+       return val;
+}
+
+static int arizona_hpdet_do_id(struct arizona_extcon_info *info, int *reading)
+{
+       struct arizona *arizona = info->arizona;
+       int id_gpio = arizona->pdata.hpdet_id_gpio;
+
+       /*
+        * If we're using HPDET for accessory identification we need
+        * to take multiple measurements, step through them in sequence.
+        */
+       if (arizona->pdata.hpdet_acc_id) {
+               info->hpdet_res[info->num_hpdet_res++] = *reading;
+
+               /*
+                * If the impedence is too high don't measure the
+                * second ground.
+                */
+               if (info->num_hpdet_res == 1 && *reading >= 45) {
+                       dev_dbg(arizona->dev, "Skipping ground flip\n");
+                       info->hpdet_res[info->num_hpdet_res++] = *reading;
+               }
+
+               if (info->num_hpdet_res == 1) {
+                       dev_dbg(arizona->dev, "Flipping ground\n");
+
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_ACCESSORY_DETECT_MODE_1,
+                                          ARIZONA_ACCDET_SRC,
+                                          ~info->micd_modes[0].src);
+
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_HEADPHONE_DETECT_1,
+                                          ARIZONA_HP_POLL, ARIZONA_HP_POLL);
+                       return -EAGAIN;
+               }
+
+               /* Only check the mic directly if we didn't already ID it */
+               if (id_gpio && info->num_hpdet_res == 2 &&
+                   !((info->hpdet_res[0] > info->hpdet_res[1] * 2))) {
+                       dev_dbg(arizona->dev, "Measuring mic\n");
+
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_ACCESSORY_DETECT_MODE_1,
+                                          ARIZONA_ACCDET_MODE_MASK |
+                                          ARIZONA_ACCDET_SRC,
+                                          ARIZONA_ACCDET_MODE_HPR |
+                                          info->micd_modes[0].src);
+
+                       gpio_set_value_cansleep(id_gpio, 1);
+
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_HEADPHONE_DETECT_1,
+                                          ARIZONA_HP_POLL, ARIZONA_HP_POLL);
+                       return -EAGAIN;
+               }
+
+               /* OK, got both.  Now, compare... */
+               dev_dbg(arizona->dev, "HPDET measured %d %d %d\n",
+                       info->hpdet_res[0], info->hpdet_res[1],
+                       info->hpdet_res[2]);
+
+
+               /* Take the headphone impedance for the main report */
+               *reading = info->hpdet_res[0];
+
+               /*
+                * Either the two grounds measure differently or we
+                * measure the mic as high impedance.
+                */
+               if ((info->hpdet_res[0] > info->hpdet_res[1] * 2) ||
+                   (id_gpio && info->hpdet_res[2] > 10)) {
+                       dev_dbg(arizona->dev, "Detected mic\n");
+                       info->mic = true;
+                       info->detecting = true;
+               } else {
+                       dev_dbg(arizona->dev, "Detected headphone\n");
+               }
+
+               /* Make sure everything is reset back to the real polarity */
+               regmap_update_bits(arizona->regmap,
+                                  ARIZONA_ACCESSORY_DETECT_MODE_1,
+                                  ARIZONA_ACCDET_SRC,
+                                  info->micd_modes[0].src);
+       }
+
+       return 0;
+}
+
+static irqreturn_t arizona_hpdet_irq(int irq, void *data)
+{
+       struct arizona_extcon_info *info = data;
+       struct arizona *arizona = info->arizona;
+       int id_gpio = arizona->pdata.hpdet_id_gpio;
+       int report = ARIZONA_CABLE_HEADPHONE;
+       unsigned int val;
+       int ret, reading;
+
+       mutex_lock(&info->lock);
+
+       /* If we got a spurious IRQ for some reason then ignore it */
+       if (!info->hpdet_active) {
+               dev_warn(arizona->dev, "Spurious HPDET IRQ\n");
+               mutex_unlock(&info->lock);
+               return IRQ_NONE;
+       }
+
+       /* If the cable was removed while measuring ignore the result */
+       ret = extcon_get_cable_state_(&info->edev, ARIZONA_CABLE_MECHANICAL);
+       if (ret < 0) {
+               dev_err(arizona->dev, "Failed to check cable state: %d\n",
+                       ret);
+               goto out;
+       } else if (!ret) {
+               dev_dbg(arizona->dev, "Ignoring HPDET for removed cable\n");
+               goto done;
+       }
+
+       ret = arizona_hpdet_read(info);
+       if (ret == -EAGAIN) {
+               goto out;
+       } else if (ret < 0) {
+               goto done;
+       }
+       reading = ret;
+
+       /* Reset back to starting range */
+       regmap_update_bits(arizona->regmap,
+                          ARIZONA_HEADPHONE_DETECT_1,
+                          ARIZONA_HP_IMPEDANCE_RANGE_MASK | ARIZONA_HP_POLL,
+                          0);
+
+       ret = arizona_hpdet_do_id(info, &reading);
+       if (ret == -EAGAIN) {
+               goto out;
+       } else if (ret < 0) {
+               goto done;
+       }
+
+       /* Report high impedence cables as line outputs */
+       if (reading >= 5000)
+               report = ARIZONA_CABLE_LINEOUT;
+       else
+               report = ARIZONA_CABLE_HEADPHONE;
+
+       ret = extcon_set_cable_state_(&info->edev, report, true);
+       if (ret != 0)
+               dev_err(arizona->dev, "Failed to report HP/line: %d\n",
+                       ret);
+
+       mutex_lock(&arizona->dapm->card->dapm_mutex);
+
+       ret = regmap_read(arizona->regmap, ARIZONA_OUTPUT_ENABLES_1, &val);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Failed to read output enables: %d\n",
+                       ret);
+               val = 0;
+       }
+
+       if (!(val & (ARIZONA_OUT1L_ENA | ARIZONA_OUT1R_ENA))) {
+               ret = regmap_update_bits(arizona->regmap, 0x225, 0x4000, 0);
+               if (ret != 0)
+                       dev_warn(arizona->dev, "Failed to undo magic: %d\n",
+                                ret);
+
+               ret = regmap_update_bits(arizona->regmap, 0x226, 0x4000, 0);
+               if (ret != 0)
+                       dev_warn(arizona->dev, "Failed to undo magic: %d\n",
+                                ret);
+       }
+
+       mutex_unlock(&arizona->dapm->card->dapm_mutex);
+
+done:
+       if (id_gpio)
+               gpio_set_value_cansleep(id_gpio, 0);
+
+       /* Revert back to MICDET mode */
+       regmap_update_bits(arizona->regmap,
+                          ARIZONA_ACCESSORY_DETECT_MODE_1,
+                          ARIZONA_ACCDET_MODE_MASK, ARIZONA_ACCDET_MODE_MIC);
+
+       /* If we have a mic then reenable MICDET */
+       if (info->mic)
+               arizona_start_mic(info);
+
+       if (info->hpdet_active) {
+               pm_runtime_put_autosuspend(info->dev);
+               info->hpdet_active = false;
+       }
+
+       info->hpdet_done = true;
+
+out:
+       mutex_unlock(&info->lock);
+
+       return IRQ_HANDLED;
+}
+
+static void arizona_identify_headphone(struct arizona_extcon_info *info)
+{
+       struct arizona *arizona = info->arizona;
+       int ret;
+
+       if (info->hpdet_done)
+               return;
+
+       dev_dbg(arizona->dev, "Starting HPDET\n");
+
+       /* Make sure we keep the device enabled during the measurement */
+       pm_runtime_get(info->dev);
+
+       info->hpdet_active = true;
+
+       if (info->mic)
+               arizona_stop_mic(info);
+
+       ret = regmap_update_bits(arizona->regmap, 0x225, 0x4000, 0x4000);
+       if (ret != 0)
+               dev_warn(arizona->dev, "Failed to do magic: %d\n", ret);
+
+       ret = regmap_update_bits(arizona->regmap, 0x226, 0x4000, 0x4000);
+       if (ret != 0)
+               dev_warn(arizona->dev, "Failed to do magic: %d\n", ret);
+
+       ret = regmap_update_bits(arizona->regmap,
+                                ARIZONA_ACCESSORY_DETECT_MODE_1,
+                                ARIZONA_ACCDET_MODE_MASK,
+                                ARIZONA_ACCDET_MODE_HPL);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Failed to set HPDETL mode: %d\n", ret);
+               goto err;
+       }
+
+       ret = regmap_update_bits(arizona->regmap, ARIZONA_HEADPHONE_DETECT_1,
+                                ARIZONA_HP_POLL, ARIZONA_HP_POLL);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Can't start HPDETL measurement: %d\n",
+                       ret);
+               goto err;
+       }
+
+       return;
+
+err:
+       regmap_update_bits(arizona->regmap, ARIZONA_ACCESSORY_DETECT_MODE_1,
+                          ARIZONA_ACCDET_MODE_MASK, ARIZONA_ACCDET_MODE_MIC);
+
+       /* Just report headphone */
+       ret = extcon_update_state(&info->edev,
+                                 1 << ARIZONA_CABLE_HEADPHONE,
+                                 1 << ARIZONA_CABLE_HEADPHONE);
+       if (ret != 0)
+               dev_err(arizona->dev, "Failed to report headphone: %d\n", ret);
+
+       if (info->mic)
+               arizona_start_mic(info);
+
+       info->hpdet_active = false;
+}
+
+static void arizona_start_hpdet_acc_id(struct arizona_extcon_info *info)
+{
+       struct arizona *arizona = info->arizona;
+       unsigned int val;
+       int ret;
+
+       dev_dbg(arizona->dev, "Starting identification via HPDET\n");
+
+       /* Make sure we keep the device enabled during the measurement */
+       pm_runtime_get_sync(info->dev);
+
+       info->hpdet_active = true;
+
+       arizona_extcon_pulse_micbias(info);
+
+       mutex_lock(&arizona->dapm->card->dapm_mutex);
+
+       ret = regmap_read(arizona->regmap, ARIZONA_OUTPUT_ENABLES_1, &val);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Failed to read output enables: %d\n",
+                       ret);
+               val = 0;
+       }
+
+       if (!(val & (ARIZONA_OUT1L_ENA | ARIZONA_OUT1R_ENA))) {
+               ret = regmap_update_bits(arizona->regmap, 0x225, 0x4000,
+                                        0x4000);
+               if (ret != 0)
+                       dev_warn(arizona->dev, "Failed to do magic: %d\n",
+                                ret);
+
+               ret = regmap_update_bits(arizona->regmap, 0x226, 0x4000,
+                                        0x4000);
+               if (ret != 0)
+                       dev_warn(arizona->dev, "Failed to do magic: %d\n",
+                                ret);
+       }
+
+       mutex_unlock(&arizona->dapm->card->dapm_mutex);
+
+       ret = regmap_update_bits(arizona->regmap,
+                                ARIZONA_ACCESSORY_DETECT_MODE_1,
+                                ARIZONA_ACCDET_SRC | ARIZONA_ACCDET_MODE_MASK,
+                                info->micd_modes[0].src |
+                                ARIZONA_ACCDET_MODE_HPL);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Failed to set HPDETL mode: %d\n", ret);
+               goto err;
+       }
+
+       ret = regmap_update_bits(arizona->regmap, ARIZONA_HEADPHONE_DETECT_1,
+                                ARIZONA_HP_POLL, ARIZONA_HP_POLL);
+       if (ret != 0) {
+               dev_err(arizona->dev, "Can't start HPDETL measurement: %d\n",
+                       ret);
+               goto err;
+       }
+
+       return;
+
+err:
+       regmap_update_bits(arizona->regmap, ARIZONA_ACCESSORY_DETECT_MODE_1,
+                          ARIZONA_ACCDET_MODE_MASK, ARIZONA_ACCDET_MODE_MIC);
+
+       /* Just report headphone */
+       ret = extcon_update_state(&info->edev,
+                                 1 << ARIZONA_CABLE_HEADPHONE,
+                                 1 << ARIZONA_CABLE_HEADPHONE);
+       if (ret != 0)
+               dev_err(arizona->dev, "Failed to report headphone: %d\n", ret);
+
+       info->hpdet_active = false;
+}
+
 static irqreturn_t arizona_micdet(int irq, void *data)
 {
        struct arizona_extcon_info *info = data;
@@ -187,16 +757,23 @@ static irqreturn_t arizona_micdet(int irq, void *data)
 
        /* If we got a high impedence we should have a headset, report it. */
        if (info->detecting && (val & 0x400)) {
+               arizona_identify_headphone(info);
+
                ret = extcon_update_state(&info->edev,
-                                         1 << ARIZONA_CABLE_MICROPHONE |
-                                         1 << ARIZONA_CABLE_HEADPHONE,
-                                         1 << ARIZONA_CABLE_MICROPHONE |
-                                         1 << ARIZONA_CABLE_HEADPHONE);
+                                         1 << ARIZONA_CABLE_MICROPHONE,
+                                         1 << ARIZONA_CABLE_MICROPHONE);
 
                if (ret != 0)
                        dev_err(arizona->dev, "Headset report failed: %d\n",
                                ret);
 
+               /* Don't need to regulate for button detection */
+               ret = regulator_allow_bypass(info->micvdd, false);
+               if (ret != 0) {
+                       dev_err(arizona->dev, "Failed to bypass MICVDD: %d\n",
+                               ret);
+               }
+
                info->mic = true;
                info->detecting = false;
                goto handled;
@@ -209,20 +786,13 @@ static irqreturn_t arizona_micdet(int irq, void *data)
         * impedence then give up and report headphones.
         */
        if (info->detecting && (val & 0x3f8)) {
-               info->jack_flips++;
-
                if (info->jack_flips >= info->micd_num_modes) {
-                       dev_dbg(arizona->dev, "Detected headphone\n");
+                       dev_dbg(arizona->dev, "Detected HP/line\n");
+                       arizona_identify_headphone(info);
+
                        info->detecting = false;
-                       arizona_stop_mic(info);
 
-                       ret = extcon_set_cable_state_(&info->edev,
-                                                     ARIZONA_CABLE_HEADPHONE,
-                                                     true);
-                       if (ret != 0)
-                               dev_err(arizona->dev,
-                                       "Headphone report failed: %d\n",
-                               ret);
+                       arizona_stop_mic(info);
                } else {
                        info->micd_mode++;
                        if (info->micd_mode == info->micd_num_modes)
@@ -258,13 +828,7 @@ static irqreturn_t arizona_micdet(int irq, void *data)
                        info->detecting = false;
                        arizona_stop_mic(info);
 
-                       ret = extcon_set_cable_state_(&info->edev,
-                                                     ARIZONA_CABLE_HEADPHONE,
-                                                     true);
-                       if (ret != 0)
-                               dev_err(arizona->dev,
-                                       "Headphone report failed: %d\n",
-                               ret);
+                       arizona_identify_headphone(info);
                } else {
                        dev_warn(arizona->dev, "Button with no mic: %x\n",
                                 val);
@@ -275,6 +839,7 @@ static irqreturn_t arizona_micdet(int irq, void *data)
                        input_report_key(info->input,
                                         arizona_lvl_to_key[i].report, 0);
                input_sync(info->input);
+               arizona_extcon_pulse_micbias(info);
        }
 
 handled:
@@ -284,17 +849,38 @@ handled:
        return IRQ_HANDLED;
 }
 
+static void arizona_hpdet_work(struct work_struct *work)
+{
+       struct arizona_extcon_info *info = container_of(work,
+                                                       struct arizona_extcon_info,
+                                                       hpdet_work.work);
+
+       mutex_lock(&info->lock);
+       arizona_start_hpdet_acc_id(info);
+       mutex_unlock(&info->lock);
+}
+
 static irqreturn_t arizona_jackdet(int irq, void *data)
 {
        struct arizona_extcon_info *info = data;
        struct arizona *arizona = info->arizona;
-       unsigned int val;
+       unsigned int val, present, mask;
        int ret, i;
 
        pm_runtime_get_sync(info->dev);
 
+       cancel_delayed_work_sync(&info->hpdet_work);
+
        mutex_lock(&info->lock);
 
+       if (arizona->pdata.jd_gpio5) {
+               mask = ARIZONA_MICD_CLAMP_STS;
+               present = 0;
+       } else {
+               mask = ARIZONA_JD1_STS;
+               present = ARIZONA_JD1_STS;
+       }
+
        ret = regmap_read(arizona->regmap, ARIZONA_AOD_IRQ_RAW_STATUS, &val);
        if (ret != 0) {
                dev_err(arizona->dev, "Failed to read jackdet status: %d\n",
@@ -304,7 +890,7 @@ static irqreturn_t arizona_jackdet(int irq, void *data)
                return IRQ_NONE;
        }
 
-       if (val & ARIZONA_JD1_STS) {
+       if ((val & mask) == present) {
                dev_dbg(arizona->dev, "Detected jack\n");
                ret = extcon_set_cable_state_(&info->edev,
                                              ARIZONA_CABLE_MECHANICAL, true);
@@ -313,12 +899,31 @@ static irqreturn_t arizona_jackdet(int irq, void *data)
                        dev_err(arizona->dev, "Mechanical report failed: %d\n",
                                ret);
 
-               arizona_start_mic(info);
+               if (!arizona->pdata.hpdet_acc_id) {
+                       info->detecting = true;
+                       info->mic = false;
+                       info->jack_flips = 0;
+
+                       arizona_start_mic(info);
+               } else {
+                       schedule_delayed_work(&info->hpdet_work,
+                                             msecs_to_jiffies(250));
+               }
+
+               regmap_update_bits(arizona->regmap,
+                                  ARIZONA_JACK_DETECT_DEBOUNCE,
+                                  ARIZONA_MICD_CLAMP_DB | ARIZONA_JD1_DB, 0);
        } else {
                dev_dbg(arizona->dev, "Detected jack removal\n");
 
                arizona_stop_mic(info);
 
+               info->num_hpdet_res = 0;
+               for (i = 0; i < ARRAY_SIZE(info->hpdet_res); i++)
+                       info->hpdet_res[i] = 0;
+               info->mic = false;
+               info->hpdet_done = false;
+
                for (i = 0; i < ARIZONA_NUM_BUTTONS; i++)
                        input_report_key(info->input,
                                         arizona_lvl_to_key[i].report, 0);
@@ -328,8 +933,20 @@ static irqreturn_t arizona_jackdet(int irq, void *data)
                if (ret != 0)
                        dev_err(arizona->dev, "Removal report failed: %d\n",
                                ret);
+
+               regmap_update_bits(arizona->regmap,
+                                  ARIZONA_JACK_DETECT_DEBOUNCE,
+                                  ARIZONA_MICD_CLAMP_DB | ARIZONA_JD1_DB,
+                                  ARIZONA_MICD_CLAMP_DB | ARIZONA_JD1_DB);
        }
 
+       /* Clear trig_sts to make sure DCVDD is not forced up */
+       regmap_write(arizona->regmap, ARIZONA_AOD_WKUP_AND_TRIG,
+                    ARIZONA_MICD_CLAMP_FALL_TRIG_STS |
+                    ARIZONA_MICD_CLAMP_RISE_TRIG_STS |
+                    ARIZONA_JD1_FALL_TRIG_STS |
+                    ARIZONA_JD1_RISE_TRIG_STS);
+
        mutex_unlock(&info->lock);
 
        pm_runtime_mark_last_busy(info->dev);
@@ -343,8 +960,12 @@ static int arizona_extcon_probe(struct platform_device *pdev)
        struct arizona *arizona = dev_get_drvdata(pdev->dev.parent);
        struct arizona_pdata *pdata;
        struct arizona_extcon_info *info;
+       int jack_irq_fall, jack_irq_rise;
        int ret, mode, i;
 
+       if (!arizona->dapm || !arizona->dapm->card)
+               return -EPROBE_DEFER;
+
        pdata = dev_get_platdata(arizona->dev);
 
        info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
@@ -364,7 +985,7 @@ static int arizona_extcon_probe(struct platform_device *pdev)
        mutex_init(&info->lock);
        info->arizona = arizona;
        info->dev = &pdev->dev;
-       info->detecting = true;
+       INIT_DELAYED_WORK(&info->hpdet_work, arizona_hpdet_work);
        platform_set_drvdata(pdev, info);
 
        switch (arizona->type) {
@@ -374,6 +995,8 @@ static int arizona_extcon_probe(struct platform_device *pdev)
                        info->micd_reva = true;
                        break;
                default:
+                       info->micd_clamp = true;
+                       info->hpdet_ip = 1;
                        break;
                }
                break;
@@ -416,9 +1039,64 @@ static int arizona_extcon_probe(struct platform_device *pdev)
                }
        }
 
+       if (arizona->pdata.hpdet_id_gpio > 0) {
+               ret = devm_gpio_request_one(&pdev->dev,
+                                           arizona->pdata.hpdet_id_gpio,
+                                           GPIOF_OUT_INIT_LOW,
+                                           "HPDET");
+               if (ret != 0) {
+                       dev_err(arizona->dev, "Failed to request GPIO%d: %d\n",
+                               arizona->pdata.hpdet_id_gpio, ret);
+                       goto err_register;
+               }
+       }
+
+       if (arizona->pdata.micd_bias_start_time)
+               regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1,
+                                  ARIZONA_MICD_BIAS_STARTTIME_MASK,
+                                  arizona->pdata.micd_bias_start_time
+                                  << ARIZONA_MICD_BIAS_STARTTIME_SHIFT);
+
+       if (arizona->pdata.micd_rate)
+               regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1,
+                                  ARIZONA_MICD_RATE_MASK,
+                                  arizona->pdata.micd_rate
+                                  << ARIZONA_MICD_RATE_SHIFT);
+
+       if (arizona->pdata.micd_dbtime)
+               regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1,
+                                  ARIZONA_MICD_DBTIME_MASK,
+                                  arizona->pdata.micd_dbtime
+                                  << ARIZONA_MICD_DBTIME_SHIFT);
+
+       /*
+        * If we have a clamp use it, activating in conjunction with
+        * GPIO5 if that is connected for jack detect operation.
+        */
+       if (info->micd_clamp) {
+               if (arizona->pdata.jd_gpio5) {
+                       /* Put the GPIO into input mode */
+                       regmap_write(arizona->regmap, ARIZONA_GPIO5_CTRL,
+                                    0xc101);
+
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_MICD_CLAMP_CONTROL,
+                                          ARIZONA_MICD_CLAMP_MODE_MASK, 0x9);
+               } else {
+                       regmap_update_bits(arizona->regmap,
+                                          ARIZONA_MICD_CLAMP_CONTROL,
+                                          ARIZONA_MICD_CLAMP_MODE_MASK, 0x4);
+               }
+
+               regmap_update_bits(arizona->regmap,
+                                  ARIZONA_JACK_DETECT_DEBOUNCE,
+                                  ARIZONA_MICD_CLAMP_DB,
+                                  ARIZONA_MICD_CLAMP_DB);
+       }
+
        arizona_extcon_set_mode(info, 0);
 
-       info->input = input_allocate_device();
+       info->input = devm_input_allocate_device(&pdev->dev);
        if (!info->input) {
                dev_err(arizona->dev, "Can't allocate input dev\n");
                ret = -ENOMEM;
@@ -436,7 +1114,15 @@ static int arizona_extcon_probe(struct platform_device *pdev)
        pm_runtime_idle(&pdev->dev);
        pm_runtime_get_sync(&pdev->dev);
 
-       ret = arizona_request_irq(arizona, ARIZONA_IRQ_JD_RISE,
+       if (arizona->pdata.jd_gpio5) {
+               jack_irq_rise = ARIZONA_IRQ_MICD_CLAMP_RISE;
+               jack_irq_fall = ARIZONA_IRQ_MICD_CLAMP_FALL;
+       } else {
+               jack_irq_rise = ARIZONA_IRQ_JD_RISE;
+               jack_irq_fall = ARIZONA_IRQ_JD_FALL;
+       }
+
+       ret = arizona_request_irq(arizona, jack_irq_rise,
                                  "JACKDET rise", arizona_jackdet, info);
        if (ret != 0) {
                dev_err(&pdev->dev, "Failed to get JACKDET rise IRQ: %d\n",
@@ -444,21 +1130,21 @@ static int arizona_extcon_probe(struct platform_device *pdev)
                goto err_input;
        }
 
-       ret = arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_RISE, 1);
+       ret = arizona_set_irq_wake(arizona, jack_irq_rise, 1);
        if (ret != 0) {
                dev_err(&pdev->dev, "Failed to set JD rise IRQ wake: %d\n",
                        ret);
                goto err_rise;
        }
 
-       ret = arizona_request_irq(arizona, ARIZONA_IRQ_JD_FALL,
+       ret = arizona_request_irq(arizona, jack_irq_fall,
                                  "JACKDET fall", arizona_jackdet, info);
        if (ret != 0) {
                dev_err(&pdev->dev, "Failed to get JD fall IRQ: %d\n", ret);
                goto err_rise_wake;
        }
 
-       ret = arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_FALL, 1);
+       ret = arizona_set_irq_wake(arizona, jack_irq_fall, 1);
        if (ret != 0) {
                dev_err(&pdev->dev, "Failed to set JD fall IRQ wake: %d\n",
                        ret);
@@ -472,11 +1158,12 @@ static int arizona_extcon_probe(struct platform_device *pdev)
                goto err_fall_wake;
        }
 
-       regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1,
-                          ARIZONA_MICD_BIAS_STARTTIME_MASK |
-                          ARIZONA_MICD_RATE_MASK,
-                          7 << ARIZONA_MICD_BIAS_STARTTIME_SHIFT |
-                          8 << ARIZONA_MICD_RATE_SHIFT);
+       ret = arizona_request_irq(arizona, ARIZONA_IRQ_HPDET,
+                                 "HPDET", arizona_hpdet_irq, info);
+       if (ret != 0) {
+               dev_err(&pdev->dev, "Failed to get HPDET IRQ: %d\n", ret);
+               goto err_micdet;
+       }
 
        arizona_clk32k_enable(arizona);
        regmap_update_bits(arizona->regmap, ARIZONA_JACK_DETECT_DEBOUNCE,
@@ -494,23 +1181,24 @@ static int arizona_extcon_probe(struct platform_device *pdev)
        ret = input_register_device(info->input);
        if (ret) {
                dev_err(&pdev->dev, "Can't register input device: %d\n", ret);
-               goto err_micdet;
+               goto err_hpdet;
        }
 
        return 0;
 
+err_hpdet:
+       arizona_free_irq(arizona, ARIZONA_IRQ_HPDET, info);
 err_micdet:
        arizona_free_irq(arizona, ARIZONA_IRQ_MICDET, info);
 err_fall_wake:
-       arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_FALL, 0);
+       arizona_set_irq_wake(arizona, jack_irq_fall, 0);
 err_fall:
-       arizona_free_irq(arizona, ARIZONA_IRQ_JD_FALL, info);
+       arizona_free_irq(arizona, jack_irq_fall, info);
 err_rise_wake:
-       arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_RISE, 0);
+       arizona_set_irq_wake(arizona, jack_irq_rise, 0);
 err_rise:
-       arizona_free_irq(arizona, ARIZONA_IRQ_JD_RISE, info);
+       arizona_free_irq(arizona, jack_irq_rise, info);
 err_input:
-       input_free_device(info->input);
 err_register:
        pm_runtime_disable(&pdev->dev);
        extcon_dev_unregister(&info->edev);
@@ -522,18 +1210,32 @@ static int arizona_extcon_remove(struct platform_device *pdev)
 {
        struct arizona_extcon_info *info = platform_get_drvdata(pdev);
        struct arizona *arizona = info->arizona;
+       int jack_irq_rise, jack_irq_fall;
 
        pm_runtime_disable(&pdev->dev);
 
-       arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_RISE, 0);
-       arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_FALL, 0);
+       regmap_update_bits(arizona->regmap,
+                          ARIZONA_MICD_CLAMP_CONTROL,
+                          ARIZONA_MICD_CLAMP_MODE_MASK, 0);
+
+       if (arizona->pdata.jd_gpio5) {
+               jack_irq_rise = ARIZONA_IRQ_MICD_CLAMP_RISE;
+               jack_irq_fall = ARIZONA_IRQ_MICD_CLAMP_FALL;
+       } else {
+               jack_irq_rise = ARIZONA_IRQ_JD_RISE;
+               jack_irq_fall = ARIZONA_IRQ_JD_FALL;
+       }
+
+       arizona_set_irq_wake(arizona, jack_irq_rise, 0);
+       arizona_set_irq_wake(arizona, jack_irq_fall, 0);
+       arizona_free_irq(arizona, ARIZONA_IRQ_HPDET, info);
        arizona_free_irq(arizona, ARIZONA_IRQ_MICDET, info);
-       arizona_free_irq(arizona, ARIZONA_IRQ_JD_RISE, info);
-       arizona_free_irq(arizona, ARIZONA_IRQ_JD_FALL, info);
+       arizona_free_irq(arizona, jack_irq_rise, info);
+       arizona_free_irq(arizona, jack_irq_fall, info);
+       cancel_delayed_work_sync(&info->hpdet_work);
        regmap_update_bits(arizona->regmap, ARIZONA_JACK_DETECT_ANALOGUE,
                           ARIZONA_JD1_ENA, 0);
        arizona_clk32k_disable(arizona);
-       input_unregister_device(info->input);
        extcon_dev_unregister(&info->edev);
 
        return 0;