forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
284 lines
6.6 KiB
284 lines
6.6 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Intel LGM USB PHY driver |
|
* |
|
* Copyright (C) 2020 Intel Corporation. |
|
*/ |
|
|
|
#include <linux/bitfield.h> |
|
#include <linux/delay.h> |
|
#include <linux/iopoll.h> |
|
#include <linux/module.h> |
|
#include <linux/of.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/regulator/consumer.h> |
|
#include <linux/reset.h> |
|
#include <linux/usb/phy.h> |
|
#include <linux/workqueue.h> |
|
|
|
#define CTRL1_OFFSET 0x14 |
|
#define SRAM_EXT_LD_DONE BIT(25) |
|
#define SRAM_INIT_DONE BIT(26) |
|
|
|
#define TCPC_OFFSET 0x1014 |
|
#define TCPC_MUX_CTL GENMASK(1, 0) |
|
#define MUX_NC 0 |
|
#define MUX_USB 1 |
|
#define MUX_DP 2 |
|
#define MUX_USBDP 3 |
|
#define TCPC_FLIPPED BIT(2) |
|
#define TCPC_LOW_POWER_EN BIT(3) |
|
#define TCPC_VALID BIT(4) |
|
#define TCPC_CONN \ |
|
(TCPC_VALID | FIELD_PREP(TCPC_MUX_CTL, MUX_USB)) |
|
#define TCPC_DISCONN \ |
|
(TCPC_VALID | FIELD_PREP(TCPC_MUX_CTL, MUX_NC) | TCPC_LOW_POWER_EN) |
|
|
|
static const char *const PHY_RESETS[] = { "phy31", "phy", }; |
|
static const char *const CTL_RESETS[] = { "apb", "ctrl", }; |
|
|
|
struct tca_apb { |
|
struct reset_control *resets[ARRAY_SIZE(PHY_RESETS)]; |
|
struct regulator *vbus; |
|
struct work_struct wk; |
|
struct usb_phy phy; |
|
|
|
bool regulator_enabled; |
|
bool phy_initialized; |
|
bool connected; |
|
}; |
|
|
|
static int get_flipped(struct tca_apb *ta, bool *flipped) |
|
{ |
|
union extcon_property_value property; |
|
int ret; |
|
|
|
ret = extcon_get_property(ta->phy.edev, EXTCON_USB_HOST, |
|
EXTCON_PROP_USB_TYPEC_POLARITY, &property); |
|
if (ret) { |
|
dev_err(ta->phy.dev, "no polarity property from extcon\n"); |
|
return ret; |
|
} |
|
|
|
*flipped = property.intval; |
|
|
|
return 0; |
|
} |
|
|
|
static int phy_init(struct usb_phy *phy) |
|
{ |
|
struct tca_apb *ta = container_of(phy, struct tca_apb, phy); |
|
void __iomem *ctrl1 = phy->io_priv + CTRL1_OFFSET; |
|
int val, ret, i; |
|
|
|
if (ta->phy_initialized) |
|
return 0; |
|
|
|
for (i = 0; i < ARRAY_SIZE(PHY_RESETS); i++) |
|
reset_control_deassert(ta->resets[i]); |
|
|
|
ret = readl_poll_timeout(ctrl1, val, val & SRAM_INIT_DONE, 10, 10 * 1000); |
|
if (ret) { |
|
dev_err(ta->phy.dev, "SRAM init failed, 0x%x\n", val); |
|
return ret; |
|
} |
|
|
|
writel(readl(ctrl1) | SRAM_EXT_LD_DONE, ctrl1); |
|
|
|
ta->phy_initialized = true; |
|
if (!ta->phy.edev) { |
|
writel(TCPC_CONN, ta->phy.io_priv + TCPC_OFFSET); |
|
return phy->set_vbus(phy, true); |
|
} |
|
|
|
schedule_work(&ta->wk); |
|
|
|
return ret; |
|
} |
|
|
|
static void phy_shutdown(struct usb_phy *phy) |
|
{ |
|
struct tca_apb *ta = container_of(phy, struct tca_apb, phy); |
|
int i; |
|
|
|
if (!ta->phy_initialized) |
|
return; |
|
|
|
ta->phy_initialized = false; |
|
flush_work(&ta->wk); |
|
ta->phy.set_vbus(&ta->phy, false); |
|
|
|
ta->connected = false; |
|
writel(TCPC_DISCONN, ta->phy.io_priv + TCPC_OFFSET); |
|
|
|
for (i = 0; i < ARRAY_SIZE(PHY_RESETS); i++) |
|
reset_control_assert(ta->resets[i]); |
|
} |
|
|
|
static int phy_set_vbus(struct usb_phy *phy, int on) |
|
{ |
|
struct tca_apb *ta = container_of(phy, struct tca_apb, phy); |
|
int ret; |
|
|
|
if (!!on == ta->regulator_enabled) |
|
return 0; |
|
|
|
if (on) |
|
ret = regulator_enable(ta->vbus); |
|
else |
|
ret = regulator_disable(ta->vbus); |
|
|
|
if (!ret) |
|
ta->regulator_enabled = on; |
|
|
|
dev_dbg(ta->phy.dev, "set vbus: %d\n", on); |
|
return ret; |
|
} |
|
|
|
static void tca_work(struct work_struct *work) |
|
{ |
|
struct tca_apb *ta = container_of(work, struct tca_apb, wk); |
|
bool connected; |
|
bool flipped = false; |
|
u32 val; |
|
int ret; |
|
|
|
ret = get_flipped(ta, &flipped); |
|
if (ret) |
|
return; |
|
|
|
connected = extcon_get_state(ta->phy.edev, EXTCON_USB_HOST); |
|
if (connected == ta->connected) |
|
return; |
|
|
|
ta->connected = connected; |
|
if (connected) { |
|
val = TCPC_CONN; |
|
if (flipped) |
|
val |= TCPC_FLIPPED; |
|
dev_dbg(ta->phy.dev, "connected%s\n", flipped ? " flipped" : ""); |
|
} else { |
|
val = TCPC_DISCONN; |
|
dev_dbg(ta->phy.dev, "disconnected\n"); |
|
} |
|
|
|
writel(val, ta->phy.io_priv + TCPC_OFFSET); |
|
|
|
ret = ta->phy.set_vbus(&ta->phy, connected); |
|
if (ret) |
|
dev_err(ta->phy.dev, "failed to set VBUS\n"); |
|
} |
|
|
|
static int id_notifier(struct notifier_block *nb, unsigned long event, void *ptr) |
|
{ |
|
struct tca_apb *ta = container_of(nb, struct tca_apb, phy.id_nb); |
|
|
|
if (ta->phy_initialized) |
|
schedule_work(&ta->wk); |
|
|
|
return NOTIFY_DONE; |
|
} |
|
|
|
static int vbus_notifier(struct notifier_block *nb, unsigned long evnt, void *ptr) |
|
{ |
|
return NOTIFY_DONE; |
|
} |
|
|
|
static int phy_probe(struct platform_device *pdev) |
|
{ |
|
struct reset_control *resets[ARRAY_SIZE(CTL_RESETS)]; |
|
struct device *dev = &pdev->dev; |
|
struct usb_phy *phy; |
|
struct tca_apb *ta; |
|
int i; |
|
|
|
ta = devm_kzalloc(dev, sizeof(*ta), GFP_KERNEL); |
|
if (!ta) |
|
return -ENOMEM; |
|
|
|
platform_set_drvdata(pdev, ta); |
|
INIT_WORK(&ta->wk, tca_work); |
|
|
|
phy = &ta->phy; |
|
phy->dev = dev; |
|
phy->label = dev_name(dev); |
|
phy->type = USB_PHY_TYPE_USB3; |
|
phy->init = phy_init; |
|
phy->shutdown = phy_shutdown; |
|
phy->set_vbus = phy_set_vbus; |
|
phy->id_nb.notifier_call = id_notifier; |
|
phy->vbus_nb.notifier_call = vbus_notifier; |
|
|
|
phy->io_priv = devm_platform_ioremap_resource(pdev, 0); |
|
if (IS_ERR(phy->io_priv)) |
|
return PTR_ERR(phy->io_priv); |
|
|
|
ta->vbus = devm_regulator_get(dev, "vbus"); |
|
if (IS_ERR(ta->vbus)) |
|
return PTR_ERR(ta->vbus); |
|
|
|
for (i = 0; i < ARRAY_SIZE(CTL_RESETS); i++) { |
|
resets[i] = devm_reset_control_get_exclusive(dev, CTL_RESETS[i]); |
|
if (IS_ERR(resets[i])) { |
|
dev_err(dev, "%s reset not found\n", CTL_RESETS[i]); |
|
return PTR_ERR(resets[i]); |
|
} |
|
} |
|
|
|
for (i = 0; i < ARRAY_SIZE(PHY_RESETS); i++) { |
|
ta->resets[i] = devm_reset_control_get_exclusive(dev, PHY_RESETS[i]); |
|
if (IS_ERR(ta->resets[i])) { |
|
dev_err(dev, "%s reset not found\n", PHY_RESETS[i]); |
|
return PTR_ERR(ta->resets[i]); |
|
} |
|
} |
|
|
|
for (i = 0; i < ARRAY_SIZE(CTL_RESETS); i++) |
|
reset_control_assert(resets[i]); |
|
|
|
for (i = 0; i < ARRAY_SIZE(PHY_RESETS); i++) |
|
reset_control_assert(ta->resets[i]); |
|
/* |
|
* Out-of-band reset of the controller after PHY reset will cause |
|
* controller malfunctioning, so we should use in-band controller |
|
* reset only and leave the controller de-asserted here. |
|
*/ |
|
for (i = 0; i < ARRAY_SIZE(CTL_RESETS); i++) |
|
reset_control_deassert(resets[i]); |
|
|
|
/* Need to wait at least 20us after de-assert the controller */ |
|
usleep_range(20, 100); |
|
|
|
return usb_add_phy_dev(phy); |
|
} |
|
|
|
static int phy_remove(struct platform_device *pdev) |
|
{ |
|
struct tca_apb *ta = platform_get_drvdata(pdev); |
|
|
|
usb_remove_phy(&ta->phy); |
|
|
|
return 0; |
|
} |
|
|
|
static const struct of_device_id intel_usb_phy_dt_ids[] = { |
|
{ .compatible = "intel,lgm-usb-phy" }, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(of, intel_usb_phy_dt_ids); |
|
|
|
static struct platform_driver lgm_phy_driver = { |
|
.driver = { |
|
.name = "lgm-usb-phy", |
|
.of_match_table = intel_usb_phy_dt_ids, |
|
}, |
|
.probe = phy_probe, |
|
.remove = phy_remove, |
|
}; |
|
|
|
module_platform_driver(lgm_phy_driver); |
|
|
|
MODULE_DESCRIPTION("Intel LGM USB PHY driver"); |
|
MODULE_AUTHOR("Li Yin <[email protected]>"); |
|
MODULE_AUTHOR("Vadivel Murugan R <[email protected]>"); |
|
MODULE_LICENSE("GPL v2");
|
|
|