extcon: arizona: Enable basic headphone identification
authorMark Brown <broonie@opensource.wolfsonmicro.com>
Thu, 10 Jan 2013 23:55:43 +0000 (08:55 +0900)
committerChanwoo Choi <cw00.choi@samsung.com>
Tue, 15 Jan 2013 06:42:17 +0000 (15:42 +0900)
Use the headphone detection to identify if the accessory is a headphone or
line load. There are two different revisions of the IP with different
register layouts, support both.

Signed-off-by: Mark Brown <broonie@opensource.wolfsonmicro.com>
Signed-off-by: Chanwoo Choi <cw00.choi@samsung.com>
Signed-off-by: Myungjoo Ham <myungjoo.ham@samsung.com>
drivers/extcon/extcon-arizona.c
drivers/mfd/wm5102-tables.c
include/linux/mfd/arizona/registers.h

index b5190a8116017fb7131a4bc33a35f83c923cb3b8..cc258a8842efb27cb95b4461f8037d90b7277ae3 100644 (file)
 #include <linux/mfd/arizona/pdata.h>
 #include <linux/mfd/arizona/registers.h>
 
+#define ARIZONA_DEFAULT_HP 32
+
 #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;
@@ -47,10 +53,14 @@ struct arizona_extcon_info {
        bool micd_reva;
        bool micd_clamp;
 
+       bool hpdet_active;
+
        bool mic;
        bool detecting;
        int jack_flips;
 
+       int hpdet_ip;
+
        struct extcon_dev edev;
 };
 
@@ -74,11 +84,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,
 };
 
@@ -100,6 +112,290 @@ 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 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);
+                       val = ARIZONA_DEFAULT_HP;
+               }
+
+               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 ARIZONA_DEFAULT_HP;
+               }
+
+               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 ARIZONA_DEFAULT_HP;
+               }
+
+               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 ARIZONA_DEFAULT_HP;
+               }
+
+               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 irqreturn_t arizona_hpdet_irq(int irq, void *data)
+{
+       struct arizona_extcon_info *info = data;
+       struct arizona *arizona = info->arizona;
+       int report = ARIZONA_CABLE_HEADPHONE;
+       int ret;
+
+       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;
+       }
+
+       /* Reset back to starting range */
+       regmap_update_bits(arizona->regmap,
+                          ARIZONA_HEADPHONE_DETECT_1,
+                          ARIZONA_HP_IMPEDANCE_RANGE_MASK, 0);
+
+       /* Report high impedence cables as line outputs */
+       if (ret >= 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);
+
+       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);
+
+done:
+       regmap_update_bits(arizona->regmap, ARIZONA_HEADPHONE_DETECT_1,
+                          ARIZONA_HP_POLL, 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;
+       }
+
+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;
+
+       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;
+}
+       }
+
+       info->hpdet_active = false;
+}
+
 static void arizona_start_mic(struct arizona_extcon_info *info)
 {
        struct arizona *arizona = info->arizona;
@@ -125,6 +421,10 @@ 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);
+
        regmap_update_bits_check(arizona->regmap, ARIZONA_MIC_DETECT_1,
                                 ARIZONA_MICD_ENA, ARIZONA_MICD_ENA,
                                 &change);
@@ -189,11 +489,11 @@ 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",
@@ -214,17 +514,12 @@ static irqreturn_t arizona_micdet(int irq, void *data)
                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)
@@ -260,13 +555,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);
@@ -387,6 +676,7 @@ static int arizona_extcon_probe(struct platform_device *pdev)
                        break;
                default:
                        info->micd_clamp = true;
+                       info->hpdet_ip = 1;
                        break;
                }
                break;
@@ -524,6 +814,13 @@ static int arizona_extcon_probe(struct platform_device *pdev)
                goto err_fall_wake;
        }
 
