[RFC 2/9] mfd: Add driver for Multifunction USB Device

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



A Multifunction USB Device is a device that supports functions like gpio
and display or any other function that can be represented as a USB regmap.
Interrupts over USB is also supported if such an endpoint is present.

Signed-off-by: Noralf Trønnes <noralf@xxxxxxxxxxx>
---
 drivers/mfd/Kconfig     |   8 +
 drivers/mfd/Makefile    |   1 +
 drivers/mfd/mud.c       | 580 ++++++++++++++++++++++++++++++++++++++++
 include/linux/mfd/mud.h |  16 ++
 4 files changed, 605 insertions(+)
 create mode 100644 drivers/mfd/mud.c
 create mode 100644 include/linux/mfd/mud.h

diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 52818dbcfe1f..9950794d907e 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -1968,6 +1968,14 @@ config MFD_STMFX
 	  additional drivers must be enabled in order to use the functionality
 	  of the device.
 
+config MFD_MUD
+	tristate "Multifunction USB Device core driver"
+	depends on USB
+	select MFD_CORE
+	select REGMAP_USB
+	help
+	  Select this to get support for the Multifunction USB Device.
+
 menu "Multimedia Capabilities Port drivers"
 	depends on ARCH_SA1100
 
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index 29e6767dd60c..0adfab9afaed 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -255,4 +255,5 @@ obj-$(CONFIG_MFD_ROHM_BD70528)	+= rohm-bd70528.o
 obj-$(CONFIG_MFD_ROHM_BD718XX)	+= rohm-bd718x7.o
 obj-$(CONFIG_MFD_STMFX) 	+= stmfx.o
 obj-$(CONFIG_MFD_RPISENSE_CORE)	+= rpisense-core.o
+obj-$(CONFIG_MFD_MUD)		+= mud.o
 
