@@ -104,6 +104,17 @@ config USB_XHCI_RZV2M
Say 'Y' to enable the support for the xHCI host controller
found in Renesas RZ/V2M SoC.
+config USB_XHCI_SEC_INTR
+ tristate "xHCI support for secondary interrupter management"
+ help
+ Say 'Y' to enable the support for the xHCI secondary management.
+ Provide a mechanism for a sideband datapath for payload associated
+ with audio class endpoints. This allows for an audio DSP to use
+ xHCI USB endpoints directly, allowing CPU to sleep while playing
+ audio. This is not the same feature as the audio sideband
+ capability mentioned within the xHCI specification, and continues
+ to utilize main system memory for data transfers.
+
config USB_XHCI_TEGRA
tristate "xHCI support for NVIDIA Tegra SoCs"
depends on PHY_TEGRA_XUSB
@@ -32,6 +32,8 @@ endif
xhci-rcar-hcd-y += xhci-rcar.o
xhci-rcar-hcd-$(CONFIG_USB_XHCI_RZV2M) += xhci-rzv2m.o
+obj-$(CONFIG_USB_XHCI_SEC_INTR) += xhci-sec-intr.o
+
obj-$(CONFIG_USB_PCI) += pci-quirks.o
obj-$(CONFIG_USB_EHCI_HCD) += ehci-hcd.o
new file mode 100644
@@ -0,0 +1,438 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * xHCI host controller secondary interrupter management
+ *
+ * Provides logic for client drivers that support utilizing xHCI secondary
+ * interrupters.
+ *
+ * Copyright (c) 2023-2024, Intel Corporation.
+ *
+ * Author: Mathias Nyman
+ */
+
+#include <linux/usb/xhci-sec-intr.h>
+#include <linux/dma-direct.h>
+
+#include "xhci.h"
+
+/* internal helpers */
+static struct sg_table *
+xhci_ring_to_sgtable(struct xhci_sec_intr *si, struct xhci_ring *ring)
+{
+ struct xhci_segment *seg;
+ struct sg_table *sgt;
+ unsigned int n_pages;
+ struct page **pages;
+ struct device *dev;
+ size_t sz;
+ int i;
+
+ dev = xhci_to_hcd(si->xhci)->self.sysdev;
+ sz = ring->num_segs * TRB_SEGMENT_SIZE;
+ n_pages = PAGE_ALIGN(sz) >> PAGE_SHIFT;
+ pages = kvmalloc_array(n_pages, sizeof(struct page *), GFP_KERNEL);
+ if (!pages)
+ return NULL;
+
+ sgt = kzalloc(sizeof(*sgt), GFP_KERNEL);
+ if (!sgt) {
+ kvfree(pages);
+ return NULL;
+ }
+
+ seg = ring->first_seg;
+ if (!seg)
+ goto err;
+ /*
+ * Rings can potentially have multiple segments, create an array that
+ * carries page references to allocated segments. Utilize the
+ * sg_alloc_table_from_pages() to create the sg table, and to ensure
+ * that page links are created.
+ */
+ for (i = 0; i < ring->num_segs; i++) {
+ dma_get_sgtable(dev, sgt, seg->trbs, seg->dma,
+ TRB_SEGMENT_SIZE);
+ pages[i] = sg_page(sgt->sgl);
+ sg_free_table(sgt);
+ seg = seg->next;
+ }
+
+ if (sg_alloc_table_from_pages(sgt, pages, n_pages, 0, sz, GFP_KERNEL))
+ goto err;
+
+ /*
+ * Save first segment dma address to sg dma_address field for the sideband
+ * client to have access to the IOVA of the ring.
+ */
+ sg_dma_address(sgt->sgl) = ring->first_seg->dma;
+
+ return sgt;
+
+err:
+ kvfree(pages);
+ kfree(sgt);
+
+ return NULL;
+}
+
+static void
+__xhci_sec_intr_remove_endpoint(struct xhci_sec_intr *si, struct xhci_virt_ep *ep)
+{
+ /*
+ * Issue a stop endpoint command when an endpoint is removed.
+ * The stop ep cmd handler will handle the ring cleanup.
+ */
+ xhci_stop_endpoint_sync(si->xhci, ep, 0, GFP_KERNEL);
+
+ ep->sec = NULL;
+ si->eps[ep->ep_index] = NULL;
+}
+
+/* endpoint api functions */
+
+/**
+ * xhci_sec_intr_add_endpoint - add endpoint to access list
+ * @si: secondary interrupter instance for this usb device
+ * @host_ep: usb host endpoint
+ *
+ * Adds an endpoint to the list of endpoints utilizing secondary interrupters
+ * for this usb device.
+ * After an endpoint is added the client can get the endpoint transfer ring
+ * buffer by calling xhci_sec_intr_get_endpoint_buffer()
+ *
+ * Return: 0 on success, negative error otherwise.
+ */
+int
+xhci_sec_intr_add_endpoint(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep)
+{
+ struct xhci_virt_ep *ep;
+ unsigned int ep_index;
+
+ mutex_lock(&si->mutex);
+ ep_index = xhci_get_endpoint_index(&host_ep->desc);
+ ep = &si->vdev->eps[ep_index];
+
+ if (ep->ep_state & EP_HAS_STREAMS) {
+ mutex_unlock(&si->mutex);
+ return -EINVAL;
+ }
+
+ /*
+ * Note, we don't know the DMA mask of the audio DSP device, if its
+ * smaller than for xhci it won't be able to access the endpoint ring
+ * buffer. This could be solved by not allowing the audio class driver
+ * to add the endpoint the normal way, but instead offload it immediately,
+ * and let this function add the endpoint and allocate the ring buffer
+ * with the smallest common DMA mask
+ */
+ if (si->eps[ep_index] || ep->sec) {
+ mutex_unlock(&si->mutex);
+ return -EBUSY;
+ }
+
+ ep->sec = si;
+ si->eps[ep_index] = ep;
+ mutex_unlock(&si->mutex);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_add_endpoint);
+
+/**
+ * xhci_sec_intr_remove_endpoint - remove endpoint from access list
+ * @si: secondary interrupter instance for this usb device
+ * @host_ep: usb host endpoint
+ *
+ * Removes an endpoint from the list of endpoints utilizing secondary
+ * interrupters for this usb device.
+ * Clients should no longer touch the endpoint transfer buffer after
+ * calling this.
+ *
+ * Return: 0 on success, negative error otherwise.
+ */
+int
+xhci_sec_intr_remove_endpoint(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep)
+{
+ struct xhci_virt_ep *ep;
+ unsigned int ep_index;
+
+ mutex_lock(&si->mutex);
+ ep_index = xhci_get_endpoint_index(&host_ep->desc);
+ ep = si->eps[ep_index];
+
+ if (!ep || !ep->sec || ep->sec != si) {
+ mutex_unlock(&si->mutex);
+ return -ENODEV;
+ }
+
+ __xhci_sec_intr_remove_endpoint(si, ep);
+ xhci_initialize_ring_info(ep->ring, 1);
+ mutex_unlock(&si->mutex);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_remove_endpoint);
+
+/**
+ * xhci_sec_intr_stop_endpoint - stops a client managed endpoint
+ * @si: secondary interrupter instance for this usb device
+ * @host_ep: usb host endpoint
+ *
+ * Issue a synchronous stop endpoint command to halt any data transfers
+ * occurring. This also is required before the xHCI controller enters
+ * suspend.
+ *
+ * Return: 0 on success, and negative on failure.
+ */
+int
+xhci_sec_intr_stop_endpoint(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep)
+{
+ struct xhci_virt_ep *ep;
+ unsigned int ep_index;
+
+ ep_index = xhci_get_endpoint_index(&host_ep->desc);
+ ep = si->eps[ep_index];
+
+ if (!ep || !ep->sec || ep->sec != si)
+ return -EINVAL;
+
+ return xhci_stop_endpoint_sync(si->xhci, ep, 0, GFP_KERNEL);
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_stop_endpoint);
+
+/**
+ * xhci_sec_intr_get_endpoint_buffer - gets the endpoint transfer buffer address
+ * @si: secondary interrupter instance for this usb device
+ * @host_ep: usb host endpoint
+ *
+ * Returns the address of the endpoint buffer where xHC controller reads queued
+ * transfer TRBs from. This is the starting address of the ringbuffer where the
+ * client should write TRBs to.
+ *
+ * Caller needs to free the returned sg_table
+ *
+ * Return: struct sg_table * if successful. NULL otherwise.
+ */
+struct sg_table *
+xhci_sec_intr_get_endpoint_buffer(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep)
+{
+ struct xhci_virt_ep *ep;
+ unsigned int ep_index;
+
+ ep_index = xhci_get_endpoint_index(&host_ep->desc);
+ ep = si->eps[ep_index];
+
+ if (!ep || !ep->sec || ep->sec != si)
+ return NULL;
+
+ return xhci_ring_to_sgtable(si, ep->ring);
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_get_endpoint_buffer);
+
+/**
+ * xhci_sec_intr_get_event_buffer - return the event buffer for this device
+ * @si: secondary interrupter instance for this usb device
+ *
+ * If a secondary xhci interupter is set up for this usb device then this
+ * function returns the address of the event buffer where xHC writes
+ * the transfer completion events.
+ *
+ * Caller needs to free the returned sg_table
+ *
+ * Return: struct sg_table * if successful. NULL otherwise.
+ */
+struct sg_table *
+xhci_sec_intr_get_event_buffer(struct xhci_sec_intr *si)
+{
+ if (!si || !si->ir)
+ return NULL;
+
+ return xhci_ring_to_sgtable(si, si->ir->event_ring);
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_get_event_buffer);
+
+/**
+ * xhci_sec_intr_create_interrupter - creates a new interrupter
+ * @si: secondary interrupter instance for this usb device
+ * @num_seg: number of event ring segments to allocate
+ * @ip_autoclear: IP autoclearing support such as MSI implemented
+ *
+ * Sets up a xhci interrupter that can be used for this sideband accessed usb
+ * device. Transfer events for this device can be routed to this interrupters
+ * event ring by setting the 'Interrupter Target' field correctly when queueing
+ * the transfer TRBs.
+ * Once this interrupter is created the interrupter target ID can be obtained
+ * by calling xhci_sec_intr_interrupter_id()
+ *
+ * Returns 0 on success, negative error otherwise
+ */
+int
+xhci_sec_intr_create_interrupter(struct xhci_sec_intr *si, int num_seg,
+ bool ip_autoclear, u32 imod_interval)
+{
+ int ret = 0;
+
+ if (!si || !si->xhci)
+ return -ENODEV;
+
+ mutex_lock(&si->mutex);
+ if (si->ir) {
+ ret = -EBUSY;
+ goto out;
+ }
+
+ si->ir = xhci_create_secondary_interrupter(xhci_to_hcd(si->xhci),
+ num_seg, imod_interval);
+ if (!si->ir) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ si->ir->ip_autoclear = ip_autoclear;
+ /* skip events for secondary interrupters by default */
+ si->ir->skip_events = true;
+
+out:
+ mutex_unlock(&si->mutex);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_create_interrupter);
+
+/**
+ * xhci_sec_intr_remove_interrupter - remove the secondary interrupter for a device
+ * @si: secondary interrupter instance for this usb device
+ *
+ * Removes a registered interrupter for a usb device. This would allow for other
+ * users to utilize this interrupter.
+ */
+void
+xhci_sec_intr_remove_interrupter(struct xhci_sec_intr *si)
+{
+ if (!si || !si->ir)
+ return;
+
+ mutex_lock(&si->mutex);
+ xhci_remove_secondary_interrupter(xhci_to_hcd(si->xhci), si->ir);
+
+ si->ir = NULL;
+ mutex_unlock(&si->mutex);
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_remove_interrupter);
+
+/**
+ * xhci_sec_intr_interrupter_id - return the interrupter target id
+ * @si: secondary interrupter instance for this usb device
+ *
+ * If a secondary xhci interrupter is set up for this usb device then this
+ * function returns the ID used by the interrupter. The client needs
+ * to write this ID to the 'Interrupter Target' field of the transfer TRBs
+ * it queues on the endpoints transfer ring to ensure transfer completion event
+ * are written by xHC to the correct interrupter event ring.
+ *
+ * Returns interrupter id on success, negative error otherwise
+ */
+int
+xhci_sec_intr_interrupter_id(struct xhci_sec_intr *si)
+{
+ if (!si || !si->ir)
+ return -ENODEV;
+
+ return si->ir->intr_num;
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_interrupter_id);
+
+/**
+ * xhci_sec_intr_register - register a secondary interrupter for a usb device
+ * @udev: usb device to be accessed via sideband
+ *
+ * Allows for clients to utilize XHCI interrupters and fetch transfer and event
+ * ring parameters for executing data transfers.
+ *
+ * Return: pointer to a new xhci_sec_intr instance if successful. NULL otherwise.
+ */
+struct xhci_sec_intr *
+xhci_sec_intr_register(struct usb_device *udev)
+{
+ struct usb_hcd *hcd = bus_to_hcd(udev->bus);
+ struct xhci_hcd *xhci = hcd_to_xhci(hcd);
+ struct xhci_virt_device *vdev;
+ struct xhci_sec_intr *si;
+
+ /* make sure the usb device is connected to a xhci controller */
+ if (!udev->slot_id)
+ return NULL;
+
+ si = kzalloc_node(sizeof(*si), GFP_KERNEL, dev_to_node(hcd->self.sysdev));
+ if (!si)
+ return NULL;
+
+ mutex_init(&si->mutex);
+
+ /* check this device isn't already controlled via sideband */
+ spin_lock_irq(&xhci->lock);
+
+ vdev = xhci->devs[udev->slot_id];
+
+ if (!vdev || vdev->sec) {
+ xhci_warn(xhci, "XHCI sideband for slot %d already in use\n",
+ udev->slot_id);
+ spin_unlock_irq(&xhci->lock);
+ kfree(si);
+ return NULL;
+ }
+
+ si->xhci = xhci;
+ si->vdev = vdev;
+ vdev->sec = si;
+
+ spin_unlock_irq(&xhci->lock);
+
+ return si;
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_register);
+
+/**
+ * xhci_sec_intr_unregister - unregister secondary interrupter for a usb device
+ * @si: secondary interrupter instance to be unregistered
+ *
+ * Unregisters sideband access to a usb device and frees the secondary interrupter
+ * instance.
+ * After this the endpoint and interrupter event buffers should no longer
+ * be accessed via sideband. The xhci driver can now take over handling
+ * the buffers.
+ */
+void
+xhci_sec_intr_unregister(struct xhci_sec_intr *si)
+{
+ struct xhci_hcd *xhci;
+ int i;
+
+ if (!si)
+ return;
+
+ xhci = si->xhci;
+
+ mutex_lock(&si->mutex);
+ for (i = 0; i < EP_CTX_PER_DEV; i++)
+ if (si->eps[i])
+ __xhci_sec_intr_remove_endpoint(si, si->eps[i]);
+ mutex_unlock(&si->mutex);
+
+ xhci_sec_intr_remove_interrupter(si);
+
+ spin_lock_irq(&xhci->lock);
+ si->xhci = NULL;
+ si->vdev->sec = NULL;
+ spin_unlock_irq(&xhci->lock);
+
+ kfree(si);
+}
+EXPORT_SYMBOL_GPL(xhci_sec_intr_unregister);
+MODULE_DESCRIPTION("xHCI sideband driver for secondary interrupter management");
+MODULE_LICENSE("GPL");
@@ -694,6 +694,8 @@ struct xhci_virt_ep {
int next_frame_id;
/* Use new Isoch TRB layout needed for extended TBC support */
bool use_extended_tbc;
+ /* set if this endpoint is controlled via sideband access*/
+ struct xhci_sec_intr *sec;
};
enum xhci_overhead_type {
@@ -756,6 +758,8 @@ struct xhci_virt_device {
u16 current_mel;
/* Used for the debugfs interfaces. */
void *debugfs_private;
+ /* set if this device is registered for sideband access */
+ struct xhci_sec_intr *sec;
};
/*
new file mode 100644
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * xHCI host controller sideband support
+ *
+ * Copyright (c) 2023, Intel Corporation.
+ *
+ * Author: Mathias Nyman <mathias.nyman@linux.intel.com>
+ */
+
+#ifndef __LINUX_XHCI_SEC_INTR_H
+#define __LINUX_XHCI_SEC_INTR_H
+
+#include <linux/scatterlist.h>
+#include <linux/usb.h>
+
+#define EP_CTX_PER_DEV 31 /* FIXME defined twice, from xhci.h */
+
+struct xhci_sec_intr;
+
+/**
+ * struct xhci_sec_intr - representation of a sideband accessed usb device.
+ * @xhci: The xhci host controller the usb device is connected to
+ * @vdev: the usb device accessed via sideband
+ * @eps: array of endpoints controlled via sideband
+ * @ir: event handling and buffer for sideband accessed device
+ * @mutex: mutex for sideband operations
+ *
+ * FIXME usb device accessed via sideband Keeping track of sideband accessed usb devices.
+ */
+
+struct xhci_sec_intr {
+ struct xhci_hcd *xhci;
+ struct xhci_virt_device *vdev;
+ struct xhci_virt_ep *eps[EP_CTX_PER_DEV];
+ struct xhci_interrupter *ir;
+
+ /* Synchronizing xHCI sec intr operations with client drivers operations */
+ struct mutex mutex;
+};
+
+struct xhci_sec_intr *
+xhci_sec_intr_register(struct usb_device *udev);
+void
+xhci_sec_intr_unregister(struct xhci_sec_intr *si);
+int
+xhci_sec_intr_add_endpoint(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep);
+int
+xhci_sec_intr_remove_endpoint(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep);
+int
+xhci_sec_intr_stop_endpoint(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep);
+struct sg_table *
+xhci_sec_intr_get_endpoint_buffer(struct xhci_sec_intr *si,
+ struct usb_host_endpoint *host_ep);
+struct sg_table *
+xhci_sec_intr_get_event_buffer(struct xhci_sec_intr *si);
+
+int
+xhci_sec_intr_create_interrupter(struct xhci_sec_intr *si, int num_seg,
+ bool ip_autoclear, u32 imod_interval);
+
+void
+xhci_sec_intr_remove_interrupter(struct xhci_sec_intr *si);
+
+int
+xhci_sec_intr_interrupter_id(struct xhci_sec_intr *si);
+
+#endif /* __LINUX_XHCI_SIDEBAND_H */