xref: /linux/drivers/extcon/extcon-usb-gpio.c (revision 119b5984675cb43c2ecdf54195b418d3155eef94)
1 // SPDX-License-Identifier: GPL-2.0-only
2 /*
3  * drivers/extcon/extcon-usb-gpio.c - USB GPIO extcon driver
4  *
5  * Copyright (C) 2015 Texas Instruments Incorporated - https://www.ti.com
6  * Author: Roger Quadros <rogerq@ti.com>
7  */
8 
9 #include <linux/extcon-provider.h>
10 #include <linux/gpio/consumer.h>
11 #include <linux/init.h>
12 #include <linux/interrupt.h>
13 #include <linux/irq.h>
14 #include <linux/kernel.h>
15 #include <linux/module.h>
16 #include <linux/platform_device.h>
17 #include <linux/slab.h>
18 #include <linux/workqueue.h>
19 #include <linux/pinctrl/consumer.h>
20 
21 #define USB_GPIO_DEBOUNCE_MS	20	/* ms */
22 
23 struct usb_extcon_info {
24 	struct device *dev;
25 	struct extcon_dev *edev;
26 
27 	struct gpio_desc *id_gpiod;
28 	struct gpio_desc *vbus_gpiod;
29 	int id_irq;
30 	int vbus_irq;
31 
32 	unsigned long debounce_jiffies;
33 	struct delayed_work wq_detcable;
34 };
35 
36 static const unsigned int usb_extcon_cable[] = {
37 	EXTCON_USB,
38 	EXTCON_USB_HOST,
39 	EXTCON_NONE,
40 };
41 
42 /*
43  * "USB" = VBUS and "USB-HOST" = !ID, so we have:
44  * Both "USB" and "USB-HOST" can't be set as active at the
45  * same time so if "USB-HOST" is active (i.e. ID is 0)  we keep "USB" inactive
46  * even if VBUS is on.
47  *
48  *  State              |    ID   |   VBUS
49  * ----------------------------------------
50  *  [1] USB            |    H    |    H
51  *  [2] none           |    H    |    L
52  *  [3] USB-HOST       |    L    |    H
53  *  [4] USB-HOST       |    L    |    L
54  *
55  * In case we have only one of these signals:
56  * - VBUS only - we want to distinguish between [1] and [2], so ID is always 1.
57  * - ID only - we want to distinguish between [1] and [4], so VBUS = ID.
58 */
59 static void usb_extcon_detect_cable(struct work_struct *work)
60 {
61 	int id, vbus;
62 	struct usb_extcon_info *info = container_of(to_delayed_work(work),
63 						    struct usb_extcon_info,
64 						    wq_detcable);
65 
66 	/* check ID and VBUS and update cable state */
67 	id = info->id_gpiod ?
68 		gpiod_get_value_cansleep(info->id_gpiod) : 1;
69 	vbus = info->vbus_gpiod ?
70 		gpiod_get_value_cansleep(info->vbus_gpiod) : id;
71 
72 	/* at first we clean states which are no longer active */
73 	if (id)
74 		extcon_set_state_sync(info->edev, EXTCON_USB_HOST, false);
75 	if (!vbus)
76 		extcon_set_state_sync(info->edev, EXTCON_USB, false);
77 
78 	if (!id) {
79 		extcon_set_state_sync(info->edev, EXTCON_USB_HOST, true);
80 	} else {
81 		if (vbus)
82 			extcon_set_state_sync(info->edev, EXTCON_USB, true);
83 	}
84 }
85 
86 static irqreturn_t usb_irq_handler(int irq, void *dev_id)
87 {
88 	struct usb_extcon_info *info = dev_id;
89 
90 	queue_delayed_work(system_power_efficient_wq, &info->wq_detcable,
91 			   info->debounce_jiffies);
92 
93 	return IRQ_HANDLED;
94 }
95 
96 static int usb_extcon_probe(struct platform_device *pdev)
97 {
98 	struct device *dev = &pdev->dev;
99 	struct device_node *np = dev->of_node;
100 	struct usb_extcon_info *info;
101 	int ret;
102 
103 	if (!np)
104 		return -EINVAL;
105 
106 	info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
107 	if (!info)
108 		return -ENOMEM;
109 
110 	info->dev = dev;
111 	info->id_gpiod = devm_gpiod_get_optional(&pdev->dev, "id", GPIOD_IN);
112 	info->vbus_gpiod = devm_gpiod_get_optional(&pdev->dev, "vbus",
113 						   GPIOD_IN);
114 
115 	if (!info->id_gpiod && !info->vbus_gpiod) {
116 		dev_err(dev, "failed to get gpios\n");
117 		return -ENODEV;
118 	}
119 
120 	if (IS_ERR(info->id_gpiod))
121 		return PTR_ERR(info->id_gpiod);
122 
123 	if (IS_ERR(info->vbus_gpiod))
124 		return PTR_ERR(info->vbus_gpiod);
125 
126 	info->edev = devm_extcon_dev_allocate(dev, usb_extcon_cable);
127 	if (IS_ERR(info->edev)) {
128 		dev_err(dev, "failed to allocate extcon device\n");
129 		return -ENOMEM;
130 	}
131 
132 	ret = devm_extcon_dev_register(dev, info->edev);
133 	if (ret < 0) {
134 		dev_err(dev, "failed to register extcon device\n");
135 		return ret;
136 	}
137 
138 	if (info->id_gpiod)
139 		ret = gpiod_set_debounce(info->id_gpiod,
140 					 USB_GPIO_DEBOUNCE_MS * 1000);
141 	if (!ret && info->vbus_gpiod)
142 		ret = gpiod_set_debounce(info->vbus_gpiod,
143 					 USB_GPIO_DEBOUNCE_MS * 1000);
144 
145 	if (ret < 0)
146 		info->debounce_jiffies = msecs_to_jiffies(USB_GPIO_DEBOUNCE_MS);
147 
148 	INIT_DELAYED_WORK(&info->wq_detcable, usb_extcon_detect_cable);
149 
150 	if (info->id_gpiod) {
151 		info->id_irq = gpiod_to_irq(info->id_gpiod);
152 		if (info->id_irq < 0) {
153 			dev_err(dev, "failed to get ID IRQ\n");
154 			return info->id_irq;
155 		}
156 
157 		ret = devm_request_threaded_irq(dev, info->id_irq, NULL,
158 						usb_irq_handler,
159 						IRQF_TRIGGER_RISING |
160 						IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
161 						pdev->name, info);
162 		if (ret < 0) {
163 			dev_err(dev, "failed to request handler for ID IRQ\n");
164 			return ret;
165 		}
166 	}
167 
168 	if (info->vbus_gpiod) {
169 		info->vbus_irq = gpiod_to_irq(info->vbus_gpiod);
170 		if (info->vbus_irq < 0) {
171 			dev_err(dev, "failed to get VBUS IRQ\n");
172 			return info->vbus_irq;
173 		}
174 
175 		ret = devm_request_threaded_irq(dev, info->vbus_irq, NULL,
176 						usb_irq_handler,
177 						IRQF_TRIGGER_RISING |
178 						IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
179 						pdev->name, info);
180 		if (ret < 0) {
181 			dev_err(dev, "failed to request handler for VBUS IRQ\n");
182 			return ret;
183 		}
184 	}
185 
186 	platform_set_drvdata(pdev, info);
187 	device_set_wakeup_capable(&pdev->dev, true);
188 
189 	/* Perform initial detection */
190 	usb_extcon_detect_cable(&info->wq_detcable.work);
191 
192 	return 0;
193 }
194 
195 static void usb_extcon_remove(struct platform_device *pdev)
196 {
197 	struct usb_extcon_info *info = platform_get_drvdata(pdev);
198 
199 	cancel_delayed_work_sync(&info->wq_detcable);
200 	device_init_wakeup(&pdev->dev, false);
201 }
202 
203 #ifdef CONFIG_PM_SLEEP
204 static int usb_extcon_suspend(struct device *dev)
205 {
206 	struct usb_extcon_info *info = dev_get_drvdata(dev);
207 	int ret = 0;
208 
209 	if (device_may_wakeup(dev)) {
210 		if (info->id_gpiod) {
211 			ret = enable_irq_wake(info->id_irq);
212 			if (ret)
213 				return ret;
214 		}
215 		if (info->vbus_gpiod) {
216 			ret = enable_irq_wake(info->vbus_irq);
217 			if (ret) {
218 				if (info->id_gpiod)
219 					disable_irq_wake(info->id_irq);
220 
221 				return ret;
222 			}
223 		}
224 	}
225 
226 	if (!device_may_wakeup(dev))
227 		pinctrl_pm_select_sleep_state(dev);
228 
229 	return ret;
230 }
231 
232 static int usb_extcon_resume(struct device *dev)
233 {
234 	struct usb_extcon_info *info = dev_get_drvdata(dev);
235 	int ret = 0;
236 
237 	if (!device_may_wakeup(dev))
238 		pinctrl_pm_select_default_state(dev);
239 
240 	if (device_may_wakeup(dev)) {
241 		if (info->id_gpiod) {
242 			ret = disable_irq_wake(info->id_irq);
243 			if (ret)
244 				return ret;
245 		}
246 		if (info->vbus_gpiod) {
247 			ret = disable_irq_wake(info->vbus_irq);
248 			if (ret) {
249 				if (info->id_gpiod)
250 					enable_irq_wake(info->id_irq);
251 
252 				return ret;
253 			}
254 		}
255 	}
256 
257 	queue_delayed_work(system_power_efficient_wq,
258 			   &info->wq_detcable, 0);
259 
260 	return ret;
261 }
262 #endif
263 
264 static SIMPLE_DEV_PM_OPS(usb_extcon_pm_ops,
265 			 usb_extcon_suspend, usb_extcon_resume);
266 
267 static const struct of_device_id usb_extcon_dt_match[] = {
268 	{ .compatible = "linux,extcon-usb-gpio", },
269 	{ /* sentinel */ }
270 };
271 MODULE_DEVICE_TABLE(of, usb_extcon_dt_match);
272 
273 static const struct platform_device_id usb_extcon_platform_ids[] = {
274 	{ .name = "extcon-usb-gpio", },
275 	{ /* sentinel */ }
276 };
277 MODULE_DEVICE_TABLE(platform, usb_extcon_platform_ids);
278 
279 static struct platform_driver usb_extcon_driver = {
280 	.probe		= usb_extcon_probe,
281 	.remove		= usb_extcon_remove,
282 	.driver		= {
283 		.name	= "extcon-usb-gpio",
284 		.pm	= &usb_extcon_pm_ops,
285 		.of_match_table = usb_extcon_dt_match,
286 	},
287 	.id_table = usb_extcon_platform_ids,
288 };
289 
290 module_platform_driver(usb_extcon_driver);
291 
292 MODULE_AUTHOR("Roger Quadros <rogerq@ti.com>");
293 MODULE_DESCRIPTION("USB GPIO extcon driver");
294 MODULE_LICENSE("GPL v2");
295