diff options
author | Simon Shields <simon@lineageos.org> | 2018-04-03 14:05:27 +1000 |
---|---|---|
committer | Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org> | 2020-01-20 02:58:43 +0100 |
commit | b4e625b1ddbd8cfa48c319fca86022513d824942 (patch) | |
tree | 2cdd35faf234d31485db91632bc954e4cdf36753 | |
parent | d868b49adac1c11dc8dc30ae6b54a1648115a20c (diff) | |
download | kernel_replicant_linux-b4e625b1ddbd8cfa48c319fca86022513d824942.tar.gz kernel_replicant_linux-b4e625b1ddbd8cfa48c319fca86022513d824942.tar.bz2 kernel_replicant_linux-b4e625b1ddbd8cfa48c319fca86022513d824942.zip |
net: usb: add Samsung IPC-over-HSIC driver
-rw-r--r-- | drivers/net/usb/Kconfig | 7 | ||||
-rw-r--r-- | drivers/net/usb/Makefile | 1 | ||||
-rw-r--r-- | drivers/net/usb/sipc_hsic.c | 297 |
3 files changed, 305 insertions, 0 deletions
diff --git a/drivers/net/usb/Kconfig b/drivers/net/usb/Kconfig index 05bdcc5917f6..28760f80b8fd 100644 --- a/drivers/net/usb/Kconfig +++ b/drivers/net/usb/Kconfig @@ -309,6 +309,13 @@ config USB_NET_DM9601 This option adds support for Davicom DM9601/DM9620/DM9621A based USB 10/100 Ethernet adapters. +config USB_NET_SAMSUNG_IPC + tristate "Samsung IPC USB link" + depends on NET_SAMSUNG_IPC + help + This option adds support for communicating to Samsung IPC modems + over USB/HSIC. + config USB_NET_SR9700 tristate "CoreChip-sz SR9700 based USB 1.1 10/100 ethernet devices" depends on USB_USBNET diff --git a/drivers/net/usb/Makefile b/drivers/net/usb/Makefile index 99fd12be2111..bb9a2c9923bc 100644 --- a/drivers/net/usb/Makefile +++ b/drivers/net/usb/Makefile @@ -41,3 +41,4 @@ obj-$(CONFIG_USB_NET_QMI_WWAN) += qmi_wwan.o obj-$(CONFIG_USB_NET_CDC_MBIM) += cdc_mbim.o obj-$(CONFIG_USB_NET_CH9200) += ch9200.o obj-$(CONFIG_USB_NET_AQC111) += aqc111.o +obj-$(CONFIG_USB_NET_SAMSUNG_IPC) += sipc_hsic.o diff --git a/drivers/net/usb/sipc_hsic.c b/drivers/net/usb/sipc_hsic.c new file mode 100644 index 000000000000..0cce30ff071b --- /dev/null +++ b/drivers/net/usb/sipc_hsic.c @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// HSIC interface for modems speaking +// Samsung's IPC v4.x protocol +// +// Copyright (C) 2018 Simon Shields <simon@lineageos.org> +// +#include <linux/module.h> +#include <linux/netdevice.h> +#include <linux/usb.h> +#include <linux/usb/cdc.h> +#include <linux/sipc.h> +#include <linux/skbuff.h> + +#define USB_EP_FMT 0 +#define USB_EP_RAW 1 +#define USB_EP_RFS 2 +#define USB_EP_CMD 3 + +struct sipc_usb_ep { + int ep; + struct sk_buff_head tx_q; + + struct urb *in_urb; + unsigned char *in_buf; + size_t in_buf_size; + + u8 bulk_in_addr; + u8 bulk_out_addr; + + struct sipc_link link_ops; + struct sipc_link_callback *cb; + struct usb_device *udev; + struct usb_interface *data_intf; +}; +#define linkops_to_ep(ops) container_of(ops, struct sipc_usb_ep, link_ops) + +static int sipc_start_rx(struct sipc_usb_ep *ep); +static int sipc_link_transmit(struct sipc_link *link, struct sk_buff *skb); + +static unsigned int usb_to_sipc_format(int ep) { + switch (ep) { + case USB_EP_FMT: + return SAMSUNG_IPC_FORMAT_FMT; + case USB_EP_RAW: + return SAMSUNG_IPC_FORMAT_RAW; + case USB_EP_RFS: + return SAMSUNG_IPC_FORMAT_RFS; + case USB_EP_CMD: + return SAMSUNG_IPC_FORMAT_CMD; + } + + return -1; +} + +static void sipc_rx_complete(struct urb *urb) +{ + struct sipc_usb_ep *ep = urb->context; + int format = usb_to_sipc_format(ep->ep); + if (format == SAMSUNG_IPC_FORMAT_RAW) + format = SAMSUNG_IPC_FORMAT_MULTI_RAW; + + if (!urb->status) { + ep->cb->receive(ep->cb, urb->transfer_buffer, + urb->actual_length, format); + sipc_start_rx(ep); + } +} + +static int sipc_start_rx(struct sipc_usb_ep *ep) +{ + struct urb *urb; + int ret; + + urb = ep->in_urb; + urb->transfer_flags = 0; + usb_fill_bulk_urb(urb, ep->udev, usb_rcvbulkpipe(ep->udev, ep->bulk_in_addr), + ep->in_buf, ep->in_buf_size, sipc_rx_complete, + ep); + + ret = usb_submit_urb(urb, GFP_ATOMIC); + if (ret) + dev_err(&ep->udev->dev, "Failed to submit rx urb: %d\n", ret); + + return ret; +} + +static void sipc_tx_complete(struct urb *urb) +{ + struct sk_buff *skb = urb->context; + dev_kfree_skb_any(skb); + usb_free_urb(urb); +} + +static int sipc_link_transmit(struct sipc_link *link, struct sk_buff *skb) +{ + struct sipc_usb_ep *ep = linkops_to_ep(link); + struct urb *urb; + int ret; + + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) + return -ENOMEM; + + urb->transfer_flags = URB_ZERO_PACKET; + usb_fill_bulk_urb(urb, ep->udev, usb_sndbulkpipe(ep->udev, ep->bulk_out_addr), + skb->data, skb->len, sipc_tx_complete, skb); + + ret = usb_submit_urb(urb, GFP_KERNEL); + if (ret < 0) { + dev_err(&ep->udev->dev, "Failed to submit URB: %d\n", ret); + usb_free_urb(urb); + return ret; + } + + return 0; +} + +static int sipc_link_open(struct sipc_link *link, int channel, int format) +{ + struct sipc_usb_ep *ep = linkops_to_ep(link); + struct sk_buff *skb; + char data = 'a'; + int ret = 0; + + /* TODO: runtime PM support */ + if (channel == 0x1) { + + skb = alloc_skb(16, GFP_KERNEL); + if (unlikely(!skb)) + return -ENOMEM; + + memcpy(skb_put(skb, sizeof(data)), &data, sizeof(data)); + + ret = sipc_link_transmit(&ep->link_ops, skb); + if (ret < 0) { + dev_kfree_skb_any(skb); + } + } + return ret; +} + +static void sipc_set_callbacks(struct sipc_link *link, + struct sipc_link_callback *cb) +{ + struct sipc_usb_ep *ep = linkops_to_ep(link); + ep->cb = cb; +} + +static int sipc_probe(struct usb_interface *intf, const struct usb_device_id *id) +{ + int ret, buflen; + struct sipc_usb_ep *ep; + struct usb_endpoint_descriptor *bulk_in, *bulk_out; + struct usb_cdc_union_desc *union_header; + struct usb_cdc_parsed_header h; + struct usb_driver *usbdrv = to_usb_driver(intf->dev.driver); + struct usb_interface *data_intf; + unsigned char *buf; + unsigned int fmt; + + buflen = intf->altsetting->extralen; + buf = intf->altsetting->extra; + + if (intf->altsetting->desc.bInterfaceSubClass != USB_CDC_SUBCLASS_ACM) { + dev_info(&intf->dev, "Skipping non-ACM endpoint\n"); + return -EINVAL; + } + + if (!buflen) { + if (intf->cur_altsetting->endpoint) { + buflen = intf->cur_altsetting->endpoint->extralen; + buf = intf->cur_altsetting->endpoint->extra; + } + } + + if (!buflen || !buf) { + dev_err(&intf->dev, "No descriptor reference\n"); + return -EINVAL; + } + + cdc_parse_cdc_header(&h, intf, buf, buflen); + union_header = h.usb_cdc_union_desc; + + ep = devm_kzalloc(&intf->dev, sizeof(*ep), GFP_KERNEL); + if (!ep) + return -ENOMEM; + + ep->udev = usb_get_dev(interface_to_usbdev(intf)); + + data_intf = usb_ifnum_to_if(ep->udev, union_header->bSlaveInterface0); + if (!data_intf || !data_intf->altsetting) { + dev_err(&intf->dev, "Couldn't find data interface!\n"); + return -EINVAL; + } + + ret = usb_find_common_endpoints(data_intf->altsetting, + &bulk_in, &bulk_out, NULL, NULL); + if (ret) { + dev_err(&intf->dev, "Couldn't find bulk-in and bulk-out endpoints: %d\n", ret); + return ret; + } + + ep->data_intf = data_intf; + ep->in_buf_size = usb_endpoint_maxp(bulk_in); + ep->bulk_in_addr = bulk_in->bEndpointAddress; + ep->in_buf = devm_kzalloc(&intf->dev, ep->in_buf_size, GFP_KERNEL); + if (!ep->in_buf) + return -ENOMEM; + ep->in_urb = usb_alloc_urb(0, GFP_KERNEL); + if (!ep->in_urb) + return -ENOMEM; + + ep->bulk_out_addr = bulk_out->bEndpointAddress; + /* divide by two because each interface has rx + tx endpoints */ + ep->ep = intf->altsetting->desc.bInterfaceNumber / 2; + + ret = usb_driver_claim_interface(usbdrv, data_intf, ep); + if (ret) { + dev_err(&intf->dev, "Failed to claim interface\n"); + goto urb_free; + } + + usb_set_intfdata(intf, ep); + + fmt = usb_to_sipc_format(ep->ep); + if (fmt < 0) { + dev_warn(&intf->dev, "Unknown EP format %d\n", ep->ep); + return 0; + } + + ep->link_ops.transmit = sipc_link_transmit; + ep->link_ops.open = sipc_link_open; + ep->link_ops.set_callbacks = sipc_set_callbacks; + + ret = sipc_set_link(&ep->link_ops, fmt); + if (ret < 0) { + dev_err(&intf->dev, "Failed to set SIPC link for fmt %u: %d\n", fmt, ret); + return ret; + } + + if (fmt == SAMSUNG_IPC_FORMAT_RAW) { + ret = sipc_set_link(&ep->link_ops, SAMSUNG_IPC_FORMAT_MULTI_RAW); + if (ret < 0) { + dev_err(&intf->dev, "Failed to set SIPC link for MULTI_RAW: %d\n", ret); + return ret; + } + } + + sipc_start_rx(ep); + return 0; +urb_free: + usb_free_urb(ep->in_urb); + return ret; +} + +static void sipc_disconnect(struct usb_interface *intf) +{ + struct sipc_usb_ep *ep = usb_get_intfdata(intf); + struct usb_driver *usbdrv = to_usb_driver(intf->dev.driver); + int fmt; + + fmt = usb_to_sipc_format(ep->ep); + if (fmt < 0) + goto invalid_format; + + sipc_clear_link(fmt); + if (fmt == SAMSUNG_IPC_FORMAT_RAW) + sipc_clear_link(SAMSUNG_IPC_FORMAT_MULTI_RAW); + +invalid_format: + usb_driver_release_interface(usbdrv, ep->data_intf); + usb_kill_urb(ep->in_urb); + usb_free_urb(ep->in_urb); +} + +static struct usb_device_id sipc_idtable[] = { + /* Infineon XMM626x */ + {USB_DEVICE_AND_INTERFACE_INFO(0x1519, 0x0020, + USB_CLASS_COMM, USB_CDC_SUBCLASS_ACM, USB_CDC_ACM_PROTO_AT_V25TER)}, + {}, +}; + +static struct usb_driver sipc_hsic_driver = { + .name = "sipc_hsic", + .probe = sipc_probe, + .disconnect = sipc_disconnect, + .id_table = sipc_idtable, + .supports_autosuspend = 0, +}; + +module_usb_driver(sipc_hsic_driver); + +MODULE_DEVICE_TABLE(usb, sipc_idtable); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Simon Shields <simon@lineageos.org>"); |