2017-11-03 13:28:30 +03:00
// SPDX-License-Identifier: GPL-2.0
2016-10-19 05:28:26 +03:00
/*
* mtu3_dr . c - dual role switch and host glue layer
*
* Copyright ( C ) 2016 MediaTek Inc .
*
* Author : Chunfeng Yun < chunfeng . yun @ mediatek . com >
*/
2019-08-29 12:22:38 +03:00
# include <linux/usb/role.h>
2016-10-19 05:28:26 +03:00
# include "mtu3.h"
# include "mtu3_dr.h"
2019-03-21 05:53:47 +03:00
# include "mtu3_debug.h"
2016-10-19 05:28:26 +03:00
# 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 ,
} ;
2019-03-21 05:53:49 +03:00
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 " ;
}
}
2016-10-19 05:28:26 +03:00
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 ;
2019-03-21 05:53:49 +03:00
dev_dbg ( ssusb - > dev , " mailbox %s \n " , mailbox_state_string ( status ) ) ;
mtu3_dbg_trace ( ssusb - > dev , " mailbox %s " , mailbox_state_string ( status ) ) ;
2016-10-19 05:28:26 +03:00
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 " ) ;
}
}
2018-05-23 11:53:20 +03:00
static void ssusb_id_work ( struct work_struct * work )
2016-10-19 05:28:26 +03:00
{
struct otg_switch_mtk * otg_sx =
2018-05-23 11:53:20 +03:00
container_of ( work , struct otg_switch_mtk , id_work ) ;
2016-10-19 05:28:26 +03:00
2018-05-23 11:53:20 +03:00
if ( otg_sx - > id_event )
2016-10-19 05:28:26 +03:00
ssusb_set_mailbox ( otg_sx , MTU3_ID_GROUND ) ;
else
ssusb_set_mailbox ( otg_sx , MTU3_ID_FLOAT ) ;
2018-05-23 11:53:20 +03:00
}
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 ) ;
2016-10-19 05:28:26 +03:00
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 ) ;
2018-05-23 11:53:20 +03:00
otg_sx - > vbus_event = event ;
schedule_work ( & otg_sx - > vbus_work ) ;
2016-10-19 05:28:26 +03:00
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 ;
2017-03-28 07:42:02 +03:00
ret = devm_extcon_register_notifier ( ssusb - > dev , edev , EXTCON_USB ,
2016-10-19 05:28:26 +03:00
& otg_sx - > vbus_nb ) ;
2019-03-21 05:53:39 +03:00
if ( ret < 0 ) {
2016-10-19 05:28:26 +03:00
dev_err ( ssusb - > dev , " failed to register notifier for USB \n " ) ;
2019-03-21 05:53:39 +03:00
return ret ;
}
2016-10-19 05:28:26 +03:00
otg_sx - > id_nb . notifier_call = ssusb_id_notifier ;
2017-03-28 07:42:02 +03:00
ret = devm_extcon_register_notifier ( ssusb - > dev , edev , EXTCON_USB_HOST ,
2016-10-19 05:28:26 +03:00
& otg_sx - > id_nb ) ;
2019-03-21 05:53:39 +03:00
if ( ret < 0 ) {
2016-10-19 05:28:26 +03:00
dev_err ( ssusb - > dev , " failed to register notifier for USB-HOST \n " ) ;
2019-03-21 05:53:39 +03:00
return ret ;
}
2016-10-19 05:28:26 +03:00
dev_dbg ( ssusb - > dev , " EXTCON_USB: %d, EXTCON_USB_HOST: %d \n " ,
2017-03-28 07:42:02 +03:00
extcon_get_state ( edev , EXTCON_USB ) ,
extcon_get_state ( edev , EXTCON_USB_HOST ) ) ;
2016-10-19 05:28:26 +03:00
/* default as host, switch to device mode if needed */
2017-03-28 07:42:02 +03:00
if ( extcon_get_state ( edev , EXTCON_USB_HOST ) = = false )
2016-10-19 05:28:26 +03:00
ssusb_set_mailbox ( otg_sx , MTU3_ID_FLOAT ) ;
2017-03-28 07:42:02 +03:00
if ( extcon_get_state ( edev , EXTCON_USB ) = = true )
2016-10-19 05:28:26 +03:00
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 .
*/
2019-08-29 12:22:38 +03:00
void ssusb_mode_switch ( struct ssusb_mtk * ssusb , int to_host )
2016-10-19 05:28:26 +03:00
{
struct otg_switch_mtk * otg_sx = & ssusb - > otg_switch ;
2017-10-13 12:10:42 +03:00
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 ) ;
}
2016-10-19 05:28:26 +03:00
}
2017-10-13 12:10:42 +03:00
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 ) ;
}
2019-08-29 12:22:38 +03:00
static int ssusb_role_sw_set ( struct device * dev , enum usb_role role )
{
struct ssusb_mtk * ssusb = dev_get_drvdata ( dev ) ;
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 device * dev )
{
struct ssusb_mtk * ssusb = dev_get_drvdata ( dev ) ;
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 ) ;
otg_sx - > role_sw = usb_role_switch_register ( ssusb - > dev , & role_sx_desc ) ;
return PTR_ERR_OR_ZERO ( otg_sx - > role_sw ) ;
}
2016-10-19 05:28:26 +03:00
int ssusb_otg_switch_init ( struct ssusb_mtk * ssusb )
{
struct otg_switch_mtk * otg_sx = & ssusb - > otg_switch ;
2019-03-21 05:53:39 +03:00
int ret = 0 ;
2016-10-19 05:28:26 +03:00
2018-05-23 11:53:20 +03:00
INIT_WORK ( & otg_sx - > id_work , ssusb_id_work ) ;
INIT_WORK ( & otg_sx - > vbus_work , ssusb_vbus_work ) ;
2018-04-24 05:52:48 +03:00
if ( otg_sx - > manual_drd_enabled )
2019-03-21 05:53:47 +03:00
ssusb_dr_debugfs_init ( ssusb ) ;
2019-08-29 12:22:38 +03:00
else if ( otg_sx - > role_sw_used )
ret = ssusb_role_sw_register ( otg_sx ) ;
2018-04-24 05:52:48 +03:00
else
2019-03-21 05:53:39 +03:00
ret = ssusb_extcon_register ( otg_sx ) ;
2016-10-19 05:28:26 +03:00
2019-03-21 05:53:39 +03:00
return ret ;
2016-10-19 05:28:26 +03:00
}
void ssusb_otg_switch_exit ( struct ssusb_mtk * ssusb )
{
struct otg_switch_mtk * otg_sx = & ssusb - > otg_switch ;
2018-05-23 11:53:20 +03:00
cancel_work_sync ( & otg_sx - > id_work ) ;
cancel_work_sync ( & otg_sx - > vbus_work ) ;
2019-08-29 12:22:38 +03:00
usb_role_switch_unregister ( otg_sx - > role_sw ) ;
2016-10-19 05:28:26 +03:00
}