forked from Minki/linux
bce3052f0c
The USB role callback functions had a parameter pointing to the parent device (struct device) of the switch. The assumption was that the switch parent is always the controller. Firstly, that may not be true in every case, and secondly, it prevents us from supporting devices that supply multiple muxes. Changing the first parameter of usb_role_switch_set_t and usb_role_switch_get_t from struct device to struct usb_role_switch. Cc: Peter Chen <Peter.Chen@nxp.com> Cc: Felipe Balbi <balbi@kernel.org> Cc: Chunfeng Yun <chunfeng.yun@mediatek.com> Cc: Bin Liu <b-liu@ti.com> Signed-off-by: Heikki Krogerus <heikki.krogerus@linux.intel.com> Link: https://lore.kernel.org/r/20200302135353.56659-6-heikki.krogerus@linux.intel.com Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
391 lines
9.5 KiB
C
391 lines
9.5 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* mtu3_dr.c - dual role switch and host glue layer
|
|
*
|
|
* Copyright (C) 2016 MediaTek Inc.
|
|
*
|
|
* Author: Chunfeng Yun <chunfeng.yun@mediatek.com>
|
|
*/
|
|
|
|
#include <linux/usb/role.h>
|
|
|
|
#include "mtu3.h"
|
|
#include "mtu3_dr.h"
|
|
#include "mtu3_debug.h"
|
|
|
|
#define USB2_PORT 2
|
|
#define USB3_PORT 3
|
|
|
|
enum mtu3_vbus_id_state {
|
|
MTU3_ID_FLOAT = 1,
|
|
MTU3_ID_GROUND,
|
|
MTU3_VBUS_OFF,
|
|
MTU3_VBUS_VALID,
|
|
};
|
|
|
|
static char *mailbox_state_string(enum mtu3_vbus_id_state state)
|
|
{
|
|
switch (state) {
|
|
case MTU3_ID_FLOAT:
|
|
return "ID_FLOAT";
|
|
case MTU3_ID_GROUND:
|
|
return "ID_GROUND";
|
|
case MTU3_VBUS_OFF:
|
|
return "VBUS_OFF";
|
|
case MTU3_VBUS_VALID:
|
|
return "VBUS_VALID";
|
|
default:
|
|
return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
static void toggle_opstate(struct ssusb_mtk *ssusb)
|
|
{
|
|
if (!ssusb->otg_switch.is_u3_drd) {
|
|
mtu3_setbits(ssusb->mac_base, U3D_DEVICE_CONTROL, DC_SESSION);
|
|
mtu3_setbits(ssusb->mac_base, U3D_POWER_MANAGEMENT, SOFT_CONN);
|
|
}
|
|
}
|
|
|
|
/* only port0 supports dual-role mode */
|
|
static int ssusb_port0_switch(struct ssusb_mtk *ssusb,
|
|
int version, bool tohost)
|
|
{
|
|
void __iomem *ibase = ssusb->ippc_base;
|
|
u32 value;
|
|
|
|
dev_dbg(ssusb->dev, "%s (switch u%d port0 to %s)\n", __func__,
|
|
version, tohost ? "host" : "device");
|
|
|
|
if (version == USB2_PORT) {
|
|
/* 1. power off and disable u2 port0 */
|
|
value = mtu3_readl(ibase, SSUSB_U2_CTRL(0));
|
|
value |= SSUSB_U2_PORT_PDN | SSUSB_U2_PORT_DIS;
|
|
mtu3_writel(ibase, SSUSB_U2_CTRL(0), value);
|
|
|
|
/* 2. power on, enable u2 port0 and select its mode */
|
|
value = mtu3_readl(ibase, SSUSB_U2_CTRL(0));
|
|
value &= ~(SSUSB_U2_PORT_PDN | SSUSB_U2_PORT_DIS);
|
|
value = tohost ? (value | SSUSB_U2_PORT_HOST_SEL) :
|
|
(value & (~SSUSB_U2_PORT_HOST_SEL));
|
|
mtu3_writel(ibase, SSUSB_U2_CTRL(0), value);
|
|
} else {
|
|
/* 1. power off and disable u3 port0 */
|
|
value = mtu3_readl(ibase, SSUSB_U3_CTRL(0));
|
|
value |= SSUSB_U3_PORT_PDN | SSUSB_U3_PORT_DIS;
|
|
mtu3_writel(ibase, SSUSB_U3_CTRL(0), value);
|
|
|
|
/* 2. power on, enable u3 port0 and select its mode */
|
|
value = mtu3_readl(ibase, SSUSB_U3_CTRL(0));
|
|
value &= ~(SSUSB_U3_PORT_PDN | SSUSB_U3_PORT_DIS);
|
|
value = tohost ? (value | SSUSB_U3_PORT_HOST_SEL) :
|
|
(value & (~SSUSB_U3_PORT_HOST_SEL));
|
|
mtu3_writel(ibase, SSUSB_U3_CTRL(0), value);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void switch_port_to_host(struct ssusb_mtk *ssusb)
|
|
{
|
|
u32 check_clk = 0;
|
|
|
|
dev_dbg(ssusb->dev, "%s\n", __func__);
|
|
|
|
ssusb_port0_switch(ssusb, USB2_PORT, true);
|
|
|
|
if (ssusb->otg_switch.is_u3_drd) {
|
|
ssusb_port0_switch(ssusb, USB3_PORT, true);
|
|
check_clk = SSUSB_U3_MAC_RST_B_STS;
|
|
}
|
|
|
|
ssusb_check_clocks(ssusb, check_clk);
|
|
|
|
/* after all clocks are stable */
|
|
toggle_opstate(ssusb);
|
|
}
|
|
|
|
static void switch_port_to_device(struct ssusb_mtk *ssusb)
|
|
{
|
|
u32 check_clk = 0;
|
|
|
|
dev_dbg(ssusb->dev, "%s\n", __func__);
|
|
|
|
ssusb_port0_switch(ssusb, USB2_PORT, false);
|
|
|
|
if (ssusb->otg_switch.is_u3_drd) {
|
|
ssusb_port0_switch(ssusb, USB3_PORT, false);
|
|
check_clk = SSUSB_U3_MAC_RST_B_STS;
|
|
}
|
|
|
|
ssusb_check_clocks(ssusb, check_clk);
|
|
}
|
|
|
|
int ssusb_set_vbus(struct otg_switch_mtk *otg_sx, int is_on)
|
|
{
|
|
struct ssusb_mtk *ssusb =
|
|
container_of(otg_sx, struct ssusb_mtk, otg_switch);
|
|
struct regulator *vbus = otg_sx->vbus;
|
|
int ret;
|
|
|
|
/* vbus is optional */
|
|
if (!vbus)
|
|
return 0;
|
|
|
|
dev_dbg(ssusb->dev, "%s: turn %s\n", __func__, is_on ? "on" : "off");
|
|
|
|
if (is_on) {
|
|
ret = regulator_enable(vbus);
|
|
if (ret) {
|
|
dev_err(ssusb->dev, "vbus regulator enable failed\n");
|
|
return ret;
|
|
}
|
|
} else {
|
|
regulator_disable(vbus);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* switch to host: -> MTU3_VBUS_OFF --> MTU3_ID_GROUND
|
|
* switch to device: -> MTU3_ID_FLOAT --> MTU3_VBUS_VALID
|
|
*/
|
|
static void ssusb_set_mailbox(struct otg_switch_mtk *otg_sx,
|
|
enum mtu3_vbus_id_state status)
|
|
{
|
|
struct ssusb_mtk *ssusb =
|
|
container_of(otg_sx, struct ssusb_mtk, otg_switch);
|
|
struct mtu3 *mtu = ssusb->u3d;
|
|
|
|
dev_dbg(ssusb->dev, "mailbox %s\n", mailbox_state_string(status));
|
|
mtu3_dbg_trace(ssusb->dev, "mailbox %s", mailbox_state_string(status));
|
|
|
|
switch (status) {
|
|
case MTU3_ID_GROUND:
|
|
switch_port_to_host(ssusb);
|
|
ssusb_set_vbus(otg_sx, 1);
|
|
ssusb->is_host = true;
|
|
break;
|
|
case MTU3_ID_FLOAT:
|
|
ssusb->is_host = false;
|
|
ssusb_set_vbus(otg_sx, 0);
|
|
switch_port_to_device(ssusb);
|
|
break;
|
|
case MTU3_VBUS_OFF:
|
|
mtu3_stop(mtu);
|
|
pm_relax(ssusb->dev);
|
|
break;
|
|
case MTU3_VBUS_VALID:
|
|
/* avoid suspend when works as device */
|
|
pm_stay_awake(ssusb->dev);
|
|
mtu3_start(mtu);
|
|
break;
|
|
default:
|
|
dev_err(ssusb->dev, "invalid state\n");
|
|
}
|
|
}
|
|
|
|
static void ssusb_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)
|
|
ssusb_set_mailbox(otg_sx, MTU3_ID_GROUND);
|
|
else
|
|
ssusb_set_mailbox(otg_sx, MTU3_ID_FLOAT);
|
|
}
|
|
|
|
static void ssusb_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)
|
|
ssusb_set_mailbox(otg_sx, MTU3_VBUS_VALID);
|
|
else
|
|
ssusb_set_mailbox(otg_sx, MTU3_VBUS_OFF);
|
|
}
|
|
|
|
/*
|
|
* @ssusb_id_notifier is called in atomic context, but @ssusb_set_mailbox
|
|
* may sleep, so use work queue here
|
|
*/
|
|
static int ssusb_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 ssusb_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 ssusb_extcon_register(struct otg_switch_mtk *otg_sx)
|
|
{
|
|
struct ssusb_mtk *ssusb =
|
|
container_of(otg_sx, struct ssusb_mtk, otg_switch);
|
|
struct extcon_dev *edev = otg_sx->edev;
|
|
int ret;
|
|
|
|
/* extcon is optional */
|
|
if (!edev)
|
|
return 0;
|
|
|
|
otg_sx->vbus_nb.notifier_call = ssusb_vbus_notifier;
|
|
ret = devm_extcon_register_notifier(ssusb->dev, edev, EXTCON_USB,
|
|
&otg_sx->vbus_nb);
|
|
if (ret < 0) {
|
|
dev_err(ssusb->dev, "failed to register notifier for USB\n");
|
|
return ret;
|
|
}
|
|
|
|
otg_sx->id_nb.notifier_call = ssusb_id_notifier;
|
|
ret = devm_extcon_register_notifier(ssusb->dev, edev, EXTCON_USB_HOST,
|
|
&otg_sx->id_nb);
|
|
if (ret < 0) {
|
|
dev_err(ssusb->dev, "failed to register notifier for USB-HOST\n");
|
|
return ret;
|
|
}
|
|
|
|
dev_dbg(ssusb->dev, "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)
|
|
ssusb_set_mailbox(otg_sx, MTU3_ID_FLOAT);
|
|
if (extcon_get_state(edev, EXTCON_USB) == true)
|
|
ssusb_set_mailbox(otg_sx, MTU3_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 ssusb_mode_switch(struct ssusb_mtk *ssusb, int to_host)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
|
|
if (to_host) {
|
|
ssusb_set_force_mode(ssusb, MTU3_DR_FORCE_HOST);
|
|
ssusb_set_mailbox(otg_sx, MTU3_VBUS_OFF);
|
|
ssusb_set_mailbox(otg_sx, MTU3_ID_GROUND);
|
|
} else {
|
|
ssusb_set_force_mode(ssusb, MTU3_DR_FORCE_DEVICE);
|
|
ssusb_set_mailbox(otg_sx, MTU3_ID_FLOAT);
|
|
ssusb_set_mailbox(otg_sx, MTU3_VBUS_VALID);
|
|
}
|
|
}
|
|
|
|
void ssusb_set_force_mode(struct ssusb_mtk *ssusb,
|
|
enum mtu3_dr_force_mode mode)
|
|
{
|
|
u32 value;
|
|
|
|
value = mtu3_readl(ssusb->ippc_base, SSUSB_U2_CTRL(0));
|
|
switch (mode) {
|
|
case MTU3_DR_FORCE_DEVICE:
|
|
value |= SSUSB_U2_PORT_FORCE_IDDIG | SSUSB_U2_PORT_RG_IDDIG;
|
|
break;
|
|
case MTU3_DR_FORCE_HOST:
|
|
value |= SSUSB_U2_PORT_FORCE_IDDIG;
|
|
value &= ~SSUSB_U2_PORT_RG_IDDIG;
|
|
break;
|
|
case MTU3_DR_FORCE_NONE:
|
|
value &= ~(SSUSB_U2_PORT_FORCE_IDDIG | SSUSB_U2_PORT_RG_IDDIG);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
mtu3_writel(ssusb->ippc_base, SSUSB_U2_CTRL(0), value);
|
|
}
|
|
|
|
static int ssusb_role_sw_set(struct usb_role_switch *sw, enum usb_role role)
|
|
{
|
|
struct ssusb_mtk *ssusb = usb_role_switch_get_drvdata(sw);
|
|
bool to_host = false;
|
|
|
|
if (role == USB_ROLE_HOST)
|
|
to_host = true;
|
|
|
|
if (to_host ^ ssusb->is_host)
|
|
ssusb_mode_switch(ssusb, to_host);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static enum usb_role ssusb_role_sw_get(struct usb_role_switch *sw)
|
|
{
|
|
struct ssusb_mtk *ssusb = usb_role_switch_get_drvdata(sw);
|
|
enum usb_role role;
|
|
|
|
role = ssusb->is_host ? USB_ROLE_HOST : USB_ROLE_DEVICE;
|
|
|
|
return role;
|
|
}
|
|
|
|
static int ssusb_role_sw_register(struct otg_switch_mtk *otg_sx)
|
|
{
|
|
struct usb_role_switch_desc role_sx_desc = { 0 };
|
|
struct ssusb_mtk *ssusb =
|
|
container_of(otg_sx, struct ssusb_mtk, otg_switch);
|
|
|
|
if (!otg_sx->role_sw_used)
|
|
return 0;
|
|
|
|
role_sx_desc.set = ssusb_role_sw_set;
|
|
role_sx_desc.get = ssusb_role_sw_get;
|
|
role_sx_desc.fwnode = dev_fwnode(ssusb->dev);
|
|
role_sx_desc.driver_data = ssusb;
|
|
otg_sx->role_sw = usb_role_switch_register(ssusb->dev, &role_sx_desc);
|
|
|
|
return PTR_ERR_OR_ZERO(otg_sx->role_sw);
|
|
}
|
|
|
|
int ssusb_otg_switch_init(struct ssusb_mtk *ssusb)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
int ret = 0;
|
|
|
|
INIT_WORK(&otg_sx->id_work, ssusb_id_work);
|
|
INIT_WORK(&otg_sx->vbus_work, ssusb_vbus_work);
|
|
|
|
if (otg_sx->manual_drd_enabled)
|
|
ssusb_dr_debugfs_init(ssusb);
|
|
else if (otg_sx->role_sw_used)
|
|
ret = ssusb_role_sw_register(otg_sx);
|
|
else
|
|
ret = ssusb_extcon_register(otg_sx);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void ssusb_otg_switch_exit(struct ssusb_mtk *ssusb)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
|
|
cancel_work_sync(&otg_sx->id_work);
|
|
cancel_work_sync(&otg_sx->vbus_work);
|
|
usb_role_switch_unregister(otg_sx->role_sw);
|
|
}
|