+
+/* QMI service disconnect handlers */
+static void qmi_disconnect_work(struct work_struct *w)
+{
+ struct intf_info *info;
+ int idx, if_idx;
+ struct snd_usb_substream *subs;
+ struct snd_usb_audio *chip;
+
+ /* find all active intf for set alt 0 and cleanup usb audio dev */
+ for (idx = 0; idx < SNDRV_CARDS; idx++) {
+ if (!atomic_read(&uadev[idx].in_use))
+ continue;
+
+ chip = uadev[idx].chip;
+ for (if_idx = 0; if_idx < uadev[idx].num_intf; if_idx++) {
+ if (!uadev[idx].info || !uadev[idx].info[if_idx].in_use)
+ continue;
+ info = &uadev[idx].info[if_idx];
+ subs = find_substream(info->pcm_card_num,
+ info->pcm_dev_num,
+ info->direction);
+ if (!subs || !chip || atomic_read(&chip->shutdown)) {
+ dev_err(&subs->dev->dev,
+ "no sub for c#%u dev#%u dir%u\n",
+ info->pcm_card_num,
+ info->pcm_dev_num,
+ info->direction);
+ continue;
+ }
+ disable_audio_stream(subs);
+ }
+ atomic_set(&uadev[idx].in_use, 0);
+ mutex_lock(&chip->mutex);
+ uaudio_dev_cleanup(&uadev[idx]);
+ mutex_unlock(&chip->mutex);
+ }
+}
+
+/**
+ * qmi_bye_cb() - qmi bye message callback
+ * @handle: QMI handle
+ * @node: id of the dying node
+ *
+ * This callback is invoked when the QMI bye control message is received
+ * from the QMI client. Handle the message accordingly by ensuring that
+ * the USB offload path is disabled and cleaned up. At this point, ADSP
+ * is not utilizing the USB bus.
+ *
+ */
+static void qmi_bye_cb(struct qmi_handle *handle, unsigned int node)
+{
+ struct uaudio_qmi_svc *svc = uaudio_svc;
+
+ if (svc->uaudio_svc_hdl != handle)
+ return;
+
+ if (svc->client_connected && svc->client_sq.sq_node == node) {
+ queue_work(svc->uaudio_wq, &svc->qmi_disconnect_work);
+ svc->client_sq.sq_node = 0;
+ svc->client_sq.sq_port = 0;
+ svc->client_sq.sq_family = 0;
+ svc->client_connected = false;
+ }
+}
+
+/**
+ * qmi_svc_disconnect_cb() - qmi client disconnected
+ * @handle: QMI handle
+ * @node: id of the dying node
+ * @port: port of the dying client
+ *
+ * Invoked when the remote QMI client is disconnected. Handle this event
+ * the same way as when the QMI bye message is received. This will ensure
+ * the USB offloading path is disabled and cleaned up.
+ *
+ */
+static void qmi_svc_disconnect_cb(struct qmi_handle *handle,
+ unsigned int node, unsigned int port)
+{
+ struct uaudio_qmi_svc *svc;
+
+ if (uaudio_svc == NULL)
+ return;
+
+ svc = uaudio_svc;
+ if (svc->uaudio_svc_hdl != handle)
+ return;
+
+ if (svc->client_connected && svc->client_sq.sq_node == node &&
+ svc->client_sq.sq_port == port) {
+ queue_work(svc->uaudio_wq, &svc->qmi_disconnect_work);
+ svc->client_sq.sq_node = 0;
+ svc->client_sq.sq_port = 0;
+ svc->client_sq.sq_family = 0;
+ svc->client_connected = false;
+ }
+}
+
+/* QMI client callback handlers from QMI interface */
+static struct qmi_ops uaudio_svc_ops_options = {
+ .bye = qmi_bye_cb,
+ .del_client = qmi_svc_disconnect_cb,
+};
+
+static enum usb_audio_device_speed_enum_v01
+get_speed_info(enum usb_device_speed udev_speed)
+{
+ switch (udev_speed) {
+ case USB_SPEED_LOW:
+ return USB_AUDIO_DEVICE_SPEED_LOW_V01;
+ case USB_SPEED_FULL:
+ return USB_AUDIO_DEVICE_SPEED_FULL_V01;
+ case USB_SPEED_HIGH:
+ return USB_AUDIO_DEVICE_SPEED_HIGH_V01;
+ case USB_SPEED_SUPER:
+ return USB_AUDIO_DEVICE_SPEED_SUPER_V01;
+ case USB_SPEED_SUPER_PLUS:
+ return USB_AUDIO_DEVICE_SPEED_SUPER_PLUS_V01;
+ default:
+ return USB_AUDIO_DEVICE_SPEED_INVALID_V01;
+ }
+}
+
+/* Offloading IOMMU management */
+static unsigned long uaudio_get_iova(unsigned long *curr_iova,
+ size_t *curr_iova_size, struct list_head *head, size_t size)
+{
+ struct iova_info *info, *new_info = NULL;
+ struct list_head *curr_head;
+ unsigned long va = 0;
+ size_t tmp_size = size;
+ bool found = false;
+
+ if (size % PAGE_SIZE) {
+ dev_err(uaudio_qdev->dev, "size %zu is not page size multiple\n",
+ size);
+ goto done;
+ }
+
+ if (size > *curr_iova_size) {
+ dev_err(uaudio_qdev->dev, "size %zu > curr size %zu\n",
+ size, *curr_iova_size);
+ goto done;
+ }
+ if (*curr_iova_size == 0) {
+ dev_err(uaudio_qdev->dev, "iova mapping is full\n");
+ goto done;
+ }
+
+ list_for_each_entry(info, head, list) {
+ /* exact size iova_info */
+ if (!info->in_use && info->size == size) {
+ info->in_use = true;
+ va = info->start_iova;
+ *curr_iova_size -= size;
+ found = true;
+ dev_dbg(uaudio_qdev->dev, "exact size: %zu found\n", size);
+ goto done;
+ } else if (!info->in_use && tmp_size >= info->size) {
+ if (!new_info)
+ new_info = info;
+ dev_dbg(uaudio_qdev->dev, "partial size: %zu found\n",
+ info->size);
+ tmp_size -= info->size;
+ if (tmp_size)
+ continue;
+
+ va = new_info->start_iova;
+ for (curr_head = &new_info->list; curr_head !=
+ &info->list; curr_head = curr_head->next) {
+ new_info = list_entry(curr_head, struct
+ iova_info, list);
+ new_info->in_use = true;
+ }
+ info->in_use = true;
+ *curr_iova_size -= size;
+ found = true;
+ goto done;
+ } else {
+ /* iova region in use */
+ new_info = NULL;
+ tmp_size = size;
+ }
+ }
+
+ info = kzalloc(sizeof(struct iova_info), GFP_KERNEL);
+ if (!info) {
+ va = 0;
+ goto done;
+ }
+
+ va = info->start_iova = *curr_iova;
+ info->size = size;
+ info->in_use = true;
+ *curr_iova += size;
+ *curr_iova_size -= size;
+ found = true;
+ list_add_tail(&info->list, head);
+
+done:
+ if (!found)
+ dev_err(uaudio_qdev->dev, "unable to find %zu size iova\n",
+ size);
+ else
+ dev_dbg(uaudio_qdev->dev,
+ "va:0x%08lx curr_iova:0x%08lx curr_iova_size:%zu\n",
+ va, *curr_iova, *curr_iova_size);
+
+ return va;
+}
+
+/**
+ * uaudio_iommu_map() - maps iommu memory for adsp
+ * @mtype: ring type
+ * @dma_coherent: dma coherent
+ * @pa: physical address for ring/buffer
+ * @size: size of memory region
+ * @sgt: sg table for memory region
+ *
+ * Maps the XHCI related resources to a memory region that is assigned to be
+ * used by the adsp. This will be mapped to the domain, which is created by
+ * the ASoC USB backend driver.
+ *
+ */
+static unsigned long uaudio_iommu_map(enum mem_type mtype, bool dma_coherent,
+ phys_addr_t pa, size_t size, struct sg_table *sgt)
+{
+ unsigned long va_sg, va = 0;
+ bool map = true;
+ int i, ret;
+ size_t sg_len, total_len = 0;
+ struct scatterlist *sg;
+ phys_addr_t pa_sg;
+ int prot = IOMMU_READ | IOMMU_WRITE;
+
+ if (dma_coherent)
+ prot |= IOMMU_CACHE;
+
+ switch (mtype) {
+ case MEM_EVENT_RING:
+ va = IOVA_BASE;
+ /* er already mapped */
+ if (uaudio_qdev->er_mapped)
+ map = false;
+ break;
+ case MEM_XFER_RING:
+ va = uaudio_get_iova(&uaudio_qdev->curr_xfer_ring_iova,
+ &uaudio_qdev->xfer_ring_iova_size, &uaudio_qdev->xfer_ring_list,
+ size);
+ break;
+ case MEM_XFER_BUF:
+ va = uaudio_get_iova(&uaudio_qdev->curr_xfer_buf_iova,
+ &uaudio_qdev->xfer_buf_iova_size, &uaudio_qdev->xfer_buf_list,
+ size);
+ break;
+ default:
+ dev_err(uaudio_qdev->dev, "unknown mem type %d\n", mtype);
+ }
+
+ if (!va || !map)
+ goto done;
+
+ if (!sgt)
+ goto skip_sgt_map;
+
+ va_sg = va;
+ for_each_sg(sgt->sgl, sg, sgt->nents, i) {
+ sg_len = PAGE_ALIGN(sg->offset + sg->length);
+ pa_sg = page_to_phys(sg_page(sg));
+ ret = iommu_map(uaudio_qdev->domain, va_sg, pa_sg, sg_len,
+ prot);
+ if (ret) {
+ dev_err(uaudio_qdev->dev, "mapping failed ret%d\n", ret);
+ dev_err(uaudio_qdev->dev,
+ "type:%d, pa:%pa iova:0x%08lx sg_len:%zu\n",
+ mtype, &pa_sg, va_sg, sg_len);
+ uaudio_iommu_unmap(MEM_XFER_BUF, va, size, total_len);
+ va = 0;
+ goto done;
+ }
+ dev_dbg(uaudio_qdev->dev,
+ "type:%d map pa:%pa to iova:0x%08lx len:%zu offset:%u\n",
+ mtype, &pa_sg, va_sg, sg_len, sg->offset);
+ va_sg += sg_len;
+ total_len += sg_len;
+ }
+
+ if (size != total_len) {
+ dev_err(uaudio_qdev->dev, "iova size %zu != mapped iova size %zu\n",
+ size, total_len);
+ uaudio_iommu_unmap(MEM_XFER_BUF, va, size, total_len);
+ va = 0;
+ }
+ return va;
+
+skip_sgt_map:
+ dev_dbg(uaudio_qdev->dev, "type:%d map pa:%pa to iova:0x%08lx size:%zu\n",
+ mtype, &pa, va, size);
+
+ ret = iommu_map(uaudio_qdev->domain, va, pa, size, prot);
+ if (ret)
+ dev_err(uaudio_qdev->dev,
+ "failed to map pa:%pa iova:0x%lx type:%d ret:%d\n",
+ &pa, va, mtype, ret);
+done:
+ return va;
+}
+
+static void uaudio_put_iova(unsigned long va, size_t size, struct list_head
+ *head, size_t *curr_iova_size)
+{
+ struct iova_info *info;
+ size_t tmp_size = size;
+ bool found = false;
+
+ list_for_each_entry(info, head, list) {
+ if (info->start_iova == va) {
+ if (!info->in_use) {
+ dev_err(uaudio_qdev->dev, "va %lu is not in use\n",
+ va);
+ return;
+ }
+ found = true;
+ info->in_use = false;
+ if (info->size == size)
+ goto done;
+ }
+
+ if (found && tmp_size >= info->size) {
+ info->in_use = false;
+ tmp_size -= info->size;
+ if (!tmp_size)
+ goto done;
+ }
+ }
+
+ if (!found) {
+ dev_err(uaudio_qdev->dev, "unable to find the va %lu\n", va);
+ return;
+ }
+done:
+ *curr_iova_size += size;
+ dev_dbg(uaudio_qdev->dev, "curr_iova_size %zu\n", *curr_iova_size);
+}
+
+/**
+ * uaudio_iommu_unmap() - unmaps iommu memory for adsp
+ * @mtype: ring type
+ * @va: virtual address to unmap
+ * @iova_size: region size
+ * @mapped_iova_size: mapped region size
+ *
+ * Unmaps the memory region that was previously assigned to the adsp.
+ *
+ */
+static void uaudio_iommu_unmap(enum mem_type mtype, unsigned long va,
+ size_t iova_size, size_t mapped_iova_size)
+{
+ size_t umap_size;
+ bool unmap = true;
+
+ if (!va || !iova_size)
+ return;
+
+ switch (mtype) {
+ case MEM_EVENT_RING:
+ if (uaudio_qdev->er_mapped)
+ uaudio_qdev->er_mapped = false;
+ else
+ unmap = false;
+ break;
+
+ case MEM_XFER_RING:
+ uaudio_put_iova(va, iova_size, &uaudio_qdev->xfer_ring_list,
+ &uaudio_qdev->xfer_ring_iova_size);
+ break;
+ case MEM_XFER_BUF:
+ uaudio_put_iova(va, iova_size, &uaudio_qdev->xfer_buf_list,
+ &uaudio_qdev->xfer_buf_iova_size);
+ break;
+ default:
+ dev_err(uaudio_qdev->dev, "unknown mem type %d\n", mtype);
+ unmap = false;
+ }
+
+ if (!unmap || !mapped_iova_size)
+ return;
+
+ dev_dbg(uaudio_qdev->dev, "type %d: unmap iova 0x%08lx size %zu\n",
+ mtype, va, mapped_iova_size);
+
+ umap_size = iommu_unmap(uaudio_qdev->domain, va, mapped_iova_size);
+ if (umap_size != mapped_iova_size)
+ dev_err(uaudio_qdev->dev,
+ "unmapped size %zu for iova 0x%08lx of mapped size %zu\n",
+ umap_size, va, mapped_iova_size);
+}
+
+/* looks up alias, if any, for controller DT node and returns the index */
+static int usb_get_controller_id(struct usb_device *udev)
+{
+ if (udev->bus->sysdev && udev->bus->sysdev->of_node)
+ return of_alias_get_id(udev->bus->sysdev->of_node, "usb");
+
+ return -ENODEV;
+}
+
+/**
+ * uaudio_dev_intf_cleanup() - cleanup transfer resources
+ * @udev: usb device
+ * @info: usb offloading interface
+ *
+ * Cleans up the transfer ring related resources which are assigned per
+ * endpoint from XHCI. This is invoked when the USB endpoints are no
+ * longer in use by the adsp.
+ *
+ */
+static void uaudio_dev_intf_cleanup(struct usb_device *udev,
+ struct intf_info *info)
+{
+ uaudio_iommu_unmap(MEM_XFER_RING, info->data_xfer_ring_va,
+ info->data_xfer_ring_size, info->data_xfer_ring_size);
+ info->data_xfer_ring_va = 0;
+ info->data_xfer_ring_size = 0;
+
+ uaudio_iommu_unmap(MEM_XFER_RING, info->sync_xfer_ring_va,
+ info->sync_xfer_ring_size, info->sync_xfer_ring_size);
+ info->sync_xfer_ring_va = 0;
+ info->sync_xfer_ring_size = 0;
+
+ uaudio_iommu_unmap(MEM_XFER_BUF, info->xfer_buf_va,
+ info->xfer_buf_size, info->xfer_buf_size);
+ info->xfer_buf_va = 0;
+
+ usb_free_coherent(udev, info->xfer_buf_size,
+ info->xfer_buf, info->xfer_buf_pa);
+ info->xfer_buf_size = 0;
+ info->xfer_buf = NULL;
+ info->xfer_buf_pa = 0;
+
+ info->in_use = false;
+}
+
+/**
+ * uaudio_event_ring_cleanup_free() - cleanup secondary event ring
+ * @dev: usb offload device
+ *
+ * Cleans up the secondary event ring that was requested. This will
+ * occur when the adsp is no longer transferring data on the USB bus
+ * across all endpoints.
+ *
+ */
+static void uaudio_event_ring_cleanup_free(struct uaudio_dev *dev)
+{
+ struct usb_hcd *hcd = bus_to_hcd(dev->udev->bus);
+
+ clear_bit(dev->card_num, &uaudio_qdev->card_slot);
+ /* all audio devices are disconnected */
+ if (!uaudio_qdev->card_slot) {
+ uaudio_iommu_unmap(MEM_EVENT_RING, IOVA_BASE, PAGE_SIZE,
+ PAGE_SIZE);
+ xhci_remove_secondary_interrupter(hcd, uaudio_qdev->ir);
+ uaudio_qdev->ir = NULL;
+ }
+}
+
+/* kref release callback when all streams are disabled */
+static void uaudio_dev_release(struct kref *kref)
+{
+ struct uaudio_dev *dev = container_of(kref, struct uaudio_dev, kref);
+
+ uaudio_event_ring_cleanup_free(dev);
+ atomic_set(&dev->in_use, 0);
+ wake_up(&dev->disconnect_wq);
+}
+
+static struct snd_usb_substream *find_substream(unsigned int card_num,
+ unsigned int pcm_idx, unsigned int direction)
+{
+ struct snd_usb_stream *as;
+ struct snd_usb_substream *subs = NULL;
+ struct snd_usb_audio *chip;
+
+ chip = uadev[card_num].chip;
+ if (!chip || atomic_read(&chip->shutdown))
+ goto done;