diff options
author | André Fabian Silva Delgado <emulatorman@parabola.nu> | 2015-08-05 17:04:01 -0300 |
---|---|---|
committer | André Fabian Silva Delgado <emulatorman@parabola.nu> | 2015-08-05 17:04:01 -0300 |
commit | 57f0f512b273f60d52568b8c6b77e17f5636edc0 (patch) | |
tree | 5e910f0e82173f4ef4f51111366a3f1299037a7b /drivers/extcon |
Initial import
Diffstat (limited to 'drivers/extcon')
-rw-r--r-- | drivers/extcon/Kconfig | 113 | ||||
-rw-r--r-- | drivers/extcon/Makefile | 16 | ||||
-rw-r--r-- | drivers/extcon/extcon-adc-jack.c | 193 | ||||
-rw-r--r-- | drivers/extcon/extcon-arizona.c | 1500 | ||||
-rw-r--r-- | drivers/extcon/extcon-gpio.c | 192 | ||||
-rw-r--r-- | drivers/extcon/extcon-max14577.c | 818 | ||||
-rw-r--r-- | drivers/extcon/extcon-max77693.c | 1313 | ||||
-rw-r--r-- | drivers/extcon/extcon-max77843.c | 881 | ||||
-rw-r--r-- | drivers/extcon/extcon-max8997.c | 801 | ||||
-rw-r--r-- | drivers/extcon/extcon-palmas.c | 304 | ||||
-rw-r--r-- | drivers/extcon/extcon-rt8973a.c | 738 | ||||
-rw-r--r-- | drivers/extcon/extcon-rt8973a.h | 203 | ||||
-rw-r--r-- | drivers/extcon/extcon-sm5502.c | 718 | ||||
-rw-r--r-- | drivers/extcon/extcon-sm5502.h | 282 | ||||
-rw-r--r-- | drivers/extcon/extcon-usb-gpio.c | 237 | ||||
-rw-r--r-- | drivers/extcon/extcon.c | 1066 |
16 files changed, 9375 insertions, 0 deletions
diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig new file mode 100644 index 000000000..fdc0bf054 --- /dev/null +++ b/drivers/extcon/Kconfig @@ -0,0 +1,113 @@ +menuconfig EXTCON + tristate "External Connector Class (extcon) support" + help + Say Y here to enable external connector class (extcon) support. + This allows monitoring external connectors by userspace + via sysfs and uevent and supports external connectors with + multiple states; i.e., an extcon that may have multiple + cables attached. For example, an external connector of a device + may be used to connect an HDMI cable and a AC adaptor, and to + host USB ports. Many of 30-pin connectors including PDMI are + also good examples. + +if EXTCON + +comment "Extcon Device Drivers" + +config EXTCON_ADC_JACK + tristate "ADC Jack extcon support" + depends on IIO + help + Say Y here to enable extcon device driver based on ADC values. + +config EXTCON_ARIZONA + tristate "Wolfson Arizona EXTCON support" + depends on MFD_ARIZONA && INPUT && SND_SOC + 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. + +config EXTCON_GPIO + tristate "GPIO extcon support" + depends on GPIOLIB + help + Say Y here to enable GPIO based extcon support. Note that GPIO + extcon supports single state per extcon instance. + +config EXTCON_MAX14577 + tristate "MAX14577/77836 EXTCON Support" + depends on MFD_MAX14577 + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the MUIC device of + Maxim MAX14577/77836. The MAX14577/77836 MUIC is a USB port accessory + detector and switch. + +config EXTCON_MAX77693 + tristate "MAX77693 EXTCON Support" + depends on MFD_MAX77693 && INPUT + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the MUIC device of + Maxim MAX77693 PMIC. The MAX77693 MUIC is a USB port accessory + detector and switch. + +config EXTCON_MAX77843 + tristate "MAX77843 EXTCON Support" + depends on MFD_MAX77843 + select IRQ_DOMAIN + select REGMAP_I2C + help + If you say yes here you get support for the MUIC device of + Maxim MAX77843. The MAX77843 MUIC is a USB port accessory + detector add switch. + +config EXTCON_MAX8997 + tristate "MAX8997 EXTCON Support" + depends on MFD_MAX8997 && IRQ_DOMAIN + help + If you say yes here you get support for the MUIC device of + Maxim MAX8997 PMIC. The MAX8997 MUIC is a USB port accessory + detector and switch. + +config EXTCON_PALMAS + tristate "Palmas USB EXTCON support" + depends on MFD_PALMAS + help + Say Y here to enable support for USB peripheral and USB host + detection by palmas usb. + +config EXTCON_RT8973A + tristate "RT8973A EXTCON support" + depends on I2C + select IRQ_DOMAIN + select REGMAP_I2C + select REGMAP_IRQ + help + If you say yes here you get support for the MUIC device of + Richtek RT8973A. The RT8973A is a USB port accessory detector + and switch that is optimized to protect low voltage system + from abnormal high input voltage (up to 28V). + +config EXTCON_SM5502 + tristate "SM5502 EXTCON support" + depends on I2C + select IRQ_DOMAIN + select REGMAP_I2C + select REGMAP_IRQ + help + If you say yes here you get support for the MUIC device of + Silicon Mitus SM5502. The SM5502 is a USB port accessory + detector and switch. + +config EXTCON_USB_GPIO + tristate "USB GPIO extcon support" + depends on GPIOLIB + help + Say Y here to enable GPIO based USB cable detection extcon support. + Used typically if GPIO is used for USB ID pin detection. + +endif # MULTISTATE_SWITCH diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile new file mode 100644 index 000000000..920411479 --- /dev/null +++ b/drivers/extcon/Makefile @@ -0,0 +1,16 @@ + +# Makefile for external connector class (extcon) devices +# + +obj-$(CONFIG_EXTCON) += extcon.o +obj-$(CONFIG_EXTCON_ADC_JACK) += extcon-adc-jack.o +obj-$(CONFIG_EXTCON_ARIZONA) += extcon-arizona.o +obj-$(CONFIG_EXTCON_GPIO) += extcon-gpio.o +obj-$(CONFIG_EXTCON_MAX14577) += extcon-max14577.o +obj-$(CONFIG_EXTCON_MAX77693) += extcon-max77693.o +obj-$(CONFIG_EXTCON_MAX77843) += extcon-max77843.o +obj-$(CONFIG_EXTCON_MAX8997) += extcon-max8997.o +obj-$(CONFIG_EXTCON_PALMAS) += extcon-palmas.o +obj-$(CONFIG_EXTCON_RT8973A) += extcon-rt8973a.o +obj-$(CONFIG_EXTCON_SM5502) += extcon-sm5502.o +obj-$(CONFIG_EXTCON_USB_GPIO) += extcon-usb-gpio.o diff --git a/drivers/extcon/extcon-adc-jack.c b/drivers/extcon/extcon-adc-jack.c new file mode 100644 index 000000000..2bb82e550 --- /dev/null +++ b/drivers/extcon/extcon-adc-jack.c @@ -0,0 +1,193 @@ +/* + * drivers/extcon/extcon-adc-jack.c + * + * Analog Jack extcon driver with ADC-based detection capability. + * + * Copyright (C) 2012 Samsung Electronics + * MyungJoo Ham <myungjoo.ham@samsung.com> + * + * Modified for calling to IIO to get adc by <anish.singh@samsung.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/device.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/workqueue.h> +#include <linux/iio/consumer.h> +#include <linux/extcon/extcon-adc-jack.h> +#include <linux/extcon.h> + +/** + * struct adc_jack_data - internal data for adc_jack device driver + * @edev: extcon device. + * @cable_names: list of supported cables. + * @num_cables: size of cable_names. + * @adc_conditions: list of adc value conditions. + * @num_conditions: size of adc_conditions. + * @irq: irq number of attach/detach event (0 if not exist). + * @handling_delay: interrupt handler will schedule extcon event + * handling at handling_delay jiffies. + * @handler: extcon event handler called by interrupt handler. + * @chan: iio channel being queried. + */ +struct adc_jack_data { + struct extcon_dev *edev; + + const char **cable_names; + int num_cables; + struct adc_jack_cond *adc_conditions; + int num_conditions; + + int irq; + unsigned long handling_delay; /* in jiffies */ + struct delayed_work handler; + + struct iio_channel *chan; +}; + +static void adc_jack_handler(struct work_struct *work) +{ + struct adc_jack_data *data = container_of(to_delayed_work(work), + struct adc_jack_data, + handler); + u32 state = 0; + int ret, adc_val; + int i; + + ret = iio_read_channel_raw(data->chan, &adc_val); + if (ret < 0) { + dev_err(&data->edev->dev, "read channel() error: %d\n", ret); + return; + } + + /* Get state from adc value with adc_conditions */ + for (i = 0; i < data->num_conditions; i++) { + struct adc_jack_cond *def = &data->adc_conditions[i]; + if (!def->state) + break; + if (def->min_adc <= adc_val && def->max_adc >= adc_val) { + state = def->state; + break; + } + } + /* if no def has met, it means state = 0 (no cables attached) */ + + extcon_set_state(data->edev, state); +} + +static irqreturn_t adc_jack_irq_thread(int irq, void *_data) +{ + struct adc_jack_data *data = _data; + + queue_delayed_work(system_power_efficient_wq, + &data->handler, data->handling_delay); + return IRQ_HANDLED; +} + +static int adc_jack_probe(struct platform_device *pdev) +{ + struct adc_jack_data *data; + struct adc_jack_pdata *pdata = dev_get_platdata(&pdev->dev); + int i, err = 0; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (!pdata->cable_names) { + dev_err(&pdev->dev, "error: cable_names not defined.\n"); + return -EINVAL; + } + + data->edev = devm_extcon_dev_allocate(&pdev->dev, pdata->cable_names); + if (IS_ERR(data->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + data->edev->name = pdata->name; + + /* Check the length of array and set num_cables */ + for (i = 0; data->edev->supported_cable[i]; i++) + ; + if (i == 0 || i > SUPPORTED_CABLE_MAX) { + dev_err(&pdev->dev, "error: pdata->cable_names size = %d\n", + i - 1); + return -EINVAL; + } + data->num_cables = i; + + if (!pdata->adc_conditions || + !pdata->adc_conditions[0].state) { + dev_err(&pdev->dev, "error: adc_conditions not defined.\n"); + return -EINVAL; + } + data->adc_conditions = pdata->adc_conditions; + + /* Check the length of array and set num_conditions */ + for (i = 0; data->adc_conditions[i].state; i++) + ; + data->num_conditions = i; + + data->chan = iio_channel_get(&pdev->dev, pdata->consumer_channel); + if (IS_ERR(data->chan)) + return PTR_ERR(data->chan); + + data->handling_delay = msecs_to_jiffies(pdata->handling_delay_ms); + + INIT_DEFERRABLE_WORK(&data->handler, adc_jack_handler); + + platform_set_drvdata(pdev, data); + + err = devm_extcon_dev_register(&pdev->dev, data->edev); + if (err) + return err; + + data->irq = platform_get_irq(pdev, 0); + if (!data->irq) { + dev_err(&pdev->dev, "platform_get_irq failed\n"); + return -ENODEV; + } + + err = request_any_context_irq(data->irq, adc_jack_irq_thread, + pdata->irq_flags, pdata->name, data); + + if (err < 0) { + dev_err(&pdev->dev, "error: irq %d\n", data->irq); + return err; + } + + return 0; +} + +static int adc_jack_remove(struct platform_device *pdev) +{ + struct adc_jack_data *data = platform_get_drvdata(pdev); + + free_irq(data->irq, data); + cancel_work_sync(&data->handler.work); + iio_channel_release(data->chan); + + return 0; +} + +static struct platform_driver adc_jack_driver = { + .probe = adc_jack_probe, + .remove = adc_jack_remove, + .driver = { + .name = "adc-jack", + }, +}; + +module_platform_driver(adc_jack_driver); + +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_DESCRIPTION("ADC Jack extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon-arizona.c b/drivers/extcon/extcon-arizona.c new file mode 100644 index 000000000..a0ed35b33 --- /dev/null +++ b/drivers/extcon/extcon-arizona.c @@ -0,0 +1,1500 @@ +/* + * 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/input.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#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_MAX_MICD_RANGE 8 + +#define ARIZONA_ACCDET_MODE_MIC 0 +#define ARIZONA_ACCDET_MODE_HPL 1 +#define ARIZONA_ACCDET_MODE_HPR 2 + +#define ARIZONA_MICD_CLAMP_MODE_JDL 0x4 +#define ARIZONA_MICD_CLAMP_MODE_JDH 0x5 +#define ARIZONA_MICD_CLAMP_MODE_JDL_GP5H 0x9 +#define ARIZONA_MICD_CLAMP_MODE_JDH_GP5H 0xb + +#define ARIZONA_HPDET_MAX 10000 + +#define HPDET_DEBOUNCE 500 +#define DEFAULT_MICD_TIMEOUT 2000 + +#define MICD_LVL_1_TO_7 (ARIZONA_MICD_LVL_1 | ARIZONA_MICD_LVL_2 | \ + ARIZONA_MICD_LVL_3 | ARIZONA_MICD_LVL_4 | \ + ARIZONA_MICD_LVL_5 | ARIZONA_MICD_LVL_6 | \ + ARIZONA_MICD_LVL_7) + +#define MICD_LVL_0_TO_7 (ARIZONA_MICD_LVL_0 | MICD_LVL_1_TO_7) + +#define MICD_LVL_0_TO_8 (MICD_LVL_0_TO_7 | ARIZONA_MICD_LVL_8) + +struct arizona_extcon_info { + struct device *dev; + struct arizona *arizona; + struct mutex lock; + struct regulator *micvdd; + struct input_dev *input; + + u16 last_jackdet; + + int micd_mode; + const struct arizona_micd_config *micd_modes; + int micd_num_modes; + + const struct arizona_micd_range *micd_ranges; + int num_micd_ranges; + + int micd_timeout; + + bool micd_reva; + bool micd_clamp; + + struct delayed_work hpdet_work; + struct delayed_work micd_detect_work; + struct delayed_work micd_timeout_work; + + bool hpdet_active; + bool hpdet_done; + bool hpdet_retried; + + 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, 0 }, + { 0, 2, 1 }, +}; + +static const struct arizona_micd_range micd_default_ranges[] = { + { .max = 11, .key = BTN_0 }, + { .max = 28, .key = BTN_1 }, + { .max = 54, .key = BTN_2 }, + { .max = 100, .key = BTN_3 }, + { .max = 186, .key = BTN_4 }, + { .max = 430, .key = BTN_5 }, +}; + +static const int arizona_micd_levels[] = { + 3, 6, 8, 11, 13, 16, 18, 21, 23, 26, 28, 31, 34, 36, 39, 41, 44, 46, + 49, 52, 54, 57, 60, 62, 65, 67, 70, 73, 75, 78, 81, 83, 89, 94, 100, + 105, 111, 116, 122, 127, 139, 150, 161, 173, 186, 196, 209, 220, 245, + 270, 295, 321, 348, 375, 402, 430, 489, 550, 614, 681, 752, 903, 1071, + 1257, +}; + +#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, +}; + +static void arizona_start_hpdet_acc_id(struct arizona_extcon_info *info); + +static void arizona_extcon_hp_clamp(struct arizona_extcon_info *info, + bool clamp) +{ + struct arizona *arizona = info->arizona; + unsigned int mask = 0, val = 0; + int ret; + + switch (arizona->type) { + case WM5110: + mask = ARIZONA_HP1L_SHRTO | ARIZONA_HP1L_FLWR | + ARIZONA_HP1L_SHRTI; + if (clamp) + val = ARIZONA_HP1L_SHRTO; + else + val = ARIZONA_HP1L_FLWR | ARIZONA_HP1L_SHRTI; + break; + default: + mask = ARIZONA_RMV_SHRT_HP1L; + if (clamp) + val = ARIZONA_RMV_SHRT_HP1L; + break; + }; + + mutex_lock(&arizona->dapm->card->dapm_mutex); + + arizona->hpdet_clamp = clamp; + + /* Keep the HP output stages disabled while doing the clamp */ + if (clamp) { + ret = regmap_update_bits(arizona->regmap, + ARIZONA_OUTPUT_ENABLES_1, + ARIZONA_OUT1L_ENA | + ARIZONA_OUT1R_ENA, 0); + if (ret != 0) + dev_warn(arizona->dev, + "Failed to disable headphone outputs: %d\n", + ret); + } + + ret = regmap_update_bits(arizona->regmap, ARIZONA_HP_CTRL_1L, + mask, val); + if (ret != 0) + dev_warn(arizona->dev, "Failed to do clamp: %d\n", + ret); + + ret = regmap_update_bits(arizona->regmap, ARIZONA_HP_CTRL_1R, + mask, val); + if (ret != 0) + dev_warn(arizona->dev, "Failed to do clamp: %d\n", + ret); + + /* Restore the desired state while not doing the clamp */ + if (!clamp) { + ret = regmap_update_bits(arizona->regmap, + ARIZONA_OUTPUT_ENABLES_1, + ARIZONA_OUT1L_ENA | + ARIZONA_OUT1R_ENA, arizona->hp_ena); + if (ret != 0) + dev_warn(arizona->dev, + "Failed to restore headphone outputs: %d\n", + ret); + } + + mutex_unlock(&arizona->dapm->card->dapm_mutex); +} + +static void arizona_extcon_set_mode(struct arizona_extcon_info *info, int mode) +{ + struct arizona *arizona = info->arizona; + + mode %= info->micd_num_modes; + + 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 << + ARIZONA_MICD_BIAS_SRC_SHIFT); + 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 const char *arizona_extcon_get_micbias(struct arizona_extcon_info *info) +{ + switch (info->micd_modes[0].bias) { + 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; + + ret = snd_soc_dapm_force_enable_pin(dapm, widget); + if (ret != 0) + dev_warn(arizona->dev, "Failed to enable %s: %d\n", + widget, ret); + + snd_soc_dapm_sync(dapm); + + if (!arizona->pdata.micd_force_micbias) { + ret = snd_soc_dapm_disable_pin(arizona->dapm, widget); + if (ret != 0) + dev_warn(arizona->dev, "Failed to disable %s: %d\n", + widget, ret); + + snd_soc_dapm_sync(dapm); + } +} + +static void arizona_start_mic(struct arizona_extcon_info *info) +{ + struct arizona *arizona = info->arizona; + bool change; + int ret; + + /* 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", + 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(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); + 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; + 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); + + ret = snd_soc_dapm_disable_pin(dapm, widget); + if (ret != 0) + dev_warn(arizona->dev, + "Failed to disable %s: %d\n", + widget, ret); + + 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); + pm_runtime_put_autosuspend(info->dev); + } +} + +static struct { + unsigned int threshold; + unsigned int factor_a; + unsigned int factor_b; +} arizona_hpdet_b_ranges[] = { + { 100, 5528, 362464 }, + { 169, 11084, 6186851 }, + { 169, 11065, 65460395 }, +}; + +#define ARIZONA_HPDET_B_RANGE_MAX 0x3fb + +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 < arizona_hpdet_b_ranges[range].threshold || + val >= ARIZONA_HPDET_B_RANGE_MAX)) { + 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 < arizona_hpdet_b_ranges[range].threshold || + val >= ARIZONA_HPDET_B_RANGE_MAX) { + dev_dbg(arizona->dev, "Measurement out of range\n"); + return ARIZONA_HPDET_MAX; + } + + 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; + /* Convert to ohms, the value is in 0.5 ohm increments */ + val /= 2; + + regmap_read(arizona->regmap, ARIZONA_HEADPHONE_DETECT_1, + &range); + range = (range & ARIZONA_HP_IMPEDANCE_RANGE_MASK) + >> ARIZONA_HP_IMPEDANCE_RANGE_SHIFT; + + /* Skip up a range, or report? */ + 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; + } + + if (range && (val < arizona_hpdet_c_ranges[range].min)) { + dev_dbg(arizona->dev, "Reporting range boundary %d\n", + arizona_hpdet_c_ranges[range].min); + val = arizona_hpdet_c_ranges[range].min; + } + } + + 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, + bool *mic) +{ + 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; + + /* Only check the mic directly if we didn't already ID it */ + if (id_gpio && info->num_hpdet_res == 1) { + 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\n", + info->hpdet_res[0], info->hpdet_res[1]); + + /* Take the headphone impedance for the main report */ + *reading = info->hpdet_res[0]; + + /* Sometimes we get false readings due to slow insert */ + if (*reading >= ARIZONA_HPDET_MAX && !info->hpdet_retried) { + dev_dbg(arizona->dev, "Retrying high impedance\n"); + info->num_hpdet_res = 0; + info->hpdet_retried = true; + arizona_start_hpdet_acc_id(info); + pm_runtime_put(info->dev); + return -EAGAIN; + } + + /* + * If we measure the mic as high impedance + */ + if (!id_gpio || info->hpdet_res[1] > 50) { + dev_dbg(arizona->dev, "Detected mic\n"); + *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; + int ret, reading; + bool mic = false; + + 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, &mic); + 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); + +done: + /* Reset back to starting range */ + regmap_update_bits(arizona->regmap, + ARIZONA_HEADPHONE_DETECT_1, + ARIZONA_HP_IMPEDANCE_RANGE_MASK | ARIZONA_HP_POLL, + 0); + + arizona_extcon_hp_clamp(info, false); + + 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 (mic || 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); + + arizona_extcon_hp_clamp(info, true); + + 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_set_cable_state_(info->edev, + ARIZONA_CABLE_HEADPHONE, true); + 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; + int hp_reading = 32; + bool mic; + 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_hp_clamp(info, true); + + 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; + } + + if (arizona->pdata.hpdet_acc_id_line) { + 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; + } + } else { + arizona_hpdet_do_id(info, &hp_reading, &mic); + } + + 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_set_cable_state_(info->edev, + ARIZONA_CABLE_HEADPHONE, true); + if (ret != 0) + dev_err(arizona->dev, "Failed to report headphone: %d\n", ret); + + info->hpdet_active = false; +} + +static void arizona_micd_timeout_work(struct work_struct *work) +{ + struct arizona_extcon_info *info = container_of(work, + struct arizona_extcon_info, + micd_timeout_work.work); + + mutex_lock(&info->lock); + + dev_dbg(info->arizona->dev, "MICD timed out, reporting HP\n"); + arizona_identify_headphone(info); + + info->detecting = false; + + arizona_stop_mic(info); + + mutex_unlock(&info->lock); +} + +static void arizona_micd_detect(struct work_struct *work) +{ + struct arizona_extcon_info *info = container_of(work, + struct arizona_extcon_info, + micd_detect_work.work); + struct arizona *arizona = info->arizona; + unsigned int val = 0, lvl; + int ret, i, key; + + cancel_delayed_work_sync(&info->micd_timeout_work); + + mutex_lock(&info->lock); + + /* 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); + mutex_unlock(&info->lock); + return; + } else if (!ret) { + dev_dbg(arizona->dev, "Ignoring MICDET for removed cable\n"); + mutex_unlock(&info->lock); + return; + } + + for (i = 0; i < 10 && !(val & MICD_LVL_0_TO_8); i++) { + ret = regmap_read(arizona->regmap, ARIZONA_MIC_DETECT_3, &val); + if (ret != 0) { + dev_err(arizona->dev, + "Failed to read MICDET: %d\n", ret); + mutex_unlock(&info->lock); + return; + } + + 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; + } + } + + if (i == 10 && !(val & MICD_LVL_0_TO_8)) { + dev_err(arizona->dev, "Failed to get valid MICDET value\n"); + mutex_unlock(&info->lock); + return; + } + + /* 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 & ARIZONA_MICD_LVL_8)) { + arizona_identify_headphone(info); + + ret = extcon_set_cable_state_(info->edev, + ARIZONA_CABLE_MICROPHONE, true); + + 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, true); + if (ret != 0) { + dev_err(arizona->dev, "Failed to bypass MICVDD: %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 & MICD_LVL_1_TO_7)) { + if (info->jack_flips >= info->micd_num_modes * 10) { + dev_dbg(arizona->dev, "Detected HP/line\n"); + arizona_identify_headphone(info); + + info->detecting = false; + + arizona_stop_mic(info); + } 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. + */ + if (val & MICD_LVL_0_TO_7) { + if (info->mic) { + dev_dbg(arizona->dev, "Mic button detected\n"); + + lvl = val & ARIZONA_MICD_LVL_MASK; + lvl >>= ARIZONA_MICD_LVL_SHIFT; + + for (i = 0; i < info->num_micd_ranges; i++) + input_report_key(info->input, + info->micd_ranges[i].key, 0); + + WARN_ON(!lvl); + WARN_ON(ffs(lvl) - 1 >= info->num_micd_ranges); + if (lvl && ffs(lvl) - 1 < info->num_micd_ranges) { + key = info->micd_ranges[ffs(lvl) - 1].key; + input_report_key(info->input, key, 1); + input_sync(info->input); + } + + } else if (info->detecting) { + dev_dbg(arizona->dev, "Headphone detected\n"); + info->detecting = false; + arizona_stop_mic(info); + + arizona_identify_headphone(info); + } else { + dev_warn(arizona->dev, "Button with no mic: %x\n", + val); + } + } else { + dev_dbg(arizona->dev, "Mic button released\n"); + for (i = 0; i < info->num_micd_ranges; i++) + input_report_key(info->input, + info->micd_ranges[i].key, 0); + input_sync(info->input); + arizona_extcon_pulse_micbias(info); + } + +handled: + if (info->detecting) + queue_delayed_work(system_power_efficient_wq, + &info->micd_timeout_work, + msecs_to_jiffies(info->micd_timeout)); + + pm_runtime_mark_last_busy(info->dev); + mutex_unlock(&info->lock); +} + +static irqreturn_t arizona_micdet(int irq, void *data) +{ + struct arizona_extcon_info *info = data; + struct arizona *arizona = info->arizona; + int debounce = arizona->pdata.micd_detect_debounce; + + cancel_delayed_work_sync(&info->micd_detect_work); + cancel_delayed_work_sync(&info->micd_timeout_work); + + mutex_lock(&info->lock); + if (!info->detecting) + debounce = 0; + mutex_unlock(&info->lock); + + if (debounce) + queue_delayed_work(system_power_efficient_wq, + &info->micd_detect_work, + msecs_to_jiffies(debounce)); + else + arizona_micd_detect(&info->micd_detect_work.work); + + 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, present, mask; + bool cancelled_hp, cancelled_mic; + int ret, i; + + cancelled_hp = cancel_delayed_work_sync(&info->hpdet_work); + cancelled_mic = cancel_delayed_work_sync(&info->micd_timeout_work); + + pm_runtime_get_sync(info->dev); + + mutex_lock(&info->lock); + + if (arizona->pdata.jd_gpio5) { + mask = ARIZONA_MICD_CLAMP_STS; + if (arizona->pdata.jd_invert) + present = ARIZONA_MICD_CLAMP_STS; + else + present = 0; + } else { + mask = ARIZONA_JD1_STS; + if (arizona->pdata.jd_invert) + present = 0; + else + 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", + ret); + mutex_unlock(&info->lock); + pm_runtime_put_autosuspend(info->dev); + return IRQ_NONE; + } + + val &= mask; + if (val == info->last_jackdet) { + dev_dbg(arizona->dev, "Suppressing duplicate JACKDET\n"); + if (cancelled_hp) + queue_delayed_work(system_power_efficient_wq, + &info->hpdet_work, + msecs_to_jiffies(HPDET_DEBOUNCE)); + + if (cancelled_mic) { + int micd_timeout = info->micd_timeout; + + queue_delayed_work(system_power_efficient_wq, + &info->micd_timeout_work, + msecs_to_jiffies(micd_timeout)); + } + + goto out; + } + info->last_jackdet = val; + + if (info->last_jackdet == present) { + 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); + + if (!arizona->pdata.hpdet_acc_id) { + info->detecting = true; + info->mic = false; + info->jack_flips = 0; + + arizona_start_mic(info); + } else { + queue_delayed_work(system_power_efficient_wq, + &info->hpdet_work, + msecs_to_jiffies(HPDET_DEBOUNCE)); + } + + 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; + info->hpdet_retried = false; + + for (i = 0; i < info->num_micd_ranges; i++) + input_report_key(info->input, + info->micd_ranges[i].key, 0); + input_sync(info->input); + + ret = extcon_update_state(info->edev, 0xffffffff, 0); + 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); + } + + if (arizona->pdata.micd_timeout) + info->micd_timeout = arizona->pdata.micd_timeout; + else + info->micd_timeout = DEFAULT_MICD_TIMEOUT; + +out: + /* 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); + pm_runtime_put_autosuspend(info->dev); + + return IRQ_HANDLED; +} + +/* Map a level onto a slot in the register bank */ +static void arizona_micd_set_level(struct arizona *arizona, int index, + unsigned int level) +{ + int reg; + unsigned int mask; + + reg = ARIZONA_MIC_DETECT_LEVEL_4 - (index / 2); + + if (!(index % 2)) { + mask = 0x3f00; + level <<= 8; + } else { + mask = 0x3f; + } + + /* Program the level itself */ + regmap_update_bits(arizona->regmap, reg, mask, level); +} + +static int arizona_extcon_probe(struct platform_device *pdev) +{ + struct arizona *arizona = dev_get_drvdata(pdev->dev.parent); + struct arizona_pdata *pdata = &arizona->pdata; + struct arizona_extcon_info *info; + unsigned int val; + unsigned int clamp_mode; + int jack_irq_fall, jack_irq_rise; + int ret, mode, i, j; + + if (!arizona->dapm || !arizona->dapm->card) + return -EPROBE_DEFER; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->micvdd = devm_regulator_get(&pdev->dev, "MICVDD"); + if (IS_ERR(info->micvdd)) { + ret = PTR_ERR(info->micvdd); + dev_err(arizona->dev, "Failed to get MICVDD: %d\n", ret); + return ret; + } + + mutex_init(&info->lock); + info->arizona = arizona; + info->dev = &pdev->dev; + info->last_jackdet = ~(ARIZONA_MICD_CLAMP_STS | ARIZONA_JD1_STS); + INIT_DELAYED_WORK(&info->hpdet_work, arizona_hpdet_work); + INIT_DELAYED_WORK(&info->micd_detect_work, arizona_micd_detect); + INIT_DELAYED_WORK(&info->micd_timeout_work, arizona_micd_timeout_work); + platform_set_drvdata(pdev, info); + + switch (arizona->type) { + case WM5102: + switch (arizona->rev) { + case 0: + info->micd_reva = true; + break; + default: + info->micd_clamp = true; + info->hpdet_ip = 1; + break; + } + break; + case WM5110: + case WM8280: + switch (arizona->rev) { + case 0 ... 2: + break; + default: + info->micd_clamp = true; + info->hpdet_ip = 2; + break; + } + break; + default: + break; + } + + info->edev = devm_extcon_dev_allocate(&pdev->dev, arizona_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + info->edev->name = "Headset Jack"; + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret < 0) { + dev_err(arizona->dev, "extcon_dev_register() failed: %d\n", + ret); + return ret; + } + + info->input = devm_input_allocate_device(&pdev->dev); + if (!info->input) { + dev_err(arizona->dev, "Can't allocate input dev\n"); + ret = -ENOMEM; + goto err_register; + } + + info->input->name = "Headset"; + info->input->phys = "arizona/extcon"; + + 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; + } + } + + 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); + + BUILD_BUG_ON(ARRAY_SIZE(arizona_micd_levels) != 0x40); + + if (arizona->pdata.num_micd_ranges) { + info->micd_ranges = pdata->micd_ranges; + info->num_micd_ranges = pdata->num_micd_ranges; + } else { + info->micd_ranges = micd_default_ranges; + info->num_micd_ranges = ARRAY_SIZE(micd_default_ranges); + } + + if (arizona->pdata.num_micd_ranges > ARIZONA_MAX_MICD_RANGE) { + dev_err(arizona->dev, "Too many MICD ranges: %d\n", + arizona->pdata.num_micd_ranges); + } + + if (info->num_micd_ranges > 1) { + for (i = 1; i < info->num_micd_ranges; i++) { + if (info->micd_ranges[i - 1].max > + info->micd_ranges[i].max) { + dev_err(arizona->dev, + "MICD ranges must be sorted\n"); + ret = -EINVAL; + goto err_input; + } + } + } + + /* Disable all buttons by default */ + regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_2, + ARIZONA_MICD_LVL_SEL_MASK, 0x81); + + /* Set up all the buttons the user specified */ + for (i = 0; i < info->num_micd_ranges; i++) { + for (j = 0; j < ARRAY_SIZE(arizona_micd_levels); j++) + if (arizona_micd_levels[j] >= info->micd_ranges[i].max) + break; + + if (j == ARRAY_SIZE(arizona_micd_levels)) { + dev_err(arizona->dev, "Unsupported MICD level %d\n", + info->micd_ranges[i].max); + ret = -EINVAL; + goto err_input; + } + + dev_dbg(arizona->dev, "%d ohms for MICD threshold %d\n", + arizona_micd_levels[j], i); + + arizona_micd_set_level(arizona, i, j); + input_set_capability(info->input, EV_KEY, + info->micd_ranges[i].key); + + /* Enable reporting of that range */ + regmap_update_bits(arizona->regmap, ARIZONA_MIC_DETECT_2, + 1 << i, 1 << i); + } + + /* Set all the remaining keys to a maximum */ + for (; i < ARIZONA_MAX_MICD_RANGE; i++) + arizona_micd_set_level(arizona, i, 0x3f); + + /* + * 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 with optional pull */ + val = 0xc101; + if (arizona->pdata.jd_gpio5_nopull) + val &= ~ARIZONA_GPN_PU; + + regmap_write(arizona->regmap, ARIZONA_GPIO5_CTRL, + val); + + if (arizona->pdata.jd_invert) + clamp_mode = ARIZONA_MICD_CLAMP_MODE_JDH_GP5H; + else + clamp_mode = ARIZONA_MICD_CLAMP_MODE_JDL_GP5H; + } else { + if (arizona->pdata.jd_invert) + clamp_mode = ARIZONA_MICD_CLAMP_MODE_JDH; + else + clamp_mode = ARIZONA_MICD_CLAMP_MODE_JDL; + } + + regmap_update_bits(arizona->regmap, + ARIZONA_MICD_CLAMP_CONTROL, + ARIZONA_MICD_CLAMP_MODE_MASK, clamp_mode); + + regmap_update_bits(arizona->regmap, + ARIZONA_JACK_DETECT_DEBOUNCE, + ARIZONA_MICD_CLAMP_DB, + ARIZONA_MICD_CLAMP_DB); + } + + arizona_extcon_set_mode(info, 0); + + pm_runtime_enable(&pdev->dev); + pm_runtime_idle(&pdev->dev); + pm_runtime_get_sync(&pdev->dev); + + 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", + ret); + goto err_input; + } + + 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, 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, jack_irq_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; + } + + 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, + ARIZONA_JD1_DB, ARIZONA_JD1_DB); + regmap_update_bits(arizona->regmap, ARIZONA_JACK_DETECT_ANALOGUE, + ARIZONA_JD1_ENA, ARIZONA_JD1_ENA); + + ret = regulator_allow_bypass(info->micvdd, true); + if (ret != 0) + dev_warn(arizona->dev, "Failed to set MICVDD to bypass: %d\n", + ret); + + pm_runtime_put(&pdev->dev); + + ret = input_register_device(info->input); + if (ret) { + dev_err(&pdev->dev, "Can't register input device: %d\n", ret); + 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, jack_irq_fall, 0); +err_fall: + arizona_free_irq(arizona, jack_irq_fall, info); +err_rise_wake: + arizona_set_irq_wake(arizona, jack_irq_rise, 0); +err_rise: + arizona_free_irq(arizona, jack_irq_rise, info); +err_input: +err_register: + pm_runtime_disable(&pdev->dev); + return ret; +} + +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); + + 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, 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); + + return 0; +} + +static struct platform_driver arizona_extcon_driver = { + .driver = { + .name = "arizona-extcon", + }, + .probe = arizona_extcon_probe, + .remove = 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"); diff --git a/drivers/extcon/extcon-gpio.c b/drivers/extcon/extcon-gpio.c new file mode 100644 index 000000000..7af33fc43 --- /dev/null +++ b/drivers/extcon/extcon-gpio.c @@ -0,0 +1,192 @@ +/* + * drivers/extcon/extcon_gpio.c + * + * Single-state GPIO extcon driver based on extcon class + * + * Copyright (C) 2008 Google, Inc. + * Author: Mike Lockwood <lockwood@android.com> + * + * Modified by MyungJoo Ham <myungjoo.ham@samsung.com> to support extcon + * (originally switch class is supported) + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * 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/extcon.h> +#include <linux/extcon/extcon-gpio.h> +#include <linux/gpio.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +struct gpio_extcon_data { + struct extcon_dev *edev; + unsigned gpio; + bool gpio_active_low; + const char *state_on; + const char *state_off; + int irq; + struct delayed_work work; + unsigned long debounce_jiffies; + bool check_on_resume; +}; + +static void gpio_extcon_work(struct work_struct *work) +{ + int state; + struct gpio_extcon_data *data = + container_of(to_delayed_work(work), struct gpio_extcon_data, + work); + + state = gpio_get_value(data->gpio); + if (data->gpio_active_low) + state = !state; + extcon_set_state(data->edev, state); +} + +static irqreturn_t gpio_irq_handler(int irq, void *dev_id) +{ + struct gpio_extcon_data *extcon_data = dev_id; + + queue_delayed_work(system_power_efficient_wq, &extcon_data->work, + extcon_data->debounce_jiffies); + return IRQ_HANDLED; +} + +static ssize_t extcon_gpio_print_state(struct extcon_dev *edev, char *buf) +{ + struct device *dev = edev->dev.parent; + struct gpio_extcon_data *extcon_data = dev_get_drvdata(dev); + const char *state; + + if (extcon_get_state(edev)) + state = extcon_data->state_on; + else + state = extcon_data->state_off; + + if (state) + return sprintf(buf, "%s\n", state); + return -EINVAL; +} + +static int gpio_extcon_probe(struct platform_device *pdev) +{ + struct gpio_extcon_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct gpio_extcon_data *extcon_data; + int ret; + + if (!pdata) + return -EBUSY; + if (!pdata->irq_flags) { + dev_err(&pdev->dev, "IRQ flag is not specified.\n"); + return -EINVAL; + } + + extcon_data = devm_kzalloc(&pdev->dev, sizeof(struct gpio_extcon_data), + GFP_KERNEL); + if (!extcon_data) + return -ENOMEM; + + extcon_data->edev = devm_extcon_dev_allocate(&pdev->dev, NULL); + if (IS_ERR(extcon_data->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + extcon_data->edev->name = pdata->name; + + extcon_data->gpio = pdata->gpio; + extcon_data->gpio_active_low = pdata->gpio_active_low; + extcon_data->state_on = pdata->state_on; + extcon_data->state_off = pdata->state_off; + extcon_data->check_on_resume = pdata->check_on_resume; + if (pdata->state_on && pdata->state_off) + extcon_data->edev->print_state = extcon_gpio_print_state; + + ret = devm_gpio_request_one(&pdev->dev, extcon_data->gpio, GPIOF_DIR_IN, + pdev->name); + if (ret < 0) + return ret; + + if (pdata->debounce) { + ret = gpio_set_debounce(extcon_data->gpio, + pdata->debounce * 1000); + if (ret < 0) + extcon_data->debounce_jiffies = + msecs_to_jiffies(pdata->debounce); + } + + ret = devm_extcon_dev_register(&pdev->dev, extcon_data->edev); + if (ret < 0) + return ret; + + INIT_DELAYED_WORK(&extcon_data->work, gpio_extcon_work); + + extcon_data->irq = gpio_to_irq(extcon_data->gpio); + if (extcon_data->irq < 0) + return extcon_data->irq; + + ret = request_any_context_irq(extcon_data->irq, gpio_irq_handler, + pdata->irq_flags, pdev->name, + extcon_data); + if (ret < 0) + return ret; + + platform_set_drvdata(pdev, extcon_data); + /* Perform initial detection */ + gpio_extcon_work(&extcon_data->work.work); + + return 0; +} + +static int gpio_extcon_remove(struct platform_device *pdev) +{ + struct gpio_extcon_data *extcon_data = platform_get_drvdata(pdev); + + cancel_delayed_work_sync(&extcon_data->work); + free_irq(extcon_data->irq, extcon_data); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int gpio_extcon_resume(struct device *dev) +{ + struct gpio_extcon_data *extcon_data; + + extcon_data = dev_get_drvdata(dev); + if (extcon_data->check_on_resume) + queue_delayed_work(system_power_efficient_wq, + &extcon_data->work, extcon_data->debounce_jiffies); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(gpio_extcon_pm_ops, NULL, gpio_extcon_resume); + +static struct platform_driver gpio_extcon_driver = { + .probe = gpio_extcon_probe, + .remove = gpio_extcon_remove, + .driver = { + .name = "extcon-gpio", + .pm = &gpio_extcon_pm_ops, + }, +}; + +module_platform_driver(gpio_extcon_driver); + +MODULE_AUTHOR("Mike Lockwood <lockwood@android.com>"); +MODULE_DESCRIPTION("GPIO extcon driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-max14577.c b/drivers/extcon/extcon-max14577.c new file mode 100644 index 000000000..3823aa4a3 --- /dev/null +++ b/drivers/extcon/extcon-max14577.c @@ -0,0 +1,818 @@ +/* + * extcon-max14577.c - MAX14577/77836 extcon driver to support MUIC + * + * Copyright (C) 2013,2014 Samsung Electronics + * Chanwoo Choi <cw00.choi@samsung.com> + * Krzysztof Kozlowski <k.kozlowski@samsung.com> + * + * 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/interrupt.h> +#include <linux/platform_device.h> +#include <linux/mfd/max14577.h> +#include <linux/mfd/max14577-private.h> +#include <linux/extcon.h> + +#define DELAY_MS_DEFAULT 17000 /* unit: millisecond */ + +enum max14577_muic_adc_debounce_time { + ADC_DEBOUNCE_TIME_5MS = 0, + ADC_DEBOUNCE_TIME_10MS, + ADC_DEBOUNCE_TIME_25MS, + ADC_DEBOUNCE_TIME_38_62MS, +}; + +enum max14577_muic_status { + MAX14577_MUIC_STATUS1 = 0, + MAX14577_MUIC_STATUS2 = 1, + MAX14577_MUIC_STATUS_END, +}; + +/** + * struct max14577_muic_irq + * @irq: the index of irq list of MUIC device. + * @name: the name of irq. + * @virq: the virtual irq to use irq domain + */ +struct max14577_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max14577_muic_irq max14577_muic_irqs[] = { + { MAX14577_IRQ_INT1_ADC, "muic-ADC" }, + { MAX14577_IRQ_INT1_ADCLOW, "muic-ADCLOW" }, + { MAX14577_IRQ_INT1_ADCERR, "muic-ADCError" }, + { MAX14577_IRQ_INT2_CHGTYP, "muic-CHGTYP" }, + { MAX14577_IRQ_INT2_CHGDETRUN, "muic-CHGDETRUN" }, + { MAX14577_IRQ_INT2_DCDTMR, "muic-DCDTMR" }, + { MAX14577_IRQ_INT2_DBCHG, "muic-DBCHG" }, + { MAX14577_IRQ_INT2_VBVOLT, "muic-VBVOLT" }, +}; + +static struct max14577_muic_irq max77836_muic_irqs[] = { + { MAX14577_IRQ_INT1_ADC, "muic-ADC" }, + { MAX14577_IRQ_INT1_ADCLOW, "muic-ADCLOW" }, + { MAX14577_IRQ_INT1_ADCERR, "muic-ADCError" }, + { MAX77836_IRQ_INT1_ADC1K, "muic-ADC1K" }, + { MAX14577_IRQ_INT2_CHGTYP, "muic-CHGTYP" }, + { MAX14577_IRQ_INT2_CHGDETRUN, "muic-CHGDETRUN" }, + { MAX14577_IRQ_INT2_DCDTMR, "muic-DCDTMR" }, + { MAX14577_IRQ_INT2_DBCHG, "muic-DBCHG" }, + { MAX14577_IRQ_INT2_VBVOLT, "muic-VBVOLT" }, + { MAX77836_IRQ_INT2_VIDRM, "muic-VIDRM" }, +}; + +struct max14577_muic_info { + struct device *dev; + struct max14577 *max14577; + struct extcon_dev *edev; + int prev_cable_type; + int prev_chg_type; + u8 status[MAX14577_MUIC_STATUS_END]; + + struct max14577_muic_irq *muic_irqs; + unsigned int muic_irqs_num; + bool irq_adc; + bool irq_chg; + struct work_struct irq_work; + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + int path_usb; + int path_uart; +}; + +enum max14577_muic_cable_group { + MAX14577_CABLE_GROUP_ADC = 0, + MAX14577_CABLE_GROUP_CHG, +}; + +/* Define supported accessory type */ +enum max14577_muic_acc_type { + MAX14577_MUIC_ADC_GROUND = 0x0, + MAX14577_MUIC_ADC_SEND_END_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S1_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S2_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S3_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S4_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S5_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S6_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S7_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S8_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S9_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S10_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S11_BUTTON, + MAX14577_MUIC_ADC_REMOTE_S12_BUTTON, + MAX14577_MUIC_ADC_RESERVED_ACC_1, + MAX14577_MUIC_ADC_RESERVED_ACC_2, + MAX14577_MUIC_ADC_RESERVED_ACC_3, + MAX14577_MUIC_ADC_RESERVED_ACC_4, + MAX14577_MUIC_ADC_RESERVED_ACC_5, + MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE2, + MAX14577_MUIC_ADC_PHONE_POWERED_DEV, + MAX14577_MUIC_ADC_TTY_CONVERTER, + MAX14577_MUIC_ADC_UART_CABLE, + MAX14577_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX14577_MUIC_ADC_FACTORY_MODE_USB_OFF, + MAX14577_MUIC_ADC_FACTORY_MODE_USB_ON, + MAX14577_MUIC_ADC_AV_CABLE_NOLOAD, + MAX14577_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF, + MAX14577_MUIC_ADC_FACTORY_MODE_UART_ON, + MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE1, /* with Remote and Simple Ctrl */ + MAX14577_MUIC_ADC_OPEN, +}; + +/* max14577 MUIC device support below list of accessories(external connector) */ +enum { + EXTCON_CABLE_USB = 0, + EXTCON_CABLE_TA, + EXTCON_CABLE_FAST_CHARGER, + EXTCON_CABLE_SLOW_CHARGER, + EXTCON_CABLE_CHARGE_DOWNSTREAM, + EXTCON_CABLE_JIG_USB_ON, + EXTCON_CABLE_JIG_USB_OFF, + EXTCON_CABLE_JIG_UART_OFF, + EXTCON_CABLE_JIG_UART_ON, + + _EXTCON_CABLE_NUM, +}; + +static const char *max14577_extcon_cable[] = { + [EXTCON_CABLE_USB] = "USB", + [EXTCON_CABLE_TA] = "TA", + [EXTCON_CABLE_FAST_CHARGER] = "Fast-charger", + [EXTCON_CABLE_SLOW_CHARGER] = "Slow-charger", + [EXTCON_CABLE_CHARGE_DOWNSTREAM] = "Charge-downstream", + [EXTCON_CABLE_JIG_USB_ON] = "JIG-USB-ON", + [EXTCON_CABLE_JIG_USB_OFF] = "JIG-USB-OFF", + [EXTCON_CABLE_JIG_UART_OFF] = "JIG-UART-OFF", + [EXTCON_CABLE_JIG_UART_ON] = "JIG-UART-ON", + + NULL, +}; + +/* + * max14577_muic_set_debounce_time - Set the debounce time of ADC + * @info: the instance including private data of max14577 MUIC + * @time: the debounce time of ADC + */ +static int max14577_muic_set_debounce_time(struct max14577_muic_info *info, + enum max14577_muic_adc_debounce_time time) +{ + u8 ret; + + switch (time) { + case ADC_DEBOUNCE_TIME_5MS: + case ADC_DEBOUNCE_TIME_10MS: + case ADC_DEBOUNCE_TIME_25MS: + case ADC_DEBOUNCE_TIME_38_62MS: + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_MUIC_REG_CONTROL3, + CTRL3_ADCDBSET_MASK, + time << CTRL3_ADCDBSET_SHIFT); + if (ret) { + dev_err(info->dev, "failed to set ADC debounce time\n"); + return ret; + } + break; + default: + dev_err(info->dev, "invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +}; + +/* + * max14577_muic_set_path - Set hardware line according to attached cable + * @info: the instance including private data of max14577 MUIC + * @value: the path according to attached cable + * @attached: the state of cable (true:attached, false:detached) + * + * The max14577 MUIC device share outside H/W line among a varity of cables + * so, this function set internal path of H/W line according to the type of + * attached cable. + */ +static int max14577_muic_set_path(struct max14577_muic_info *info, + u8 val, bool attached) +{ + int ret = 0; + u8 ctrl1, ctrl2 = 0; + + /* Set open state to path before changing hw path */ + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_MUIC_REG_CONTROL1, + CLEAR_IDBEN_MICEN_MASK, CTRL1_SW_OPEN); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl1 = val; + else + ctrl1 = CTRL1_SW_OPEN; + + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_MUIC_REG_CONTROL1, + CLEAR_IDBEN_MICEN_MASK, ctrl1); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl2 |= CTRL2_CPEN_MASK; /* LowPwr=0, CPEn=1 */ + else + ctrl2 |= CTRL2_LOWPWR_MASK; /* LowPwr=1, CPEn=0 */ + + ret = max14577_update_reg(info->max14577->regmap, + MAX14577_REG_CONTROL2, + CTRL2_LOWPWR_MASK | CTRL2_CPEN_MASK, ctrl2); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + dev_dbg(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +/* + * max14577_muic_get_cable_type - Return cable type and check cable state + * @info: the instance including private data of max14577 MUIC + * @group: the path according to attached cable + * @attached: store cable state and return + * + * This function check the cable state either attached or detached, + * and then divide precise type of cable according to cable group. + * - max14577_CABLE_GROUP_ADC + * - max14577_CABLE_GROUP_CHG + */ +static int max14577_muic_get_cable_type(struct max14577_muic_info *info, + enum max14577_muic_cable_group group, bool *attached) +{ + int cable_type = 0; + int adc; + int chg_type; + + switch (group) { + case MAX14577_CABLE_GROUP_ADC: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[MAX14577_MUIC_STATUS1] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type) for handling cable when cable is + * detached. + */ + if (adc == MAX14577_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX14577_MUIC_ADC_OPEN; + } else { + *attached = true; + + cable_type = info->prev_cable_type = adc; + } + break; + case MAX14577_CABLE_GROUP_CHG: + /* + * Read charger type to check cable type and decide cable state + * according to type of charger cable. + */ + chg_type = info->status[MAX14577_MUIC_STATUS2] & + STATUS2_CHGTYP_MASK; + chg_type >>= STATUS2_CHGTYP_SHIFT; + + if (chg_type == MAX14577_CHARGER_TYPE_NONE) { + *attached = false; + + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX14577_CHARGER_TYPE_NONE; + } else { + *attached = true; + + /* + * Check current cable state/cable type and store cable + * type(info->prev_chg_type) for handling cable when + * charger cable is detached. + */ + cable_type = info->prev_chg_type = chg_type; + } + + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max14577_muic_jig_handler(struct max14577_muic_info *info, + int cable_type, bool attached) +{ + char cable_name[32]; + int ret = 0; + u8 path = CTRL1_SW_OPEN; + + dev_dbg(info->dev, + "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_OFF: /* ADC_JIG_USB_OFF */ + /* PATH:AP_USB */ + strcpy(cable_name, "JIG-USB-OFF"); + path = CTRL1_SW_USB; + break; + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_ON: /* ADC_JIG_USB_ON */ + /* PATH:AP_USB */ + strcpy(cable_name, "JIG-USB-ON"); + path = CTRL1_SW_USB; + break; + case MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF: /* ADC_JIG_UART_OFF */ + /* PATH:AP_UART */ + strcpy(cable_name, "JIG-UART-OFF"); + path = CTRL1_SW_UART; + break; + default: + dev_err(info->dev, "failed to detect %s jig cable\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + ret = max14577_muic_set_path(info, path, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, cable_name, attached); + + return 0; +} + +static int max14577_muic_adc_handler(struct max14577_muic_info *info) +{ + int cable_type; + bool attached; + int ret = 0; + + /* Check accessory state which is either detached or attached */ + cable_type = max14577_muic_get_cable_type(info, + MAX14577_CABLE_GROUP_ADC, &attached); + + dev_dbg(info->dev, + "external connector is %s (adc:0x%02x, prev_adc:0x%x)\n", + attached ? "attached" : "detached", cable_type, + info->prev_cable_type); + + switch (cable_type) { + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX14577_MUIC_ADC_FACTORY_MODE_USB_ON: + case MAX14577_MUIC_ADC_FACTORY_MODE_UART_OFF: + /* JIG */ + ret = max14577_muic_jig_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX14577_MUIC_ADC_GROUND: + case MAX14577_MUIC_ADC_SEND_END_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S3_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S7_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S9_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S10_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX14577_MUIC_ADC_REMOTE_S12_BUTTON: + case MAX14577_MUIC_ADC_RESERVED_ACC_1: + case MAX14577_MUIC_ADC_RESERVED_ACC_2: + case MAX14577_MUIC_ADC_RESERVED_ACC_3: + case MAX14577_MUIC_ADC_RESERVED_ACC_4: + case MAX14577_MUIC_ADC_RESERVED_ACC_5: + case MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE2: + case MAX14577_MUIC_ADC_PHONE_POWERED_DEV: + case MAX14577_MUIC_ADC_TTY_CONVERTER: + case MAX14577_MUIC_ADC_UART_CABLE: + case MAX14577_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX14577_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX14577_MUIC_ADC_CEA936A_TYPE2_CHG: + case MAX14577_MUIC_ADC_FACTORY_MODE_UART_ON: + case MAX14577_MUIC_ADC_AUDIO_DEVICE_TYPE1: + /* + * This accessory isn't used in general case if it is specially + * needed to detect additional accessory, should implement + * proper operation when this accessory is attached/detached. + */ + dev_info(info->dev, + "accessory is %s but it isn't used (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s accessory (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max14577_muic_chg_handler(struct max14577_muic_info *info) +{ + int chg_type; + bool attached; + int ret = 0; + + chg_type = max14577_muic_get_cable_type(info, + MAX14577_CABLE_GROUP_CHG, &attached); + + dev_dbg(info->dev, + "external connector is %s(chg_type:0x%x, prev_chg_type:0x%x)\n", + attached ? "attached" : "detached", + chg_type, info->prev_chg_type); + + switch (chg_type) { + case MAX14577_CHARGER_TYPE_USB: + /* PATH:AP_USB */ + ret = max14577_muic_set_path(info, info->path_usb, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "USB", attached); + break; + case MAX14577_CHARGER_TYPE_DEDICATED_CHG: + extcon_set_cable_state(info->edev, "TA", attached); + break; + case MAX14577_CHARGER_TYPE_DOWNSTREAM_PORT: + extcon_set_cable_state(info->edev, + "Charge-downstream", attached); + break; + case MAX14577_CHARGER_TYPE_SPECIAL_500MA: + extcon_set_cable_state(info->edev, "Slow-charger", attached); + break; + case MAX14577_CHARGER_TYPE_SPECIAL_1A: + extcon_set_cable_state(info->edev, "Fast-charger", attached); + break; + case MAX14577_CHARGER_TYPE_NONE: + case MAX14577_CHARGER_TYPE_DEAD_BATTERY: + break; + default: + dev_err(info->dev, + "failed to detect %s accessory (chg_type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + return -EINVAL; + } + + return 0; +} + +static void max14577_muic_irq_work(struct work_struct *work) +{ + struct max14577_muic_info *info = container_of(work, + struct max14577_muic_info, irq_work); + int ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + ret = max14577_bulk_read(info->max14577->regmap, + MAX14577_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return; + } + + if (info->irq_adc) { + ret = max14577_muic_adc_handler(info); + info->irq_adc = false; + } + if (info->irq_chg) { + ret = max14577_muic_chg_handler(info); + info->irq_chg = false; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +/* + * Sets irq_adc or irq_chg in max14577_muic_info and returns 1. + * Returns 0 if irq_type does not match registered IRQ for this device type. + */ +static int max14577_parse_irq(struct max14577_muic_info *info, int irq_type) +{ + switch (irq_type) { + case MAX14577_IRQ_INT1_ADC: + case MAX14577_IRQ_INT1_ADCLOW: + case MAX14577_IRQ_INT1_ADCERR: + /* Handle all of accessory except for + type of charger accessory */ + info->irq_adc = true; + return 1; + case MAX14577_IRQ_INT2_CHGTYP: + case MAX14577_IRQ_INT2_CHGDETRUN: + case MAX14577_IRQ_INT2_DCDTMR: + case MAX14577_IRQ_INT2_DBCHG: + case MAX14577_IRQ_INT2_VBVOLT: + /* Handle charger accessory */ + info->irq_chg = true; + return 1; + default: + return 0; + } +} + +/* + * Sets irq_adc or irq_chg in max14577_muic_info and returns 1. + * Returns 0 if irq_type does not match registered IRQ for this device type. + */ +static int max77836_parse_irq(struct max14577_muic_info *info, int irq_type) +{ + /* First check common max14577 interrupts */ + if (max14577_parse_irq(info, irq_type)) + return 1; + + switch (irq_type) { + case MAX77836_IRQ_INT1_ADC1K: + info->irq_adc = true; + return 1; + case MAX77836_IRQ_INT2_VIDRM: + /* Handle charger accessory */ + info->irq_chg = true; + return 1; + default: + return 0; + } +} + +static irqreturn_t max14577_muic_irq_handler(int irq, void *data) +{ + struct max14577_muic_info *info = data; + int i, irq_type = -1; + bool irq_parsed; + + /* + * We may be called multiple times for different nested IRQ-s. + * Including changes in INT1_ADC and INT2_CGHTYP at once. + * However we only need to know whether it was ADC, charger + * or both interrupts so decode IRQ and turn on proper flags. + */ + for (i = 0; i < info->muic_irqs_num; i++) + if (irq == info->muic_irqs[i].virq) + irq_type = info->muic_irqs[i].irq; + + switch (info->max14577->dev_type) { + case MAXIM_DEVICE_TYPE_MAX77836: + irq_parsed = max77836_parse_irq(info, irq_type); + break; + case MAXIM_DEVICE_TYPE_MAX14577: + default: + irq_parsed = max14577_parse_irq(info, irq_type); + break; + } + + if (!irq_parsed) { + dev_err(info->dev, "muic interrupt: irq %d occurred, skipped\n", + irq_type); + return IRQ_HANDLED; + } + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static int max14577_muic_detect_accessory(struct max14577_muic_info *info) +{ + int ret = 0; + int adc; + int chg_type; + bool attached; + + mutex_lock(&info->mutex); + + /* Read STATUSx register to detect accessory */ + ret = max14577_bulk_read(info->max14577->regmap, + MAX14577_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return ret; + } + + adc = max14577_muic_get_cable_type(info, MAX14577_CABLE_GROUP_ADC, + &attached); + if (attached && adc != MAX14577_MUIC_ADC_OPEN) { + ret = max14577_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + chg_type = max14577_muic_get_cable_type(info, MAX14577_CABLE_GROUP_CHG, + &attached); + if (attached && chg_type != MAX14577_CHARGER_TYPE_NONE) { + ret = max14577_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + mutex_unlock(&info->mutex); + + return 0; +} + +static void max14577_muic_detect_cable_wq(struct work_struct *work) +{ + struct max14577_muic_info *info = container_of(to_delayed_work(work), + struct max14577_muic_info, wq_detcable); + + max14577_muic_detect_accessory(info); +} + +static int max14577_muic_probe(struct platform_device *pdev) +{ + struct max14577 *max14577 = dev_get_drvdata(pdev->dev.parent); + struct max14577_muic_info *info; + int delay_jiffies; + int ret; + int i; + u8 id; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->max14577 = max14577; + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, max14577_muic_irq_work); + + switch (max14577->dev_type) { + case MAXIM_DEVICE_TYPE_MAX77836: + info->muic_irqs = max77836_muic_irqs; + info->muic_irqs_num = ARRAY_SIZE(max77836_muic_irqs); + break; + case MAXIM_DEVICE_TYPE_MAX14577: + default: + info->muic_irqs = max14577_muic_irqs; + info->muic_irqs_num = ARRAY_SIZE(max14577_muic_irqs); + } + + /* Support irq domain for max14577 MUIC device */ + for (i = 0; i < info->muic_irqs_num; i++) { + struct max14577_muic_irq *muic_irq = &info->muic_irqs[i]; + unsigned int virq = 0; + + virq = regmap_irq_get_virq(max14577->irq_data, muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max14577_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + max14577_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + + info->edev->name = dev_name(&pdev->dev); + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return ret; + } + + /* Default h/w line path */ + info->path_usb = CTRL1_SW_USB; + info->path_uart = CTRL1_SW_UART; + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + + /* Set initial path for UART */ + max14577_muic_set_path(info, info->path_uart, true); + + /* Check revision number of MUIC device*/ + ret = max14577_read_reg(info->max14577->regmap, + MAX14577_REG_DEVICEID, &id); + if (ret < 0) { + dev_err(&pdev->dev, "failed to read revision number\n"); + return ret; + } + dev_info(info->dev, "device ID : 0x%x\n", id); + + /* Set ADC debounce time */ + max14577_muic_set_debounce_time(info, ADC_DEBOUNCE_TIME_25MS); + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, max14577_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + delay_jiffies); + + return ret; +} + +static int max14577_muic_remove(struct platform_device *pdev) +{ + struct max14577_muic_info *info = platform_get_drvdata(pdev); + + cancel_work_sync(&info->irq_work); + + return 0; +} + +static const struct platform_device_id max14577_muic_id[] = { + { "max14577-muic", MAXIM_DEVICE_TYPE_MAX14577, }, + { "max77836-muic", MAXIM_DEVICE_TYPE_MAX77836, }, + { } +}; +MODULE_DEVICE_TABLE(platform, max14577_muic_id); + +static struct platform_driver max14577_muic_driver = { + .driver = { + .name = "max14577-muic", + }, + .probe = max14577_muic_probe, + .remove = max14577_muic_remove, + .id_table = max14577_muic_id, +}; + +module_platform_driver(max14577_muic_driver); + +MODULE_DESCRIPTION("Maxim 14577/77836 Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>, Krzysztof Kozlowski <k.kozlowski@samsung.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:extcon-max14577"); diff --git a/drivers/extcon/extcon-max77693.c b/drivers/extcon/extcon-max77693.c new file mode 100644 index 000000000..a66bec8f6 --- /dev/null +++ b/drivers/extcon/extcon-max77693.c @@ -0,0 +1,1313 @@ +/* + * extcon-max77693.c - MAX77693 extcon driver to support MAX77693 MUIC + * + * Copyright (C) 2012 Samsung Electrnoics + * Chanwoo Choi <cw00.choi@samsung.com> + * + * 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/input.h> +#include <linux/interrupt.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/mfd/max77693.h> +#include <linux/mfd/max77693-private.h> +#include <linux/extcon.h> +#include <linux/regmap.h> +#include <linux/irqdomain.h> + +#define DEV_NAME "max77693-muic" +#define DELAY_MS_DEFAULT 20000 /* unit: millisecond */ + +/* + * Default value of MAX77693 register to bring up MUIC device. + * If user don't set some initial value for MUIC device through platform data, + * extcon-max77693 driver use 'default_init_data' to bring up base operation + * of MAX77693 MUIC device. + */ +static struct max77693_reg_data default_init_data[] = { + { + /* STATUS2 - [3]ChgDetRun */ + .addr = MAX77693_MUIC_REG_STATUS2, + .data = STATUS2_CHGDETRUN_MASK, + }, { + /* INTMASK1 - Unmask [3]ADC1KM,[0]ADCM */ + .addr = MAX77693_MUIC_REG_INTMASK1, + .data = INTMASK1_ADC1K_MASK + | INTMASK1_ADC_MASK, + }, { + /* INTMASK2 - Unmask [0]ChgTypM */ + .addr = MAX77693_MUIC_REG_INTMASK2, + .data = INTMASK2_CHGTYP_MASK, + }, { + /* INTMASK3 - Mask all of interrupts */ + .addr = MAX77693_MUIC_REG_INTMASK3, + .data = 0x0, + }, { + /* CDETCTRL2 */ + .addr = MAX77693_MUIC_REG_CDETCTRL2, + .data = CDETCTRL2_VIDRMEN_MASK + | CDETCTRL2_DXOVPEN_MASK, + }, +}; + +enum max77693_muic_adc_debounce_time { + ADC_DEBOUNCE_TIME_5MS = 0, + ADC_DEBOUNCE_TIME_10MS, + ADC_DEBOUNCE_TIME_25MS, + ADC_DEBOUNCE_TIME_38_62MS, +}; + +struct max77693_muic_info { + struct device *dev; + struct max77693_dev *max77693; + struct extcon_dev *edev; + int prev_cable_type; + int prev_cable_type_gnd; + int prev_chg_type; + int prev_button_type; + u8 status[2]; + + int irq; + struct work_struct irq_work; + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; + + /* Button of dock device */ + struct input_dev *dock; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + int path_usb; + int path_uart; +}; + +enum max77693_muic_cable_group { + MAX77693_CABLE_GROUP_ADC = 0, + MAX77693_CABLE_GROUP_ADC_GND, + MAX77693_CABLE_GROUP_CHG, + MAX77693_CABLE_GROUP_VBVOLT, +}; + +enum max77693_muic_charger_type { + MAX77693_CHARGER_TYPE_NONE = 0, + MAX77693_CHARGER_TYPE_USB, + MAX77693_CHARGER_TYPE_DOWNSTREAM_PORT, + MAX77693_CHARGER_TYPE_DEDICATED_CHG, + MAX77693_CHARGER_TYPE_APPLE_500MA, + MAX77693_CHARGER_TYPE_APPLE_1A_2A, + MAX77693_CHARGER_TYPE_DEAD_BATTERY = 7, +}; + +/** + * struct max77693_muic_irq + * @irq: the index of irq list of MUIC device. + * @name: the name of irq. + * @virq: the virtual irq to use irq domain + */ +struct max77693_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max77693_muic_irq muic_irqs[] = { + { MAX77693_MUIC_IRQ_INT1_ADC, "muic-ADC" }, + { MAX77693_MUIC_IRQ_INT1_ADC_LOW, "muic-ADCLOW" }, + { MAX77693_MUIC_IRQ_INT1_ADC_ERR, "muic-ADCError" }, + { MAX77693_MUIC_IRQ_INT1_ADC1K, "muic-ADC1K" }, + { MAX77693_MUIC_IRQ_INT2_CHGTYP, "muic-CHGTYP" }, + { MAX77693_MUIC_IRQ_INT2_CHGDETREUN, "muic-CHGDETREUN" }, + { MAX77693_MUIC_IRQ_INT2_DCDTMR, "muic-DCDTMR" }, + { MAX77693_MUIC_IRQ_INT2_DXOVP, "muic-DXOVP" }, + { MAX77693_MUIC_IRQ_INT2_VBVOLT, "muic-VBVOLT" }, + { MAX77693_MUIC_IRQ_INT2_VIDRM, "muic-VIDRM" }, + { MAX77693_MUIC_IRQ_INT3_EOC, "muic-EOC" }, + { MAX77693_MUIC_IRQ_INT3_CGMBC, "muic-CGMBC" }, + { MAX77693_MUIC_IRQ_INT3_OVP, "muic-OVP" }, + { MAX77693_MUIC_IRQ_INT3_MBCCHG_ERR, "muic-MBCCHG_ERR" }, + { MAX77693_MUIC_IRQ_INT3_CHG_ENABLED, "muic-CHG_ENABLED" }, + { MAX77693_MUIC_IRQ_INT3_BAT_DET, "muic-BAT_DET" }, +}; + +/* Define supported accessory type */ +enum max77693_muic_acc_type { + MAX77693_MUIC_ADC_GROUND = 0x0, + MAX77693_MUIC_ADC_SEND_END_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S1_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S2_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S3_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S4_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S5_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S6_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S7_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S8_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S9_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S10_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S11_BUTTON, + MAX77693_MUIC_ADC_REMOTE_S12_BUTTON, + MAX77693_MUIC_ADC_RESERVED_ACC_1, + MAX77693_MUIC_ADC_RESERVED_ACC_2, + MAX77693_MUIC_ADC_RESERVED_ACC_3, + MAX77693_MUIC_ADC_RESERVED_ACC_4, + MAX77693_MUIC_ADC_RESERVED_ACC_5, + MAX77693_MUIC_ADC_CEA936_AUDIO, + MAX77693_MUIC_ADC_PHONE_POWERED_DEV, + MAX77693_MUIC_ADC_TTY_CONVERTER, + MAX77693_MUIC_ADC_UART_CABLE, + MAX77693_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX77693_MUIC_ADC_FACTORY_MODE_USB_OFF, + MAX77693_MUIC_ADC_FACTORY_MODE_USB_ON, + MAX77693_MUIC_ADC_AV_CABLE_NOLOAD, + MAX77693_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF, + MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON, + MAX77693_MUIC_ADC_AUDIO_MODE_REMOTE, + MAX77693_MUIC_ADC_OPEN, + + /* The below accessories have same ADC value so ADCLow and + ADC1K bit is used to separate specific accessory */ + /* ADC|VBVolot|ADCLow|ADC1K| */ + MAX77693_MUIC_GND_USB_HOST = 0x100, /* 0x0| 0| 0| 0| */ + MAX77693_MUIC_GND_USB_HOST_VB = 0x104, /* 0x0| 1| 0| 0| */ + MAX77693_MUIC_GND_AV_CABLE_LOAD = 0x102,/* 0x0| 0| 1| 0| */ + MAX77693_MUIC_GND_MHL = 0x103, /* 0x0| 0| 1| 1| */ + MAX77693_MUIC_GND_MHL_VB = 0x107, /* 0x0| 1| 1| 1| */ +}; + +/* + * MAX77693 MUIC device support below list of accessories(external connector) + */ +enum { + EXTCON_CABLE_USB = 0, + EXTCON_CABLE_USB_HOST, + EXTCON_CABLE_TA, + EXTCON_CABLE_FAST_CHARGER, + EXTCON_CABLE_SLOW_CHARGER, + EXTCON_CABLE_CHARGE_DOWNSTREAM, + EXTCON_CABLE_MHL, + EXTCON_CABLE_MHL_TA, + EXTCON_CABLE_JIG_USB_ON, + EXTCON_CABLE_JIG_USB_OFF, + EXTCON_CABLE_JIG_UART_OFF, + EXTCON_CABLE_JIG_UART_ON, + EXTCON_CABLE_DOCK_SMART, + EXTCON_CABLE_DOCK_DESK, + EXTCON_CABLE_DOCK_AUDIO, + + _EXTCON_CABLE_NUM, +}; + +static const char *max77693_extcon_cable[] = { + [EXTCON_CABLE_USB] = "USB", + [EXTCON_CABLE_USB_HOST] = "USB-Host", + [EXTCON_CABLE_TA] = "TA", + [EXTCON_CABLE_FAST_CHARGER] = "Fast-charger", + [EXTCON_CABLE_SLOW_CHARGER] = "Slow-charger", + [EXTCON_CABLE_CHARGE_DOWNSTREAM] = "Charge-downstream", + [EXTCON_CABLE_MHL] = "MHL", + [EXTCON_CABLE_MHL_TA] = "MHL-TA", + [EXTCON_CABLE_JIG_USB_ON] = "JIG-USB-ON", + [EXTCON_CABLE_JIG_USB_OFF] = "JIG-USB-OFF", + [EXTCON_CABLE_JIG_UART_OFF] = "JIG-UART-OFF", + [EXTCON_CABLE_JIG_UART_ON] = "JIG-UART-ON", + [EXTCON_CABLE_DOCK_SMART] = "Dock-Smart", + [EXTCON_CABLE_DOCK_DESK] = "Dock-Desk", + [EXTCON_CABLE_DOCK_AUDIO] = "Dock-Audio", + + NULL, +}; + +/* + * max77693_muic_set_debounce_time - Set the debounce time of ADC + * @info: the instance including private data of max77693 MUIC + * @time: the debounce time of ADC + */ +static int max77693_muic_set_debounce_time(struct max77693_muic_info *info, + enum max77693_muic_adc_debounce_time time) +{ + int ret; + + switch (time) { + case ADC_DEBOUNCE_TIME_5MS: + case ADC_DEBOUNCE_TIME_10MS: + case ADC_DEBOUNCE_TIME_25MS: + case ADC_DEBOUNCE_TIME_38_62MS: + /* + * Don't touch BTLDset, JIGset when you want to change adc + * debounce time. If it writes other than 0 to BTLDset, JIGset + * muic device will be reset and loose current state. + */ + ret = regmap_write(info->max77693->regmap_muic, + MAX77693_MUIC_REG_CTRL3, + time << CONTROL3_ADCDBSET_SHIFT); + if (ret) { + dev_err(info->dev, "failed to set ADC debounce time\n"); + return ret; + } + break; + default: + dev_err(info->dev, "invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +}; + +/* + * max77693_muic_set_path - Set hardware line according to attached cable + * @info: the instance including private data of max77693 MUIC + * @value: the path according to attached cable + * @attached: the state of cable (true:attached, false:detached) + * + * The max77693 MUIC device share outside H/W line among a varity of cables + * so, this function set internal path of H/W line according to the type of + * attached cable. + */ +static int max77693_muic_set_path(struct max77693_muic_info *info, + u8 val, bool attached) +{ + int ret = 0; + unsigned int ctrl1, ctrl2 = 0; + + if (attached) + ctrl1 = val; + else + ctrl1 = CONTROL1_SW_OPEN; + + ret = regmap_update_bits(info->max77693->regmap_muic, + MAX77693_MUIC_REG_CTRL1, COMP_SW_MASK, ctrl1); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl2 |= CONTROL2_CPEN_MASK; /* LowPwr=0, CPEn=1 */ + else + ctrl2 |= CONTROL2_LOWPWR_MASK; /* LowPwr=1, CPEn=0 */ + + ret = regmap_update_bits(info->max77693->regmap_muic, + MAX77693_MUIC_REG_CTRL2, + CONTROL2_LOWPWR_MASK | CONTROL2_CPEN_MASK, ctrl2); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + dev_info(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +/* + * max77693_muic_get_cable_type - Return cable type and check cable state + * @info: the instance including private data of max77693 MUIC + * @group: the path according to attached cable + * @attached: store cable state and return + * + * This function check the cable state either attached or detached, + * and then divide precise type of cable according to cable group. + * - MAX77693_CABLE_GROUP_ADC + * - MAX77693_CABLE_GROUP_ADC_GND + * - MAX77693_CABLE_GROUP_CHG + * - MAX77693_CABLE_GROUP_VBVOLT + */ +static int max77693_muic_get_cable_type(struct max77693_muic_info *info, + enum max77693_muic_cable_group group, bool *attached) +{ + int cable_type = 0; + int adc; + int adc1k; + int adclow; + int vbvolt; + int chg_type; + + switch (group) { + case MAX77693_CABLE_GROUP_ADC: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type) for handling cable when cable is + * detached. + */ + if (adc == MAX77693_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX77693_MUIC_ADC_OPEN; + } else { + *attached = true; + + cable_type = info->prev_cable_type = adc; + } + break; + case MAX77693_CABLE_GROUP_ADC_GND: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type/_gnd) for handling cable when cable + * is detached. + */ + if (adc == MAX77693_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type_gnd; + info->prev_cable_type_gnd = MAX77693_MUIC_ADC_OPEN; + } else { + *attached = true; + + adclow = info->status[0] & STATUS1_ADCLOW_MASK; + adclow >>= STATUS1_ADCLOW_SHIFT; + adc1k = info->status[0] & STATUS1_ADC1K_MASK; + adc1k >>= STATUS1_ADC1K_SHIFT; + + vbvolt = info->status[1] & STATUS2_VBVOLT_MASK; + vbvolt >>= STATUS2_VBVOLT_SHIFT; + + /** + * [0x1|VBVolt|ADCLow|ADC1K] + * [0x1| 0| 0| 0] USB_HOST + * [0x1| 1| 0| 0] USB_HSOT_VB + * [0x1| 0| 1| 0] Audio Video cable with load + * [0x1| 0| 1| 1] MHL without charging cable + * [0x1| 1| 1| 1] MHL with charging cable + */ + cable_type = ((0x1 << 8) + | (vbvolt << 2) + | (adclow << 1) + | adc1k); + + info->prev_cable_type = adc; + info->prev_cable_type_gnd = cable_type; + } + + break; + case MAX77693_CABLE_GROUP_CHG: + /* + * Read charger type to check cable type and decide cable state + * according to type of charger cable. + */ + chg_type = info->status[1] & STATUS2_CHGTYP_MASK; + chg_type >>= STATUS2_CHGTYP_SHIFT; + + if (chg_type == MAX77693_CHARGER_TYPE_NONE) { + *attached = false; + + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77693_CHARGER_TYPE_NONE; + } else { + *attached = true; + + /* + * Check current cable state/cable type and store cable + * type(info->prev_chg_type) for handling cable when + * charger cable is detached. + */ + cable_type = info->prev_chg_type = chg_type; + } + + break; + case MAX77693_CABLE_GROUP_VBVOLT: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + chg_type = info->status[1] & STATUS2_CHGTYP_MASK; + chg_type >>= STATUS2_CHGTYP_SHIFT; + + if (adc == MAX77693_MUIC_ADC_OPEN + && chg_type == MAX77693_CHARGER_TYPE_NONE) + *attached = false; + else + *attached = true; + + /* + * Read vbvolt field, if vbvolt is 1, + * this cable is used for charging. + */ + vbvolt = info->status[1] & STATUS2_VBVOLT_MASK; + vbvolt >>= STATUS2_VBVOLT_SHIFT; + + cable_type = vbvolt; + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max77693_muic_dock_handler(struct max77693_muic_info *info, + int cable_type, bool attached) +{ + int ret = 0; + int vbvolt; + bool cable_attached; + char dock_name[CABLE_NAME_MAX]; + + dev_info(info->dev, + "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX77693_MUIC_ADC_RESERVED_ACC_3: /* Dock-Smart */ + /* + * Check power cable whether attached or detached state. + * The Dock-Smart device need surely external power supply. + * If power cable(USB/TA) isn't connected to Dock device, + * user can't use Dock-Smart for desktop mode. + */ + vbvolt = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_VBVOLT, &cable_attached); + if (attached && !vbvolt) { + dev_warn(info->dev, + "Cannot detect external power supply\n"); + return 0; + } + + /* + * Notify Dock-Smart/MHL state. + * - Dock-Smart device include three type of cable which + * are HDMI, USB for mouse/keyboard and micro-usb port + * for USB/TA cable. Dock-Smart device need always exteranl + * power supply(USB/TA cable through micro-usb cable). Dock- + * Smart device support screen output of target to separate + * monitor and mouse/keyboard for desktop mode. + * + * Features of 'USB/TA cable with Dock-Smart device' + * - Support MHL + * - Support external output feature of audio + * - Support charging through micro-usb port without data + * connection if TA cable is connected to target. + * - Support charging and data connection through micro-usb port + * if USB cable is connected between target and host + * device. + * - Support OTG(On-The-Go) device (Ex: Mouse/Keyboard) + */ + ret = max77693_muic_set_path(info, info->path_usb, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "Dock-Smart", attached); + extcon_set_cable_state(info->edev, "MHL", attached); + goto out; + case MAX77693_MUIC_ADC_AUDIO_MODE_REMOTE: /* Dock-Desk */ + strcpy(dock_name, "Dock-Desk"); + break; + case MAX77693_MUIC_ADC_AV_CABLE_NOLOAD: /* Dock-Audio */ + strcpy(dock_name, "Dock-Audio"); + if (!attached) + extcon_set_cable_state(info->edev, "USB", false); + break; + default: + dev_err(info->dev, "failed to detect %s dock device\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + /* Dock-Car/Desk/Audio, PATH:AUDIO */ + ret = max77693_muic_set_path(info, CONTROL1_SW_AUDIO, attached); + if (ret < 0) + return ret; + extcon_set_cable_state(info->edev, dock_name, attached); + +out: + return 0; +} + +static int max77693_muic_dock_button_handler(struct max77693_muic_info *info, + int button_type, bool attached) +{ + struct input_dev *dock = info->dock; + unsigned int code; + + switch (button_type) { + case MAX77693_MUIC_ADC_REMOTE_S3_BUTTON-1 + ... MAX77693_MUIC_ADC_REMOTE_S3_BUTTON+1: + /* DOCK_KEY_PREV */ + code = KEY_PREVIOUSSONG; + break; + case MAX77693_MUIC_ADC_REMOTE_S7_BUTTON-1 + ... MAX77693_MUIC_ADC_REMOTE_S7_BUTTON+1: + /* DOCK_KEY_NEXT */ + code = KEY_NEXTSONG; + break; + case MAX77693_MUIC_ADC_REMOTE_S9_BUTTON: + /* DOCK_VOL_DOWN */ + code = KEY_VOLUMEDOWN; + break; + case MAX77693_MUIC_ADC_REMOTE_S10_BUTTON: + /* DOCK_VOL_UP */ + code = KEY_VOLUMEUP; + break; + case MAX77693_MUIC_ADC_REMOTE_S12_BUTTON-1 + ... MAX77693_MUIC_ADC_REMOTE_S12_BUTTON+1: + /* DOCK_KEY_PLAY_PAUSE */ + code = KEY_PLAYPAUSE; + break; + default: + dev_err(info->dev, + "failed to detect %s key (adc:0x%x)\n", + attached ? "pressed" : "released", button_type); + return -EINVAL; + } + + input_event(dock, EV_KEY, code, attached); + input_sync(dock); + + return 0; +} + +static int max77693_muic_adc_ground_handler(struct max77693_muic_info *info) +{ + int cable_type_gnd; + int ret = 0; + bool attached; + + cable_type_gnd = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC_GND, &attached); + + switch (cable_type_gnd) { + case MAX77693_MUIC_GND_USB_HOST: + case MAX77693_MUIC_GND_USB_HOST_VB: + /* USB_HOST, PATH: AP_USB */ + ret = max77693_muic_set_path(info, CONTROL1_SW_USB, attached); + if (ret < 0) + return ret; + extcon_set_cable_state(info->edev, "USB-Host", attached); + break; + case MAX77693_MUIC_GND_AV_CABLE_LOAD: + /* Audio Video Cable with load, PATH:AUDIO */ + ret = max77693_muic_set_path(info, CONTROL1_SW_AUDIO, attached); + if (ret < 0) + return ret; + extcon_set_cable_state(info->edev, + "Audio-video-load", attached); + break; + case MAX77693_MUIC_GND_MHL: + case MAX77693_MUIC_GND_MHL_VB: + /* MHL or MHL with USB/TA cable */ + extcon_set_cable_state(info->edev, "MHL", attached); + break; + default: + dev_err(info->dev, "failed to detect %s cable of gnd type\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + return 0; +} + +static int max77693_muic_jig_handler(struct max77693_muic_info *info, + int cable_type, bool attached) +{ + char cable_name[32]; + int ret = 0; + u8 path = CONTROL1_SW_OPEN; + + dev_info(info->dev, + "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_OFF: /* ADC_JIG_USB_OFF */ + /* PATH:AP_USB */ + strcpy(cable_name, "JIG-USB-OFF"); + path = CONTROL1_SW_USB; + break; + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_ON: /* ADC_JIG_USB_ON */ + /* PATH:AP_USB */ + strcpy(cable_name, "JIG-USB-ON"); + path = CONTROL1_SW_USB; + break; + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF: /* ADC_JIG_UART_OFF */ + /* PATH:AP_UART */ + strcpy(cable_name, "JIG-UART-OFF"); + path = CONTROL1_SW_UART; + break; + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON: /* ADC_JIG_UART_ON */ + /* PATH:AP_UART */ + strcpy(cable_name, "JIG-UART-ON"); + path = CONTROL1_SW_UART; + break; + default: + dev_err(info->dev, "failed to detect %s jig cable\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + ret = max77693_muic_set_path(info, path, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, cable_name, attached); + + return 0; +} + +static int max77693_muic_adc_handler(struct max77693_muic_info *info) +{ + int cable_type; + int button_type; + bool attached; + int ret = 0; + + /* Check accessory state which is either detached or attached */ + cable_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC, &attached); + + dev_info(info->dev, + "external connector is %s (adc:0x%02x, prev_adc:0x%x)\n", + attached ? "attached" : "detached", cable_type, + info->prev_cable_type); + + switch (cable_type) { + case MAX77693_MUIC_ADC_GROUND: + /* USB_HOST/MHL/Audio */ + max77693_muic_adc_ground_handler(info); + break; + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX77693_MUIC_ADC_FACTORY_MODE_USB_ON: + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_OFF: + case MAX77693_MUIC_ADC_FACTORY_MODE_UART_ON: + /* JIG */ + ret = max77693_muic_jig_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX77693_MUIC_ADC_RESERVED_ACC_3: /* Dock-Smart */ + case MAX77693_MUIC_ADC_AUDIO_MODE_REMOTE: /* Dock-Desk */ + case MAX77693_MUIC_ADC_AV_CABLE_NOLOAD: /* Dock-Audio */ + /* + * DOCK device + * + * The MAX77693 MUIC device can detect total 34 cable type + * except of charger cable and MUIC device didn't define + * specfic role of cable in the range of from 0x01 to 0x12 + * of ADC value. So, can use/define cable with no role according + * to schema of hardware board. + */ + ret = max77693_muic_dock_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX77693_MUIC_ADC_REMOTE_S3_BUTTON: /* DOCK_KEY_PREV */ + case MAX77693_MUIC_ADC_REMOTE_S7_BUTTON: /* DOCK_KEY_NEXT */ + case MAX77693_MUIC_ADC_REMOTE_S9_BUTTON: /* DOCK_VOL_DOWN */ + case MAX77693_MUIC_ADC_REMOTE_S10_BUTTON: /* DOCK_VOL_UP */ + case MAX77693_MUIC_ADC_REMOTE_S12_BUTTON: /* DOCK_KEY_PLAY_PAUSE */ + /* + * Button of DOCK device + * - the Prev/Next/Volume Up/Volume Down/Play-Pause button + * + * The MAX77693 MUIC device can detect total 34 cable type + * except of charger cable and MUIC device didn't define + * specfic role of cable in the range of from 0x01 to 0x12 + * of ADC value. So, can use/define cable with no role according + * to schema of hardware board. + */ + if (attached) + button_type = info->prev_button_type = cable_type; + else + button_type = info->prev_button_type; + + ret = max77693_muic_dock_button_handler(info, button_type, + attached); + if (ret < 0) + return ret; + break; + case MAX77693_MUIC_ADC_SEND_END_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX77693_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX77693_MUIC_ADC_RESERVED_ACC_1: + case MAX77693_MUIC_ADC_RESERVED_ACC_2: + case MAX77693_MUIC_ADC_RESERVED_ACC_4: + case MAX77693_MUIC_ADC_RESERVED_ACC_5: + case MAX77693_MUIC_ADC_CEA936_AUDIO: + case MAX77693_MUIC_ADC_PHONE_POWERED_DEV: + case MAX77693_MUIC_ADC_TTY_CONVERTER: + case MAX77693_MUIC_ADC_UART_CABLE: + case MAX77693_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX77693_MUIC_ADC_CEA936A_TYPE2_CHG: + /* + * This accessory isn't used in general case if it is specially + * needed to detect additional accessory, should implement + * proper operation when this accessory is attached/detached. + */ + dev_info(info->dev, + "accessory is %s but it isn't used (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s accessory (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max77693_muic_chg_handler(struct max77693_muic_info *info) +{ + int chg_type; + int cable_type_gnd; + int cable_type; + bool attached; + bool cable_attached; + int ret = 0; + + chg_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_CHG, &attached); + + dev_info(info->dev, + "external connector is %s(chg_type:0x%x, prev_chg_type:0x%x)\n", + attached ? "attached" : "detached", + chg_type, info->prev_chg_type); + + switch (chg_type) { + case MAX77693_CHARGER_TYPE_USB: + case MAX77693_CHARGER_TYPE_DEDICATED_CHG: + case MAX77693_CHARGER_TYPE_NONE: + /* Check MAX77693_CABLE_GROUP_ADC_GND type */ + cable_type_gnd = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC_GND, + &cable_attached); + switch (cable_type_gnd) { + case MAX77693_MUIC_GND_MHL: + case MAX77693_MUIC_GND_MHL_VB: + /* + * MHL cable with MHL-TA(USB/TA) cable + * - MHL cable include two port(HDMI line and separate + * micro-usb port. When the target connect MHL cable, + * extcon driver check whether MHL-TA(USB/TA) cable is + * connected. If MHL-TA cable is connected, extcon + * driver notify state to notifiee for charging battery. + * + * Features of 'MHL-TA(USB/TA) with MHL cable' + * - Support MHL + * - Support charging through micro-usb port without + * data connection + */ + extcon_set_cable_state(info->edev, "MHL-TA", attached); + if (!cable_attached) + extcon_set_cable_state(info->edev, + "MHL", cable_attached); + break; + } + + /* Check MAX77693_CABLE_GROUP_ADC type */ + cable_type = max77693_muic_get_cable_type(info, + MAX77693_CABLE_GROUP_ADC, + &cable_attached); + switch (cable_type) { + case MAX77693_MUIC_ADC_AV_CABLE_NOLOAD: /* Dock-Audio */ + /* + * Dock-Audio device with USB/TA cable + * - Dock device include two port(Dock-Audio and micro- + * usb port). When the target connect Dock-Audio device, + * extcon driver check whether USB/TA cable is connected + * or not. If USB/TA cable is connected, extcon driver + * notify state to notifiee for charging battery. + * + * Features of 'USB/TA cable with Dock-Audio device' + * - Support external output feature of audio. + * - Support charging through micro-usb port without + * data connection. + */ + extcon_set_cable_state(info->edev, "USB", attached); + + if (!cable_attached) + extcon_set_cable_state(info->edev, "Dock-Audio", + cable_attached); + break; + case MAX77693_MUIC_ADC_RESERVED_ACC_3: /* Dock-Smart */ + /* + * Dock-Smart device with USB/TA cable + * - Dock-Desk device include three type of cable which + * are HDMI, USB for mouse/keyboard and micro-usb port + * for USB/TA cable. Dock-Smart device need always + * exteranl power supply(USB/TA cable through micro-usb + * cable). Dock-Smart device support screen output of + * target to separate monitor and mouse/keyboard for + * desktop mode. + * + * Features of 'USB/TA cable with Dock-Smart device' + * - Support MHL + * - Support external output feature of audio + * - Support charging through micro-usb port without + * data connection if TA cable is connected to target. + * - Support charging and data connection through micro- + * usb port if USB cable is connected between target + * and host device + * - Support OTG(On-The-Go) device (Ex: Mouse/Keyboard) + */ + ret = max77693_muic_set_path(info, info->path_usb, + attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "Dock-Smart", + attached); + extcon_set_cable_state(info->edev, "MHL", attached); + + break; + } + + /* Check MAX77693_CABLE_GROUP_CHG type */ + switch (chg_type) { + case MAX77693_CHARGER_TYPE_NONE: + /* + * When MHL(with USB/TA cable) or Dock-Audio with USB/TA + * cable is attached, muic device happen below two irq. + * - 'MAX77693_MUIC_IRQ_INT1_ADC' for detecting + * MHL/Dock-Audio. + * - 'MAX77693_MUIC_IRQ_INT2_CHGTYP' for detecting + * USB/TA cable connected to MHL or Dock-Audio. + * Always, happen eariler MAX77693_MUIC_IRQ_INT1_ADC + * irq than MAX77693_MUIC_IRQ_INT2_CHGTYP irq. + * + * If user attach MHL (with USB/TA cable and immediately + * detach MHL with USB/TA cable before MAX77693_MUIC_IRQ + * _INT2_CHGTYP irq is happened, USB/TA cable remain + * connected state to target. But USB/TA cable isn't + * connected to target. The user be face with unusual + * action. So, driver should check this situation in + * spite of, that previous charger type is N/A. + */ + break; + case MAX77693_CHARGER_TYPE_USB: + /* Only USB cable, PATH:AP_USB */ + ret = max77693_muic_set_path(info, info->path_usb, + attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "USB", attached); + break; + case MAX77693_CHARGER_TYPE_DEDICATED_CHG: + /* Only TA cable */ + extcon_set_cable_state(info->edev, "TA", attached); + break; + } + break; + case MAX77693_CHARGER_TYPE_DOWNSTREAM_PORT: + extcon_set_cable_state(info->edev, + "Charge-downstream", attached); + break; + case MAX77693_CHARGER_TYPE_APPLE_500MA: + extcon_set_cable_state(info->edev, "Slow-charger", attached); + break; + case MAX77693_CHARGER_TYPE_APPLE_1A_2A: + extcon_set_cable_state(info->edev, "Fast-charger", attached); + break; + case MAX77693_CHARGER_TYPE_DEAD_BATTERY: + break; + default: + dev_err(info->dev, + "failed to detect %s accessory (chg_type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + return -EINVAL; + } + + return 0; +} + +static void max77693_muic_irq_work(struct work_struct *work) +{ + struct max77693_muic_info *info = container_of(work, + struct max77693_muic_info, irq_work); + int irq_type = -1; + int i, ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) + if (info->irq == muic_irqs[i].virq) + irq_type = muic_irqs[i].irq; + + ret = regmap_bulk_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return; + } + + switch (irq_type) { + case MAX77693_MUIC_IRQ_INT1_ADC: + case MAX77693_MUIC_IRQ_INT1_ADC_LOW: + case MAX77693_MUIC_IRQ_INT1_ADC_ERR: + case MAX77693_MUIC_IRQ_INT1_ADC1K: + /* Handle all of accessory except for + type of charger accessory */ + ret = max77693_muic_adc_handler(info); + break; + case MAX77693_MUIC_IRQ_INT2_CHGTYP: + case MAX77693_MUIC_IRQ_INT2_CHGDETREUN: + case MAX77693_MUIC_IRQ_INT2_DCDTMR: + case MAX77693_MUIC_IRQ_INT2_DXOVP: + case MAX77693_MUIC_IRQ_INT2_VBVOLT: + case MAX77693_MUIC_IRQ_INT2_VIDRM: + /* Handle charger accessory */ + ret = max77693_muic_chg_handler(info); + break; + case MAX77693_MUIC_IRQ_INT3_EOC: + case MAX77693_MUIC_IRQ_INT3_CGMBC: + case MAX77693_MUIC_IRQ_INT3_OVP: + case MAX77693_MUIC_IRQ_INT3_MBCCHG_ERR: + case MAX77693_MUIC_IRQ_INT3_CHG_ENABLED: + case MAX77693_MUIC_IRQ_INT3_BAT_DET: + break; + default: + dev_err(info->dev, "muic interrupt: irq %d occurred\n", + irq_type); + mutex_unlock(&info->mutex); + return; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +static irqreturn_t max77693_muic_irq_handler(int irq, void *data) +{ + struct max77693_muic_info *info = data; + + info->irq = irq; + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static const struct regmap_config max77693_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, +}; + +static int max77693_muic_detect_accessory(struct max77693_muic_info *info) +{ + int ret = 0; + int adc; + int chg_type; + bool attached; + + mutex_lock(&info->mutex); + + /* Read STATUSx register to detect accessory */ + ret = regmap_bulk_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_STATUS1, info->status, 2); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return ret; + } + + adc = max77693_muic_get_cable_type(info, MAX77693_CABLE_GROUP_ADC, + &attached); + if (attached && adc != MAX77693_MUIC_ADC_OPEN) { + ret = max77693_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + chg_type = max77693_muic_get_cable_type(info, MAX77693_CABLE_GROUP_CHG, + &attached); + if (attached && chg_type != MAX77693_CHARGER_TYPE_NONE) { + ret = max77693_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger accessory\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + mutex_unlock(&info->mutex); + + return 0; +} + +static void max77693_muic_detect_cable_wq(struct work_struct *work) +{ + struct max77693_muic_info *info = container_of(to_delayed_work(work), + struct max77693_muic_info, wq_detcable); + + max77693_muic_detect_accessory(info); +} + +static int max77693_muic_probe(struct platform_device *pdev) +{ + struct max77693_dev *max77693 = dev_get_drvdata(pdev->dev.parent); + struct max77693_platform_data *pdata = dev_get_platdata(max77693->dev); + struct max77693_muic_info *info; + struct max77693_reg_data *init_data; + int num_init_data; + int delay_jiffies; + int ret; + int i; + unsigned int id; + + info = devm_kzalloc(&pdev->dev, sizeof(struct max77693_muic_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->max77693 = max77693; + if (info->max77693->regmap_muic) { + dev_dbg(&pdev->dev, "allocate register map\n"); + } else { + info->max77693->regmap_muic = devm_regmap_init_i2c( + info->max77693->muic, + &max77693_muic_regmap_config); + if (IS_ERR(info->max77693->regmap_muic)) { + ret = PTR_ERR(info->max77693->regmap_muic); + dev_err(max77693->dev, + "failed to allocate register map: %d\n", ret); + return ret; + } + } + + /* Register input device for button of dock device */ + info->dock = devm_input_allocate_device(&pdev->dev); + if (!info->dock) { + dev_err(&pdev->dev, "%s: failed to allocate input\n", __func__); + return -ENOMEM; + } + info->dock->name = "max77693-muic/dock"; + info->dock->phys = "max77693-muic/extcon"; + info->dock->dev.parent = &pdev->dev; + + __set_bit(EV_REP, info->dock->evbit); + + input_set_capability(info->dock, EV_KEY, KEY_VOLUMEUP); + input_set_capability(info->dock, EV_KEY, KEY_VOLUMEDOWN); + input_set_capability(info->dock, EV_KEY, KEY_PLAYPAUSE); + input_set_capability(info->dock, EV_KEY, KEY_PREVIOUSSONG); + input_set_capability(info->dock, EV_KEY, KEY_NEXTSONG); + + ret = input_register_device(info->dock); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot register input device error(%d)\n", + ret); + return ret; + } + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, max77693_muic_irq_work); + + /* Support irq domain for MAX77693 MUIC device */ + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) { + struct max77693_muic_irq *muic_irq = &muic_irqs[i]; + unsigned int virq = 0; + + virq = regmap_irq_get_virq(max77693->irq_data_muic, + muic_irq->irq); + if (!virq) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max77693_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + max77693_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + info->edev->name = DEV_NAME; + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return ret; + } + + /* Initialize MUIC register by using platform data or default data */ + if (pdata && pdata->muic_data) { + init_data = pdata->muic_data->init_data; + num_init_data = pdata->muic_data->num_init_data; + } else { + init_data = default_init_data; + num_init_data = ARRAY_SIZE(default_init_data); + } + + for (i = 0; i < num_init_data; i++) { + enum max77693_irq_source irq_src + = MAX77693_IRQ_GROUP_NR; + + regmap_write(info->max77693->regmap_muic, + init_data[i].addr, + init_data[i].data); + + switch (init_data[i].addr) { + case MAX77693_MUIC_REG_INTMASK1: + irq_src = MUIC_INT1; + break; + case MAX77693_MUIC_REG_INTMASK2: + irq_src = MUIC_INT2; + break; + case MAX77693_MUIC_REG_INTMASK3: + irq_src = MUIC_INT3; + break; + } + + if (irq_src < MAX77693_IRQ_GROUP_NR) + info->max77693->irq_masks_cur[irq_src] + = init_data[i].data; + } + + if (pdata && pdata->muic_data) { + struct max77693_muic_platform_data *muic_pdata + = pdata->muic_data; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + if (muic_pdata->path_uart) + info->path_uart = muic_pdata->path_uart; + else + info->path_uart = CONTROL1_SW_UART; + + if (muic_pdata->path_usb) + info->path_usb = muic_pdata->path_usb; + else + info->path_usb = CONTROL1_SW_USB; + + /* + * Default delay time for detecting cable state + * after certain time. + */ + if (muic_pdata->detcable_delay_ms) + delay_jiffies = + msecs_to_jiffies(muic_pdata->detcable_delay_ms); + else + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } else { + info->path_usb = CONTROL1_SW_USB; + info->path_uart = CONTROL1_SW_UART; + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } + + /* Set initial path for UART */ + max77693_muic_set_path(info, info->path_uart, true); + + /* Check revision number of MUIC device*/ + ret = regmap_read(info->max77693->regmap_muic, + MAX77693_MUIC_REG_ID, &id); + if (ret < 0) { + dev_err(&pdev->dev, "failed to read revision number\n"); + return ret; + } + dev_info(info->dev, "device ID : 0x%x\n", id); + + /* Set ADC debounce time */ + max77693_muic_set_debounce_time(info, ADC_DEBOUNCE_TIME_25MS); + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, max77693_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + delay_jiffies); + + return ret; +} + +static int max77693_muic_remove(struct platform_device *pdev) +{ + struct max77693_muic_info *info = platform_get_drvdata(pdev); + + cancel_work_sync(&info->irq_work); + input_unregister_device(info->dock); + + return 0; +} + +static struct platform_driver max77693_muic_driver = { + .driver = { + .name = DEV_NAME, + }, + .probe = max77693_muic_probe, + .remove = max77693_muic_remove, +}; + +module_platform_driver(max77693_muic_driver); + +MODULE_DESCRIPTION("Maxim MAX77693 Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:extcon-max77693"); diff --git a/drivers/extcon/extcon-max77843.c b/drivers/extcon/extcon-max77843.c new file mode 100644 index 000000000..8db6a926e --- /dev/null +++ b/drivers/extcon/extcon-max77843.c @@ -0,0 +1,881 @@ +/* + * extcon-max77843.c - Maxim MAX77843 extcon driver to support + * MUIC(Micro USB Interface Controller) + * + * Copyright (C) 2015 Samsung Electronics + * Author: Jaewon Kim <jaewon02.kim@samsung.com> + * + * 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. + */ + +#include <linux/extcon.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/mfd/max77843-private.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/workqueue.h> + +#define DELAY_MS_DEFAULT 15000 /* unit: millisecond */ + +enum max77843_muic_status { + MAX77843_MUIC_STATUS1 = 0, + MAX77843_MUIC_STATUS2, + MAX77843_MUIC_STATUS3, + + MAX77843_MUIC_STATUS_NUM, +}; + +struct max77843_muic_info { + struct device *dev; + struct max77843 *max77843; + struct extcon_dev *edev; + + struct mutex mutex; + struct work_struct irq_work; + struct delayed_work wq_detcable; + + u8 status[MAX77843_MUIC_STATUS_NUM]; + int prev_cable_type; + int prev_chg_type; + int prev_gnd_type; + + bool irq_adc; + bool irq_chg; +}; + +enum max77843_muic_cable_group { + MAX77843_CABLE_GROUP_ADC = 0, + MAX77843_CABLE_GROUP_ADC_GND, + MAX77843_CABLE_GROUP_CHG, +}; + +enum max77843_muic_adc_debounce_time { + MAX77843_DEBOUNCE_TIME_5MS = 0, + MAX77843_DEBOUNCE_TIME_10MS, + MAX77843_DEBOUNCE_TIME_25MS, + MAX77843_DEBOUNCE_TIME_38_62MS, +}; + +/* Define accessory cable type */ +enum max77843_muic_accessory_type { + MAX77843_MUIC_ADC_GROUND = 0, + MAX77843_MUIC_ADC_SEND_END_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S1_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S2_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S3_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S4_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S5_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S6_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S7_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S8_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S9_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S10_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S11_BUTTON, + MAX77843_MUIC_ADC_REMOTE_S12_BUTTON, + MAX77843_MUIC_ADC_RESERVED_ACC_1, + MAX77843_MUIC_ADC_RESERVED_ACC_2, + MAX77843_MUIC_ADC_RESERVED_ACC_3, + MAX77843_MUIC_ADC_RESERVED_ACC_4, + MAX77843_MUIC_ADC_RESERVED_ACC_5, + MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE2, + MAX77843_MUIC_ADC_PHONE_POWERED_DEV, + MAX77843_MUIC_ADC_TTY_CONVERTER, + MAX77843_MUIC_ADC_UART_CABLE, + MAX77843_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX77843_MUIC_ADC_FACTORY_MODE_USB_OFF, + MAX77843_MUIC_ADC_FACTORY_MODE_USB_ON, + MAX77843_MUIC_ADC_AV_CABLE_NOLOAD, + MAX77843_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF, + MAX77843_MUIC_ADC_FACTORY_MODE_UART_ON, + MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE1, + MAX77843_MUIC_ADC_OPEN, + + /* The blow accessories should check + not only ADC value but also ADC1K and VBVolt value. */ + /* Offset|ADC1K|VBVolt| */ + MAX77843_MUIC_GND_USB_HOST = 0x100, /* 0x1| 0| 0| */ + MAX77843_MUIC_GND_USB_HOST_VB = 0x101, /* 0x1| 0| 1| */ + MAX77843_MUIC_GND_MHL = 0x102, /* 0x1| 1| 0| */ + MAX77843_MUIC_GND_MHL_VB = 0x103, /* 0x1| 1| 1| */ +}; + +/* Define charger cable type */ +enum max77843_muic_charger_type { + MAX77843_MUIC_CHG_NONE = 0, + MAX77843_MUIC_CHG_USB, + MAX77843_MUIC_CHG_DOWNSTREAM, + MAX77843_MUIC_CHG_DEDICATED, + MAX77843_MUIC_CHG_SPECIAL_500MA, + MAX77843_MUIC_CHG_SPECIAL_1A, + MAX77843_MUIC_CHG_SPECIAL_BIAS, + MAX77843_MUIC_CHG_RESERVED, + MAX77843_MUIC_CHG_GND, +}; + +enum { + MAX77843_CABLE_USB = 0, + MAX77843_CABLE_USB_HOST, + MAX77843_CABLE_TA, + MAX77843_CABLE_CHARGE_DOWNSTREAM, + MAX77843_CABLE_FAST_CHARGER, + MAX77843_CABLE_SLOW_CHARGER, + MAX77843_CABLE_MHL, + MAX77843_CABLE_MHL_TA, + MAX77843_CABLE_JIG_USB_ON, + MAX77843_CABLE_JIG_USB_OFF, + MAX77843_CABLE_JIG_UART_ON, + MAX77843_CABLE_JIG_UART_OFF, + + MAX77843_CABLE_NUM, +}; + +static const char *max77843_extcon_cable[] = { + [MAX77843_CABLE_USB] = "USB", + [MAX77843_CABLE_USB_HOST] = "USB-HOST", + [MAX77843_CABLE_TA] = "TA", + [MAX77843_CABLE_CHARGE_DOWNSTREAM] = "CHARGER-DOWNSTREAM", + [MAX77843_CABLE_FAST_CHARGER] = "FAST-CHARGER", + [MAX77843_CABLE_SLOW_CHARGER] = "SLOW-CHARGER", + [MAX77843_CABLE_MHL] = "MHL", + [MAX77843_CABLE_MHL_TA] = "MHL-TA", + [MAX77843_CABLE_JIG_USB_ON] = "JIG-USB-ON", + [MAX77843_CABLE_JIG_USB_OFF] = "JIG-USB-OFF", + [MAX77843_CABLE_JIG_UART_ON] = "JIG-UART-ON", + [MAX77843_CABLE_JIG_UART_OFF] = "JIG-UART-OFF", +}; + +struct max77843_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max77843_muic_irq max77843_muic_irqs[] = { + { MAX77843_MUIC_IRQ_INT1_ADC, "MUIC-ADC" }, + { MAX77843_MUIC_IRQ_INT1_ADCERROR, "MUIC-ADC_ERROR" }, + { MAX77843_MUIC_IRQ_INT1_ADC1K, "MUIC-ADC1K" }, + { MAX77843_MUIC_IRQ_INT2_CHGTYP, "MUIC-CHGTYP" }, + { MAX77843_MUIC_IRQ_INT2_CHGDETRUN, "MUIC-CHGDETRUN" }, + { MAX77843_MUIC_IRQ_INT2_DCDTMR, "MUIC-DCDTMR" }, + { MAX77843_MUIC_IRQ_INT2_DXOVP, "MUIC-DXOVP" }, + { MAX77843_MUIC_IRQ_INT2_VBVOLT, "MUIC-VBVOLT" }, + { MAX77843_MUIC_IRQ_INT3_VBADC, "MUIC-VBADC" }, + { MAX77843_MUIC_IRQ_INT3_VDNMON, "MUIC-VDNMON" }, + { MAX77843_MUIC_IRQ_INT3_DNRES, "MUIC-DNRES" }, + { MAX77843_MUIC_IRQ_INT3_MPNACK, "MUIC-MPNACK"}, + { MAX77843_MUIC_IRQ_INT3_MRXBUFOW, "MUIC-MRXBUFOW"}, + { MAX77843_MUIC_IRQ_INT3_MRXTRF, "MUIC-MRXTRF"}, + { MAX77843_MUIC_IRQ_INT3_MRXPERR, "MUIC-MRXPERR"}, + { MAX77843_MUIC_IRQ_INT3_MRXRDY, "MUIC-MRXRDY"}, +}; + +static const struct regmap_config max77843_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = MAX77843_MUIC_REG_END, +}; + +static const struct regmap_irq max77843_muic_irq[] = { + /* INT1 interrupt */ + { .reg_offset = 0, .mask = MAX77843_MUIC_ADC, }, + { .reg_offset = 0, .mask = MAX77843_MUIC_ADCERROR, }, + { .reg_offset = 0, .mask = MAX77843_MUIC_ADC1K, }, + + /* INT2 interrupt */ + { .reg_offset = 1, .mask = MAX77843_MUIC_CHGTYP, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_CHGDETRUN, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_DCDTMR, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_DXOVP, }, + { .reg_offset = 1, .mask = MAX77843_MUIC_VBVOLT, }, + + /* INT3 interrupt */ + { .reg_offset = 2, .mask = MAX77843_MUIC_VBADC, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_VDNMON, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_DNRES, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MPNACK, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXBUFOW, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXTRF, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXPERR, }, + { .reg_offset = 2, .mask = MAX77843_MUIC_MRXRDY, }, +}; + +static const struct regmap_irq_chip max77843_muic_irq_chip = { + .name = "max77843-muic", + .status_base = MAX77843_MUIC_REG_INT1, + .mask_base = MAX77843_MUIC_REG_INTMASK1, + .mask_invert = true, + .num_regs = 3, + .irqs = max77843_muic_irq, + .num_irqs = ARRAY_SIZE(max77843_muic_irq), +}; + +static int max77843_muic_set_path(struct max77843_muic_info *info, + u8 val, bool attached) +{ + struct max77843 *max77843 = info->max77843; + int ret = 0; + unsigned int ctrl1, ctrl2; + + if (attached) + ctrl1 = val; + else + ctrl1 = CONTROL1_SW_OPEN; + + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL1, + CONTROL1_COM_SW, ctrl1); + if (ret < 0) { + dev_err(info->dev, "Cannot switch MUIC port\n"); + return ret; + } + + if (attached) + ctrl2 = MAX77843_MUIC_CONTROL2_CPEN_MASK; + else + ctrl2 = MAX77843_MUIC_CONTROL2_LOWPWR_MASK; + + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL2, + MAX77843_MUIC_CONTROL2_LOWPWR_MASK | + MAX77843_MUIC_CONTROL2_CPEN_MASK, ctrl2); + if (ret < 0) { + dev_err(info->dev, "Cannot update lowpower mode\n"); + return ret; + } + + dev_dbg(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +static int max77843_muic_get_cable_type(struct max77843_muic_info *info, + enum max77843_muic_cable_group group, bool *attached) +{ + int adc, chg_type, cable_type, gnd_type; + + adc = info->status[MAX77843_MUIC_STATUS1] & + MAX77843_MUIC_STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + switch (group) { + case MAX77843_CABLE_GROUP_ADC: + if (adc == MAX77843_MUIC_ADC_OPEN) { + *attached = false; + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX77843_MUIC_ADC_OPEN; + } else { + *attached = true; + cable_type = info->prev_cable_type = adc; + } + break; + case MAX77843_CABLE_GROUP_CHG: + chg_type = info->status[MAX77843_MUIC_STATUS2] & + MAX77843_MUIC_STATUS2_CHGTYP_MASK; + + /* Check GROUND accessory with charger cable */ + if (adc == MAX77843_MUIC_ADC_GROUND) { + if (chg_type == MAX77843_MUIC_CHG_NONE) { + /* The following state when charger cable is + * disconnected but the GROUND accessory still + * connected */ + *attached = false; + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77843_MUIC_CHG_NONE; + } else { + + /* The following state when charger cable is + * connected on the GROUND accessory */ + *attached = true; + cable_type = MAX77843_MUIC_CHG_GND; + info->prev_chg_type = MAX77843_MUIC_CHG_GND; + } + break; + } + + if (chg_type == MAX77843_MUIC_CHG_NONE) { + *attached = false; + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX77843_MUIC_CHG_NONE; + } else { + *attached = true; + cable_type = info->prev_chg_type = chg_type; + } + break; + case MAX77843_CABLE_GROUP_ADC_GND: + if (adc == MAX77843_MUIC_ADC_OPEN) { + *attached = false; + cable_type = info->prev_gnd_type; + info->prev_gnd_type = MAX77843_MUIC_ADC_OPEN; + } else { + *attached = true; + + /* Offset|ADC1K|VBVolt| + * 0x1| 0| 0| USB-HOST + * 0x1| 0| 1| USB-HOST with VB + * 0x1| 1| 0| MHL + * 0x1| 1| 1| MHL with VB */ + /* Get ADC1K register bit */ + gnd_type = (info->status[MAX77843_MUIC_STATUS1] & + MAX77843_MUIC_STATUS1_ADC1K_MASK); + + /* Get VBVolt register bit */ + gnd_type |= (info->status[MAX77843_MUIC_STATUS2] & + MAX77843_MUIC_STATUS2_VBVOLT_MASK); + gnd_type >>= STATUS2_VBVOLT_SHIFT; + + /* Offset of GND cable */ + gnd_type |= MAX77843_MUIC_GND_USB_HOST; + cable_type = info->prev_gnd_type = gnd_type; + } + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max77843_muic_adc_gnd_handler(struct max77843_muic_info *info) +{ + int ret, gnd_cable_type; + bool attached; + + gnd_cable_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC_GND, &attached); + dev_dbg(info->dev, "external connector is %s (gnd:0x%02x)\n", + attached ? "attached" : "detached", gnd_cable_type); + + switch (gnd_cable_type) { + case MAX77843_MUIC_GND_USB_HOST: + case MAX77843_MUIC_GND_USB_HOST_VB: + ret = max77843_muic_set_path(info, CONTROL1_SW_USB, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "USB-HOST", attached); + break; + case MAX77843_MUIC_GND_MHL_VB: + case MAX77843_MUIC_GND_MHL: + ret = max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "MHL", attached); + break; + default: + dev_err(info->dev, "failed to detect %s accessory(gnd:0x%x)\n", + attached ? "attached" : "detached", gnd_cable_type); + return -EINVAL; + } + + return 0; +} + +static int max77843_muic_jig_handler(struct max77843_muic_info *info, + int cable_type, bool attached) +{ + int ret; + + dev_dbg(info->dev, "external connector is %s (adc:0x%02x)\n", + attached ? "attached" : "detached", cable_type); + + switch (cable_type) { + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_OFF: + ret = max77843_muic_set_path(info, CONTROL1_SW_USB, attached); + if (ret < 0) + return ret; + extcon_set_cable_state(info->edev, "JIG-USB-OFF", attached); + break; + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_ON: + ret = max77843_muic_set_path(info, CONTROL1_SW_USB, attached); + if (ret < 0) + return ret; + extcon_set_cable_state(info->edev, "JIG-USB-ON", attached); + break; + case MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF: + ret = max77843_muic_set_path(info, CONTROL1_SW_UART, attached); + if (ret < 0) + return ret; + extcon_set_cable_state(info->edev, "JIG-UART-OFF", attached); + break; + default: + ret = max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + if (ret < 0) + return ret; + break; + } + + return 0; +} + +static int max77843_muic_adc_handler(struct max77843_muic_info *info) +{ + int ret, cable_type; + bool attached; + + cable_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC, &attached); + + dev_dbg(info->dev, + "external connector is %s (adc:0x%02x, prev_adc:0x%x)\n", + attached ? "attached" : "detached", cable_type, + info->prev_cable_type); + + switch (cable_type) { + case MAX77843_MUIC_ADC_GROUND: + ret = max77843_muic_adc_gnd_handler(info); + if (ret < 0) + return ret; + break; + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX77843_MUIC_ADC_FACTORY_MODE_USB_ON: + case MAX77843_MUIC_ADC_FACTORY_MODE_UART_OFF: + ret = max77843_muic_jig_handler(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX77843_MUIC_ADC_SEND_END_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S3_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S7_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S9_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S10_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX77843_MUIC_ADC_REMOTE_S12_BUTTON: + case MAX77843_MUIC_ADC_RESERVED_ACC_1: + case MAX77843_MUIC_ADC_RESERVED_ACC_2: + case MAX77843_MUIC_ADC_RESERVED_ACC_3: + case MAX77843_MUIC_ADC_RESERVED_ACC_4: + case MAX77843_MUIC_ADC_RESERVED_ACC_5: + case MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE2: + case MAX77843_MUIC_ADC_PHONE_POWERED_DEV: + case MAX77843_MUIC_ADC_TTY_CONVERTER: + case MAX77843_MUIC_ADC_UART_CABLE: + case MAX77843_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX77843_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX77843_MUIC_ADC_CEA936A_TYPE2_CHG: + case MAX77843_MUIC_ADC_FACTORY_MODE_UART_ON: + case MAX77843_MUIC_ADC_AUDIO_DEVICE_TYPE1: + case MAX77843_MUIC_ADC_OPEN: + dev_err(info->dev, + "accessory is %s but it isn't used (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s accessory (adc:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max77843_muic_chg_handler(struct max77843_muic_info *info) +{ + int ret, chg_type, gnd_type; + bool attached; + + chg_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_CHG, &attached); + + dev_dbg(info->dev, + "external connector is %s(chg_type:0x%x, prev_chg_type:0x%x)\n", + attached ? "attached" : "detached", + chg_type, info->prev_chg_type); + + switch (chg_type) { + case MAX77843_MUIC_CHG_USB: + ret = max77843_muic_set_path(info, CONTROL1_SW_USB, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "USB", attached); + break; + case MAX77843_MUIC_CHG_DOWNSTREAM: + ret = max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, + "CHARGER-DOWNSTREAM", attached); + break; + case MAX77843_MUIC_CHG_DEDICATED: + ret = max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "TA", attached); + break; + case MAX77843_MUIC_CHG_SPECIAL_500MA: + ret = max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "SLOW-CHAREGER", attached); + break; + case MAX77843_MUIC_CHG_SPECIAL_1A: + ret = max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + if (ret < 0) + return ret; + + extcon_set_cable_state(info->edev, "FAST-CHARGER", attached); + break; + case MAX77843_MUIC_CHG_GND: + gnd_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC_GND, &attached); + + /* Charger cable on MHL accessory is attach or detach */ + if (gnd_type == MAX77843_MUIC_GND_MHL_VB) + extcon_set_cable_state(info->edev, "MHL-TA", true); + else if (gnd_type == MAX77843_MUIC_GND_MHL) + extcon_set_cable_state(info->edev, "MHL-TA", false); + break; + case MAX77843_MUIC_CHG_NONE: + break; + default: + dev_err(info->dev, + "failed to detect %s accessory (chg_type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + + max77843_muic_set_path(info, CONTROL1_SW_OPEN, attached); + return -EINVAL; + } + + return 0; +} + +static void max77843_muic_irq_work(struct work_struct *work) +{ + struct max77843_muic_info *info = container_of(work, + struct max77843_muic_info, irq_work); + struct max77843 *max77843 = info->max77843; + int ret = 0; + + mutex_lock(&info->mutex); + + ret = regmap_bulk_read(max77843->regmap_muic, + MAX77843_MUIC_REG_STATUS1, info->status, + MAX77843_MUIC_STATUS_NUM); + if (ret) { + dev_err(info->dev, "Cannot read STATUS registers\n"); + mutex_unlock(&info->mutex); + return; + } + + if (info->irq_adc) { + ret = max77843_muic_adc_handler(info); + if (ret) + dev_err(info->dev, "Unknown cable type\n"); + info->irq_adc = false; + } + + if (info->irq_chg) { + ret = max77843_muic_chg_handler(info); + if (ret) + dev_err(info->dev, "Unknown charger type\n"); + info->irq_chg = false; + } + + mutex_unlock(&info->mutex); +} + +static irqreturn_t max77843_muic_irq_handler(int irq, void *data) +{ + struct max77843_muic_info *info = data; + int i, irq_type = -1; + + for (i = 0; i < ARRAY_SIZE(max77843_muic_irqs); i++) + if (irq == max77843_muic_irqs[i].virq) + irq_type = max77843_muic_irqs[i].irq; + + switch (irq_type) { + case MAX77843_MUIC_IRQ_INT1_ADC: + case MAX77843_MUIC_IRQ_INT1_ADCERROR: + case MAX77843_MUIC_IRQ_INT1_ADC1K: + info->irq_adc = true; + break; + case MAX77843_MUIC_IRQ_INT2_CHGTYP: + case MAX77843_MUIC_IRQ_INT2_CHGDETRUN: + case MAX77843_MUIC_IRQ_INT2_DCDTMR: + case MAX77843_MUIC_IRQ_INT2_DXOVP: + case MAX77843_MUIC_IRQ_INT2_VBVOLT: + info->irq_chg = true; + break; + case MAX77843_MUIC_IRQ_INT3_VBADC: + case MAX77843_MUIC_IRQ_INT3_VDNMON: + case MAX77843_MUIC_IRQ_INT3_DNRES: + case MAX77843_MUIC_IRQ_INT3_MPNACK: + case MAX77843_MUIC_IRQ_INT3_MRXBUFOW: + case MAX77843_MUIC_IRQ_INT3_MRXTRF: + case MAX77843_MUIC_IRQ_INT3_MRXPERR: + case MAX77843_MUIC_IRQ_INT3_MRXRDY: + break; + default: + dev_err(info->dev, "Cannot recognize IRQ(%d)\n", irq_type); + break; + } + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static void max77843_muic_detect_cable_wq(struct work_struct *work) +{ + struct max77843_muic_info *info = container_of(to_delayed_work(work), + struct max77843_muic_info, wq_detcable); + struct max77843 *max77843 = info->max77843; + int chg_type, adc, ret; + bool attached; + + mutex_lock(&info->mutex); + + ret = regmap_bulk_read(max77843->regmap_muic, + MAX77843_MUIC_REG_STATUS1, info->status, + MAX77843_MUIC_STATUS_NUM); + if (ret) { + dev_err(info->dev, "Cannot read STATUS registers\n"); + goto err_cable_wq; + } + + adc = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_ADC, &attached); + if (attached && adc != MAX77843_MUIC_ADC_OPEN) { + ret = max77843_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect accessory\n"); + goto err_cable_wq; + } + } + + chg_type = max77843_muic_get_cable_type(info, + MAX77843_CABLE_GROUP_CHG, &attached); + if (attached && chg_type != MAX77843_MUIC_CHG_NONE) { + ret = max77843_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger accessory\n"); + goto err_cable_wq; + } + } + +err_cable_wq: + mutex_unlock(&info->mutex); +} + +static int max77843_muic_set_debounce_time(struct max77843_muic_info *info, + enum max77843_muic_adc_debounce_time time) +{ + struct max77843 *max77843 = info->max77843; + int ret; + + switch (time) { + case MAX77843_DEBOUNCE_TIME_5MS: + case MAX77843_DEBOUNCE_TIME_10MS: + case MAX77843_DEBOUNCE_TIME_25MS: + case MAX77843_DEBOUNCE_TIME_38_62MS: + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL4, + MAX77843_MUIC_CONTROL4_ADCDBSET_MASK, + time << CONTROL4_ADCDBSET_SHIFT); + if (ret < 0) { + dev_err(info->dev, "Cannot write MUIC regmap\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +} + +static int max77843_init_muic_regmap(struct max77843 *max77843) +{ + int ret; + + max77843->i2c_muic = i2c_new_dummy(max77843->i2c->adapter, + I2C_ADDR_MUIC); + if (!max77843->i2c_muic) { + dev_err(&max77843->i2c->dev, + "Cannot allocate I2C device for MUIC\n"); + return -ENOMEM; + } + + i2c_set_clientdata(max77843->i2c_muic, max77843); + + max77843->regmap_muic = devm_regmap_init_i2c(max77843->i2c_muic, + &max77843_muic_regmap_config); + if (IS_ERR(max77843->regmap_muic)) { + ret = PTR_ERR(max77843->regmap_muic); + goto err_muic_i2c; + } + + ret = regmap_add_irq_chip(max77843->regmap_muic, max77843->irq, + IRQF_TRIGGER_LOW | IRQF_ONESHOT | IRQF_SHARED, + 0, &max77843_muic_irq_chip, &max77843->irq_data_muic); + if (ret < 0) { + dev_err(&max77843->i2c->dev, "Cannot add MUIC IRQ chip\n"); + goto err_muic_i2c; + } + + return 0; + +err_muic_i2c: + i2c_unregister_device(max77843->i2c_muic); + + return ret; +} + +static int max77843_muic_probe(struct platform_device *pdev) +{ + struct max77843 *max77843 = dev_get_drvdata(pdev->dev.parent); + struct max77843_muic_info *info; + unsigned int id; + int i, ret; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->max77843 = max77843; + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + /* Initialize i2c and regmap */ + ret = max77843_init_muic_regmap(max77843); + if (ret) { + dev_err(&pdev->dev, "Failed to init MUIC regmap\n"); + return ret; + } + + /* Turn off auto detection configuration */ + ret = regmap_update_bits(max77843->regmap_muic, + MAX77843_MUIC_REG_CONTROL4, + MAX77843_MUIC_CONTROL4_USBAUTO_MASK | + MAX77843_MUIC_CONTROL4_FCTAUTO_MASK, + CONTROL4_AUTO_DISABLE); + + /* Initialize extcon device */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, + max77843_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "Failed to allocate memory for extcon\n"); + ret = -ENODEV; + goto err_muic_irq; + } + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "Failed to register extcon device\n"); + goto err_muic_irq; + } + + /* Set ADC debounce time */ + max77843_muic_set_debounce_time(info, MAX77843_DEBOUNCE_TIME_25MS); + + /* Set initial path for UART */ + max77843_muic_set_path(info, CONTROL1_SW_UART, true); + + /* Check revision number of MUIC device */ + ret = regmap_read(max77843->regmap_muic, MAX77843_MUIC_REG_ID, &id); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to read revision number\n"); + goto err_muic_irq; + } + dev_info(info->dev, "MUIC device ID : 0x%x\n", id); + + /* Support virtual irq domain for max77843 MUIC device */ + INIT_WORK(&info->irq_work, max77843_muic_irq_work); + + for (i = 0; i < ARRAY_SIZE(max77843_muic_irqs); i++) { + struct max77843_muic_irq *muic_irq = &max77843_muic_irqs[i]; + unsigned int virq = 0; + + virq = regmap_irq_get_virq(max77843->irq_data_muic, + muic_irq->irq); + if (virq <= 0) { + ret = -EINVAL; + goto err_muic_irq; + } + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(&pdev->dev, virq, NULL, + max77843_muic_irq_handler, IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "Failed to request irq (IRQ: %d, error: %d)\n", + muic_irq->irq, ret); + goto err_muic_irq; + } + } + + /* Detect accessory after completing the initialization of platform */ + INIT_DELAYED_WORK(&info->wq_detcable, max77843_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, + &info->wq_detcable, msecs_to_jiffies(DELAY_MS_DEFAULT)); + + return 0; + +err_muic_irq: + regmap_del_irq_chip(max77843->irq, max77843->irq_data_muic); + i2c_unregister_device(max77843->i2c_muic); + + return ret; +} + +static int max77843_muic_remove(struct platform_device *pdev) +{ + struct max77843_muic_info *info = platform_get_drvdata(pdev); + struct max77843 *max77843 = info->max77843; + + cancel_work_sync(&info->irq_work); + regmap_del_irq_chip(max77843->irq, max77843->irq_data_muic); + i2c_unregister_device(max77843->i2c_muic); + + return 0; +} + +static const struct platform_device_id max77843_muic_id[] = { + { "max77843-muic", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(platform, max77843_muic_id); + +static struct platform_driver max77843_muic_driver = { + .driver = { + .name = "max77843-muic", + }, + .probe = max77843_muic_probe, + .remove = max77843_muic_remove, + .id_table = max77843_muic_id, +}; + +static int __init max77843_muic_init(void) +{ + return platform_driver_register(&max77843_muic_driver); +} +subsys_initcall(max77843_muic_init); + +MODULE_DESCRIPTION("Maxim MAX77843 Extcon driver"); +MODULE_AUTHOR("Jaewon Kim <jaewon02.kim@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-max8997.c b/drivers/extcon/extcon-max8997.c new file mode 100644 index 000000000..5774e56c6 --- /dev/null +++ b/drivers/extcon/extcon-max8997.c @@ -0,0 +1,801 @@ +/* + * extcon-max8997.c - MAX8997 extcon driver to support MAX8997 MUIC + * + * Copyright (C) 2012 Samsung Electronics + * Donggeun Kim <dg77.kim@samsung.com> + * + * 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/platform_device.h> +#include <linux/kobject.h> +#include <linux/mfd/max8997.h> +#include <linux/mfd/max8997-private.h> +#include <linux/extcon.h> +#include <linux/irqdomain.h> + +#define DEV_NAME "max8997-muic" +#define DELAY_MS_DEFAULT 20000 /* unit: millisecond */ + +enum max8997_muic_adc_debounce_time { + ADC_DEBOUNCE_TIME_0_5MS = 0, /* 0.5ms */ + ADC_DEBOUNCE_TIME_10MS, /* 10ms */ + ADC_DEBOUNCE_TIME_25MS, /* 25ms */ + ADC_DEBOUNCE_TIME_38_62MS, /* 38.62ms */ +}; + +struct max8997_muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +static struct max8997_muic_irq muic_irqs[] = { + { MAX8997_MUICIRQ_ADCError, "muic-ADCERROR" }, + { MAX8997_MUICIRQ_ADCLow, "muic-ADCLOW" }, + { MAX8997_MUICIRQ_ADC, "muic-ADC" }, + { MAX8997_MUICIRQ_VBVolt, "muic-VBVOLT" }, + { MAX8997_MUICIRQ_DBChg, "muic-DBCHG" }, + { MAX8997_MUICIRQ_DCDTmr, "muic-DCDTMR" }, + { MAX8997_MUICIRQ_ChgDetRun, "muic-CHGDETRUN" }, + { MAX8997_MUICIRQ_ChgTyp, "muic-CHGTYP" }, + { MAX8997_MUICIRQ_OVP, "muic-OVP" }, +}; + +/* Define supported cable type */ +enum max8997_muic_acc_type { + MAX8997_MUIC_ADC_GROUND = 0x0, + MAX8997_MUIC_ADC_MHL, /* MHL*/ + MAX8997_MUIC_ADC_REMOTE_S1_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S2_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S3_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S4_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S5_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S6_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S7_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S8_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S9_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S10_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S11_BUTTON, + MAX8997_MUIC_ADC_REMOTE_S12_BUTTON, + MAX8997_MUIC_ADC_RESERVED_ACC_1, + MAX8997_MUIC_ADC_RESERVED_ACC_2, + MAX8997_MUIC_ADC_RESERVED_ACC_3, + MAX8997_MUIC_ADC_RESERVED_ACC_4, + MAX8997_MUIC_ADC_RESERVED_ACC_5, + MAX8997_MUIC_ADC_CEA936_AUDIO, + MAX8997_MUIC_ADC_PHONE_POWERED_DEV, + MAX8997_MUIC_ADC_TTY_CONVERTER, + MAX8997_MUIC_ADC_UART_CABLE, + MAX8997_MUIC_ADC_CEA936A_TYPE1_CHG, + MAX8997_MUIC_ADC_FACTORY_MODE_USB_OFF, /* JIG-USB-OFF */ + MAX8997_MUIC_ADC_FACTORY_MODE_USB_ON, /* JIG-USB-ON */ + MAX8997_MUIC_ADC_AV_CABLE_NOLOAD, /* DESKDOCK */ + MAX8997_MUIC_ADC_CEA936A_TYPE2_CHG, + MAX8997_MUIC_ADC_FACTORY_MODE_UART_OFF, /* JIG-UART */ + MAX8997_MUIC_ADC_FACTORY_MODE_UART_ON, /* CARDOCK */ + MAX8997_MUIC_ADC_AUDIO_MODE_REMOTE, + MAX8997_MUIC_ADC_OPEN, /* OPEN */ +}; + +enum max8997_muic_cable_group { + MAX8997_CABLE_GROUP_ADC = 0, + MAX8997_CABLE_GROUP_ADC_GND, + MAX8997_CABLE_GROUP_CHG, + MAX8997_CABLE_GROUP_VBVOLT, +}; + +enum max8997_muic_usb_type { + MAX8997_USB_HOST, + MAX8997_USB_DEVICE, +}; + +enum max8997_muic_charger_type { + MAX8997_CHARGER_TYPE_NONE = 0, + MAX8997_CHARGER_TYPE_USB, + MAX8997_CHARGER_TYPE_DOWNSTREAM_PORT, + MAX8997_CHARGER_TYPE_DEDICATED_CHG, + MAX8997_CHARGER_TYPE_500MA, + MAX8997_CHARGER_TYPE_1A, + MAX8997_CHARGER_TYPE_DEAD_BATTERY = 7, +}; + +struct max8997_muic_info { + struct device *dev; + struct i2c_client *muic; + struct extcon_dev *edev; + int prev_cable_type; + int prev_chg_type; + u8 status[2]; + + int irq; + struct work_struct irq_work; + struct mutex mutex; + + struct max8997_muic_platform_data *muic_pdata; + enum max8997_muic_charger_type pre_charger_type; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + int path_usb; + int path_uart; +}; + +enum { + EXTCON_CABLE_USB = 0, + EXTCON_CABLE_USB_HOST, + EXTCON_CABLE_TA, + EXTCON_CABLE_FAST_CHARGER, + EXTCON_CABLE_SLOW_CHARGER, + EXTCON_CABLE_CHARGE_DOWNSTREAM, + EXTCON_CABLE_MHL, + EXTCON_CABLE_DOCK_DESK, + EXTCON_CABLE_DOCK_CARD, + EXTCON_CABLE_JIG, + + _EXTCON_CABLE_NUM, +}; + +static const char *max8997_extcon_cable[] = { + [EXTCON_CABLE_USB] = "USB", + [EXTCON_CABLE_USB_HOST] = "USB-Host", + [EXTCON_CABLE_TA] = "TA", + [EXTCON_CABLE_FAST_CHARGER] = "Fast-charger", + [EXTCON_CABLE_SLOW_CHARGER] = "Slow-charger", + [EXTCON_CABLE_CHARGE_DOWNSTREAM] = "Charge-downstream", + [EXTCON_CABLE_MHL] = "MHL", + [EXTCON_CABLE_DOCK_DESK] = "Dock-Desk", + [EXTCON_CABLE_DOCK_CARD] = "Dock-Card", + [EXTCON_CABLE_JIG] = "JIG", + + NULL, +}; + +/* + * max8997_muic_set_debounce_time - Set the debounce time of ADC + * @info: the instance including private data of max8997 MUIC + * @time: the debounce time of ADC + */ +static int max8997_muic_set_debounce_time(struct max8997_muic_info *info, + enum max8997_muic_adc_debounce_time time) +{ + int ret; + + switch (time) { + case ADC_DEBOUNCE_TIME_0_5MS: + case ADC_DEBOUNCE_TIME_10MS: + case ADC_DEBOUNCE_TIME_25MS: + case ADC_DEBOUNCE_TIME_38_62MS: + ret = max8997_update_reg(info->muic, + MAX8997_MUIC_REG_CONTROL3, + time << CONTROL3_ADCDBSET_SHIFT, + CONTROL3_ADCDBSET_MASK); + if (ret) { + dev_err(info->dev, "failed to set ADC debounce time\n"); + return ret; + } + break; + default: + dev_err(info->dev, "invalid ADC debounce time\n"); + return -EINVAL; + } + + return 0; +}; + +/* + * max8997_muic_set_path - Set hardware line according to attached cable + * @info: the instance including private data of max8997 MUIC + * @value: the path according to attached cable + * @attached: the state of cable (true:attached, false:detached) + * + * The max8997 MUIC device share outside H/W line among a varity of cables, + * so this function set internal path of H/W line according to the type of + * attached cable. + */ +static int max8997_muic_set_path(struct max8997_muic_info *info, + u8 val, bool attached) +{ + int ret = 0; + u8 ctrl1, ctrl2 = 0; + + if (attached) + ctrl1 = val; + else + ctrl1 = CONTROL1_SW_OPEN; + + ret = max8997_update_reg(info->muic, + MAX8997_MUIC_REG_CONTROL1, ctrl1, COMP_SW_MASK); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + if (attached) + ctrl2 |= CONTROL2_CPEN_MASK; /* LowPwr=0, CPEn=1 */ + else + ctrl2 |= CONTROL2_LOWPWR_MASK; /* LowPwr=1, CPEn=0 */ + + ret = max8997_update_reg(info->muic, + MAX8997_MUIC_REG_CONTROL2, ctrl2, + CONTROL2_LOWPWR_MASK | CONTROL2_CPEN_MASK); + if (ret < 0) { + dev_err(info->dev, "failed to update MUIC register\n"); + return ret; + } + + dev_info(info->dev, + "CONTROL1 : 0x%02x, CONTROL2 : 0x%02x, state : %s\n", + ctrl1, ctrl2, attached ? "attached" : "detached"); + + return 0; +} + +/* + * max8997_muic_get_cable_type - Return cable type and check cable state + * @info: the instance including private data of max8997 MUIC + * @group: the path according to attached cable + * @attached: store cable state and return + * + * This function check the cable state either attached or detached, + * and then divide precise type of cable according to cable group. + * - MAX8997_CABLE_GROUP_ADC + * - MAX8997_CABLE_GROUP_CHG + */ +static int max8997_muic_get_cable_type(struct max8997_muic_info *info, + enum max8997_muic_cable_group group, bool *attached) +{ + int cable_type = 0; + int adc; + int chg_type; + + switch (group) { + case MAX8997_CABLE_GROUP_ADC: + /* + * Read ADC value to check cable type and decide cable state + * according to cable type + */ + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + /* + * Check current cable state/cable type and store cable type + * (info->prev_cable_type) for handling cable when cable is + * detached. + */ + if (adc == MAX8997_MUIC_ADC_OPEN) { + *attached = false; + + cable_type = info->prev_cable_type; + info->prev_cable_type = MAX8997_MUIC_ADC_OPEN; + } else { + *attached = true; + + cable_type = info->prev_cable_type = adc; + } + break; + case MAX8997_CABLE_GROUP_CHG: + /* + * Read charger type to check cable type and decide cable state + * according to type of charger cable. + */ + chg_type = info->status[1] & STATUS2_CHGTYP_MASK; + chg_type >>= STATUS2_CHGTYP_SHIFT; + + if (chg_type == MAX8997_CHARGER_TYPE_NONE) { + *attached = false; + + cable_type = info->prev_chg_type; + info->prev_chg_type = MAX8997_CHARGER_TYPE_NONE; + } else { + *attached = true; + + /* + * Check current cable state/cable type and store cable + * type(info->prev_chg_type) for handling cable when + * charger cable is detached. + */ + cable_type = info->prev_chg_type = chg_type; + } + + break; + default: + dev_err(info->dev, "Unknown cable group (%d)\n", group); + cable_type = -EINVAL; + break; + } + + return cable_type; +} + +static int max8997_muic_handle_usb(struct max8997_muic_info *info, + enum max8997_muic_usb_type usb_type, bool attached) +{ + int ret = 0; + + if (usb_type == MAX8997_USB_HOST) { + ret = max8997_muic_set_path(info, info->path_usb, attached); + if (ret < 0) { + dev_err(info->dev, "failed to update muic register\n"); + return ret; + } + } + + switch (usb_type) { + case MAX8997_USB_HOST: + extcon_set_cable_state(info->edev, "USB-Host", attached); + break; + case MAX8997_USB_DEVICE: + extcon_set_cable_state(info->edev, "USB", attached); + break; + default: + dev_err(info->dev, "failed to detect %s usb cable\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + return 0; +} + +static int max8997_muic_handle_dock(struct max8997_muic_info *info, + int cable_type, bool attached) +{ + int ret = 0; + + ret = max8997_muic_set_path(info, CONTROL1_SW_AUDIO, attached); + if (ret) { + dev_err(info->dev, "failed to update muic register\n"); + return ret; + } + + switch (cable_type) { + case MAX8997_MUIC_ADC_AV_CABLE_NOLOAD: + extcon_set_cable_state(info->edev, "Dock-desk", attached); + break; + case MAX8997_MUIC_ADC_FACTORY_MODE_UART_ON: + extcon_set_cable_state(info->edev, "Dock-card", attached); + break; + default: + dev_err(info->dev, "failed to detect %s dock device\n", + attached ? "attached" : "detached"); + return -EINVAL; + } + + return 0; +} + +static int max8997_muic_handle_jig_uart(struct max8997_muic_info *info, + bool attached) +{ + int ret = 0; + + /* switch to UART */ + ret = max8997_muic_set_path(info, info->path_uart, attached); + if (ret) { + dev_err(info->dev, "failed to update muic register\n"); + return ret; + } + + extcon_set_cable_state(info->edev, "JIG", attached); + + return 0; +} + +static int max8997_muic_adc_handler(struct max8997_muic_info *info) +{ + int cable_type; + bool attached; + int ret = 0; + + /* Check cable state which is either detached or attached */ + cable_type = max8997_muic_get_cable_type(info, + MAX8997_CABLE_GROUP_ADC, &attached); + + switch (cable_type) { + case MAX8997_MUIC_ADC_GROUND: + ret = max8997_muic_handle_usb(info, MAX8997_USB_HOST, attached); + if (ret < 0) + return ret; + break; + case MAX8997_MUIC_ADC_MHL: + extcon_set_cable_state(info->edev, "MHL", attached); + break; + case MAX8997_MUIC_ADC_FACTORY_MODE_USB_OFF: + case MAX8997_MUIC_ADC_FACTORY_MODE_USB_ON: + ret = max8997_muic_handle_usb(info, + MAX8997_USB_DEVICE, attached); + if (ret < 0) + return ret; + break; + case MAX8997_MUIC_ADC_AV_CABLE_NOLOAD: + case MAX8997_MUIC_ADC_FACTORY_MODE_UART_ON: + ret = max8997_muic_handle_dock(info, cable_type, attached); + if (ret < 0) + return ret; + break; + case MAX8997_MUIC_ADC_FACTORY_MODE_UART_OFF: + ret = max8997_muic_handle_jig_uart(info, attached); + break; + case MAX8997_MUIC_ADC_REMOTE_S1_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S2_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S3_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S4_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S5_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S6_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S7_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S8_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S9_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S10_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S11_BUTTON: + case MAX8997_MUIC_ADC_REMOTE_S12_BUTTON: + case MAX8997_MUIC_ADC_RESERVED_ACC_1: + case MAX8997_MUIC_ADC_RESERVED_ACC_2: + case MAX8997_MUIC_ADC_RESERVED_ACC_3: + case MAX8997_MUIC_ADC_RESERVED_ACC_4: + case MAX8997_MUIC_ADC_RESERVED_ACC_5: + case MAX8997_MUIC_ADC_CEA936_AUDIO: + case MAX8997_MUIC_ADC_PHONE_POWERED_DEV: + case MAX8997_MUIC_ADC_TTY_CONVERTER: + case MAX8997_MUIC_ADC_UART_CABLE: + case MAX8997_MUIC_ADC_CEA936A_TYPE1_CHG: + case MAX8997_MUIC_ADC_CEA936A_TYPE2_CHG: + case MAX8997_MUIC_ADC_AUDIO_MODE_REMOTE: + /* + * This cable isn't used in general case if it is specially + * needed to detect additional cable, should implement + * proper operation when this cable is attached/detached. + */ + dev_info(info->dev, + "cable is %s but it isn't used (type:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EAGAIN; + default: + dev_err(info->dev, + "failed to detect %s unknown cable (type:0x%x)\n", + attached ? "attached" : "detached", cable_type); + return -EINVAL; + } + + return 0; +} + +static int max8997_muic_chg_handler(struct max8997_muic_info *info) +{ + int chg_type; + bool attached; + int adc; + + chg_type = max8997_muic_get_cable_type(info, + MAX8997_CABLE_GROUP_CHG, &attached); + + switch (chg_type) { + case MAX8997_CHARGER_TYPE_NONE: + break; + case MAX8997_CHARGER_TYPE_USB: + adc = info->status[0] & STATUS1_ADC_MASK; + adc >>= STATUS1_ADC_SHIFT; + + if ((adc & STATUS1_ADC_MASK) == MAX8997_MUIC_ADC_OPEN) { + max8997_muic_handle_usb(info, + MAX8997_USB_DEVICE, attached); + } + break; + case MAX8997_CHARGER_TYPE_DOWNSTREAM_PORT: + extcon_set_cable_state(info->edev, + "Charge-downstream", attached); + break; + case MAX8997_CHARGER_TYPE_DEDICATED_CHG: + extcon_set_cable_state(info->edev, "TA", attached); + break; + case MAX8997_CHARGER_TYPE_500MA: + extcon_set_cable_state(info->edev, "Slow-charger", attached); + break; + case MAX8997_CHARGER_TYPE_1A: + extcon_set_cable_state(info->edev, "Fast-charger", attached); + break; + default: + dev_err(info->dev, + "failed to detect %s unknown chg cable (type:0x%x)\n", + attached ? "attached" : "detached", chg_type); + return -EINVAL; + } + + return 0; +} + +static void max8997_muic_irq_work(struct work_struct *work) +{ + struct max8997_muic_info *info = container_of(work, + struct max8997_muic_info, irq_work); + int irq_type = 0; + int i, ret; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) + if (info->irq == muic_irqs[i].virq) + irq_type = muic_irqs[i].irq; + + ret = max8997_bulk_read(info->muic, MAX8997_MUIC_REG_STATUS1, + 2, info->status); + if (ret) { + dev_err(info->dev, "failed to read muic register\n"); + mutex_unlock(&info->mutex); + return; + } + + switch (irq_type) { + case MAX8997_MUICIRQ_ADCError: + case MAX8997_MUICIRQ_ADCLow: + case MAX8997_MUICIRQ_ADC: + /* Handle all of cable except for charger cable */ + ret = max8997_muic_adc_handler(info); + break; + case MAX8997_MUICIRQ_VBVolt: + case MAX8997_MUICIRQ_DBChg: + case MAX8997_MUICIRQ_DCDTmr: + case MAX8997_MUICIRQ_ChgDetRun: + case MAX8997_MUICIRQ_ChgTyp: + /* Handle charger cable */ + ret = max8997_muic_chg_handler(info); + break; + case MAX8997_MUICIRQ_OVP: + break; + default: + dev_info(info->dev, "misc interrupt: irq %d occurred\n", + irq_type); + mutex_unlock(&info->mutex); + return; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +static irqreturn_t max8997_muic_irq_handler(int irq, void *data) +{ + struct max8997_muic_info *info = data; + + dev_dbg(info->dev, "irq:%d\n", irq); + info->irq = irq; + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static int max8997_muic_detect_dev(struct max8997_muic_info *info) +{ + int ret = 0; + int adc; + int chg_type; + bool attached; + + mutex_lock(&info->mutex); + + /* Read STATUSx register to detect accessory */ + ret = max8997_bulk_read(info->muic, + MAX8997_MUIC_REG_STATUS1, 2, info->status); + if (ret) { + dev_err(info->dev, "failed to read MUIC register\n"); + mutex_unlock(&info->mutex); + return ret; + } + + adc = max8997_muic_get_cable_type(info, MAX8997_CABLE_GROUP_ADC, + &attached); + if (attached && adc != MAX8997_MUIC_ADC_OPEN) { + ret = max8997_muic_adc_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect ADC cable\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + chg_type = max8997_muic_get_cable_type(info, MAX8997_CABLE_GROUP_CHG, + &attached); + if (attached && chg_type != MAX8997_CHARGER_TYPE_NONE) { + ret = max8997_muic_chg_handler(info); + if (ret < 0) { + dev_err(info->dev, "Cannot detect charger cable\n"); + mutex_unlock(&info->mutex); + return ret; + } + } + + mutex_unlock(&info->mutex); + + return 0; +} + +static void max8997_muic_detect_cable_wq(struct work_struct *work) +{ + struct max8997_muic_info *info = container_of(to_delayed_work(work), + struct max8997_muic_info, wq_detcable); + int ret; + + ret = max8997_muic_detect_dev(info); + if (ret < 0) + dev_err(info->dev, "failed to detect cable type\n"); +} + +static int max8997_muic_probe(struct platform_device *pdev) +{ + struct max8997_dev *max8997 = dev_get_drvdata(pdev->dev.parent); + struct max8997_platform_data *pdata = dev_get_platdata(max8997->dev); + struct max8997_muic_info *info; + int delay_jiffies; + int ret, i; + + info = devm_kzalloc(&pdev->dev, sizeof(struct max8997_muic_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = &pdev->dev; + info->muic = max8997->muic; + + platform_set_drvdata(pdev, info); + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, max8997_muic_irq_work); + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) { + struct max8997_muic_irq *muic_irq = &muic_irqs[i]; + unsigned int virq = 0; + + virq = irq_create_mapping(max8997->irq_domain, muic_irq->irq); + if (!virq) { + ret = -EINVAL; + goto err_irq; + } + muic_irq->virq = virq; + + ret = request_threaded_irq(virq, NULL, + max8997_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(&pdev->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + goto err_irq; + } + } + + /* External connector */ + info->edev = devm_extcon_dev_allocate(&pdev->dev, max8997_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); + ret = -ENOMEM; + goto err_irq; + } + info->edev->name = DEV_NAME; + + ret = devm_extcon_dev_register(&pdev->dev, info->edev); + if (ret) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + goto err_irq; + } + + if (pdata && pdata->muic_pdata) { + struct max8997_muic_platform_data *muic_pdata + = pdata->muic_pdata; + + /* Initialize registers according to platform data */ + for (i = 0; i < muic_pdata->num_init_data; i++) { + max8997_write_reg(info->muic, + muic_pdata->init_data[i].addr, + muic_pdata->init_data[i].data); + } + + /* + * Default usb/uart path whether UART/USB or AUX_UART/AUX_USB + * h/w path of COMP2/COMN1 on CONTROL1 register. + */ + if (muic_pdata->path_uart) + info->path_uart = muic_pdata->path_uart; + else + info->path_uart = CONTROL1_SW_UART; + + if (muic_pdata->path_usb) + info->path_usb = muic_pdata->path_usb; + else + info->path_usb = CONTROL1_SW_USB; + + /* + * Default delay time for detecting cable state + * after certain time. + */ + if (muic_pdata->detcable_delay_ms) + delay_jiffies = + msecs_to_jiffies(muic_pdata->detcable_delay_ms); + else + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } else { + info->path_uart = CONTROL1_SW_UART; + info->path_usb = CONTROL1_SW_USB; + delay_jiffies = msecs_to_jiffies(DELAY_MS_DEFAULT); + } + + /* Set initial path for UART */ + max8997_muic_set_path(info, info->path_uart, true); + + /* Set ADC debounce time */ + max8997_muic_set_debounce_time(info, ADC_DEBOUNCE_TIME_25MS); + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, max8997_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + delay_jiffies); + + return 0; + +err_irq: + while (--i >= 0) + free_irq(muic_irqs[i].virq, info); + return ret; +} + +static int max8997_muic_remove(struct platform_device *pdev) +{ + struct max8997_muic_info *info = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < ARRAY_SIZE(muic_irqs); i++) + free_irq(muic_irqs[i].virq, info); + cancel_work_sync(&info->irq_work); + + return 0; +} + +static struct platform_driver max8997_muic_driver = { + .driver = { + .name = DEV_NAME, + }, + .probe = max8997_muic_probe, + .remove = max8997_muic_remove, +}; + +module_platform_driver(max8997_muic_driver); + +MODULE_DESCRIPTION("Maxim MAX8997 Extcon driver"); +MODULE_AUTHOR("Donggeun Kim <dg77.kim@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-palmas.c b/drivers/extcon/extcon-palmas.c new file mode 100644 index 000000000..11c6757b6 --- /dev/null +++ b/drivers/extcon/extcon-palmas.c @@ -0,0 +1,304 @@ +/* + * Palmas USB transceiver driver + * + * Copyright (C) 2013 Texas Instruments Incorporated - http://www.ti.com + * 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. + * + * Author: Graeme Gregory <gg@slimlogic.co.uk> + * Author: Kishon Vijay Abraham I <kishon@ti.com> + * + * Based on twl6030_usb.c + * + * Author: Hema HK <hemahk@ti.com> + * + * 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/module.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/err.h> +#include <linux/mfd/palmas.h> +#include <linux/of.h> +#include <linux/of_platform.h> + +static const char *palmas_extcon_cable[] = { + [0] = "USB", + [1] = "USB-HOST", + NULL, +}; + +static const int mutually_exclusive[] = {0x3, 0x0}; + +static void palmas_usb_wakeup(struct palmas *palmas, int enable) +{ + if (enable) + palmas_write(palmas, PALMAS_USB_OTG_BASE, PALMAS_USB_WAKEUP, + PALMAS_USB_WAKEUP_ID_WK_UP_COMP); + else + palmas_write(palmas, PALMAS_USB_OTG_BASE, PALMAS_USB_WAKEUP, 0); +} + +static irqreturn_t palmas_vbus_irq_handler(int irq, void *_palmas_usb) +{ + struct palmas_usb *palmas_usb = _palmas_usb; + unsigned int vbus_line_state; + + palmas_read(palmas_usb->palmas, PALMAS_INTERRUPT_BASE, + PALMAS_INT3_LINE_STATE, &vbus_line_state); + + if (vbus_line_state & PALMAS_INT3_LINE_STATE_VBUS) { + if (palmas_usb->linkstat != PALMAS_USB_STATE_VBUS) { + palmas_usb->linkstat = PALMAS_USB_STATE_VBUS; + extcon_set_cable_state(palmas_usb->edev, "USB", true); + dev_info(palmas_usb->dev, "USB cable is attached\n"); + } else { + dev_dbg(palmas_usb->dev, + "Spurious connect event detected\n"); + } + } else if (!(vbus_line_state & PALMAS_INT3_LINE_STATE_VBUS)) { + if (palmas_usb->linkstat == PALMAS_USB_STATE_VBUS) { + palmas_usb->linkstat = PALMAS_USB_STATE_DISCONNECT; + extcon_set_cable_state(palmas_usb->edev, "USB", false); + dev_info(palmas_usb->dev, "USB cable is detached\n"); + } else { + dev_dbg(palmas_usb->dev, + "Spurious disconnect event detected\n"); + } + } + + return IRQ_HANDLED; +} + +static irqreturn_t palmas_id_irq_handler(int irq, void *_palmas_usb) +{ + unsigned int set, id_src; + struct palmas_usb *palmas_usb = _palmas_usb; + + palmas_read(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_LATCH_SET, &set); + palmas_read(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_SRC, &id_src); + + if ((set & PALMAS_USB_ID_INT_SRC_ID_GND) && + (id_src & PALMAS_USB_ID_INT_SRC_ID_GND)) { + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_LATCH_CLR, + PALMAS_USB_ID_INT_EN_HI_CLR_ID_GND); + palmas_usb->linkstat = PALMAS_USB_STATE_ID; + extcon_set_cable_state(palmas_usb->edev, "USB-HOST", true); + dev_info(palmas_usb->dev, "USB-HOST cable is attached\n"); + } else if ((set & PALMAS_USB_ID_INT_SRC_ID_FLOAT) && + (id_src & PALMAS_USB_ID_INT_SRC_ID_FLOAT)) { + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_LATCH_CLR, + PALMAS_USB_ID_INT_EN_HI_CLR_ID_FLOAT); + palmas_usb->linkstat = PALMAS_USB_STATE_DISCONNECT; + extcon_set_cable_state(palmas_usb->edev, "USB-HOST", false); + dev_info(palmas_usb->dev, "USB-HOST cable is detached\n"); + } else if ((palmas_usb->linkstat == PALMAS_USB_STATE_ID) && + (!(set & PALMAS_USB_ID_INT_SRC_ID_GND))) { + palmas_usb->linkstat = PALMAS_USB_STATE_DISCONNECT; + extcon_set_cable_state(palmas_usb->edev, "USB-HOST", false); + dev_info(palmas_usb->dev, "USB-HOST cable is detached\n"); + } else if ((palmas_usb->linkstat == PALMAS_USB_STATE_DISCONNECT) && + (id_src & PALMAS_USB_ID_INT_SRC_ID_GND)) { + palmas_usb->linkstat = PALMAS_USB_STATE_ID; + extcon_set_cable_state(palmas_usb->edev, "USB-HOST", true); + dev_info(palmas_usb->dev, " USB-HOST cable is attached\n"); + } + + return IRQ_HANDLED; +} + +static void palmas_enable_irq(struct palmas_usb *palmas_usb) +{ + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_VBUS_CTRL_SET, + PALMAS_USB_VBUS_CTRL_SET_VBUS_ACT_COMP); + + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_CTRL_SET, PALMAS_USB_ID_CTRL_SET_ID_ACT_COMP); + + palmas_write(palmas_usb->palmas, PALMAS_USB_OTG_BASE, + PALMAS_USB_ID_INT_EN_HI_SET, + PALMAS_USB_ID_INT_EN_HI_SET_ID_GND | + PALMAS_USB_ID_INT_EN_HI_SET_ID_FLOAT); + + if (palmas_usb->enable_vbus_detection) + palmas_vbus_irq_handler(palmas_usb->vbus_irq, palmas_usb); + + /* cold plug for host mode needs this delay */ + if (palmas_usb->enable_id_detection) { + msleep(30); + palmas_id_irq_handler(palmas_usb->id_irq, palmas_usb); + } +} + +static int palmas_usb_probe(struct platform_device *pdev) +{ + struct palmas *palmas = dev_get_drvdata(pdev->dev.parent); + struct palmas_usb_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct device_node *node = pdev->dev.of_node; + struct palmas_usb *palmas_usb; + int status; + + palmas_usb = devm_kzalloc(&pdev->dev, sizeof(*palmas_usb), GFP_KERNEL); + if (!palmas_usb) + return -ENOMEM; + + if (node && !pdata) { + palmas_usb->wakeup = of_property_read_bool(node, "ti,wakeup"); + palmas_usb->enable_id_detection = of_property_read_bool(node, + "ti,enable-id-detection"); + palmas_usb->enable_vbus_detection = of_property_read_bool(node, + "ti,enable-vbus-detection"); + } else { + palmas_usb->wakeup = true; + palmas_usb->enable_id_detection = true; + palmas_usb->enable_vbus_detection = true; + + if (pdata) + palmas_usb->wakeup = pdata->wakeup; + } + + palmas->usb = palmas_usb; + palmas_usb->palmas = palmas; + + palmas_usb->dev = &pdev->dev; + + palmas_usb->id_otg_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_ID_OTG_IRQ); + palmas_usb->id_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_ID_IRQ); + palmas_usb->vbus_otg_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_VBUS_OTG_IRQ); + palmas_usb->vbus_irq = regmap_irq_get_virq(palmas->irq_data, + PALMAS_VBUS_IRQ); + + palmas_usb_wakeup(palmas, palmas_usb->wakeup); + + platform_set_drvdata(pdev, palmas_usb); + + palmas_usb->edev = devm_extcon_dev_allocate(&pdev->dev, + palmas_extcon_cable); + if (IS_ERR(palmas_usb->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + palmas_usb->edev->name = kstrdup(node->name, GFP_KERNEL); + palmas_usb->edev->mutually_exclusive = mutually_exclusive; + + status = devm_extcon_dev_register(&pdev->dev, palmas_usb->edev); + if (status) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + kfree(palmas_usb->edev->name); + return status; + } + + if (palmas_usb->enable_id_detection) { + status = devm_request_threaded_irq(palmas_usb->dev, + palmas_usb->id_irq, + NULL, palmas_id_irq_handler, + IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | + IRQF_ONESHOT | IRQF_EARLY_RESUME, + "palmas_usb_id", palmas_usb); + if (status < 0) { + dev_err(&pdev->dev, "can't get IRQ %d, err %d\n", + palmas_usb->id_irq, status); + kfree(palmas_usb->edev->name); + return status; + } + } + + if (palmas_usb->enable_vbus_detection) { + status = devm_request_threaded_irq(palmas_usb->dev, + palmas_usb->vbus_irq, NULL, + palmas_vbus_irq_handler, + IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | + IRQF_ONESHOT | IRQF_EARLY_RESUME, + "palmas_usb_vbus", palmas_usb); + if (status < 0) { + dev_err(&pdev->dev, "can't get IRQ %d, err %d\n", + palmas_usb->vbus_irq, status); + kfree(palmas_usb->edev->name); + return status; + } + } + + palmas_enable_irq(palmas_usb); + device_set_wakeup_capable(&pdev->dev, true); + return 0; +} + +static int palmas_usb_remove(struct platform_device *pdev) +{ + struct palmas_usb *palmas_usb = platform_get_drvdata(pdev); + + kfree(palmas_usb->edev->name); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int palmas_usb_suspend(struct device *dev) +{ + struct palmas_usb *palmas_usb = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) { + if (palmas_usb->enable_vbus_detection) + enable_irq_wake(palmas_usb->vbus_irq); + if (palmas_usb->enable_id_detection) + enable_irq_wake(palmas_usb->id_irq); + } + return 0; +} + +static int palmas_usb_resume(struct device *dev) +{ + struct palmas_usb *palmas_usb = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) { + if (palmas_usb->enable_vbus_detection) + disable_irq_wake(palmas_usb->vbus_irq); + if (palmas_usb->enable_id_detection) + disable_irq_wake(palmas_usb->id_irq); + } + return 0; +}; +#endif + +static SIMPLE_DEV_PM_OPS(palmas_pm_ops, palmas_usb_suspend, palmas_usb_resume); + +static const struct of_device_id of_palmas_match_tbl[] = { + { .compatible = "ti,palmas-usb", }, + { .compatible = "ti,palmas-usb-vid", }, + { .compatible = "ti,twl6035-usb", }, + { .compatible = "ti,twl6035-usb-vid", }, + { /* end */ } +}; + +static struct platform_driver palmas_usb_driver = { + .probe = palmas_usb_probe, + .remove = palmas_usb_remove, + .driver = { + .name = "palmas-usb", + .of_match_table = of_palmas_match_tbl, + .pm = &palmas_pm_ops, + }, +}; + +module_platform_driver(palmas_usb_driver); + +MODULE_ALIAS("platform:palmas-usb"); +MODULE_AUTHOR("Graeme Gregory <gg@slimlogic.co.uk>"); +MODULE_DESCRIPTION("Palmas USB transceiver driver"); +MODULE_LICENSE("GPL"); +MODULE_DEVICE_TABLE(of, of_palmas_match_tbl); diff --git a/drivers/extcon/extcon-rt8973a.c b/drivers/extcon/extcon-rt8973a.c new file mode 100644 index 000000000..9ccd5af89 --- /dev/null +++ b/drivers/extcon/extcon-rt8973a.c @@ -0,0 +1,738 @@ +/* + * extcon-rt8973a.c - Richtek RT8973A extcon driver to support USB switches + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + * Author: Chanwoo Choi <cw00.choi@samsung.com> + * + * 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. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/input.h> +#include <linux/interrupt.h> +#include <linux/irqdomain.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/extcon.h> + +#include "extcon-rt8973a.h" + +#define DELAY_MS_DEFAULT 20000 /* unit: millisecond */ + +struct muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +struct reg_data { + u8 reg; + u8 mask; + u8 val; + bool invert; +}; + +struct rt8973a_muic_info { + struct device *dev; + struct extcon_dev *edev; + + struct i2c_client *i2c; + struct regmap *regmap; + + struct regmap_irq_chip_data *irq_data; + struct muic_irq *muic_irqs; + unsigned int num_muic_irqs; + int irq; + bool irq_attach; + bool irq_detach; + bool irq_ovp; + bool irq_otp; + struct work_struct irq_work; + + struct reg_data *reg_data; + unsigned int num_reg_data; + bool auto_config; + + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; +}; + +/* Default value of RT8973A register to bring up MUIC device. */ +static struct reg_data rt8973a_reg_data[] = { + { + .reg = RT8973A_REG_CONTROL1, + .mask = RT8973A_REG_CONTROL1_ADC_EN_MASK + | RT8973A_REG_CONTROL1_USB_CHD_EN_MASK + | RT8973A_REG_CONTROL1_CHGTYP_MASK + | RT8973A_REG_CONTROL1_SWITCH_OPEN_MASK + | RT8973A_REG_CONTROL1_AUTO_CONFIG_MASK + | RT8973A_REG_CONTROL1_INTM_MASK, + .val = RT8973A_REG_CONTROL1_ADC_EN_MASK + | RT8973A_REG_CONTROL1_USB_CHD_EN_MASK + | RT8973A_REG_CONTROL1_CHGTYP_MASK, + .invert = false, + }, + { /* sentinel */ } +}; + +/* List of detectable cables */ +enum { + EXTCON_CABLE_USB = 0, + EXTCON_CABLE_USB_HOST, + EXTCON_CABLE_TA, + EXTCON_CABLE_JIG_OFF_USB, + EXTCON_CABLE_JIG_ON_USB, + EXTCON_CABLE_JIG_OFF_UART, + EXTCON_CABLE_JIG_ON_UART, + + EXTCON_CABLE_END, +}; + +static const char *rt8973a_extcon_cable[] = { + [EXTCON_CABLE_USB] = "USB", + [EXTCON_CABLE_USB_HOST] = "USB-Host", + [EXTCON_CABLE_TA] = "TA", + [EXTCON_CABLE_JIG_OFF_USB] = "JIG-USB-OFF", + [EXTCON_CABLE_JIG_ON_USB] = "JIG-USB-ON", + [EXTCON_CABLE_JIG_OFF_UART] = "JIG-UART-OFF", + [EXTCON_CABLE_JIG_ON_UART] = "JIG-UART-ON", + NULL, +}; + +/* Define OVP (Over Voltage Protection), OTP (Over Temperature Protection) */ +enum rt8973a_event_type { + RT8973A_EVENT_ATTACH = 1, + RT8973A_EVENT_DETACH, + RT8973A_EVENT_OVP, + RT8973A_EVENT_OTP, +}; + +/* Define supported accessory type */ +enum rt8973a_muic_acc_type { + RT8973A_MUIC_ADC_OTG = 0x0, + RT8973A_MUIC_ADC_AUDIO_SEND_END_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S1_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S2_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S3_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S4_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S5_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S6_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S7_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S8_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S9_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S10_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S11_BUTTON, + RT8973A_MUIC_ADC_AUDIO_REMOTE_S12_BUTTON, + RT8973A_MUIC_ADC_RESERVED_ACC_1, + RT8973A_MUIC_ADC_RESERVED_ACC_2, + RT8973A_MUIC_ADC_RESERVED_ACC_3, + RT8973A_MUIC_ADC_RESERVED_ACC_4, + RT8973A_MUIC_ADC_RESERVED_ACC_5, + RT8973A_MUIC_ADC_AUDIO_TYPE2, + RT8973A_MUIC_ADC_PHONE_POWERED_DEV, + RT8973A_MUIC_ADC_UNKNOWN_ACC_1, + RT8973A_MUIC_ADC_UNKNOWN_ACC_2, + RT8973A_MUIC_ADC_TA, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB, + RT8973A_MUIC_ADC_UNKNOWN_ACC_3, + RT8973A_MUIC_ADC_UNKNOWN_ACC_4, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART, + RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART, + RT8973A_MUIC_ADC_UNKNOWN_ACC_5, + RT8973A_MUIC_ADC_OPEN = 0x1f, + + /* The below accessories has same ADC value (0x1f). + So, Device type1 is used to separate specific accessory. */ + /* |---------|--ADC| */ + /* | [7:5]|[4:0]| */ + RT8973A_MUIC_ADC_USB = 0x3f, /* | 001|11111| */ +}; + +/* List of supported interrupt for RT8973A */ +static struct muic_irq rt8973a_muic_irqs[] = { + { RT8973A_INT1_ATTACH, "muic-attach" }, + { RT8973A_INT1_DETACH, "muic-detach" }, + { RT8973A_INT1_CHGDET, "muic-chgdet" }, + { RT8973A_INT1_DCD_T, "muic-dcd-t" }, + { RT8973A_INT1_OVP, "muic-ovp" }, + { RT8973A_INT1_CONNECT, "muic-connect" }, + { RT8973A_INT1_ADC_CHG, "muic-adc-chg" }, + { RT8973A_INT1_OTP, "muic-otp" }, + { RT8973A_INT2_UVLO, "muic-uvlo" }, + { RT8973A_INT2_POR, "muic-por" }, + { RT8973A_INT2_OTP_FET, "muic-otp-fet" }, + { RT8973A_INT2_OVP_FET, "muic-ovp-fet" }, + { RT8973A_INT2_OCP_LATCH, "muic-ocp-latch" }, + { RT8973A_INT2_OCP, "muic-ocp" }, + { RT8973A_INT2_OVP_OCP, "muic-ovp-ocp" }, +}; + +/* Define interrupt list of RT8973A to register regmap_irq */ +static const struct regmap_irq rt8973a_irqs[] = { + /* INT1 interrupts */ + { .reg_offset = 0, .mask = RT8973A_INT1_ATTACH_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_DETACH_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_CHGDET_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_DCD_T_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_OVP_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_CONNECT_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_ADC_CHG_MASK, }, + { .reg_offset = 0, .mask = RT8973A_INT1_OTP_MASK, }, + + /* INT2 interrupts */ + { .reg_offset = 1, .mask = RT8973A_INT2_UVLOT_MASK,}, + { .reg_offset = 1, .mask = RT8973A_INT2_POR_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OTP_FET_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OVP_FET_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OCP_LATCH_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OCP_MASK, }, + { .reg_offset = 1, .mask = RT8973A_INT2_OVP_OCP_MASK, }, +}; + +static const struct regmap_irq_chip rt8973a_muic_irq_chip = { + .name = "rt8973a", + .status_base = RT8973A_REG_INT1, + .mask_base = RT8973A_REG_INTM1, + .mask_invert = false, + .num_regs = 2, + .irqs = rt8973a_irqs, + .num_irqs = ARRAY_SIZE(rt8973a_irqs), +}; + +/* Define regmap configuration of RT8973A for I2C communication */ +static bool rt8973a_muic_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case RT8973A_REG_INTM1: + case RT8973A_REG_INTM2: + return true; + default: + break; + } + return false; +} + +static const struct regmap_config rt8973a_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .volatile_reg = rt8973a_muic_volatile_reg, + .max_register = RT8973A_REG_END, +}; + +/* Change DM_CON/DP_CON/VBUSIN switch according to cable type */ +static int rt8973a_muic_set_path(struct rt8973a_muic_info *info, + unsigned int con_sw, bool attached) +{ + int ret; + + /* + * Don't need to set h/w path according to cable type + * if Auto-configuration mode of CONTROL1 register is true. + */ + if (info->auto_config) + return 0; + + if (!attached) + con_sw = DM_DP_SWITCH_UART; + + switch (con_sw) { + case DM_DP_SWITCH_OPEN: + case DM_DP_SWITCH_USB: + case DM_DP_SWITCH_UART: + ret = regmap_update_bits(info->regmap, RT8973A_REG_MANUAL_SW1, + RT8973A_REG_MANUAL_SW1_DP_MASK | + RT8973A_REG_MANUAL_SW1_DM_MASK, + con_sw); + if (ret < 0) { + dev_err(info->dev, + "cannot update DM_CON/DP_CON switch\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Unknown DM_CON/DP_CON switch type (%d)\n", + con_sw); + return -EINVAL; + } + + return 0; +} + +static int rt8973a_muic_get_cable_type(struct rt8973a_muic_info *info) +{ + unsigned int adc, dev1; + int ret, cable_type; + + /* Read ADC value according to external cable or button */ + ret = regmap_read(info->regmap, RT8973A_REG_ADC, &adc); + if (ret) { + dev_err(info->dev, "failed to read ADC register\n"); + return ret; + } + cable_type = adc & RT8973A_REG_ADC_MASK; + + /* Read Device 1 reigster to identify correct cable type */ + ret = regmap_read(info->regmap, RT8973A_REG_DEV1, &dev1); + if (ret) { + dev_err(info->dev, "failed to read DEV1 register\n"); + return ret; + } + + switch (adc) { + case RT8973A_MUIC_ADC_OPEN: + if (dev1 & RT8973A_REG_DEV1_USB_MASK) + cable_type = RT8973A_MUIC_ADC_USB; + else if (dev1 & RT8973A_REG_DEV1_DCPORT_MASK) + cable_type = RT8973A_MUIC_ADC_TA; + else + cable_type = RT8973A_MUIC_ADC_OPEN; + break; + default: + break; + } + + return cable_type; +} + +static int rt8973a_muic_cable_handler(struct rt8973a_muic_info *info, + enum rt8973a_event_type event) +{ + static unsigned int prev_cable_type; + const char **cable_names = info->edev->supported_cable; + unsigned int con_sw = DM_DP_SWITCH_UART; + int ret, idx = 0, cable_type; + bool attached = false; + + if (!cable_names) + return 0; + + switch (event) { + case RT8973A_EVENT_ATTACH: + cable_type = rt8973a_muic_get_cable_type(info); + attached = true; + break; + case RT8973A_EVENT_DETACH: + cable_type = prev_cable_type; + attached = false; + break; + case RT8973A_EVENT_OVP: + case RT8973A_EVENT_OTP: + dev_warn(info->dev, + "happen Over %s issue. Need to disconnect all cables\n", + event == RT8973A_EVENT_OVP ? "Voltage" : "Temperature"); + cable_type = prev_cable_type; + attached = false; + break; + default: + dev_err(info->dev, + "Cannot handle this event (event:%d)\n", event); + return -EINVAL; + } + prev_cable_type = cable_type; + + switch (cable_type) { + case RT8973A_MUIC_ADC_OTG: + idx = EXTCON_CABLE_USB_HOST; + con_sw = DM_DP_SWITCH_USB; + break; + case RT8973A_MUIC_ADC_TA: + idx = EXTCON_CABLE_TA; + con_sw = DM_DP_SWITCH_OPEN; + break; + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB: + idx = EXTCON_CABLE_JIG_OFF_USB; + con_sw = DM_DP_SWITCH_UART; + break; + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB: + idx = EXTCON_CABLE_JIG_ON_USB; + con_sw = DM_DP_SWITCH_UART; + break; + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART: + idx = EXTCON_CABLE_JIG_OFF_UART; + con_sw = DM_DP_SWITCH_UART; + break; + case RT8973A_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART: + idx = EXTCON_CABLE_JIG_ON_UART; + con_sw = DM_DP_SWITCH_UART; + break; + case RT8973A_MUIC_ADC_USB: + idx = EXTCON_CABLE_USB; + con_sw = DM_DP_SWITCH_USB; + break; + case RT8973A_MUIC_ADC_OPEN: + return 0; + case RT8973A_MUIC_ADC_UNKNOWN_ACC_1: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_2: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_3: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_4: + case RT8973A_MUIC_ADC_UNKNOWN_ACC_5: + dev_warn(info->dev, + "Unknown accessory type (adc:0x%x)\n", cable_type); + return 0; + case RT8973A_MUIC_ADC_AUDIO_SEND_END_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S1_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S2_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S3_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S4_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S5_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S6_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S7_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S8_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S9_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S10_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S11_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_REMOTE_S12_BUTTON: + case RT8973A_MUIC_ADC_AUDIO_TYPE2: + dev_warn(info->dev, + "Audio device/button type (adc:0x%x)\n", cable_type); + return 0; + case RT8973A_MUIC_ADC_RESERVED_ACC_1: + case RT8973A_MUIC_ADC_RESERVED_ACC_2: + case RT8973A_MUIC_ADC_RESERVED_ACC_3: + case RT8973A_MUIC_ADC_RESERVED_ACC_4: + case RT8973A_MUIC_ADC_RESERVED_ACC_5: + case RT8973A_MUIC_ADC_PHONE_POWERED_DEV: + return 0; + default: + dev_err(info->dev, + "Cannot handle this cable_type (adc:0x%x)\n", + cable_type); + return -EINVAL; + } + + /* Change internal hardware path(DM_CON/DP_CON) */ + ret = rt8973a_muic_set_path(info, con_sw, attached); + if (ret < 0) + return ret; + + /* Change the state of external accessory */ + extcon_set_cable_state(info->edev, cable_names[idx], attached); + + return 0; +} + +static void rt8973a_muic_irq_work(struct work_struct *work) +{ + struct rt8973a_muic_info *info = container_of(work, + struct rt8973a_muic_info, irq_work); + int ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + /* Detect attached or detached cables */ + if (info->irq_attach) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_ATTACH); + info->irq_attach = false; + } + + if (info->irq_detach) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_DETACH); + info->irq_detach = false; + } + + if (info->irq_ovp) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_OVP); + info->irq_ovp = false; + } + + if (info->irq_otp) { + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_OTP); + info->irq_otp = false; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +static irqreturn_t rt8973a_muic_irq_handler(int irq, void *data) +{ + struct rt8973a_muic_info *info = data; + int i, irq_type = -1; + + for (i = 0; i < info->num_muic_irqs; i++) + if (irq == info->muic_irqs[i].virq) + irq_type = info->muic_irqs[i].irq; + + switch (irq_type) { + case RT8973A_INT1_ATTACH: + info->irq_attach = true; + break; + case RT8973A_INT1_DETACH: + info->irq_detach = true; + break; + case RT8973A_INT1_OVP: + info->irq_ovp = true; + break; + case RT8973A_INT1_OTP: + info->irq_otp = true; + break; + case RT8973A_INT1_CHGDET: + case RT8973A_INT1_DCD_T: + case RT8973A_INT1_CONNECT: + case RT8973A_INT1_ADC_CHG: + case RT8973A_INT2_UVLO: + case RT8973A_INT2_POR: + case RT8973A_INT2_OTP_FET: + case RT8973A_INT2_OVP_FET: + case RT8973A_INT2_OCP_LATCH: + case RT8973A_INT2_OCP: + case RT8973A_INT2_OVP_OCP: + default: + dev_dbg(info->dev, + "Cannot handle this interrupt (%d)\n", irq_type); + break; + } + + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static void rt8973a_muic_detect_cable_wq(struct work_struct *work) +{ + struct rt8973a_muic_info *info = container_of(to_delayed_work(work), + struct rt8973a_muic_info, wq_detcable); + int ret; + + /* Notify the state of connector cable or not */ + ret = rt8973a_muic_cable_handler(info, RT8973A_EVENT_ATTACH); + if (ret < 0) + dev_warn(info->dev, "failed to detect cable state\n"); +} + +static void rt8973a_init_dev_type(struct rt8973a_muic_info *info) +{ + unsigned int data, vendor_id, version_id; + int i, ret; + + /* To test I2C, Print version_id and vendor_id of RT8973A */ + ret = regmap_read(info->regmap, RT8973A_REG_DEVICE_ID, &data); + if (ret) { + dev_err(info->dev, + "failed to read DEVICE_ID register: %d\n", ret); + return; + } + + vendor_id = ((data & RT8973A_REG_DEVICE_ID_VENDOR_MASK) >> + RT8973A_REG_DEVICE_ID_VENDOR_SHIFT); + version_id = ((data & RT8973A_REG_DEVICE_ID_VERSION_MASK) >> + RT8973A_REG_DEVICE_ID_VERSION_SHIFT); + + dev_info(info->dev, "Device type: version: 0x%x, vendor: 0x%x\n", + version_id, vendor_id); + + /* Initiazle the register of RT8973A device to bring-up */ + for (i = 0; i < info->num_reg_data; i++) { + u8 reg = info->reg_data[i].reg; + u8 mask = info->reg_data[i].mask; + u8 val = 0; + + if (info->reg_data[i].invert) + val = ~info->reg_data[i].val; + else + val = info->reg_data[i].val; + + regmap_update_bits(info->regmap, reg, mask, val); + } + + /* Check whether RT8973A is auto swithcing mode or not */ + ret = regmap_read(info->regmap, RT8973A_REG_CONTROL1, &data); + if (ret) { + dev_err(info->dev, + "failed to read CONTROL1 register: %d\n", ret); + return; + } + + data &= RT8973A_REG_CONTROL1_AUTO_CONFIG_MASK; + if (data) { + info->auto_config = true; + dev_info(info->dev, + "Enable Auto-configuration for internal path\n"); + } +} + +static int rt8973a_muic_i2c_probe(struct i2c_client *i2c, + const struct i2c_device_id *id) +{ + struct device_node *np = i2c->dev.of_node; + struct rt8973a_muic_info *info; + int i, ret, irq_flags; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&i2c->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + i2c_set_clientdata(i2c, info); + + info->dev = &i2c->dev; + info->i2c = i2c; + info->irq = i2c->irq; + info->muic_irqs = rt8973a_muic_irqs; + info->num_muic_irqs = ARRAY_SIZE(rt8973a_muic_irqs); + info->reg_data = rt8973a_reg_data; + info->num_reg_data = ARRAY_SIZE(rt8973a_reg_data); + + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, rt8973a_muic_irq_work); + + info->regmap = devm_regmap_init_i2c(i2c, &rt8973a_muic_regmap_config); + if (IS_ERR(info->regmap)) { + ret = PTR_ERR(info->regmap); + dev_err(info->dev, "failed to allocate register map: %d\n", + ret); + return ret; + } + + /* Support irq domain for RT8973A MUIC device */ + irq_flags = IRQF_TRIGGER_FALLING | IRQF_ONESHOT | IRQF_SHARED; + ret = regmap_add_irq_chip(info->regmap, info->irq, irq_flags, 0, + &rt8973a_muic_irq_chip, &info->irq_data); + if (ret != 0) { + dev_err(info->dev, "failed to add irq_chip (irq:%d, err:%d)\n", + info->irq, ret); + return ret; + } + + for (i = 0; i < info->num_muic_irqs; i++) { + struct muic_irq *muic_irq = &info->muic_irqs[i]; + unsigned int virq = 0; + + virq = regmap_irq_get_virq(info->irq_data, muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(info->dev, virq, NULL, + rt8973a_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(info->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Allocate extcon device */ + info->edev = devm_extcon_dev_allocate(info->dev, rt8973a_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(info->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + info->edev->name = np->name; + + /* Register extcon device */ + ret = devm_extcon_dev_register(info->dev, info->edev); + if (ret) { + dev_err(info->dev, "failed to register extcon device\n"); + return ret; + } + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, rt8973a_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + msecs_to_jiffies(DELAY_MS_DEFAULT)); + + /* Initialize RT8973A device and print vendor id and version id */ + rt8973a_init_dev_type(info); + + return 0; +} + +static int rt8973a_muic_i2c_remove(struct i2c_client *i2c) +{ + struct rt8973a_muic_info *info = i2c_get_clientdata(i2c); + + regmap_del_irq_chip(info->irq, info->irq_data); + + return 0; +} + +static const struct of_device_id rt8973a_dt_match[] = { + { .compatible = "richtek,rt8973a-muic" }, + { }, +}; + +#ifdef CONFIG_PM_SLEEP +static int rt8973a_muic_suspend(struct device *dev) +{ + struct i2c_client *i2c = container_of(dev, struct i2c_client, dev); + struct rt8973a_muic_info *info = i2c_get_clientdata(i2c); + + enable_irq_wake(info->irq); + + return 0; +} + +static int rt8973a_muic_resume(struct device *dev) +{ + struct i2c_client *i2c = container_of(dev, struct i2c_client, dev); + struct rt8973a_muic_info *info = i2c_get_clientdata(i2c); + + disable_irq_wake(info->irq); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(rt8973a_muic_pm_ops, + rt8973a_muic_suspend, rt8973a_muic_resume); + +static const struct i2c_device_id rt8973a_i2c_id[] = { + { "rt8973a", TYPE_RT8973A }, + { } +}; +MODULE_DEVICE_TABLE(i2c, rt8973a_i2c_id); + +static struct i2c_driver rt8973a_muic_i2c_driver = { + .driver = { + .name = "rt8973a", + .owner = THIS_MODULE, + .pm = &rt8973a_muic_pm_ops, + .of_match_table = rt8973a_dt_match, + }, + .probe = rt8973a_muic_i2c_probe, + .remove = rt8973a_muic_i2c_remove, + .id_table = rt8973a_i2c_id, +}; + +static int __init rt8973a_muic_i2c_init(void) +{ + return i2c_add_driver(&rt8973a_muic_i2c_driver); +} +subsys_initcall(rt8973a_muic_i2c_init); + +MODULE_DESCRIPTION("Richtek RT8973A Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-rt8973a.h b/drivers/extcon/extcon-rt8973a.h new file mode 100644 index 000000000..9dc3e0227 --- /dev/null +++ b/drivers/extcon/extcon-rt8973a.h @@ -0,0 +1,203 @@ +/* + * rt8973a.h + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + * + * 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. + */ + +#ifndef __LINUX_EXTCON_RT8973A_H +#define __LINUX_EXTCON_RT8973A_H + +enum rt8973a_types { + TYPE_RT8973A, +}; + +/* RT8973A registers */ +enum rt8973A_reg { + RT8973A_REG_DEVICE_ID = 0x1, + RT8973A_REG_CONTROL1, + RT8973A_REG_INT1, + RT8973A_REG_INT2, + RT8973A_REG_INTM1, + RT8973A_REG_INTM2, + RT8973A_REG_ADC, + RT8973A_REG_RSVD_1, + RT8973A_REG_RSVD_2, + RT8973A_REG_DEV1, + RT8973A_REG_DEV2, + RT8973A_REG_RSVD_3, + RT8973A_REG_RSVD_4, + RT8973A_REG_RSVD_5, + RT8973A_REG_RSVD_6, + RT8973A_REG_RSVD_7, + RT8973A_REG_RSVD_8, + RT8973A_REG_RSVD_9, + RT8973A_REG_MANUAL_SW1, + RT8973A_REG_MANUAL_SW2, + RT8973A_REG_RSVD_10, + RT8973A_REG_RSVD_11, + RT8973A_REG_RSVD_12, + RT8973A_REG_RSVD_13, + RT8973A_REG_RSVD_14, + RT8973A_REG_RSVD_15, + RT8973A_REG_RESET, + + RT8973A_REG_END, +}; + +/* Define RT8973A MASK/SHIFT constant */ +#define RT8973A_REG_DEVICE_ID_VENDOR_SHIFT 0 +#define RT8973A_REG_DEVICE_ID_VERSION_SHIFT 3 +#define RT8973A_REG_DEVICE_ID_VENDOR_MASK (0x7 << RT8973A_REG_DEVICE_ID_VENDOR_SHIFT) +#define RT8973A_REG_DEVICE_ID_VERSION_MASK (0x1f << RT8973A_REG_DEVICE_ID_VERSION_SHIFT) + +#define RT8973A_REG_CONTROL1_INTM_SHIFT 0 +#define RT8973A_REG_CONTROL1_AUTO_CONFIG_SHIFT 2 +#define RT8973A_REG_CONTROL1_I2C_RST_EN_SHIFT 3 +#define RT8973A_REG_CONTROL1_SWITCH_OPEN_SHIFT 4 +#define RT8973A_REG_CONTROL1_CHGTYP_SHIFT 5 +#define RT8973A_REG_CONTROL1_USB_CHD_EN_SHIFT 6 +#define RT8973A_REG_CONTROL1_ADC_EN_SHIFT 7 +#define RT8973A_REG_CONTROL1_INTM_MASK (0x1 << RT8973A_REG_CONTROL1_INTM_SHIFT) +#define RT8973A_REG_CONTROL1_AUTO_CONFIG_MASK (0x1 << RT8973A_REG_CONTROL1_AUTO_CONFIG_SHIFT) +#define RT8973A_REG_CONTROL1_I2C_RST_EN_MASK (0x1 << RT8973A_REG_CONTROL1_I2C_RST_EN_SHIFT) +#define RT8973A_REG_CONTROL1_SWITCH_OPEN_MASK (0x1 << RT8973A_REG_CONTROL1_SWITCH_OPEN_SHIFT) +#define RT8973A_REG_CONTROL1_CHGTYP_MASK (0x1 << RT8973A_REG_CONTROL1_CHGTYP_SHIFT) +#define RT8973A_REG_CONTROL1_USB_CHD_EN_MASK (0x1 << RT8973A_REG_CONTROL1_USB_CHD_EN_SHIFT) +#define RT8973A_REG_CONTROL1_ADC_EN_MASK (0x1 << RT8973A_REG_CONTROL1_ADC_EN_SHIFT) + +#define RT9873A_REG_INTM1_ATTACH_SHIFT 0 +#define RT9873A_REG_INTM1_DETACH_SHIFT 1 +#define RT9873A_REG_INTM1_CHGDET_SHIFT 2 +#define RT9873A_REG_INTM1_DCD_T_SHIFT 3 +#define RT9873A_REG_INTM1_OVP_SHIFT 4 +#define RT9873A_REG_INTM1_CONNECT_SHIFT 5 +#define RT9873A_REG_INTM1_ADC_CHG_SHIFT 6 +#define RT9873A_REG_INTM1_OTP_SHIFT 7 +#define RT9873A_REG_INTM1_ATTACH_MASK (0x1 << RT9873A_REG_INTM1_ATTACH_SHIFT) +#define RT9873A_REG_INTM1_DETACH_MASK (0x1 << RT9873A_REG_INTM1_DETACH_SHIFT) +#define RT9873A_REG_INTM1_CHGDET_MASK (0x1 << RT9873A_REG_INTM1_CHGDET_SHIFT) +#define RT9873A_REG_INTM1_DCD_T_MASK (0x1 << RT9873A_REG_INTM1_DCD_T_SHIFT) +#define RT9873A_REG_INTM1_OVP_MASK (0x1 << RT9873A_REG_INTM1_OVP_SHIFT) +#define RT9873A_REG_INTM1_CONNECT_MASK (0x1 << RT9873A_REG_INTM1_CONNECT_SHIFT) +#define RT9873A_REG_INTM1_ADC_CHG_MASK (0x1 << RT9873A_REG_INTM1_ADC_CHG_SHIFT) +#define RT9873A_REG_INTM1_OTP_MASK (0x1 << RT9873A_REG_INTM1_OTP_SHIFT) + +#define RT9873A_REG_INTM2_UVLO_SHIFT 1 +#define RT9873A_REG_INTM2_POR_SHIFT 2 +#define RT9873A_REG_INTM2_OTP_FET_SHIFT 3 +#define RT9873A_REG_INTM2_OVP_FET_SHIFT 4 +#define RT9873A_REG_INTM2_OCP_LATCH_SHIFT 5 +#define RT9873A_REG_INTM2_OCP_SHIFT 6 +#define RT9873A_REG_INTM2_OVP_OCP_SHIFT 7 +#define RT9873A_REG_INTM2_UVLO_MASK (0x1 << RT9873A_REG_INTM2_UVLO_SHIFT) +#define RT9873A_REG_INTM2_POR_MASK (0x1 << RT9873A_REG_INTM2_POR_SHIFT) +#define RT9873A_REG_INTM2_OTP_FET_MASK (0x1 << RT9873A_REG_INTM2_OTP_FET_SHIFT) +#define RT9873A_REG_INTM2_OVP_FET_MASK (0x1 << RT9873A_REG_INTM2_OVP_FET_SHIFT) +#define RT9873A_REG_INTM2_OCP_LATCH_MASK (0x1 << RT9873A_REG_INTM2_OCP_LATCH_SHIFT) +#define RT9873A_REG_INTM2_OCP_MASK (0x1 << RT9873A_REG_INTM2_OCP_SHIFT) +#define RT9873A_REG_INTM2_OVP_OCP_MASK (0x1 << RT9873A_REG_INTM2_OVP_OCP_SHIFT) + +#define RT8973A_REG_ADC_SHIFT 0 +#define RT8973A_REG_ADC_MASK (0x1f << RT8973A_REG_ADC_SHIFT) + +#define RT8973A_REG_DEV1_OTG_SHIFT 0 +#define RT8973A_REG_DEV1_SDP_SHIFT 2 +#define RT8973A_REG_DEV1_UART_SHIFT 3 +#define RT8973A_REG_DEV1_CAR_KIT_TYPE1_SHIFT 4 +#define RT8973A_REG_DEV1_CDPORT_SHIFT 5 +#define RT8973A_REG_DEV1_DCPORT_SHIFT 6 +#define RT8973A_REG_DEV1_OTG_MASK (0x1 << RT8973A_REG_DEV1_OTG_SHIFT) +#define RT8973A_REG_DEV1_SDP_MASK (0x1 << RT8973A_REG_DEV1_SDP_SHIFT) +#define RT8973A_REG_DEV1_UART_MASK (0x1 << RT8973A_REG_DEV1_UART_SHIFT) +#define RT8973A_REG_DEV1_CAR_KIT_TYPE1_MASK (0x1 << RT8973A_REG_DEV1_CAR_KIT_TYPE1_SHIFT) +#define RT8973A_REG_DEV1_CDPORT_MASK (0x1 << RT8973A_REG_DEV1_CDPORT_SHIFT) +#define RT8973A_REG_DEV1_DCPORT_MASK (0x1 << RT8973A_REG_DEV1_DCPORT_SHIFT) +#define RT8973A_REG_DEV1_USB_MASK (RT8973A_REG_DEV1_SDP_MASK \ + | RT8973A_REG_DEV1_CDPORT_MASK) + +#define RT8973A_REG_DEV2_JIG_USB_ON_SHIFT 0 +#define RT8973A_REG_DEV2_JIG_USB_OFF_SHIFT 1 +#define RT8973A_REG_DEV2_JIG_UART_ON_SHIFT 2 +#define RT8973A_REG_DEV2_JIG_UART_OFF_SHIFT 3 +#define RT8973A_REG_DEV2_JIG_USB_ON_MASK (0x1 << RT8973A_REG_DEV2_JIG_USB_ON_SHIFT) +#define RT8973A_REG_DEV2_JIG_USB_OFF_MASK (0x1 << RT8973A_REG_DEV2_JIG_USB_OFF_SHIFT) +#define RT8973A_REG_DEV2_JIG_UART_ON_MASK (0x1 << RT8973A_REG_DEV2_JIG_UART_ON_SHIFT) +#define RT8973A_REG_DEV2_JIG_UART_OFF_MASK (0x1 << RT8973A_REG_DEV2_JIG_UART_OFF_SHIFT) + +#define RT8973A_REG_MANUAL_SW1_DP_SHIFT 2 +#define RT8973A_REG_MANUAL_SW1_DM_SHIFT 5 +#define RT8973A_REG_MANUAL_SW1_DP_MASK (0x7 << RT8973A_REG_MANUAL_SW1_DP_SHIFT) +#define RT8973A_REG_MANUAL_SW1_DM_MASK (0x7 << RT8973A_REG_MANUAL_SW1_DM_SHIFT) +#define DM_DP_CON_SWITCH_OPEN 0x0 +#define DM_DP_CON_SWITCH_USB 0x1 +#define DM_DP_CON_SWITCH_UART 0x3 +#define DM_DP_SWITCH_OPEN ((DM_DP_CON_SWITCH_OPEN << RT8973A_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_OPEN << RT8973A_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_USB ((DM_DP_CON_SWITCH_USB << RT8973A_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_USB << RT8973A_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_UART ((DM_DP_CON_SWITCH_UART << RT8973A_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_UART << RT8973A_REG_MANUAL_SW1_DM_SHIFT)) + +#define RT8973A_REG_MANUAL_SW2_FET_ON_SHIFT 0 +#define RT8973A_REG_MANUAL_SW2_JIG_ON_SHIFT 2 +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_SHIFT 3 +#define RT8973A_REG_MANUAL_SW2_FET_ON_MASK (0x1 << RT8973A_REG_MANUAL_SW2_FET_ON_SHIFT) +#define RT8973A_REG_MANUAL_SW2_JIG_ON_MASK (0x1 << RT8973A_REG_MANUAL_SW2_JIG_ON_SHIFT) +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_MASK (0x1 << RT8973A_REG_MANUAL_SW2_BOOT_SW_SHIFT) +#define RT8973A_REG_MANUAL_SW2_FET_ON 0 +#define RT8973A_REG_MANUAL_SW2_FET_OFF 0x1 +#define RT8973A_REG_MANUAL_SW2_JIG_OFF 0 +#define RT8973A_REG_MANUAL_SW2_JIG_ON 0x1 +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_ON 0 +#define RT8973A_REG_MANUAL_SW2_BOOT_SW_OFF 0x1 + +#define RT8973A_REG_RESET_SHIFT 0 +#define RT8973A_REG_RESET_MASK (0x1 << RT8973A_REG_RESET_SHIFT) +#define RT8973A_REG_RESET 0x1 + +/* RT8973A Interrupts */ +enum rt8973a_irq { + /* Interrupt1*/ + RT8973A_INT1_ATTACH, + RT8973A_INT1_DETACH, + RT8973A_INT1_CHGDET, + RT8973A_INT1_DCD_T, + RT8973A_INT1_OVP, + RT8973A_INT1_CONNECT, + RT8973A_INT1_ADC_CHG, + RT8973A_INT1_OTP, + + /* Interrupt2*/ + RT8973A_INT2_UVLO, + RT8973A_INT2_POR, + RT8973A_INT2_OTP_FET, + RT8973A_INT2_OVP_FET, + RT8973A_INT2_OCP_LATCH, + RT8973A_INT2_OCP, + RT8973A_INT2_OVP_OCP, + + RT8973A_NUM, +}; + +#define RT8973A_INT1_ATTACH_MASK BIT(0) +#define RT8973A_INT1_DETACH_MASK BIT(1) +#define RT8973A_INT1_CHGDET_MASK BIT(2) +#define RT8973A_INT1_DCD_T_MASK BIT(3) +#define RT8973A_INT1_OVP_MASK BIT(4) +#define RT8973A_INT1_CONNECT_MASK BIT(5) +#define RT8973A_INT1_ADC_CHG_MASK BIT(6) +#define RT8973A_INT1_OTP_MASK BIT(7) +#define RT8973A_INT2_UVLOT_MASK BIT(0) +#define RT8973A_INT2_POR_MASK BIT(1) +#define RT8973A_INT2_OTP_FET_MASK BIT(2) +#define RT8973A_INT2_OVP_FET_MASK BIT(3) +#define RT8973A_INT2_OCP_LATCH_MASK BIT(4) +#define RT8973A_INT2_OCP_MASK BIT(5) +#define RT8973A_INT2_OVP_OCP_MASK BIT(6) + +#endif /* __LINUX_EXTCON_RT8973A_H */ diff --git a/drivers/extcon/extcon-sm5502.c b/drivers/extcon/extcon-sm5502.c new file mode 100644 index 000000000..2f93cf307 --- /dev/null +++ b/drivers/extcon/extcon-sm5502.c @@ -0,0 +1,718 @@ +/* + * extcon-sm5502.c - Silicon Mitus SM5502 extcon drvier to support USB switches + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + * Author: Chanwoo Choi <cw00.choi@samsung.com> + * + * 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. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/irqdomain.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/extcon.h> + +#include "extcon-sm5502.h" + +#define DELAY_MS_DEFAULT 17000 /* unit: millisecond */ + +struct muic_irq { + unsigned int irq; + const char *name; + unsigned int virq; +}; + +struct reg_data { + u8 reg; + unsigned int val; + bool invert; +}; + +struct sm5502_muic_info { + struct device *dev; + struct extcon_dev *edev; + + struct i2c_client *i2c; + struct regmap *regmap; + + struct regmap_irq_chip_data *irq_data; + struct muic_irq *muic_irqs; + unsigned int num_muic_irqs; + int irq; + bool irq_attach; + bool irq_detach; + struct work_struct irq_work; + + struct reg_data *reg_data; + unsigned int num_reg_data; + + struct mutex mutex; + + /* + * Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + struct delayed_work wq_detcable; +}; + +/* Default value of SM5502 register to bring up MUIC device. */ +static struct reg_data sm5502_reg_data[] = { + { + .reg = SM5502_REG_CONTROL, + .val = SM5502_REG_CONTROL_MASK_INT_MASK, + .invert = false, + }, { + .reg = SM5502_REG_INTMASK1, + .val = SM5502_REG_INTM1_KP_MASK + | SM5502_REG_INTM1_LKP_MASK + | SM5502_REG_INTM1_LKR_MASK, + .invert = true, + }, { + .reg = SM5502_REG_INTMASK2, + .val = SM5502_REG_INTM2_VBUS_DET_MASK + | SM5502_REG_INTM2_REV_ACCE_MASK + | SM5502_REG_INTM2_ADC_CHG_MASK + | SM5502_REG_INTM2_STUCK_KEY_MASK + | SM5502_REG_INTM2_STUCK_KEY_RCV_MASK + | SM5502_REG_INTM2_MHL_MASK, + .invert = true, + }, + { } +}; + +/* List of detectable cables */ +enum { + EXTCON_CABLE_USB = 0, + EXTCON_CABLE_USB_HOST, + EXTCON_CABLE_TA, + + EXTCON_CABLE_END, +}; + +static const char *sm5502_extcon_cable[] = { + [EXTCON_CABLE_USB] = "USB", + [EXTCON_CABLE_USB_HOST] = "USB-Host", + [EXTCON_CABLE_TA] = "TA", + NULL, +}; + +/* Define supported accessory type */ +enum sm5502_muic_acc_type { + SM5502_MUIC_ADC_GROUND = 0x0, + SM5502_MUIC_ADC_SEND_END_BUTTON, + SM5502_MUIC_ADC_REMOTE_S1_BUTTON, + SM5502_MUIC_ADC_REMOTE_S2_BUTTON, + SM5502_MUIC_ADC_REMOTE_S3_BUTTON, + SM5502_MUIC_ADC_REMOTE_S4_BUTTON, + SM5502_MUIC_ADC_REMOTE_S5_BUTTON, + SM5502_MUIC_ADC_REMOTE_S6_BUTTON, + SM5502_MUIC_ADC_REMOTE_S7_BUTTON, + SM5502_MUIC_ADC_REMOTE_S8_BUTTON, + SM5502_MUIC_ADC_REMOTE_S9_BUTTON, + SM5502_MUIC_ADC_REMOTE_S10_BUTTON, + SM5502_MUIC_ADC_REMOTE_S11_BUTTON, + SM5502_MUIC_ADC_REMOTE_S12_BUTTON, + SM5502_MUIC_ADC_RESERVED_ACC_1, + SM5502_MUIC_ADC_RESERVED_ACC_2, + SM5502_MUIC_ADC_RESERVED_ACC_3, + SM5502_MUIC_ADC_RESERVED_ACC_4, + SM5502_MUIC_ADC_RESERVED_ACC_5, + SM5502_MUIC_ADC_AUDIO_TYPE2, + SM5502_MUIC_ADC_PHONE_POWERED_DEV, + SM5502_MUIC_ADC_TTY_CONVERTER, + SM5502_MUIC_ADC_UART_CABLE, + SM5502_MUIC_ADC_TYPE1_CHARGER, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB, + SM5502_MUIC_ADC_AUDIO_VIDEO_CABLE, + SM5502_MUIC_ADC_TYPE2_CHARGER, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART, + SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART, + SM5502_MUIC_ADC_AUDIO_TYPE1, + SM5502_MUIC_ADC_OPEN = 0x1f, + + /* The below accessories have same ADC value (0x1f or 0x1e). + So, Device type1 is used to separate specific accessory. */ + /* |---------|--ADC| */ + /* | [7:5]|[4:0]| */ + SM5502_MUIC_ADC_AUDIO_TYPE1_FULL_REMOTE = 0x3e, /* | 001|11110| */ + SM5502_MUIC_ADC_AUDIO_TYPE1_SEND_END = 0x5e, /* | 010|11110| */ + /* |Dev Type1|--ADC| */ + SM5502_MUIC_ADC_OPEN_USB = 0x5f, /* | 010|11111| */ + SM5502_MUIC_ADC_OPEN_TA = 0xdf, /* | 110|11111| */ + SM5502_MUIC_ADC_OPEN_USB_OTG = 0xff, /* | 111|11111| */ +}; + +/* List of supported interrupt for SM5502 */ +static struct muic_irq sm5502_muic_irqs[] = { + { SM5502_IRQ_INT1_ATTACH, "muic-attach" }, + { SM5502_IRQ_INT1_DETACH, "muic-detach" }, + { SM5502_IRQ_INT1_KP, "muic-kp" }, + { SM5502_IRQ_INT1_LKP, "muic-lkp" }, + { SM5502_IRQ_INT1_LKR, "muic-lkr" }, + { SM5502_IRQ_INT1_OVP_EVENT, "muic-ovp-event" }, + { SM5502_IRQ_INT1_OCP_EVENT, "muic-ocp-event" }, + { SM5502_IRQ_INT1_OVP_OCP_DIS, "muic-ovp-ocp-dis" }, + { SM5502_IRQ_INT2_VBUS_DET, "muic-vbus-det" }, + { SM5502_IRQ_INT2_REV_ACCE, "muic-rev-acce" }, + { SM5502_IRQ_INT2_ADC_CHG, "muic-adc-chg" }, + { SM5502_IRQ_INT2_STUCK_KEY, "muic-stuck-key" }, + { SM5502_IRQ_INT2_STUCK_KEY_RCV, "muic-stuck-key-rcv" }, + { SM5502_IRQ_INT2_MHL, "muic-mhl" }, +}; + +/* Define interrupt list of SM5502 to register regmap_irq */ +static const struct regmap_irq sm5502_irqs[] = { + /* INT1 interrupts */ + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_ATTACH_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_DETACH_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_KP_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_LKP_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_LKR_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_OVP_EVENT_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_OCP_EVENT_MASK, }, + { .reg_offset = 0, .mask = SM5502_IRQ_INT1_OVP_OCP_DIS_MASK, }, + + /* INT2 interrupts */ + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_VBUS_DET_MASK,}, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_REV_ACCE_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_ADC_CHG_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_STUCK_KEY_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_STUCK_KEY_RCV_MASK, }, + { .reg_offset = 1, .mask = SM5502_IRQ_INT2_MHL_MASK, }, +}; + +static const struct regmap_irq_chip sm5502_muic_irq_chip = { + .name = "sm5502", + .status_base = SM5502_REG_INT1, + .mask_base = SM5502_REG_INTMASK1, + .mask_invert = false, + .num_regs = 2, + .irqs = sm5502_irqs, + .num_irqs = ARRAY_SIZE(sm5502_irqs), +}; + +/* Define regmap configuration of SM5502 for I2C communication */ +static bool sm5502_muic_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case SM5502_REG_INTMASK1: + case SM5502_REG_INTMASK2: + return true; + default: + break; + } + return false; +} + +static const struct regmap_config sm5502_muic_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .volatile_reg = sm5502_muic_volatile_reg, + .max_register = SM5502_REG_END, +}; + +/* Change DM_CON/DP_CON/VBUSIN switch according to cable type */ +static int sm5502_muic_set_path(struct sm5502_muic_info *info, + unsigned int con_sw, unsigned int vbus_sw, + bool attached) +{ + int ret; + + if (!attached) { + con_sw = DM_DP_SWITCH_OPEN; + vbus_sw = VBUSIN_SWITCH_OPEN; + } + + switch (con_sw) { + case DM_DP_SWITCH_OPEN: + case DM_DP_SWITCH_USB: + case DM_DP_SWITCH_AUDIO: + case DM_DP_SWITCH_UART: + ret = regmap_update_bits(info->regmap, SM5502_REG_MANUAL_SW1, + SM5502_REG_MANUAL_SW1_DP_MASK | + SM5502_REG_MANUAL_SW1_DM_MASK, + con_sw); + if (ret < 0) { + dev_err(info->dev, + "cannot update DM_CON/DP_CON switch\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Unknown DM_CON/DP_CON switch type (%d)\n", + con_sw); + return -EINVAL; + }; + + switch (vbus_sw) { + case VBUSIN_SWITCH_OPEN: + case VBUSIN_SWITCH_VBUSOUT: + case VBUSIN_SWITCH_MIC: + case VBUSIN_SWITCH_VBUSOUT_WITH_USB: + ret = regmap_update_bits(info->regmap, SM5502_REG_MANUAL_SW1, + SM5502_REG_MANUAL_SW1_VBUSIN_MASK, + vbus_sw); + if (ret < 0) { + dev_err(info->dev, + "cannot update VBUSIN switch\n"); + return ret; + } + break; + default: + dev_err(info->dev, "Unknown VBUS switch type (%d)\n", vbus_sw); + return -EINVAL; + }; + + return 0; +} + +/* Return cable type of attached or detached accessories */ +static unsigned int sm5502_muic_get_cable_type(struct sm5502_muic_info *info) +{ + unsigned int cable_type = -1, adc, dev_type1; + int ret; + + /* Read ADC value according to external cable or button */ + ret = regmap_read(info->regmap, SM5502_REG_ADC, &adc); + if (ret) { + dev_err(info->dev, "failed to read ADC register\n"); + return ret; + } + + /* + * If ADC is SM5502_MUIC_ADC_GROUND(0x0), external cable hasn't + * connected with to MUIC device. + */ + cable_type = adc & SM5502_REG_ADC_MASK; + if (cable_type == SM5502_MUIC_ADC_GROUND) + return SM5502_MUIC_ADC_GROUND; + + switch (cable_type) { + case SM5502_MUIC_ADC_GROUND: + case SM5502_MUIC_ADC_SEND_END_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S1_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S2_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S3_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S4_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S5_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S6_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S7_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S8_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S9_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S10_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S11_BUTTON: + case SM5502_MUIC_ADC_REMOTE_S12_BUTTON: + case SM5502_MUIC_ADC_RESERVED_ACC_1: + case SM5502_MUIC_ADC_RESERVED_ACC_2: + case SM5502_MUIC_ADC_RESERVED_ACC_3: + case SM5502_MUIC_ADC_RESERVED_ACC_4: + case SM5502_MUIC_ADC_RESERVED_ACC_5: + case SM5502_MUIC_ADC_AUDIO_TYPE2: + case SM5502_MUIC_ADC_PHONE_POWERED_DEV: + case SM5502_MUIC_ADC_TTY_CONVERTER: + case SM5502_MUIC_ADC_UART_CABLE: + case SM5502_MUIC_ADC_TYPE1_CHARGER: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_USB: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_USB: + case SM5502_MUIC_ADC_AUDIO_VIDEO_CABLE: + case SM5502_MUIC_ADC_TYPE2_CHARGER: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_OFF_UART: + case SM5502_MUIC_ADC_FACTORY_MODE_BOOT_ON_UART: + break; + case SM5502_MUIC_ADC_AUDIO_TYPE1: + /* + * Check whether cable type is + * SM5502_MUIC_ADC_AUDIO_TYPE1_FULL_REMOTE + * or SM5502_MUIC_ADC_AUDIO_TYPE1_SEND_END + * by using Button event. + */ + break; + case SM5502_MUIC_ADC_OPEN: + ret = regmap_read(info->regmap, SM5502_REG_DEV_TYPE1, + &dev_type1); + if (ret) { + dev_err(info->dev, "failed to read DEV_TYPE1 reg\n"); + return ret; + } + + switch (dev_type1) { + case SM5502_REG_DEV_TYPE1_USB_SDP_MASK: + cable_type = SM5502_MUIC_ADC_OPEN_USB; + break; + case SM5502_REG_DEV_TYPE1_DEDICATED_CHG_MASK: + cable_type = SM5502_MUIC_ADC_OPEN_TA; + break; + case SM5502_REG_DEV_TYPE1_USB_OTG_MASK: + cable_type = SM5502_MUIC_ADC_OPEN_USB_OTG; + break; + default: + dev_dbg(info->dev, + "cannot identify the cable type: adc(0x%x)\n", + adc); + return -EINVAL; + }; + break; + default: + dev_err(info->dev, + "failed to identify the cable type: adc(0x%x)\n", adc); + return -EINVAL; + }; + + return cable_type; +} + +static int sm5502_muic_cable_handler(struct sm5502_muic_info *info, + bool attached) +{ + static unsigned int prev_cable_type = SM5502_MUIC_ADC_GROUND; + const char **cable_names = info->edev->supported_cable; + unsigned int cable_type = SM5502_MUIC_ADC_GROUND; + unsigned int con_sw = DM_DP_SWITCH_OPEN; + unsigned int vbus_sw = VBUSIN_SWITCH_OPEN; + unsigned int idx = 0; + int ret; + + if (!cable_names) + return 0; + + /* Get the type of attached or detached cable */ + if (attached) + cable_type = sm5502_muic_get_cable_type(info); + else + cable_type = prev_cable_type; + prev_cable_type = cable_type; + + switch (cable_type) { + case SM5502_MUIC_ADC_OPEN_USB: + idx = EXTCON_CABLE_USB; + con_sw = DM_DP_SWITCH_USB; + vbus_sw = VBUSIN_SWITCH_VBUSOUT_WITH_USB; + break; + case SM5502_MUIC_ADC_OPEN_TA: + idx = EXTCON_CABLE_TA; + con_sw = DM_DP_SWITCH_OPEN; + vbus_sw = VBUSIN_SWITCH_VBUSOUT; + break; + case SM5502_MUIC_ADC_OPEN_USB_OTG: + idx = EXTCON_CABLE_USB_HOST; + con_sw = DM_DP_SWITCH_USB; + vbus_sw = VBUSIN_SWITCH_OPEN; + break; + default: + dev_dbg(info->dev, + "cannot handle this cable_type (0x%x)\n", cable_type); + return 0; + }; + + /* Change internal hardware path(DM_CON/DP_CON, VBUSIN) */ + ret = sm5502_muic_set_path(info, con_sw, vbus_sw, attached); + if (ret < 0) + return ret; + + /* Change the state of external accessory */ + extcon_set_cable_state(info->edev, cable_names[idx], attached); + + return 0; +} + +static void sm5502_muic_irq_work(struct work_struct *work) +{ + struct sm5502_muic_info *info = container_of(work, + struct sm5502_muic_info, irq_work); + int ret = 0; + + if (!info->edev) + return; + + mutex_lock(&info->mutex); + + /* Detect attached or detached cables */ + if (info->irq_attach) { + ret = sm5502_muic_cable_handler(info, true); + info->irq_attach = false; + } + if (info->irq_detach) { + ret = sm5502_muic_cable_handler(info, false); + info->irq_detach = false; + } + + if (ret < 0) + dev_err(info->dev, "failed to handle MUIC interrupt\n"); + + mutex_unlock(&info->mutex); +} + +/* + * Sets irq_attach or irq_detach in sm5502_muic_info and returns 0. + * Returns -ESRCH if irq_type does not match registered IRQ for this dev type. + */ +static int sm5502_parse_irq(struct sm5502_muic_info *info, int irq_type) +{ + switch (irq_type) { + case SM5502_IRQ_INT1_ATTACH: + info->irq_attach = true; + break; + case SM5502_IRQ_INT1_DETACH: + info->irq_detach = true; + break; + case SM5502_IRQ_INT1_KP: + case SM5502_IRQ_INT1_LKP: + case SM5502_IRQ_INT1_LKR: + case SM5502_IRQ_INT1_OVP_EVENT: + case SM5502_IRQ_INT1_OCP_EVENT: + case SM5502_IRQ_INT1_OVP_OCP_DIS: + case SM5502_IRQ_INT2_VBUS_DET: + case SM5502_IRQ_INT2_REV_ACCE: + case SM5502_IRQ_INT2_ADC_CHG: + case SM5502_IRQ_INT2_STUCK_KEY: + case SM5502_IRQ_INT2_STUCK_KEY_RCV: + case SM5502_IRQ_INT2_MHL: + default: + break; + } + + return 0; +} + +static irqreturn_t sm5502_muic_irq_handler(int irq, void *data) +{ + struct sm5502_muic_info *info = data; + int i, irq_type = -1, ret; + + for (i = 0; i < info->num_muic_irqs; i++) + if (irq == info->muic_irqs[i].virq) + irq_type = info->muic_irqs[i].irq; + + ret = sm5502_parse_irq(info, irq_type); + if (ret < 0) { + dev_warn(info->dev, "cannot handle is interrupt:%d\n", + irq_type); + return IRQ_HANDLED; + } + schedule_work(&info->irq_work); + + return IRQ_HANDLED; +} + +static void sm5502_muic_detect_cable_wq(struct work_struct *work) +{ + struct sm5502_muic_info *info = container_of(to_delayed_work(work), + struct sm5502_muic_info, wq_detcable); + int ret; + + /* Notify the state of connector cable or not */ + ret = sm5502_muic_cable_handler(info, true); + if (ret < 0) + dev_warn(info->dev, "failed to detect cable state\n"); +} + +static void sm5502_init_dev_type(struct sm5502_muic_info *info) +{ + unsigned int reg_data, vendor_id, version_id; + int i, ret; + + /* To test I2C, Print version_id and vendor_id of SM5502 */ + ret = regmap_read(info->regmap, SM5502_REG_DEVICE_ID, ®_data); + if (ret) { + dev_err(info->dev, + "failed to read DEVICE_ID register: %d\n", ret); + return; + } + + vendor_id = ((reg_data & SM5502_REG_DEVICE_ID_VENDOR_MASK) >> + SM5502_REG_DEVICE_ID_VENDOR_SHIFT); + version_id = ((reg_data & SM5502_REG_DEVICE_ID_VERSION_MASK) >> + SM5502_REG_DEVICE_ID_VERSION_SHIFT); + + dev_info(info->dev, "Device type: version: 0x%x, vendor: 0x%x\n", + version_id, vendor_id); + + /* Initiazle the register of SM5502 device to bring-up */ + for (i = 0; i < info->num_reg_data; i++) { + unsigned int val = 0; + + if (!info->reg_data[i].invert) + val |= ~info->reg_data[i].val; + else + val = info->reg_data[i].val; + regmap_write(info->regmap, info->reg_data[i].reg, val); + } +} + +static int sm5022_muic_i2c_probe(struct i2c_client *i2c, + const struct i2c_device_id *id) +{ + struct device_node *np = i2c->dev.of_node; + struct sm5502_muic_info *info; + int i, ret, irq_flags; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&i2c->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + i2c_set_clientdata(i2c, info); + + info->dev = &i2c->dev; + info->i2c = i2c; + info->irq = i2c->irq; + info->muic_irqs = sm5502_muic_irqs; + info->num_muic_irqs = ARRAY_SIZE(sm5502_muic_irqs); + info->reg_data = sm5502_reg_data; + info->num_reg_data = ARRAY_SIZE(sm5502_reg_data); + + mutex_init(&info->mutex); + + INIT_WORK(&info->irq_work, sm5502_muic_irq_work); + + info->regmap = devm_regmap_init_i2c(i2c, &sm5502_muic_regmap_config); + if (IS_ERR(info->regmap)) { + ret = PTR_ERR(info->regmap); + dev_err(info->dev, "failed to allocate register map: %d\n", + ret); + return ret; + } + + /* Support irq domain for SM5502 MUIC device */ + irq_flags = IRQF_TRIGGER_FALLING | IRQF_ONESHOT | IRQF_SHARED; + ret = regmap_add_irq_chip(info->regmap, info->irq, irq_flags, 0, + &sm5502_muic_irq_chip, &info->irq_data); + if (ret != 0) { + dev_err(info->dev, "failed to request IRQ %d: %d\n", + info->irq, ret); + return ret; + } + + for (i = 0; i < info->num_muic_irqs; i++) { + struct muic_irq *muic_irq = &info->muic_irqs[i]; + unsigned int virq = 0; + + virq = regmap_irq_get_virq(info->irq_data, muic_irq->irq); + if (virq <= 0) + return -EINVAL; + muic_irq->virq = virq; + + ret = devm_request_threaded_irq(info->dev, virq, NULL, + sm5502_muic_irq_handler, + IRQF_NO_SUSPEND, + muic_irq->name, info); + if (ret) { + dev_err(info->dev, + "failed: irq request (IRQ: %d, error :%d)\n", + muic_irq->irq, ret); + return ret; + } + } + + /* Allocate extcon device */ + info->edev = devm_extcon_dev_allocate(info->dev, sm5502_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(info->dev, "failed to allocate memory for extcon\n"); + return -ENOMEM; + } + info->edev->name = np->name; + + /* Register extcon device */ + ret = devm_extcon_dev_register(info->dev, info->edev); + if (ret) { + dev_err(info->dev, "failed to register extcon device\n"); + return ret; + } + + /* + * Detect accessory after completing the initialization of platform + * + * - Use delayed workqueue to detect cable state and then + * notify cable state to notifiee/platform through uevent. + * After completing the booting of platform, the extcon provider + * driver should notify cable state to upper layer. + */ + INIT_DELAYED_WORK(&info->wq_detcable, sm5502_muic_detect_cable_wq); + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + msecs_to_jiffies(DELAY_MS_DEFAULT)); + + /* Initialize SM5502 device and print vendor id and version id */ + sm5502_init_dev_type(info); + + return 0; +} + +static int sm5502_muic_i2c_remove(struct i2c_client *i2c) +{ + struct sm5502_muic_info *info = i2c_get_clientdata(i2c); + + regmap_del_irq_chip(info->irq, info->irq_data); + + return 0; +} + +static const struct of_device_id sm5502_dt_match[] = { + { .compatible = "siliconmitus,sm5502-muic" }, + { }, +}; + +#ifdef CONFIG_PM_SLEEP +static int sm5502_muic_suspend(struct device *dev) +{ + struct i2c_client *i2c = container_of(dev, struct i2c_client, dev); + struct sm5502_muic_info *info = i2c_get_clientdata(i2c); + + enable_irq_wake(info->irq); + + return 0; +} + +static int sm5502_muic_resume(struct device *dev) +{ + struct i2c_client *i2c = container_of(dev, struct i2c_client, dev); + struct sm5502_muic_info *info = i2c_get_clientdata(i2c); + + disable_irq_wake(info->irq); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(sm5502_muic_pm_ops, + sm5502_muic_suspend, sm5502_muic_resume); + +static const struct i2c_device_id sm5502_i2c_id[] = { + { "sm5502", TYPE_SM5502 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, sm5502_i2c_id); + +static struct i2c_driver sm5502_muic_i2c_driver = { + .driver = { + .name = "sm5502", + .owner = THIS_MODULE, + .pm = &sm5502_muic_pm_ops, + .of_match_table = sm5502_dt_match, + }, + .probe = sm5022_muic_i2c_probe, + .remove = sm5502_muic_i2c_remove, + .id_table = sm5502_i2c_id, +}; + +static int __init sm5502_muic_i2c_init(void) +{ + return i2c_add_driver(&sm5502_muic_i2c_driver); +} +subsys_initcall(sm5502_muic_i2c_init); + +MODULE_DESCRIPTION("Silicon Mitus SM5502 Extcon driver"); +MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/extcon/extcon-sm5502.h b/drivers/extcon/extcon-sm5502.h new file mode 100644 index 000000000..974b53222 --- /dev/null +++ b/drivers/extcon/extcon-sm5502.h @@ -0,0 +1,282 @@ +/* + * sm5502.h + * + * Copyright (c) 2014 Samsung Electronics Co., Ltd + * + * 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. + */ + +#ifndef __LINUX_EXTCON_SM5502_H +#define __LINUX_EXTCON_SM5502_H + +enum sm5502_types { + TYPE_SM5502, +}; + +/* SM5502 registers */ +enum sm5502_reg { + SM5502_REG_DEVICE_ID = 0x01, + SM5502_REG_CONTROL, + SM5502_REG_INT1, + SM5502_REG_INT2, + SM5502_REG_INTMASK1, + SM5502_REG_INTMASK2, + SM5502_REG_ADC, + SM5502_REG_TIMING_SET1, + SM5502_REG_TIMING_SET2, + SM5502_REG_DEV_TYPE1, + SM5502_REG_DEV_TYPE2, + SM5502_REG_BUTTON1, + SM5502_REG_BUTTON2, + SM5502_REG_CAR_KIT_STATUS, + SM5502_REG_RSVD1, + SM5502_REG_RSVD2, + SM5502_REG_RSVD3, + SM5502_REG_RSVD4, + SM5502_REG_MANUAL_SW1, + SM5502_REG_MANUAL_SW2, + SM5502_REG_DEV_TYPE3, + SM5502_REG_RSVD5, + SM5502_REG_RSVD6, + SM5502_REG_RSVD7, + SM5502_REG_RSVD8, + SM5502_REG_RSVD9, + SM5502_REG_RESET, + SM5502_REG_RSVD10, + SM5502_REG_RESERVED_ID1, + SM5502_REG_RSVD11, + SM5502_REG_RSVD12, + SM5502_REG_RESERVED_ID2, + SM5502_REG_RSVD13, + SM5502_REG_OCP, + SM5502_REG_RSVD14, + SM5502_REG_RSVD15, + SM5502_REG_RSVD16, + SM5502_REG_RSVD17, + SM5502_REG_RSVD18, + SM5502_REG_RSVD19, + SM5502_REG_RSVD20, + SM5502_REG_RSVD21, + SM5502_REG_RSVD22, + SM5502_REG_RSVD23, + SM5502_REG_RSVD24, + SM5502_REG_RSVD25, + SM5502_REG_RSVD26, + SM5502_REG_RSVD27, + SM5502_REG_RSVD28, + SM5502_REG_RSVD29, + SM5502_REG_RSVD30, + SM5502_REG_RSVD31, + SM5502_REG_RSVD32, + SM5502_REG_RSVD33, + SM5502_REG_RSVD34, + SM5502_REG_RSVD35, + SM5502_REG_RSVD36, + SM5502_REG_RESERVED_ID3, + + SM5502_REG_END, +}; + +/* Define SM5502 MASK/SHIFT constant */ +#define SM5502_REG_DEVICE_ID_VENDOR_SHIFT 0 +#define SM5502_REG_DEVICE_ID_VERSION_SHIFT 3 +#define SM5502_REG_DEVICE_ID_VENDOR_MASK (0x3 << SM5502_REG_DEVICE_ID_VENDOR_SHIFT) +#define SM5502_REG_DEVICE_ID_VERSION_MASK (0x1f << SM5502_REG_DEVICE_ID_VERSION_SHIFT) + +#define SM5502_REG_CONTROL_MASK_INT_SHIFT 0 +#define SM5502_REG_CONTROL_WAIT_SHIFT 1 +#define SM5502_REG_CONTROL_MANUAL_SW_SHIFT 2 +#define SM5502_REG_CONTROL_RAW_DATA_SHIFT 3 +#define SM5502_REG_CONTROL_SW_OPEN_SHIFT 4 +#define SM5502_REG_CONTROL_MASK_INT_MASK (0x1 << SM5502_REG_CONTROL_MASK_INT_SHIFT) +#define SM5502_REG_CONTROL_WAIT_MASK (0x1 << SM5502_REG_CONTROL_WAIT_SHIFT) +#define SM5502_REG_CONTROL_MANUAL_SW_MASK (0x1 << SM5502_REG_CONTROL_MANUAL_SW_SHIFT) +#define SM5502_REG_CONTROL_RAW_DATA_MASK (0x1 << SM5502_REG_CONTROL_RAW_DATA_SHIFT) +#define SM5502_REG_CONTROL_SW_OPEN_MASK (0x1 << SM5502_REG_CONTROL_SW_OPEN_SHIFT) + +#define SM5502_REG_INTM1_ATTACH_SHIFT 0 +#define SM5502_REG_INTM1_DETACH_SHIFT 1 +#define SM5502_REG_INTM1_KP_SHIFT 2 +#define SM5502_REG_INTM1_LKP_SHIFT 3 +#define SM5502_REG_INTM1_LKR_SHIFT 4 +#define SM5502_REG_INTM1_OVP_EVENT_SHIFT 5 +#define SM5502_REG_INTM1_OCP_EVENT_SHIFT 6 +#define SM5502_REG_INTM1_OVP_OCP_DIS_SHIFT 7 +#define SM5502_REG_INTM1_ATTACH_MASK (0x1 << SM5502_REG_INTM1_ATTACH_SHIFT) +#define SM5502_REG_INTM1_DETACH_MASK (0x1 << SM5502_REG_INTM1_DETACH_SHIFT) +#define SM5502_REG_INTM1_KP_MASK (0x1 << SM5502_REG_INTM1_KP_SHIFT) +#define SM5502_REG_INTM1_LKP_MASK (0x1 << SM5502_REG_INTM1_LKP_SHIFT) +#define SM5502_REG_INTM1_LKR_MASK (0x1 << SM5502_REG_INTM1_LKR_SHIFT) +#define SM5502_REG_INTM1_OVP_EVENT_MASK (0x1 << SM5502_REG_INTM1_OVP_EVENT_SHIFT) +#define SM5502_REG_INTM1_OCP_EVENT_MASK (0x1 << SM5502_REG_INTM1_OCP_EVENT_SHIFT) +#define SM5502_REG_INTM1_OVP_OCP_DIS_MASK (0x1 << SM5502_REG_INTM1_OVP_OCP_DIS_SHIFT) + +#define SM5502_REG_INTM2_VBUS_DET_SHIFT 0 +#define SM5502_REG_INTM2_REV_ACCE_SHIFT 1 +#define SM5502_REG_INTM2_ADC_CHG_SHIFT 2 +#define SM5502_REG_INTM2_STUCK_KEY_SHIFT 3 +#define SM5502_REG_INTM2_STUCK_KEY_RCV_SHIFT 4 +#define SM5502_REG_INTM2_MHL_SHIFT 5 +#define SM5502_REG_INTM2_VBUS_DET_MASK (0x1 << SM5502_REG_INTM2_VBUS_DET_SHIFT) +#define SM5502_REG_INTM2_REV_ACCE_MASK (0x1 << SM5502_REG_INTM2_REV_ACCE_SHIFT) +#define SM5502_REG_INTM2_ADC_CHG_MASK (0x1 << SM5502_REG_INTM2_ADC_CHG_SHIFT) +#define SM5502_REG_INTM2_STUCK_KEY_MASK (0x1 << SM5502_REG_INTM2_STUCK_KEY_SHIFT) +#define SM5502_REG_INTM2_STUCK_KEY_RCV_MASK (0x1 << SM5502_REG_INTM2_STUCK_KEY_RCV_SHIFT) +#define SM5502_REG_INTM2_MHL_MASK (0x1 << SM5502_REG_INTM2_MHL_SHIFT) + +#define SM5502_REG_ADC_SHIFT 0 +#define SM5502_REG_ADC_MASK (0x1f << SM5502_REG_ADC_SHIFT) + +#define SM5502_REG_TIMING_SET1_KEY_PRESS_SHIFT 4 +#define SM5502_REG_TIMING_SET1_KEY_PRESS_MASK (0xf << SM5502_REG_TIMING_SET1_KEY_PRESS_SHIFT) +#define TIMING_KEY_PRESS_100MS 0x0 +#define TIMING_KEY_PRESS_200MS 0x1 +#define TIMING_KEY_PRESS_300MS 0x2 +#define TIMING_KEY_PRESS_400MS 0x3 +#define TIMING_KEY_PRESS_500MS 0x4 +#define TIMING_KEY_PRESS_600MS 0x5 +#define TIMING_KEY_PRESS_700MS 0x6 +#define TIMING_KEY_PRESS_800MS 0x7 +#define TIMING_KEY_PRESS_900MS 0x8 +#define TIMING_KEY_PRESS_1000MS 0x9 +#define SM5502_REG_TIMING_SET1_ADC_DET_SHIFT 0 +#define SM5502_REG_TIMING_SET1_ADC_DET_MASK (0xf << SM5502_REG_TIMING_SET1_ADC_DET_SHIFT) +#define TIMING_ADC_DET_50MS 0x0 +#define TIMING_ADC_DET_100MS 0x1 +#define TIMING_ADC_DET_150MS 0x2 +#define TIMING_ADC_DET_200MS 0x3 +#define TIMING_ADC_DET_300MS 0x4 +#define TIMING_ADC_DET_400MS 0x5 +#define TIMING_ADC_DET_500MS 0x6 +#define TIMING_ADC_DET_600MS 0x7 +#define TIMING_ADC_DET_700MS 0x8 +#define TIMING_ADC_DET_800MS 0x9 +#define TIMING_ADC_DET_900MS 0xA +#define TIMING_ADC_DET_1000MS 0xB + +#define SM5502_REG_TIMING_SET2_SW_WAIT_SHIFT 4 +#define SM5502_REG_TIMING_SET2_SW_WAIT_MASK (0xf << SM5502_REG_TIMING_SET2_SW_WAIT_SHIFT) +#define TIMING_SW_WAIT_10MS 0x0 +#define TIMING_SW_WAIT_30MS 0x1 +#define TIMING_SW_WAIT_50MS 0x2 +#define TIMING_SW_WAIT_70MS 0x3 +#define TIMING_SW_WAIT_90MS 0x4 +#define TIMING_SW_WAIT_110MS 0x5 +#define TIMING_SW_WAIT_130MS 0x6 +#define TIMING_SW_WAIT_150MS 0x7 +#define TIMING_SW_WAIT_170MS 0x8 +#define TIMING_SW_WAIT_190MS 0x9 +#define TIMING_SW_WAIT_210MS 0xA +#define SM5502_REG_TIMING_SET2_LONG_KEY_SHIFT 0 +#define SM5502_REG_TIMING_SET2_LONG_KEY_MASK (0xf << SM5502_REG_TIMING_SET2_LONG_KEY_SHIFT) +#define TIMING_LONG_KEY_300MS 0x0 +#define TIMING_LONG_KEY_400MS 0x1 +#define TIMING_LONG_KEY_500MS 0x2 +#define TIMING_LONG_KEY_600MS 0x3 +#define TIMING_LONG_KEY_700MS 0x4 +#define TIMING_LONG_KEY_800MS 0x5 +#define TIMING_LONG_KEY_900MS 0x6 +#define TIMING_LONG_KEY_1000MS 0x7 +#define TIMING_LONG_KEY_1100MS 0x8 +#define TIMING_LONG_KEY_1200MS 0x9 +#define TIMING_LONG_KEY_1300MS 0xA +#define TIMING_LONG_KEY_1400MS 0xB +#define TIMING_LONG_KEY_1500MS 0xC + +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE1_SHIFT 0 +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE2_SHIFT 1 +#define SM5502_REG_DEV_TYPE1_USB_SDP_SHIFT 2 +#define SM5502_REG_DEV_TYPE1_UART_SHIFT 3 +#define SM5502_REG_DEV_TYPE1_CAR_KIT_CHARGER_SHIFT 4 +#define SM5502_REG_DEV_TYPE1_USB_CHG_SHIFT 5 +#define SM5502_REG_DEV_TYPE1_DEDICATED_CHG_SHIFT 6 +#define SM5502_REG_DEV_TYPE1_USB_OTG_SHIFT 7 +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE1_MASK (0x1 << SM5502_REG_DEV_TYPE1_AUDIO_TYPE1_SHIFT) +#define SM5502_REG_DEV_TYPE1_AUDIO_TYPE1__MASK (0x1 << SM5502_REG_DEV_TYPE1_AUDIO_TYPE2_SHIFT) +#define SM5502_REG_DEV_TYPE1_USB_SDP_MASK (0x1 << SM5502_REG_DEV_TYPE1_USB_SDP_SHIFT) +#define SM5502_REG_DEV_TYPE1_UART_MASK (0x1 << SM5502_REG_DEV_TYPE1_UART_SHIFT) +#define SM5502_REG_DEV_TYPE1_CAR_KIT_CHARGER_MASK (0x1 << SM5502_REG_DEV_TYPE1_CAR_KIT_CHARGER_SHIFT) +#define SM5502_REG_DEV_TYPE1_USB_CHG_MASK (0x1 << SM5502_REG_DEV_TYPE1_USB_CHG_SHIFT) +#define SM5502_REG_DEV_TYPE1_DEDICATED_CHG_MASK (0x1 << SM5502_REG_DEV_TYPE1_DEDICATED_CHG_SHIFT) +#define SM5502_REG_DEV_TYPE1_USB_OTG_MASK (0x1 << SM5502_REG_DEV_TYPE1_USB_OTG_SHIFT) + +#define SM5502_REG_DEV_TYPE2_JIG_USB_ON_SHIFT 0 +#define SM5502_REG_DEV_TYPE2_JIG_USB_OFF_SHIFT 1 +#define SM5502_REG_DEV_TYPE2_JIG_UART_ON_SHIFT 2 +#define SM5502_REG_DEV_TYPE2_JIG_UART_OFF_SHIFT 3 +#define SM5502_REG_DEV_TYPE2_PPD_SHIFT 4 +#define SM5502_REG_DEV_TYPE2_TTY_SHIFT 5 +#define SM5502_REG_DEV_TYPE2_AV_CABLE_SHIFT 6 +#define SM5502_REG_DEV_TYPE2_JIG_USB_ON_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_USB_ON_SHIFT) +#define SM5502_REG_DEV_TYPE2_JIG_USB_OFF_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_USB_OFF_SHIFT) +#define SM5502_REG_DEV_TYPE2_JIG_UART_ON_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_UART_ON_SHIFT) +#define SM5502_REG_DEV_TYPE2_JIG_UART_OFF_MASK (0x1 << SM5502_REG_DEV_TYPE2_JIG_UART_OFF_SHIFT) +#define SM5502_REG_DEV_TYPE2_PPD_MASK (0x1 << SM5502_REG_DEV_TYPE2_PPD_SHIFT) +#define SM5502_REG_DEV_TYPE2_TTY_MASK (0x1 << SM5502_REG_DEV_TYPE2_TTY_SHIFT) +#define SM5502_REG_DEV_TYPE2_AV_CABLE_MASK (0x1 << SM5502_REG_DEV_TYPE2_AV_CABLE_SHIFT) + +#define SM5502_REG_MANUAL_SW1_VBUSIN_SHIFT 0 +#define SM5502_REG_MANUAL_SW1_DP_SHIFT 2 +#define SM5502_REG_MANUAL_SW1_DM_SHIFT 5 +#define SM5502_REG_MANUAL_SW1_VBUSIN_MASK (0x3 << SM5502_REG_MANUAL_SW1_VBUSIN_SHIFT) +#define SM5502_REG_MANUAL_SW1_DP_MASK (0x7 << SM5502_REG_MANUAL_SW1_DP_SHIFT) +#define SM5502_REG_MANUAL_SW1_DM_MASK (0x7 << SM5502_REG_MANUAL_SW1_DM_SHIFT) +#define VBUSIN_SWITCH_OPEN 0x0 +#define VBUSIN_SWITCH_VBUSOUT 0x1 +#define VBUSIN_SWITCH_MIC 0x2 +#define VBUSIN_SWITCH_VBUSOUT_WITH_USB 0x3 +#define DM_DP_CON_SWITCH_OPEN 0x0 +#define DM_DP_CON_SWITCH_USB 0x1 +#define DM_DP_CON_SWITCH_AUDIO 0x2 +#define DM_DP_CON_SWITCH_UART 0x3 +#define DM_DP_SWITCH_OPEN ((DM_DP_CON_SWITCH_OPEN <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_OPEN <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_USB ((DM_DP_CON_SWITCH_USB <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_USB <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_AUDIO ((DM_DP_CON_SWITCH_AUDIO <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_AUDIO <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) +#define DM_DP_SWITCH_UART ((DM_DP_CON_SWITCH_UART <<SM5502_REG_MANUAL_SW1_DP_SHIFT) \ + | (DM_DP_CON_SWITCH_UART <<SM5502_REG_MANUAL_SW1_DM_SHIFT)) + +/* SM5502 Interrupts */ +enum sm5502_irq { + /* INT1 */ + SM5502_IRQ_INT1_ATTACH, + SM5502_IRQ_INT1_DETACH, + SM5502_IRQ_INT1_KP, + SM5502_IRQ_INT1_LKP, + SM5502_IRQ_INT1_LKR, + SM5502_IRQ_INT1_OVP_EVENT, + SM5502_IRQ_INT1_OCP_EVENT, + SM5502_IRQ_INT1_OVP_OCP_DIS, + + /* INT2 */ + SM5502_IRQ_INT2_VBUS_DET, + SM5502_IRQ_INT2_REV_ACCE, + SM5502_IRQ_INT2_ADC_CHG, + SM5502_IRQ_INT2_STUCK_KEY, + SM5502_IRQ_INT2_STUCK_KEY_RCV, + SM5502_IRQ_INT2_MHL, + + SM5502_IRQ_NUM, +}; + +#define SM5502_IRQ_INT1_ATTACH_MASK BIT(0) +#define SM5502_IRQ_INT1_DETACH_MASK BIT(1) +#define SM5502_IRQ_INT1_KP_MASK BIT(2) +#define SM5502_IRQ_INT1_LKP_MASK BIT(3) +#define SM5502_IRQ_INT1_LKR_MASK BIT(4) +#define SM5502_IRQ_INT1_OVP_EVENT_MASK BIT(5) +#define SM5502_IRQ_INT1_OCP_EVENT_MASK BIT(6) +#define SM5502_IRQ_INT1_OVP_OCP_DIS_MASK BIT(7) +#define SM5502_IRQ_INT2_VBUS_DET_MASK BIT(0) +#define SM5502_IRQ_INT2_REV_ACCE_MASK BIT(1) +#define SM5502_IRQ_INT2_ADC_CHG_MASK BIT(2) +#define SM5502_IRQ_INT2_STUCK_KEY_MASK BIT(3) +#define SM5502_IRQ_INT2_STUCK_KEY_RCV_MASK BIT(4) +#define SM5502_IRQ_INT2_MHL_MASK BIT(5) + +#endif /* __LINUX_EXTCON_SM5502_H */ diff --git a/drivers/extcon/extcon-usb-gpio.c b/drivers/extcon/extcon-usb-gpio.c new file mode 100644 index 000000000..e45d1f13f --- /dev/null +++ b/drivers/extcon/extcon-usb-gpio.c @@ -0,0 +1,237 @@ +/** + * drivers/extcon/extcon-usb-gpio.c - USB GPIO extcon driver + * + * Copyright (C) 2015 Texas Instruments Incorporated - http://www.ti.com + * Author: Roger Quadros <rogerq@ti.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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/extcon.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/irq.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_gpio.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +#define USB_GPIO_DEBOUNCE_MS 20 /* ms */ + +struct usb_extcon_info { + struct device *dev; + struct extcon_dev *edev; + + struct gpio_desc *id_gpiod; + int id_irq; + + unsigned long debounce_jiffies; + struct delayed_work wq_detcable; +}; + +/* List of detectable cables */ +enum { + EXTCON_CABLE_USB = 0, + EXTCON_CABLE_USB_HOST, + + EXTCON_CABLE_END, +}; + +static const char *usb_extcon_cable[] = { + [EXTCON_CABLE_USB] = "USB", + [EXTCON_CABLE_USB_HOST] = "USB-HOST", + NULL, +}; + +static void usb_extcon_detect_cable(struct work_struct *work) +{ + int id; + struct usb_extcon_info *info = container_of(to_delayed_work(work), + struct usb_extcon_info, + wq_detcable); + + /* check ID and update cable state */ + id = gpiod_get_value_cansleep(info->id_gpiod); + if (id) { + /* + * ID = 1 means USB HOST cable detached. + * As we don't have event for USB peripheral cable attached, + * we simulate USB peripheral attach here. + */ + extcon_set_cable_state(info->edev, + usb_extcon_cable[EXTCON_CABLE_USB_HOST], + false); + extcon_set_cable_state(info->edev, + usb_extcon_cable[EXTCON_CABLE_USB], + true); + } else { + /* + * ID = 0 means USB HOST cable attached. + * As we don't have event for USB peripheral cable detached, + * we simulate USB peripheral detach here. + */ + extcon_set_cable_state(info->edev, + usb_extcon_cable[EXTCON_CABLE_USB], + false); + extcon_set_cable_state(info->edev, + usb_extcon_cable[EXTCON_CABLE_USB_HOST], + true); + } +} + +static irqreturn_t usb_irq_handler(int irq, void *dev_id) +{ + struct usb_extcon_info *info = dev_id; + + queue_delayed_work(system_power_efficient_wq, &info->wq_detcable, + info->debounce_jiffies); + + return IRQ_HANDLED; +} + +static int usb_extcon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + struct usb_extcon_info *info; + int ret; + + if (!np) + return -EINVAL; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->dev = dev; + info->id_gpiod = devm_gpiod_get(&pdev->dev, "id"); + if (IS_ERR(info->id_gpiod)) { + dev_err(dev, "failed to get ID GPIO\n"); + return PTR_ERR(info->id_gpiod); + } + + info->edev = devm_extcon_dev_allocate(dev, usb_extcon_cable); + if (IS_ERR(info->edev)) { + dev_err(dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + ret = devm_extcon_dev_register(dev, info->edev); + if (ret < 0) { + dev_err(dev, "failed to register extcon device\n"); + return ret; + } + + ret = gpiod_set_debounce(info->id_gpiod, + USB_GPIO_DEBOUNCE_MS * 1000); + if (ret < 0) + info->debounce_jiffies = msecs_to_jiffies(USB_GPIO_DEBOUNCE_MS); + + INIT_DELAYED_WORK(&info->wq_detcable, usb_extcon_detect_cable); + + info->id_irq = gpiod_to_irq(info->id_gpiod); + if (info->id_irq < 0) { + dev_err(dev, "failed to get ID IRQ\n"); + return info->id_irq; + } + + ret = devm_request_threaded_irq(dev, info->id_irq, NULL, + usb_irq_handler, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + pdev->name, info); + if (ret < 0) { + dev_err(dev, "failed to request handler for ID IRQ\n"); + return ret; + } + + platform_set_drvdata(pdev, info); + device_init_wakeup(dev, 1); + + /* Perform initial detection */ + usb_extcon_detect_cable(&info->wq_detcable.work); + + return 0; +} + +static int usb_extcon_remove(struct platform_device *pdev) +{ + struct usb_extcon_info *info = platform_get_drvdata(pdev); + + cancel_delayed_work_sync(&info->wq_detcable); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int usb_extcon_suspend(struct device *dev) +{ + struct usb_extcon_info *info = dev_get_drvdata(dev); + int ret = 0; + + if (device_may_wakeup(dev)) { + ret = enable_irq_wake(info->id_irq); + if (ret) + return ret; + } + + /* + * We don't want to process any IRQs after this point + * as GPIOs used behind I2C subsystem might not be + * accessible until resume completes. So disable IRQ. + */ + disable_irq(info->id_irq); + + return ret; +} + +static int usb_extcon_resume(struct device *dev) +{ + struct usb_extcon_info *info = dev_get_drvdata(dev); + int ret = 0; + + if (device_may_wakeup(dev)) { + ret = disable_irq_wake(info->id_irq); + if (ret) + return ret; + } + + enable_irq(info->id_irq); + + return ret; +} +#endif + +static SIMPLE_DEV_PM_OPS(usb_extcon_pm_ops, + usb_extcon_suspend, usb_extcon_resume); + +static const struct of_device_id usb_extcon_dt_match[] = { + { .compatible = "linux,extcon-usb-gpio", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, usb_extcon_dt_match); + +static struct platform_driver usb_extcon_driver = { + .probe = usb_extcon_probe, + .remove = usb_extcon_remove, + .driver = { + .name = "extcon-usb-gpio", + .pm = &usb_extcon_pm_ops, + .of_match_table = usb_extcon_dt_match, + }, +}; + +module_platform_driver(usb_extcon_driver); + +MODULE_AUTHOR("Roger Quadros <rogerq@ti.com>"); +MODULE_DESCRIPTION("USB GPIO extcon driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/extcon/extcon.c b/drivers/extcon/extcon.c new file mode 100644 index 000000000..4c9f165e4 --- /dev/null +++ b/drivers/extcon/extcon.c @@ -0,0 +1,1066 @@ +/* + * drivers/extcon/extcon_class.c + * + * External connector (extcon) class driver + * + * Copyright (C) 2012 Samsung Electronics + * Author: Donggeun Kim <dg77.kim@samsung.com> + * Author: MyungJoo Ham <myungjoo.ham@samsung.com> + * + * based on android/drivers/switch/switch_class.c + * Copyright (C) 2008 Google, Inc. + * Author: Mike Lockwood <lockwood@android.com> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * 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/module.h> +#include <linux/types.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/err.h> +#include <linux/extcon.h> +#include <linux/of.h> +#include <linux/slab.h> +#include <linux/sysfs.h> + +/* + * extcon_cable_name suggests the standard cable names for commonly used + * cable types. + * + * However, please do not use extcon_cable_name directly for extcon_dev + * struct's supported_cable pointer unless your device really supports + * every single port-type of the following cable names. Please choose cable + * names that are actually used in your extcon device. + */ +const char extcon_cable_name[][CABLE_NAME_MAX + 1] = { + [EXTCON_USB] = "USB", + [EXTCON_USB_HOST] = "USB-Host", + [EXTCON_TA] = "TA", + [EXTCON_FAST_CHARGER] = "Fast-charger", + [EXTCON_SLOW_CHARGER] = "Slow-charger", + [EXTCON_CHARGE_DOWNSTREAM] = "Charge-downstream", + [EXTCON_HDMI] = "HDMI", + [EXTCON_MHL] = "MHL", + [EXTCON_DVI] = "DVI", + [EXTCON_VGA] = "VGA", + [EXTCON_DOCK] = "Dock", + [EXTCON_LINE_IN] = "Line-in", + [EXTCON_LINE_OUT] = "Line-out", + [EXTCON_MIC_IN] = "Microphone", + [EXTCON_HEADPHONE_OUT] = "Headphone", + [EXTCON_SPDIF_IN] = "SPDIF-in", + [EXTCON_SPDIF_OUT] = "SPDIF-out", + [EXTCON_VIDEO_IN] = "Video-in", + [EXTCON_VIDEO_OUT] = "Video-out", + [EXTCON_MECHANICAL] = "Mechanical", +}; + +static struct class *extcon_class; +#if defined(CONFIG_ANDROID) +static struct class_compat *switch_class; +#endif /* CONFIG_ANDROID */ + +static LIST_HEAD(extcon_dev_list); +static DEFINE_MUTEX(extcon_dev_list_lock); + +/** + * check_mutually_exclusive - Check if new_state violates mutually_exclusive + * condition. + * @edev: the extcon device + * @new_state: new cable attach status for @edev + * + * Returns 0 if nothing violates. Returns the index + 1 for the first + * violated condition. + */ +static int check_mutually_exclusive(struct extcon_dev *edev, u32 new_state) +{ + int i = 0; + + if (!edev->mutually_exclusive) + return 0; + + for (i = 0; edev->mutually_exclusive[i]; i++) { + int weight; + u32 correspondants = new_state & edev->mutually_exclusive[i]; + + /* calculate the total number of bits set */ + weight = hweight32(correspondants); + if (weight > 1) + return i + 1; + } + + return 0; +} + +static ssize_t state_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + int i, count = 0; + struct extcon_dev *edev = dev_get_drvdata(dev); + + if (edev->print_state) { + int ret = edev->print_state(edev, buf); + + if (ret >= 0) + return ret; + /* Use default if failed */ + } + + if (edev->max_supported == 0) + return sprintf(buf, "%u\n", edev->state); + + for (i = 0; i < SUPPORTED_CABLE_MAX; i++) { + if (!edev->supported_cable[i]) + break; + count += sprintf(buf + count, "%s=%d\n", + edev->supported_cable[i], + !!(edev->state & (1 << i))); + } + + return count; +} + +static ssize_t state_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + u32 state; + ssize_t ret = 0; + struct extcon_dev *edev = dev_get_drvdata(dev); + + ret = sscanf(buf, "0x%x", &state); + if (ret == 0) + ret = -EINVAL; + else + ret = extcon_set_state(edev, state); + + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_RW(state); + +static ssize_t name_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct extcon_dev *edev = dev_get_drvdata(dev); + + /* Optional callback given by the user */ + if (edev->print_name) { + int ret = edev->print_name(edev, buf); + + if (ret >= 0) + return ret; + } + + return sprintf(buf, "%s\n", dev_name(&edev->dev)); +} +static DEVICE_ATTR_RO(name); + +static ssize_t cable_name_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct extcon_cable *cable = container_of(attr, struct extcon_cable, + attr_name); + + return sprintf(buf, "%s\n", + cable->edev->supported_cable[cable->cable_index]); +} + +static ssize_t cable_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct extcon_cable *cable = container_of(attr, struct extcon_cable, + attr_state); + + return sprintf(buf, "%d\n", + extcon_get_cable_state_(cable->edev, + cable->cable_index)); +} + +/** + * extcon_update_state() - Update the cable attach states of the extcon device + * only for the masked bits. + * @edev: the extcon device + * @mask: the bit mask to designate updated bits. + * @state: new cable attach status for @edev + * + * Changing the state sends uevent with environment variable containing + * the name of extcon device (envp[0]) and the state output (envp[1]). + * Tizen uses this format for extcon device to get events from ports. + * Android uses this format as well. + * + * Note that the notifier provides which bits are changed in the state + * variable with the val parameter (second) to the callback. + */ +int extcon_update_state(struct extcon_dev *edev, u32 mask, u32 state) +{ + char name_buf[120]; + char state_buf[120]; + char *prop_buf; + char *envp[3]; + int env_offset = 0; + int length; + unsigned long flags; + + spin_lock_irqsave(&edev->lock, flags); + + if (edev->state != ((edev->state & ~mask) | (state & mask))) { + u32 old_state = edev->state; + + if (check_mutually_exclusive(edev, (edev->state & ~mask) | + (state & mask))) { + spin_unlock_irqrestore(&edev->lock, flags); + return -EPERM; + } + + edev->state &= ~mask; + edev->state |= state & mask; + + raw_notifier_call_chain(&edev->nh, old_state, edev); + /* This could be in interrupt handler */ + prop_buf = (char *)get_zeroed_page(GFP_ATOMIC); + if (prop_buf) { + length = name_show(&edev->dev, NULL, prop_buf); + if (length > 0) { + if (prop_buf[length - 1] == '\n') + prop_buf[length - 1] = 0; + snprintf(name_buf, sizeof(name_buf), + "NAME=%s", prop_buf); + envp[env_offset++] = name_buf; + } + length = state_show(&edev->dev, NULL, prop_buf); + if (length > 0) { + if (prop_buf[length - 1] == '\n') + prop_buf[length - 1] = 0; + snprintf(state_buf, sizeof(state_buf), + "STATE=%s", prop_buf); + envp[env_offset++] = state_buf; + } + envp[env_offset] = NULL; + /* Unlock early before uevent */ + spin_unlock_irqrestore(&edev->lock, flags); + + kobject_uevent_env(&edev->dev.kobj, KOBJ_CHANGE, envp); + free_page((unsigned long)prop_buf); + } else { + /* Unlock early before uevent */ + spin_unlock_irqrestore(&edev->lock, flags); + + dev_err(&edev->dev, "out of memory in extcon_set_state\n"); + kobject_uevent(&edev->dev.kobj, KOBJ_CHANGE); + } + } else { + /* No changes */ + spin_unlock_irqrestore(&edev->lock, flags); + } + + return 0; +} +EXPORT_SYMBOL_GPL(extcon_update_state); + +/** + * extcon_set_state() - Set the cable attach states of the extcon device. + * @edev: the extcon device + * @state: new cable attach status for @edev + * + * Note that notifier provides which bits are changed in the state + * variable with the val parameter (second) to the callback. + */ +int extcon_set_state(struct extcon_dev *edev, u32 state) +{ + return extcon_update_state(edev, 0xffffffff, state); +} +EXPORT_SYMBOL_GPL(extcon_set_state); + +/** + * extcon_find_cable_index() - Get the cable index based on the cable name. + * @edev: the extcon device that has the cable. + * @cable_name: cable name to be searched. + * + * Note that accessing a cable state based on cable_index is faster than + * cable_name because using cable_name induces a loop with strncmp(). + * Thus, when get/set_cable_state is repeatedly used, using cable_index + * is recommended. + */ +int extcon_find_cable_index(struct extcon_dev *edev, const char *cable_name) +{ + int i; + + if (edev->supported_cable) { + for (i = 0; edev->supported_cable[i]; i++) { + if (!strncmp(edev->supported_cable[i], + cable_name, CABLE_NAME_MAX)) + return i; + } + } + + return -EINVAL; +} +EXPORT_SYMBOL_GPL(extcon_find_cable_index); + +/** + * extcon_get_cable_state_() - Get the status of a specific cable. + * @edev: the extcon device that has the cable. + * @index: cable index that can be retrieved by extcon_find_cable_index(). + */ +int extcon_get_cable_state_(struct extcon_dev *edev, int index) +{ + if (index < 0 || (edev->max_supported && edev->max_supported <= index)) + return -EINVAL; + + return !!(edev->state & (1 << index)); +} +EXPORT_SYMBOL_GPL(extcon_get_cable_state_); + +/** + * extcon_get_cable_state() - Get the status of a specific cable. + * @edev: the extcon device that has the cable. + * @cable_name: cable name. + * + * Note that this is slower than extcon_get_cable_state_. + */ +int extcon_get_cable_state(struct extcon_dev *edev, const char *cable_name) +{ + return extcon_get_cable_state_(edev, extcon_find_cable_index + (edev, cable_name)); +} +EXPORT_SYMBOL_GPL(extcon_get_cable_state); + +/** + * extcon_set_cable_state_() - Set the status of a specific cable. + * @edev: the extcon device that has the cable. + * @index: cable index that can be retrieved by + * extcon_find_cable_index(). + * @cable_state: the new cable status. The default semantics is + * true: attached / false: detached. + */ +int extcon_set_cable_state_(struct extcon_dev *edev, + int index, bool cable_state) +{ + u32 state; + + if (index < 0 || (edev->max_supported && edev->max_supported <= index)) + return -EINVAL; + + state = cable_state ? (1 << index) : 0; + return extcon_update_state(edev, 1 << index, state); +} +EXPORT_SYMBOL_GPL(extcon_set_cable_state_); + +/** + * extcon_set_cable_state() - Set the status of a specific cable. + * @edev: the extcon device that has the cable. + * @cable_name: cable name. + * @cable_state: the new cable status. The default semantics is + * true: attached / false: detached. + * + * Note that this is slower than extcon_set_cable_state_. + */ +int extcon_set_cable_state(struct extcon_dev *edev, + const char *cable_name, bool cable_state) +{ + return extcon_set_cable_state_(edev, extcon_find_cable_index + (edev, cable_name), cable_state); +} +EXPORT_SYMBOL_GPL(extcon_set_cable_state); + +/** + * extcon_get_extcon_dev() - Get the extcon device instance from the name + * @extcon_name: The extcon name provided with extcon_dev_register() + */ +struct extcon_dev *extcon_get_extcon_dev(const char *extcon_name) +{ + struct extcon_dev *sd; + + mutex_lock(&extcon_dev_list_lock); + list_for_each_entry(sd, &extcon_dev_list, entry) { + if (!strcmp(sd->name, extcon_name)) + goto out; + } + sd = NULL; +out: + mutex_unlock(&extcon_dev_list_lock); + return sd; +} +EXPORT_SYMBOL_GPL(extcon_get_extcon_dev); + +static int _call_per_cable(struct notifier_block *nb, unsigned long val, + void *ptr) +{ + struct extcon_specific_cable_nb *obj = container_of(nb, + struct extcon_specific_cable_nb, internal_nb); + struct extcon_dev *edev = ptr; + + if ((val & (1 << obj->cable_index)) != + (edev->state & (1 << obj->cable_index))) { + bool cable_state = true; + + obj->previous_value = val; + + if (val & (1 << obj->cable_index)) + cable_state = false; + + return obj->user_nb->notifier_call(obj->user_nb, + cable_state, ptr); + } + + return NOTIFY_OK; +} + +/** + * extcon_register_interest() - Register a notifier for a state change of a + * specific cable, not an entier set of cables of a + * extcon device. + * @obj: an empty extcon_specific_cable_nb object to be returned. + * @extcon_name: the name of extcon device. + * if NULL, extcon_register_interest will register + * every cable with the target cable_name given. + * @cable_name: the target cable name. + * @nb: the notifier block to get notified. + * + * Provide an empty extcon_specific_cable_nb. extcon_register_interest() sets + * the struct for you. + * + * extcon_register_interest is a helper function for those who want to get + * notification for a single specific cable's status change. If a user wants + * to get notification for any changes of all cables of a extcon device, + * he/she should use the general extcon_register_notifier(). + * + * Note that the second parameter given to the callback of nb (val) is + * "old_state", not the current state. The current state can be retrieved + * by looking at the third pameter (edev pointer)'s state value. + */ +int extcon_register_interest(struct extcon_specific_cable_nb *obj, + const char *extcon_name, const char *cable_name, + struct notifier_block *nb) +{ + unsigned long flags; + int ret; + + if (!obj || !cable_name || !nb) + return -EINVAL; + + if (extcon_name) { + obj->edev = extcon_get_extcon_dev(extcon_name); + if (!obj->edev) + return -ENODEV; + + obj->cable_index = extcon_find_cable_index(obj->edev, + cable_name); + if (obj->cable_index < 0) + return obj->cable_index; + + obj->user_nb = nb; + + obj->internal_nb.notifier_call = _call_per_cable; + + spin_lock_irqsave(&obj->edev->lock, flags); + ret = raw_notifier_chain_register(&obj->edev->nh, + &obj->internal_nb); + spin_unlock_irqrestore(&obj->edev->lock, flags); + return ret; + } else { + struct class_dev_iter iter; + struct extcon_dev *extd; + struct device *dev; + + if (!extcon_class) + return -ENODEV; + class_dev_iter_init(&iter, extcon_class, NULL, NULL); + while ((dev = class_dev_iter_next(&iter))) { + extd = dev_get_drvdata(dev); + + if (extcon_find_cable_index(extd, cable_name) < 0) + continue; + + class_dev_iter_exit(&iter); + return extcon_register_interest(obj, extd->name, + cable_name, nb); + } + + return -ENODEV; + } +} +EXPORT_SYMBOL_GPL(extcon_register_interest); + +/** + * extcon_unregister_interest() - Unregister the notifier registered by + * extcon_register_interest(). + * @obj: the extcon_specific_cable_nb object returned by + * extcon_register_interest(). + */ +int extcon_unregister_interest(struct extcon_specific_cable_nb *obj) +{ + unsigned long flags; + int ret; + + if (!obj) + return -EINVAL; + + spin_lock_irqsave(&obj->edev->lock, flags); + ret = raw_notifier_chain_unregister(&obj->edev->nh, &obj->internal_nb); + spin_unlock_irqrestore(&obj->edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_unregister_interest); + +/** + * extcon_register_notifier() - Register a notifiee to get notified by + * any attach status changes from the extcon. + * @edev: the extcon device. + * @nb: a notifier block to be registered. + * + * Note that the second parameter given to the callback of nb (val) is + * "old_state", not the current state. The current state can be retrieved + * by looking at the third pameter (edev pointer)'s state value. + */ +int extcon_register_notifier(struct extcon_dev *edev, + struct notifier_block *nb) +{ + unsigned long flags; + int ret; + + spin_lock_irqsave(&edev->lock, flags); + ret = raw_notifier_chain_register(&edev->nh, nb); + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_register_notifier); + +/** + * extcon_unregister_notifier() - Unregister a notifiee from the extcon device. + * @edev: the extcon device. + * @nb: a registered notifier block to be unregistered. + */ +int extcon_unregister_notifier(struct extcon_dev *edev, + struct notifier_block *nb) +{ + unsigned long flags; + int ret; + + spin_lock_irqsave(&edev->lock, flags); + ret = raw_notifier_chain_unregister(&edev->nh, nb); + spin_unlock_irqrestore(&edev->lock, flags); + + return ret; +} +EXPORT_SYMBOL_GPL(extcon_unregister_notifier); + +static struct attribute *extcon_attrs[] = { + &dev_attr_state.attr, + &dev_attr_name.attr, + NULL, +}; +ATTRIBUTE_GROUPS(extcon); + +static int create_extcon_class(void) +{ + if (!extcon_class) { + extcon_class = class_create(THIS_MODULE, "extcon"); + if (IS_ERR(extcon_class)) + return PTR_ERR(extcon_class); + extcon_class->dev_groups = extcon_groups; + +#if defined(CONFIG_ANDROID) + switch_class = class_compat_register("switch"); + if (WARN(!switch_class, "cannot allocate")) + return -ENOMEM; +#endif /* CONFIG_ANDROID */ + } + + return 0; +} + +static void extcon_dev_release(struct device *dev) +{ +} + +static const char *muex_name = "mutually_exclusive"; +static void dummy_sysfs_dev_release(struct device *dev) +{ +} + +/* + * extcon_dev_allocate() - Allocate the memory of extcon device. + * @supported_cable: Array of supported cable names ending with NULL. + * If supported_cable is NULL, cable name related APIs + * are disabled. + * + * This function allocates the memory for extcon device without allocating + * memory in each extcon provider driver and initialize default setting for + * extcon device. + * + * Return the pointer of extcon device if success or ERR_PTR(err) if fail + */ +struct extcon_dev *extcon_dev_allocate(const char **supported_cable) +{ + struct extcon_dev *edev; + + edev = kzalloc(sizeof(*edev), GFP_KERNEL); + if (!edev) + return ERR_PTR(-ENOMEM); + + edev->max_supported = 0; + edev->supported_cable = supported_cable; + + return edev; +} + +/* + * extcon_dev_free() - Free the memory of extcon device. + * @edev: the extcon device to free + */ +void extcon_dev_free(struct extcon_dev *edev) +{ + kfree(edev); +} +EXPORT_SYMBOL_GPL(extcon_dev_free); + +static int devm_extcon_dev_match(struct device *dev, void *res, void *data) +{ + struct extcon_dev **r = res; + + if (WARN_ON(!r || !*r)) + return 0; + + return *r == data; +} + +static void devm_extcon_dev_release(struct device *dev, void *res) +{ + extcon_dev_free(*(struct extcon_dev **)res); +} + +/** + * devm_extcon_dev_allocate - Allocate managed extcon device + * @dev: device owning the extcon device being created + * @supported_cable: Array of supported cable names ending with NULL. + * If supported_cable is NULL, cable name related APIs + * are disabled. + * + * This function manages automatically the memory of extcon device using device + * resource management and simplify the control of freeing the memory of extcon + * device. + * + * Returns the pointer memory of allocated extcon_dev if success + * or ERR_PTR(err) if fail + */ +struct extcon_dev *devm_extcon_dev_allocate(struct device *dev, + const char **supported_cable) +{ + struct extcon_dev **ptr, *edev; + + ptr = devres_alloc(devm_extcon_dev_release, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + edev = extcon_dev_allocate(supported_cable); + if (IS_ERR(edev)) { + devres_free(ptr); + return edev; + } + + edev->dev.parent = dev; + + *ptr = edev; + devres_add(dev, ptr); + + return edev; +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_allocate); + +void devm_extcon_dev_free(struct device *dev, struct extcon_dev *edev) +{ + WARN_ON(devres_release(dev, devm_extcon_dev_release, + devm_extcon_dev_match, edev)); +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_free); + +/** + * extcon_dev_register() - Register a new extcon device + * @edev : the new extcon device (should be allocated before calling) + * + * Among the members of edev struct, please set the "user initializing data" + * in any case and set the "optional callbacks" if required. However, please + * do not set the values of "internal data", which are initialized by + * this function. + */ +int extcon_dev_register(struct extcon_dev *edev) +{ + int ret, index = 0; + + if (!extcon_class) { + ret = create_extcon_class(); + if (ret < 0) + return ret; + } + + if (edev->supported_cable) { + /* Get size of array */ + for (index = 0; edev->supported_cable[index]; index++) + ; + edev->max_supported = index; + } else { + edev->max_supported = 0; + } + + if (index > SUPPORTED_CABLE_MAX) { + dev_err(&edev->dev, "extcon: maximum number of supported cables exceeded.\n"); + return -EINVAL; + } + + edev->dev.class = extcon_class; + edev->dev.release = extcon_dev_release; + + edev->name = edev->name ? edev->name : dev_name(edev->dev.parent); + if (IS_ERR_OR_NULL(edev->name)) { + dev_err(&edev->dev, + "extcon device name is null\n"); + return -EINVAL; + } + dev_set_name(&edev->dev, "%s", edev->name); + + if (edev->max_supported) { + char buf[10]; + char *str; + struct extcon_cable *cable; + + edev->cables = kzalloc(sizeof(struct extcon_cable) * + edev->max_supported, GFP_KERNEL); + if (!edev->cables) { + ret = -ENOMEM; + goto err_sysfs_alloc; + } + for (index = 0; index < edev->max_supported; index++) { + cable = &edev->cables[index]; + + snprintf(buf, 10, "cable.%d", index); + str = kzalloc(sizeof(char) * (strlen(buf) + 1), + GFP_KERNEL); + if (!str) { + for (index--; index >= 0; index--) { + cable = &edev->cables[index]; + kfree(cable->attr_g.name); + } + ret = -ENOMEM; + + goto err_alloc_cables; + } + strcpy(str, buf); + + cable->edev = edev; + cable->cable_index = index; + cable->attrs[0] = &cable->attr_name.attr; + cable->attrs[1] = &cable->attr_state.attr; + cable->attrs[2] = NULL; + cable->attr_g.name = str; + cable->attr_g.attrs = cable->attrs; + + sysfs_attr_init(&cable->attr_name.attr); + cable->attr_name.attr.name = "name"; + cable->attr_name.attr.mode = 0444; + cable->attr_name.show = cable_name_show; + + sysfs_attr_init(&cable->attr_state.attr); + cable->attr_state.attr.name = "state"; + cable->attr_state.attr.mode = 0444; + cable->attr_state.show = cable_state_show; + } + } + + if (edev->max_supported && edev->mutually_exclusive) { + char buf[80]; + char *name; + + /* Count the size of mutually_exclusive array */ + for (index = 0; edev->mutually_exclusive[index]; index++) + ; + + edev->attrs_muex = kzalloc(sizeof(struct attribute *) * + (index + 1), GFP_KERNEL); + if (!edev->attrs_muex) { + ret = -ENOMEM; + goto err_muex; + } + + edev->d_attrs_muex = kzalloc(sizeof(struct device_attribute) * + index, GFP_KERNEL); + if (!edev->d_attrs_muex) { + ret = -ENOMEM; + kfree(edev->attrs_muex); + goto err_muex; + } + + for (index = 0; edev->mutually_exclusive[index]; index++) { + sprintf(buf, "0x%x", edev->mutually_exclusive[index]); + name = kzalloc(sizeof(char) * (strlen(buf) + 1), + GFP_KERNEL); + if (!name) { + for (index--; index >= 0; index--) { + kfree(edev->d_attrs_muex[index].attr. + name); + } + kfree(edev->d_attrs_muex); + kfree(edev->attrs_muex); + ret = -ENOMEM; + goto err_muex; + } + strcpy(name, buf); + sysfs_attr_init(&edev->d_attrs_muex[index].attr); + edev->d_attrs_muex[index].attr.name = name; + edev->d_attrs_muex[index].attr.mode = 0000; + edev->attrs_muex[index] = &edev->d_attrs_muex[index] + .attr; + } + edev->attr_g_muex.name = muex_name; + edev->attr_g_muex.attrs = edev->attrs_muex; + + } + + if (edev->max_supported) { + edev->extcon_dev_type.groups = + kzalloc(sizeof(struct attribute_group *) * + (edev->max_supported + 2), GFP_KERNEL); + if (!edev->extcon_dev_type.groups) { + ret = -ENOMEM; + goto err_alloc_groups; + } + + edev->extcon_dev_type.name = dev_name(&edev->dev); + edev->extcon_dev_type.release = dummy_sysfs_dev_release; + + for (index = 0; index < edev->max_supported; index++) + edev->extcon_dev_type.groups[index] = + &edev->cables[index].attr_g; + if (edev->mutually_exclusive) + edev->extcon_dev_type.groups[index] = + &edev->attr_g_muex; + + edev->dev.type = &edev->extcon_dev_type; + } + + ret = device_register(&edev->dev); + if (ret) { + put_device(&edev->dev); + goto err_dev; + } +#if defined(CONFIG_ANDROID) + if (switch_class) + ret = class_compat_create_link(switch_class, &edev->dev, NULL); +#endif /* CONFIG_ANDROID */ + + spin_lock_init(&edev->lock); + + RAW_INIT_NOTIFIER_HEAD(&edev->nh); + + dev_set_drvdata(&edev->dev, edev); + edev->state = 0; + + mutex_lock(&extcon_dev_list_lock); + list_add(&edev->entry, &extcon_dev_list); + mutex_unlock(&extcon_dev_list_lock); + + return 0; + +err_dev: + if (edev->max_supported) + kfree(edev->extcon_dev_type.groups); +err_alloc_groups: + if (edev->max_supported && edev->mutually_exclusive) { + for (index = 0; edev->mutually_exclusive[index]; index++) + kfree(edev->d_attrs_muex[index].attr.name); + kfree(edev->d_attrs_muex); + kfree(edev->attrs_muex); + } +err_muex: + for (index = 0; index < edev->max_supported; index++) + kfree(edev->cables[index].attr_g.name); +err_alloc_cables: + if (edev->max_supported) + kfree(edev->cables); +err_sysfs_alloc: + return ret; +} +EXPORT_SYMBOL_GPL(extcon_dev_register); + +/** + * extcon_dev_unregister() - Unregister the extcon device. + * @edev: the extcon device instance to be unregistered. + * + * Note that this does not call kfree(edev) because edev was not allocated + * by this class. + */ +void extcon_dev_unregister(struct extcon_dev *edev) +{ + int index; + + mutex_lock(&extcon_dev_list_lock); + list_del(&edev->entry); + mutex_unlock(&extcon_dev_list_lock); + + if (IS_ERR_OR_NULL(get_device(&edev->dev))) { + dev_err(&edev->dev, "Failed to unregister extcon_dev (%s)\n", + dev_name(&edev->dev)); + return; + } + + device_unregister(&edev->dev); + + if (edev->mutually_exclusive && edev->max_supported) { + for (index = 0; edev->mutually_exclusive[index]; + index++) + kfree(edev->d_attrs_muex[index].attr.name); + kfree(edev->d_attrs_muex); + kfree(edev->attrs_muex); + } + + for (index = 0; index < edev->max_supported; index++) + kfree(edev->cables[index].attr_g.name); + + if (edev->max_supported) { + kfree(edev->extcon_dev_type.groups); + kfree(edev->cables); + } + +#if defined(CONFIG_ANDROID) + if (switch_class) + class_compat_remove_link(switch_class, &edev->dev, NULL); +#endif + put_device(&edev->dev); +} +EXPORT_SYMBOL_GPL(extcon_dev_unregister); + +static void devm_extcon_dev_unreg(struct device *dev, void *res) +{ + extcon_dev_unregister(*(struct extcon_dev **)res); +} + +/** + * devm_extcon_dev_register() - Resource-managed extcon_dev_register() + * @dev: device to allocate extcon device + * @edev: the new extcon device to register + * + * Managed extcon_dev_register() function. If extcon device is attached with + * this function, that extcon device is automatically unregistered on driver + * detach. Internally this function calls extcon_dev_register() function. + * To get more information, refer that function. + * + * If extcon device is registered with this function and the device needs to be + * unregistered separately, devm_extcon_dev_unregister() should be used. + * + * Returns 0 if success or negaive error number if failure. + */ +int devm_extcon_dev_register(struct device *dev, struct extcon_dev *edev) +{ + struct extcon_dev **ptr; + int ret; + + ptr = devres_alloc(devm_extcon_dev_unreg, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return -ENOMEM; + + ret = extcon_dev_register(edev); + if (ret) { + devres_free(ptr); + return ret; + } + + *ptr = edev; + devres_add(dev, ptr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_register); + +/** + * devm_extcon_dev_unregister() - Resource-managed extcon_dev_unregister() + * @dev: device the extcon belongs to + * @edev: the extcon device to unregister + * + * Unregister extcon device that is registered with devm_extcon_dev_register() + * function. + */ +void devm_extcon_dev_unregister(struct device *dev, struct extcon_dev *edev) +{ + WARN_ON(devres_release(dev, devm_extcon_dev_unreg, + devm_extcon_dev_match, edev)); +} +EXPORT_SYMBOL_GPL(devm_extcon_dev_unregister); + +#ifdef CONFIG_OF +/* + * extcon_get_edev_by_phandle - Get the extcon device from devicetree + * @dev - instance to the given device + * @index - index into list of extcon_dev + * + * return the instance of extcon device + */ +struct extcon_dev *extcon_get_edev_by_phandle(struct device *dev, int index) +{ + struct device_node *node; + struct extcon_dev *edev; + + if (!dev->of_node) { + dev_err(dev, "device does not have a device node entry\n"); + return ERR_PTR(-EINVAL); + } + + node = of_parse_phandle(dev->of_node, "extcon", index); + if (!node) { + dev_err(dev, "failed to get phandle in %s node\n", + dev->of_node->full_name); + return ERR_PTR(-ENODEV); + } + + mutex_lock(&extcon_dev_list_lock); + list_for_each_entry(edev, &extcon_dev_list, entry) { + if (edev->dev.parent && edev->dev.parent->of_node == node) { + mutex_unlock(&extcon_dev_list_lock); + return edev; + } + } + mutex_unlock(&extcon_dev_list_lock); + + return ERR_PTR(-EPROBE_DEFER); +} +#else +struct extcon_dev *extcon_get_edev_by_phandle(struct device *dev, int index) +{ + return ERR_PTR(-ENOSYS); +} +#endif /* CONFIG_OF */ +EXPORT_SYMBOL_GPL(extcon_get_edev_by_phandle); + +static int __init extcon_class_init(void) +{ + return create_extcon_class(); +} +module_init(extcon_class_init); + +static void __exit extcon_class_exit(void) +{ +#if defined(CONFIG_ANDROID) + class_compat_unregister(switch_class); +#endif + class_destroy(extcon_class); +} +module_exit(extcon_class_exit); + +MODULE_AUTHOR("Mike Lockwood <lockwood@android.com>"); +MODULE_AUTHOR("Donggeun Kim <dg77.kim@samsung.com>"); +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_DESCRIPTION("External connector (extcon) class driver"); +MODULE_LICENSE("GPL"); |