| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Tahvo USB transceiver driver | |
| * | |
| * Copyright (C) 2005-2006 Nokia Corporation | |
| * | |
| * Parts copied from isp1301_omap.c. | |
| * Copyright (C) 2004 Texas Instruments | |
| * Copyright (C) 2004 David Brownell | |
| * | |
| * Original driver written by Juha Yrjölä, Tony Lindgren and Timo Teräs. | |
| * Modified for Retu/Tahvo MFD by Aaro Koskinen. | |
| */ | |
| #include <linux/io.h> | |
| #include <linux/clk.h> | |
| #include <linux/usb.h> | |
| #include <linux/extcon-provider.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/string_choices.h> | |
| #include <linux/usb/otg.h> | |
| #include <linux/mfd/retu.h> | |
| #include <linux/usb/gadget.h> | |
| #include <linux/platform_device.h> | |
| #define DRIVER_NAME "tahvo-usb" | |
| #define TAHVO_REG_IDSR 0x02 | |
| #define TAHVO_REG_USBR 0x06 | |
| #define USBR_SLAVE_CONTROL (1 << 8) | |
| #define USBR_VPPVIO_SW (1 << 7) | |
| #define USBR_SPEED (1 << 6) | |
| #define USBR_REGOUT (1 << 5) | |
| #define USBR_MASTER_SW2 (1 << 4) | |
| #define USBR_MASTER_SW1 (1 << 3) | |
| #define USBR_SLAVE_SW (1 << 2) | |
| #define USBR_NSUSPEND (1 << 1) | |
| #define USBR_SEMODE (1 << 0) | |
| #define TAHVO_MODE_HOST 0 | |
| #define TAHVO_MODE_PERIPHERAL 1 | |
| struct tahvo_usb { | |
| struct platform_device *pt_dev; | |
| struct usb_phy phy; | |
| int vbus_state; | |
| struct mutex serialize; | |
| struct clk *ick; | |
| int irq; | |
| int tahvo_mode; | |
| struct extcon_dev *extcon; | |
| }; | |
| static const unsigned int tahvo_cable[] = { | |
| EXTCON_USB, | |
| EXTCON_USB_HOST, | |
| EXTCON_NONE, | |
| }; | |
| static ssize_t vbus_show(struct device *device, | |
| struct device_attribute *attr, char *buf) | |
| { | |
| struct tahvo_usb *tu = dev_get_drvdata(device); | |
| return sprintf(buf, "%s\n", str_on_off(tu->vbus_state)); | |
| } | |
| static DEVICE_ATTR_RO(vbus); | |
| static void check_vbus_state(struct tahvo_usb *tu) | |
| { | |
| struct retu_dev *rdev = dev_get_drvdata(tu->pt_dev->dev.parent); | |
| int reg, prev_state; | |
| reg = retu_read(rdev, TAHVO_REG_IDSR); | |
| if (reg & TAHVO_STAT_VBUS) { | |
| switch (tu->phy.otg->state) { | |
| case OTG_STATE_B_IDLE: | |
| /* Enable the gadget driver */ | |
| if (tu->phy.otg->gadget) | |
| usb_gadget_vbus_connect(tu->phy.otg->gadget); | |
| tu->phy.otg->state = OTG_STATE_B_PERIPHERAL; | |
| usb_phy_set_event(&tu->phy, USB_EVENT_ENUMERATED); | |
| break; | |
| case OTG_STATE_A_IDLE: | |
| /* | |
| * Session is now valid assuming the USB hub is driving | |
| * Vbus. | |
| */ | |
| tu->phy.otg->state = OTG_STATE_A_HOST; | |
| break; | |
| default: | |
| break; | |
| } | |
| dev_info(&tu->pt_dev->dev, "USB cable connected\n"); | |
| } else { | |
| switch (tu->phy.otg->state) { | |
| case OTG_STATE_B_PERIPHERAL: | |
| if (tu->phy.otg->gadget) | |
| usb_gadget_vbus_disconnect(tu->phy.otg->gadget); | |
| tu->phy.otg->state = OTG_STATE_B_IDLE; | |
| usb_phy_set_event(&tu->phy, USB_EVENT_NONE); | |
| break; | |
| case OTG_STATE_A_HOST: | |
| tu->phy.otg->state = OTG_STATE_A_IDLE; | |
| break; | |
| default: | |
| break; | |
| } | |
| dev_info(&tu->pt_dev->dev, "USB cable disconnected\n"); | |
| } | |
| prev_state = tu->vbus_state; | |
| tu->vbus_state = reg & TAHVO_STAT_VBUS; | |
| if (prev_state != tu->vbus_state) { | |
| extcon_set_state_sync(tu->extcon, EXTCON_USB, tu->vbus_state); | |
| sysfs_notify(&tu->pt_dev->dev.kobj, NULL, "vbus_state"); | |
| } | |
| } | |
| static void tahvo_usb_become_host(struct tahvo_usb *tu) | |
| { | |
| struct retu_dev *rdev = dev_get_drvdata(tu->pt_dev->dev.parent); | |
| extcon_set_state_sync(tu->extcon, EXTCON_USB_HOST, true); | |
| /* Power up the transceiver in USB host mode */ | |
| retu_write(rdev, TAHVO_REG_USBR, USBR_REGOUT | USBR_NSUSPEND | | |
| USBR_MASTER_SW2 | USBR_MASTER_SW1); | |
| tu->phy.otg->state = OTG_STATE_A_IDLE; | |
| check_vbus_state(tu); | |
| } | |
| static void tahvo_usb_stop_host(struct tahvo_usb *tu) | |
| { | |
| tu->phy.otg->state = OTG_STATE_A_IDLE; | |
| } | |
| static void tahvo_usb_become_peripheral(struct tahvo_usb *tu) | |
| { | |
| struct retu_dev *rdev = dev_get_drvdata(tu->pt_dev->dev.parent); | |
| extcon_set_state_sync(tu->extcon, EXTCON_USB_HOST, false); | |
| /* Power up transceiver and set it in USB peripheral mode */ | |
| retu_write(rdev, TAHVO_REG_USBR, USBR_SLAVE_CONTROL | USBR_REGOUT | | |
| USBR_NSUSPEND | USBR_SLAVE_SW); | |
| tu->phy.otg->state = OTG_STATE_B_IDLE; | |
| check_vbus_state(tu); | |
| } | |
| static void tahvo_usb_stop_peripheral(struct tahvo_usb *tu) | |
| { | |
| if (tu->phy.otg->gadget) | |
| usb_gadget_vbus_disconnect(tu->phy.otg->gadget); | |
| tu->phy.otg->state = OTG_STATE_B_IDLE; | |
| } | |
| static void tahvo_usb_power_off(struct tahvo_usb *tu) | |
| { | |
| struct retu_dev *rdev = dev_get_drvdata(tu->pt_dev->dev.parent); | |
| /* Disable gadget controller if any */ | |
| if (tu->phy.otg->gadget) | |
| usb_gadget_vbus_disconnect(tu->phy.otg->gadget); | |
| /* Power off transceiver */ | |
| retu_write(rdev, TAHVO_REG_USBR, 0); | |
| tu->phy.otg->state = OTG_STATE_UNDEFINED; | |
| } | |
| static int tahvo_usb_set_suspend(struct usb_phy *dev, int suspend) | |
| { | |
| struct tahvo_usb *tu = container_of(dev, struct tahvo_usb, phy); | |
| struct retu_dev *rdev = dev_get_drvdata(tu->pt_dev->dev.parent); | |
| u16 w; | |
| dev_dbg(&tu->pt_dev->dev, "%s\n", __func__); | |
| w = retu_read(rdev, TAHVO_REG_USBR); | |
| if (suspend) | |
| w &= ~USBR_NSUSPEND; | |
| else | |
| w |= USBR_NSUSPEND; | |
| retu_write(rdev, TAHVO_REG_USBR, w); | |
| return 0; | |
| } | |
| static int tahvo_usb_set_host(struct usb_otg *otg, struct usb_bus *host) | |
| { | |
| struct tahvo_usb *tu = container_of(otg->usb_phy, struct tahvo_usb, | |
| phy); | |
| mutex_lock(&tu->serialize); | |
| if (host == NULL) { | |
| if (tu->tahvo_mode == TAHVO_MODE_HOST) | |
| tahvo_usb_power_off(tu); | |
| otg->host = NULL; | |
| mutex_unlock(&tu->serialize); | |
| return 0; | |
| } | |
| if (tu->tahvo_mode == TAHVO_MODE_HOST) { | |
| otg->host = NULL; | |
| tahvo_usb_become_host(tu); | |
| } | |
| otg->host = host; | |
| mutex_unlock(&tu->serialize); | |
| return 0; | |
| } | |
| static int tahvo_usb_set_peripheral(struct usb_otg *otg, | |
| struct usb_gadget *gadget) | |
| { | |
| struct tahvo_usb *tu = container_of(otg->usb_phy, struct tahvo_usb, | |
| phy); | |
| mutex_lock(&tu->serialize); | |
| if (!gadget) { | |
| if (tu->tahvo_mode == TAHVO_MODE_PERIPHERAL) | |
| tahvo_usb_power_off(tu); | |
| tu->phy.otg->gadget = NULL; | |
| mutex_unlock(&tu->serialize); | |
| return 0; | |
| } | |
| tu->phy.otg->gadget = gadget; | |
| if (tu->tahvo_mode == TAHVO_MODE_PERIPHERAL) | |
| tahvo_usb_become_peripheral(tu); | |
| mutex_unlock(&tu->serialize); | |
| return 0; | |
| } | |
| static irqreturn_t tahvo_usb_vbus_interrupt(int irq, void *_tu) | |
| { | |
| struct tahvo_usb *tu = _tu; | |
| mutex_lock(&tu->serialize); | |
| check_vbus_state(tu); | |
| mutex_unlock(&tu->serialize); | |
| return IRQ_HANDLED; | |
| } | |
| static ssize_t otg_mode_show(struct device *device, | |
| struct device_attribute *attr, char *buf) | |
| { | |
| struct tahvo_usb *tu = dev_get_drvdata(device); | |
| switch (tu->tahvo_mode) { | |
| case TAHVO_MODE_HOST: | |
| return sprintf(buf, "host\n"); | |
| case TAHVO_MODE_PERIPHERAL: | |
| return sprintf(buf, "peripheral\n"); | |
| } | |
| return -EINVAL; | |
| } | |
| static ssize_t otg_mode_store(struct device *device, | |
| struct device_attribute *attr, | |
| const char *buf, size_t count) | |
| { | |
| struct tahvo_usb *tu = dev_get_drvdata(device); | |
| int r; | |
| mutex_lock(&tu->serialize); | |
| if (count >= 4 && strncmp(buf, "host", 4) == 0) { | |
| if (tu->tahvo_mode == TAHVO_MODE_PERIPHERAL) | |
| tahvo_usb_stop_peripheral(tu); | |
| tu->tahvo_mode = TAHVO_MODE_HOST; | |
| if (tu->phy.otg->host) { | |
| dev_info(device, "HOST mode: host controller present\n"); | |
| tahvo_usb_become_host(tu); | |
| } else { | |
| dev_info(device, "HOST mode: no host controller, powering off\n"); | |
| tahvo_usb_power_off(tu); | |
| } | |
| r = strlen(buf); | |
| } else if (count >= 10 && strncmp(buf, "peripheral", 10) == 0) { | |
| if (tu->tahvo_mode == TAHVO_MODE_HOST) | |
| tahvo_usb_stop_host(tu); | |
| tu->tahvo_mode = TAHVO_MODE_PERIPHERAL; | |
| if (tu->phy.otg->gadget) { | |
| dev_info(device, "PERIPHERAL mode: gadget driver present\n"); | |
| tahvo_usb_become_peripheral(tu); | |
| } else { | |
| dev_info(device, "PERIPHERAL mode: no gadget driver, powering off\n"); | |
| tahvo_usb_power_off(tu); | |
| } | |
| r = strlen(buf); | |
| } else { | |
| r = -EINVAL; | |
| } | |
| mutex_unlock(&tu->serialize); | |
| return r; | |
| } | |
| static DEVICE_ATTR_RW(otg_mode); | |
| static struct attribute *tahvo_attrs[] = { | |
| &dev_attr_vbus.attr, | |
| &dev_attr_otg_mode.attr, | |
| NULL | |
| }; | |
| ATTRIBUTE_GROUPS(tahvo); | |
| static int tahvo_usb_probe(struct platform_device *pdev) | |
| { | |
| struct retu_dev *rdev = dev_get_drvdata(pdev->dev.parent); | |
| struct tahvo_usb *tu; | |
| int ret; | |
| tu = devm_kzalloc(&pdev->dev, sizeof(*tu), GFP_KERNEL); | |
| if (!tu) | |
| return -ENOMEM; | |
| tu->phy.otg = devm_kzalloc(&pdev->dev, sizeof(*tu->phy.otg), | |
| GFP_KERNEL); | |
| if (!tu->phy.otg) | |
| return -ENOMEM; | |
| tu->pt_dev = pdev; | |
| /* Default mode */ | |
| #ifdef CONFIG_TAHVO_USB_HOST_BY_DEFAULT | |
| tu->tahvo_mode = TAHVO_MODE_HOST; | |
| #else | |
| tu->tahvo_mode = TAHVO_MODE_PERIPHERAL; | |
| #endif | |
| mutex_init(&tu->serialize); | |
| tu->ick = devm_clk_get(&pdev->dev, "usb_l4_ick"); | |
| if (!IS_ERR(tu->ick)) | |
| clk_enable(tu->ick); | |
| /* | |
| * Set initial state, so that we generate kevents only on state changes. | |
| */ | |
| tu->vbus_state = retu_read(rdev, TAHVO_REG_IDSR) & TAHVO_STAT_VBUS; | |
| tu->extcon = devm_extcon_dev_allocate(&pdev->dev, tahvo_cable); | |
| if (IS_ERR(tu->extcon)) { | |
| dev_err(&pdev->dev, "failed to allocate memory for extcon\n"); | |
| ret = PTR_ERR(tu->extcon); | |
| goto err_disable_clk; | |
| } | |
| ret = devm_extcon_dev_register(&pdev->dev, tu->extcon); | |
| if (ret) { | |
| dev_err(&pdev->dev, "could not register extcon device: %d\n", | |
| ret); | |
| goto err_disable_clk; | |
| } | |
| /* Set the initial cable state. */ | |
| extcon_set_state_sync(tu->extcon, EXTCON_USB_HOST, | |
| tu->tahvo_mode == TAHVO_MODE_HOST); | |
| extcon_set_state_sync(tu->extcon, EXTCON_USB, tu->vbus_state); | |
| /* Create OTG interface */ | |
| tahvo_usb_power_off(tu); | |
| tu->phy.dev = &pdev->dev; | |
| tu->phy.otg->state = OTG_STATE_UNDEFINED; | |
| tu->phy.label = DRIVER_NAME; | |
| tu->phy.set_suspend = tahvo_usb_set_suspend; | |
| tu->phy.otg->usb_phy = &tu->phy; | |
| tu->phy.otg->set_host = tahvo_usb_set_host; | |
| tu->phy.otg->set_peripheral = tahvo_usb_set_peripheral; | |
| ret = usb_add_phy(&tu->phy, USB_PHY_TYPE_USB2); | |
| if (ret < 0) { | |
| dev_err(&pdev->dev, "cannot register USB transceiver: %d\n", | |
| ret); | |
| goto err_disable_clk; | |
| } | |
| dev_set_drvdata(&pdev->dev, tu); | |
| tu->irq = ret = platform_get_irq(pdev, 0); | |
| if (ret < 0) | |
| goto err_remove_phy; | |
| ret = request_threaded_irq(tu->irq, NULL, tahvo_usb_vbus_interrupt, | |
| IRQF_ONESHOT, | |
| "tahvo-vbus", tu); | |
| if (ret) { | |
| dev_err(&pdev->dev, "could not register tahvo-vbus irq: %d\n", | |
| ret); | |
| goto err_remove_phy; | |
| } | |
| return 0; | |
| err_remove_phy: | |
| usb_remove_phy(&tu->phy); | |
| err_disable_clk: | |
| if (!IS_ERR(tu->ick)) | |
| clk_disable(tu->ick); | |
| return ret; | |
| } | |
| static void tahvo_usb_remove(struct platform_device *pdev) | |
| { | |
| struct tahvo_usb *tu = platform_get_drvdata(pdev); | |
| free_irq(tu->irq, tu); | |
| usb_remove_phy(&tu->phy); | |
| if (!IS_ERR(tu->ick)) | |
| clk_disable(tu->ick); | |
| } | |
| static struct platform_driver tahvo_usb_driver = { | |
| .probe = tahvo_usb_probe, | |
| .remove = tahvo_usb_remove, | |
| .driver = { | |
| .name = "tahvo-usb", | |
| .dev_groups = tahvo_groups, | |
| }, | |
| }; | |
| module_platform_driver(tahvo_usb_driver); | |
| MODULE_DESCRIPTION("Tahvo USB transceiver driver"); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Juha Yrjölä, Tony Lindgren, and Timo Teräs"); | |
| MODULE_AUTHOR("Aaro Koskinen <aaro.koskinen@iki.fi>"); |