diff --git a/drivers/mfd/mud.c b/drivers/mfd/mud.c
new file mode 100644
index 000000000000..f5f31478656d
--- /dev/null
+++ b/drivers/mfd/mud.c
@@ -0,0 +1,580 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2020 Noralf Trønnes
+ */
+
+#include <linux/bitmap.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/list.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/mud.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/regmap_usb.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+/* Temporary debugging aid */
+#undef dev_dbg
+#define dev_dbg dev_info
+
+#define mdebug(fmt, ...)			\
+do {						\
+	if (1)					\
+		pr_debug(fmt, ##__VA_ARGS__);	\
+} while (0)
+
+struct mud_irq_event {
+	struct list_head node;
+	DECLARE_BITMAP(status, REGMAP_USB_MAX_MAPS);
+};
+
+struct mud_irq {
+	struct irq_domain *domain;
+	unsigned int num_irqs;
+
+	struct workqueue_struct	*workq;
+	struct work_struct work;
+	struct urb *urb;
+
+	spinlock_t lock; /* Protect the values below */
+	unsigned long *mask;
+	u16 tag;
+	struct list_head eventlist;
+
+	unsigned int stats_illegal;
+	unsigned int stats_already_seen;
+	unsigned int stats_lost;
+};
+
+struct mud_device {
+	struct usb_device *usb;
+	struct mud_irq *mirq;
+	struct mfd_cell *cells;
+	unsigned int num_cells;
+};
+
+static void mud_irq_work(struct work_struct *work)
+{
+	struct mud_irq *mirq = container_of(work, struct mud_irq, work);
+	struct mud_irq_event *event;
+	unsigned long n, flags;
+	unsigned int irq;
+
+	mdebug("%s: IN\n", __func__);
+
+	while (true) {
+		spin_lock_irqsave(&mirq->lock, flags);
+		event = list_first_entry_or_null(&mirq->eventlist, struct mud_irq_event, node);
+		if (event) {
+			list_del(&event->node);
+			mdebug("    status: %*pb\n", mirq->num_irqs, event->status);
+			bitmap_and(event->status, event->status, mirq->mask, mirq->num_irqs);
+		}
+		spin_unlock_irqrestore(&mirq->lock, flags);
+		if (!event)
+			break;
+
+		for_each_set_bit(n, event->status, mirq->num_irqs) {
+			irq = irq_find_mapping(mirq->domain, n);
+			mdebug("        n=%lu irq=%u\n", n, irq);
+			if (irq)
+				handle_nested_irq(irq);
+		}
+
+		kfree(event);
+	}
+
+	mdebug("%s: OUT\n", __func__);
+}
+
+#define BYTES_PER_LONG	(BITS_PER_LONG / BITS_PER_BYTE)
+
+static void mud_irq_queue(struct urb *urb)
+{
+	u8 *buf = urb->transfer_buffer + sizeof(u16);
+	struct mud_irq *mirq = urb->context;
+	struct device *dev = &urb->dev->dev;
+	struct mud_irq_event *event = NULL;
+	unsigned int i, tag, diff;
+	unsigned long flags;
+
+	if (urb->actual_length != urb->transfer_buffer_length) {
+		dev_err_once(dev, "Interrupt packet wrong length: %u\n",
+			     urb->actual_length);
+		mirq->stats_illegal++;
+		return;
+	}
+
+	spin_lock_irqsave(&mirq->lock, flags);
+
+	tag = le16_to_cpup(urb->transfer_buffer);
+	if (tag == mirq->tag) {
+		dev_dbg(dev, "Interrupt tag=%u already seen, ignoring\n", tag);
+		mirq->stats_already_seen++;
+		goto unlock;
+	}
+
+	if (tag > mirq->tag)
+		diff = tag - mirq->tag;
+	else
+		diff = U16_MAX - mirq->tag + tag + 1;
+
+	if (diff > 1) {
+		dev_err_once(dev, "Interrupts lost: %u\n", diff - 1);
+		mirq->stats_lost += diff - 1;
+	}
+
+	event = kzalloc(sizeof(*event), GFP_ATOMIC);
+	if (!event) {
+		mirq->stats_lost += 1;
+		goto unlock;
+	}
+
+	list_add_tail(&event->node, &mirq->eventlist);
+
+	for (i = 0; i < (urb->transfer_buffer_length - sizeof(u16)); i++) {
+		unsigned long *val = &event->status[i / BYTES_PER_LONG];
+		unsigned int mod = i % BYTES_PER_LONG;
+
+		if (!mod)
+			*val = buf[i];
+		else
+			*val |= ((unsigned long)buf[i]) << (mod * BITS_PER_BYTE);
+	}
+
+	mdebug("%s: tag=%u\n", __func__, tag);
+
+	mirq->tag = tag;
+unlock:
+	spin_unlock_irqrestore(&mirq->lock, flags);
+
+	if (event)
+		queue_work(mirq->workq, &mirq->work);
+}
+
+static void mud_irq_urb_completion(struct urb *urb)
+{
+	struct device *dev = &urb->dev->dev;
+	int ret;
+
+	mdebug("%s: actual_length=%u\n", __func__, urb->actual_length);
+
+	switch (urb->status) {
+	case 0:
+		mud_irq_queue(urb);
+		break;
+	case -EPROTO:	/* FIXME: verify: dwc2 reports this on disconnect */
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		dev_dbg(dev, "irq urb shutting down with status: %d\n", urb->status);
+		return;
+	default:
+		dev_dbg(dev, "irq urb failure with status: %d\n", urb->status);
+		break;
+	}
+
+	ret = usb_submit_urb(urb, GFP_ATOMIC);
+	if (ret && ret != -ENODEV)
+		dev_err(dev, "irq usb_submit_urb failed with result %d\n", ret);
+}
+
+static void mud_irq_mask(struct irq_data *data)
+{
+	struct mud_irq *mirq = irq_data_get_irq_chip_data(data);
+	unsigned long flags;
+
+	mdebug("%s: hwirq=%lu\n", __func__, data->hwirq);
+
+	spin_lock_irqsave(&mirq->lock, flags);
+	clear_bit(data->hwirq, mirq->mask);
+	spin_unlock_irqrestore(&mirq->lock, flags);
+}
+
+static void mud_irq_unmask(struct irq_data *data)
+{
+	struct mud_irq *mirq = irq_data_get_irq_chip_data(data);
+	unsigned long flags;
+
+	mdebug("%s: hwirq=%lu\n", __func__, data->hwirq);
+
+	spin_lock_irqsave(&mirq->lock, flags);
+	set_bit(data->hwirq, mirq->mask);
+	spin_unlock_irqrestore(&mirq->lock, flags);
+}
+
+static struct irq_chip mud_irq_chip = {
+	.name			= "mud-irq",
+	.irq_mask		= mud_irq_mask,
+	.irq_unmask		= mud_irq_unmask,
+};
+
+static void __maybe_unused
+mud_irq_domain_debug_show(struct seq_file *m, struct irq_domain *d,
+			  struct irq_data *data, int ind)
+{
+	struct mud_irq *mirq = d ? d->host_data : irq_data_get_irq_chip_data(data);
+	unsigned long flags;
+
+	spin_lock_irqsave(&mirq->lock, flags);
+
+	seq_printf(m, "%*sTag:          %u\n", ind, "", mirq->tag);
+	seq_printf(m, "%*sIllegal:      %u\n", ind, "", mirq->stats_illegal);
+	seq_printf(m, "%*sAlready seen: %u\n", ind, "", mirq->stats_already_seen);
+	seq_printf(m, "%*sLost:         %u\n", ind, "", mirq->stats_lost);
+
+	spin_unlock_irqrestore(&mirq->lock, flags);
+}
+
+static int mud_irq_domain_map(struct irq_domain *d, unsigned int virq,
+			      irq_hw_number_t hwirq)
+{
+	irq_set_chip_data(virq, d->host_data);
+	irq_set_chip_and_handler(virq, &mud_irq_chip, handle_simple_irq);
+	irq_set_nested_thread(virq, true);
+	irq_set_noprobe(virq);
+
+	return 0;
+}
+
+static void mud_irq_domain_unmap(struct irq_domain *d, unsigned int virq)
+{
+	irq_set_chip_and_handler(virq, NULL, NULL);
+	irq_set_chip_data(virq, NULL);
+}
+
+static const struct irq_domain_ops mud_irq_ops = {
+	.map		= mud_irq_domain_map,
+	.unmap		= mud_irq_domain_unmap,
+#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
+	.debug_show	= mud_irq_domain_debug_show,
+#endif
+};
+
+static int mud_irq_start(struct mud_irq *mirq)
+{
+	if (!mirq)
+		return 0;
+
+	return usb_submit_urb(mirq->urb, GFP_KERNEL);
+}
+
+static void mud_irq_stop(struct mud_irq *mirq)
+{
+	if (!mirq)
+		return;
+
+	usb_kill_urb(mirq->urb);
+	flush_work(&mirq->work);
+}
+
+static void mud_irq_release(struct mud_irq *mirq)
+{
+	if (!mirq)
+		return;
+
+	if (mirq->workq)
+		destroy_workqueue(mirq->workq);
+
+	if (mirq->domain) {
+		irq_hw_number_t hwirq;
+
+		for (hwirq = 0; hwirq < mirq->num_irqs; hwirq++)
+			irq_dispose_mapping(irq_find_mapping(mirq->domain, hwirq));
+
+		irq_domain_remove(mirq->domain);
+	}
+
+	usb_free_coherent(mirq->urb->dev, mirq->urb->transfer_buffer_length,
+			  mirq->urb->transfer_buffer, mirq->urb->transfer_dma);
+	usb_free_urb(mirq->urb);
+	bitmap_free(mirq->mask);
+	kfree(mirq);
+}
+
+static struct mud_irq *mud_irq_create(struct usb_interface *interface,
+				      unsigned int num_irqs)
+{
+	struct usb_device *usb = interface_to_usbdev(interface);
+	struct device *dev = &interface->dev;
+	struct usb_endpoint_descriptor *ep;
+	struct fwnode_handle *fn;
+	struct urb *urb = NULL;
+	struct mud_irq *mirq;
+	void *buf = NULL;
+	size_t buf_size;
+	int ret;
+
+	mdebug("%s: dev->id=%d\n", __func__, dev->id);
+
+	ret = usb_find_int_in_endpoint(interface->cur_altsetting, &ep);
+	if (ret == -ENXIO)
+		return NULL;
+	if (ret)
+		return ERR_PTR(ret);
+
+	mirq = kzalloc(sizeof(*mirq), GFP_KERNEL);
+	if (!mirq)
+		return ERR_PTR(-ENOMEM);
+
+	mirq->mask = bitmap_zalloc(num_irqs, GFP_KERNEL);
+	if (!mirq->mask) {
+		ret = -ENOMEM;
+		goto release;
+	}
+
+	spin_lock_init(&mirq->lock);
+	mirq->num_irqs = num_irqs;
+
+	urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!urb) {
+		ret = -ENOMEM;
+		goto release;
+	}
+
+	buf_size = usb_endpoint_maxp(ep);
+	if (buf_size != (sizeof(u16) + DIV_ROUND_UP(num_irqs, BITS_PER_BYTE))) {
+		dev_err(dev, "Interrupt endpoint wMaxPacketSize too small: %zu\n", buf_size);
+		ret = -EINVAL;
+		goto release;
+	}
+
+	buf = usb_alloc_coherent(usb, buf_size, GFP_KERNEL, &urb->transfer_dma);
+	if (!buf) {
+		usb_free_urb(urb);
+		ret = -ENOMEM;
+		goto release;
+	}
+
+	usb_fill_int_urb(urb, usb,
+			 usb_rcvintpipe(usb, usb_endpoint_num(ep)),
+			 buf, buf_size, mud_irq_urb_completion,
+			 mirq, ep->bInterval);
+	urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	mirq->urb = urb;
+
+	if (dev->of_node) {
+		fn = of_node_to_fwnode(dev->of_node);
+	} else {
+		fn = irq_domain_alloc_named_fwnode("mud-irq");
+		if (!fn) {
+			ret = -ENOMEM;
+			goto release;
+		}
+	}
+
+	mirq->domain = irq_domain_create_linear(fn, num_irqs, &mud_irq_ops, mirq);
+	if (!dev->of_node)
+		irq_domain_free_fwnode(fn);
+	if (!mirq->domain) {
+		ret = -ENOMEM;
+		goto release;
+	}
+
+	INIT_LIST_HEAD(&mirq->eventlist);
+	INIT_WORK(&mirq->work, mud_irq_work);
+
+	mirq->workq = alloc_workqueue("mud-irq/%s", WQ_HIGHPRI, 0, dev_name(dev));
+	if (!mirq->workq) {
+		ret = -ENOMEM;
+		goto release;
+	}
+
+	return mirq;
+
+release:
+	mud_irq_release(mirq);
+
+	return ERR_PTR(ret);
+}
+
+static int mud_probe_regmap(struct usb_interface *interface, struct mfd_cell *cell,
+			    unsigned int index, struct mud_irq *mirq)
+{
+	struct mud_cell_pdata *pdata;
+	struct resource *res = NULL;
+	int ret;
+
+	pdata = kzalloc(sizeof(*pdata), GFP_KERNEL);
+	if (!pdata)
+		return -ENOMEM;
+
+	ret = regmap_usb_get_map_descriptor(interface, index, &pdata->desc);
+	if (ret)
+		goto error;
+
+	mdebug("%s: name='%s' index=%u\n", __func__, pdata->desc.name, index);
+	mdebug("    bRegisterValueBits=%u\n", pdata->desc.bRegisterValueBits);
+	mdebug("    bCompression=0x%02x\n", pdata->desc.bCompression);
+	mdebug("    bMaxTransferSizeOrder=%u (%ukB)\n",
+	       pdata->desc.bMaxTransferSizeOrder,
+	       (1 << pdata->desc.bMaxTransferSizeOrder) / 1024);
+
+	if (mirq) {
+		res = kzalloc(sizeof(*res), GFP_KERNEL);
+		if (!res) {
+			ret = -ENOMEM;
+			goto error;
+		}
+
+		res->flags = IORESOURCE_IRQ;
+		res->start = irq_create_mapping(mirq->domain, index);
+		mdebug("    res->start=%u\n", (unsigned int)res->start);
+		res->end = res->start;
+
+		cell->resources = res;
+		cell->num_resources = 1;
+	}
+
+	pdata->interface = interface;
+	pdata->index = index;
+	cell->name = pdata->desc.name;
+	cell->platform_data = pdata;
+	cell->pdata_size = sizeof(*pdata);
+	/*
+	 * A Multifunction USB Device can have multiple functions of the same
+	 * type. mfd_add_device() in its current form will only match on the
+	 * first node in the Device Tree.
+	 */
+	cell->of_compatible = cell->name;
+
+	return 0;
+
+error:
+	kfree(res);
+	kfree(pdata);
+
+	return ret;
+}
+
+static void mud_free(struct mud_device *mud)
+{
+	unsigned int i;
+
+	mud_irq_release(mud->mirq);
+
+	for (i = 0; i < mud->num_cells; i++) {
+		kfree(mud->cells[i].platform_data);
+		kfree(mud->cells[i].resources);
+	}
+
+	kfree(mud->cells);
+	kfree(mud);
+}
+
+static int mud_probe(struct usb_interface *interface,
+		     const struct usb_device_id *id)
+{
+	struct device *dev = &interface->dev;
+	unsigned int i, num_regmaps;
+	struct mud_device *mud;
+	int ret;
+
+	mdebug("%s: interface->dev.of_node=%px usb->dev.of_node=%px",
+	       __func__, interface->dev.of_node,
+	       usb_get_dev(interface_to_usbdev(interface))->dev.of_node);
+
+	ret = regmap_usb_get_interface_descriptor(interface, &num_regmaps);
+	if (ret)
+		return ret;
+	if (!num_regmaps)
+		return -EINVAL;
+
+	mdebug("%s: num_regmaps=%u\n", __func__, num_regmaps);
+
+	mud = kzalloc(sizeof(*mud), GFP_KERNEL);
+	if (!mud)
+		return -ENOMEM;
+
+	mud->mirq = mud_irq_create(interface, num_regmaps);
+	if (IS_ERR(mud->mirq)) {
+		ret = PTR_ERR(mud->mirq);
+		goto err_free;
+	}
+
+	mud->num_cells = num_regmaps;
+	mud->cells = kcalloc(num_regmaps, sizeof(*mud->cells), GFP_KERNEL);
+	if (!mud->cells)
+		goto err_free;
+
+	for (i = 0; i < num_regmaps; i++) {
+		ret = mud_probe_regmap(interface, &mud->cells[i], i, mud->mirq);
+		if (ret) {
+			dev_err(dev, "Failed to probe regmap index %i (error %d)\n", i, ret);
+			goto err_free;
+		}
+	}
+
+	ret = mud_irq_start(mud->mirq);
+	if (ret) {
+		dev_err(dev, "Failed to start irq (error %d)\n", ret);
+		goto err_free;
+	}
+
+	ret = mfd_add_hotplug_devices(dev, mud->cells, mud->num_cells);
+	if (ret) {
+		dev_err(dev, "Failed to add mfd devices to core.");
+		goto err_stop;
+	}
+
+	mud->usb = usb_get_dev(interface_to_usbdev(interface));
+
+	usb_set_intfdata(interface, mud);
+
+	if (mud->usb->product)
+		dev_info(dev, "%s\n", mud->usb->product);
+
+	return 0;
+
+err_stop:
+	mud_irq_stop(mud->mirq);
+err_free:
+	mud_free(mud);
+
+	return ret;
+}
+
+static void mud_disconnect(struct usb_interface *interface)
+{
+	struct mud_device *mud = usb_get_intfdata(interface);
+
+	mfd_remove_devices(&interface->dev);
+	mud_irq_stop(mud->mirq);
+	usb_put_dev(mud->usb);
+	mud_free(mud);
+
+	dev_dbg(&interface->dev, "disconnected\n");
+}
+
+static const struct usb_device_id mud_table[] = {
+	/*
+	 * FIXME:
+	 * Apply for a proper pid: https://github.com/openmoko/openmoko-usb-oui
+	 *
+	 * Or maybe the Linux Foundation will provide one from their vendor id.
+	 */
+	{ USB_DEVICE_INTERFACE_CLASS(0x1d50, 0x6150, USB_CLASS_VENDOR_SPEC) },
+	{ }
+};
+
+MODULE_DEVICE_TABLE(usb, mud_table);
+
+static struct usb_driver mud_driver = {
+	.name		= "mud",
+	.probe		= mud_probe,
+	.disconnect	= mud_disconnect,
+	.id_table	= mud_table,
+};
+
+module_usb_driver(mud_driver);
+
+MODULE_DESCRIPTION("Generic USB Device mfd core driver");
+MODULE_AUTHOR("Noralf Trønnes <noralf@xxxxxxxxxxx>");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/mud.h b/include/linux/mfd/mud.h
new file mode 100644
index 000000000000..b2059fa57429
--- /dev/null
+++ b/include/linux/mfd/mud.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __LINUX_MUD_H
+#define __LINUX_MUD_H
+
+#include <linux/regmap_usb.h>
+
+struct usb_interface;
+
+struct mud_cell_pdata {
+	struct usb_interface *interface;
+	unsigned int index;
+	struct regmap_usb_map_descriptor desc;
+};
+
+#endif
-- 
2.23.0




[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux