diff options
author | Mark Brown <broonie@opensource.wolfsonmicro.com> | 2012-06-24 12:09:45 +0100 |
---|---|---|
committer | Greg Kroah-Hartman <gregkh@linuxfoundation.org> | 2012-06-26 16:01:02 -0700 |
commit | f2c32a882d2c1cde6fc552a5a3d34b4c1330edb8 (patch) | |
tree | 811b6a02363ccad00713a88cb4301d639ec406c6 | |
parent | df6b3cfe20f3e22a7997255f1df753005deb914f (diff) | |
download | linux-f2c32a882d2c1cde6fc552a5a3d34b4c1330edb8.tar.gz linux-f2c32a882d2c1cde6fc552a5a3d34b4c1330edb8.tar.bz2 linux-f2c32a882d2c1cde6fc552a5a3d34b4c1330edb8.zip |
Extcon: Arizona: Add driver for Wolfson Arizona class devices
Most Wolfson Arizona class audio hub CODECs include a flexible ultra low
power accessory detection subsystem. This driver exposes initial support
for this subsystem via the Extcon framework, implementing support for
ultra low power detection of headphone and headset with the ability to
detect the polarity of a headset.
The functionality of the devices is much richer and more flexible than
the current driver, future patches will extend the features of the
driver to more fully exploit this.
Signed-off-by: Mark Brown <broonie@opensource.wolfsonmicro.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
-rw-r--r-- | drivers/extcon/Kconfig | 8 | ||||
-rw-r--r-- | drivers/extcon/Makefile | 1 | ||||
-rw-r--r-- | drivers/extcon/extcon-arizona.c | 491 |
3 files changed, 500 insertions, 0 deletions
diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig index 29c5cf852efc..bb385ac629a8 100644 --- a/drivers/extcon/Kconfig +++ b/drivers/extcon/Kconfig @@ -29,4 +29,12 @@ config EXTCON_MAX8997 Maxim MAX8997 PMIC. The MAX8997 MUIC is a USB port accessory detector and switch. +config EXTCON_ARIZONA + tristate "Wolfson Arizona EXTCON support" + depends on MFD_ARIZONA + help + Say Y here to enable support for external accessory detection + with Wolfson Arizona devices. These are audio CODECs with + advanced audio accessory detection support. + endif # MULTISTATE_SWITCH diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile index 86020bdb6da0..e932caaa311c 100644 --- a/drivers/extcon/Makefile +++ b/drivers/extcon/Makefile @@ -5,3 +5,4 @@ obj-$(CONFIG_EXTCON) += extcon_class.o obj-$(CONFIG_EXTCON_GPIO) += extcon_gpio.o obj-$(CONFIG_EXTCON_MAX8997) += extcon-max8997.o +obj-$(CONFIG_EXTCON_ARIZONA) += extcon-arizona.o diff --git a/drivers/extcon/extcon-arizona.c b/drivers/extcon/extcon-arizona.c new file mode 100644 index 000000000000..b068bc9defe1 --- /dev/null +++ b/drivers/extcon/extcon-arizona.c @@ -0,0 +1,491 @@ +/* + * extcon-arizona.c - Extcon driver Wolfson Arizona devices + * + * Copyright (C) 2012 Wolfson Microelectronics plc + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/err.h> +#include <linux/gpio.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/regulator/consumer.h> +#include <linux/extcon.h> + +#include <linux/mfd/arizona/core.h> +#include <linux/mfd/arizona/pdata.h> +#include <linux/mfd/arizona/registers.h> + +struct arizona_extcon_info { + struct device *dev; + struct arizona *arizona; + struct mutex lock; + struct regulator *micvdd; + + int micd_mode; + const struct arizona_micd_config *micd_modes; + int micd_num_modes; + + bool micd_reva; + + bool mic; + bool detecting; + int jack_flips; + + 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 }, +}; + +#define ARIZONA_CABLE_MECHANICAL "Mechanical" +#define ARIZONA_CABLE_HEADPHONE "Headphone" +#define ARIZONA_CABLE_HEADSET "Headset" + +static const char *arizona_cable[] = { + ARIZONA_CABLE_MECHANICAL, + ARIZONA_CABLE_HEADSET, + ARIZONA_CABLE_HEADPHONE, + NULL, +}; + +static const u32 arizona_exclusions[] = { + 0x6, /* Headphone and headset */ + 0, +}; + +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); + regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_1, + ARIZONA_MICD_BIAS_SRC_MASK, + info->micd_modes[mode].bias); + regmap_update_bits(arizona->regmap, ARIZONA_ACCESSORY_DETECT_MODE_1, + ARIZONA_ACCDET_SRC, info->micd_modes[mode].src); + + info->micd_mode = mode; + + dev_dbg(arizona->dev, "Set jack polarity to %d\n", mode); +} + +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); + + ret = regulator_enable(info->micvdd); + if (ret != 0) { + dev_err(arizona->dev, "Failed to enable MICVDD: %d\n", + ret); + } + + if (info->micd_reva) { + regmap_write(arizona->regmap, 0x80, 0x3); + regmap_write(arizona->regmap, 0x294, 0); + regmap_write(arizona->regmap, 0x80, 0x0); + } + + regmap_update_bits_check(arizona->regmap, ARIZONA_MIC_DETECT_1, + ARIZONA_MICD_ENA, ARIZONA_MICD_ENA, + &change); + if (!change) { + regulator_disable(info->micvdd); + pm_runtime_put_autosuspend(info->dev); + } +} + +static void arizona_stop_mic(struct arizona_extcon_info *info) +{ + struct arizona *arizona = info->arizona; + bool change; + + regmap_update_bits_check(arizona->regmap, ARIZONA_MIC_DETECT_1, + ARIZONA_MICD_ENA, 0, + &change); + + if (info->micd_reva) { + regmap_write(arizona->regmap, 0x80, 0x3); + regmap_write(arizona->regmap, 0x294, 2); + regmap_write(arizona->regmap, 0x80, 0x0); + } + + if (change) { + regulator_disable(info->micvdd); + pm_runtime_put_autosuspend(info->dev); + } +} + +static irqreturn_t arizona_micdet(int irq, void *data) +{ + struct arizona_extcon_info *info = data; + struct arizona *arizona = info->arizona; + unsigned int val; + int ret; + + mutex_lock(&info->lock); + + ret = regmap_read(arizona->regmap, ARIZONA_MIC_DETECT_3, &val); + if (ret != 0) { + dev_err(arizona->dev, "Failed to read MICDET: %d\n", ret); + return IRQ_NONE; + } + + dev_dbg(arizona->dev, "MICDET: %x\n", val); + + if (!(val & ARIZONA_MICD_VALID)) { + dev_warn(arizona->dev, "Microphone detection state invalid\n"); + mutex_unlock(&info->lock); + return IRQ_NONE; + } + + /* Due to jack detect this should never happen */ + if (!(val & ARIZONA_MICD_STS)) { + dev_warn(arizona->dev, "Detected open circuit\n"); + info->detecting = false; + goto handled; + } + + /* If we got a high impedence we should have a headset, report it. */ + if (info->detecting && (val & 0x400)) { + ret = extcon_set_cable_state(&info->edev, + ARIZONA_CABLE_HEADSET, true); + + if (ret != 0) + dev_err(arizona->dev, "Headset report failed: %d\n", + ret); + + info->mic = true; + info->detecting = false; + goto handled; + } + + /* If we detected a lower impedence during initial startup + * then we probably have the wrong polarity, flip it. Don't + * do this for the lowest impedences to speed up detection of + * plain headphones. If both polarities report a low + * 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"); + info->detecting = false; + ret = extcon_set_cable_state(&info->edev, + ARIZONA_CABLE_HEADPHONE, + true); + if (ret != 0) + dev_err(arizona->dev, + "Headphone report failed: %d\n", + ret); + } else { + info->micd_mode++; + if (info->micd_mode == info->micd_num_modes) + info->micd_mode = 0; + arizona_extcon_set_mode(info, info->micd_mode); + + info->jack_flips++; + } + + goto handled; + } + + /* + * If we're still detecting and we detect a short then we've + * got a headphone. Otherwise it's a button press, the + * button reporting is stubbed out for now. + */ + if (val & 0x3fc) { + if (info->mic) { + dev_dbg(arizona->dev, "Mic button detected\n"); + + } else if (info->detecting) { + dev_dbg(arizona->dev, "Headphone detected\n"); + 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); + } else { + dev_warn(arizona->dev, "Button with no mic: %x\n", + val); + } + } else { + dev_dbg(arizona->dev, "Mic button released\n"); + } + +handled: + pm_runtime_mark_last_busy(info->dev); + mutex_unlock(&info->lock); + + return IRQ_HANDLED; +} + +static irqreturn_t arizona_jackdet(int irq, void *data) +{ + struct arizona_extcon_info *info = data; + struct arizona *arizona = info->arizona; + unsigned int val; + int ret; + + pm_runtime_get_sync(info->dev); + + mutex_lock(&info->lock); + + 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", + ret); + mutex_unlock(&info->lock); + pm_runtime_put_autosuspend(info->dev); + return IRQ_NONE; + } + + if (val & ARIZONA_JD1_STS) { + dev_dbg(arizona->dev, "Detected jack\n"); + ret = extcon_set_cable_state(&info->edev, + ARIZONA_CABLE_MECHANICAL, true); + + if (ret != 0) + dev_err(arizona->dev, "Mechanical report failed: %d\n", + ret); + + arizona_start_mic(info); + } else { + dev_dbg(arizona->dev, "Detected jack removal\n"); + + arizona_stop_mic(info); + + ret = extcon_update_state(&info->edev, 0xffffffff, 0); + if (ret != 0) + dev_err(arizona->dev, "Removal report failed: %d\n", + ret); + } + + mutex_unlock(&info->lock); + + pm_runtime_mark_last_busy(info->dev); + pm_runtime_put_autosuspend(info->dev); + + return IRQ_HANDLED; +} + +static int __devinit 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 ret, mode; + + pdata = dev_get_platdata(arizona->dev); + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) { + dev_err(&pdev->dev, "failed to allocate memory\n"); + ret = -ENOMEM; + goto err; + } + + info->micvdd = devm_regulator_get(arizona->dev, "MICVDD"); + if (IS_ERR(info->micvdd)) { + ret = PTR_ERR(info->micvdd); + dev_err(arizona->dev, "Failed to get MICVDD: %d\n", ret); + goto err; + } + + mutex_init(&info->lock); + info->arizona = arizona; + info->dev = &pdev->dev; + info->detecting = true; + platform_set_drvdata(pdev, info); + + switch (arizona->type) { + case WM5102: + switch (arizona->rev) { + case 0: + info->micd_reva = true; + break; + default: + break; + } + break; + default: + break; + } + + info->edev.name = "Headset Jack"; + info->edev.supported_cable = arizona_cable; + info->edev.mutually_exclusive = arizona_exclusions; + + ret = extcon_dev_register(&info->edev, arizona->dev); + if (ret < 0) { + dev_err(arizona->dev, "extcon_dev_regster() failed: %d\n", + ret); + goto err; + } + + if (pdata->num_micd_configs) { + info->micd_modes = pdata->micd_configs; + info->micd_num_modes = pdata->num_micd_configs; + } else { + info->micd_modes = micd_default_modes; + info->micd_num_modes = ARRAY_SIZE(micd_default_modes); + } + + if (arizona->pdata.micd_pol_gpio > 0) { + if (info->micd_modes[0].gpio) + mode = GPIOF_OUT_INIT_HIGH; + else + mode = GPIOF_OUT_INIT_LOW; + + ret = devm_gpio_request_one(&pdev->dev, + arizona->pdata.micd_pol_gpio, + mode, + "MICD polarity"); + if (ret != 0) { + dev_err(arizona->dev, "Failed to request GPIO%d: %d\n", + arizona->pdata.micd_pol_gpio, ret); + goto err_register; + } + } + + arizona_extcon_set_mode(info, 0); + + pm_runtime_enable(&pdev->dev); + pm_runtime_idle(&pdev->dev); + pm_runtime_get_sync(&pdev->dev); + + ret = arizona_request_irq(arizona, ARIZONA_IRQ_JD_RISE, + "JACKDET rise", arizona_jackdet, info); + if (ret != 0) { + dev_err(&pdev->dev, "Failed to get JACKDET rise IRQ: %d\n", + ret); + goto err_register; + } + + ret = arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_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, + "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); + if (ret != 0) { + dev_err(&pdev->dev, "Failed to set JD fall IRQ wake: %d\n", + ret); + goto err_fall; + } + + ret = arizona_request_irq(arizona, ARIZONA_IRQ_MICDET, + "MICDET", arizona_micdet, info); + if (ret != 0) { + dev_err(&pdev->dev, "Failed to get MICDET IRQ: %d\n", ret); + 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); + + arizona_clk32k_enable(arizona); + regmap_update_bits(arizona->regmap, ARIZONA_JACK_DETECT_DEBOUNCE, + ARIZONA_JD1_DB, ARIZONA_JD1_DB); + regmap_update_bits(arizona->regmap, ARIZONA_JACK_DETECT_ANALOGUE, + ARIZONA_JD1_ENA, ARIZONA_JD1_ENA); + + pm_runtime_put(&pdev->dev); + + return 0; + +err_fall_wake: + arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_FALL, 0); +err_fall: + arizona_free_irq(arizona, ARIZONA_IRQ_JD_FALL, info); +err_rise_wake: + arizona_set_irq_wake(arizona, ARIZONA_IRQ_JD_RISE, 0); +err_rise: + arizona_free_irq(arizona, ARIZONA_IRQ_JD_RISE, info); +err_register: + pm_runtime_disable(&pdev->dev); + extcon_dev_unregister(&info->edev); +err: + return ret; +} + +static int __devexit arizona_extcon_remove(struct platform_device *pdev) +{ + struct arizona_extcon_info *info = platform_get_drvdata(pdev); + struct arizona *arizona = info->arizona; + + 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); + 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); + regmap_update_bits(arizona->regmap, ARIZONA_JACK_DETECT_ANALOGUE, + ARIZONA_JD1_ENA, 0); + arizona_clk32k_disable(arizona); + extcon_dev_unregister(&info->edev); + + return 0; +} + +static struct platform_driver arizona_extcon_driver = { + .driver = { + .name = "arizona-extcon", + .owner = THIS_MODULE, + }, + .probe = arizona_extcon_probe, + .remove = __devexit_p(arizona_extcon_remove), +}; + +module_platform_driver(arizona_extcon_driver); + +MODULE_DESCRIPTION("Arizona Extcon driver"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:extcon-arizona"); |