diff options
author | codeworkx <daniel.hillenbrand@codeworkx.de> | 2012-06-02 13:09:29 +0200 |
---|---|---|
committer | codeworkx <daniel.hillenbrand@codeworkx.de> | 2012-06-02 13:09:29 +0200 |
commit | c6da2cfeb05178a11c6d062a06f8078150ee492f (patch) | |
tree | f3b4021d252c52d6463a9b3c1bb7245e399b009c /drivers/usb/misc | |
parent | c6d7c4dbff353eac7919342ae6b3299a378160a6 (diff) | |
download | kernel_samsung_smdk4412-c6da2cfeb05178a11c6d062a06f8078150ee492f.zip kernel_samsung_smdk4412-c6da2cfeb05178a11c6d062a06f8078150ee492f.tar.gz kernel_samsung_smdk4412-c6da2cfeb05178a11c6d062a06f8078150ee492f.tar.bz2 |
samsung update 1
Diffstat (limited to 'drivers/usb/misc')
-rw-r--r-- | drivers/usb/misc/Kconfig | 42 | ||||
-rw-r--r-- | drivers/usb/misc/Makefile | 6 | ||||
-rw-r--r-- | drivers/usb/misc/diag_bridge.c | 485 | ||||
-rw-r--r-- | drivers/usb/misc/diag_bridge_test.c | 207 | ||||
-rw-r--r-- | drivers/usb/misc/exynos-usb-switch.c | 642 | ||||
-rw-r--r-- | drivers/usb/misc/exynos-usb-switch.h | 63 | ||||
-rw-r--r-- | drivers/usb/misc/mdm_ctrl_bridge.c | 768 | ||||
-rw-r--r-- | drivers/usb/misc/mdm_data_bridge.c | 1079 |
8 files changed, 3292 insertions, 0 deletions
diff --git a/drivers/usb/misc/Kconfig b/drivers/usb/misc/Kconfig index 1bfcd02..b2a96a5 100644 --- a/drivers/usb/misc/Kconfig +++ b/drivers/usb/misc/Kconfig @@ -244,3 +244,45 @@ config USB_YUREX To compile this driver as a module, choose M here: the module will be called yurex. +config USB_EXYNOS_SWITCH + bool "EXYNOS USB SWITCH driver" + depends on USB_EHCI_S5P || USB_OHCI_S5P || USB_XHCI_EXYNOS + depends on USB_SUSPEND + depends on S5P_DEV_USB_SWITCH + help + This driver is for switching host & device controller software. + +config USB_QCOM_DIAG_BRIDGE + tristate "USB Qualcomm diagnostic bridge driver" + depends on USB + help + Say Y here if you have a Qualcomm modem device connected via USB that + will be bridged in kernel space. This driver communicates with the + diagnostic interface and allows for bridging with the diag forwarding + driver. + + To compile this driver as a module, choose M here: the + module will be called diag_bridge. If unsure, choose N. + +config USB_QCOM_DIAG_BRIDGE_TEST + tristate "USB Qualcomm diagnostic bridge driver test" + depends on USB && USB_QCOM_DIAG_BRIDGE + help + Say Y here if you want to enable the test hook for the + Qualcomm diag bridge driver. When enabled, this will create + a debugfs file entry named "diag_bridge_test" which can be used + to send a ping command to the diag port of the modem over USB. + + To compile this driver as a module, choose M here: the + module will be called diag_bridge_test. If unsure, choose N. + +config USB_QCOM_MDM_BRIDGE + tristate "USB Qualcomm modem bridge driver for DUN and RMNET" + depends on USB + help + Say Y here if you have a Qualcomm modem device connected via USB that + will be bridged in kernel space. This driver works as a bridge to pass + control and data packets between the modem and peripheral usb gadget + driver for dial up network and RMNET. + To compile this driver as a module, choose M here: the module + will be called mdm_bridge. If unsure, choose N. diff --git a/drivers/usb/misc/Makefile b/drivers/usb/misc/Makefile index 796ce7e..1a00ac5 100644 --- a/drivers/usb/misc/Makefile +++ b/drivers/usb/misc/Makefile @@ -25,5 +25,11 @@ obj-$(CONFIG_USB_TRANCEVIBRATOR) += trancevibrator.o obj-$(CONFIG_USB_USS720) += uss720.o obj-$(CONFIG_USB_SEVSEG) += usbsevseg.o obj-$(CONFIG_USB_YUREX) += yurex.o +obj-$(CONFIG_USB_EXYNOS_SWITCH) += exynos-usb-switch.o obj-$(CONFIG_USB_SISUSBVGA) += sisusbvga/ + +obj-$(CONFIG_USB_QCOM_DIAG_BRIDGE) += diag_bridge.o +obj-$(CONFIG_USB_QCOM_DIAG_BRIDGE_TEST) += diag_bridge_test.o +mdm_bridge-y := mdm_ctrl_bridge.o mdm_data_bridge.o +obj-$(CONFIG_USB_QCOM_MDM_BRIDGE) += mdm_bridge.o diff --git a/drivers/usb/misc/diag_bridge.c b/drivers/usb/misc/diag_bridge.c new file mode 100644 index 0000000..9794918 --- /dev/null +++ b/drivers/usb/misc/diag_bridge.c @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2011-2012, Code Aurora Forum. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/kernel.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/kref.h> +#include <linux/platform_device.h> +#include <linux/uaccess.h> +#include <linux/usb.h> +#include <linux/debugfs.h> +#include <mach/diag_bridge.h> + +#define DRIVER_DESC "USB host diag bridge driver" +#define DRIVER_VERSION "1.0" + +struct diag_bridge { + struct usb_device *udev; + struct usb_interface *ifc; + struct usb_anchor submitted; + __u8 in_epAddr; + __u8 out_epAddr; + int err; + struct kref kref; + struct diag_bridge_ops *ops; + struct platform_device *pdev; + + /* debugging counters */ + unsigned long bytes_to_host; + unsigned long bytes_to_mdm; + unsigned pending_reads; + unsigned pending_writes; +}; +struct diag_bridge *__dev; + +int diag_bridge_open(struct diag_bridge_ops *ops) +{ + struct diag_bridge *dev = __dev; + + if (!dev) { + err("dev is null"); + return -ENODEV; + } + + dev->ops = ops; + dev->err = 0; + + return 0; +} +EXPORT_SYMBOL(diag_bridge_open); + +void diag_bridge_close(void) +{ + struct diag_bridge *dev = __dev; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + usb_kill_anchored_urbs(&dev->submitted); + + dev->ops = 0; +} +EXPORT_SYMBOL(diag_bridge_close); + +static void diag_bridge_read_cb(struct urb *urb) +{ + struct diag_bridge *dev = urb->context; + struct diag_bridge_ops *cbs = dev->ops; + + dev_dbg(&dev->udev->dev, "%s: status:%d actual:%d\n", __func__, + urb->status, urb->actual_length); + + if (urb->status == -EPROTO) { + dev_err(&dev->udev->dev, "%s: proto error\n", __func__); + /* save error so that subsequent read/write returns ESHUTDOWN */ + dev->err = urb->status; + return; + } + + cbs->read_complete_cb(cbs->ctxt, + urb->transfer_buffer, + urb->transfer_buffer_length, + urb->status < 0 ? urb->status : urb->actual_length); + + dev->bytes_to_host += urb->actual_length; + dev->pending_reads--; +} + +int diag_bridge_read(char *data, int size) +{ + struct urb *urb = NULL; + unsigned int pipe; + struct diag_bridge *dev = __dev; + int ret; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + if (!size) { + dev_err(&dev->udev->dev, "invalid size:%d\n", size); + return -EINVAL; + } + + if (!dev->ifc) { + dev_err(&dev->udev->dev, "device is disconnected\n"); + return -ENODEV; + } + + /* if there was a previous unrecoverable error, just quit */ + if (dev->err) + return -ESHUTDOWN; + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + dev_err(&dev->udev->dev, "unable to allocate urb\n"); + return -ENOMEM; + } + + ret = usb_autopm_get_interface(dev->ifc); + if (ret < 0) { + dev_err(&dev->udev->dev, "autopm_get failed:%d\n", ret); + usb_free_urb(urb); + return ret; + } + + pipe = usb_rcvbulkpipe(dev->udev, dev->in_epAddr); + usb_fill_bulk_urb(urb, dev->udev, pipe, data, size, + diag_bridge_read_cb, dev); + usb_anchor_urb(urb, &dev->submitted); + dev->pending_reads++; + + ret = usb_submit_urb(urb, GFP_KERNEL); + if (ret) { + dev_err(&dev->udev->dev, "submitting urb failed err:%d\n", ret); + dev->pending_reads--; + usb_unanchor_urb(urb); + usb_free_urb(urb); + usb_autopm_put_interface(dev->ifc); + return ret; + } + + usb_autopm_put_interface(dev->ifc); + usb_free_urb(urb); + + return 0; +} +EXPORT_SYMBOL(diag_bridge_read); + +static void diag_bridge_write_cb(struct urb *urb) +{ + struct diag_bridge *dev = urb->context; + struct diag_bridge_ops *cbs = dev->ops; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + usb_autopm_put_interface_async(dev->ifc); + + if (urb->status == -EPROTO) { + dev_err(&dev->udev->dev, "%s: proto error\n", __func__); + /* save error so that subsequent read/write returns ESHUTDOWN */ + dev->err = urb->status; + return; + } + + cbs->write_complete_cb(cbs->ctxt, + urb->transfer_buffer, + urb->transfer_buffer_length, + urb->status < 0 ? urb->status : urb->actual_length); + + dev->bytes_to_mdm += urb->actual_length; + dev->pending_writes--; +} + +int diag_bridge_write(char *data, int size) +{ + struct urb *urb = NULL; + unsigned int pipe; + struct diag_bridge *dev = __dev; + int ret; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + if (!size) { + dev_err(&dev->udev->dev, "invalid size:%d\n", size); + return -EINVAL; + } + + if (!dev->ifc) { + dev_err(&dev->udev->dev, "device is disconnected\n"); + return -ENODEV; + } + + /* if there was a previous unrecoverable error, just quit */ + if (dev->err) + return -ESHUTDOWN; + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + err("unable to allocate urb"); + return -ENOMEM; + } + + ret = usb_autopm_get_interface(dev->ifc); + if (ret < 0) { + dev_err(&dev->udev->dev, "autopm_get failed:%d\n", ret); + usb_free_urb(urb); + return ret; + } + + pipe = usb_sndbulkpipe(dev->udev, dev->out_epAddr); + usb_fill_bulk_urb(urb, dev->udev, pipe, data, size, + diag_bridge_write_cb, dev); + usb_anchor_urb(urb, &dev->submitted); + dev->pending_writes++; + + ret = usb_submit_urb(urb, GFP_KERNEL); + if (ret) { + dev_err(&dev->udev->dev, "submitting urb failed err:%d\n", ret); + dev->pending_writes--; + usb_unanchor_urb(urb); + usb_free_urb(urb); + usb_autopm_put_interface(dev->ifc); + return ret; + } + + usb_free_urb(urb); + + return 0; +} +EXPORT_SYMBOL(diag_bridge_write); + +static void diag_bridge_delete(struct kref *kref) +{ + struct diag_bridge *dev = + container_of(kref, struct diag_bridge, kref); + + usb_put_dev(dev->udev); + __dev = 0; + kfree(dev); +} + +#if defined(CONFIG_DEBUG_FS) +#define DEBUG_BUF_SIZE 512 +static ssize_t diag_read_stats(struct file *file, char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct diag_bridge *dev = __dev; + char *buf; + int ret; + + buf = kzalloc(sizeof(char) * DEBUG_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + ret = scnprintf(buf, DEBUG_BUF_SIZE, + "epin:%d, epout:%d\n" + "bytes to host: %lu\n" + "bytes to mdm: %lu\n" + "pending reads: %u\n" + "pending writes: %u\n" + "last error: %d\n", + dev->in_epAddr, dev->out_epAddr, + dev->bytes_to_host, dev->bytes_to_mdm, + dev->pending_reads, dev->pending_writes, + dev->err); + + ret = simple_read_from_buffer(ubuf, count, ppos, buf, ret); + kfree(buf); + return ret; +} + +static ssize_t diag_reset_stats(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + struct diag_bridge *dev = __dev; + + dev->bytes_to_host = dev->bytes_to_mdm = 0; + dev->pending_reads = dev->pending_writes = 0; + + return count; +} + +const struct file_operations diag_stats_ops = { + .read = diag_read_stats, + .write = diag_reset_stats, +}; + +static struct dentry *dent; + +static void diag_bridge_debugfs_init(void) +{ + struct dentry *dfile; + + dent = debugfs_create_dir("diag_bridge", 0); + if (IS_ERR(dent)) + return; + + dfile = debugfs_create_file("status", 0444, dent, 0, &diag_stats_ops); + if (!dfile || IS_ERR(dfile)) + debugfs_remove(dent); +} + +static void diag_bridge_debugfs_cleanup(void) +{ + if (dent) { + debugfs_remove_recursive(dent); + dent = NULL; + } +} +#else +static inline void diag_bridge_debugfs_init(void) { } +static inline void diag_bridge_debugfs_cleanup(void) { } +#endif + +static int +diag_bridge_probe(struct usb_interface *ifc, const struct usb_device_id *id) +{ + struct diag_bridge *dev; + struct usb_host_interface *ifc_desc; + struct usb_endpoint_descriptor *ep_desc; + int i; + int ret = -ENOMEM; + __u8 ifc_num; + + dbg("%s: id:%lu", __func__, id->driver_info); + + ifc_num = ifc->cur_altsetting->desc.bInterfaceNumber; + + /* is this interface supported ? */ + if (ifc_num != id->driver_info) + return -ENODEV; + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) { + pr_err("%s: unable to allocate dev\n", __func__); + return -ENOMEM; + } + dev->pdev = platform_device_alloc("diag_bridge", -1); + if (!dev->pdev) { + pr_err("%s: unable to allocate platform device\n", __func__); + kfree(dev); + return -ENOMEM; + } + __dev = dev; + + dev->udev = usb_get_dev(interface_to_usbdev(ifc)); + dev->ifc = ifc; + kref_init(&dev->kref); + init_usb_anchor(&dev->submitted); + + ifc_desc = ifc->cur_altsetting; + for (i = 0; i < ifc_desc->desc.bNumEndpoints; i++) { + ep_desc = &ifc_desc->endpoint[i].desc; + + if (!dev->in_epAddr && usb_endpoint_is_bulk_in(ep_desc)) + dev->in_epAddr = ep_desc->bEndpointAddress; + + if (!dev->out_epAddr && usb_endpoint_is_bulk_out(ep_desc)) + dev->out_epAddr = ep_desc->bEndpointAddress; + } + + if (!(dev->in_epAddr && dev->out_epAddr)) { + err("could not find bulk in and bulk out endpoints"); + ret = -ENODEV; + goto error; + } + + usb_set_intfdata(ifc, dev); + diag_bridge_debugfs_init(); + platform_device_add(dev->pdev); + + dev_dbg(&dev->udev->dev, "%s: complete\n", __func__); + + return 0; + +error: + if (dev) + kref_put(&dev->kref, diag_bridge_delete); + + return ret; +} + +static void diag_bridge_disconnect(struct usb_interface *ifc) +{ + struct diag_bridge *dev = usb_get_intfdata(ifc); + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + platform_device_del(dev->pdev); + diag_bridge_debugfs_cleanup(); + kref_put(&dev->kref, diag_bridge_delete); + usb_set_intfdata(ifc, NULL); +} + +static int diag_bridge_suspend(struct usb_interface *ifc, pm_message_t message) +{ + struct diag_bridge *dev = usb_get_intfdata(ifc); + struct diag_bridge_ops *cbs = dev->ops; + int ret = 0; + + if (cbs && cbs->suspend) { + ret = cbs->suspend(cbs->ctxt); + if (ret) { + dev_dbg(&dev->udev->dev, + "%s: diag veto'd suspend\n", __func__); + return ret; + } + + usb_kill_anchored_urbs(&dev->submitted); + } + + return ret; +} + +static int diag_bridge_resume(struct usb_interface *ifc) +{ + struct diag_bridge *dev = usb_get_intfdata(ifc); + struct diag_bridge_ops *cbs = dev->ops; + + + if (cbs && cbs->resume) + cbs->resume(cbs->ctxt); + + return 0; +} + +#define VALID_INTERFACE_NUM 0 +static const struct usb_device_id diag_bridge_ids[] = { + { USB_DEVICE(0x5c6, 0x9001), + .driver_info = VALID_INTERFACE_NUM, }, + { USB_DEVICE(0x5c6, 0x9034), + .driver_info = VALID_INTERFACE_NUM, }, + { USB_DEVICE(0x5c6, 0x9048), + .driver_info = VALID_INTERFACE_NUM, }, + { USB_DEVICE(0x5c6, 0x904C), + .driver_info = VALID_INTERFACE_NUM, }, + + {} /* terminating entry */ +}; +MODULE_DEVICE_TABLE(usb, diag_bridge_ids); + +static struct usb_driver diag_bridge_driver = { + .name = "diag_bridge", + .probe = diag_bridge_probe, + .disconnect = diag_bridge_disconnect, + .suspend = diag_bridge_suspend, + .resume = diag_bridge_resume, + .id_table = diag_bridge_ids, + .supports_autosuspend = 1, +}; + +static int __init diag_bridge_init(void) +{ + int ret; + + ret = usb_register(&diag_bridge_driver); + if (ret) { + err("%s: unable to register diag driver", __func__); + return ret; + } + + return 0; +} + +static void __exit diag_bridge_exit(void) +{ + usb_deregister(&diag_bridge_driver); +} + +module_init(diag_bridge_init); +module_exit(diag_bridge_exit); + +MODULE_DESCRIPTION(DRIVER_DESC); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/misc/diag_bridge_test.c b/drivers/usb/misc/diag_bridge_test.c new file mode 100644 index 0000000..5bc0828 --- /dev/null +++ b/drivers/usb/misc/diag_bridge_test.c @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2011, Code Aurora Forum. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/slab.h> +#include <linux/kernel.h> +#include <linux/device.h> +#include <linux/platform_device.h> +#include <linux/debugfs.h> +#include <linux/crc-ccitt.h> +#include <mach/diag_bridge.h> + +#define DRIVER_DESC "USB host diag bridge driver test" +#define DRIVER_VERSION "1.0" + +#define RD_BUF_SIZE 2048 +#define DIAG_TEST_CONNECTED 0 + +struct diag_test_dev { + char *read_buf; + struct work_struct read_w; + unsigned long flags; + + struct diag_bridge_ops ops; +}; +static struct diag_test_dev *__dev; +static struct dentry *dent; + +static void +diag_test_read_complete_cb(void *d, char *buf, size_t size, size_t actual) +{ + if (actual < 0) { + pr_err("%s: read complete err\n", __func__); + return; + } + + print_hex_dump(KERN_INFO, "to_host:", 0, 1, 1, buf, actual, false); +} +static void diag_test_read_work(struct work_struct *w) +{ + struct diag_test_dev *dev = + container_of(w, struct diag_test_dev, read_w); + + memset(dev->read_buf, 0, RD_BUF_SIZE); + diag_bridge_read(dev->read_buf, RD_BUF_SIZE); +} + +static void +diag_test_write_complete_cb(void *d, char *buf, size_t size, size_t actual) +{ + struct diag_test_dev *dev = d; + + if (actual > 0) + schedule_work(&dev->read_w); +} + +#if defined(CONFIG_DEBUG_FS) +#define DEBUG_BUF_SIZE 1024 +static ssize_t send_ping_cmd(struct file *file, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct diag_test_dev *dev = __dev; + unsigned char *buf; + int temp = sizeof(unsigned char) * 4; + + if (!dev) + return -ENODEV; + + buf = kmalloc(temp, GFP_KERNEL); + if (!buf) { + pr_err("%s: unable to allocate mem for ping cmd\n", + __func__); + return -ENOMEM; + } + + /* hdlc encoded ping command */ + buf[0] = 0x0C; + buf[1] = 0x14; + buf[2] = 0x3A; + buf[3] = 0x7E; + + diag_bridge_write(buf, temp); + + return count; +} + +const struct file_operations diag_test_ping_ops = { + .write = send_ping_cmd, +}; + +static void diag_test_debug_init(void) +{ + struct dentry *dfile; + + dent = debugfs_create_dir("diag_test", 0); + if (IS_ERR(dent)) + return; + + dfile = debugfs_create_file("send_ping", 0444, dent, + 0, &diag_test_ping_ops); + if (!dfile || IS_ERR(dfile)) + debugfs_remove(dent); +} +#else +static void diag_test_debug_init(void) { } +#endif + +static int diag_test_remove(struct platform_device *pdev) +{ + diag_bridge_close(); + + if (dent) { + debugfs_remove_recursive(dent); + dent = NULL; + } + + return 0; +} + +static int diag_test_probe(struct platform_device *pdev) +{ + struct diag_test_dev *dev = __dev; + int ret = 0; + + pr_info("%s:\n", __func__); + + ret = diag_bridge_open(&dev->ops); + if (ret) + pr_err("diag open failed: %d", ret); + + + diag_test_debug_init(); + + return ret; +} + +static struct platform_driver diag_test = { + .remove = diag_test_remove, + .probe = diag_test_probe, + .driver = { + .name = "diag_bridge", + .owner = THIS_MODULE, + }, +}; + +static int __init diag_test_init(void) +{ + struct diag_test_dev *dev; + int ret = 0; + + pr_info("%s\n", __func__); + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + + __dev = dev; + + dev->ops.read_complete_cb = diag_test_read_complete_cb; + dev->ops.write_complete_cb = diag_test_write_complete_cb; + dev->read_buf = kmalloc(RD_BUF_SIZE, GFP_KERNEL); + if (!dev->read_buf) { + pr_err("%s: unable to allocate read buffer\n", __func__); + kfree(dev); + return -ENOMEM; + } + + dev->ops.ctxt = dev; + INIT_WORK(&dev->read_w, diag_test_read_work); + + ret = platform_driver_register(&diag_test); + if (ret) + pr_err("%s: platform driver %s register failed %d\n", + __func__, diag_test.driver.name, ret); + + return ret; +} + +static void __exit diag_test_exit(void) +{ + struct diag_test_dev *dev = __dev; + + pr_info("%s:\n", __func__); + + if (test_bit(DIAG_TEST_CONNECTED, &dev->flags)) + diag_bridge_close(); + + kfree(dev->read_buf); + kfree(dev); + +} + +module_init(diag_test_init); +module_exit(diag_test_exit); + +MODULE_DESCRIPTION(DRIVER_DESC); +MODULE_VERSION(DRIVER_VERSION); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/misc/exynos-usb-switch.c b/drivers/usb/misc/exynos-usb-switch.c new file mode 100644 index 0000000..519c845 --- /dev/null +++ b/drivers/usb/misc/exynos-usb-switch.c @@ -0,0 +1,642 @@ +/* + * exynos-usb-switch.c - USB switch driver for Exynos + * + * Copyright (c) 2010-2011 Samsung Electronics Co., Ltd. + * Yulgon Kim <yulgon.kim@samsung.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. +*/ + +#include <linux/gpio.h> +#include <linux/interrupt.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> + +#include <plat/devs.h> +#include <plat/ehci.h> +#include <plat/usbgadget.h> +#include <plat/usb-switch.h> + +#include <mach/regs-clock.h> + +#include "../gadget/s3c_udc.h" +#include "../gadget/exynos_ss_udc.h" +#include "exynos-usb-switch.h" + +#define DRIVER_DESC "Exynos USB Switch Driver" +#define SWITCH_WAIT_TIME 500 +#define WAIT_TIMES 10 + +static const char switch_name[] = "exynos_usb_switch"; +static struct exynos_usb_switch *our_switch; + +#if defined(CONFIG_BATTERY_SAMSUNG) || defined(CONFIG_BATTERY_SAMSUNG_S2PLUS) +void exynos_usb_cable_connect(void) +{ + samsung_cable_check_status(1); +} + +void exynos_usb_cable_disconnect(void) +{ + samsung_cable_check_status(0); +} +#endif + +static int is_host_detect(struct exynos_usb_switch *usb_switch) +{ + if (!usb_switch->gpio_host_detect) + return 0; + return !gpio_get_value(usb_switch->gpio_host_detect); +} + +static int is_device_detect(struct exynos_usb_switch *usb_switch) +{ + if (!usb_switch->gpio_device_detect) + return 0; + return gpio_get_value(usb_switch->gpio_device_detect); +} + +static int is_drd_host_detect(struct exynos_usb_switch *usb_switch) +{ + if (!usb_switch->gpio_drd_host_detect) + return 0; + return !gpio_get_value(usb_switch->gpio_drd_host_detect); +} + +static int is_drd_device_detect(struct exynos_usb_switch *usb_switch) +{ + if (!usb_switch->gpio_drd_device_detect) + return 0; + return gpio_get_value(usb_switch->gpio_drd_device_detect); +} + +static void set_host_vbus(struct exynos_usb_switch *usb_switch, int value) +{ + gpio_set_value(usb_switch->gpio_host_vbus, value); +} + +static int exynos_change_usb_mode(struct exynos_usb_switch *usb_switch, + enum usb_cable_status mode) +{ + struct s3c_udc *udc; + struct exynos_ss_udc *ss_udc; + unsigned long cur_mode = usb_switch->connect; + int ret = 0; + + if (test_bit(USB_DEVICE_ATTACHED, &cur_mode) || + test_bit(USB_HOST_ATTACHED, &cur_mode)) { + if (mode == USB_DEVICE_ATTACHED || + mode == USB_HOST_ATTACHED) { + printk(KERN_DEBUG "Skip request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + } else if (test_bit(USB_DRD_DEVICE_ATTACHED, &cur_mode) || + test_bit(USB_DRD_HOST_ATTACHED, &cur_mode)) { + if (mode == USB_DRD_DEVICE_ATTACHED || + mode == USB_DRD_HOST_ATTACHED) { + printk(KERN_DEBUG "Skip request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + } + + if (!test_bit(USB_DEVICE_ATTACHED, &cur_mode) && + mode == USB_DEVICE_DETACHED) { + printk(KERN_DEBUG "Skip request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } else if (!test_bit(USB_HOST_ATTACHED, &cur_mode) && + mode == USB_HOST_DETACHED) { + printk(KERN_DEBUG "Skip request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } else if (!test_bit(USB_DRD_DEVICE_ATTACHED, &cur_mode) && + mode == USB_DRD_DEVICE_DETACHED) { + printk(KERN_DEBUG "Skip request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } else if (!test_bit(USB_DRD_HOST_ATTACHED, &cur_mode) && + mode == USB_DRD_HOST_DETACHED) { + printk(KERN_DEBUG "Skip request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + + switch (mode) { + case USB_DEVICE_DETACHED: + if (test_bit(USB_HOST_ATTACHED, &cur_mode)) { + printk(KERN_ERR "Abnormal request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + udc = dev_get_drvdata(usb_switch->s3c_udc_dev); + if (udc && udc->gadget.ops && udc->gadget.ops->vbus_session) + udc->gadget.ops->vbus_session(&udc->gadget, 0); + clear_bit(USB_DEVICE_ATTACHED, &usb_switch->connect); + break; + case USB_DEVICE_ATTACHED: + udc = dev_get_drvdata(usb_switch->s3c_udc_dev); + if (udc && udc->gadget.ops && udc->gadget.ops->vbus_session) + udc->gadget.ops->vbus_session(&udc->gadget, 1); + set_bit(USB_DEVICE_ATTACHED, &usb_switch->connect); + break; + case USB_HOST_DETACHED: + if (test_bit(USB_DEVICE_ATTACHED, &cur_mode)) { + printk(KERN_ERR "Abnormal request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + if (usb_switch->ohci_dev) + pm_runtime_put(usb_switch->ohci_dev); + if (usb_switch->ehci_dev) + pm_runtime_put(usb_switch->ehci_dev); + if (usb_switch->gpio_host_vbus) + set_host_vbus(usb_switch, 0); + +#if defined(CONFIG_BATTERY_SAMSUNG) || defined(CONFIG_BATTERY_SAMSUNG_S2PLUS) + exynos_usb_cable_disconnect(); +#endif + clear_bit(USB_HOST_ATTACHED, &usb_switch->connect); + break; + case USB_HOST_ATTACHED: +#if defined(CONFIG_BATTERY_SAMSUNG) || defined(CONFIG_BATTERY_SAMSUNG_S2PLUS) + exynos_usb_cable_connect(); +#endif + if (usb_switch->gpio_host_vbus) + set_host_vbus(usb_switch, 1); + + if (usb_switch->ehci_dev) + pm_runtime_get_sync(usb_switch->ehci_dev); + if (usb_switch->ohci_dev) + pm_runtime_get_sync(usb_switch->ohci_dev); + set_bit(USB_HOST_ATTACHED, &usb_switch->connect); + break; + case USB_DRD_DEVICE_DETACHED: + if (test_bit(USB_DRD_HOST_ATTACHED, &cur_mode)) { + printk(KERN_ERR "Abnormal request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + + ss_udc = dev_get_drvdata(usb_switch->exynos_udc_dev); + if (ss_udc && ss_udc->gadget.ops && + ss_udc->gadget.ops->vbus_session) + ss_udc->gadget.ops->vbus_session(&ss_udc->gadget, 0); + clear_bit(USB_DRD_DEVICE_ATTACHED, &usb_switch->connect); + break; + case USB_DRD_DEVICE_ATTACHED: + ss_udc = dev_get_drvdata(usb_switch->exynos_udc_dev); + if (ss_udc && ss_udc->gadget.ops && + ss_udc->gadget.ops->vbus_session) + ss_udc->gadget.ops->vbus_session(&ss_udc->gadget, 1); + set_bit(USB_DRD_DEVICE_ATTACHED, &usb_switch->connect); + break; + case USB_DRD_HOST_DETACHED: + if (test_bit(USB_DRD_DEVICE_ATTACHED, &cur_mode)) { + printk(KERN_ERR "Abnormal request %d, current %lu\n", + mode, cur_mode); + return -EPERM; + } + + if (usb_switch->xhci_dev) + pm_runtime_put_sync(usb_switch->xhci_dev); +#if defined(CONFIG_BATTERY_SAMSUNG) + exynos_usb_cable_disconnect(); +#endif + clear_bit(USB_DRD_HOST_ATTACHED, &usb_switch->connect); + break; + case USB_DRD_HOST_ATTACHED: +#if defined(CONFIG_BATTERY_SAMSUNG) + exynos_usb_cable_connect(); +#endif + if (usb_switch->xhci_dev) + pm_runtime_get_sync(usb_switch->xhci_dev); + set_bit(USB_DRD_HOST_ATTACHED, &usb_switch->connect); + break; + default: + printk(KERN_ERR "Does not changed\n"); + } + printk(KERN_INFO "usb cable = %d\n", mode); + + return ret; +} + +static void exnos_usb_switch_worker(struct work_struct *work) +{ + struct exynos_usb_switch *usb_switch = + container_of(work, struct exynos_usb_switch, switch_work); + int cnt = 0; + + mutex_lock(&usb_switch->mutex); + /* If already device detached or host_detected, */ + if (!is_device_detect(usb_switch) || is_host_detect(usb_switch)) + goto done; + if (!usb_switch->ehci_dev || !usb_switch->ohci_dev) + goto detect; + + while (!pm_runtime_suspended(usb_switch->ehci_dev) || + !pm_runtime_suspended(usb_switch->ohci_dev)) { + + mutex_unlock(&usb_switch->mutex); + msleep(SWITCH_WAIT_TIME); + mutex_lock(&usb_switch->mutex); + + /* If already device detached or host_detected, */ + if (!is_device_detect(usb_switch) || is_host_detect(usb_switch)) + goto done; + + if (cnt++ > WAIT_TIMES) { + printk(KERN_ERR "%s:device not attached by host\n", + __func__); + goto done; + } + + } + + if (cnt > 1) + printk(KERN_INFO "Device wait host power during %d\n", (cnt-1)); +detect: + /* Check Device, VBUS PIN high active */ + exynos_change_usb_mode(usb_switch, USB_DEVICE_ATTACHED); +done: + mutex_unlock(&usb_switch->mutex); +} + +static void exnos_usb_drd_switch_worker(struct work_struct *work) +{ + struct exynos_usb_switch *usb_switch = + container_of(work, struct exynos_usb_switch, switch_drd_work); + int cnt = 0; + + mutex_lock(&usb_switch->mutex); + /* If already device detached or host_detected, */ + if (!is_drd_device_detect(usb_switch) || is_drd_host_detect(usb_switch)) + goto done; + if (!usb_switch->xhci_dev) + goto detect; + + while (!pm_runtime_suspended(usb_switch->xhci_dev) || + usb_switch->xhci_dev->power.is_suspended) { + mutex_unlock(&usb_switch->mutex); + msleep(SWITCH_WAIT_TIME); + mutex_lock(&usb_switch->mutex); + + /* If already device detached or host_detected, */ + if (!is_drd_device_detect(usb_switch) || + is_drd_host_detect(usb_switch)) + goto done; + + if (cnt++ > WAIT_TIMES) { + printk(KERN_ERR "%s:device not attached by host\n", + __func__); + goto done; + } + + } + + if (cnt > 1) + printk(KERN_INFO "Device wait host power during %d\n", (cnt-1)); + +detect: + /* Check Device, VBUS PIN high active */ + exynos_change_usb_mode(usb_switch, USB_DRD_DEVICE_ATTACHED); +done: + mutex_unlock(&usb_switch->mutex); +} + +static irqreturn_t exynos_host_detect_thread(int irq, void *data) +{ + struct exynos_usb_switch *usb_switch = data; + + mutex_lock(&usb_switch->mutex); + + if (is_host_detect(usb_switch)) + exynos_change_usb_mode(usb_switch, USB_HOST_ATTACHED); + else + exynos_change_usb_mode(usb_switch, USB_HOST_DETACHED); + + mutex_unlock(&usb_switch->mutex); + + return IRQ_HANDLED; +} + +static irqreturn_t exynos_device_detect_thread(int irq, void *data) +{ + struct exynos_usb_switch *usb_switch = data; + + mutex_lock(&usb_switch->mutex); + + /* Debounce connect delay */ + msleep(20); + + if (is_host_detect(usb_switch)) + printk(KERN_DEBUG "Not expected situation\n"); + else if (is_device_detect(usb_switch)) { + if (usb_switch->gpio_host_vbus) + exynos_change_usb_mode(usb_switch, USB_DEVICE_ATTACHED); + else + queue_work(usb_switch->workqueue, &usb_switch->switch_work); + } else { + /* VBUS PIN low */ + exynos_change_usb_mode(usb_switch, USB_DEVICE_DETACHED); + } + + mutex_unlock(&usb_switch->mutex); + + return IRQ_HANDLED; +} + +static irqreturn_t exynos_drd_host_detect_thread(int irq, void *data) +{ + struct exynos_usb_switch *usb_switch = data; + + mutex_lock(&usb_switch->mutex); + + if (is_drd_host_detect(usb_switch)) + exynos_change_usb_mode(usb_switch, USB_DRD_HOST_ATTACHED); + else + exynos_change_usb_mode(usb_switch, USB_DRD_HOST_DETACHED); + + mutex_unlock(&usb_switch->mutex); + + return IRQ_HANDLED; +} + +static irqreturn_t exynos_drd_device_detect_thread(int irq, void *data) +{ + struct exynos_usb_switch *usb_switch = data; + + mutex_lock(&usb_switch->mutex); + + /* Debounce connect delay */ + msleep(20); + + if (is_drd_host_detect(usb_switch)) + printk(KERN_DEBUG "Not expected situation\n"); + else if (is_drd_device_detect(usb_switch)) + queue_work(usb_switch->workqueue, &usb_switch->switch_drd_work); + else { + /* VBUS PIN low */ + exynos_change_usb_mode(usb_switch, USB_DRD_DEVICE_DETACHED); + } + + mutex_unlock(&usb_switch->mutex); + + return IRQ_HANDLED; +} + +static int exynos_usb_status_init(struct exynos_usb_switch *usb_switch) +{ + mutex_lock(&usb_switch->mutex); + + /* 2.0 USB */ + if (!test_bit(USB_DRD_DEVICE_ATTACHED, &usb_switch->connect) || + !test_bit(USB_DRD_HOST_ATTACHED, &usb_switch->connect)) { + if (is_host_detect(usb_switch)) + exynos_change_usb_mode(usb_switch, USB_HOST_ATTACHED); + else if (is_device_detect(usb_switch)) { + if (usb_switch->gpio_host_vbus) + exynos_change_usb_mode(usb_switch, + USB_DEVICE_ATTACHED); + else + queue_work(usb_switch->workqueue, + &usb_switch->switch_work); + } + } + + /* 3.0 USB */ + if (!test_bit(USB_DRD_DEVICE_ATTACHED, &usb_switch->connect) || + !test_bit(USB_DRD_HOST_ATTACHED, &usb_switch->connect)) { + if (is_drd_host_detect(usb_switch)) + exynos_change_usb_mode(usb_switch, + USB_DRD_HOST_ATTACHED); + else if (is_drd_device_detect(usb_switch)) + queue_work(usb_switch->workqueue, + &usb_switch->switch_drd_work); + } + + mutex_unlock(&usb_switch->mutex); + + return 0; +} + +#ifdef CONFIG_PM +static int exynos_usbswitch_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct exynos_usb_switch *usb_switch = platform_get_drvdata(pdev); + + dev_dbg(dev, "%s\n", __func__); + mutex_lock(&usb_switch->mutex); + if (test_bit(USB_DEVICE_ATTACHED, &usb_switch->connect)) + exynos_change_usb_mode(usb_switch, USB_DEVICE_DETACHED); + + if (test_bit(USB_DRD_DEVICE_ATTACHED, &usb_switch->connect)) + exynos_change_usb_mode(usb_switch, USB_DRD_DEVICE_DETACHED); + + usb_switch->connect = 0; + mutex_unlock(&usb_switch->mutex); + + return 0; +} + +static int exynos_usbswitch_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct exynos_usb_switch *usb_switch = platform_get_drvdata(pdev); + + dev_dbg(dev, "%s\n", __func__); + exynos_usb_status_init(usb_switch); + + return 0; +} +#else +#define exynos_usbswitch_suspend NULL +#define exynos_usbswitch_resume NULL +#endif + +static int __devinit exynos_usbswitch_probe(struct platform_device *pdev) +{ + struct s5p_usbswitch_platdata *pdata = dev_get_platdata(&pdev->dev); + struct device *dev = &pdev->dev; + struct exynos_usb_switch *usb_switch; + int irq; + int ret = 0; + + usb_switch = kzalloc(sizeof(struct exynos_usb_switch), GFP_KERNEL); + if (!usb_switch) { + ret = -ENOMEM; + return ret; + } + + our_switch = usb_switch; + mutex_init(&usb_switch->mutex); + usb_switch->workqueue = create_singlethread_workqueue("usb_switch"); + INIT_WORK(&usb_switch->switch_work, exnos_usb_switch_worker); + INIT_WORK(&usb_switch->switch_drd_work, exnos_usb_drd_switch_worker); + + usb_switch->gpio_host_detect = pdata->gpio_host_detect; + usb_switch->gpio_device_detect = pdata->gpio_device_detect; + usb_switch->gpio_host_vbus = pdata->gpio_host_vbus; + usb_switch->gpio_drd_host_detect = pdata->gpio_drd_host_detect; + usb_switch->gpio_drd_device_detect = pdata->gpio_drd_device_detect; + + usb_switch->ehci_dev = pdata->ehci_dev; + usb_switch->ohci_dev = pdata->ohci_dev; + usb_switch->xhci_dev = pdata->xhci_dev; + usb_switch->s3c_udc_dev = pdata->s3c_udc_dev; + usb_switch->exynos_udc_dev = pdata->exynos_udc_dev; + + /* USB Device detect IRQ */ + irq = platform_get_irq(pdev, 1); + if (irq > 0 && usb_switch->s3c_udc_dev) { + ret = request_threaded_irq(irq, NULL, + exynos_device_detect_thread, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "DEVICE_DETECT", usb_switch); + if (ret) { + dev_err(dev, "Failed to request device irq %d\n", irq); + goto fail; + } + usb_switch->device_detect_irq = irq; + } else if (usb_switch->s3c_udc_dev) + exynos_change_usb_mode(usb_switch, USB_DEVICE_ATTACHED); + else + dev_info(dev, "Disable device detect IRQ\n"); + + /* USB Host detect IRQ */ + irq = platform_get_irq(pdev, 0); + if (irq > 0 && (usb_switch->ehci_dev || usb_switch->ohci_dev)) { + ret = request_threaded_irq(irq, NULL, + exynos_host_detect_thread, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "HOST_DETECT", usb_switch); + if (ret) { + dev_err(dev, "Failed to request host irq %d\n", irq); + goto fail_gpio_device_detect; + } + usb_switch->host_detect_irq = irq; + } else if (usb_switch->ehci_dev || usb_switch->ohci_dev) + exynos_change_usb_mode(usb_switch, USB_HOST_ATTACHED); + else + dev_info(dev, "Disable host detect IRQ\n"); + + + /* USB DRD Device detect IRQ */ + irq = platform_get_irq(pdev, 3); + if (irq > 0 && usb_switch->exynos_udc_dev) { + ret = request_threaded_irq(irq, NULL, + exynos_drd_device_detect_thread, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "DRD_DEVICE_DETECT", usb_switch); + if (ret) { + dev_err(dev, "Failed to request drd device irq %d\n", + irq); + goto fail_gpio_host_detect; + } + usb_switch->device_drd_detect_irq = irq; + } else if (usb_switch->exynos_udc_dev) + exynos_change_usb_mode(usb_switch, USB_DRD_DEVICE_ATTACHED); + else + dev_info(dev, "Disable drd device detect IRQ\n"); + + /* USB DRD Host detect IRQ */ + irq = platform_get_irq(pdev, 2); + if (irq > 0 && usb_switch->xhci_dev) { + ret = request_threaded_irq(irq, NULL, + exynos_drd_host_detect_thread, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "DRD_HOST_DETECT", usb_switch); + if (ret) { + dev_err(dev, "Failed to request drd host irq %d\n", + irq); + goto fail_gpio_drd_device_detect; + } + usb_switch->host_drd_detect_irq = irq; + } else if (usb_switch->xhci_dev) + exynos_change_usb_mode(usb_switch, USB_DRD_HOST_ATTACHED); + else + dev_info(dev, "Disable drd host detect IRQ\n"); + + exynos_usb_status_init(usb_switch); + + platform_set_drvdata(pdev, usb_switch); + + return ret; +fail_gpio_drd_device_detect: + free_irq(usb_switch->device_drd_detect_irq, usb_switch); +fail_gpio_host_detect: + free_irq(usb_switch->host_detect_irq, usb_switch); +fail_gpio_device_detect: + free_irq(usb_switch->device_detect_irq, usb_switch); +fail: + cancel_work_sync(&usb_switch->switch_work); + destroy_workqueue(usb_switch->workqueue); + mutex_destroy(&usb_switch->mutex); + kfree(usb_switch); + return ret; +} + +static int __devexit exynos_usbswitch_remove(struct platform_device *pdev) +{ + struct exynos_usb_switch *usb_switch = platform_get_drvdata(pdev); + + free_irq(usb_switch->host_drd_detect_irq, usb_switch); + free_irq(usb_switch->device_drd_detect_irq, usb_switch); + free_irq(usb_switch->host_detect_irq, usb_switch); + free_irq(usb_switch->device_detect_irq, usb_switch); + platform_set_drvdata(pdev, 0); + + cancel_work_sync(&usb_switch->switch_work); + destroy_workqueue(usb_switch->workqueue); + mutex_destroy(&usb_switch->mutex); + kfree(usb_switch); + + return 0; +} + +static const struct dev_pm_ops exynos_usbswitch_pm_ops = { + .suspend = exynos_usbswitch_suspend, + .resume = exynos_usbswitch_resume, +}; + +static struct platform_driver exynos_usbswitch_driver = { + .probe = exynos_usbswitch_probe, + .remove = __devexit_p(exynos_usbswitch_remove), + .driver = { + .name = "exynos-usb-switch", + .owner = THIS_MODULE, + .pm = &exynos_usbswitch_pm_ops, + }, +}; + +static int __init exynos_usbswitch_init(void) +{ + int ret; + + ret = platform_device_register(&s5p_device_usbswitch); + if (ret < 0) + return ret; + + ret = platform_driver_register(&exynos_usbswitch_driver); + if (!ret) + printk(KERN_INFO "%s: " DRIVER_DESC "\n", switch_name); + + return ret; +} +late_initcall(exynos_usbswitch_init); + +static void __exit exynos_usbswitch_exit(void) +{ + platform_driver_unregister(&exynos_usbswitch_driver); +} +module_exit(exynos_usbswitch_exit); + +MODULE_DESCRIPTION("Exynos USB switch driver"); +MODULE_AUTHOR("<yulgon.kim@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/usb/misc/exynos-usb-switch.h b/drivers/usb/misc/exynos-usb-switch.h new file mode 100644 index 0000000..3749987 --- /dev/null +++ b/drivers/usb/misc/exynos-usb-switch.h @@ -0,0 +1,63 @@ +/* + * exynos-usb-switch.h - USB switch driver for Exynos + * + * Copyright (c) 2010-2011 Samsung Electronics Co., Ltd. + * Yulgon Kim <yulgon.kim@samsung.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. +*/ +#ifndef __EXYNOS_USB_SWITCH +#define __EXYNOS_USB_SWITCH + +#define SWITCH_WAIT_TIME 500 +#define WAIT_TIMES 10 + +enum usb_cable_status { + USB_DEVICE_ATTACHED, + USB_HOST_ATTACHED, + USB_DRD_DEVICE_ATTACHED, + USB_DRD_HOST_ATTACHED, + USB_DEVICE_DETACHED, + USB_HOST_DETACHED, + USB_DRD_DEVICE_DETACHED, + USB_DRD_HOST_DETACHED, +}; + +struct exynos_usb_switch { + unsigned long connect; + + unsigned int host_detect_irq; + unsigned int device_detect_irq; + unsigned int host_drd_detect_irq; + unsigned int device_drd_detect_irq; + unsigned int gpio_host_detect; + unsigned int gpio_device_detect; + unsigned int gpio_host_vbus; + unsigned int gpio_drd_host_detect; + unsigned int gpio_drd_device_detect; + + struct device *ehci_dev; + struct device *ohci_dev; + struct device *xhci_dev; + + struct device *s3c_udc_dev; + struct device *exynos_udc_dev; + + struct workqueue_struct *workqueue; + struct work_struct switch_work; + struct work_struct switch_drd_work; + struct mutex mutex; + atomic_t usb_status; + int (*get_usb_mode)(void); + int (*change_usb_mode)(int mode); +}; + +extern int s5p_ehci_port_power_off(struct platform_device *pdev); +extern int s5p_ohci_port_power_off(struct platform_device *pdev); + +extern int s5p_ehci_port_power_on(struct platform_device *pdev); +extern int s5p_ohci_port_power_on(struct platform_device *pdev); + +#endif diff --git a/drivers/usb/misc/mdm_ctrl_bridge.c b/drivers/usb/misc/mdm_ctrl_bridge.c new file mode 100644 index 0000000..4755790 --- /dev/null +++ b/drivers/usb/misc/mdm_ctrl_bridge.c @@ -0,0 +1,768 @@ +/* Copyright (c) 2011-2012, Code Aurora Forum. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/kernel.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/kref.h> +#include <linux/debugfs.h> +#include <linux/platform_device.h> +#include <linux/uaccess.h> +#include <linux/ratelimit.h> +#include <linux/usb/ch9.h> +#include <linux/usb/cdc.h> +#include <linux/termios.h> +#include <asm/unaligned.h> +#include <mach/usb_bridge.h> + +static const char const *ctrl_bridge_names[] = { + "dun_ctrl_hsic0", + "rmnet_ctrl_hsic0" +}; + +/* polling interval for Interrupt ep */ +#define HS_INTERVAL 7 +#define FS_LS_INTERVAL 3 + +#define ACM_CTRL_DTR (1 << 0) +#define DEFAULT_READ_URB_LENGTH 4096 + +#define SUSPENDED BIT(0) + +struct ctrl_bridge { + struct usb_device *udev; + struct usb_interface *intf; + + unsigned int int_pipe; + struct urb *inturb; + void *intbuf; + + struct urb *readurb; + void *readbuf; + + struct usb_anchor tx_submitted; + struct usb_anchor tx_deferred; + struct usb_ctrlrequest *in_ctlreq; + + struct bridge *brdg; + struct platform_device *pdev; + + unsigned long flags; + + /* input control lines (DSR, CTS, CD, RI) */ + unsigned int cbits_tohost; + + /* output control lines (DTR, RTS) */ + unsigned int cbits_tomdm; + + /* counters */ + unsigned int snd_encap_cmd; + unsigned int get_encap_res; + unsigned int resp_avail; + unsigned int set_ctrl_line_sts; + unsigned int notify_ser_state; +}; + +static struct ctrl_bridge *__dev[MAX_BRIDGE_DEVICES]; + +/* counter used for indexing ctrl bridge devices */ +static int ch_id; + +unsigned int ctrl_bridge_get_cbits_tohost(unsigned int id) +{ + struct ctrl_bridge *dev; + + if (id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[id]; + if (!dev) + return -ENODEV; + + return dev->cbits_tohost; +} +EXPORT_SYMBOL(ctrl_bridge_get_cbits_tohost); + +int ctrl_bridge_set_cbits(unsigned int id, unsigned int cbits) +{ + struct ctrl_bridge *dev; + struct bridge *brdg; + int retval; + + if (id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[id]; + if (!dev) + return -ENODEV; + + pr_debug("%s: dev[id] =%u cbits : %u\n", __func__, id, cbits); + + brdg = dev->brdg; + if (!brdg) + return -ENODEV; + + dev->cbits_tomdm = cbits; + + retval = ctrl_bridge_write(id, NULL, 0); + + /* if DTR is high, update latest modem info to host */ + if (brdg && (cbits & ACM_CTRL_DTR) && brdg->ops.send_cbits) + brdg->ops.send_cbits(brdg->ctx, dev->cbits_tohost); + + return retval; +} +EXPORT_SYMBOL(ctrl_bridge_set_cbits); + +static void resp_avail_cb(struct urb *urb) +{ + struct ctrl_bridge *dev = urb->context; + struct usb_device *udev; + int status = 0; + int resubmit_urb = 1; + struct bridge *brdg = dev->brdg; + + udev = interface_to_usbdev(dev->intf); + switch (urb->status) { + case 0: + /*success*/ + dev->get_encap_res++; + if (brdg && brdg->ops.send_pkt) + brdg->ops.send_pkt(brdg->ctx, urb->transfer_buffer, + urb->actual_length); + break; + + /*do not resubmit*/ + case -ESHUTDOWN: + case -ENOENT: + case -ECONNRESET: + /* unplug */ + case -EPROTO: + /*babble error*/ + resubmit_urb = 0; + /*resubmit*/ + case -EOVERFLOW: + default: + dev_dbg(&udev->dev, "%s: non zero urb status = %d\n", + __func__, urb->status); + } + + if (resubmit_urb) { + /*re- submit int urb to check response available*/ + usb_anchor_urb(dev->inturb, &dev->tx_submitted); + status = usb_submit_urb(dev->inturb, GFP_ATOMIC); + if (status) { + dev_err(&udev->dev, + "%s: Error re-submitting Int URB %d\n", + __func__, status); + usb_unanchor_urb(dev->inturb); + } + } +} + +static void notification_available_cb(struct urb *urb) +{ + int status; + struct usb_cdc_notification *ctrl; + struct usb_device *udev; + struct ctrl_bridge *dev = urb->context; + struct bridge *brdg = dev->brdg; + unsigned int ctrl_bits; + unsigned char *data; + + udev = interface_to_usbdev(dev->intf); + + switch (urb->status) { + case 0: + /*success*/ + break; + case -ESHUTDOWN: + case -ENOENT: + case -ECONNRESET: + case -EPROTO: + /* unplug */ + return; + case -EPIPE: + dev_err(&udev->dev, "%s: stall on int endpoint\n", __func__); + /* TBD : halt to be cleared in work */ + case -EOVERFLOW: + default: + pr_debug_ratelimited("%s: non zero urb status = %d\n", + __func__, urb->status); + goto resubmit_int_urb; + } + + ctrl = (struct usb_cdc_notification *)urb->transfer_buffer; + data = (unsigned char *)(ctrl + 1); + + switch (ctrl->bNotificationType) { + case USB_CDC_NOTIFY_RESPONSE_AVAILABLE: + dev->resp_avail++; + usb_fill_control_urb(dev->readurb, udev, + usb_rcvctrlpipe(udev, 0), + (unsigned char *)dev->in_ctlreq, + dev->readbuf, + DEFAULT_READ_URB_LENGTH, + resp_avail_cb, dev); + + usb_anchor_urb(dev->readurb, &dev->tx_submitted); + status = usb_submit_urb(dev->readurb, GFP_ATOMIC); + if (status) { + dev_err(&udev->dev, + "%s: Error submitting Read URB %d\n", + __func__, status); + usb_unanchor_urb(dev->readurb); + goto resubmit_int_urb; + } + return; + case USB_CDC_NOTIFY_NETWORK_CONNECTION: + dev_dbg(&udev->dev, "%s network\n", ctrl->wValue ? + "connected to" : "disconnected from"); + break; + case USB_CDC_NOTIFY_SERIAL_STATE: + dev->notify_ser_state++; + ctrl_bits = get_unaligned_le16(data); + dev_dbg(&udev->dev, "serial state: %d\n", ctrl_bits); + dev->cbits_tohost = ctrl_bits; + if (brdg && brdg->ops.send_cbits) + brdg->ops.send_cbits(brdg->ctx, ctrl_bits); + break; + default: + dev_err(&udev->dev, "%s: unknown notification %d received:" + "index %d len %d data0 %d data1 %d", + __func__, ctrl->bNotificationType, ctrl->wIndex, + ctrl->wLength, data[0], data[1]); + } + +resubmit_int_urb: + usb_anchor_urb(urb, &dev->tx_submitted); + status = usb_submit_urb(urb, GFP_ATOMIC); + if (status) { + dev_err(&udev->dev, "%s: Error re-submitting Int URB %d\n", + __func__, status); + usb_unanchor_urb(urb); + } +} + +int ctrl_bridge_start_read(struct ctrl_bridge *dev) +{ + int retval = 0; + + if (!dev->inturb) { + dev_err(&dev->udev->dev, "%s: inturb is NULL\n", __func__); + return -ENODEV; + } + + if (!dev->inturb->anchor) { + usb_anchor_urb(dev->inturb, &dev->tx_submitted); + retval = usb_submit_urb(dev->inturb, GFP_KERNEL); + if (retval < 0) { + dev_err(&dev->udev->dev, + "%s error submitting int urb %d\n", + __func__, retval); + usb_unanchor_urb(dev->inturb); + } + } + + return retval; +} + +int ctrl_bridge_open(struct bridge *brdg) +{ + struct ctrl_bridge *dev; + int ret; + + if (!brdg) { + err("bridge is null\n"); + return -EINVAL; + } + + if (brdg->ch_id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[brdg->ch_id]; + if (!dev) { + err("dev is null\n"); + return -ENODEV; + } + + dev->brdg = brdg; + dev->snd_encap_cmd = 0; + dev->get_encap_res = 0; + dev->resp_avail = 0; + dev->set_ctrl_line_sts = 0; + dev->notify_ser_state = 0; + + ret = usb_autopm_get_interface(dev->intf); + if (ret < 0) { + dev_err(&dev->udev->dev, "%s autopm_get fail: %d\n", + __func__, ret); + return ret; + } + + ret = ctrl_bridge_start_read(dev); + usb_autopm_put_interface(dev->intf); + return ret; +} +EXPORT_SYMBOL(ctrl_bridge_open); + +void ctrl_bridge_close(unsigned int id) +{ + struct ctrl_bridge *dev; + + if (id >= MAX_BRIDGE_DEVICES) + return; + + dev = __dev[id]; + if (!dev || !dev->brdg) + return; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + ctrl_bridge_set_cbits(dev->brdg->ch_id, 0); + usb_unlink_anchored_urbs(&dev->tx_submitted); + + dev->brdg = NULL; +} +EXPORT_SYMBOL(ctrl_bridge_close); + +static void ctrl_write_callback(struct urb *urb) +{ + struct ctrl_bridge *dev = urb->context; + + if (urb->status) { + pr_debug("Write status/size %d/%d\n", + urb->status, urb->actual_length); + } + + kfree(urb->transfer_buffer); + kfree(urb->setup_packet); + usb_free_urb(urb); + usb_autopm_put_interface_async(dev->intf); +} + +int ctrl_bridge_write(unsigned int id, char *data, size_t size) +{ + int result; + struct urb *writeurb; + struct usb_ctrlrequest *out_ctlreq; + struct usb_device *udev; + struct ctrl_bridge *dev; + + if (id >= MAX_BRIDGE_DEVICES) { + result = -EINVAL; + goto free_data; + } + + dev = __dev[id]; + + if (!dev) { + result = -ENODEV; + goto free_data; + } + + udev = interface_to_usbdev(dev->intf); + + dev_dbg(&udev->dev, "%s:[id]:%u: write (%d bytes)\n", + __func__, id, size); + + writeurb = usb_alloc_urb(0, GFP_ATOMIC); + if (!writeurb) { + dev_err(&udev->dev, "%s: error allocating read urb\n", + __func__); + result = -ENOMEM; + goto free_data; + } + + out_ctlreq = kmalloc(sizeof(*out_ctlreq), GFP_ATOMIC); + if (!out_ctlreq) { + dev_err(&udev->dev, + "%s: error allocating setup packet buffer\n", + __func__); + result = -ENOMEM; + goto free_urb; + } + + /* CDC Send Encapsulated Request packet */ + out_ctlreq->bRequestType = (USB_DIR_OUT | USB_TYPE_CLASS | + USB_RECIP_INTERFACE); + if (!data && !size) { + out_ctlreq->bRequest = USB_CDC_REQ_SET_CONTROL_LINE_STATE; + out_ctlreq->wValue = dev->cbits_tomdm; + dev->set_ctrl_line_sts++; + } else { + out_ctlreq->bRequest = USB_CDC_SEND_ENCAPSULATED_COMMAND; + out_ctlreq->wValue = 0; + dev->snd_encap_cmd++; + } + out_ctlreq->wIndex = + dev->intf->cur_altsetting->desc.bInterfaceNumber; + out_ctlreq->wLength = cpu_to_le16(size); + + usb_fill_control_urb(writeurb, udev, + usb_sndctrlpipe(udev, 0), + (unsigned char *)out_ctlreq, + (void *)data, size, + ctrl_write_callback, dev); + + result = usb_autopm_get_interface_async(dev->intf); + if (result < 0) { + dev_err(&udev->dev, "%s: unable to resume interface: %d\n", + __func__, result); + + /* + * Revisit: if (result == -EPERM) + * bridge_suspend(dev->intf, PMSG_SUSPEND); + */ + + goto free_ctrlreq; + } + + if (test_bit(SUSPENDED, &dev->flags)) { + usb_anchor_urb(writeurb, &dev->tx_deferred); + goto deferred; + } + + usb_anchor_urb(writeurb, &dev->tx_submitted); + result = usb_submit_urb(writeurb, GFP_ATOMIC); + if (result < 0) { + dev_err(&udev->dev, "%s: submit URB error %d\n", + __func__, result); + usb_autopm_put_interface_async(dev->intf); + goto unanchor_urb; + } +deferred: + return size; + +unanchor_urb: + usb_unanchor_urb(writeurb); +free_ctrlreq: + kfree(out_ctlreq); +free_urb: + usb_free_urb(writeurb); +free_data: + kfree(data); + + return result; +} +EXPORT_SYMBOL(ctrl_bridge_write); + +int ctrl_bridge_suspend(unsigned int id) +{ + struct ctrl_bridge *dev; + + if (id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[id]; + if (!dev) + return -ENODEV; + + set_bit(SUSPENDED, &dev->flags); + usb_kill_anchored_urbs(&dev->tx_submitted); + + return 0; +} + +int ctrl_bridge_resume(unsigned int id) +{ + struct ctrl_bridge *dev; + struct urb *urb; + + if (id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[id]; + if (!dev) + return -ENODEV; + + if (!test_and_clear_bit(SUSPENDED, &dev->flags)) + return 0; + + /* submit pending write requests */ + while ((urb = usb_get_from_anchor(&dev->tx_deferred))) { + int ret; + usb_anchor_urb(urb, &dev->tx_submitted); + ret = usb_submit_urb(urb, GFP_ATOMIC); + if (ret < 0) { + usb_unanchor_urb(urb); + kfree(urb->setup_packet); + kfree(urb->transfer_buffer); + usb_free_urb(urb); + usb_autopm_put_interface_async(dev->intf); + } + } + + /* if the bridge is open, resume reading */ + if (dev->brdg) + return ctrl_bridge_start_read(dev); + + return 0; +} + +#if defined(CONFIG_DEBUG_FS) +#define DEBUG_BUF_SIZE 1024 +static ssize_t ctrl_bridge_read_stats(struct file *file, char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct ctrl_bridge *dev; + char *buf; + int ret; + int i; + int temp = 0; + + buf = kzalloc(sizeof(char) * DEBUG_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + for (i = 0; i < ch_id; i++) { + dev = __dev[i]; + if (!dev) + continue; + + temp += scnprintf(buf + temp, DEBUG_BUF_SIZE - temp, + "\nName#%s dev %p\n" + "snd encap cmd cnt: %u\n" + "get encap res cnt: %u\n" + "res available cnt: %u\n" + "set ctrlline sts cnt: %u\n" + "notify ser state cnt: %u\n" + "cbits_tomdm: %d\n" + "cbits_tohost: %d\n" + "suspended: %d\n", + dev->pdev->name, dev, + dev->snd_encap_cmd, + dev->get_encap_res, + dev->resp_avail, + dev->set_ctrl_line_sts, + dev->notify_ser_state, + dev->cbits_tomdm, + dev->cbits_tohost, + test_bit(SUSPENDED, &dev->flags)); + } + + ret = simple_read_from_buffer(ubuf, count, ppos, buf, temp); + + kfree(buf); + + return ret; +} + +static ssize_t ctrl_bridge_reset_stats(struct file *file, + const char __user *buf, size_t count, loff_t *ppos) +{ + struct ctrl_bridge *dev; + int i; + + for (i = 0; i < ch_id; i++) { + dev = __dev[i]; + if (!dev) + continue; + + dev->snd_encap_cmd = 0; + dev->get_encap_res = 0; + dev->resp_avail = 0; + dev->set_ctrl_line_sts = 0; + dev->notify_ser_state = 0; + } + return count; +} + +const struct file_operations ctrl_stats_ops = { + .read = ctrl_bridge_read_stats, + .write = ctrl_bridge_reset_stats, +}; + +struct dentry *ctrl_dent; +struct dentry *ctrl_dfile; +static void ctrl_bridge_debugfs_init(void) +{ + ctrl_dent = debugfs_create_dir("ctrl_hsic_bridge", 0); + if (IS_ERR(ctrl_dent)) + return; + + ctrl_dfile = + debugfs_create_file("status", 0644, ctrl_dent, 0, + &ctrl_stats_ops); + if (!ctrl_dfile || IS_ERR(ctrl_dfile)) + debugfs_remove(ctrl_dent); +} + +static void ctrl_bridge_debugfs_exit(void) +{ + debugfs_remove(ctrl_dfile); + debugfs_remove(ctrl_dent); +} + +#else +static void ctrl_bridge_debugfs_init(void) { } +static void ctrl_bridge_debugfs_exit(void) { } +#endif + +int +ctrl_bridge_probe(struct usb_interface *ifc, struct usb_host_endpoint *int_in, + int id) +{ + struct ctrl_bridge *dev; + struct usb_device *udev; + struct usb_endpoint_descriptor *ep; + u16 wMaxPacketSize; + int retval = 0; + int interval; + + udev = interface_to_usbdev(ifc); + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) { + dev_err(&udev->dev, "%s: unable to allocate dev\n", + __func__); + return -ENOMEM; + } + dev->pdev = platform_device_alloc(ctrl_bridge_names[id], id); + if (!dev->pdev) { + dev_err(&dev->udev->dev, + "%s: unable to allocate platform device\n", __func__); + retval = -ENOMEM; + goto nomem; + } + + dev->udev = udev; + dev->int_pipe = usb_rcvintpipe(udev, + int_in->desc.bEndpointAddress & USB_ENDPOINT_NUMBER_MASK); + dev->intf = ifc; + + init_usb_anchor(&dev->tx_submitted); + init_usb_anchor(&dev->tx_deferred); + + /*use max pkt size from ep desc*/ + ep = &dev->intf->cur_altsetting->endpoint[0].desc; + + dev->inturb = usb_alloc_urb(0, GFP_KERNEL); + if (!dev->inturb) { + dev_err(&udev->dev, "%s: error allocating int urb\n", __func__); + retval = -ENOMEM; + goto pdev_del; + } + + wMaxPacketSize = le16_to_cpu(ep->wMaxPacketSize); + + dev->intbuf = kmalloc(wMaxPacketSize, GFP_KERNEL); + if (!dev->intbuf) { + dev_err(&udev->dev, "%s: error allocating int buffer\n", + __func__); + retval = -ENOMEM; + goto free_inturb; + } + + interval = + (udev->speed == USB_SPEED_HIGH) ? HS_INTERVAL : FS_LS_INTERVAL; + + usb_fill_int_urb(dev->inturb, udev, dev->int_pipe, + dev->intbuf, wMaxPacketSize, + notification_available_cb, dev, interval); + + dev->readurb = usb_alloc_urb(0, GFP_KERNEL); + if (!dev->readurb) { + dev_err(&udev->dev, "%s: error allocating read urb\n", + __func__); + retval = -ENOMEM; + goto free_intbuf; + } + + dev->readbuf = kmalloc(DEFAULT_READ_URB_LENGTH, GFP_KERNEL); + if (!dev->readbuf) { + dev_err(&udev->dev, "%s: error allocating read buffer\n", + __func__); + retval = -ENOMEM; + goto free_rurb; + } + + dev->in_ctlreq = kmalloc(sizeof(*dev->in_ctlreq), GFP_KERNEL); + if (!dev->in_ctlreq) { + dev_err(&udev->dev, + "%s:error allocating setup packet buffer\n", + __func__); + retval = -ENOMEM; + goto free_rbuf; + } + + dev->in_ctlreq->bRequestType = + (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE); + dev->in_ctlreq->bRequest = USB_CDC_GET_ENCAPSULATED_RESPONSE; + dev->in_ctlreq->wValue = 0; + dev->in_ctlreq->wIndex = + dev->intf->cur_altsetting->desc.bInterfaceNumber; + dev->in_ctlreq->wLength = cpu_to_le16(DEFAULT_READ_URB_LENGTH); + + __dev[id] = dev; + + platform_device_add(dev->pdev); + + ch_id++; + + return retval; + +free_rbuf: + kfree(dev->readbuf); +free_rurb: + usb_free_urb(dev->readurb); +free_intbuf: + kfree(dev->intbuf); +free_inturb: + usb_free_urb(dev->inturb); +pdev_del: + platform_device_del(dev->pdev); +nomem: + kfree(dev); + + return retval; +} + +void ctrl_bridge_disconnect(unsigned int id) +{ + struct ctrl_bridge *dev = __dev[id]; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + platform_device_del(dev->pdev); + + kfree(dev->in_ctlreq); + kfree(dev->readbuf); + kfree(dev->intbuf); + + usb_free_urb(dev->readurb); + usb_free_urb(dev->inturb); + + __dev[id] = NULL; + ch_id--; + + kfree(dev); +} + +static int __init ctrl_bridge_init(void) +{ + ctrl_bridge_debugfs_init(); + + return 0; +} +module_init(ctrl_bridge_init); + +static void __exit ctrl_bridge_exit(void) +{ + ctrl_bridge_debugfs_exit(); +} +module_exit(ctrl_bridge_exit); + +MODULE_DESCRIPTION("Qualcomm modem control bridge driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/misc/mdm_data_bridge.c b/drivers/usb/misc/mdm_data_bridge.c new file mode 100644 index 0000000..6af9664 --- /dev/null +++ b/drivers/usb/misc/mdm_data_bridge.c @@ -0,0 +1,1079 @@ +/* Copyright (c) 2011-2012, Code Aurora Forum. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/kernel.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/debugfs.h> +#include <linux/platform_device.h> +#include <linux/uaccess.h> +#include <linux/ratelimit.h> +#include <mach/usb_bridge.h> + +#define MAX_RX_URBS 50 +#define RMNET_RX_BUFSIZE 2048 + +#define STOP_SUBMIT_URB_LIMIT 500 +#define FLOW_CTRL_EN_THRESHOLD 500 +#define FLOW_CTRL_DISABLE 300 +#define FLOW_CTRL_SUPPORT 1 + +static const char const *data_bridge_names[] = { + "dun_data_hsic0", + "rmnet_data_hsic0" +}; + +static struct workqueue_struct *bridge_wq; + +static unsigned int fctrl_support = FLOW_CTRL_SUPPORT; +module_param(fctrl_support, uint, S_IRUGO | S_IWUSR); + +static unsigned int fctrl_en_thld = FLOW_CTRL_EN_THRESHOLD; +module_param(fctrl_en_thld, uint, S_IRUGO | S_IWUSR); + +static unsigned int fctrl_dis_thld = FLOW_CTRL_DISABLE; +module_param(fctrl_dis_thld, uint, S_IRUGO | S_IWUSR); + +unsigned int max_rx_urbs = MAX_RX_URBS; +module_param(max_rx_urbs, uint, S_IRUGO | S_IWUSR); + +unsigned int stop_submit_urb_limit = STOP_SUBMIT_URB_LIMIT; +module_param(stop_submit_urb_limit, uint, S_IRUGO | S_IWUSR); + +static unsigned tx_urb_mult = 20; +module_param(tx_urb_mult, uint, S_IRUGO|S_IWUSR); + +#define TX_HALT BIT(0) +#define RX_HALT BIT(1) +#define SUSPENDED BIT(2) + +struct data_bridge { + struct usb_interface *intf; + struct usb_device *udev; + int id; + + unsigned int bulk_in; + unsigned int bulk_out; + int err; + + /* keep track of in-flight URBs */ + struct usb_anchor tx_active; + struct usb_anchor rx_active; + + /* keep track of outgoing URBs during suspend */ + struct usb_anchor delayed; + + struct list_head rx_idle; + struct sk_buff_head rx_done; + + struct workqueue_struct *wq; + struct work_struct process_rx_w; + + struct bridge *brdg; + + /* work queue function for handling halt conditions */ + struct work_struct kevent; + + unsigned long flags; + + struct platform_device *pdev; + + /* counters */ + atomic_t pending_txurbs; + unsigned int txurb_drp_cnt; + unsigned long to_host; + unsigned long to_modem; + unsigned int tx_throttled_cnt; + unsigned int tx_unthrottled_cnt; + unsigned int rx_throttled_cnt; + unsigned int rx_unthrottled_cnt; +}; + +static struct data_bridge *__dev[MAX_BRIDGE_DEVICES]; + +/* counter used for indexing data bridge devices */ +static int ch_id; + +static unsigned int get_timestamp(void); +static void dbg_timestamp(char *, struct sk_buff *); +static int submit_rx_urb(struct data_bridge *dev, struct urb *urb, + gfp_t flags); + +static inline bool rx_halted(struct data_bridge *dev) +{ + return test_bit(RX_HALT, &dev->flags); +} + +static inline bool rx_throttled(struct bridge *brdg) +{ + return test_bit(RX_THROTTLED, &brdg->flags); +} + +int data_bridge_unthrottle_rx(unsigned int id) +{ + struct data_bridge *dev; + + if (id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[id]; + if (!dev || !dev->brdg) + return -ENODEV; + + dev->rx_unthrottled_cnt++; + queue_work(dev->wq, &dev->process_rx_w); + + return 0; +} +EXPORT_SYMBOL(data_bridge_unthrottle_rx); + +static void data_bridge_process_rx(struct work_struct *work) +{ + int retval; + unsigned long flags; + struct urb *rx_idle; + struct sk_buff *skb; + struct timestamp_info *info; + struct data_bridge *dev = + container_of(work, struct data_bridge, process_rx_w); + + struct bridge *brdg = dev->brdg; + + if (!brdg || !brdg->ops.send_pkt || rx_halted(dev)) + return; + + while (!rx_throttled(brdg) && (skb = skb_dequeue(&dev->rx_done))) { + dev->to_host++; + info = (struct timestamp_info *)skb->cb; + info->rx_done_sent = get_timestamp(); + /* hand off sk_buff to client,they'll need to free it */ + retval = brdg->ops.send_pkt(brdg->ctx, skb, skb->len); + if (retval == -ENOTCONN || retval == -EINVAL) { + return; + } else if (retval == -EBUSY) { + dev->rx_throttled_cnt++; + break; + } + } + + spin_lock_irqsave(&dev->rx_done.lock, flags); + while (!list_empty(&dev->rx_idle)) { + if (dev->rx_done.qlen > stop_submit_urb_limit) + break; + + rx_idle = list_first_entry(&dev->rx_idle, struct urb, urb_list); + list_del(&rx_idle->urb_list); + spin_unlock_irqrestore(&dev->rx_done.lock, flags); + retval = submit_rx_urb(dev, rx_idle, GFP_KERNEL); + spin_lock_irqsave(&dev->rx_done.lock, flags); + if (retval) { + list_add_tail(&rx_idle->urb_list, &dev->rx_idle); + break; + } + } + spin_unlock_irqrestore(&dev->rx_done.lock, flags); +} + +static void data_bridge_read_cb(struct urb *urb) +{ + struct bridge *brdg; + struct sk_buff *skb = urb->context; + struct timestamp_info *info = (struct timestamp_info *)skb->cb; + struct data_bridge *dev = info->dev; + bool queue = 0; + + brdg = dev->brdg; + skb_put(skb, urb->actual_length); + + switch (urb->status) { + case 0: /* success */ + queue = 1; + info->rx_done = get_timestamp(); + spin_lock(&dev->rx_done.lock); + __skb_queue_tail(&dev->rx_done, skb); + spin_unlock(&dev->rx_done.lock); + break; + + /*do not resubmit*/ + case -EPIPE: + set_bit(RX_HALT, &dev->flags); + dev_err(&dev->udev->dev, "%s: epout halted\n", __func__); + schedule_work(&dev->kevent); + /* FALLTHROUGH */ + case -ESHUTDOWN: + case -ENOENT: /* suspended */ + case -ECONNRESET: /* unplug */ + case -EPROTO: + dev_kfree_skb_any(skb); + break; + + /*resubmit */ + case -EOVERFLOW: /*babble error*/ + default: + queue = 1; + dev_kfree_skb_any(skb); + pr_debug_ratelimited("%s: non zero urb status = %d\n", + __func__, urb->status); + break; + } + + spin_lock(&dev->rx_done.lock); + list_add_tail(&urb->urb_list, &dev->rx_idle); + spin_unlock(&dev->rx_done.lock); + + if (queue) + queue_work(dev->wq, &dev->process_rx_w); +} + +static int submit_rx_urb(struct data_bridge *dev, struct urb *rx_urb, + gfp_t flags) +{ + struct sk_buff *skb; + struct timestamp_info *info; + int retval = -EINVAL; + unsigned int created; + + created = get_timestamp(); + skb = alloc_skb(RMNET_RX_BUFSIZE, flags); + if (!skb) + return -ENOMEM; + + info = (struct timestamp_info *)skb->cb; + info->dev = dev; + info->created = created; + + usb_fill_bulk_urb(rx_urb, dev->udev, dev->bulk_in, + skb->data, RMNET_RX_BUFSIZE, + data_bridge_read_cb, skb); + + if (test_bit(SUSPENDED, &dev->flags)) + goto suspended; + + usb_anchor_urb(rx_urb, &dev->rx_active); + info->rx_queued = get_timestamp(); + retval = usb_submit_urb(rx_urb, flags); + if (retval) + goto fail; + + return 0; +fail: + usb_unanchor_urb(rx_urb); +suspended: + dev_kfree_skb_any(skb); + + return retval; +} + +static int data_bridge_prepare_rx(struct data_bridge *dev) +{ + int i; + struct urb *rx_urb; + + for (i = 0; i < max_rx_urbs; i++) { + rx_urb = usb_alloc_urb(0, GFP_KERNEL); + if (!rx_urb) + return -ENOMEM; + + list_add_tail(&rx_urb->urb_list, &dev->rx_idle); + } + return 0; +} + +int data_bridge_open(struct bridge *brdg) +{ + struct data_bridge *dev; + + if (!brdg) { + err("bridge is null\n"); + return -EINVAL; + } + + if (brdg->ch_id >= MAX_BRIDGE_DEVICES) + return -EINVAL; + + dev = __dev[brdg->ch_id]; + if (!dev) { + err("dev is null\n"); + return -ENODEV; + } + + dev_dbg(&dev->udev->dev, "%s: dev:%p\n", __func__, dev); + + dev->brdg = brdg; + dev->err = 0; + atomic_set(&dev->pending_txurbs, 0); + dev->to_host = 0; + dev->to_modem = 0; + dev->txurb_drp_cnt = 0; + dev->tx_throttled_cnt = 0; + dev->tx_unthrottled_cnt = 0; + dev->rx_throttled_cnt = 0; + dev->rx_unthrottled_cnt = 0; + + queue_work(dev->wq, &dev->process_rx_w); + + return 0; +} +EXPORT_SYMBOL(data_bridge_open); + +void data_bridge_close(unsigned int id) +{ + struct data_bridge *dev; + struct sk_buff *skb; + unsigned long flags; + + if (id >= MAX_BRIDGE_DEVICES) + return; + + dev = __dev[id]; + if (!dev || !dev->brdg) + return; + + dev_dbg(&dev->udev->dev, "%s:\n", __func__); + + usb_unlink_anchored_urbs(&dev->tx_active); + usb_unlink_anchored_urbs(&dev->rx_active); + usb_unlink_anchored_urbs(&dev->delayed); + + spin_lock_irqsave(&dev->rx_done.lock, flags); + while ((skb = __skb_dequeue(&dev->rx_done))) + dev_kfree_skb_any(skb); + spin_unlock_irqrestore(&dev->rx_done.lock, flags); + + dev->brdg = NULL; +} +EXPORT_SYMBOL(data_bridge_close); + +static void defer_kevent(struct work_struct *work) +{ + int status; + struct data_bridge *dev = + container_of(work, struct data_bridge, kevent); + + if (!dev) + return; + + if (test_bit(TX_HALT, &dev->flags)) { + usb_unlink_anchored_urbs(&dev->tx_active); + + status = usb_autopm_get_interface(dev->intf); + if (status < 0) { + dev_err(&dev->udev->dev, + "can't acquire interface, status %d\n", status); + return; + } + + status = usb_clear_halt(dev->udev, dev->bulk_out); + usb_autopm_put_interface(dev->intf); + if (status < 0 && status != -EPIPE && status != -ESHUTDOWN) + dev_err(&dev->udev->dev, + "can't clear tx halt, status %d\n", status); + else + clear_bit(TX_HALT, &dev->flags); + } + + if (test_bit(RX_HALT, &dev->flags)) { + usb_unlink_anchored_urbs(&dev->rx_active); + + status = usb_autopm_get_interface(dev->intf); + if (status < 0) { + dev_err(&dev->udev->dev, + "can't acquire interface, status %d\n", status); + return; + } + + status = usb_clear_halt(dev->udev, dev->bulk_in); + usb_autopm_put_interface(dev->intf); + if (status < 0 && status != -EPIPE && status != -ESHUTDOWN) + dev_err(&dev->udev->dev, + "can't clear rx halt, status %d\n", status); + else { + clear_bit(RX_HALT, &dev->flags); + if (dev->brdg) + queue_work(dev->wq, &dev->process_rx_w); + } + } +} + +static void data_bridge_write_cb(struct urb *urb) +{ + struct sk_buff *skb = urb->context; + struct timestamp_info *info = (struct timestamp_info *)skb->cb; + struct data_bridge *dev = info->dev; + struct bridge *brdg = dev->brdg; + int pending; + + pr_debug("%s: dev:%p\n", __func__, dev); + + switch (urb->status) { + case 0: /*success*/ + dbg_timestamp("UL", skb); + break; + case -EPROTO: + dev->err = -EPROTO; + break; + case -EPIPE: + set_bit(TX_HALT, &dev->flags); + dev_err(&dev->udev->dev, "%s: epout halted\n", __func__); + schedule_work(&dev->kevent); + /* FALLTHROUGH */ + case -ESHUTDOWN: + case -ENOENT: /* suspended */ + case -ECONNRESET: /* unplug */ + case -EOVERFLOW: /*babble error*/ + /* FALLTHROUGH */ + default: + pr_debug_ratelimited("%s: non zero urb status = %d\n", + __func__, urb->status); + } + + usb_free_urb(urb); + dev_kfree_skb_any(skb); + + pending = atomic_dec_return(&dev->pending_txurbs); + + /*flow ctrl*/ + if (brdg && fctrl_support && pending <= fctrl_dis_thld && + test_and_clear_bit(TX_THROTTLED, &brdg->flags)) { + pr_debug_ratelimited("%s: disable flow ctrl: pend urbs:%u\n", + __func__, pending); + dev->tx_unthrottled_cnt++; + if (brdg->ops.unthrottle_tx) + brdg->ops.unthrottle_tx(brdg->ctx); + } + + usb_autopm_put_interface_async(dev->intf); +} + +int data_bridge_write(unsigned int id, struct sk_buff *skb) +{ + int result; + int size = skb->len; + int pending; + struct urb *txurb; + struct timestamp_info *info = (struct timestamp_info *)skb->cb; + struct data_bridge *dev = __dev[id]; + struct bridge *brdg; + + if (!dev || !dev->brdg || dev->err || !usb_get_intfdata(dev->intf)) + return -ENODEV; + + brdg = dev->brdg; + if (!brdg) + return -ENODEV; + + dev_dbg(&dev->udev->dev, "%s: write (%d bytes)\n", __func__, skb->len); + + result = usb_autopm_get_interface(dev->intf); + if (result < 0) { + dev_err(&dev->udev->dev, "%s: resume failure\n", __func__); + goto error; + } + + txurb = usb_alloc_urb(0, GFP_KERNEL); + if (!txurb) { + dev_err(&dev->udev->dev, "%s: error allocating read urb\n", + __func__); + result = -ENOMEM; + goto error; + } + + /* store dev pointer in skb */ + info->dev = dev; + info->tx_queued = get_timestamp(); + + usb_fill_bulk_urb(txurb, dev->udev, dev->bulk_out, + skb->data, skb->len, data_bridge_write_cb, skb); + + if (test_bit(SUSPENDED, &dev->flags)) { + usb_anchor_urb(txurb, &dev->delayed); + goto free_urb; + } + + pending = atomic_inc_return(&dev->pending_txurbs); + usb_anchor_urb(txurb, &dev->tx_active); + + if (atomic_read(&dev->pending_txurbs) % tx_urb_mult) + txurb->transfer_flags |= URB_NO_INTERRUPT; + + result = usb_submit_urb(txurb, GFP_KERNEL); + if (result < 0) { + usb_unanchor_urb(txurb); + atomic_dec(&dev->pending_txurbs); + dev_err(&dev->udev->dev, "%s: submit URB error %d\n", + __func__, result); + goto free_urb; + } + + dev->to_modem++; + dev_dbg(&dev->udev->dev, "%s: pending_txurbs: %u\n", __func__, pending); + + /* flow control: last urb submitted but return -EBUSY */ + if (fctrl_support && pending > fctrl_en_thld) { + set_bit(TX_THROTTLED, &brdg->flags); + dev->tx_throttled_cnt++; + pr_debug_ratelimited("%s: enable flow ctrl pend txurbs:%u\n", + __func__, pending); + return -EBUSY; + } + + return size; + +free_urb: + usb_free_urb(txurb); +error: + dev->txurb_drp_cnt++; + usb_autopm_put_interface(dev->intf); + + return result; +} +EXPORT_SYMBOL(data_bridge_write); + +static int data_bridge_resume(struct data_bridge *dev) +{ + struct urb *urb; + int retval; + + if (!test_and_clear_bit(SUSPENDED, &dev->flags)) + return 0; + + while ((urb = usb_get_from_anchor(&dev->delayed))) { + usb_anchor_urb(urb, &dev->tx_active); + atomic_inc(&dev->pending_txurbs); + retval = usb_submit_urb(urb, GFP_ATOMIC); + if (retval < 0) { + atomic_dec(&dev->pending_txurbs); + usb_unanchor_urb(urb); + + /* TODO: need to free urb data */ + usb_scuttle_anchored_urbs(&dev->delayed); + break; + } + dev->to_modem++; + dev->txurb_drp_cnt--; + } + + if (dev->brdg) + queue_work(dev->wq, &dev->process_rx_w); + + return 0; +} + +static int bridge_resume(struct usb_interface *iface) +{ + int retval = 0; + int oldstate; + struct data_bridge *dev = usb_get_intfdata(iface); + + oldstate = iface->dev.power.power_state.event; + iface->dev.power.power_state.event = PM_EVENT_ON; + + if (oldstate & PM_EVENT_SUSPEND) { + retval = data_bridge_resume(dev); + if (!retval) + retval = ctrl_bridge_resume(dev->id); + } + + return retval; +} + +static int data_bridge_suspend(struct data_bridge *dev, pm_message_t message) +{ + if (atomic_read(&dev->pending_txurbs) && + (message.event & PM_EVENT_AUTO)) + return -EBUSY; + + set_bit(SUSPENDED, &dev->flags); + + usb_kill_anchored_urbs(&dev->tx_active); + usb_kill_anchored_urbs(&dev->rx_active); + + return 0; +} + +static int bridge_suspend(struct usb_interface *intf, pm_message_t message) +{ + int retval; + struct data_bridge *dev = usb_get_intfdata(intf); + + retval = data_bridge_suspend(dev, message); + if (!retval) { + retval = ctrl_bridge_suspend(dev->id); + intf->dev.power.power_state.event = message.event; + } + + return retval; +} + +static int data_bridge_probe(struct usb_interface *iface, + struct usb_host_endpoint *bulk_in, + struct usb_host_endpoint *bulk_out, int id) +{ + struct data_bridge *dev; + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) { + err("%s: unable to allocate dev\n", __func__); + return -ENOMEM; + } + + dev->pdev = platform_device_alloc(data_bridge_names[id], id); + if (!dev->pdev) { + err("%s: unable to allocate platform device\n", __func__); + kfree(dev); + return -ENOMEM; + } + + init_usb_anchor(&dev->tx_active); + init_usb_anchor(&dev->rx_active); + init_usb_anchor(&dev->delayed); + + INIT_LIST_HEAD(&dev->rx_idle); + skb_queue_head_init(&dev->rx_done); + + dev->wq = bridge_wq; + dev->id = id; + dev->udev = interface_to_usbdev(iface); + dev->intf = iface; + + dev->bulk_in = usb_rcvbulkpipe(dev->udev, + bulk_in->desc.bEndpointAddress & USB_ENDPOINT_NUMBER_MASK); + + dev->bulk_out = usb_sndbulkpipe(dev->udev, + bulk_out->desc.bEndpointAddress & USB_ENDPOINT_NUMBER_MASK); + + usb_set_intfdata(iface, dev); + + INIT_WORK(&dev->kevent, defer_kevent); + INIT_WORK(&dev->process_rx_w, data_bridge_process_rx); + + __dev[id] = dev; + + /*allocate list of rx urbs*/ + data_bridge_prepare_rx(dev); + + platform_device_add(dev->pdev); + + return 0; +} + +#if defined(CONFIG_DEBUG_FS) +#define DEBUG_BUF_SIZE 1024 + +static unsigned int record_timestamp; +module_param(record_timestamp, uint, S_IRUGO | S_IWUSR); + +static struct timestamp_buf dbg_data = { + .idx = 0, + .lck = __RW_LOCK_UNLOCKED(lck) +}; + +/*get_timestamp - returns time of day in us */ +static unsigned int get_timestamp(void) +{ + struct timeval tval; + unsigned int stamp; + + if (!record_timestamp) + return 0; + + do_gettimeofday(&tval); + /* 2^32 = 4294967296. Limit to 4096s. */ + stamp = tval.tv_sec & 0xFFF; + stamp = stamp * 1000000 + tval.tv_usec; + return stamp; +} + +static void dbg_inc(unsigned *idx) +{ + *idx = (*idx + 1) & (DBG_DATA_MAX-1); +} + +/** +* dbg_timestamp - Stores timestamp values of a SKB life cycle +* to debug buffer +* @event: "UL": Uplink Data +* @skb: SKB used to store timestamp values to debug buffer +*/ +static void dbg_timestamp(char *event, struct sk_buff * skb) +{ + unsigned long flags; + struct timestamp_info *info = (struct timestamp_info *)skb->cb; + + if (!record_timestamp) + return; + + write_lock_irqsave(&dbg_data.lck, flags); + + scnprintf(dbg_data.buf[dbg_data.idx], DBG_DATA_MSG, + "%p %u[%s] %u %u %u %u %u %u\n", + skb, skb->len, event, info->created, info->rx_queued, + info->rx_done, info->rx_done_sent, info->tx_queued, + get_timestamp()); + + dbg_inc(&dbg_data.idx); + + write_unlock_irqrestore(&dbg_data.lck, flags); +} + +/* show_timestamp: displays the timestamp buffer */ +static ssize_t show_timestamp(struct file *file, char __user *ubuf, + size_t count, loff_t *ppos) +{ + unsigned long flags; + unsigned i; + unsigned j = 0; + char *buf; + int ret = 0; + + if (!record_timestamp) + return 0; + + buf = kzalloc(sizeof(char) * 4 * DEBUG_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + read_lock_irqsave(&dbg_data.lck, flags); + + i = dbg_data.idx; + for (dbg_inc(&i); i != dbg_data.idx; dbg_inc(&i)) { + if (!strnlen(dbg_data.buf[i], DBG_DATA_MSG)) + continue; + j += scnprintf(buf + j, (4 * DEBUG_BUF_SIZE) - j, + "%s\n", dbg_data.buf[i]); + } + + read_unlock_irqrestore(&dbg_data.lck, flags); + + ret = simple_read_from_buffer(ubuf, count, ppos, buf, j); + + kfree(buf); + + return ret; +} + +const struct file_operations data_timestamp_ops = { + .read = show_timestamp, +}; + +static ssize_t data_bridge_read_stats(struct file *file, char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct data_bridge *dev; + char *buf; + int ret; + int i; + int temp = 0; + + buf = kzalloc(sizeof(char) * DEBUG_BUF_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + for (i = 0; i < ch_id; i++) { + dev = __dev[i]; + if (!dev) + continue; + + temp += scnprintf(buf + temp, DEBUG_BUF_SIZE - temp, + "\nName#%s dev %p\n" + "pending tx urbs: %u\n" + "tx urb drp cnt: %u\n" + "to host: %lu\n" + "to mdm: %lu\n" + "tx throttled cnt: %u\n" + "tx unthrottled cnt: %u\n" + "rx throttled cnt: %u\n" + "rx unthrottled cnt: %u\n" + "rx done skb qlen: %u\n" + "dev err: %d\n" + "suspended: %d\n" + "TX_HALT: %d\n" + "RX_HALT: %d\n", + dev->pdev->name, dev, + atomic_read(&dev->pending_txurbs), + dev->txurb_drp_cnt, + dev->to_host, + dev->to_modem, + dev->tx_throttled_cnt, + dev->tx_unthrottled_cnt, + dev->rx_throttled_cnt, + dev->rx_unthrottled_cnt, + dev->rx_done.qlen, + dev->err, + test_bit(SUSPENDED, &dev->flags), + test_bit(TX_HALT, &dev->flags), + test_bit(RX_HALT, &dev->flags)); + + } + + ret = simple_read_from_buffer(ubuf, count, ppos, buf, temp); + + kfree(buf); + + return ret; +} + +static ssize_t data_bridge_reset_stats(struct file *file, + const char __user *buf, size_t count, loff_t *ppos) +{ + struct data_bridge *dev; + int i; + + for (i = 0; i < ch_id; i++) { + dev = __dev[i]; + if (!dev) + continue; + + dev->to_host = 0; + dev->to_modem = 0; + dev->txurb_drp_cnt = 0; + dev->tx_throttled_cnt = 0; + dev->tx_unthrottled_cnt = 0; + dev->rx_throttled_cnt = 0; + dev->rx_unthrottled_cnt = 0; + } + return count; +} + +const struct file_operations data_stats_ops = { + .read = data_bridge_read_stats, + .write = data_bridge_reset_stats, +}; + +static struct dentry *data_dent; +static struct dentry *data_dfile_stats; +static struct dentry *data_dfile_tstamp; + +static void data_bridge_debugfs_init(void) +{ + data_dent = debugfs_create_dir("data_hsic_bridge", 0); + if (IS_ERR(data_dent)) + return; + + data_dfile_stats = debugfs_create_file("status", 0644, data_dent, 0, + &data_stats_ops); + if (!data_dfile_stats || IS_ERR(data_dfile_stats)) { + debugfs_remove(data_dent); + return; + } + + data_dfile_tstamp = debugfs_create_file("timestamp", 0644, data_dent, + 0, &data_timestamp_ops); + if (!data_dfile_tstamp || IS_ERR(data_dfile_tstamp)) + debugfs_remove(data_dent); +} + +static void data_bridge_debugfs_exit(void) +{ + debugfs_remove(data_dfile_stats); + debugfs_remove(data_dfile_tstamp); + debugfs_remove(data_dent); +} + +#else +static void data_bridge_debugfs_init(void) { } +static void data_bridge_debugfs_exit(void) { } +static void dbg_timestamp(char *event, struct sk_buff * skb) +{ + return; +} + +static unsigned int get_timestamp(void) +{ + return 0; +} + +#endif + +static int __devinit +bridge_probe(struct usb_interface *iface, const struct usb_device_id *id) +{ + struct usb_host_endpoint *endpoint = NULL; + struct usb_host_endpoint *bulk_in = NULL; + struct usb_host_endpoint *bulk_out = NULL; + struct usb_host_endpoint *int_in = NULL; + struct usb_device *udev; + int i; + int status = 0; + int numends; + unsigned int iface_num; + + iface_num = iface->cur_altsetting->desc.bInterfaceNumber; + + if (iface->num_altsetting != 1) { + err("%s invalid num_altsetting %u\n", + __func__, iface->num_altsetting); + return -EINVAL; + } + + udev = interface_to_usbdev(iface); + usb_get_dev(udev); + + if (!test_bit(iface_num, &id->driver_info)) + return -ENODEV; + + numends = iface->cur_altsetting->desc.bNumEndpoints; + for (i = 0; i < numends; i++) { + endpoint = iface->cur_altsetting->endpoint + i; + if (!endpoint) { + dev_err(&udev->dev, "%s: invalid endpoint %u\n", + __func__, i); + status = -EINVAL; + goto out; + } + + if (usb_endpoint_is_bulk_in(&endpoint->desc)) + bulk_in = endpoint; + else if (usb_endpoint_is_bulk_out(&endpoint->desc)) + bulk_out = endpoint; + else if (usb_endpoint_is_int_in(&endpoint->desc)) + int_in = endpoint; + } + + if (!bulk_in || !bulk_out || !int_in) { + dev_err(&udev->dev, "%s: invalid endpoints\n", __func__); + status = -EINVAL; + goto out; + } + + status = data_bridge_probe(iface, bulk_in, bulk_out, ch_id); + if (status < 0) { + dev_err(&udev->dev, "data_bridge_probe failed %d\n", status); + goto out; + } + + status = ctrl_bridge_probe(iface, int_in, ch_id); + if (status < 0) { + dev_err(&udev->dev, "ctrl_bridge_probe failed %d\n", status); + goto free_data_bridge; + } + + ch_id++; + + return 0; + +free_data_bridge: + platform_device_del(__dev[ch_id]->pdev); + usb_set_intfdata(iface, NULL); + kfree(__dev[ch_id]); + __dev[ch_id] = NULL; +out: + usb_put_dev(udev); + + return status; +} + +static void bridge_disconnect(struct usb_interface *intf) +{ + struct data_bridge *dev = usb_get_intfdata(intf); + struct list_head *head; + struct urb *rx_urb; + unsigned long flags; + + if (!dev) { + err("%s: data device not found\n", __func__); + return; + } + + ch_id--; + ctrl_bridge_disconnect(ch_id); + platform_device_del(dev->pdev); + usb_set_intfdata(intf, NULL); + __dev[ch_id] = NULL; + + cancel_work_sync(&dev->process_rx_w); + cancel_work_sync(&dev->kevent); + + /*free rx urbs*/ + head = &dev->rx_idle; + spin_lock_irqsave(&dev->rx_done.lock, flags); + while (!list_empty(head)) { + rx_urb = list_entry(head->next, struct urb, urb_list); + list_del(&rx_urb->urb_list); + usb_free_urb(rx_urb); + } + spin_unlock_irqrestore(&dev->rx_done.lock, flags); + + usb_put_dev(dev->udev); + kfree(dev); +} + +/*bit position represents interface number*/ +#define PID9001_IFACE_MASK 0xC +#define PID9034_IFACE_MASK 0xC +#define PID9048_IFACE_MASK 0x18 +#define PID904C_IFACE_MASK 0x28 + +static const struct usb_device_id bridge_ids[] = { + { USB_DEVICE(0x5c6, 0x9001), + .driver_info = PID9001_IFACE_MASK, + }, + { USB_DEVICE(0x5c6, 0x9034), + .driver_info = PID9034_IFACE_MASK, + }, + { USB_DEVICE(0x5c6, 0x9048), + .driver_info = PID9048_IFACE_MASK, + }, + { USB_DEVICE(0x5c6, 0x904c), + .driver_info = PID904C_IFACE_MASK, + }, + + { } /* Terminating entry */ +}; +MODULE_DEVICE_TABLE(usb, bridge_ids); + +static struct usb_driver bridge_driver = { + .name = "mdm_bridge", + .probe = bridge_probe, + .disconnect = bridge_disconnect, + .id_table = bridge_ids, + .suspend = bridge_suspend, + .resume = bridge_resume, + .supports_autosuspend = 1, +}; + +static int __init bridge_init(void) +{ + int ret; + + ret = usb_register(&bridge_driver); + if (ret) { + err("%s: unable to register mdm_bridge driver", __func__); + return ret; + } + + bridge_wq = create_singlethread_workqueue("mdm_bridge"); + if (!bridge_wq) { + usb_deregister(&bridge_driver); + pr_err("%s: Unable to create workqueue:bridge\n", __func__); + return -ENOMEM; + } + + data_bridge_debugfs_init(); + + return 0; +} + +static void __exit bridge_exit(void) +{ + data_bridge_debugfs_exit(); + destroy_workqueue(bridge_wq); + usb_deregister(&bridge_driver); +} + +module_init(bridge_init); +module_exit(bridge_exit); + +MODULE_DESCRIPTION("Qualcomm modem data bridge driver"); +MODULE_LICENSE("GPL v2"); |