Commit | Line | Data |
---|---|---|
d0ed062a CY |
1 | /* |
2 | * mtu3_dr.c - dual role switch and host glue layer | |
3 | * | |
4 | * Copyright (C) 2016 MediaTek Inc. | |
5 | * | |
6 | * Author: Chunfeng Yun <chunfeng.yun@mediatek.com> | |
7 | * | |
8 | * This software is licensed under the terms of the GNU General Public | |
9 | * License version 2, as published by the Free Software Foundation, and | |
10 | * may be copied, distributed, and modified under those terms. | |
11 | * | |
12 | * This program is distributed in the hope that it will be useful, | |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | * GNU General Public License for more details. | |
16 | * | |
17 | */ | |
18 | ||
19 | #include <linux/debugfs.h> | |
20 | #include <linux/irq.h> | |
21 | #include <linux/kernel.h> | |
22 | #include <linux/of_device.h> | |
23 | #include <linux/pinctrl/consumer.h> | |
24 | #include <linux/seq_file.h> | |
25 | #include <linux/uaccess.h> | |
26 | ||
27 | #include "mtu3.h" | |
28 | #include "mtu3_dr.h" | |
29 | ||
30 | #define USB2_PORT 2 | |
31 | #define USB3_PORT 3 | |
32 | ||
33 | enum mtu3_vbus_id_state { | |
34 | MTU3_ID_FLOAT = 1, | |
35 | MTU3_ID_GROUND, | |
36 | MTU3_VBUS_OFF, | |
37 | MTU3_VBUS_VALID, | |
38 | }; | |
39 | ||
40 | static void toggle_opstate(struct ssusb_mtk *ssusb) | |
41 | { | |
42 | if (!ssusb->otg_switch.is_u3_drd) { | |
43 | mtu3_setbits(ssusb->mac_base, U3D_DEVICE_CONTROL, DC_SESSION); | |
44 | mtu3_setbits(ssusb->mac_base, U3D_POWER_MANAGEMENT, SOFT_CONN); | |
45 | } | |
46 | } | |
47 | ||
48 | /* only port0 supports dual-role mode */ | |
49 | static int ssusb_port0_switch(struct ssusb_mtk *ssusb, | |
50 | int version, bool tohost) | |
51 | { | |
52 | void __iomem *ibase = ssusb->ippc_base; | |
53 | u32 value; | |
54 | ||
55 | dev_dbg(ssusb->dev, "%s (switch u%d port0 to %s)\n", __func__, | |
56 | version, tohost ? "host" : "device"); | |
57 | ||
58 | if (version == USB2_PORT) { | |
59 | /* 1. power off and disable u2 port0 */ | |
60 | value = mtu3_readl(ibase, SSUSB_U2_CTRL(0)); | |
61 | value |= SSUSB_U2_PORT_PDN | SSUSB_U2_PORT_DIS; | |
62 | mtu3_writel(ibase, SSUSB_U2_CTRL(0), value); | |
63 | ||
64 | /* 2. power on, enable u2 port0 and select its mode */ | |
65 | value = mtu3_readl(ibase, SSUSB_U2_CTRL(0)); | |
66 | value &= ~(SSUSB_U2_PORT_PDN | SSUSB_U2_PORT_DIS); | |
67 | value = tohost ? (value | SSUSB_U2_PORT_HOST_SEL) : | |
68 | (value & (~SSUSB_U2_PORT_HOST_SEL)); | |
69 | mtu3_writel(ibase, SSUSB_U2_CTRL(0), value); | |
70 | } else { | |
71 | /* 1. power off and disable u3 port0 */ | |
72 | value = mtu3_readl(ibase, SSUSB_U3_CTRL(0)); | |
73 | value |= SSUSB_U3_PORT_PDN | SSUSB_U3_PORT_DIS; | |
74 | mtu3_writel(ibase, SSUSB_U3_CTRL(0), value); | |
75 | ||
76 | /* 2. power on, enable u3 port0 and select its mode */ | |
77 | value = mtu3_readl(ibase, SSUSB_U3_CTRL(0)); | |
78 | value &= ~(SSUSB_U3_PORT_PDN | SSUSB_U3_PORT_DIS); | |
79 | value = tohost ? (value | SSUSB_U3_PORT_HOST_SEL) : | |
80 | (value & (~SSUSB_U3_PORT_HOST_SEL)); | |
81 | mtu3_writel(ibase, SSUSB_U3_CTRL(0), value); | |
82 | } | |
83 | ||
84 | return 0; | |
85 | } | |
86 | ||
87 | static void switch_port_to_host(struct ssusb_mtk *ssusb) | |
88 | { | |
89 | u32 check_clk = 0; | |
90 | ||
91 | dev_dbg(ssusb->dev, "%s\n", __func__); | |
92 | ||
93 | ssusb_port0_switch(ssusb, USB2_PORT, true); | |
94 | ||
95 | if (ssusb->otg_switch.is_u3_drd) { | |
96 | ssusb_port0_switch(ssusb, USB3_PORT, true); | |
97 | check_clk = SSUSB_U3_MAC_RST_B_STS; | |
98 | } | |
99 | ||
100 | ssusb_check_clocks(ssusb, check_clk); | |
101 | ||
102 | /* after all clocks are stable */ | |
103 | toggle_opstate(ssusb); | |
104 | } | |
105 | ||
106 | static void switch_port_to_device(struct ssusb_mtk *ssusb) | |
107 | { | |
108 | u32 check_clk = 0; | |
109 | ||
110 | dev_dbg(ssusb->dev, "%s\n", __func__); | |
111 | ||
112 | ssusb_port0_switch(ssusb, USB2_PORT, false); | |
113 | ||
114 | if (ssusb->otg_switch.is_u3_drd) { | |
115 | ssusb_port0_switch(ssusb, USB3_PORT, false); | |
116 | check_clk = SSUSB_U3_MAC_RST_B_STS; | |
117 | } | |
118 | ||
119 | ssusb_check_clocks(ssusb, check_clk); | |
120 | } | |
121 | ||
122 | int ssusb_set_vbus(struct otg_switch_mtk *otg_sx, int is_on) | |
123 | { | |
124 | struct ssusb_mtk *ssusb = | |
125 | container_of(otg_sx, struct ssusb_mtk, otg_switch); | |
126 | struct regulator *vbus = otg_sx->vbus; | |
127 | int ret; | |
128 | ||
129 | /* vbus is optional */ | |
130 | if (!vbus) | |
131 | return 0; | |
132 | ||
133 | dev_dbg(ssusb->dev, "%s: turn %s\n", __func__, is_on ? "on" : "off"); | |
134 | ||
135 | if (is_on) { | |
136 | ret = regulator_enable(vbus); | |
137 | if (ret) { | |
138 | dev_err(ssusb->dev, "vbus regulator enable failed\n"); | |
139 | return ret; | |
140 | } | |
141 | } else { | |
142 | regulator_disable(vbus); | |
143 | } | |
144 | ||
145 | return 0; | |
146 | } | |
147 | ||
148 | /* | |
149 | * switch to host: -> MTU3_VBUS_OFF --> MTU3_ID_GROUND | |
150 | * switch to device: -> MTU3_ID_FLOAT --> MTU3_VBUS_VALID | |
151 | */ | |
152 | static void ssusb_set_mailbox(struct otg_switch_mtk *otg_sx, | |
153 | enum mtu3_vbus_id_state status) | |
154 | { | |
155 | struct ssusb_mtk *ssusb = | |
156 | container_of(otg_sx, struct ssusb_mtk, otg_switch); | |
157 | struct mtu3 *mtu = ssusb->u3d; | |
158 | ||
159 | dev_dbg(ssusb->dev, "mailbox state(%d)\n", status); | |
160 | ||
161 | switch (status) { | |
162 | case MTU3_ID_GROUND: | |
163 | switch_port_to_host(ssusb); | |
164 | ssusb_set_vbus(otg_sx, 1); | |
165 | ssusb->is_host = true; | |
166 | break; | |
167 | case MTU3_ID_FLOAT: | |
168 | ssusb->is_host = false; | |
169 | ssusb_set_vbus(otg_sx, 0); | |
170 | switch_port_to_device(ssusb); | |
171 | break; | |
172 | case MTU3_VBUS_OFF: | |
173 | mtu3_stop(mtu); | |
174 | pm_relax(ssusb->dev); | |
175 | break; | |
176 | case MTU3_VBUS_VALID: | |
177 | /* avoid suspend when works as device */ | |
178 | pm_stay_awake(ssusb->dev); | |
179 | mtu3_start(mtu); | |
180 | break; | |
181 | default: | |
182 | dev_err(ssusb->dev, "invalid state\n"); | |
183 | } | |
184 | } | |
185 | ||
186 | static int ssusb_id_notifier(struct notifier_block *nb, | |
187 | unsigned long event, void *ptr) | |
188 | { | |
189 | struct otg_switch_mtk *otg_sx = | |
190 | container_of(nb, struct otg_switch_mtk, id_nb); | |
191 | ||
192 | if (event) | |
193 | ssusb_set_mailbox(otg_sx, MTU3_ID_GROUND); | |
194 | else | |
195 | ssusb_set_mailbox(otg_sx, MTU3_ID_FLOAT); | |
196 | ||
197 | return NOTIFY_DONE; | |
198 | } | |
199 | ||
200 | static int ssusb_vbus_notifier(struct notifier_block *nb, | |
201 | unsigned long event, void *ptr) | |
202 | { | |
203 | struct otg_switch_mtk *otg_sx = | |
204 | container_of(nb, struct otg_switch_mtk, vbus_nb); | |
205 | ||
206 | if (event) | |
207 | ssusb_set_mailbox(otg_sx, MTU3_VBUS_VALID); | |
208 | else | |
209 | ssusb_set_mailbox(otg_sx, MTU3_VBUS_OFF); | |
210 | ||
211 | return NOTIFY_DONE; | |
212 | } | |
213 | ||
214 | static int ssusb_extcon_register(struct otg_switch_mtk *otg_sx) | |
215 | { | |
216 | struct ssusb_mtk *ssusb = | |
217 | container_of(otg_sx, struct ssusb_mtk, otg_switch); | |
218 | struct extcon_dev *edev = otg_sx->edev; | |
219 | int ret; | |
220 | ||
221 | /* extcon is optional */ | |
222 | if (!edev) | |
223 | return 0; | |
224 | ||
225 | otg_sx->vbus_nb.notifier_call = ssusb_vbus_notifier; | |
226 | ret = extcon_register_notifier(edev, EXTCON_USB, | |
227 | &otg_sx->vbus_nb); | |
228 | if (ret < 0) | |
229 | dev_err(ssusb->dev, "failed to register notifier for USB\n"); | |
230 | ||
231 | otg_sx->id_nb.notifier_call = ssusb_id_notifier; | |
232 | ret = extcon_register_notifier(edev, EXTCON_USB_HOST, | |
233 | &otg_sx->id_nb); | |
234 | if (ret < 0) | |
235 | dev_err(ssusb->dev, "failed to register notifier for USB-HOST\n"); | |
236 | ||
237 | dev_dbg(ssusb->dev, "EXTCON_USB: %d, EXTCON_USB_HOST: %d\n", | |
238 | extcon_get_cable_state_(edev, EXTCON_USB), | |
239 | extcon_get_cable_state_(edev, EXTCON_USB_HOST)); | |
240 | ||
241 | /* default as host, switch to device mode if needed */ | |
242 | if (extcon_get_cable_state_(edev, EXTCON_USB_HOST) == false) | |
243 | ssusb_set_mailbox(otg_sx, MTU3_ID_FLOAT); | |
244 | if (extcon_get_cable_state_(edev, EXTCON_USB) == true) | |
245 | ssusb_set_mailbox(otg_sx, MTU3_VBUS_VALID); | |
246 | ||
247 | return 0; | |
248 | } | |
249 | ||
250 | static void extcon_register_dwork(struct work_struct *work) | |
251 | { | |
252 | struct delayed_work *dwork = to_delayed_work(work); | |
253 | struct otg_switch_mtk *otg_sx = | |
254 | container_of(dwork, struct otg_switch_mtk, extcon_reg_dwork); | |
255 | ||
256 | ssusb_extcon_register(otg_sx); | |
257 | } | |
258 | ||
259 | /* | |
260 | * We provide an interface via debugfs to switch between host and device modes | |
261 | * depending on user input. | |
262 | * This is useful in special cases, such as uses TYPE-A receptacle but also | |
263 | * wants to support dual-role mode. | |
264 | * It generates cable state changes by pulling up/down IDPIN and | |
265 | * notifies driver to switch mode by "extcon-usb-gpio". | |
266 | * NOTE: when use MICRO receptacle, should not enable this interface. | |
267 | */ | |
268 | static void ssusb_mode_manual_switch(struct ssusb_mtk *ssusb, int to_host) | |
269 | { | |
270 | struct otg_switch_mtk *otg_sx = &ssusb->otg_switch; | |
271 | ||
272 | if (to_host) | |
273 | pinctrl_select_state(otg_sx->id_pinctrl, otg_sx->id_ground); | |
274 | else | |
275 | pinctrl_select_state(otg_sx->id_pinctrl, otg_sx->id_float); | |
276 | } | |
277 | ||
278 | ||
279 | static int ssusb_mode_show(struct seq_file *sf, void *unused) | |
280 | { | |
281 | struct ssusb_mtk *ssusb = sf->private; | |
282 | ||
283 | seq_printf(sf, "current mode: %s(%s drd)\n(echo device/host)\n", | |
284 | ssusb->is_host ? "host" : "device", | |
285 | ssusb->otg_switch.manual_drd_enabled ? "manual" : "auto"); | |
286 | ||
287 | return 0; | |
288 | } | |
289 | ||
290 | static int ssusb_mode_open(struct inode *inode, struct file *file) | |
291 | { | |
292 | return single_open(file, ssusb_mode_show, inode->i_private); | |
293 | } | |
294 | ||
295 | static ssize_t ssusb_mode_write(struct file *file, | |
296 | const char __user *ubuf, size_t count, loff_t *ppos) | |
297 | { | |
298 | struct seq_file *sf = file->private_data; | |
299 | struct ssusb_mtk *ssusb = sf->private; | |
300 | char buf[16]; | |
301 | ||
302 | if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count))) | |
303 | return -EFAULT; | |
304 | ||
305 | if (!strncmp(buf, "host", 4) && !ssusb->is_host) { | |
306 | ssusb_mode_manual_switch(ssusb, 1); | |
307 | } else if (!strncmp(buf, "device", 6) && ssusb->is_host) { | |
308 | ssusb_mode_manual_switch(ssusb, 0); | |
309 | } else { | |
310 | dev_err(ssusb->dev, "wrong or duplicated setting\n"); | |
311 | return -EINVAL; | |
312 | } | |
313 | ||
314 | return count; | |
315 | } | |
316 | ||
317 | static const struct file_operations ssusb_mode_fops = { | |
318 | .open = ssusb_mode_open, | |
319 | .write = ssusb_mode_write, | |
320 | .read = seq_read, | |
321 | .llseek = seq_lseek, | |
322 | .release = single_release, | |
323 | }; | |
324 | ||
325 | static void ssusb_debugfs_init(struct ssusb_mtk *ssusb) | |
326 | { | |
327 | struct dentry *root; | |
328 | struct dentry *file; | |
329 | ||
330 | root = debugfs_create_dir(dev_name(ssusb->dev), usb_debug_root); | |
331 | if (IS_ERR_OR_NULL(root)) { | |
332 | if (!root) | |
333 | dev_err(ssusb->dev, "create debugfs root failed\n"); | |
334 | return; | |
335 | } | |
336 | ssusb->dbgfs_root = root; | |
337 | ||
338 | file = debugfs_create_file("mode", S_IRUGO | S_IWUSR, root, | |
339 | ssusb, &ssusb_mode_fops); | |
340 | if (!file) | |
341 | dev_dbg(ssusb->dev, "create debugfs mode failed\n"); | |
342 | } | |
343 | ||
344 | static void ssusb_debugfs_exit(struct ssusb_mtk *ssusb) | |
345 | { | |
346 | debugfs_remove_recursive(ssusb->dbgfs_root); | |
347 | } | |
348 | ||
349 | int ssusb_otg_switch_init(struct ssusb_mtk *ssusb) | |
350 | { | |
351 | struct otg_switch_mtk *otg_sx = &ssusb->otg_switch; | |
352 | ||
353 | INIT_DELAYED_WORK(&otg_sx->extcon_reg_dwork, extcon_register_dwork); | |
354 | ||
355 | if (otg_sx->manual_drd_enabled) | |
356 | ssusb_debugfs_init(ssusb); | |
357 | ||
358 | /* It is enough to delay 1s for waiting for host initialization */ | |
359 | schedule_delayed_work(&otg_sx->extcon_reg_dwork, HZ); | |
360 | ||
361 | return 0; | |
362 | } | |
363 | ||
364 | void ssusb_otg_switch_exit(struct ssusb_mtk *ssusb) | |
365 | { | |
366 | struct otg_switch_mtk *otg_sx = &ssusb->otg_switch; | |
367 | ||
368 | cancel_delayed_work(&otg_sx->extcon_reg_dwork); | |
369 | ||
370 | if (otg_sx->edev) { | |
371 | extcon_unregister_notifier(otg_sx->edev, | |
372 | EXTCON_USB, &otg_sx->vbus_nb); | |
373 | extcon_unregister_notifier(otg_sx->edev, | |
374 | EXTCON_USB_HOST, &otg_sx->id_nb); | |
375 | } | |
376 | ||
377 | if (otg_sx->manual_drd_enabled) | |
378 | ssusb_debugfs_exit(ssusb); | |
379 | } |