blob: 800a35b48ab139eb0b04826377f044db5de3aa36 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
/*
* snps_udc_plat.c - Synopsys UDC Platform Driver
*
* Copyright (C) 2016 Broadcom
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation version 2.
*
* This program is distributed "as is" WITHOUT ANY WARRANTY of any
* kind, whether express or implied; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include <linux/extcon.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/phy/phy.h>
#include <linux/module.h>
#include <linux/dmapool.h>
#include <linux/interrupt.h>
#include <linux/moduleparam.h>
#include "amd5536udc.h"
/* description */
#define UDC_MOD_DESCRIPTION "Synopsys UDC platform driver"
static void start_udc(struct udc *udc)
{
if (udc->driver) {
dev_info(udc->dev, "Connecting...\n");
udc_enable_dev_setup_interrupts(udc);
udc_basic_init(udc);
udc->connected = 1;
}
}
static void stop_udc(struct udc *udc)
{
int tmp;
u32 reg;
spin_lock(&udc->lock);
/* Flush the receieve fifo */
reg = readl(&udc->regs->ctl);
reg |= AMD_BIT(UDC_DEVCTL_SRX_FLUSH);
writel(reg, &udc->regs->ctl);
reg = readl(&udc->regs->ctl);
reg &= ~(AMD_BIT(UDC_DEVCTL_SRX_FLUSH));
writel(reg, &udc->regs->ctl);
dev_dbg(udc->dev, "ep rx queue flushed\n");
/* Mask interrupts. Required more so when the
* UDC is connected to a DRD phy.
*/
udc_mask_unused_interrupts(udc);
/* Disconnect gadget driver */
if (udc->driver) {
spin_unlock(&udc->lock);
udc->driver->disconnect(&udc->gadget);
spin_lock(&udc->lock);
/* empty queues */
for (tmp = 0; tmp < UDC_EP_NUM; tmp++)
empty_req_queue(&udc->ep[tmp]);
}
udc->connected = 0;
spin_unlock(&udc->lock);
dev_info(udc->dev, "Device disconnected\n");
}
static void udc_drd_work(struct work_struct *work)
{
struct udc *udc;
udc = container_of(to_delayed_work(work),
struct udc, drd_work);
if (udc->conn_type) {
dev_dbg(udc->dev, "idle -> device\n");
start_udc(udc);
} else {
dev_dbg(udc->dev, "device -> idle\n");
stop_udc(udc);
}
}
static int usbd_connect_notify(struct notifier_block *self,
unsigned long event, void *ptr)
{
struct udc *udc = container_of(self, struct udc, nb);
dev_dbg(udc->dev, "%s: event: %lu\n", __func__, event);
udc->conn_type = event;
schedule_delayed_work(&udc->drd_work, 0);
return NOTIFY_OK;
}
static int udc_plat_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct resource *res;
struct udc *udc;
int ret;
udc = devm_kzalloc(dev, sizeof(*udc), GFP_KERNEL);
if (!udc)
return -ENOMEM;
spin_lock_init(&udc->lock);
udc->dev = dev;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
udc->virt_addr = devm_ioremap_resource(dev, res);
if (IS_ERR(udc->regs))
return PTR_ERR(udc->regs);
/* udc csr registers base */
udc->csr = udc->virt_addr + UDC_CSR_ADDR;
/* dev registers base */
udc->regs = udc->virt_addr + UDC_DEVCFG_ADDR;
/* ep registers base */
udc->ep_regs = udc->virt_addr + UDC_EPREGS_ADDR;
/* fifo's base */
udc->rxfifo = (u32 __iomem *)(udc->virt_addr + UDC_RXFIFO_ADDR);
udc->txfifo = (u32 __iomem *)(udc->virt_addr + UDC_TXFIFO_ADDR);
udc->phys_addr = (unsigned long)res->start;
udc->irq = irq_of_parse_and_map(dev->of_node, 0);
if (udc->irq <= 0) {
dev_err(dev, "Can't parse and map interrupt\n");
return -EINVAL;
}
udc->udc_phy = devm_of_phy_get_by_index(dev, dev->of_node, 0);
if (IS_ERR(udc->udc_phy)) {
dev_err(dev, "Failed to obtain phy from device tree\n");
return PTR_ERR(udc->udc_phy);
}
ret = phy_init(udc->udc_phy);
if (ret) {
dev_err(dev, "UDC phy init failed");
return ret;
}
ret = phy_power_on(udc->udc_phy);
if (ret) {
dev_err(dev, "UDC phy power on failed");
phy_exit(udc->udc_phy);
return ret;
}
/* Register for extcon if supported */
if (of_get_property(dev->of_node, "extcon", NULL)) {
udc->edev = extcon_get_edev_by_phandle(dev, 0);
if (IS_ERR(udc->edev)) {
if (PTR_ERR(udc->edev) == -EPROBE_DEFER)
return -EPROBE_DEFER;
dev_err(dev, "Invalid or missing extcon\n");
ret = PTR_ERR(udc->edev);
goto exit_phy;
}
udc->nb.notifier_call = usbd_connect_notify;
ret = extcon_register_notifier(udc->edev, EXTCON_USB,
&udc->nb);
if (ret < 0) {
dev_err(dev, "Can't register extcon device\n");
goto exit_phy;
}
ret = extcon_get_state(udc->edev, EXTCON_USB);
if (ret < 0) {
dev_err(dev, "Can't get cable state\n");
goto exit_extcon;
} else if (ret) {
udc->conn_type = ret;
}
INIT_DELAYED_WORK(&udc->drd_work, udc_drd_work);
}
/* init dma pools */
if (use_dma) {
ret = init_dma_pools(udc);
if (ret != 0)
goto exit_extcon;
}
ret = devm_request_irq(dev, udc->irq, udc_irq, IRQF_SHARED,
"snps-udc", udc);
if (ret < 0) {
dev_err(dev, "Request irq %d failed for UDC\n", udc->irq);
goto exit_dma;
}
platform_set_drvdata(pdev, udc);
udc->chiprev = UDC_BCM_REV;
if (udc_probe(udc)) {
ret = -ENODEV;
goto exit_dma;
}
dev_info(dev, "Synopsys UDC platform driver probe successful\n");
return 0;
exit_dma:
if (use_dma)
free_dma_pools(udc);
exit_extcon:
if (udc->edev)
extcon_unregister_notifier(udc->edev, EXTCON_USB, &udc->nb);
exit_phy:
if (udc->udc_phy) {
phy_power_off(udc->udc_phy);
phy_exit(udc->udc_phy);
}
return ret;
}
static int udc_plat_remove(struct platform_device *pdev)
{
struct udc *dev;
dev = platform_get_drvdata(pdev);
usb_del_gadget_udc(&dev->gadget);
/* gadget driver must not be registered */
if (WARN_ON(dev->driver))
return 0;
/* dma pool cleanup */
free_dma_pools(dev);
udc_remove(dev);
platform_set_drvdata(pdev, NULL);
if (dev->drd_wq) {
flush_workqueue(dev->drd_wq);
destroy_workqueue(dev->drd_wq);
}
phy_power_off(dev->udc_phy);
phy_exit(dev->udc_phy);
extcon_unregister_notifier(dev->edev, EXTCON_USB, &dev->nb);
dev_info(&pdev->dev, "Synopsys UDC platform driver removed\n");
return 0;
}
#ifdef CONFIG_PM_SLEEP
static int udc_plat_suspend(struct device *dev)
{
struct udc *udc;
udc = dev_get_drvdata(dev);
stop_udc(udc);
if (extcon_get_state(udc->edev, EXTCON_USB) > 0) {
dev_dbg(udc->dev, "device -> idle\n");
stop_udc(udc);
}
phy_power_off(udc->udc_phy);
phy_exit(udc->udc_phy);
return 0;
}
static int udc_plat_resume(struct device *dev)
{
struct udc *udc;
int ret;
udc = dev_get_drvdata(dev);
ret = phy_init(udc->udc_phy);
if (ret) {
dev_err(udc->dev, "UDC phy init failure");
return ret;
}
ret = phy_power_on(udc->udc_phy);
if (ret) {
dev_err(udc->dev, "UDC phy power on failure");
phy_exit(udc->udc_phy);
return ret;
}
if (extcon_get_state(udc->edev, EXTCON_USB) > 0) {
dev_dbg(udc->dev, "idle -> device\n");
start_udc(udc);
}
return 0;
}
static const struct dev_pm_ops udc_plat_pm_ops = {
.suspend = udc_plat_suspend,
.resume = udc_plat_resume,
};
#endif
#if defined(CONFIG_OF)
static const struct of_device_id of_udc_match[] = {
{ .compatible = "brcm,ns2-udc", },
{ .compatible = "brcm,cygnus-udc", },
{ .compatible = "brcm,iproc-udc", },
{ }
};
MODULE_DEVICE_TABLE(of, of_udc_match);
#endif
static struct platform_driver udc_plat_driver = {
.probe = udc_plat_probe,
.remove = udc_plat_remove,
.driver = {
.name = "snps-udc-plat",
.of_match_table = of_match_ptr(of_udc_match),
#ifdef CONFIG_PM_SLEEP
.pm = &udc_plat_pm_ops,
#endif
},
};
module_platform_driver(udc_plat_driver);
MODULE_DESCRIPTION(UDC_MOD_DESCRIPTION);
MODULE_AUTHOR("Broadcom");
MODULE_LICENSE("GPL v2");