1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /** |
3 | * extcon-qcom-spmi-misc.c - Qualcomm USB extcon driver to support USB ID |
4 | * and VBUS detection based on extcon-usb-gpio.c. |
5 | * |
6 | * Copyright (C) 2016 Linaro, Ltd. |
7 | * Stephen Boyd <stephen.boyd@linaro.org> |
8 | */ |
9 | |
10 | #include <linux/devm-helpers.h> |
11 | #include <linux/extcon-provider.h> |
12 | #include <linux/init.h> |
13 | #include <linux/interrupt.h> |
14 | #include <linux/kernel.h> |
15 | #include <linux/module.h> |
16 | #include <linux/mod_devicetable.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/slab.h> |
19 | #include <linux/workqueue.h> |
20 | |
21 | #define USB_ID_DEBOUNCE_MS 5 /* ms */ |
22 | |
23 | struct qcom_usb_extcon_info { |
24 | struct extcon_dev *edev; |
25 | int id_irq; |
26 | int vbus_irq; |
27 | struct delayed_work wq_detcable; |
28 | unsigned long debounce_jiffies; |
29 | }; |
30 | |
31 | static const unsigned int qcom_usb_extcon_cable[] = { |
32 | EXTCON_USB, |
33 | EXTCON_USB_HOST, |
34 | EXTCON_NONE, |
35 | }; |
36 | |
37 | static void qcom_usb_extcon_detect_cable(struct work_struct *work) |
38 | { |
39 | bool state = false; |
40 | int ret; |
41 | union extcon_property_value val; |
42 | struct qcom_usb_extcon_info *info = container_of(to_delayed_work(work), |
43 | struct qcom_usb_extcon_info, |
44 | wq_detcable); |
45 | |
46 | if (info->id_irq > 0) { |
47 | /* check ID and update cable state */ |
48 | ret = irq_get_irqchip_state(irq: info->id_irq, |
49 | which: IRQCHIP_STATE_LINE_LEVEL, state: &state); |
50 | if (ret) |
51 | return; |
52 | |
53 | if (!state) { |
54 | val.intval = true; |
55 | extcon_set_property(edev: info->edev, EXTCON_USB_HOST, |
56 | EXTCON_PROP_USB_SS, prop_val: val); |
57 | } |
58 | extcon_set_state_sync(edev: info->edev, EXTCON_USB_HOST, state: !state); |
59 | } |
60 | |
61 | if (info->vbus_irq > 0) { |
62 | /* check VBUS and update cable state */ |
63 | ret = irq_get_irqchip_state(irq: info->vbus_irq, |
64 | which: IRQCHIP_STATE_LINE_LEVEL, state: &state); |
65 | if (ret) |
66 | return; |
67 | |
68 | if (state) { |
69 | val.intval = true; |
70 | extcon_set_property(edev: info->edev, EXTCON_USB, |
71 | EXTCON_PROP_USB_SS, prop_val: val); |
72 | } |
73 | extcon_set_state_sync(edev: info->edev, EXTCON_USB, state); |
74 | } |
75 | } |
76 | |
77 | static irqreturn_t qcom_usb_irq_handler(int irq, void *dev_id) |
78 | { |
79 | struct qcom_usb_extcon_info *info = dev_id; |
80 | |
81 | queue_delayed_work(wq: system_power_efficient_wq, dwork: &info->wq_detcable, |
82 | delay: info->debounce_jiffies); |
83 | |
84 | return IRQ_HANDLED; |
85 | } |
86 | |
87 | static int qcom_usb_extcon_probe(struct platform_device *pdev) |
88 | { |
89 | struct device *dev = &pdev->dev; |
90 | struct qcom_usb_extcon_info *info; |
91 | int ret; |
92 | |
93 | info = devm_kzalloc(dev, size: sizeof(*info), GFP_KERNEL); |
94 | if (!info) |
95 | return -ENOMEM; |
96 | |
97 | info->edev = devm_extcon_dev_allocate(dev, cable: qcom_usb_extcon_cable); |
98 | if (IS_ERR(ptr: info->edev)) { |
99 | dev_err(dev, "failed to allocate extcon device\n" ); |
100 | return -ENOMEM; |
101 | } |
102 | |
103 | ret = devm_extcon_dev_register(dev, edev: info->edev); |
104 | if (ret < 0) { |
105 | dev_err(dev, "failed to register extcon device\n" ); |
106 | return ret; |
107 | } |
108 | |
109 | ret = extcon_set_property_capability(edev: info->edev, |
110 | EXTCON_USB, EXTCON_PROP_USB_SS); |
111 | ret |= extcon_set_property_capability(edev: info->edev, |
112 | EXTCON_USB_HOST, EXTCON_PROP_USB_SS); |
113 | if (ret) { |
114 | dev_err(dev, "failed to register extcon props rc=%d\n" , |
115 | ret); |
116 | return ret; |
117 | } |
118 | |
119 | info->debounce_jiffies = msecs_to_jiffies(USB_ID_DEBOUNCE_MS); |
120 | |
121 | ret = devm_delayed_work_autocancel(dev, w: &info->wq_detcable, |
122 | worker: qcom_usb_extcon_detect_cable); |
123 | if (ret) |
124 | return ret; |
125 | |
126 | info->id_irq = platform_get_irq_byname_optional(dev: pdev, name: "usb_id" ); |
127 | if (info->id_irq > 0) { |
128 | ret = devm_request_threaded_irq(dev, irq: info->id_irq, NULL, |
129 | thread_fn: qcom_usb_irq_handler, |
130 | IRQF_TRIGGER_RISING | |
131 | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, |
132 | devname: pdev->name, dev_id: info); |
133 | if (ret < 0) { |
134 | dev_err(dev, "failed to request handler for ID IRQ\n" ); |
135 | return ret; |
136 | } |
137 | } |
138 | |
139 | info->vbus_irq = platform_get_irq_byname_optional(dev: pdev, name: "usb_vbus" ); |
140 | if (info->vbus_irq > 0) { |
141 | ret = devm_request_threaded_irq(dev, irq: info->vbus_irq, NULL, |
142 | thread_fn: qcom_usb_irq_handler, |
143 | IRQF_TRIGGER_RISING | |
144 | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, |
145 | devname: pdev->name, dev_id: info); |
146 | if (ret < 0) { |
147 | dev_err(dev, "failed to request handler for VBUS IRQ\n" ); |
148 | return ret; |
149 | } |
150 | } |
151 | |
152 | if (info->id_irq < 0 && info->vbus_irq < 0) { |
153 | dev_err(dev, "ID and VBUS IRQ not found\n" ); |
154 | return -EINVAL; |
155 | } |
156 | |
157 | platform_set_drvdata(pdev, data: info); |
158 | device_init_wakeup(dev, enable: 1); |
159 | |
160 | /* Perform initial detection */ |
161 | qcom_usb_extcon_detect_cable(work: &info->wq_detcable.work); |
162 | |
163 | return 0; |
164 | } |
165 | |
166 | #ifdef CONFIG_PM_SLEEP |
167 | static int qcom_usb_extcon_suspend(struct device *dev) |
168 | { |
169 | struct qcom_usb_extcon_info *info = dev_get_drvdata(dev); |
170 | int ret = 0; |
171 | |
172 | if (device_may_wakeup(dev)) { |
173 | if (info->id_irq > 0) |
174 | ret = enable_irq_wake(irq: info->id_irq); |
175 | if (info->vbus_irq > 0) |
176 | ret = enable_irq_wake(irq: info->vbus_irq); |
177 | } |
178 | |
179 | return ret; |
180 | } |
181 | |
182 | static int qcom_usb_extcon_resume(struct device *dev) |
183 | { |
184 | struct qcom_usb_extcon_info *info = dev_get_drvdata(dev); |
185 | int ret = 0; |
186 | |
187 | if (device_may_wakeup(dev)) { |
188 | if (info->id_irq > 0) |
189 | ret = disable_irq_wake(irq: info->id_irq); |
190 | if (info->vbus_irq > 0) |
191 | ret = disable_irq_wake(irq: info->vbus_irq); |
192 | } |
193 | |
194 | return ret; |
195 | } |
196 | #endif |
197 | |
198 | static SIMPLE_DEV_PM_OPS(qcom_usb_extcon_pm_ops, |
199 | qcom_usb_extcon_suspend, qcom_usb_extcon_resume); |
200 | |
201 | static const struct of_device_id qcom_usb_extcon_dt_match[] = { |
202 | { .compatible = "qcom,pm8941-misc" , }, |
203 | { } |
204 | }; |
205 | MODULE_DEVICE_TABLE(of, qcom_usb_extcon_dt_match); |
206 | |
207 | static struct platform_driver qcom_usb_extcon_driver = { |
208 | .probe = qcom_usb_extcon_probe, |
209 | .driver = { |
210 | .name = "extcon-pm8941-misc" , |
211 | .pm = &qcom_usb_extcon_pm_ops, |
212 | .of_match_table = qcom_usb_extcon_dt_match, |
213 | }, |
214 | }; |
215 | module_platform_driver(qcom_usb_extcon_driver); |
216 | |
217 | MODULE_DESCRIPTION("QCOM USB ID extcon driver" ); |
218 | MODULE_AUTHOR("Stephen Boyd <stephen.boyd@linaro.org>" ); |
219 | MODULE_LICENSE("GPL v2" ); |
220 | |