11802d0beSThomas Gleixner // SPDX-License-Identifier: GPL-2.0-only
238085c98SStephen Boyd /**
338085c98SStephen Boyd * extcon-qcom-spmi-misc.c - Qualcomm USB extcon driver to support USB ID
47b1222b2SAnirudh Ghayal * and VBUS detection based on extcon-usb-gpio.c.
538085c98SStephen Boyd *
638085c98SStephen Boyd * Copyright (C) 2016 Linaro, Ltd.
738085c98SStephen Boyd * Stephen Boyd <stephen.boyd@linaro.org>
838085c98SStephen Boyd */
938085c98SStephen Boyd
10f94a5becSMatti Vaittinen #include <linux/devm-helpers.h>
11176aa360SChanwoo Choi #include <linux/extcon-provider.h>
1238085c98SStephen Boyd #include <linux/init.h>
1338085c98SStephen Boyd #include <linux/interrupt.h>
1438085c98SStephen Boyd #include <linux/kernel.h>
1538085c98SStephen Boyd #include <linux/module.h>
16ac316725SRandy Dunlap #include <linux/mod_devicetable.h>
1738085c98SStephen Boyd #include <linux/platform_device.h>
1838085c98SStephen Boyd #include <linux/slab.h>
1938085c98SStephen Boyd #include <linux/workqueue.h>
2038085c98SStephen Boyd
2138085c98SStephen Boyd #define USB_ID_DEBOUNCE_MS 5 /* ms */
2238085c98SStephen Boyd
2338085c98SStephen Boyd struct qcom_usb_extcon_info {
2438085c98SStephen Boyd struct extcon_dev *edev;
257b1222b2SAnirudh Ghayal int id_irq;
267b1222b2SAnirudh Ghayal int vbus_irq;
2738085c98SStephen Boyd struct delayed_work wq_detcable;
2838085c98SStephen Boyd unsigned long debounce_jiffies;
2938085c98SStephen Boyd };
3038085c98SStephen Boyd
3138085c98SStephen Boyd static const unsigned int qcom_usb_extcon_cable[] = {
327b1222b2SAnirudh Ghayal EXTCON_USB,
3338085c98SStephen Boyd EXTCON_USB_HOST,
3438085c98SStephen Boyd EXTCON_NONE,
3538085c98SStephen Boyd };
3638085c98SStephen Boyd
qcom_usb_extcon_detect_cable(struct work_struct * work)3738085c98SStephen Boyd static void qcom_usb_extcon_detect_cable(struct work_struct *work)
3838085c98SStephen Boyd {
397b1222b2SAnirudh Ghayal bool state = false;
4038085c98SStephen Boyd int ret;
417b1222b2SAnirudh Ghayal union extcon_property_value val;
4238085c98SStephen Boyd struct qcom_usb_extcon_info *info = container_of(to_delayed_work(work),
4338085c98SStephen Boyd struct qcom_usb_extcon_info,
4438085c98SStephen Boyd wq_detcable);
4538085c98SStephen Boyd
467b1222b2SAnirudh Ghayal if (info->id_irq > 0) {
4738085c98SStephen Boyd /* check ID and update cable state */
487b1222b2SAnirudh Ghayal ret = irq_get_irqchip_state(info->id_irq,
497b1222b2SAnirudh Ghayal IRQCHIP_STATE_LINE_LEVEL, &state);
5038085c98SStephen Boyd if (ret)
5138085c98SStephen Boyd return;
5238085c98SStephen Boyd
537b1222b2SAnirudh Ghayal if (!state) {
547b1222b2SAnirudh Ghayal val.intval = true;
557b1222b2SAnirudh Ghayal extcon_set_property(info->edev, EXTCON_USB_HOST,
567b1222b2SAnirudh Ghayal EXTCON_PROP_USB_SS, val);
577b1222b2SAnirudh Ghayal }
587b1222b2SAnirudh Ghayal extcon_set_state_sync(info->edev, EXTCON_USB_HOST, !state);
597b1222b2SAnirudh Ghayal }
607b1222b2SAnirudh Ghayal
617b1222b2SAnirudh Ghayal if (info->vbus_irq > 0) {
627b1222b2SAnirudh Ghayal /* check VBUS and update cable state */
637b1222b2SAnirudh Ghayal ret = irq_get_irqchip_state(info->vbus_irq,
647b1222b2SAnirudh Ghayal IRQCHIP_STATE_LINE_LEVEL, &state);
657b1222b2SAnirudh Ghayal if (ret)
667b1222b2SAnirudh Ghayal return;
677b1222b2SAnirudh Ghayal
687b1222b2SAnirudh Ghayal if (state) {
697b1222b2SAnirudh Ghayal val.intval = true;
707b1222b2SAnirudh Ghayal extcon_set_property(info->edev, EXTCON_USB,
717b1222b2SAnirudh Ghayal EXTCON_PROP_USB_SS, val);
727b1222b2SAnirudh Ghayal }
737b1222b2SAnirudh Ghayal extcon_set_state_sync(info->edev, EXTCON_USB, state);
747b1222b2SAnirudh Ghayal }
7538085c98SStephen Boyd }
7638085c98SStephen Boyd
qcom_usb_irq_handler(int irq,void * dev_id)7738085c98SStephen Boyd static irqreturn_t qcom_usb_irq_handler(int irq, void *dev_id)
7838085c98SStephen Boyd {
7938085c98SStephen Boyd struct qcom_usb_extcon_info *info = dev_id;
8038085c98SStephen Boyd
8138085c98SStephen Boyd queue_delayed_work(system_power_efficient_wq, &info->wq_detcable,
8238085c98SStephen Boyd info->debounce_jiffies);
8338085c98SStephen Boyd
8438085c98SStephen Boyd return IRQ_HANDLED;
8538085c98SStephen Boyd }
8638085c98SStephen Boyd
qcom_usb_extcon_probe(struct platform_device * pdev)8738085c98SStephen Boyd static int qcom_usb_extcon_probe(struct platform_device *pdev)
8838085c98SStephen Boyd {
8938085c98SStephen Boyd struct device *dev = &pdev->dev;
9038085c98SStephen Boyd struct qcom_usb_extcon_info *info;
9138085c98SStephen Boyd int ret;
9238085c98SStephen Boyd
9338085c98SStephen Boyd info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL);
9438085c98SStephen Boyd if (!info)
9538085c98SStephen Boyd return -ENOMEM;
9638085c98SStephen Boyd
9738085c98SStephen Boyd info->edev = devm_extcon_dev_allocate(dev, qcom_usb_extcon_cable);
9838085c98SStephen Boyd if (IS_ERR(info->edev)) {
9938085c98SStephen Boyd dev_err(dev, "failed to allocate extcon device\n");
10038085c98SStephen Boyd return -ENOMEM;
10138085c98SStephen Boyd }
10238085c98SStephen Boyd
10338085c98SStephen Boyd ret = devm_extcon_dev_register(dev, info->edev);
10438085c98SStephen Boyd if (ret < 0) {
10538085c98SStephen Boyd dev_err(dev, "failed to register extcon device\n");
10638085c98SStephen Boyd return ret;
10738085c98SStephen Boyd }
10838085c98SStephen Boyd
1097b1222b2SAnirudh Ghayal ret = extcon_set_property_capability(info->edev,
1107b1222b2SAnirudh Ghayal EXTCON_USB, EXTCON_PROP_USB_SS);
1117b1222b2SAnirudh Ghayal ret |= extcon_set_property_capability(info->edev,
1127b1222b2SAnirudh Ghayal EXTCON_USB_HOST, EXTCON_PROP_USB_SS);
1137b1222b2SAnirudh Ghayal if (ret) {
1147b1222b2SAnirudh Ghayal dev_err(dev, "failed to register extcon props rc=%d\n",
1157b1222b2SAnirudh Ghayal ret);
1167b1222b2SAnirudh Ghayal return ret;
1177b1222b2SAnirudh Ghayal }
1187b1222b2SAnirudh Ghayal
11938085c98SStephen Boyd info->debounce_jiffies = msecs_to_jiffies(USB_ID_DEBOUNCE_MS);
120f94a5becSMatti Vaittinen
121f94a5becSMatti Vaittinen ret = devm_delayed_work_autocancel(dev, &info->wq_detcable,
122f94a5becSMatti Vaittinen qcom_usb_extcon_detect_cable);
123f94a5becSMatti Vaittinen if (ret)
124f94a5becSMatti Vaittinen return ret;
12538085c98SStephen Boyd
126*fe551bc9SBryan O'Donoghue info->id_irq = platform_get_irq_byname_optional(pdev, "usb_id");
1277b1222b2SAnirudh Ghayal if (info->id_irq > 0) {
1287b1222b2SAnirudh Ghayal ret = devm_request_threaded_irq(dev, info->id_irq, NULL,
12938085c98SStephen Boyd qcom_usb_irq_handler,
13038085c98SStephen Boyd IRQF_TRIGGER_RISING |
13138085c98SStephen Boyd IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
13238085c98SStephen Boyd pdev->name, info);
13338085c98SStephen Boyd if (ret < 0) {
13438085c98SStephen Boyd dev_err(dev, "failed to request handler for ID IRQ\n");
13538085c98SStephen Boyd return ret;
13638085c98SStephen Boyd }
1377b1222b2SAnirudh Ghayal }
1387b1222b2SAnirudh Ghayal
139*fe551bc9SBryan O'Donoghue info->vbus_irq = platform_get_irq_byname_optional(pdev, "usb_vbus");
1407b1222b2SAnirudh Ghayal if (info->vbus_irq > 0) {
1417b1222b2SAnirudh Ghayal ret = devm_request_threaded_irq(dev, info->vbus_irq, NULL,
1427b1222b2SAnirudh Ghayal qcom_usb_irq_handler,
1437b1222b2SAnirudh Ghayal IRQF_TRIGGER_RISING |
1447b1222b2SAnirudh Ghayal IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
1457b1222b2SAnirudh Ghayal pdev->name, info);
1467b1222b2SAnirudh Ghayal if (ret < 0) {
1477b1222b2SAnirudh Ghayal dev_err(dev, "failed to request handler for VBUS IRQ\n");
1487b1222b2SAnirudh Ghayal return ret;
1497b1222b2SAnirudh Ghayal }
1507b1222b2SAnirudh Ghayal }
1517b1222b2SAnirudh Ghayal
1527b1222b2SAnirudh Ghayal if (info->id_irq < 0 && info->vbus_irq < 0) {
1537b1222b2SAnirudh Ghayal dev_err(dev, "ID and VBUS IRQ not found\n");
1547b1222b2SAnirudh Ghayal return -EINVAL;
1557b1222b2SAnirudh Ghayal }
15638085c98SStephen Boyd
15738085c98SStephen Boyd platform_set_drvdata(pdev, info);
15838085c98SStephen Boyd device_init_wakeup(dev, 1);
15938085c98SStephen Boyd
16038085c98SStephen Boyd /* Perform initial detection */
16138085c98SStephen Boyd qcom_usb_extcon_detect_cable(&info->wq_detcable.work);
16238085c98SStephen Boyd
16338085c98SStephen Boyd return 0;
16438085c98SStephen Boyd }
16538085c98SStephen Boyd
16638085c98SStephen Boyd #ifdef CONFIG_PM_SLEEP
qcom_usb_extcon_suspend(struct device * dev)16738085c98SStephen Boyd static int qcom_usb_extcon_suspend(struct device *dev)
16838085c98SStephen Boyd {
16938085c98SStephen Boyd struct qcom_usb_extcon_info *info = dev_get_drvdata(dev);
17038085c98SStephen Boyd int ret = 0;
17138085c98SStephen Boyd
1727b1222b2SAnirudh Ghayal if (device_may_wakeup(dev)) {
1737b1222b2SAnirudh Ghayal if (info->id_irq > 0)
1747b1222b2SAnirudh Ghayal ret = enable_irq_wake(info->id_irq);
1757b1222b2SAnirudh Ghayal if (info->vbus_irq > 0)
1767b1222b2SAnirudh Ghayal ret = enable_irq_wake(info->vbus_irq);
1777b1222b2SAnirudh Ghayal }
17838085c98SStephen Boyd
17938085c98SStephen Boyd return ret;
18038085c98SStephen Boyd }
18138085c98SStephen Boyd
qcom_usb_extcon_resume(struct device * dev)18238085c98SStephen Boyd static int qcom_usb_extcon_resume(struct device *dev)
18338085c98SStephen Boyd {
18438085c98SStephen Boyd struct qcom_usb_extcon_info *info = dev_get_drvdata(dev);
18538085c98SStephen Boyd int ret = 0;
18638085c98SStephen Boyd
1877b1222b2SAnirudh Ghayal if (device_may_wakeup(dev)) {
1887b1222b2SAnirudh Ghayal if (info->id_irq > 0)
1897b1222b2SAnirudh Ghayal ret = disable_irq_wake(info->id_irq);
1907b1222b2SAnirudh Ghayal if (info->vbus_irq > 0)
1917b1222b2SAnirudh Ghayal ret = disable_irq_wake(info->vbus_irq);
1927b1222b2SAnirudh Ghayal }
19338085c98SStephen Boyd
19438085c98SStephen Boyd return ret;
19538085c98SStephen Boyd }
19638085c98SStephen Boyd #endif
19738085c98SStephen Boyd
19838085c98SStephen Boyd static SIMPLE_DEV_PM_OPS(qcom_usb_extcon_pm_ops,
19938085c98SStephen Boyd qcom_usb_extcon_suspend, qcom_usb_extcon_resume);
20038085c98SStephen Boyd
20138085c98SStephen Boyd static const struct of_device_id qcom_usb_extcon_dt_match[] = {
20238085c98SStephen Boyd { .compatible = "qcom,pm8941-misc", },
20338085c98SStephen Boyd { }
20438085c98SStephen Boyd };
20538085c98SStephen Boyd MODULE_DEVICE_TABLE(of, qcom_usb_extcon_dt_match);
20638085c98SStephen Boyd
20738085c98SStephen Boyd static struct platform_driver qcom_usb_extcon_driver = {
20838085c98SStephen Boyd .probe = qcom_usb_extcon_probe,
20938085c98SStephen Boyd .driver = {
21038085c98SStephen Boyd .name = "extcon-pm8941-misc",
21138085c98SStephen Boyd .pm = &qcom_usb_extcon_pm_ops,
21238085c98SStephen Boyd .of_match_table = qcom_usb_extcon_dt_match,
21338085c98SStephen Boyd },
21438085c98SStephen Boyd };
21538085c98SStephen Boyd module_platform_driver(qcom_usb_extcon_driver);
21638085c98SStephen Boyd
21738085c98SStephen Boyd MODULE_DESCRIPTION("QCOM USB ID extcon driver");
21838085c98SStephen Boyd MODULE_AUTHOR("Stephen Boyd <stephen.boyd@linaro.org>");
21938085c98SStephen Boyd MODULE_LICENSE("GPL v2");
220