[RFC 2/9] mfd: Add driver for Multifunction USB Device
Noralf Trønnes
noralf at tronnes.org
Sun Feb 16 17:21:10 UTC 2020
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 at tronnes.org>
---
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 at tronnes.org>");
+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
More information about the dri-devel
mailing list