root/drivers/extcon/extcon-adc-jack.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * drivers/extcon/extcon-adc-jack.c
 *
 * Analog Jack extcon driver with ADC-based detection capability.
 *
 * Copyright (C) 2016 Samsung Electronics
 * Chanwoo Choi <cw00.choi@samsung.com>
 *
 * Copyright (C) 2012 Samsung Electronics
 * MyungJoo Ham <myungjoo.ham@samsung.com>
 *
 * Modified for calling to IIO to get adc by <anish.singh@samsung.com>
 */

#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-provider.h>

/**
 * struct adc_jack_data - internal data for adc_jack device driver
 * @dev:                The device structure associated with the adc_jack.
 * @edev:               extcon device.
 * @cable_names:        list of supported cables.
 * @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.
 * @wakeup_source:      Indicates if the device can wake up the system.
 */
struct adc_jack_data {
        struct device *dev;
        struct extcon_dev *edev;

        const unsigned int **cable_names;
        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;
        bool wakeup_source;
};

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);
        struct adc_jack_cond *def;
        int ret, adc_val;
        int i;

        ret = iio_read_channel_raw(data->chan, &adc_val);
        if (ret < 0) {
                dev_err(data->dev, "read channel() error: %d\n", ret);
                return;
        }

        /* Get state from adc value with adc_conditions */
        for (i = 0; i < data->num_conditions; i++) {
                def = &data->adc_conditions[i];
                if (def->min_adc <= adc_val && def->max_adc >= adc_val) {
                        extcon_set_state_sync(data->edev, def->id, true);
                        return;
                }
        }

        /* Set the detached state if adc value is not included in the range */
        for (i = 0; i < data->num_conditions; i++) {
                def = &data->adc_conditions[i];
                extcon_set_state_sync(data->edev, def->id, false);
        }
}

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->dev = &pdev->dev;
        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;
        }

        if (!pdata->adc_conditions) {
                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].id != EXTCON_NONE; i++);
        data->num_conditions = i;

        data->chan = devm_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);
        data->wakeup_source = pdata->wakeup_source;

        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 < 0)
                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;
        }

        if (data->wakeup_source)
                device_init_wakeup(&pdev->dev, 1);

        adc_jack_handler(&data->handler.work);
        return 0;
}

static void adc_jack_remove(struct platform_device *pdev)
{
        struct adc_jack_data *data = platform_get_drvdata(pdev);

        if (data->wakeup_source)
                device_init_wakeup(&pdev->dev, false);
        free_irq(data->irq, data);
        cancel_work_sync(&data->handler.work);
}

#ifdef CONFIG_PM_SLEEP
static int adc_jack_suspend(struct device *dev)
{
        struct adc_jack_data *data = dev_get_drvdata(dev);

        cancel_delayed_work_sync(&data->handler);
        if (device_may_wakeup(data->dev))
                enable_irq_wake(data->irq);

        return 0;
}

static int adc_jack_resume(struct device *dev)
{
        struct adc_jack_data *data = dev_get_drvdata(dev);

        if (device_may_wakeup(data->dev))
                disable_irq_wake(data->irq);

        return 0;
}
#endif /* CONFIG_PM_SLEEP */

static SIMPLE_DEV_PM_OPS(adc_jack_pm_ops,
                adc_jack_suspend, adc_jack_resume);

static struct platform_driver adc_jack_driver = {
        .probe          = adc_jack_probe,
        .remove         = adc_jack_remove,
        .driver         = {
                .name   = "adc-jack",
                .pm = &adc_jack_pm_ops,
        },
};

module_platform_driver(adc_jack_driver);

MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>");
MODULE_DESCRIPTION("ADC Jack extcon driver");
MODULE_LICENSE("GPL v2");