| // SPDX-License-Identifier: GPL-2.0 |
| |
| /* |
| * xHCI host controller sideband support |
| * |
| * Copyright (c) 2023-2025, Intel Corporation. |
| * |
| * Author: Mathias Nyman |
| */ |
| |
| #include <linux/usb/xhci-sideband.h> |
| #include <linux/dma-direct.h> |
| |
| #include "xhci.h" |
| |
| /* sideband internal helpers */ |
| static struct sg_table * |
| xhci_ring_to_sgtable(struct xhci_sideband *sb, 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(sb->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_sideband_remove_endpoint(struct xhci_sideband *sb, 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(sb->xhci, ep, 0, GFP_KERNEL); |
| |
| ep->sideband = NULL; |
| sb->eps[ep->ep_index] = NULL; |
| } |
| |
| /* sideband api functions */ |
| |
| /** |
| * xhci_sideband_notify_ep_ring_free - notify client of xfer ring free |
| * @sb: sideband instance for this usb device |
| * @ep_index: usb endpoint index |
| * |
| * Notifies the xHCI sideband client driver of a xHCI transfer ring free |
| * routine. This will allow for the client to ensure that all transfers |
| * are completed. |
| * |
| * The callback should be synchronous, as the ring free happens after. |
| */ |
| void xhci_sideband_notify_ep_ring_free(struct xhci_sideband *sb, |
| unsigned int ep_index) |
| { |
| struct xhci_sideband_event evt; |
| |
| evt.type = XHCI_SIDEBAND_XFER_RING_FREE; |
| evt.evt_data = &ep_index; |
| |
| if (sb->notify_client) |
| sb->notify_client(sb->intf, &evt); |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_notify_ep_ring_free); |
| |
| /** |
| * xhci_sideband_add_endpoint - add endpoint to sideband access list |
| * @sb: sideband instance for this usb device |
| * @host_ep: usb host endpoint |
| * |
| * Adds an endpoint to the list of sideband accessed endpoints for this usb |
| * device. |
| * After an endpoint is added the sideband client can get the endpoint transfer |
| * ring buffer by calling xhci_sideband_endpoint_buffer() |
| * |
| * Return: 0 on success, negative error otherwise. |
| */ |
| int |
| xhci_sideband_add_endpoint(struct xhci_sideband *sb, |
| struct usb_host_endpoint *host_ep) |
| { |
| struct xhci_virt_ep *ep; |
| unsigned int ep_index; |
| |
| mutex_lock(&sb->mutex); |
| ep_index = xhci_get_endpoint_index(&host_ep->desc); |
| ep = &sb->vdev->eps[ep_index]; |
| |
| if (ep->ep_state & EP_HAS_STREAMS) { |
| mutex_unlock(&sb->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 (sb->eps[ep_index] || ep->sideband) { |
| mutex_unlock(&sb->mutex); |
| return -EBUSY; |
| } |
| |
| ep->sideband = sb; |
| sb->eps[ep_index] = ep; |
| mutex_unlock(&sb->mutex); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_add_endpoint); |
| |
| /** |
| * xhci_sideband_remove_endpoint - remove endpoint from sideband access list |
| * @sb: sideband instance for this usb device |
| * @host_ep: usb host endpoint |
| * |
| * Removes an endpoint from the list of sideband accessed endpoints for this usb |
| * device. |
| * sideband client should no longer touch the endpoint transfer buffer after |
| * calling this. |
| * |
| * Return: 0 on success, negative error otherwise. |
| */ |
| int |
| xhci_sideband_remove_endpoint(struct xhci_sideband *sb, |
| struct usb_host_endpoint *host_ep) |
| { |
| struct xhci_virt_ep *ep; |
| unsigned int ep_index; |
| |
| mutex_lock(&sb->mutex); |
| ep_index = xhci_get_endpoint_index(&host_ep->desc); |
| ep = sb->eps[ep_index]; |
| |
| if (!ep || !ep->sideband || ep->sideband != sb) { |
| mutex_unlock(&sb->mutex); |
| return -ENODEV; |
| } |
| |
| __xhci_sideband_remove_endpoint(sb, ep); |
| xhci_initialize_ring_info(ep->ring); |
| mutex_unlock(&sb->mutex); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_remove_endpoint); |
| |
| int |
| xhci_sideband_stop_endpoint(struct xhci_sideband *sb, |
| 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 = sb->eps[ep_index]; |
| |
| if (!ep || !ep->sideband || ep->sideband != sb) |
| return -EINVAL; |
| |
| return xhci_stop_endpoint_sync(sb->xhci, ep, 0, GFP_KERNEL); |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_stop_endpoint); |
| |
| /** |
| * xhci_sideband_get_endpoint_buffer - gets the endpoint transfer buffer address |
| * @sb: sideband 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 |
| * sideband 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_sideband_get_endpoint_buffer(struct xhci_sideband *sb, |
| 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 = sb->eps[ep_index]; |
| |
| if (!ep || !ep->ring || !ep->sideband || ep->sideband != sb) |
| return NULL; |
| |
| return xhci_ring_to_sgtable(sb, ep->ring); |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_get_endpoint_buffer); |
| |
| /** |
| * xhci_sideband_get_event_buffer - return the event buffer for this device |
| * @sb: sideband 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_sideband_get_event_buffer(struct xhci_sideband *sb) |
| { |
| if (!sb || !sb->ir) |
| return NULL; |
| |
| return xhci_ring_to_sgtable(sb, sb->ir->event_ring); |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_get_event_buffer); |
| |
| /** |
| * xhci_sideband_create_interrupter - creates a new interrupter for this sideband |
| * @sb: sideband 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_sideband_interrupter_id() |
| * |
| * Returns 0 on success, negative error otherwise |
| */ |
| int |
| xhci_sideband_create_interrupter(struct xhci_sideband *sb, int num_seg, |
| bool ip_autoclear, u32 imod_interval, int intr_num) |
| { |
| int ret = 0; |
| |
| if (!sb || !sb->xhci) |
| return -ENODEV; |
| |
| mutex_lock(&sb->mutex); |
| if (sb->ir) { |
| ret = -EBUSY; |
| goto out; |
| } |
| |
| sb->ir = xhci_create_secondary_interrupter(xhci_to_hcd(sb->xhci), |
| num_seg, imod_interval, |
| intr_num); |
| if (!sb->ir) { |
| ret = -ENOMEM; |
| goto out; |
| } |
| |
| sb->ir->ip_autoclear = ip_autoclear; |
| |
| out: |
| mutex_unlock(&sb->mutex); |
| |
| return ret; |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_create_interrupter); |
| |
| /** |
| * xhci_sideband_remove_interrupter - remove the interrupter from a sideband |
| * @sb: sideband instance for this usb device |
| * |
| * Removes a registered interrupt for a sideband. This would allow for other |
| * sideband users to utilize this interrupter. |
| */ |
| void |
| xhci_sideband_remove_interrupter(struct xhci_sideband *sb) |
| { |
| if (!sb || !sb->ir) |
| return; |
| |
| mutex_lock(&sb->mutex); |
| xhci_remove_secondary_interrupter(xhci_to_hcd(sb->xhci), sb->ir); |
| |
| sb->ir = NULL; |
| mutex_unlock(&sb->mutex); |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_remove_interrupter); |
| |
| /** |
| * xhci_sideband_interrupter_id - return the interrupter target id |
| * @sb: sideband 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 sideband 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 othgerwise |
| */ |
| int |
| xhci_sideband_interrupter_id(struct xhci_sideband *sb) |
| { |
| if (!sb || !sb->ir) |
| return -ENODEV; |
| |
| return sb->ir->intr_num; |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_interrupter_id); |
| |
| /** |
| * xhci_sideband_register - register a sideband for a usb device |
| * @intf: usb interface associated with the sideband device |
| * |
| * Allows for clients to utilize XHCI interrupters and fetch transfer and event |
| * ring parameters for executing data transfers. |
| * |
| * Return: pointer to a new xhci_sideband instance if successful. NULL otherwise. |
| */ |
| struct xhci_sideband * |
| xhci_sideband_register(struct usb_interface *intf, enum xhci_sideband_type type, |
| int (*notify_client)(struct usb_interface *intf, |
| struct xhci_sideband_event *evt)) |
| { |
| struct usb_device *udev = interface_to_usbdev(intf); |
| struct usb_hcd *hcd = bus_to_hcd(udev->bus); |
| struct xhci_hcd *xhci = hcd_to_xhci(hcd); |
| struct xhci_virt_device *vdev; |
| struct xhci_sideband *sb; |
| |
| /* |
| * Make sure the usb device is connected to a xhci controller. Fail |
| * registration if the type is anything other than XHCI_SIDEBAND_VENDOR, |
| * as this is the only type that is currently supported by xhci-sideband. |
| */ |
| if (!udev->slot_id || type != XHCI_SIDEBAND_VENDOR) |
| return NULL; |
| |
| sb = kzalloc_node(sizeof(*sb), GFP_KERNEL, dev_to_node(hcd->self.sysdev)); |
| if (!sb) |
| return NULL; |
| |
| mutex_init(&sb->mutex); |
| |
| /* check this device isn't already controlled via sideband */ |
| spin_lock_irq(&xhci->lock); |
| |
| vdev = xhci->devs[udev->slot_id]; |
| |
| if (!vdev || vdev->sideband) { |
| xhci_warn(xhci, "XHCI sideband for slot %d already in use\n", |
| udev->slot_id); |
| spin_unlock_irq(&xhci->lock); |
| kfree(sb); |
| return NULL; |
| } |
| |
| sb->xhci = xhci; |
| sb->vdev = vdev; |
| sb->intf = intf; |
| sb->type = type; |
| sb->notify_client = notify_client; |
| vdev->sideband = sb; |
| |
| spin_unlock_irq(&xhci->lock); |
| |
| return sb; |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_register); |
| |
| /** |
| * xhci_sideband_unregister - unregister sideband access to a usb device |
| * @sb: sideband instance to be unregistered |
| * |
| * Unregisters sideband access to a usb device and frees the sideband |
| * 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_sideband_unregister(struct xhci_sideband *sb) |
| { |
| struct xhci_hcd *xhci; |
| int i; |
| |
| if (!sb) |
| return; |
| |
| xhci = sb->xhci; |
| |
| mutex_lock(&sb->mutex); |
| for (i = 0; i < EP_CTX_PER_DEV; i++) |
| if (sb->eps[i]) |
| __xhci_sideband_remove_endpoint(sb, sb->eps[i]); |
| mutex_unlock(&sb->mutex); |
| |
| xhci_sideband_remove_interrupter(sb); |
| |
| spin_lock_irq(&xhci->lock); |
| sb->xhci = NULL; |
| sb->vdev->sideband = NULL; |
| spin_unlock_irq(&xhci->lock); |
| |
| kfree(sb); |
| } |
| EXPORT_SYMBOL_GPL(xhci_sideband_unregister); |
| MODULE_DESCRIPTION("xHCI sideband driver for secondary interrupter management"); |
| MODULE_LICENSE("GPL"); |