| /* Copyright (c) 2013-2015, 2017-2018, The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only 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/module.h> |
| #include <linux/init.h> |
| #include <linux/kernel.h> |
| #include <linux/err.h> |
| #include <linux/errno.h> |
| #include <linux/slab.h> |
| #include <linux/of_platform.h> |
| #include <linux/interrupt.h> |
| #include <linux/of_gpio.h> |
| #include <linux/gpio.h> |
| #include <linux/extcon.h> |
| #include <linux/regulator/consumer.h> |
| |
| struct gpio_usbdetect { |
| struct platform_device *pdev; |
| struct regulator *vin; |
| int vbus_det_irq; |
| int id_det_irq; |
| int gpio; |
| struct extcon_dev *extcon_dev; |
| int vbus_state; |
| bool id_state; |
| }; |
| |
| static const unsigned int gpio_usb_extcon_table[] = { |
| EXTCON_USB, |
| EXTCON_USB_HOST, |
| EXTCON_NONE, |
| }; |
| |
| static irqreturn_t gpio_usbdetect_vbus_irq(int irq, void *data) |
| { |
| struct gpio_usbdetect *usb = data; |
| union extcon_property_value val; |
| |
| usb->vbus_state = gpio_get_value(usb->gpio); |
| if (usb->vbus_state) { |
| dev_dbg(&usb->pdev->dev, "setting vbus notification\n"); |
| val.intval = true; |
| extcon_set_property(usb->extcon_dev, EXTCON_USB, |
| EXTCON_PROP_USB_SS, val); |
| extcon_set_cable_state_(usb->extcon_dev, EXTCON_USB, 1); |
| } else { |
| dev_dbg(&usb->pdev->dev, "setting vbus removed notification\n"); |
| extcon_set_cable_state_(usb->extcon_dev, EXTCON_USB, 0); |
| } |
| |
| return IRQ_HANDLED; |
| } |
| |
| static irqreturn_t gpio_usbdetect_id_irq(int irq, void *data) |
| { |
| struct gpio_usbdetect *usb = data; |
| int ret; |
| |
| ret = irq_get_irqchip_state(irq, IRQCHIP_STATE_LINE_LEVEL, |
| &usb->id_state); |
| if (ret < 0) { |
| dev_err(&usb->pdev->dev, "unable to read ID IRQ LINE\n"); |
| return IRQ_HANDLED; |
| } |
| |
| return IRQ_WAKE_THREAD; |
| } |
| |
| static irqreturn_t gpio_usbdetect_id_irq_thread(int irq, void *data) |
| { |
| struct gpio_usbdetect *usb = data; |
| bool curr_id_state; |
| static int prev_id_state = -EINVAL; |
| union extcon_property_value val; |
| |
| curr_id_state = usb->id_state; |
| if (curr_id_state == prev_id_state) { |
| dev_dbg(&usb->pdev->dev, "no change in ID state\n"); |
| return IRQ_HANDLED; |
| } |
| |
| if (curr_id_state) { |
| dev_dbg(&usb->pdev->dev, "stopping usb host\n"); |
| extcon_set_cable_state_(usb->extcon_dev, EXTCON_USB_HOST, 0); |
| enable_irq(usb->vbus_det_irq); |
| } else { |
| dev_dbg(&usb->pdev->dev, "starting usb HOST\n"); |
| disable_irq(usb->vbus_det_irq); |
| val.intval = true; |
| extcon_set_property(usb->extcon_dev, EXTCON_USB_HOST, |
| EXTCON_PROP_USB_SS, val); |
| extcon_set_cable_state_(usb->extcon_dev, EXTCON_USB_HOST, 1); |
| } |
| |
| prev_id_state = curr_id_state; |
| return IRQ_HANDLED; |
| } |
| |
| static const u32 gpio_usb_extcon_exclusive[] = {0x3, 0}; |
| |
| static int gpio_usbdetect_probe(struct platform_device *pdev) |
| { |
| struct gpio_usbdetect *usb; |
| int rc; |
| |
| usb = devm_kzalloc(&pdev->dev, sizeof(*usb), GFP_KERNEL); |
| if (!usb) |
| return -ENOMEM; |
| |
| usb->pdev = pdev; |
| |
| usb->extcon_dev = devm_extcon_dev_allocate(&pdev->dev, |
| gpio_usb_extcon_table); |
| if (IS_ERR(usb->extcon_dev)) { |
| dev_err(&pdev->dev, "failed to allocate a extcon device\n"); |
| return PTR_ERR(usb->extcon_dev); |
| } |
| |
| usb->extcon_dev->mutually_exclusive = gpio_usb_extcon_exclusive; |
| rc = devm_extcon_dev_register(&pdev->dev, usb->extcon_dev); |
| if (rc) { |
| dev_err(&pdev->dev, "failed to register extcon device\n"); |
| return rc; |
| } |
| |
| rc = extcon_set_property_capability(usb->extcon_dev, |
| EXTCON_USB, EXTCON_PROP_USB_SS); |
| rc |= extcon_set_property_capability(usb->extcon_dev, |
| EXTCON_USB_HOST, EXTCON_PROP_USB_SS); |
| if (rc) { |
| dev_err(&pdev->dev, "failed to register extcon props rc=%d\n", |
| rc); |
| return rc; |
| } |
| |
| if (of_get_property(pdev->dev.of_node, "vin-supply", NULL)) { |
| usb->vin = devm_regulator_get(&pdev->dev, "vin"); |
| if (IS_ERR(usb->vin)) { |
| dev_err(&pdev->dev, "Failed to get VIN regulator: %ld\n", |
| PTR_ERR(usb->vin)); |
| return PTR_ERR(usb->vin); |
| } |
| } |
| |
| if (usb->vin) { |
| rc = regulator_enable(usb->vin); |
| if (rc) { |
| dev_err(&pdev->dev, "Failed to enable VIN regulator: %d\n", |
| rc); |
| return rc; |
| } |
| } |
| |
| usb->gpio = of_get_named_gpio(pdev->dev.of_node, |
| "qcom,vbus-det-gpio", 0); |
| if (usb->gpio < 0) { |
| dev_err(&pdev->dev, "Failed to get gpio: %d\n", usb->gpio); |
| rc = usb->gpio; |
| goto error; |
| } |
| |
| rc = gpio_request(usb->gpio, "vbus-det-gpio"); |
| if (rc < 0) { |
| dev_err(&pdev->dev, "Failed to request gpio: %d\n", rc); |
| goto error; |
| } |
| |
| usb->vbus_det_irq = gpio_to_irq(usb->gpio); |
| if (usb->vbus_det_irq < 0) { |
| dev_err(&pdev->dev, "get vbus_det_irq failed\n"); |
| rc = usb->vbus_det_irq; |
| goto error; |
| } |
| |
| rc = devm_request_threaded_irq(&pdev->dev, usb->vbus_det_irq, |
| NULL, gpio_usbdetect_vbus_irq, |
| IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | |
| IRQF_ONESHOT, "vbus_det_irq", usb); |
| if (rc) { |
| dev_err(&pdev->dev, "request for vbus_det_irq failed: %d\n", |
| rc); |
| goto error; |
| } |
| |
| usb->id_det_irq = platform_get_irq_byname(pdev, "pmic_id_irq"); |
| if (usb->id_det_irq < 0) { |
| dev_err(&pdev->dev, "get id_det_irq failed\n"); |
| rc = usb->id_det_irq; |
| goto error; |
| } |
| |
| rc = devm_request_threaded_irq(&pdev->dev, usb->id_det_irq, |
| gpio_usbdetect_id_irq, |
| gpio_usbdetect_id_irq_thread, |
| IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | |
| IRQF_ONESHOT, "id_det_irq", usb); |
| if (rc) { |
| dev_err(&pdev->dev, "request for id_det_irq failed: %d\n", rc); |
| goto error; |
| } |
| |
| enable_irq_wake(usb->vbus_det_irq); |
| enable_irq_wake(usb->id_det_irq); |
| dev_set_drvdata(&pdev->dev, usb); |
| |
| if (usb->id_det_irq) { |
| gpio_usbdetect_id_irq(usb->id_det_irq, usb); |
| if (!usb->id_state) { |
| gpio_usbdetect_id_irq_thread(usb->id_det_irq, usb); |
| return 0; |
| } |
| } |
| |
| /* Read and report initial VBUS state */ |
| gpio_usbdetect_vbus_irq(usb->vbus_det_irq, usb); |
| |
| return 0; |
| |
| error: |
| if (usb->vin) |
| regulator_disable(usb->vin); |
| return rc; |
| } |
| |
| static int gpio_usbdetect_remove(struct platform_device *pdev) |
| { |
| struct gpio_usbdetect *usb = dev_get_drvdata(&pdev->dev); |
| |
| disable_irq_wake(usb->vbus_det_irq); |
| disable_irq(usb->vbus_det_irq); |
| disable_irq_wake(usb->id_det_irq); |
| disable_irq(usb->id_det_irq); |
| if (usb->vin) |
| regulator_disable(usb->vin); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id of_match_table[] = { |
| { .compatible = "qcom,gpio-usbdetect", }, |
| {} |
| }; |
| |
| static struct platform_driver gpio_usbdetect_driver = { |
| .driver = { |
| .name = "qcom,gpio-usbdetect", |
| .of_match_table = of_match_table, |
| }, |
| .probe = gpio_usbdetect_probe, |
| .remove = gpio_usbdetect_remove, |
| }; |
| |
| module_driver(gpio_usbdetect_driver, platform_driver_register, |
| platform_driver_unregister); |
| |
| MODULE_DESCRIPTION("GPIO USB VBUS Detection driver"); |
| MODULE_LICENSE("GPL v2"); |