@@ -4,7 +4,7 @@
# subsystems should select the appropriate symbols.
config REGMAP
- default y if (REGMAP_I2C || REGMAP_SPI || REGMAP_SPMI || REGMAP_W1 || REGMAP_AC97 || REGMAP_MMIO || REGMAP_IRQ || REGMAP_SCCB || REGMAP_I3C)
+ default y if (REGMAP_I2C || REGMAP_SPI || REGMAP_SPMI || REGMAP_W1 || REGMAP_AC97 || REGMAP_MMIO || REGMAP_IRQ || REGMAP_SCCB || REGMAP_I3C || REGMAP_USB)
select IRQ_DOMAIN if REGMAP_IRQ
bool
@@ -53,3 +53,9 @@ config REGMAP_SCCB
config REGMAP_I3C
tristate
depends on I3C
+
+config REGMAP_USB
+ tristate
+ depends on USB
+ select LZ4_COMPRESS
+ select LZ4_DECOMPRESS
@@ -17,3 +17,4 @@ obj-$(CONFIG_REGMAP_W1) += regmap-w1.o
obj-$(CONFIG_REGMAP_SOUNDWIRE) += regmap-sdw.o
obj-$(CONFIG_REGMAP_SCCB) += regmap-sccb.o
obj-$(CONFIG_REGMAP_I3C) += regmap-i3c.o
+obj-$(CONFIG_REGMAP_USB) += regmap-usb.o
new file mode 100644
@@ -0,0 +1,1026 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Register map access API - USB support
+ *
+ * Copyright 2020 Noralf Trønnes
+ */
+
+#include <linux/debugfs.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/list.h>
+#include <linux/lz4.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 "internal.h"
+
+/**
+ * DOC: overview
+ *
+ * This regmap over USB supports multiple regmaps over a single USB interface.
+ * Two endpoints are needed and the first IN and OUT endpoints are used.
+ * A REGMAP_USB_DT_INTERFACE descriptor request is issued to get the number of
+ * regmaps supported on the interface. A REGMAP_USB_DT_MAP descriptor request is
+ * issued to get details about a specific regmap. This is done when
+ * devm_regmap_init_usb() is called to get access to a regmap.
+ *
+ * A regmap transfer begins with the host sending OUT a ®map_usb_header which
+ * contains info about the index of the regmap, the register address etc. Next
+ * it does an IN or OUT transfer of the register value(s) depending on if it's a
+ * read or write. This transfer can be compressed using lz4 if the device
+ * supports it. Finally a ®map_usb_status IN request is issued to receive the
+ * status of the transfer.
+ *
+ * If a transfer fails with the error code -EPIPE, a reset control request
+ * (REGMAP_USB_REQ_PROTOCOL_RESET) is issued. The device should reset it's state
+ * machine and return its previous error code if any. The device can halt its
+ * IN/OUT endpoints to force the host to perform a reset if it fails to
+ * understand a transfer.
+ */
+
+/* Provides exclusive interface access */
+struct regmap_usb_interface {
+ struct usb_interface *interface;
+ struct mutex lock; /* Ensures exclusive interface access */
+ unsigned int refcount;
+ struct list_head link;
+
+ u32 tag;
+};
+
+struct regmap_usb_context;
+
+struct regmap_usb_transfer {
+ struct regmap_usb_context *ctx;
+ struct usb_anchor anchor;
+ struct urb *header_urb;
+ struct urb *buf_out_urb;
+ struct urb *buf_in_urb;
+ void *buf;
+ size_t bufsize;
+ struct urb *status_urb;
+ spinlock_t lock; /* Protect dynamic values */
+ u32 tag;
+ int status;
+
+ u8 compression;
+ void *buf_in_dest;
+ unsigned int length;
+ unsigned int actual_length;
+
+ ktime_t start; /* FIXME: Temporary debug/perf aid */
+};
+
+struct regmap_usb_context {
+ struct usb_device *usb;
+ struct regmap_usb_interface *ruif;
+ u8 ifnum;
+ unsigned int in_pipe;
+ unsigned int out_pipe;
+ u16 index;
+ unsigned int val_bytes;
+ void *lz4_comp_mem;
+ u8 compression;
+ unsigned int max_transfer_size;
+ struct regmap_usb_transfer *transfers[2];
+#ifdef CONFIG_DEBUG_FS
+ u64 stats_length;
+ u64 stats_actual_length;
+ unsigned int num_resets;
+ unsigned int num_errors;
+#endif
+};
+
+/* FIXME: Temporary debugging aid */
+static unsigned int debug = 8;
+
+#define udebug(level, fmt, ...) \
+do { \
+ if ((level) <= debug) \
+ pr_debug(fmt, ##__VA_ARGS__); \
+} while (0)
+
+static LIST_HEAD(regmap_usb_interfaces);
+static DEFINE_MUTEX(regmap_usb_interfaces_lock);
+
+static struct regmap_usb_interface *regmap_usb_interface_get(struct usb_interface *interface)
+{
+ struct regmap_usb_interface *ruif, *entry;
+
+ mutex_lock(®map_usb_interfaces_lock);
+ list_for_each_entry(entry, ®map_usb_interfaces, link)
+ if (entry->interface == interface) {
+ ruif = entry;
+ ruif->refcount++;
+ goto out_unlock;
+ }
+
+ ruif = kzalloc(sizeof(*ruif), GFP_KERNEL);
+ if (!ruif) {
+ ruif = ERR_PTR(-ENOMEM);
+ goto out_unlock;
+ }
+
+ mutex_init(&ruif->lock);
+ ruif->interface = interface;
+ ruif->refcount++;
+ list_add(&ruif->link, ®map_usb_interfaces);
+out_unlock:
+ mutex_unlock(®map_usb_interfaces_lock);
+
+ return ruif;
+}
+
+static void regmap_usb_interface_put(struct regmap_usb_interface *ruif)
+{
+ mutex_lock(®map_usb_interfaces_lock);
+ if (--ruif->refcount)
+ goto out_unlock;
+
+ list_del(&ruif->link);
+ mutex_destroy(&ruif->lock);
+ kfree(ruif);
+out_unlock:
+ mutex_unlock(®map_usb_interfaces_lock);
+}
+
+#ifdef CONFIG_DEBUG_FS
+static void regmap_usb_stats_add_length(struct regmap_usb_context *ctx, unsigned int len)
+{
+ ctx->stats_length += len;
+ /* Did it wrap around? */
+ if (ctx->stats_length <= len && ctx->stats_actual_length) {
+ ctx->stats_length = len;
+ ctx->stats_actual_length = 0;
+ }
+}
+
+#define regmap_usb_stats_add(c, v) \
+ (c) += v
+#else
+static void regmap_usb_stats_add_length(struct regmap_usb_context *ctx, unsigned int len)
+{
+}
+
+#define regmap_usb_stats_add(c, v)
+#endif
+
+static int regmap_usb_protocol_reset(struct regmap_usb_context *ctx)
+{
+ u8 *prev_errno;
+ int ret;
+
+ regmap_usb_stats_add(ctx->num_resets, 1);
+
+ prev_errno = kmalloc(1, GFP_ATOMIC);
+ if (!prev_errno)
+ return -ENOMEM;
+
+ usb_clear_halt(ctx->usb, ctx->out_pipe);
+ usb_clear_halt(ctx->usb, ctx->in_pipe);
+
+ ret = usb_control_msg(ctx->usb, usb_rcvctrlpipe(ctx->usb, 0),
+ REGMAP_USB_REQ_PROTOCOL_RESET,
+ USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_INTERFACE,
+ 0, ctx->ifnum, prev_errno, 1,
+ USB_CTRL_SET_TIMEOUT);
+ udebug(0, "%s: ret=%d, prev_errno=%u\n", __func__, ret, *prev_errno);
+ if (ret < 0 || ret != 1) {
+ /* FIXME: Try a USB port reset? */
+ ret = -EPIPE;
+ goto free;
+ }
+
+ ret = *prev_errno;
+free:
+ kfree(prev_errno);
+
+ return ret ? -ret : -EPIPE;
+}
+
+static void regmap_usb_header_urb_completion(struct urb *urb)
+{
+ struct regmap_usb_transfer *transfer = urb->context;
+ unsigned long flags;
+
+ spin_lock_irqsave(&transfer->lock, flags);
+ if (urb->status)
+ transfer->status = urb->status;
+ else if (urb->actual_length != urb->transfer_buffer_length)
+ transfer->status = -EREMOTEIO;
+ transfer->start = ktime_get();
+ spin_unlock_irqrestore(&transfer->lock, flags);
+
+ udebug(4, "%s: transfer: status=%d (%d), tag=%u\n",
+ __func__, urb->status, transfer->status, transfer->tag);
+}
+
+static void regmap_usb_status_urb_completion(struct urb *urb)
+{
+ struct regmap_usb_status *status = urb->transfer_buffer;
+ struct regmap_usb_transfer *transfer = urb->context;
+ unsigned long flags;
+ int stat;
+
+ udebug(4, "%s: urb->status=%d, signature=0x%x, tag=%u (expected %u)\n",
+ __func__, urb->status, le32_to_cpu(status->signature),
+ le16_to_cpu(status->tag), transfer->tag);
+
+ if (urb->status)
+ stat = urb->status;
+ else if (urb->actual_length != urb->transfer_buffer_length)
+ stat = -EREMOTEIO;
+ else if (le32_to_cpu(status->signature) != REGMAP_USB_STATUS_SIGNATURE ||
+ le16_to_cpu(status->tag) != transfer->tag)
+ stat = -EBADMSG;
+ else
+ stat = -status->status;
+
+ spin_lock_irqsave(&transfer->lock, flags);
+ if (!transfer->status)
+ transfer->status = stat;
+ spin_unlock_irqrestore(&transfer->lock, flags);
+}
+
+static long mud_drm_throughput(ktime_t begin, ktime_t end, size_t len)
+{
+ long throughput;
+
+ throughput = ktime_us_delta(end, begin);
+ throughput = throughput ? (len * 1000) / throughput : 0;
+ throughput = throughput * 1000 / 1024;
+
+ return throughput;
+}
+
+static void regmap_usb_buf_in_urb_completion(struct urb *urb)
+{
+ struct regmap_usb_transfer *transfer = urb->context;
+ unsigned long flags;
+ ktime_t start, end;
+
+ spin_lock_irqsave(&transfer->lock, flags);
+ if (urb->status && !transfer->status)
+ transfer->status = urb->status;
+ transfer->actual_length = urb->actual_length;
+ start = transfer->start;
+ spin_unlock_irqrestore(&transfer->lock, flags);
+
+ end = ktime_get();
+
+ udebug(4, "%s: IN: status=%d, tag=%u, %ld kB/s (%lld ms), len=%u\n",
+ __func__, urb->status, transfer->tag,
+ mud_drm_throughput(start, end, urb->actual_length),
+ ktime_ms_delta(end, start), urb->actual_length);
+}
+
+static void regmap_usb_buf_out_urb_completion(struct urb *urb)
+{
+ struct regmap_usb_transfer *transfer = urb->context;
+ unsigned long flags;
+ ktime_t start, end;
+
+ spin_lock_irqsave(&transfer->lock, flags);
+ if (!transfer->status) {
+ if (urb->status)
+ transfer->status = urb->status;
+ else if (urb->actual_length != urb->transfer_buffer_length)
+ transfer->status = -EREMOTEIO;
+ }
+ start = transfer->start;
+ spin_unlock_irqrestore(&transfer->lock, flags);
+
+ end = ktime_get();
+
+ udebug(4, "%s: OUT: status=%d, tag=%u, %ld kB/s (%lld ms), len=%u\n",
+ __func__, transfer->status, transfer->tag,
+ mud_drm_throughput(start, end, urb->transfer_buffer_length),
+ ktime_ms_delta(end, start), urb->transfer_buffer_length);
+}
+
+static struct urb *regmap_usb_alloc_urb(struct usb_device *usb, unsigned int pipe,
+ size_t size, usb_complete_t complete_fn,
+ struct regmap_usb_transfer *transfer)
+{
+ void *buf = NULL;
+ struct urb *urb;
+
+ urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!urb)
+ return NULL;
+
+ if (size) {
+ buf = usb_alloc_coherent(usb, size, GFP_KERNEL, &urb->transfer_dma);
+ if (!buf) {
+ usb_free_urb(urb);
+ return NULL;
+ }
+ }
+
+ usb_fill_bulk_urb(urb, usb, pipe, buf, size, complete_fn, transfer);
+ if (size)
+ urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ return urb;
+}
+
+static void regmap_usb_free_transfer(struct regmap_usb_transfer *transfer)
+{
+ struct urb *urb;
+
+ if (!transfer)
+ return;
+
+ urb = transfer->header_urb;
+ if (urb)
+ usb_free_coherent(urb->dev, urb->transfer_buffer_length,
+ urb->transfer_buffer, urb->transfer_dma);
+ usb_free_urb(urb);
+
+ urb = transfer->status_urb;
+ if (urb)
+ usb_free_coherent(urb->dev, urb->transfer_buffer_length,
+ urb->transfer_buffer, urb->transfer_dma);
+ usb_free_urb(urb);
+
+ usb_free_urb(transfer->buf_in_urb);
+ usb_free_urb(transfer->buf_out_urb);
+ kfree(transfer->buf);
+ kfree(transfer);
+}
+
+static struct regmap_usb_transfer *regmap_usb_alloc_transfer(struct regmap_usb_context *ctx)
+{
+ struct regmap_usb_transfer *transfer;
+
+ transfer = kzalloc(sizeof(*transfer), GFP_KERNEL);
+ if (!transfer)
+ return NULL;
+
+ init_usb_anchor(&transfer->anchor);
+ spin_lock_init(&transfer->lock);
+ transfer->ctx = ctx;
+
+ transfer->header_urb = regmap_usb_alloc_urb(ctx->usb, ctx->out_pipe,
+ sizeof(struct regmap_usb_header),
+ regmap_usb_header_urb_completion,
+ transfer);
+ if (!transfer->header_urb)
+ goto error;
+
+ transfer->status_urb = regmap_usb_alloc_urb(ctx->usb, ctx->in_pipe,
+ sizeof(struct regmap_usb_status),
+ regmap_usb_status_urb_completion,
+ transfer);
+ if (!transfer->status_urb)
+ goto error;
+
+ transfer->buf_in_urb = regmap_usb_alloc_urb(ctx->usb, ctx->in_pipe, 0,
+ regmap_usb_buf_in_urb_completion,
+ transfer);
+ if (!transfer->buf_in_urb)
+ goto error;
+
+ transfer->buf_out_urb = regmap_usb_alloc_urb(ctx->usb, ctx->out_pipe, 0,
+ regmap_usb_buf_out_urb_completion,
+ transfer);
+ if (!transfer->buf_out_urb)
+ goto error;
+
+ transfer->bufsize = ctx->max_transfer_size;
+retry:
+ transfer->buf = kmalloc(transfer->bufsize, GFP_KERNEL);
+ if (!transfer->buf) {
+ if (transfer->bufsize < 32) /* Give up */
+ goto error;
+
+ transfer->bufsize /= 2;
+ goto retry;
+ }
+
+ return transfer;
+
+error:
+ regmap_usb_free_transfer(transfer);
+
+ return NULL;
+}
+
+static void regmap_usb_free_transfers(struct regmap_usb_context *ctx)
+{
+ regmap_usb_free_transfer(ctx->transfers[0]);
+ regmap_usb_free_transfer(ctx->transfers[1]);
+}
+
+static int regmap_usb_alloc_transfers(struct regmap_usb_context *ctx)
+{
+ ctx->transfers[0] = regmap_usb_alloc_transfer(ctx);
+ ctx->transfers[1] = regmap_usb_alloc_transfer(ctx);
+ if (!ctx->transfers[0] || !ctx->transfers[1]) {
+ regmap_usb_free_transfers(ctx);
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static int regmap_usb_submit_urb(struct urb *urb, struct regmap_usb_transfer *transfer)
+{
+ int ret;
+
+ usb_anchor_urb(urb, &transfer->anchor);
+ ret = usb_submit_urb(urb, GFP_KERNEL);
+ if (ret)
+ usb_unanchor_urb(urb);
+
+ return ret;
+}
+
+static void regmap_usb_kill_transfers(struct regmap_usb_context *ctx)
+{
+ usb_kill_anchored_urbs(&ctx->transfers[0]->anchor);
+ usb_kill_anchored_urbs(&ctx->transfers[1]->anchor);
+}
+
+static int regmap_usb_submit_transfer(struct regmap_usb_transfer *transfer,
+ unsigned int regnr, u32 flags, void *buf, size_t len)
+{
+ struct regmap_usb_context *ctx = transfer->ctx;
+ struct regmap_usb_header *header;
+ struct urb *urb;
+ int ret;
+
+ spin_lock_irq(&transfer->lock);
+ transfer->actual_length = 0;
+ transfer->status = 0;
+ transfer->tag = ++ctx->ruif->tag;
+ spin_unlock_irq(&transfer->lock);
+
+ udebug(3, "%s: regnr=0x%x, in=%u flags=0x%x, len=%zu, transfer->buf=%s, tag=%u\n",
+ __func__, regnr, !!(flags & REGMAP_USB_HEADER_FLAG_IN), flags, len,
+ buf == transfer->buf ? "yes" : "no", ctx->ruif->tag);
+
+ header = transfer->header_urb->transfer_buffer;
+ header->signature = cpu_to_le32(REGMAP_USB_HEADER_SIGNATURE);
+ header->index = cpu_to_le16(ctx->index);
+ header->tag = cpu_to_le16(ctx->ruif->tag);
+ header->flags = cpu_to_le32(flags);
+ header->regnr = cpu_to_le32(regnr);
+ header->length = cpu_to_le32(len);
+
+ ret = regmap_usb_submit_urb(transfer->header_urb, transfer);
+ if (ret)
+ goto error;
+
+ if (flags & REGMAP_USB_HEADER_FLAG_IN)
+ urb = transfer->buf_in_urb;
+ else
+ urb = transfer->buf_out_urb;
+
+ urb->transfer_buffer = buf;
+ urb->transfer_buffer_length = len;
+
+ ret = regmap_usb_submit_urb(urb, transfer);
+ if (ret)
+ goto error;
+
+ ret = regmap_usb_submit_urb(transfer->status_urb, transfer);
+ if (ret)
+ goto error;
+
+ return 0;
+
+error:
+ regmap_usb_kill_transfers(ctx);
+
+ return ret;
+}
+
+static int regmap_usb_wait_anchor(struct regmap_usb_transfer *transfer)
+{
+ int remain;
+
+ remain = usb_wait_anchor_empty_timeout(&transfer->anchor, 5000);
+ if (!remain) {
+ /* Kill pending first */
+ if (transfer == transfer->ctx->transfers[0])
+ usb_kill_anchored_urbs(&transfer->ctx->transfers[1]->anchor);
+ else
+ usb_kill_anchored_urbs(&transfer->ctx->transfers[0]->anchor);
+ usb_kill_anchored_urbs(&transfer->anchor);
+
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+static int regmap_usb_transfer_decompress(struct regmap_usb_transfer *transfer)
+{
+ unsigned int length, actual_length;
+ u8 compression;
+ void *dest;
+ int ret;
+
+ spin_lock_irq(&transfer->lock);
+ length = transfer->buf_in_urb->transfer_buffer_length;
+ actual_length = transfer->actual_length;
+ compression = transfer->compression;
+ transfer->compression = 0;
+ dest = transfer->buf_in_dest;
+ spin_unlock_irq(&transfer->lock);
+
+ udebug(3, "%s: dest=%px length=%u actual_length=%u\n",
+ __func__, dest, length, actual_length);
+
+ if (!actual_length) /* This transfer has not been used */
+ return 0;
+
+ if (!length) /* FIXME: necessary? */
+ return -EINVAL;
+
+ regmap_usb_stats_add(transfer->ctx->stats_actual_length, actual_length);
+
+ if (!compression) {
+ if (actual_length != length)
+ return -EREMOTEIO;
+
+ return 0;
+ }
+
+ if (actual_length == length) { /* Device did not compress */
+ memcpy(dest, transfer->buf, length);
+ return 0;
+ }
+
+ if (compression & REGMAP_USB_COMPRESSION_LZ4) {
+ ret = LZ4_decompress_safe(transfer->buf, dest,
+ actual_length, transfer->bufsize);
+ udebug(3, " decompress: ret=%d\n", ret);
+ } else {
+ return -EINVAL;
+ }
+
+ if (ret < 0 || ret != length)
+ return -EREMOTEIO;
+
+ return 0;
+}
+
+static int regmap_usb_transfer(struct regmap_usb_context *ctx, bool in,
+ const void *reg, void *buf, size_t len)
+{
+ struct regmap_usb_transfer *transfer = NULL;
+ unsigned int i, regnr, actual_length;
+ size_t chunk, trlen, complen = 0;
+ size_t orglen = len;
+ ktime_t start, end;
+ void *trbuf;
+ u32 flags;
+ int ret;
+
+ regnr = *(u32 *)reg;
+
+ for (i = 0; i < 2; i++) {
+ struct regmap_usb_transfer *transfer = ctx->transfers[i];
+
+ spin_lock_irq(&transfer->lock);
+ transfer->actual_length = 0;
+ transfer->compression = 0;
+ transfer->status = 0;
+ spin_unlock_irq(&transfer->lock);
+ }
+
+ /* FIXME: This did not work */
+ /* Use 2 transfers to maximize compressed transfers */
+ if (0 && ctx->compression &&
+ ctx->transfers[0]->bufsize == ctx->transfers[1]->bufsize &&
+ len > 128 && len <= ctx->transfers[0]->bufsize)
+ complen = len / 2;
+
+ mutex_lock(&ctx->ruif->lock);
+
+ udebug(2, "\n%s: regnr=0x%x, in=%u len=%zu, buf=%px, is_vmalloc=%u\n",
+ __func__, regnr, in, len, buf, is_vmalloc_addr(buf));
+
+ start = ktime_get();
+
+ i = 0;
+ while (len) {
+ transfer = ctx->transfers[i];
+ i = !i;
+
+ chunk = min(complen ? complen : transfer->bufsize, len);
+ trlen = chunk;
+ flags = 0;
+
+ regmap_usb_stats_add_length(ctx, chunk);
+
+ ret = regmap_usb_wait_anchor(transfer);
+ if (ret) {
+ udebug(0, "FAIL first wait %d\n", ret);
+ goto error;
+ }
+
+ spin_lock_irq(&transfer->lock);
+ ret = transfer->status;
+ actual_length = transfer->actual_length;
+ spin_unlock_irq(&transfer->lock);
+ if (ret) {
+ udebug(0, "FAIL transfer %d\n", ret);
+ goto error;
+ }
+
+ if (in && ctx->compression && actual_length) {
+ ret = regmap_usb_transfer_decompress(transfer);
+ if (ret)
+ goto error;
+ }
+
+ trbuf = buf;
+
+ if (!in) {
+ ret = 0;
+ /* LZ4_minLength = 13, use the next power of two value */
+ if (ctx->compression & REGMAP_USB_COMPRESSION_LZ4 && chunk >= 16) {
+ ret = LZ4_compress_default(buf, transfer->buf, chunk,
+ chunk, ctx->lz4_comp_mem);
+ udebug(3, " compress[%u](chunk=%zu): ret=%d\n", !i, chunk, ret);
+ }
+ if (ret > 0) {
+ flags |= REGMAP_USB_COMPRESSION_LZ4;
+ trbuf = transfer->buf;
+ trlen = ret;
+ } else if (is_vmalloc_addr(buf)) {
+ memcpy(transfer->buf, buf, chunk);
+ trbuf = transfer->buf;
+ }
+ regmap_usb_stats_add(ctx->stats_actual_length, trlen);
+ } else {
+ flags |= REGMAP_USB_HEADER_FLAG_IN;
+ if (ctx->compression & REGMAP_USB_COMPRESSION_LZ4 && trlen >= 16) {
+ flags |= REGMAP_USB_COMPRESSION_LZ4;
+ trbuf = transfer->buf;
+
+ spin_lock_irq(&transfer->lock);
+ transfer->compression = REGMAP_USB_COMPRESSION_LZ4;
+ transfer->buf_in_dest = buf;
+ spin_unlock_irq(&transfer->lock);
+ }
+ }
+
+ ret = regmap_usb_submit_transfer(transfer, regnr, flags, trbuf, trlen);
+ if (ret) {
+ udebug(0, "FAIL submit %d\n", ret);
+ goto error;
+ }
+
+ len -= chunk;
+ buf += chunk;
+ regnr += chunk / ctx->val_bytes;
+ }
+
+ ret = regmap_usb_wait_anchor(transfer);
+ if (ret) {
+ udebug(0, "FAIL second wait%d\n", ret);
+ goto error;
+ }
+
+ for (i = 0; i < 2; i++) {
+ struct regmap_usb_transfer *transfer = ctx->transfers[i];
+
+ spin_lock_irq(&transfer->lock);
+ ret = transfer->status;
+ spin_unlock_irq(&transfer->lock);
+ if (ret) {
+ udebug(0, "FAIL transfer2[%u] %d\n", i, ret);
+ goto error;
+ }
+ }
+
+ if (in && ctx->compression) {
+ ret = regmap_usb_transfer_decompress(ctx->transfers[0]);
+ if (ret)
+ goto error;
+ ret = regmap_usb_transfer_decompress(ctx->transfers[1]);
+ }
+
+error:
+ /*
+ * FIXME: What errors should warrant a reset?
+ * Verify that the DOC section is correct.
+ */
+ if (ret == -EPIPE || ret == -ETIMEDOUT)
+ ret = regmap_usb_protocol_reset(ctx);
+
+ if (ret)
+ regmap_usb_stats_add(ctx->num_errors, 1);
+
+ if (debug >= 2) {
+ end = ktime_get();
+ pr_debug("%s: ret=%d %ld kB/s (%lld ms)\n", __func__, ret,
+ mud_drm_throughput(start, end, orglen),
+ ktime_ms_delta(end, start));
+ }
+
+ mutex_unlock(&ctx->ruif->lock);
+
+ return ret;
+}
+
+static int regmap_usb_gather_write(void *context,
+ const void *reg, size_t reg_len,
+ const void *val, size_t val_len)
+{
+ return regmap_usb_transfer(context, false, reg, (void *)val, val_len);
+}
+
+static int regmap_usb_write(void *context, const void *data, size_t count)
+{
+ struct regmap_usb_context *ctx = context;
+ size_t val_len = count - sizeof(u32);
+ void *val;
+ int ret;
+
+ /* buffer needs to be properly aligned for DMA use */
+ val = kmemdup(data + sizeof(u32), val_len, GFP_KERNEL);
+ if (!val)
+ return -ENOMEM;
+
+ ret = regmap_usb_gather_write(ctx, data, sizeof(u32), val, val_len);
+ kfree(val);
+
+ return ret;
+}
+
+static int regmap_usb_read(void *context, const void *reg_buf, size_t reg_size,
+ void *val_buf, size_t val_size)
+{
+ return regmap_usb_transfer(context, true, reg_buf, val_buf, val_size);
+}
+
+static void regmap_usb_free_context(void *context)
+{
+ struct regmap_usb_context *ctx = context;
+
+ udebug(1, "%s:\n", __func__);
+
+ regmap_usb_interface_put(ctx->ruif);
+ regmap_usb_free_transfers(ctx);
+ kfree(ctx->lz4_comp_mem);
+ kfree(ctx);
+}
+
+static const struct regmap_bus regmap_usb = {
+ .write = regmap_usb_write,
+ .gather_write = regmap_usb_gather_write,
+ .read = regmap_usb_read,
+ .free_context = regmap_usb_free_context,
+ /* regmap_usb_transfer() handles reg_format: */
+ .reg_format_endian_default = REGMAP_ENDIAN_NATIVE,
+ .val_format_endian_default = REGMAP_ENDIAN_LITTLE,
+};
+
+#ifdef CONFIG_DEBUG_FS
+static int regmap_usb_debugfs_usbinfo_show(struct seq_file *s, void *ignored)
+{
+ struct regmap_usb_context *ctx = s->private;
+
+ mutex_lock(&ctx->ruif->lock);
+
+ seq_printf(s, "USB interface: %s\n", dev_name(&ctx->ruif->interface->dev));
+ seq_printf(s, "Regmap index: %u\n", ctx->index);
+ seq_printf(s, "Max transfer size: %u\n", ctx->max_transfer_size);
+ seq_printf(s, "Tag: %u\n", ctx->ruif->tag);
+ seq_printf(s, "Number of errors: %u\n", ctx->num_errors);
+ seq_printf(s, "Number of resets: %u\n", ctx->num_resets);
+
+ seq_puts(s, "Compression: ");
+ if (ctx->compression & REGMAP_USB_COMPRESSION_LZ4)
+ seq_puts(s, " lz4");
+ seq_puts(s, "\n");
+
+ if (ctx->compression) {
+ u64 remainder;
+ u64 ratio = div64_u64_rem(ctx->stats_length, ctx->stats_actual_length,
+ &remainder);
+ u64 ratio_frac = div64_u64(remainder * 10, ctx->stats_actual_length);
+
+ seq_printf(s, "Compression ratio: %llu.%llu\n", ratio, ratio_frac);
+ }
+
+ mutex_unlock(&ctx->ruif->lock);
+
+ return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(regmap_usb_debugfs_usbinfo);
+
+static void regmap_usb_debugfs_init(struct regmap *map)
+{
+ if (!map->debugfs)
+ return;
+
+ debugfs_create_file("usbinfo", 0400, map->debugfs, map->bus_context,
+ ®map_usb_debugfs_usbinfo_fops);
+}
+#else
+static void regmap_usb_debugfs_init(struct regmap *map) {}
+#endif
+
+static struct regmap_usb_context *
+regmap_usb_gen_context(struct usb_interface *interface, unsigned int index,
+ const struct regmap_config *config)
+{
+ struct usb_host_interface *alt = interface->cur_altsetting;
+ struct usb_device *usb = interface_to_usbdev(interface);
+ struct usb_endpoint_descriptor *ep_in, *ep_out;
+ unsigned int num_regmaps, max_transfer_size;
+ struct regmap_usb_map_descriptor map_desc;
+ struct regmap_usb_context *ctx;
+ int ret;
+
+ ret = regmap_usb_get_interface_descriptor(interface, &num_regmaps);
+ if (ret)
+ return ERR_PTR(ret);
+
+ if (!num_regmaps)
+ return ERR_PTR(-EINVAL);
+
+ if (index >= num_regmaps)
+ return ERR_PTR(-ENOENT);
+
+ ret = regmap_usb_get_map_descriptor(interface, index, &map_desc);
+ if (ret)
+ return ERR_PTR(ret);
+
+ if (config->reg_bits != 32 ||
+ config->val_bits != map_desc.bRegisterValueBits)
+ return ERR_PTR(-EINVAL);
+
+ max_transfer_size = 1 << map_desc.bMaxTransferSizeOrder;
+ if (max_transfer_size < (config->val_bits / 8))
+ return ERR_PTR(-EINVAL);
+
+ max_transfer_size = min_t(unsigned long, max_transfer_size, KMALLOC_MAX_SIZE);
+
+ ret = usb_find_common_endpoints(alt, &ep_in, &ep_out, NULL, NULL);
+ if (ret)
+ return ERR_PTR(ret);
+
+ ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
+ if (!ctx)
+ return ERR_PTR(-ENOMEM);
+
+ ctx->usb = usb;
+ ctx->index = index;
+ ctx->ifnum = alt->desc.bInterfaceNumber;
+ ctx->val_bytes = config->val_bits / 8;
+ ctx->compression = map_desc.bCompression;
+ ctx->max_transfer_size = max_transfer_size;
+ ctx->in_pipe = usb_rcvbulkpipe(usb, usb_endpoint_num(ep_in));
+ ctx->out_pipe = usb_sndbulkpipe(usb, usb_endpoint_num(ep_out));
+
+ if (ctx->compression & REGMAP_USB_COMPRESSION_LZ4) {
+ ctx->lz4_comp_mem = kmalloc(LZ4_MEM_COMPRESS, GFP_KERNEL);
+ if (!ctx->lz4_comp_mem) {
+ ret = -ENOMEM;
+ goto err_free;
+ }
+ }
+
+ ret = regmap_usb_alloc_transfers(ctx);
+ if (ret)
+ goto err_free;
+
+ ctx->ruif = regmap_usb_interface_get(interface);
+ if (IS_ERR(ctx->ruif)) {
+ ret = PTR_ERR(ctx->ruif);
+ goto err_free_transfers;
+ }
+
+ return ctx;
+
+err_free_transfers:
+ regmap_usb_free_transfers(ctx);
+err_free:
+ kfree(ctx->lz4_comp_mem);
+ kfree(ctx);
+
+ return ERR_PTR(ret);
+}
+
+struct regmap *__devm_regmap_init_usb(struct device *dev,
+ struct usb_interface *interface,
+ unsigned int index,
+ const struct regmap_config *config,
+ struct lock_class_key *lock_key,
+ const char *lock_name)
+{
+ struct regmap_usb_context *ctx;
+ struct regmap *map;
+
+ ctx = regmap_usb_gen_context(interface, index, config);
+ if (IS_ERR(ctx))
+ return ERR_CAST(ctx);
+
+ map = __devm_regmap_init(dev, ®map_usb, ctx, config,
+ lock_key, lock_name);
+ if (!IS_ERR(map))
+ regmap_usb_debugfs_init(map);
+
+ return map;
+}
+EXPORT_SYMBOL(__devm_regmap_init_usb);
+
+static int regmap_usb_get_descriptor(struct usb_interface *interface, u8 type,
+ u8 index, void *desc, size_t size)
+{
+ u8 ifnum = interface->cur_altsetting->desc.bInterfaceNumber;
+ struct usb_device *usb = interface_to_usbdev(interface);
+ u8 *buf;
+ int ret;
+
+ buf = kmalloc(size, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = usb_control_msg(usb, usb_rcvctrlpipe(usb, 0),
+ USB_REQ_GET_DESCRIPTOR,
+ USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_INTERFACE,
+ (type << 8) + index, ifnum, buf, size,
+ USB_CTRL_GET_TIMEOUT);
+ if (ret < 0)
+ goto free;
+
+ if (ret != size || buf[0] != size || buf[1] != type) {
+ ret = -ENODATA;
+ goto free;
+ }
+
+ memcpy(desc, buf, size);
+free:
+ kfree(buf);
+
+ return ret;
+}
+
+/**
+ * regmap_usb_get_interface_descriptor() - Get regmap interface descriptor
+ * @interface: USB interface
+ * @num_regmaps: Returns the number of regmaps supported on this interface
+ *
+ * Returns:
+ * Zero on success, negative error code on failure.
+ */
+int regmap_usb_get_interface_descriptor(struct usb_interface *interface,
+ unsigned int *num_regmaps)
+{
+ struct regmap_usb_interface_descriptor desc;
+ int ret;
+
+ ret = regmap_usb_get_descriptor(interface, REGMAP_USB_DT_INTERFACE, 0,
+ &desc, sizeof(desc));
+ if (ret < 0)
+ return ret;
+
+ *num_regmaps = desc.bNumRegmaps;
+
+ return 0;
+}
+EXPORT_SYMBOL(regmap_usb_get_interface_descriptor);
+
+/**
+ * regmap_usb_get_map_descriptor() - Get regmap descriptor
+ * @interface: USB interface
+ * @index: Index of register
+ * @desc: Returned descriptor (little endian representation)
+ *
+ * Returns:
+ * Zero on success, negative error code on failure.
+ */
+int regmap_usb_get_map_descriptor(struct usb_interface *interface,
+ unsigned int index,
+ struct regmap_usb_map_descriptor *desc)
+{
+ int ret;
+
+ ret = regmap_usb_get_descriptor(interface, REGMAP_USB_DT_MAP, index,
+ desc, sizeof(*desc));
+ if (ret < 0)
+ return ret;
+
+ if (desc->name[31] != '\0')
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL(regmap_usb_get_map_descriptor);
+
+MODULE_LICENSE("GPL");
@@ -32,6 +32,7 @@ struct regmap_range_cfg;
struct regmap_field;
struct snd_ac97;
struct sdw_slave;
+struct usb_interface;
/* An enum of all the supported cache types */
enum regcache_type {
@@ -618,6 +619,12 @@ struct regmap *__devm_regmap_init_sdw(struct sdw_slave *sdw,
const struct regmap_config *config,
struct lock_class_key *lock_key,
const char *lock_name);
+struct regmap *__devm_regmap_init_usb(struct device *dev,
+ struct usb_interface *interface,
+ unsigned int index,
+ const struct regmap_config *config,
+ struct lock_class_key *lock_key,
+ const char *lock_name);
struct regmap *__devm_regmap_init_slimbus(struct slim_device *slimbus,
const struct regmap_config *config,
struct lock_class_key *lock_key,
@@ -971,6 +978,22 @@ bool regmap_ac97_default_volatile(struct device *dev, unsigned int reg);
__regmap_lockdep_wrapper(__devm_regmap_init_sdw, #config, \
sdw, config)
+/**
+ * devm_regmap_init_usb() - Initialise managed register map
+ *
+ * @dev: Parent device
+ * @interface: USB interface
+ * @index: Index of register
+ * @config: Configuration for register map
+ *
+ * The return value will be an ERR_PTR() on error or a valid pointer
+ * to a struct regmap. The regmap will be automatically freed by the
+ * device management code.
+ */
+#define devm_regmap_init_usb(dev, interface, index, config) \
+ __regmap_lockdep_wrapper(__devm_regmap_init_usb, #config, \
+ dev, interface, index, config)
+
/**
* devm_regmap_init_slimbus() - Initialise managed register map
*
new file mode 100644
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright 2020 Noralf Trønnes
+ */
+
+#ifndef __LINUX_REGMAP_USB_H
+#define __LINUX_REGMAP_USB_H
+
+#include <linux/types.h>
+#include <uapi/linux/usb/ch9.h>
+
+struct usb_interface;
+
+#define REGMAP_USB_MAX_MAPS 255
+
+#define REGMAP_USB_DT_INTERFACE (USB_TYPE_VENDOR | 0x01)
+#define REGMAP_USB_DT_MAP (USB_TYPE_VENDOR | 0x02)
+
+/**
+ * struct regmap_usb_interface_descriptor - Regmap interface descriptor
+ * @bLength: Size of descriptor in bytes
+ * @bDescriptorType: DescriptorType (REGMAP_USB_DT_INTERFACE)
+ * @wNumRegmaps: Number of regmaps on this interface
+ */
+struct regmap_usb_interface_descriptor {
+ __u8 bLength;
+ __u8 bDescriptorType;
+
+ __u8 bNumRegmaps;
+} __packed;
+
+/**
+ * struct regmap_usb_map_descriptor - Regmap descriptor
+ * @bLength: Size of descriptor in bytes
+ * @bDescriptorType: DescriptorType (REGMAP_USB_DT_MAP)
+ * @name: Register name (NUL terminated)
+ * @bRegisterValueBits: Number of bits in the register value
+ * @bCompression: Supported compression types
+ * @bMaxTransferSizeOrder: Maximum transfer size the device can handle as log2.
+ */
+struct regmap_usb_map_descriptor {
+ __u8 bLength;
+ __u8 bDescriptorType;
+
+ __u8 name[32];
+ __u8 bRegisterValueBits;
+ __u8 bCompression;
+#define REGMAP_USB_COMPRESSION_LZ4 BIT(0)
+ __u8 bMaxTransferSizeOrder;
+} __packed;
+
+#define REGMAP_USB_REQ_PROTOCOL_RESET 0xff /* Returns previous error code as u8 */
+
+/**
+ * struct regmap_usb_header - Transfer header
+ * @signature: Magic value (0x2389abc2)
+ * @index: Index of adressed register
+ * @tag: Sequential transfer number
+ * @flags: Transfer flags
+ * @regnr: Register number
+ * @length: Transfer length
+ */
+struct regmap_usb_header {
+#define REGMAP_USB_HEADER_SIGNATURE 0x2389abc2
+ __le32 signature;
+ __le16 index;
+ __le16 tag;
+ __le32 flags;
+#define REGMAP_USB_HEADER_FLAG_IN BIT(31)
+/* First 8 bits are the same as the descriptor compression bits */
+#define REGMAP_USB_HEADER_FLAG_COMPRESSION_MASK 0xff
+ __le32 regnr;
+ __le32 length;
+} __packed;
+
+/**
+ * struct regmap_usb_status - Transfer status
+ * @signature: Magic value (0x83e7b803)
+ * @index: Index of adressed register
+ * @tag: Sequential transfer number (the same as the one received in the header)
+ * @status: Status value of the transfer (Zero on success or a Linux errno on failure)
+ */
+struct regmap_usb_status {
+#define REGMAP_USB_STATUS_SIGNATURE 0x83e7b803
+ __le32 signature;
+ __le16 index;
+ __le16 tag;
+ __u8 status;
+} __packed;
+
+int regmap_usb_get_interface_descriptor(struct usb_interface *interface,
+ unsigned int *num_regmaps);
+int regmap_usb_get_map_descriptor(struct usb_interface *interface,
+ unsigned int index,
+ struct regmap_usb_map_descriptor *desc);
+
+#endif
Add support for regmap over USB for use with the Multifunction USB Device. Two endpoints IN/OUT are used. Up to 255 regmaps are supported on one USB interface. The register index width is always 32-bit, but the register value can be 8, 16 or 32 bits wide. LZ4 compression is supported on bulk transfers. Signed-off-by: Noralf Trønnes <noralf@tronnes.org> --- drivers/base/regmap/Kconfig | 8 +- drivers/base/regmap/Makefile | 1 + drivers/base/regmap/regmap-usb.c | 1026 ++++++++++++++++++++++++++++++ include/linux/regmap.h | 23 + include/linux/regmap_usb.h | 97 +++ 5 files changed, 1154 insertions(+), 1 deletion(-) create mode 100644 drivers/base/regmap/regmap-usb.c create mode 100644 include/linux/regmap_usb.h