mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2024-08-26 02:39:48 +00:00
![Greg Kroah-Hartman](/assets/img/avatar_default.png)
When calling debugfs functions, there is no need to ever check the return value. The function can work or not, but the code logic should never do something different based on this. Cc: Chunfeng Yun <chunfeng.yun@mediatek.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
439 lines
11 KiB
C
439 lines
11 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/debugfs.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/pinctrl/consumer.h>
|
|
#include <linux/seq_file.h>
|
|
#include <linux/uaccess.h>
|
|
|
|
#include "mtu3.h"
|
|
#include "mtu3_dr.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 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 state(%d)\n", 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");
|
|
|
|
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");
|
|
|
|
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.
|
|
*/
|
|
static void ssusb_mode_manual_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);
|
|
}
|
|
}
|
|
|
|
static int ssusb_mode_show(struct seq_file *sf, void *unused)
|
|
{
|
|
struct ssusb_mtk *ssusb = sf->private;
|
|
|
|
seq_printf(sf, "current mode: %s(%s drd)\n(echo device/host)\n",
|
|
ssusb->is_host ? "host" : "device",
|
|
ssusb->otg_switch.manual_drd_enabled ? "manual" : "auto");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ssusb_mode_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ssusb_mode_show, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ssusb_mode_write(struct file *file,
|
|
const char __user *ubuf, size_t count, loff_t *ppos)
|
|
{
|
|
struct seq_file *sf = file->private_data;
|
|
struct ssusb_mtk *ssusb = sf->private;
|
|
char buf[16];
|
|
|
|
if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))
|
|
return -EFAULT;
|
|
|
|
if (!strncmp(buf, "host", 4) && !ssusb->is_host) {
|
|
ssusb_mode_manual_switch(ssusb, 1);
|
|
} else if (!strncmp(buf, "device", 6) && ssusb->is_host) {
|
|
ssusb_mode_manual_switch(ssusb, 0);
|
|
} else {
|
|
dev_err(ssusb->dev, "wrong or duplicated setting\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations ssusb_mode_fops = {
|
|
.open = ssusb_mode_open,
|
|
.write = ssusb_mode_write,
|
|
.read = seq_read,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
};
|
|
|
|
static int ssusb_vbus_show(struct seq_file *sf, void *unused)
|
|
{
|
|
struct ssusb_mtk *ssusb = sf->private;
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
|
|
seq_printf(sf, "vbus state: %s\n(echo on/off)\n",
|
|
regulator_is_enabled(otg_sx->vbus) ? "on" : "off");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ssusb_vbus_open(struct inode *inode, struct file *file)
|
|
{
|
|
return single_open(file, ssusb_vbus_show, inode->i_private);
|
|
}
|
|
|
|
static ssize_t ssusb_vbus_write(struct file *file,
|
|
const char __user *ubuf, size_t count, loff_t *ppos)
|
|
{
|
|
struct seq_file *sf = file->private_data;
|
|
struct ssusb_mtk *ssusb = sf->private;
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
char buf[16];
|
|
bool enable;
|
|
|
|
if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))
|
|
return -EFAULT;
|
|
|
|
if (kstrtobool(buf, &enable)) {
|
|
dev_err(ssusb->dev, "wrong setting\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
ssusb_set_vbus(otg_sx, enable);
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct file_operations ssusb_vbus_fops = {
|
|
.open = ssusb_vbus_open,
|
|
.write = ssusb_vbus_write,
|
|
.read = seq_read,
|
|
.llseek = seq_lseek,
|
|
.release = single_release,
|
|
};
|
|
|
|
static void ssusb_debugfs_init(struct ssusb_mtk *ssusb)
|
|
{
|
|
struct dentry *root;
|
|
|
|
root = debugfs_create_dir(dev_name(ssusb->dev), usb_debug_root);
|
|
ssusb->dbgfs_root = root;
|
|
|
|
debugfs_create_file("mode", 0644, root, ssusb, &ssusb_mode_fops);
|
|
debugfs_create_file("vbus", 0644, root, ssusb, &ssusb_vbus_fops);
|
|
}
|
|
|
|
static void ssusb_debugfs_exit(struct ssusb_mtk *ssusb)
|
|
{
|
|
debugfs_remove_recursive(ssusb->dbgfs_root);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
int ssusb_otg_switch_init(struct ssusb_mtk *ssusb)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
|
|
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_debugfs_init(ssusb);
|
|
else
|
|
ssusb_extcon_register(otg_sx);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void ssusb_otg_switch_exit(struct ssusb_mtk *ssusb)
|
|
{
|
|
struct otg_switch_mtk *otg_sx = &ssusb->otg_switch;
|
|
|
|
if (otg_sx->manual_drd_enabled)
|
|
ssusb_debugfs_exit(ssusb);
|
|
|
|
cancel_work_sync(&otg_sx->id_work);
|
|
cancel_work_sync(&otg_sx->vbus_work);
|
|
}
|