6db4831e98
Android 14
484 lines
12 KiB
C
484 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* musb_dr.c - dual role switch and host glue layer
|
|
*
|
|
* Copyright (C) 2021 MediaTek Inc.
|
|
*
|
|
* Author: Macpaul Lin <macpaul.lin@mediatek.com>
|
|
*/
|
|
|
|
#include <linux/of_platform.h>
|
|
|
|
#include <usb20.h>
|
|
#include <musb_dr.h>
|
|
#include <musb_host.h>
|
|
#include <musb_gadget.h>
|
|
#ifdef CONFIG_DEBUG_FS
|
|
#include <musb_debug.h>
|
|
#endif
|
|
|
|
#if IS_ENABLED(CONFIG_MTK_BASE_POWER)
|
|
#include "mtk_spm_resource_req.h"
|
|
#endif
|
|
|
|
#define USB2_PORT 2
|
|
|
|
enum mt_usb_vbus_id_state {
|
|
MUSB_ID_FLOAT = 1,
|
|
MUSB_ID_GROUND,
|
|
MUSB_VBUS_OFF,
|
|
MUSB_VBUS_VALID,
|
|
};
|
|
|
|
static char *mailbox_state_string(enum mt_usb_vbus_id_state state)
|
|
{
|
|
switch (state) {
|
|
case MUSB_ID_FLOAT:
|
|
return "ID_FLOAT";
|
|
case MUSB_ID_GROUND:
|
|
return "ID_GROUND";
|
|
case MUSB_VBUS_OFF:
|
|
return "VBUS_OFF";
|
|
case MUSB_VBUS_VALID:
|
|
return "VBUS_VALID";
|
|
default:
|
|
return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
int mt_usb_set_vbus(struct otg_switch_mtk *otg_sx, int is_on)
|
|
{
|
|
struct mt_usb_glue *glue =
|
|
container_of(otg_sx, struct mt_usb_glue, otg_sx);
|
|
struct musb *musb = glue->mtk_musb;
|
|
struct regulator *vbus = otg_sx->vbus;
|
|
int ret;
|
|
|
|
/* vbus is optional */
|
|
if (!vbus)
|
|
return 0;
|
|
|
|
dev_dbg(musb->controller, "%s: turn %s\n", __func__, is_on ? "on" : "off");
|
|
|
|
if (is_on) {
|
|
ret = regulator_enable(vbus);
|
|
if (ret) {
|
|
dev_err(musb->controller, "vbus regulator enable failed\n");
|
|
return ret;
|
|
}
|
|
} else {
|
|
regulator_disable(vbus);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(mt_usb_set_vbus);
|
|
|
|
static void mt_usb_gadget_disconnect(struct musb *musb)
|
|
{
|
|
/* notify gadget driver */
|
|
if (musb->g.speed == USB_SPEED_UNKNOWN)
|
|
return;
|
|
|
|
if (musb->gadget_driver && musb->gadget_driver->disconnect) {
|
|
musb->gadget_driver->disconnect(&musb->g);
|
|
musb->g.speed = USB_SPEED_UNKNOWN;
|
|
}
|
|
|
|
usb_gadget_set_state(&musb->g, USB_STATE_NOTATTACHED);
|
|
}
|
|
|
|
/*
|
|
* switch to host: -> MUSB_VBUS_OFF --> MUSB_ID_GROUND
|
|
* switch to device: -> MUSB_ID_FLOAT --> MUSB_VBUS_VALID
|
|
*/
|
|
static void mt_usb_set_mailbox(struct otg_switch_mtk *otg_sx,
|
|
enum mt_usb_vbus_id_state status)
|
|
{
|
|
struct mt_usb_glue *glue =
|
|
container_of(otg_sx, struct mt_usb_glue, otg_sx);
|
|
struct musb *musb = glue->mtk_musb;
|
|
int i;
|
|
|
|
dev_info(musb->controller, "mailbox %s\n", mailbox_state_string(status));
|
|
switch (status) {
|
|
case MUSB_ID_GROUND:
|
|
mt_usb_set_vbus(otg_sx, 1);
|
|
musb->is_ready = true;
|
|
otg_sx->sw_state |= MUSB_ID_GROUND;
|
|
mt_usb_host_connect(0);
|
|
break;
|
|
case MUSB_ID_FLOAT:
|
|
mt_usb_host_disconnect(0);
|
|
musb->is_ready = false;
|
|
/* turn off VBUS until do_host_work switch to DEV mode */
|
|
for (i = 0; i < 6; i++) {
|
|
if (!musb->is_host)
|
|
break;
|
|
mdelay(50);
|
|
}
|
|
mt_usb_set_vbus(otg_sx, 0);
|
|
otg_sx->sw_state &= ~MUSB_ID_GROUND;
|
|
break;
|
|
case MUSB_VBUS_OFF:
|
|
/* ToDo or fix: killing any outstanding requests */
|
|
mt_usb_set_vbus(otg_sx, false);
|
|
musb->usb_connected = 0;
|
|
musb->is_host = false;
|
|
mt_usb_disconnect(); /* sync to UI */
|
|
mt_usb_gadget_disconnect(musb); /* sync to UI */
|
|
otg_sx->sw_state &= ~MUSB_VBUS_VALID;
|
|
break;
|
|
case MUSB_VBUS_VALID:
|
|
mt_usb_set_vbus(otg_sx, true);
|
|
/* avoid suspend when works as device */
|
|
otg_sx->sw_state |= MUSB_VBUS_VALID;
|
|
musb->usb_connected = 1;
|
|
mt_usb_connect();
|
|
break;
|
|
default:
|
|
dev_err(musb->controller, "invalid state\n");
|
|
}
|
|
}
|
|
|
|
static void mt_usb_id_work(struct work_struct *work)
|
|
{
|
|
struct otg_switch_mtk *otg_sx =
|
|
container_of(work, struct otg_switch_mtk, id_work);
|
|
|
|
if (otg_sx->id_event)
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_GROUND);
|
|
else
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_FLOAT);
|
|
}
|
|
|
|
static void mt_usb_vbus_work(struct work_struct *work)
|
|
{
|
|
struct otg_switch_mtk *otg_sx =
|
|
container_of(work, struct otg_switch_mtk, vbus_work);
|
|
|
|
if (otg_sx->vbus_event)
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_VALID);
|
|
else
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_OFF);
|
|
}
|
|
|
|
/*
|
|
* @mt_usb_id_notifier is called in atomic context, but @mt_usb_set_mailbox
|
|
* may sleep, so use work queue here
|
|
*/
|
|
static int mt_usb_id_notifier(struct notifier_block *nb,
|
|
unsigned long event, void *ptr)
|
|
{
|
|
struct otg_switch_mtk *otg_sx =
|
|
container_of(nb, struct otg_switch_mtk, id_nb);
|
|
|
|
otg_sx->id_event = event;
|
|
schedule_work(&otg_sx->id_work);
|
|
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
static int mt_usb_vbus_notifier(struct notifier_block *nb,
|
|
unsigned long event, void *ptr)
|
|
{
|
|
struct otg_switch_mtk *otg_sx =
|
|
container_of(nb, struct otg_switch_mtk, vbus_nb);
|
|
|
|
otg_sx->vbus_event = event;
|
|
schedule_work(&otg_sx->vbus_work);
|
|
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
static int mt_usb_extcon_register(struct otg_switch_mtk *otg_sx)
|
|
{
|
|
struct mt_usb_glue *glue =
|
|
container_of(otg_sx, struct mt_usb_glue, otg_sx);
|
|
struct musb *musb = glue->mtk_musb;
|
|
struct extcon_dev *edev = otg_sx->edev;
|
|
int ret;
|
|
|
|
/* extcon is optional */
|
|
if (!edev)
|
|
return 0;
|
|
|
|
otg_sx->vbus_nb.notifier_call = mt_usb_vbus_notifier;
|
|
ret = devm_extcon_register_notifier(musb->controller, edev, EXTCON_USB,
|
|
&otg_sx->vbus_nb);
|
|
if (ret < 0) {
|
|
dev_err(musb->controller, "failed to register notifier for USB\n");
|
|
return ret;
|
|
}
|
|
|
|
otg_sx->id_nb.notifier_call = mt_usb_id_notifier;
|
|
ret = devm_extcon_register_notifier(musb->controller, edev, EXTCON_USB_HOST,
|
|
&otg_sx->id_nb);
|
|
if (ret < 0) {
|
|
dev_err(musb->controller, "failed to register notifier for USB-HOST\n");
|
|
return ret;
|
|
}
|
|
|
|
dev_dbg(musb->controller, "EXTCON_USB: %d, EXTCON_USB_HOST: %d\n",
|
|
extcon_get_state(edev, EXTCON_USB),
|
|
extcon_get_state(edev, EXTCON_USB_HOST));
|
|
|
|
/* default as host, switch to device mode if needed */
|
|
if (extcon_get_state(edev, EXTCON_USB_HOST) == false)
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_FLOAT);
|
|
if (extcon_get_state(edev, EXTCON_USB) == true)
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_VALID);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* We provide an interface via debugfs to switch between host and device modes
|
|
* depending on user input.
|
|
* This is useful in special cases, such as uses TYPE-A receptacle but also
|
|
* wants to support dual-role mode.
|
|
*/
|
|
void mt_usb_mode_switch(struct musb *musb, int to_host)
|
|
{
|
|
struct mt_usb_glue *glue =
|
|
container_of(&musb, struct mt_usb_glue, mtk_musb);
|
|
struct otg_switch_mtk *otg_sx = &glue->otg_sx;
|
|
|
|
if (to_host) {
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_OFF);
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_GROUND);
|
|
} else {
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_FLOAT);
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_VALID);
|
|
}
|
|
}
|
|
EXPORT_SYMBOL(mt_usb_mode_switch);
|
|
|
|
static int mt_usb_role_sx_set(struct device *dev, enum usb_role role)
|
|
{
|
|
struct mt_usb_glue *glue = dev_get_drvdata(dev);
|
|
struct otg_switch_mtk *otg_sx = &glue->otg_sx;
|
|
bool id_event, vbus_event;
|
|
static bool first_init = true;
|
|
|
|
dev_info(dev, "role_sx_set role %d, latest_role: %d\n",
|
|
role, otg_sx->latest_role);
|
|
|
|
/* Avoid transit from HOST -> DEV with NONE state */
|
|
if ((role == USB_ROLE_DEVICE && otg_sx->latest_role == USB_ROLE_HOST) ||
|
|
(role == USB_ROLE_HOST && otg_sx->latest_role == USB_ROLE_DEVICE)) {
|
|
DBG(0, "force USB_ROLE_NONE transit state.\n");
|
|
mt_usb_role_sx_set(dev, USB_ROLE_NONE);
|
|
}
|
|
|
|
otg_sx->latest_role = role;
|
|
|
|
if (otg_sx->op_mode != MUSB_DR_OPERATION_NORMAL) {
|
|
dev_info(dev, "op_mode %d, skip set role\n", otg_sx->op_mode);
|
|
return 0;
|
|
}
|
|
|
|
id_event = (role == USB_ROLE_HOST);
|
|
vbus_event = (role == USB_ROLE_DEVICE);
|
|
|
|
#ifdef CONFIG_MTK_UART_USB_SWITCH
|
|
in_uart_mode = usb_phy_check_in_uart_mode();
|
|
if (in_uart_mode) {
|
|
DBG(0, "At UART mode. Switch to USB is not support\n");
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_OFF);
|
|
phy_set_mode(glue->phy, PHY_MODE_INVALID);
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
if (!!(otg_sx->sw_state & MUSB_VBUS_VALID) ^ vbus_event) {
|
|
if (vbus_event) {
|
|
dev_info(dev, "%s: if vbus_event true\n", __func__);
|
|
phy_set_mode(glue->phy, PHY_MODE_USB_DEVICE);
|
|
phy_power_on(glue->phy);
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_VALID);
|
|
} else {
|
|
mt_usb_set_mailbox(otg_sx, MUSB_VBUS_OFF);
|
|
dev_info(dev, "%s: if vbus_event false\n", __func__);
|
|
phy_power_off(glue->phy);
|
|
}
|
|
}
|
|
|
|
if (!!(otg_sx->sw_state & MUSB_ID_GROUND) ^ id_event) {
|
|
if (id_event) {
|
|
dev_info(dev, "%s: if id_event true\n", __func__);
|
|
|
|
phy_power_on(glue->phy);
|
|
|
|
/* PHY mode will be set in host_connect work */
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_GROUND);
|
|
} else {
|
|
/*
|
|
* add this for reduce boot 200ms
|
|
* and add delay 200ms for plugout
|
|
*/
|
|
if (!first_init)
|
|
mdelay(200);
|
|
else
|
|
first_init = false;
|
|
|
|
/* PHY mode will be set in host_disconnect work */
|
|
mt_usb_set_mailbox(otg_sx, MUSB_ID_FLOAT);
|
|
phy_power_off(glue->phy);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static enum usb_role mt_usb_role_sx_get(struct device *dev)
|
|
{
|
|
struct mt_usb_glue *glue = dev_get_drvdata(dev);
|
|
struct musb *musb = glue->mtk_musb;
|
|
enum usb_role role;
|
|
|
|
role = musb->is_host ? USB_ROLE_HOST : USB_ROLE_DEVICE;
|
|
|
|
return role;
|
|
}
|
|
|
|
static int mt_usb_role_sw_register(struct otg_switch_mtk *otg_sx)
|
|
{
|
|
struct usb_role_switch_desc role_sx_desc = { 0 };
|
|
struct mt_usb_glue *glue =
|
|
container_of(otg_sx, struct mt_usb_glue, otg_sx);
|
|
struct musb *musb = glue->mtk_musb;
|
|
|
|
if (!otg_sx->role_sw_used)
|
|
return 0;
|
|
|
|
role_sx_desc.set = mt_usb_role_sx_set;
|
|
role_sx_desc.get = mt_usb_role_sx_get;
|
|
role_sx_desc.allow_userspace_control = true;
|
|
otg_sx->role_sw = usb_role_switch_register(glue->dev, &role_sx_desc);
|
|
|
|
if (IS_ERR(otg_sx->role_sw))
|
|
return PTR_ERR(otg_sx->role_sw);
|
|
|
|
mt_usb_role_sx_set(glue->dev, USB_ROLE_NONE);
|
|
musb->usb_connected = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t cmode_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct musb *mtk_musb = dev_get_drvdata(dev);
|
|
struct otg_switch_mtk *otg_sx = mtk_musb->otg_sx;
|
|
enum usb_role role = otg_sx->latest_role;
|
|
|
|
/* note: can't use container_of() by mtk_musb glue, use otg_sx here */
|
|
struct mt_usb_glue *glue =
|
|
container_of(otg_sx, struct mt_usb_glue, otg_sx);
|
|
int mode;
|
|
|
|
if (kstrtoint(buf, 10, &mode))
|
|
return -EINVAL;
|
|
|
|
dev_info(dev, "store cmode %d op_mode %d\n", mode, otg_sx->op_mode);
|
|
|
|
if (otg_sx->op_mode != mode) {
|
|
/* set switch role */
|
|
switch (mode) {
|
|
case MUSB_DR_OPERATION_NONE:
|
|
otg_sx->latest_role = USB_ROLE_NONE;
|
|
break;
|
|
case MUSB_DR_OPERATION_NORMAL:
|
|
/* switch usb role to latest role */
|
|
break;
|
|
case MUSB_DR_OPERATION_HOST:
|
|
otg_sx->latest_role = USB_ROLE_HOST;
|
|
break;
|
|
case MUSB_DR_OPERATION_DEVICE:
|
|
otg_sx->latest_role = USB_ROLE_DEVICE;
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
/* switch operation mode to normal temporarily */
|
|
otg_sx->op_mode = MUSB_DR_OPERATION_NORMAL;
|
|
/* switch usb role */
|
|
mt_usb_role_sx_set(glue->dev, otg_sx->latest_role);
|
|
/* update operation mode */
|
|
otg_sx->op_mode = mode;
|
|
/* restore role */
|
|
otg_sx->latest_role = role;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t cmode_show(struct device *dev,
|
|
struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct musb *mtk_musb = dev_get_drvdata(dev);
|
|
struct otg_switch_mtk *otg_sx = mtk_musb->otg_sx;
|
|
|
|
return sprintf(buf, "%d\n", otg_sx->op_mode);
|
|
}
|
|
static DEVICE_ATTR_RW(cmode);
|
|
|
|
static struct attribute *mt_usb_dr_attrs[] = {
|
|
&dev_attr_cmode.attr,
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group mt_usb_dr_group = {
|
|
.attrs = mt_usb_dr_attrs,
|
|
};
|
|
|
|
int mt_usb_otg_switch_init(struct mt_usb_glue *glue)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &glue->otg_sx;
|
|
struct musb *mtk_musb = glue->mtk_musb;
|
|
int ret = 0;
|
|
|
|
/* we need to keep otg_sx here for cmode operations */
|
|
mtk_musb->otg_sx = otg_sx;
|
|
|
|
INIT_WORK(&otg_sx->id_work, mt_usb_id_work);
|
|
INIT_WORK(&otg_sx->vbus_work, mt_usb_vbus_work);
|
|
|
|
/* default as host, update state */
|
|
otg_sx->sw_state = mtk_musb->is_host ?
|
|
MUSB_ID_GROUND : MUSB_VBUS_VALID;
|
|
|
|
/* initial operation mode */
|
|
otg_sx->op_mode = MUSB_DR_OPERATION_NORMAL;
|
|
|
|
ret = sysfs_create_group(&mtk_musb->controller->kobj, &mt_usb_dr_group);
|
|
if (ret)
|
|
dev_info(mtk_musb->controller, "error creating sysfs attributes\n");
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
if (otg_sx->manual_drd_enabled)
|
|
musb_dr_debugfs_init(mtk_musb);
|
|
#endif
|
|
else if (otg_sx->role_sw_used)
|
|
ret = mt_usb_role_sw_register(otg_sx);
|
|
else
|
|
ret = mt_usb_extcon_register(otg_sx);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(mt_usb_otg_switch_init);
|
|
|
|
void mt_usb_otg_switch_exit(struct mt_usb_glue *glue)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &glue->otg_sx;
|
|
struct musb *mtk_musb = glue->mtk_musb;
|
|
|
|
cancel_work_sync(&otg_sx->id_work);
|
|
cancel_work_sync(&otg_sx->vbus_work);
|
|
usb_role_switch_unregister(otg_sx->role_sw);
|
|
sysfs_remove_group(&mtk_musb->controller->kobj, &mt_usb_dr_group);
|
|
}
|