+       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;
+       }
+
        regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1,
                           ARIZONA_MICD_RATE_MASK,
                           8 << ARIZONA_MICD_RATE_SHIFT);
@@ -544,11 +841,13 @@ 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:
index 088872ab63389e49d04dd3f38fb02664fdc929ba..311cc3657b0b28ded9ac198c096c840e0c1fd885 100644 (file)
@@ -1106,6 +1106,8 @@ static bool wm5102_readable_register(struct device *dev, unsigned int reg)
        case ARIZONA_ACCESSORY_DETECT_MODE_1:
        case ARIZONA_HEADPHONE_DETECT_1:
        case ARIZONA_HEADPHONE_DETECT_2:
+       case ARIZONA_HP_DACVAL:
+       case ARIZONA_MICD_CLAMP_CONTROL:
        case ARIZONA_MIC_DETECT_1:
        case ARIZONA_MIC_DETECT_2:
        case ARIZONA_MIC_DETECT_3:
@@ -1875,6 +1877,7 @@ static bool wm5102_volatile_register(struct device *dev, unsigned int reg)
        case ARIZONA_DSP1_STATUS_2:
        case ARIZONA_DSP1_STATUS_3:
        case ARIZONA_HEADPHONE_DETECT_2:
+       case ARIZONA_HP_DACVAL:
        case ARIZONA_MIC_DETECT_3:
                return true;
        default:
index f3211f06cec6aca80d5a4e24b42f18d30a9eaf87..e9150533e70108f10cc75db3217765708955d7aa 100644 (file)
 #define ARIZONA_ACCESSORY_DETECT_MODE_1          0x293
 #define ARIZONA_HEADPHONE_DETECT_1               0x29B
 #define ARIZONA_HEADPHONE_DETECT_2               0x29C
+#define ARIZONA_HP_DACVAL                       0x29F
 #define ARIZONA_MICD_CLAMP_CONTROL               0x2A2
 #define ARIZONA_MIC_DETECT_1                     0x2A3
 #define ARIZONA_MIC_DETECT_2                     0x2A4
 /*
  * R667 (0x29B) - Headphone Detect 1
  */
+#define ARIZONA_HP_IMPEDANCE_RANGE_MASK          0x0600  /* HP_IMPEDANCE_RANGE - [10:9] */
+#define ARIZONA_HP_IMPEDANCE_RANGE_SHIFT              9  /* HP_IMPEDANCE_RANGE - [10:9] */
+#define ARIZONA_HP_IMPEDANCE_RANGE_WIDTH              2  /* HP_IMPEDANCE_RANGE - [10:9] */
 #define ARIZONA_HP_STEP_SIZE                     0x0100  /* HP_STEP_SIZE */
 #define ARIZONA_HP_STEP_SIZE_MASK                0x0100  /* HP_STEP_SIZE */
 #define ARIZONA_HP_STEP_SIZE_SHIFT                    8  /* HP_STEP_SIZE */
 #define ARIZONA_HP_LVL_SHIFT                          0  /* HP_LVL - [6:0] */
 #define ARIZONA_HP_LVL_WIDTH                          7  /* HP_LVL - [6:0] */
 
+#define ARIZONA_HP_DONE_B                        0x8000  /* HP_DONE */
+#define ARIZONA_HP_DONE_B_MASK                   0x8000  /* HP_DONE */
+#define ARIZONA_HP_DONE_B_SHIFT                      15  /* HP_DONE */
+#define ARIZONA_HP_DONE_B_WIDTH                       1  /* HP_DONE */
+#define ARIZONA_HP_LVL_B_MASK                    0x7FFF  /* HP_LVL - [14:0] */
+#define ARIZONA_HP_LVL_B_SHIFT                        0  /* HP_LVL - [14:0] */
+#define ARIZONA_HP_LVL_B_WIDTH                       15  /* HP_LVL - [14:0] */
+
 /*
  * R674 (0x2A2) - MICD clamp control
  */