| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2022-2025 Qualcomm Innovation Center, Inc. All rights reserved. |
| */ |
| |
| #include <linux/auxiliary_bus.h> |
| #include <linux/ctype.h> |
| #include <linux/dma-mapping.h> |
| #include <linux/dma-map-ops.h> |
| #include <linux/init.h> |
| #include <linux/iommu.h> |
| #include <linux/module.h> |
| #include <linux/moduleparam.h> |
| #include <linux/soc/qcom/qmi.h> |
| #include <linux/usb.h> |
| #include <linux/usb/audio.h> |
| #include <linux/usb/audio-v2.h> |
| #include <linux/usb/audio-v3.h> |
| #include <linux/usb/hcd.h> |
| #include <linux/usb/quirks.h> |
| #include <linux/usb/xhci-sideband.h> |
| |
| #include <sound/control.h> |
| #include <sound/core.h> |
| #include <sound/info.h> |
| #include <sound/initval.h> |
| #include <sound/pcm.h> |
| #include <sound/pcm_params.h> |
| #include <sound/q6usboffload.h> |
| #include <sound/soc.h> |
| #include <sound/soc-usb.h> |
| |
| #include "../usbaudio.h" |
| #include "../card.h" |
| #include "../endpoint.h" |
| #include "../format.h" |
| #include "../helper.h" |
| #include "../pcm.h" |
| #include "../power.h" |
| |
| #include "mixer_usb_offload.h" |
| #include "usb_audio_qmi_v01.h" |
| |
| /* Stream disable request timeout during USB device disconnect */ |
| #define DEV_RELEASE_WAIT_TIMEOUT 10000 /* in ms */ |
| |
| /* Data interval calculation parameters */ |
| #define BUS_INTERVAL_FULL_SPEED 1000 /* in us */ |
| #define BUS_INTERVAL_HIGHSPEED_AND_ABOVE 125 /* in us */ |
| #define MAX_BINTERVAL_ISOC_EP 16 |
| |
| #define QMI_STREAM_REQ_CARD_NUM_MASK 0xffff0000 |
| #define QMI_STREAM_REQ_DEV_NUM_MASK 0xff00 |
| #define QMI_STREAM_REQ_DIRECTION 0xff |
| |
| /* iommu resource parameters and management */ |
| #define PREPEND_SID_TO_IOVA(iova, sid) ((u64)(((u64)(iova)) | \ |
| (((u64)sid) << 32))) |
| #define IOVA_MASK(iova) (((u64)(iova)) & 0xFFFFFFFF) |
| #define IOVA_BASE 0x1000 |
| #define IOVA_XFER_RING_BASE (IOVA_BASE + PAGE_SIZE * (SNDRV_CARDS + 1)) |
| #define IOVA_XFER_BUF_BASE (IOVA_XFER_RING_BASE + PAGE_SIZE * SNDRV_CARDS * 32) |
| #define IOVA_XFER_RING_MAX (IOVA_XFER_BUF_BASE - PAGE_SIZE) |
| #define IOVA_XFER_BUF_MAX (0xfffff000 - PAGE_SIZE) |
| |
| #define MAX_XFER_BUFF_LEN (24 * PAGE_SIZE) |
| |
| struct iova_info { |
| struct list_head list; |
| unsigned long start_iova; |
| size_t size; |
| bool in_use; |
| }; |
| |
| struct intf_info { |
| /* IOMMU ring/buffer mapping information */ |
| unsigned long data_xfer_ring_va; |
| size_t data_xfer_ring_size; |
| unsigned long sync_xfer_ring_va; |
| size_t sync_xfer_ring_size; |
| dma_addr_t xfer_buf_iova; |
| size_t xfer_buf_size; |
| dma_addr_t xfer_buf_dma; |
| u8 *xfer_buf_cpu; |
| |
| /* USB endpoint information */ |
| unsigned int data_ep_pipe; |
| unsigned int sync_ep_pipe; |
| unsigned int data_ep_idx; |
| unsigned int sync_ep_idx; |
| |
| u8 intf_num; |
| u8 pcm_card_num; |
| u8 pcm_dev_num; |
| u8 direction; |
| bool in_use; |
| }; |
| |
| struct uaudio_qmi_dev { |
| struct device *dev; |
| struct q6usb_offload *data; |
| struct auxiliary_device *auxdev; |
| |
| /* list to keep track of available iova */ |
| struct list_head xfer_ring_list; |
| size_t xfer_ring_iova_size; |
| unsigned long curr_xfer_ring_iova; |
| struct list_head xfer_buf_list; |
| size_t xfer_buf_iova_size; |
| unsigned long curr_xfer_buf_iova; |
| |
| /* bit fields representing pcm card enabled */ |
| unsigned long card_slot; |
| /* indicate event ring mapped or not */ |
| bool er_mapped; |
| }; |
| |
| struct uaudio_dev { |
| struct usb_device *udev; |
| /* audio control interface */ |
| struct usb_host_interface *ctrl_intf; |
| unsigned int usb_core_id; |
| atomic_t in_use; |
| struct kref kref; |
| wait_queue_head_t disconnect_wq; |
| |
| /* interface specific */ |
| int num_intf; |
| struct intf_info *info; |
| struct snd_usb_audio *chip; |
| |
| /* xhci sideband */ |
| struct xhci_sideband *sb; |
| |
| /* SoC USB device */ |
| struct snd_soc_usb_device *sdev; |
| }; |
| |
| static struct uaudio_dev uadev[SNDRV_CARDS]; |
| static struct uaudio_qmi_dev *uaudio_qdev; |
| static struct uaudio_qmi_svc *uaudio_svc; |
| static DEFINE_MUTEX(qdev_mutex); |
| |
| struct uaudio_qmi_svc { |
| struct qmi_handle *uaudio_svc_hdl; |
| struct sockaddr_qrtr client_sq; |
| bool client_connected; |
| }; |
| |
| enum mem_type { |
| MEM_EVENT_RING, |
| MEM_XFER_RING, |
| MEM_XFER_BUF, |
| }; |
| |
| /* Supported audio formats */ |
| enum usb_qmi_audio_format { |
| USB_QMI_PCM_FORMAT_S8 = 0, |
| USB_QMI_PCM_FORMAT_U8, |
| USB_QMI_PCM_FORMAT_S16_LE, |
| USB_QMI_PCM_FORMAT_S16_BE, |
| USB_QMI_PCM_FORMAT_U16_LE, |
| USB_QMI_PCM_FORMAT_U16_BE, |
| USB_QMI_PCM_FORMAT_S24_LE, |
| USB_QMI_PCM_FORMAT_S24_BE, |
| USB_QMI_PCM_FORMAT_U24_LE, |
| USB_QMI_PCM_FORMAT_U24_BE, |
| USB_QMI_PCM_FORMAT_S24_3LE, |
| USB_QMI_PCM_FORMAT_S24_3BE, |
| USB_QMI_PCM_FORMAT_U24_3LE, |
| USB_QMI_PCM_FORMAT_U24_3BE, |
| USB_QMI_PCM_FORMAT_S32_LE, |
| USB_QMI_PCM_FORMAT_S32_BE, |
| USB_QMI_PCM_FORMAT_U32_LE, |
| USB_QMI_PCM_FORMAT_U32_BE, |
| }; |
| |
| static int usb_qmi_get_pcm_num(struct snd_usb_audio *chip, int direction) |
| { |
| struct snd_usb_substream *subs = NULL; |
| struct snd_usb_stream *as; |
| int count = 0; |
| |
| list_for_each_entry(as, &chip->pcm_list, list) { |
| subs = &as->substream[direction]; |
| if (subs->ep_num) |
| count++; |
| } |
| |
| return count; |
| } |
| |
| static enum usb_qmi_audio_device_speed_enum_v01 |
| get_speed_info(enum usb_device_speed udev_speed) |
| { |
| switch (udev_speed) { |
| case USB_SPEED_LOW: |
| return USB_QMI_DEVICE_SPEED_LOW_V01; |
| case USB_SPEED_FULL: |
| return USB_QMI_DEVICE_SPEED_FULL_V01; |
| case USB_SPEED_HIGH: |
| return USB_QMI_DEVICE_SPEED_HIGH_V01; |
| case USB_SPEED_SUPER: |
| return USB_QMI_DEVICE_SPEED_SUPER_V01; |
| case USB_SPEED_SUPER_PLUS: |
| return USB_QMI_DEVICE_SPEED_SUPER_PLUS_V01; |
| default: |
| return USB_QMI_DEVICE_SPEED_INVALID_V01; |
| } |
| } |
| |
| static struct snd_usb_substream *find_substream(unsigned int card_num, |
| unsigned int pcm_idx, |
| unsigned int direction) |
| { |
| struct snd_usb_substream *subs = NULL; |
| struct snd_usb_audio *chip; |
| struct snd_usb_stream *as; |
| |
| chip = uadev[card_num].chip; |
| if (!chip || atomic_read(&chip->shutdown)) |
| goto done; |
| |
| if (pcm_idx >= chip->pcm_devs) |
| goto done; |
| |
| if (direction > SNDRV_PCM_STREAM_CAPTURE) |
| goto done; |
| |
| list_for_each_entry(as, &chip->pcm_list, list) { |
| if (as->pcm_index == pcm_idx) { |
| subs = &as->substream[direction]; |
| goto done; |
| } |
| } |
| |
| done: |
| return subs; |
| } |
| |
| static int info_idx_from_ifnum(int card_num, int intf_num, bool enable) |
| { |
| int i; |
| |
| /* |
| * default index 0 is used when info is allocated upon |
| * first enable audio stream req for a pcm device |
| */ |
| if (enable && !uadev[card_num].info) |
| return 0; |
| |
| for (i = 0; i < uadev[card_num].num_intf; i++) { |
| if (enable && !uadev[card_num].info[i].in_use) |
| return i; |
| else if (!enable && |
| uadev[card_num].info[i].intf_num == intf_num) |
| return i; |
| } |
| |
| return -EINVAL; |
| } |
| |
| static int get_data_interval_from_si(struct snd_usb_substream *subs, |
| u32 service_interval) |
| { |
| unsigned int bus_intval_mult; |
| unsigned int bus_intval; |
| unsigned int binterval; |
| |
| if (subs->dev->speed >= USB_SPEED_HIGH) |
| bus_intval = BUS_INTERVAL_HIGHSPEED_AND_ABOVE; |
| else |
| bus_intval = BUS_INTERVAL_FULL_SPEED; |
| |
| if (service_interval % bus_intval) |
| return -EINVAL; |
| |
| bus_intval_mult = service_interval / bus_intval; |
| binterval = ffs(bus_intval_mult); |
| if (!binterval || binterval > MAX_BINTERVAL_ISOC_EP) |
| return -EINVAL; |
| |
| /* check if another bit is set then bail out */ |
| bus_intval_mult = bus_intval_mult >> binterval; |
| if (bus_intval_mult) |
| return -EINVAL; |
| |
| return (binterval - 1); |
| } |
| |
| /* maps audio format received over QMI to asound.h based pcm format */ |
| static snd_pcm_format_t map_pcm_format(enum usb_qmi_audio_format fmt_received) |
| { |
| switch (fmt_received) { |
| case USB_QMI_PCM_FORMAT_S8: |
| return SNDRV_PCM_FORMAT_S8; |
| case USB_QMI_PCM_FORMAT_U8: |
| return SNDRV_PCM_FORMAT_U8; |
| case USB_QMI_PCM_FORMAT_S16_LE: |
| return SNDRV_PCM_FORMAT_S16_LE; |
| case USB_QMI_PCM_FORMAT_S16_BE: |
| return SNDRV_PCM_FORMAT_S16_BE; |
| case USB_QMI_PCM_FORMAT_U16_LE: |
| return SNDRV_PCM_FORMAT_U16_LE; |
| case USB_QMI_PCM_FORMAT_U16_BE: |
| return SNDRV_PCM_FORMAT_U16_BE; |
| case USB_QMI_PCM_FORMAT_S24_LE: |
| return SNDRV_PCM_FORMAT_S24_LE; |
| case USB_QMI_PCM_FORMAT_S24_BE: |
| return SNDRV_PCM_FORMAT_S24_BE; |
| case USB_QMI_PCM_FORMAT_U24_LE: |
| return SNDRV_PCM_FORMAT_U24_LE; |
| case USB_QMI_PCM_FORMAT_U24_BE: |
| return SNDRV_PCM_FORMAT_U24_BE; |
| case USB_QMI_PCM_FORMAT_S24_3LE: |
| return SNDRV_PCM_FORMAT_S24_3LE; |
| case USB_QMI_PCM_FORMAT_S24_3BE: |
| return SNDRV_PCM_FORMAT_S24_3BE; |
| case USB_QMI_PCM_FORMAT_U24_3LE: |
| return SNDRV_PCM_FORMAT_U24_3LE; |
| case USB_QMI_PCM_FORMAT_U24_3BE: |
| return SNDRV_PCM_FORMAT_U24_3BE; |
| case USB_QMI_PCM_FORMAT_S32_LE: |
| return SNDRV_PCM_FORMAT_S32_LE; |
| case USB_QMI_PCM_FORMAT_S32_BE: |
| return SNDRV_PCM_FORMAT_S32_BE; |
| case USB_QMI_PCM_FORMAT_U32_LE: |
| return SNDRV_PCM_FORMAT_U32_LE; |
| case USB_QMI_PCM_FORMAT_U32_BE: |
| return SNDRV_PCM_FORMAT_U32_BE; |
| default: |
| /* |
| * We expect the caller to do input validation so we should |
| * never hit this. But we do have to return a proper |
| * snd_pcm_format_t value due to the __bitwise attribute; so |
| * just return the equivalent of 0 in case of bad input. |
| */ |
| return SNDRV_PCM_FORMAT_S8; |
| } |
| } |
| |
| /* |
| * Sends QMI disconnect indication message, assumes chip->mutex and qdev_mutex |
| * lock held by caller. |
| */ |
| static int uaudio_send_disconnect_ind(struct snd_usb_audio *chip) |
| { |
| struct qmi_uaudio_stream_ind_msg_v01 disconnect_ind = {0}; |
| struct uaudio_qmi_svc *svc = uaudio_svc; |
| struct uaudio_dev *dev; |
| int ret = 0; |
| |
| dev = &uadev[chip->card->number]; |
| |
| if (atomic_read(&dev->in_use)) { |
| mutex_unlock(&chip->mutex); |
| mutex_unlock(&qdev_mutex); |
| dev_dbg(uaudio_qdev->data->dev, "sending qmi indication suspend\n"); |
| disconnect_ind.dev_event = USB_QMI_DEV_DISCONNECT_V01; |
| disconnect_ind.slot_id = dev->udev->slot_id; |
| disconnect_ind.controller_num = dev->usb_core_id; |
| disconnect_ind.controller_num_valid = 1; |
| ret = qmi_send_indication(svc->uaudio_svc_hdl, &svc->client_sq, |
| QMI_UAUDIO_STREAM_IND_V01, |
| QMI_UAUDIO_STREAM_IND_MSG_V01_MAX_MSG_LEN, |
| qmi_uaudio_stream_ind_msg_v01_ei, |
| &disconnect_ind); |
| if (ret < 0) |
| dev_err(uaudio_qdev->data->dev, |
| "qmi send failed with err: %d\n", ret); |
| |
| ret = wait_event_interruptible_timeout(dev->disconnect_wq, |
| !atomic_read(&dev->in_use), |
| msecs_to_jiffies(DEV_RELEASE_WAIT_TIMEOUT)); |
| if (!ret) { |
| dev_err(uaudio_qdev->data->dev, |
| "timeout while waiting for dev_release\n"); |
| atomic_set(&dev->in_use, 0); |
| } else if (ret < 0) { |
| dev_err(uaudio_qdev->data->dev, |
| "failed with ret %d\n", ret); |
| atomic_set(&dev->in_use, 0); |
| } |
| mutex_lock(&qdev_mutex); |
| mutex_lock(&chip->mutex); |
| } |
| |
| return ret; |
| } |
| |
| /* 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; |
| size_t tmp_size = size; |
| unsigned long iova = 0; |
| |
| if (size % PAGE_SIZE) |
| goto done; |
| |
| if (size > *curr_iova_size) |
| goto done; |
| |
| if (*curr_iova_size == 0) |
| goto done; |
| |
| list_for_each_entry(info, head, list) { |
| /* exact size iova_info */ |
| if (!info->in_use && info->size == size) { |
| info->in_use = true; |
| iova = info->start_iova; |
| *curr_iova_size -= size; |
| goto done; |
| } else if (!info->in_use && tmp_size >= info->size) { |
| if (!new_info) |
| new_info = info; |
| tmp_size -= info->size; |
| if (tmp_size) |
| continue; |
| |
| iova = 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; |
| goto done; |
| } else { |
| /* iova region in use */ |
| new_info = NULL; |
| tmp_size = size; |
| } |
| } |
| |
| info = kzalloc(sizeof(*info), GFP_KERNEL); |
| if (!info) { |
| iova = 0; |
| goto done; |
| } |
| |
| iova = *curr_iova; |
| info->start_iova = *curr_iova; |
| info->size = size; |
| info->in_use = true; |
| *curr_iova += size; |
| *curr_iova_size -= size; |
| list_add_tail(&info->list, head); |
| |
| done: |
| return iova; |
| } |
| |
| static void uaudio_put_iova(unsigned long iova, 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 == iova) { |
| if (!info->in_use) |
| 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) |
| return; |
| |
| done: |
| *curr_iova_size += size; |
| } |
| |
| /** |
| * uaudio_iommu_unmap() - unmaps iommu memory for adsp |
| * @mtype: ring type |
| * @iova: 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 iova, |
| size_t iova_size, size_t mapped_iova_size) |
| { |
| size_t umap_size; |
| bool unmap = true; |
| |
| if (!iova || !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(iova, iova_size, &uaudio_qdev->xfer_ring_list, |
| &uaudio_qdev->xfer_ring_iova_size); |
| break; |
| case MEM_XFER_BUF: |
| uaudio_put_iova(iova, iova_size, &uaudio_qdev->xfer_buf_list, |
| &uaudio_qdev->xfer_buf_iova_size); |
| break; |
| default: |
| unmap = false; |
| } |
| |
| if (!unmap || !mapped_iova_size) |
| return; |
| |
| umap_size = iommu_unmap(uaudio_qdev->data->domain, iova, mapped_iova_size); |
| if (umap_size != mapped_iova_size) |
| dev_err(uaudio_qdev->data->dev, |
| "unmapped size %zu for iova 0x%08lx of mapped size %zu\n", |
| umap_size, iova, mapped_iova_size); |
| } |
| |
| /** |
| * 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) |
| { |
| struct scatterlist *sg; |
| unsigned long iova = 0; |
| size_t total_len = 0; |
| unsigned long iova_sg; |
| phys_addr_t pa_sg; |
| bool map = true; |
| size_t sg_len; |
| int prot; |
| int ret; |
| int i; |
| |
| prot = IOMMU_READ | IOMMU_WRITE; |
| |
| if (dma_coherent) |
| prot |= IOMMU_CACHE; |
| |
| switch (mtype) { |
| case MEM_EVENT_RING: |
| iova = IOVA_BASE; |
| /* er already mapped */ |
| if (uaudio_qdev->er_mapped) |
| map = false; |
| break; |
| case MEM_XFER_RING: |
| iova = 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: |
| iova = 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->data->dev, "unknown mem type %d\n", mtype); |
| } |
| |
| if (!iova || !map) |
| goto done; |
| |
| if (!sgt) |
| goto skip_sgt_map; |
| |
| iova_sg = iova; |
| 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->data->domain, iova_sg, pa_sg, sg_len, |
| prot, GFP_KERNEL); |
| if (ret) { |
| uaudio_iommu_unmap(MEM_XFER_BUF, iova, size, total_len); |
| iova = 0; |
| goto done; |
| } |
| |
| iova_sg += sg_len; |
| total_len += sg_len; |
| } |
| |
| if (size != total_len) { |
| uaudio_iommu_unmap(MEM_XFER_BUF, iova, size, total_len); |
| iova = 0; |
| } |
| return iova; |
| |
| skip_sgt_map: |
| iommu_map(uaudio_qdev->data->domain, iova, pa, size, prot, GFP_KERNEL); |
| |
| done: |
| return iova; |
| } |
| |
| /* 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_iova, info->xfer_buf_size, |
| info->xfer_buf_size); |
| info->xfer_buf_iova = 0; |
| |
| usb_free_coherent(udev, info->xfer_buf_size, info->xfer_buf_cpu, |
| info->xfer_buf_dma); |
| info->xfer_buf_size = 0; |
| info->xfer_buf_cpu = NULL; |
| info->xfer_buf_dma = 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) |
| { |
| clear_bit(dev->chip->card->number, &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_sideband_remove_interrupter(uadev[dev->chip->card->number].sb); |
| } |
| } |
| |
| static void uaudio_dev_cleanup(struct uaudio_dev *dev) |
| { |
| int if_idx; |
| |
| if (!dev->udev) |
| return; |
| |
| /* free xfer buffer and unmap xfer ring and buf per interface */ |
| for (if_idx = 0; if_idx < dev->num_intf; if_idx++) { |
| if (!dev->info[if_idx].in_use) |
| continue; |
| uaudio_dev_intf_cleanup(dev->udev, &dev->info[if_idx]); |
| dev_dbg(uaudio_qdev->data->dev, |
| "release resources: intf# %d card# %d\n", |
| dev->info[if_idx].intf_num, dev->chip->card->number); |
| } |
| |
| dev->num_intf = 0; |
| |
| /* free interface info */ |
| kfree(dev->info); |
| dev->info = NULL; |
| uaudio_event_ring_cleanup_free(dev); |
| dev->udev = NULL; |
| } |
| |
| /** |
| * disable_audio_stream() - disable usb snd endpoints |
| * @subs: usb substream |
| * |
| * Closes the USB SND endpoints associated with the current audio stream |
| * used. This will decrement the USB SND endpoint opened reference count. |
| * |
| */ |
| static void disable_audio_stream(struct snd_usb_substream *subs) |
| { |
| struct snd_usb_audio *chip = subs->stream->chip; |
| |
| snd_usb_hw_free(subs); |
| snd_usb_autosuspend(chip); |
| } |
| |
| /* QMI service disconnect handlers */ |
| static void qmi_stop_session(void) |
| { |
| struct snd_usb_substream *subs; |
| struct usb_host_endpoint *ep; |
| struct snd_usb_audio *chip; |
| struct intf_info *info; |
| int pcm_card_num; |
| int if_idx; |
| int idx; |
| |
| mutex_lock(&qdev_mutex); |
| /* 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]; |
| pcm_card_num = info->pcm_card_num; |
| subs = find_substream(pcm_card_num, info->pcm_dev_num, |
| info->direction); |
| if (!subs || !chip || atomic_read(&chip->shutdown)) { |
| dev_err(&uadev[idx].udev->dev, |
| "no sub for c#%u dev#%u dir%u\n", |
| info->pcm_card_num, |
| info->pcm_dev_num, |
| info->direction); |
| continue; |
| } |
| /* Release XHCI endpoints */ |
| if (info->data_ep_pipe) |
| ep = usb_pipe_endpoint(uadev[pcm_card_num].udev, |
| info->data_ep_pipe); |
| xhci_sideband_remove_endpoint(uadev[pcm_card_num].sb, ep); |
| |
| if (info->sync_ep_pipe) |
| ep = usb_pipe_endpoint(uadev[pcm_card_num].udev, |
| info->sync_ep_pipe); |
| xhci_sideband_remove_endpoint(uadev[pcm_card_num].sb, ep); |
| |
| disable_audio_stream(subs); |
| } |
| atomic_set(&uadev[idx].in_use, 0); |
| mutex_lock(&chip->mutex); |
| uaudio_dev_cleanup(&uadev[idx]); |
| mutex_unlock(&chip->mutex); |
| } |
| mutex_unlock(&qdev_mutex); |
| } |
| |
| /** |
| * uaudio_sideband_notifier() - xHCI sideband event handler |
| * @intf: USB interface handle |
| * @evt: xHCI sideband event type |
| * |
| * This callback is executed when the xHCI sideband encounters a sequence |
| * that requires the sideband clients to take action. An example, is when |
| * xHCI frees the transfer ring, so the client has to ensure that the |
| * offload path is halted. |
| * |
| */ |
| static int uaudio_sideband_notifier(struct usb_interface *intf, |
| struct xhci_sideband_event *evt) |
| { |
| struct snd_usb_audio *chip; |
| struct uaudio_dev *dev; |
| int if_idx; |
| |
| if (!intf || !evt) |
| return 0; |
| |
| chip = usb_get_intfdata(intf); |
| |
| mutex_lock(&qdev_mutex); |
| mutex_lock(&chip->mutex); |
| |
| dev = &uadev[chip->card->number]; |
| |
| if (evt->type == XHCI_SIDEBAND_XFER_RING_FREE) { |
| unsigned int *ep = (unsigned int *) evt->evt_data; |
| |
| for (if_idx = 0; if_idx < dev->num_intf; if_idx++) { |
| if (dev->info[if_idx].data_ep_idx == *ep || |
| dev->info[if_idx].sync_ep_idx == *ep) |
| uaudio_send_disconnect_ind(chip); |
| } |
| } |
| |
| mutex_unlock(&chip->mutex); |
| mutex_unlock(&qdev_mutex); |
| |
| return 0; |
| } |
| |
| /** |
| * 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) { |
| qmi_stop_session(); |
| |
| /* clear QMI client parameters to block further QMI messages */ |
| 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) |
| 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) { |
| qmi_stop_session(); |
| |
| /* clear QMI client parameters to block further QMI messages */ |
| 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, |
| }; |
| |
| /* 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); |
| } |
| |
| /** |
| * enable_audio_stream() - enable usb snd endpoints |
| * @subs: usb substream |
| * @pcm_format: pcm format requested |
| * @channels: number of channels |
| * @cur_rate: sample rate |
| * @datainterval: interval |
| * |
| * Opens all USB SND endpoints used for the data interface. This will increment |
| * the USB SND endpoint's opened count. Requests to keep the interface resumed |
| * until the audio stream is stopped. Will issue the USB set interface control |
| * message to enable the data interface. |
| * |
| */ |
| static int enable_audio_stream(struct snd_usb_substream *subs, |
| snd_pcm_format_t pcm_format, |
| unsigned int channels, unsigned int cur_rate, |
| int datainterval) |
| { |
| struct snd_pcm_hw_params params; |
| struct snd_usb_audio *chip; |
| struct snd_interval *i; |
| struct snd_mask *m; |
| int ret; |
| |
| chip = subs->stream->chip; |
| |
| _snd_pcm_hw_params_any(¶ms); |
| |
| m = hw_param_mask(¶ms, SNDRV_PCM_HW_PARAM_FORMAT); |
| snd_mask_leave(m, pcm_format); |
| |
| i = hw_param_interval(¶ms, SNDRV_PCM_HW_PARAM_CHANNELS); |
| snd_interval_setinteger(i); |
| i->min = channels; |
| i->max = channels; |
| |
| i = hw_param_interval(¶ms, SNDRV_PCM_HW_PARAM_RATE); |
| snd_interval_setinteger(i); |
| i->min = cur_rate; |
| i->max = cur_rate; |
| |
| pm_runtime_barrier(&chip->intf[0]->dev); |
| snd_usb_autoresume(chip); |
| |
| ret = snd_usb_hw_params(subs, ¶ms); |
| if (ret < 0) |
| goto put_suspend; |
| |
| if (!atomic_read(&chip->shutdown)) { |
| ret = snd_usb_lock_shutdown(chip); |
| if (ret < 0) |
| goto detach_ep; |
| |
| if (subs->sync_endpoint) { |
| ret = snd_usb_endpoint_prepare(chip, subs->sync_endpoint); |
| if (ret < 0) |
| goto unlock; |
| } |
| |
| ret = snd_usb_endpoint_prepare(chip, subs->data_endpoint); |
| if (ret < 0) |
| goto unlock; |
| |
| snd_usb_unlock_shutdown(chip); |
| |
| dev_dbg(uaudio_qdev->data->dev, |
| "selected %s iface:%d altsetting:%d datainterval:%dus\n", |
| subs->direction ? "capture" : "playback", |
| subs->cur_audiofmt->iface, subs->cur_audiofmt->altsetting, |
| (1 << subs->cur_audiofmt->datainterval) * |
| (subs->dev->speed >= USB_SPEED_HIGH ? |
| BUS_INTERVAL_HIGHSPEED_AND_ABOVE : |
| BUS_INTERVAL_FULL_SPEED)); |
| } |
| |
| return 0; |
| |
| unlock: |
| snd_usb_unlock_shutdown(chip); |
| |
| detach_ep: |
| snd_usb_hw_free(subs); |
| |
| put_suspend: |
| snd_usb_autosuspend(chip); |
| |
| return ret; |
| } |
| |
| /** |
| * uaudio_transfer_buffer_setup() - fetch and populate xfer buffer params |
| * @subs: usb substream |
| * @xfer_buf: xfer buf to be allocated |
| * @xfer_buf_len: size of allocation |
| * @mem_info: QMI response info |
| * |
| * Allocates and maps the transfer buffers that will be utilized by the |
| * audio DSP. Will populate the information in the QMI response that is |
| * sent back to the stream enable request. |
| * |
| */ |
| static int uaudio_transfer_buffer_setup(struct snd_usb_substream *subs, |
| void **xfer_buf_cpu, u32 xfer_buf_len, |
| struct mem_info_v01 *mem_info) |
| { |
| struct sg_table xfer_buf_sgt; |
| dma_addr_t xfer_buf_dma; |
| void *xfer_buf; |
| phys_addr_t xfer_buf_pa; |
| u32 len = xfer_buf_len; |
| bool dma_coherent; |
| dma_addr_t xfer_buf_dma_sysdev; |
| u32 remainder; |
| u32 mult; |
| int ret; |
| |
| dma_coherent = dev_is_dma_coherent(subs->dev->bus->sysdev); |
| |
| /* xfer buffer, multiple of 4K only */ |
| if (!len) |
| len = PAGE_SIZE; |
| |
| mult = len / PAGE_SIZE; |
| remainder = len % PAGE_SIZE; |
| len = mult * PAGE_SIZE; |
| len += remainder ? PAGE_SIZE : 0; |
| |
| if (len > MAX_XFER_BUFF_LEN) { |
| dev_err(uaudio_qdev->data->dev, |
| "req buf len %d > max buf len %lu, setting %lu\n", |
| len, MAX_XFER_BUFF_LEN, MAX_XFER_BUFF_LEN); |
| len = MAX_XFER_BUFF_LEN; |
| } |
| |
| /* get buffer mapped into subs->dev */ |
| xfer_buf = usb_alloc_coherent(subs->dev, len, GFP_KERNEL, &xfer_buf_dma); |
| if (!xfer_buf) |
| return -ENOMEM; |
| |
| /* Remapping is not possible if xfer_buf is outside of linear map */ |
| xfer_buf_pa = virt_to_phys(xfer_buf); |
| if (WARN_ON(!page_is_ram(PFN_DOWN(xfer_buf_pa)))) { |
| ret = -ENXIO; |
| goto unmap_sync; |
| } |
| dma_get_sgtable(subs->dev->bus->sysdev, &xfer_buf_sgt, xfer_buf, |
| xfer_buf_dma, len); |
| |
| /* map the physical buffer into sysdev as well */ |
| xfer_buf_dma_sysdev = uaudio_iommu_map(MEM_XFER_BUF, dma_coherent, |
| xfer_buf_pa, len, &xfer_buf_sgt); |
| if (!xfer_buf_dma_sysdev) { |
| ret = -ENOMEM; |
| goto unmap_sync; |
| } |
| |
| mem_info->dma = xfer_buf_dma; |
| mem_info->size = len; |
| mem_info->iova = PREPEND_SID_TO_IOVA(xfer_buf_dma_sysdev, uaudio_qdev->data->sid); |
| *xfer_buf_cpu = xfer_buf; |
| sg_free_table(&xfer_buf_sgt); |
| |
| return 0; |
| |
| unmap_sync: |
| usb_free_coherent(subs->dev, len, xfer_buf, xfer_buf_dma); |
| |
| return ret; |
| } |
| |
| /** |
| * uaudio_endpoint_setup() - fetch and populate endpoint params |
| * @subs: usb substream |
| * @endpoint: usb endpoint to add |
| * @card_num: uadev index |
| * @mem_info: QMI response info |
| * @ep_desc: QMI ep desc response field |
| * |
| * Initialize the USB endpoint being used for a particular USB |
| * stream. Will request XHCI sec intr to reserve the EP for |
| * offloading as well as populating the QMI response with the |
| * transfer ring parameters. |
| * |
| */ |
| static phys_addr_t |
| uaudio_endpoint_setup(struct snd_usb_substream *subs, |
| struct snd_usb_endpoint *endpoint, int card_num, |
| struct mem_info_v01 *mem_info, |
| struct usb_endpoint_descriptor_v01 *ep_desc) |
| { |
| struct usb_host_endpoint *ep; |
| phys_addr_t tr_pa = 0; |
| struct sg_table *sgt; |
| bool dma_coherent; |
| unsigned long iova; |
| struct page *pg; |
| int ret = -ENODEV; |
| |
| dma_coherent = dev_is_dma_coherent(subs->dev->bus->sysdev); |
| |
| ep = usb_pipe_endpoint(subs->dev, endpoint->pipe); |
| if (!ep) { |
| dev_err(uaudio_qdev->data->dev, "data ep # %d context is null\n", |
| subs->data_endpoint->ep_num); |
| goto exit; |
| } |
| |
| memcpy(ep_desc, &ep->desc, sizeof(ep->desc)); |
| |
| ret = xhci_sideband_add_endpoint(uadev[card_num].sb, ep); |
| if (ret < 0) { |
| dev_err(&subs->dev->dev, |
| "failed to add data ep to sec intr\n"); |
| ret = -ENODEV; |
| goto exit; |
| } |
| |
| sgt = xhci_sideband_get_endpoint_buffer(uadev[card_num].sb, ep); |
| if (!sgt) { |
| dev_err(&subs->dev->dev, |
| "failed to get data ep ring address\n"); |
| ret = -ENODEV; |
| goto remove_ep; |
| } |
| |
| pg = sg_page(sgt->sgl); |
| tr_pa = page_to_phys(pg); |
| mem_info->dma = sg_dma_address(sgt->sgl); |
| sg_free_table(sgt); |
| |
| /* data transfer ring */ |
| iova = uaudio_iommu_map(MEM_XFER_RING, dma_coherent, tr_pa, |
| PAGE_SIZE, NULL); |
| if (!iova) { |
| ret = -ENOMEM; |
| goto clear_pa; |
| } |
| |
| mem_info->iova = PREPEND_SID_TO_IOVA(iova, uaudio_qdev->data->sid); |
| mem_info->size = PAGE_SIZE; |
| |
| return 0; |
| |
| clear_pa: |
| mem_info->dma = 0; |
| remove_ep: |
| xhci_sideband_remove_endpoint(uadev[card_num].sb, ep); |
| exit: |
| return ret; |
| } |
| |
| /** |
| * uaudio_event_ring_setup() - fetch and populate event ring params |
| * @subs: usb substream |
| * @card_num: uadev index |
| * @mem_info: QMI response info |
| * |
| * Register secondary interrupter to XHCI and fetch the event buffer info |
| * and populate the information into the QMI response. |
| * |
| */ |
| static int uaudio_event_ring_setup(struct snd_usb_substream *subs, |
| int card_num, struct mem_info_v01 *mem_info) |
| { |
| struct sg_table *sgt; |
| phys_addr_t er_pa; |
| bool dma_coherent; |
| unsigned long iova; |
| struct page *pg; |
| int ret; |
| |
| dma_coherent = dev_is_dma_coherent(subs->dev->bus->sysdev); |
| er_pa = 0; |
| |
| /* event ring */ |
| ret = xhci_sideband_create_interrupter(uadev[card_num].sb, 1, false, |
| 0, uaudio_qdev->data->intr_num); |
| if (ret < 0) { |
| dev_err(&subs->dev->dev, "failed to fetch interrupter\n"); |
| goto exit; |
| } |
| |
| sgt = xhci_sideband_get_event_buffer(uadev[card_num].sb); |
| if (!sgt) { |
| dev_err(&subs->dev->dev, |
| "failed to get event ring address\n"); |
| ret = -ENODEV; |
| goto remove_interrupter; |
| } |
| |
| pg = sg_page(sgt->sgl); |
| er_pa = page_to_phys(pg); |
| mem_info->dma = sg_dma_address(sgt->sgl); |
| sg_free_table(sgt); |
| |
| iova = uaudio_iommu_map(MEM_EVENT_RING, dma_coherent, er_pa, |
| PAGE_SIZE, NULL); |
| if (!iova) { |
| ret = -ENOMEM; |
| goto clear_pa; |
| } |
| |
| mem_info->iova = PREPEND_SID_TO_IOVA(iova, uaudio_qdev->data->sid); |
| mem_info->size = PAGE_SIZE; |
| |
| return 0; |
| |
| clear_pa: |
| mem_info->dma = 0; |
| remove_interrupter: |
| xhci_sideband_remove_interrupter(uadev[card_num].sb); |
| exit: |
| return ret; |
| } |
| |
| /** |
| * uaudio_populate_uac_desc() - parse UAC parameters and populate QMI resp |
| * @subs: usb substream |
| * @resp: QMI response buffer |
| * |
| * Parses information specified within UAC descriptors which explain the |
| * sample parameters that the device expects. This information is populated |
| * to the QMI response sent back to the audio DSP. |
| * |
| */ |
| static int uaudio_populate_uac_desc(struct snd_usb_substream *subs, |
| struct qmi_uaudio_stream_resp_msg_v01 *resp) |
| { |
| struct usb_interface_descriptor *altsd; |
| struct usb_host_interface *alts; |
| struct usb_interface *iface; |
| int protocol; |
| |
| iface = usb_ifnum_to_if(subs->dev, subs->cur_audiofmt->iface); |
| if (!iface) { |
| dev_err(&subs->dev->dev, "interface # %d does not exist\n", |
| subs->cur_audiofmt->iface); |
| return -ENODEV; |
| } |
| |
| alts = &iface->altsetting[subs->cur_audiofmt->altset_idx]; |
| altsd = get_iface_desc(alts); |
| protocol = altsd->bInterfaceProtocol; |
| |
| if (protocol == UAC_VERSION_1) { |
| struct uac1_as_header_descriptor *as; |
| |
| as = snd_usb_find_csint_desc(alts->extra, alts->extralen, NULL, |
| UAC_AS_GENERAL); |
| if (!as) { |
| dev_err(&subs->dev->dev, |
| "%u:%d : no UAC_AS_GENERAL desc\n", |
| subs->cur_audiofmt->iface, |
| subs->cur_audiofmt->altset_idx); |
| return -ENODEV; |
| } |
| |
| resp->data_path_delay = as->bDelay; |
| resp->data_path_delay_valid = 1; |
| |
| resp->usb_audio_subslot_size = subs->cur_audiofmt->fmt_sz; |
| resp->usb_audio_subslot_size_valid = 1; |
| |
| resp->usb_audio_spec_revision = le16_to_cpu((__force __le16)0x0100); |
| resp->usb_audio_spec_revision_valid = 1; |
| } else if (protocol == UAC_VERSION_2) { |
| resp->usb_audio_subslot_size = subs->cur_audiofmt->fmt_sz; |
| resp->usb_audio_subslot_size_valid = 1; |
| |
| resp->usb_audio_spec_revision = le16_to_cpu((__force __le16)0x0200); |
| resp->usb_audio_spec_revision_valid = 1; |
| } else if (protocol == UAC_VERSION_3) { |
| if (iface->intf_assoc->bFunctionSubClass == |
| UAC3_FUNCTION_SUBCLASS_FULL_ADC_3_0) { |
| dev_err(&subs->dev->dev, |
| "full adc is not supported\n"); |
| return -EINVAL; |
| } |
| |
| switch (le16_to_cpu(get_endpoint(alts, 0)->wMaxPacketSize)) { |
| case UAC3_BADD_EP_MAXPSIZE_SYNC_MONO_16: |
| case UAC3_BADD_EP_MAXPSIZE_SYNC_STEREO_16: |
| case UAC3_BADD_EP_MAXPSIZE_ASYNC_MONO_16: |
| case UAC3_BADD_EP_MAXPSIZE_ASYNC_STEREO_16: { |
| resp->usb_audio_subslot_size = 0x2; |
| break; |
| } |
| |
| case UAC3_BADD_EP_MAXPSIZE_SYNC_MONO_24: |
| case UAC3_BADD_EP_MAXPSIZE_SYNC_STEREO_24: |
| case UAC3_BADD_EP_MAXPSIZE_ASYNC_MONO_24: |
| case UAC3_BADD_EP_MAXPSIZE_ASYNC_STEREO_24: { |
| resp->usb_audio_subslot_size = 0x3; |
| break; |
| } |
| |
| default: |
| dev_err(&subs->dev->dev, |
| "%d: %u: Invalid wMaxPacketSize\n", |
| subs->cur_audiofmt->iface, |
| subs->cur_audiofmt->altset_idx); |
| return -EINVAL; |
| } |
| resp->usb_audio_subslot_size_valid = 1; |
| } else { |
| dev_err(&subs->dev->dev, "unknown protocol version %x\n", |
| protocol); |
| return -ENODEV; |
| } |
| |
| memcpy(&resp->std_as_opr_intf_desc, &alts->desc, sizeof(alts->desc)); |
| |
| return 0; |
| } |
| |
| /** |
| * prepare_qmi_response() - prepare stream enable response |
| * @subs: usb substream |
| * @req_msg: QMI request message |
| * @resp: QMI response buffer |
| * @info_idx: usb interface array index |
| * |
| * Prepares the QMI response for a USB QMI stream enable request. Will parse |
| * out the parameters within the stream enable request, in order to match |
| * requested audio profile to the ones exposed by the USB device connected. |
| * |
| * In addition, will fetch the XHCI transfer resources needed for the handoff to |
| * happen. This includes, transfer ring and buffer addresses and secondary event |
| * ring address. These parameters will be communicated as part of the USB QMI |
| * stream enable response. |
| * |
| */ |
| static int prepare_qmi_response(struct snd_usb_substream *subs, |
| struct qmi_uaudio_stream_req_msg_v01 *req_msg, |
| struct qmi_uaudio_stream_resp_msg_v01 *resp, |
| int info_idx) |
| { |
| struct q6usb_offload *data; |
| int pcm_dev_num; |
| int card_num; |
| void *xfer_buf_cpu; |
| int ret; |
| |
| pcm_dev_num = (req_msg->usb_token & QMI_STREAM_REQ_DEV_NUM_MASK) >> 8; |
| card_num = (req_msg->usb_token & QMI_STREAM_REQ_CARD_NUM_MASK) >> 16; |
| |
| if (!uadev[card_num].ctrl_intf) { |
| dev_err(&subs->dev->dev, "audio ctrl intf info not cached\n"); |
| return -ENODEV; |
| } |
| |
| ret = uaudio_populate_uac_desc(subs, resp); |
| if (ret < 0) |
| return ret; |
| |
| resp->slot_id = subs->dev->slot_id; |
| resp->slot_id_valid = 1; |
| |
| data = snd_soc_usb_find_priv_data(uaudio_qdev->auxdev->dev.parent); |
| if (!data) { |
| dev_err(&subs->dev->dev, "No private data found\n"); |
| return -ENODEV; |
| } |
| |
| uaudio_qdev->data = data; |
| |
| resp->std_as_opr_intf_desc_valid = 1; |
| ret = uaudio_endpoint_setup(subs, subs->data_endpoint, card_num, |
| &resp->xhci_mem_info.tr_data, |
| &resp->std_as_data_ep_desc); |
| if (ret < 0) |
| return ret; |
| |
| resp->std_as_data_ep_desc_valid = 1; |
| |
| if (subs->sync_endpoint) { |
| ret = uaudio_endpoint_setup(subs, subs->sync_endpoint, card_num, |
| &resp->xhci_mem_info.tr_sync, |
| &resp->std_as_sync_ep_desc); |
| if (ret < 0) |
| goto drop_data_ep; |
| |
| resp->std_as_sync_ep_desc_valid = 1; |
| } |
| |
| resp->interrupter_num_valid = 1; |
| resp->controller_num_valid = 0; |
| ret = usb_get_controller_id(subs->dev); |
| if (ret >= 0) { |
| resp->controller_num = ret; |
| resp->controller_num_valid = 1; |
| } |
| |
| /* event ring */ |
| ret = uaudio_event_ring_setup(subs, card_num, |
| &resp->xhci_mem_info.evt_ring); |
| if (ret < 0) |
| goto drop_sync_ep; |
| |
| uaudio_qdev->er_mapped = true; |
| resp->interrupter_num = xhci_sideband_interrupter_id(uadev[card_num].sb); |
| |
| resp->speed_info = get_speed_info(subs->dev->speed); |
| if (resp->speed_info == USB_QMI_DEVICE_SPEED_INVALID_V01) { |
| ret = -ENODEV; |
| goto free_sec_ring; |
| } |
| |
| resp->speed_info_valid = 1; |
| |
| ret = uaudio_transfer_buffer_setup(subs, &xfer_buf_cpu, req_msg->xfer_buff_size, |
| &resp->xhci_mem_info.xfer_buff); |
| if (ret < 0) { |
| ret = -ENOMEM; |
| goto free_sec_ring; |
| } |
| |
| resp->xhci_mem_info_valid = 1; |
| |
| if (!atomic_read(&uadev[card_num].in_use)) { |
| kref_init(&uadev[card_num].kref); |
| init_waitqueue_head(&uadev[card_num].disconnect_wq); |
| uadev[card_num].num_intf = |
| subs->dev->config->desc.bNumInterfaces; |
| uadev[card_num].info = kcalloc(uadev[card_num].num_intf, |
| sizeof(struct intf_info), |
| GFP_KERNEL); |
| if (!uadev[card_num].info) { |
| ret = -ENOMEM; |
| goto unmap_er; |
| } |
| uadev[card_num].udev = subs->dev; |
| atomic_set(&uadev[card_num].in_use, 1); |
| } else { |
| kref_get(&uadev[card_num].kref); |
| } |
| |
| uadev[card_num].usb_core_id = resp->controller_num; |
| |
| /* cache intf specific info to use it for unmap and free xfer buf */ |
| uadev[card_num].info[info_idx].data_xfer_ring_va = |
| IOVA_MASK(resp->xhci_mem_info.tr_data.iova); |
| uadev[card_num].info[info_idx].data_xfer_ring_size = PAGE_SIZE; |
| uadev[card_num].info[info_idx].sync_xfer_ring_va = |
| IOVA_MASK(resp->xhci_mem_info.tr_sync.iova); |
| uadev[card_num].info[info_idx].sync_xfer_ring_size = PAGE_SIZE; |
| uadev[card_num].info[info_idx].xfer_buf_iova = |
| IOVA_MASK(resp->xhci_mem_info.xfer_buff.iova); |
| uadev[card_num].info[info_idx].xfer_buf_dma = |
| resp->xhci_mem_info.xfer_buff.dma; |
| uadev[card_num].info[info_idx].xfer_buf_size = |
| resp->xhci_mem_info.xfer_buff.size; |
| uadev[card_num].info[info_idx].data_ep_pipe = subs->data_endpoint ? |
| subs->data_endpoint->pipe : 0; |
| uadev[card_num].info[info_idx].sync_ep_pipe = subs->sync_endpoint ? |
| subs->sync_endpoint->pipe : 0; |
| uadev[card_num].info[info_idx].data_ep_idx = subs->data_endpoint ? |
| subs->data_endpoint->ep_num : 0; |
| uadev[card_num].info[info_idx].sync_ep_idx = subs->sync_endpoint ? |
| subs->sync_endpoint->ep_num : 0; |
| uadev[card_num].info[info_idx].xfer_buf_cpu = xfer_buf_cpu; |
| uadev[card_num].info[info_idx].pcm_card_num = card_num; |
| uadev[card_num].info[info_idx].pcm_dev_num = pcm_dev_num; |
| uadev[card_num].info[info_idx].direction = subs->direction; |
| uadev[card_num].info[info_idx].intf_num = subs->cur_audiofmt->iface; |
| uadev[card_num].info[info_idx].in_use = true; |
| |
| set_bit(card_num, &uaudio_qdev->card_slot); |
| |
| return 0; |
| |
| unmap_er: |
| uaudio_iommu_unmap(MEM_EVENT_RING, IOVA_BASE, PAGE_SIZE, PAGE_SIZE); |
| free_sec_ring: |
| xhci_sideband_remove_interrupter(uadev[card_num].sb); |
| drop_sync_ep: |
| if (subs->sync_endpoint) { |
| uaudio_iommu_unmap(MEM_XFER_RING, |
| IOVA_MASK(resp->xhci_mem_info.tr_sync.iova), |
| PAGE_SIZE, PAGE_SIZE); |
| xhci_sideband_remove_endpoint(uadev[card_num].sb, |
| usb_pipe_endpoint(subs->dev, subs->sync_endpoint->pipe)); |
| } |
| drop_data_ep: |
| uaudio_iommu_unmap(MEM_XFER_RING, IOVA_MASK(resp->xhci_mem_info.tr_data.iova), |
| PAGE_SIZE, PAGE_SIZE); |
| xhci_sideband_remove_endpoint(uadev[card_num].sb, |
| usb_pipe_endpoint(subs->dev, subs->data_endpoint->pipe)); |
| |
| return ret; |
| } |
| |
| /** |
| * handle_uaudio_stream_req() - handle stream enable/disable request |
| * @handle: QMI client handle |
| * @sq: qrtr socket |
| * @txn: QMI transaction context |
| * @decoded_msg: decoded QMI message |
| * |
| * Main handler for the QMI stream enable/disable requests. This executes the |
| * corresponding enable/disable stream apis, respectively. |
| * |
| */ |
| static void handle_uaudio_stream_req(struct qmi_handle *handle, |
| struct sockaddr_qrtr *sq, |
| struct qmi_txn *txn, |
| const void *decoded_msg) |
| { |
| struct qmi_uaudio_stream_req_msg_v01 *req_msg; |
| struct qmi_uaudio_stream_resp_msg_v01 resp = {{0}, 0}; |
| struct uaudio_qmi_svc *svc = uaudio_svc; |
| struct snd_usb_audio *chip = NULL; |
| struct snd_usb_substream *subs; |
| struct usb_host_endpoint *ep; |
| int datainterval = -EINVAL; |
| int info_idx = -EINVAL; |
| struct intf_info *info; |
| u8 pcm_card_num; |
| u8 pcm_dev_num; |
| u8 direction; |
| int ret = 0; |
| |
| if (!svc->client_connected) { |
| svc->client_sq = *sq; |
| svc->client_connected = true; |
| } |
| |
| mutex_lock(&qdev_mutex); |
| req_msg = (struct qmi_uaudio_stream_req_msg_v01 *)decoded_msg; |
| if (!req_msg->audio_format_valid || !req_msg->bit_rate_valid || |
| !req_msg->number_of_ch_valid || !req_msg->xfer_buff_size_valid) { |
| ret = -EINVAL; |
| goto response; |
| } |
| |
| if (!uaudio_qdev) { |
| ret = -EINVAL; |
| goto response; |
| } |
| |
| direction = (req_msg->usb_token & QMI_STREAM_REQ_DIRECTION); |
| pcm_dev_num = (req_msg->usb_token & QMI_STREAM_REQ_DEV_NUM_MASK) >> 8; |
| pcm_card_num = (req_msg->usb_token & QMI_STREAM_REQ_CARD_NUM_MASK) >> 16; |
| if (pcm_card_num >= SNDRV_CARDS) { |
| ret = -EINVAL; |
| goto response; |
| } |
| |
| if (req_msg->audio_format > USB_QMI_PCM_FORMAT_U32_BE) { |
| ret = -EINVAL; |
| goto response; |
| } |
| |
| subs = find_substream(pcm_card_num, pcm_dev_num, direction); |
| chip = uadev[pcm_card_num].chip; |
| if (!subs || !chip || atomic_read(&chip->shutdown)) { |
| ret = -ENODEV; |
| goto response; |
| } |
| |
| info_idx = info_idx_from_ifnum(pcm_card_num, subs->cur_audiofmt ? |
| subs->cur_audiofmt->iface : -1, req_msg->enable); |
| if (atomic_read(&chip->shutdown) || !subs->stream || !subs->stream->pcm || |
| !subs->stream->chip) { |
| ret = -ENODEV; |
| goto response; |
| } |
| |
| mutex_lock(&chip->mutex); |
| if (req_msg->enable) { |
| if (info_idx < 0 || chip->system_suspend || subs->opened) { |
| ret = -EBUSY; |
| mutex_unlock(&chip->mutex); |
| |
| goto response; |
| } |
| subs->opened = 1; |
| } |
| mutex_unlock(&chip->mutex); |
| |
| if (req_msg->service_interval_valid) { |
| ret = get_data_interval_from_si(subs, |
| req_msg->service_interval); |
| if (ret == -EINVAL) |
| goto response; |
| |
| datainterval = ret; |
| } |
| |
| uadev[pcm_card_num].ctrl_intf = chip->ctrl_intf; |
| |
| if (req_msg->enable) { |
| ret = enable_audio_stream(subs, |
| map_pcm_format(req_msg->audio_format), |
| req_msg->number_of_ch, req_msg->bit_rate, |
| datainterval); |
| |
| if (!ret) |
| ret = prepare_qmi_response(subs, req_msg, &resp, |
| info_idx); |
| if (ret < 0) { |
| mutex_lock(&chip->mutex); |
| subs->opened = 0; |
| mutex_unlock(&chip->mutex); |
| } |
| } else { |
| info = &uadev[pcm_card_num].info[info_idx]; |
| if (info->data_ep_pipe) { |
| ep = usb_pipe_endpoint(uadev[pcm_card_num].udev, |
| info->data_ep_pipe); |
| if (ep) { |
| xhci_sideband_stop_endpoint(uadev[pcm_card_num].sb, |
| ep); |
| xhci_sideband_remove_endpoint(uadev[pcm_card_num].sb, |
| ep); |
| } |
| |
| info->data_ep_pipe = 0; |
| } |
| |
| if (info->sync_ep_pipe) { |
| ep = usb_pipe_endpoint(uadev[pcm_card_num].udev, |
| info->sync_ep_pipe); |
| if (ep) { |
| xhci_sideband_stop_endpoint(uadev[pcm_card_num].sb, |
| ep); |
| xhci_sideband_remove_endpoint(uadev[pcm_card_num].sb, |
| ep); |
| } |
| |
| info->sync_ep_pipe = 0; |
| } |
| |
| disable_audio_stream(subs); |
| mutex_lock(&chip->mutex); |
| subs->opened = 0; |
| mutex_unlock(&chip->mutex); |
| } |
| |
| response: |
| if (!req_msg->enable && ret != -EINVAL && ret != -ENODEV) { |
| mutex_lock(&chip->mutex); |
| if (info_idx >= 0) { |
| info = &uadev[pcm_card_num].info[info_idx]; |
| uaudio_dev_intf_cleanup(uadev[pcm_card_num].udev, |
| info); |
| } |
| if (atomic_read(&uadev[pcm_card_num].in_use)) |
| kref_put(&uadev[pcm_card_num].kref, |
| uaudio_dev_release); |
| mutex_unlock(&chip->mutex); |
| } |
| mutex_unlock(&qdev_mutex); |
| |
| resp.usb_token = req_msg->usb_token; |
| resp.usb_token_valid = 1; |
| resp.internal_status = ret; |
| resp.internal_status_valid = 1; |
| resp.status = ret ? USB_QMI_STREAM_REQ_FAILURE_V01 : ret; |
| resp.status_valid = 1; |
| ret = qmi_send_response(svc->uaudio_svc_hdl, sq, txn, |
| QMI_UAUDIO_STREAM_RESP_V01, |
| QMI_UAUDIO_STREAM_RESP_MSG_V01_MAX_MSG_LEN, |
| qmi_uaudio_stream_resp_msg_v01_ei, &resp); |
| } |
| |
| static struct qmi_msg_handler uaudio_stream_req_handlers = { |
| .type = QMI_REQUEST, |
| .msg_id = QMI_UAUDIO_STREAM_REQ_V01, |
| .ei = qmi_uaudio_stream_req_msg_v01_ei, |
| .decoded_size = QMI_UAUDIO_STREAM_REQ_MSG_V01_MAX_MSG_LEN, |
| .fn = handle_uaudio_stream_req, |
| }; |
| |
| /** |
| * qc_usb_audio_offload_init_qmi_dev() - initializes qmi dev |
| * |
| * Initializes the USB qdev, which is used to carry information pertaining to |
| * the offloading resources. This device is freed only when there are no longer |
| * any offloading candidates. (i.e, when all audio devices are disconnected) |
| * |
| */ |
| static int qc_usb_audio_offload_init_qmi_dev(void) |
| { |
| uaudio_qdev = kzalloc(sizeof(*uaudio_qdev), GFP_KERNEL); |
| if (!uaudio_qdev) |
| return -ENOMEM; |
| |
| /* initialize xfer ring and xfer buf iova list */ |
| INIT_LIST_HEAD(&uaudio_qdev->xfer_ring_list); |
| uaudio_qdev->curr_xfer_ring_iova = IOVA_XFER_RING_BASE; |
| uaudio_qdev->xfer_ring_iova_size = |
| IOVA_XFER_RING_MAX - IOVA_XFER_RING_BASE; |
| |
| INIT_LIST_HEAD(&uaudio_qdev->xfer_buf_list); |
| uaudio_qdev->curr_xfer_buf_iova = IOVA_XFER_BUF_BASE; |
| uaudio_qdev->xfer_buf_iova_size = |
| IOVA_XFER_BUF_MAX - IOVA_XFER_BUF_BASE; |
| |
| return 0; |
| } |
| |
| /* Populates ppcm_idx array with supported PCM indexes */ |
| static int qc_usb_audio_offload_fill_avail_pcms(struct snd_usb_audio *chip, |
| struct snd_soc_usb_device *sdev) |
| { |
| struct snd_usb_stream *as; |
| struct snd_usb_substream *subs; |
| int idx = 0; |
| |
| list_for_each_entry(as, &chip->pcm_list, list) { |
| subs = &as->substream[SNDRV_PCM_STREAM_PLAYBACK]; |
| if (subs->ep_num) { |
| sdev->ppcm_idx[idx] = as->pcm->device; |
| idx++; |
| } |
| /* |
| * Break if the current index exceeds the number of possible |
| * playback streams counted from the UAC descriptors. |
| */ |
| if (idx >= sdev->num_playback) |
| break; |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * qc_usb_audio_offload_probe() - platform op connect handler |
| * @chip: USB SND device |
| * |
| * Platform connect handler when a USB SND device is detected. Will |
| * notify SOC USB about the connection to enable the USB ASoC backend |
| * and populate internal USB chip array. |
| * |
| */ |
| static void qc_usb_audio_offload_probe(struct snd_usb_audio *chip) |
| { |
| struct usb_interface *intf = chip->intf[chip->num_interfaces - 1]; |
| struct usb_interface_descriptor *altsd; |
| struct usb_host_interface *alts; |
| struct snd_soc_usb_device *sdev; |
| struct xhci_sideband *sb; |
| |
| /* |
| * If there is no priv_data, or no playback paths, the connected |
| * device doesn't support offloading. Avoid populating entries for |
| * this device. |
| */ |
| if (!snd_soc_usb_find_priv_data(uaudio_qdev->auxdev->dev.parent) || |
| !usb_qmi_get_pcm_num(chip, 0)) |
| return; |
| |
| mutex_lock(&qdev_mutex); |
| mutex_lock(&chip->mutex); |
| if (!uadev[chip->card->number].chip) { |
| sdev = kzalloc(sizeof(*sdev), GFP_KERNEL); |
| if (!sdev) |
| goto exit; |
| |
| sb = xhci_sideband_register(intf, XHCI_SIDEBAND_VENDOR, |
| uaudio_sideband_notifier); |
| if (!sb) |
| goto free_sdev; |
| } else { |
| sb = uadev[chip->card->number].sb; |
| sdev = uadev[chip->card->number].sdev; |
| } |
| |
| uadev[chip->card->number].sb = sb; |
| uadev[chip->card->number].chip = chip; |
| uadev[chip->card->number].sdev = sdev; |
| |
| alts = &intf->altsetting[0]; |
| altsd = get_iface_desc(alts); |
| |
| /* Wait until all PCM devices are populated before notifying soc-usb */ |
| if (altsd->bInterfaceNumber == chip->last_iface) { |
| sdev->num_playback = usb_qmi_get_pcm_num(chip, 0); |
| |
| /* |
| * Allocate playback pcm index array based on number of possible |
| * playback paths within the UAC descriptors. |
| */ |
| sdev->ppcm_idx = kcalloc(sdev->num_playback, sizeof(unsigned int), |
| GFP_KERNEL); |
| if (!sdev->ppcm_idx) |
| goto unreg_xhci; |
| |
| qc_usb_audio_offload_fill_avail_pcms(chip, sdev); |
| sdev->card_idx = chip->card->number; |
| sdev->chip_idx = chip->index; |
| |
| snd_usb_offload_create_ctl(chip, uaudio_qdev->auxdev->dev.parent); |
| snd_soc_usb_connect(uaudio_qdev->auxdev->dev.parent, sdev); |
| } |
| |
| mutex_unlock(&chip->mutex); |
| mutex_unlock(&qdev_mutex); |
| |
| return; |
| |
| unreg_xhci: |
| xhci_sideband_unregister(sb); |
| uadev[chip->card->number].sb = NULL; |
| free_sdev: |
| kfree(sdev); |
| uadev[chip->card->number].sdev = NULL; |
| uadev[chip->card->number].chip = NULL; |
| exit: |
| mutex_unlock(&chip->mutex); |
| mutex_unlock(&qdev_mutex); |
| } |
| |
| /** |
| * qc_usb_audio_cleanup_qmi_dev() - release qmi device |
| * |
| * Frees the USB qdev. Only occurs when there are no longer any potential |
| * devices that can utilize USB audio offloading. |
| * |
| */ |
| static void qc_usb_audio_cleanup_qmi_dev(void) |
| { |
| kfree(uaudio_qdev); |
| uaudio_qdev = NULL; |
| } |
| |
| /** |
| * qc_usb_audio_offload_disconnect() - platform op disconnect handler |
| * @chip: USB SND device |
| * |
| * Platform disconnect handler. Will ensure that any pending stream is |
| * halted by issuing a QMI disconnect indication packet to the adsp. |
| * |
| */ |
| static void qc_usb_audio_offload_disconnect(struct snd_usb_audio *chip) |
| { |
| struct uaudio_dev *dev; |
| int card_num; |
| |
| if (!chip) |
| return; |
| |
| card_num = chip->card->number; |
| if (card_num >= SNDRV_CARDS) |
| return; |
| |
| mutex_lock(&qdev_mutex); |
| mutex_lock(&chip->mutex); |
| dev = &uadev[card_num]; |
| |
| /* Device has already been cleaned up, or never populated */ |
| if (!dev->chip) { |
| mutex_unlock(&chip->mutex); |
| mutex_unlock(&qdev_mutex); |
| return; |
| } |
| |
| /* cleaned up already */ |
| if (!dev->udev) |
| goto done; |
| |
| uaudio_send_disconnect_ind(chip); |
| uaudio_dev_cleanup(dev); |
| done: |
| /* |
| * If num_interfaces == 1, the last USB SND interface is being removed. |
| * This is to accommodate for devices w/ multiple UAC functions. |
| */ |
| if (chip->num_interfaces == 1) { |
| snd_soc_usb_disconnect(uaudio_qdev->auxdev->dev.parent, dev->sdev); |
| xhci_sideband_unregister(dev->sb); |
| dev->chip = NULL; |
| kfree(dev->sdev->ppcm_idx); |
| kfree(dev->sdev); |
| dev->sdev = NULL; |
| } |
| mutex_unlock(&chip->mutex); |
| |
| mutex_unlock(&qdev_mutex); |
| } |
| |
| /** |
| * qc_usb_audio_offload_suspend() - USB offload PM suspend handler |
| * @intf: USB interface |
| * @message: suspend type |
| * |
| * PM suspend handler to ensure that the USB offloading driver is able to stop |
| * any pending traffic, so that the bus can be suspended. |
| * |
| */ |
| static void qc_usb_audio_offload_suspend(struct usb_interface *intf, |
| pm_message_t message) |
| { |
| struct snd_usb_audio *chip = usb_get_intfdata(intf); |
| int card_num; |
| |
| if (!chip) |
| return; |
| |
| card_num = chip->card->number; |
| if (card_num >= SNDRV_CARDS) |
| return; |
| |
| mutex_lock(&qdev_mutex); |
| mutex_lock(&chip->mutex); |
| |
| uaudio_send_disconnect_ind(chip); |
| |
| mutex_unlock(&chip->mutex); |
| mutex_unlock(&qdev_mutex); |
| } |
| |
| static struct snd_usb_platform_ops offload_ops = { |
| .connect_cb = qc_usb_audio_offload_probe, |
| .disconnect_cb = qc_usb_audio_offload_disconnect, |
| .suspend_cb = qc_usb_audio_offload_suspend, |
| }; |
| |
| static int qc_usb_audio_probe(struct auxiliary_device *auxdev, |
| const struct auxiliary_device_id *id) |
| |
| { |
| struct uaudio_qmi_svc *svc; |
| int ret; |
| |
| svc = kzalloc(sizeof(*svc), GFP_KERNEL); |
| if (!svc) |
| return -ENOMEM; |
| |
| svc->uaudio_svc_hdl = kzalloc(sizeof(*svc->uaudio_svc_hdl), GFP_KERNEL); |
| if (!svc->uaudio_svc_hdl) { |
| ret = -ENOMEM; |
| goto free_svc; |
| } |
| |
| ret = qmi_handle_init(svc->uaudio_svc_hdl, |
| QMI_UAUDIO_STREAM_REQ_MSG_V01_MAX_MSG_LEN, |
| &uaudio_svc_ops_options, |
| &uaudio_stream_req_handlers); |
| ret = qmi_add_server(svc->uaudio_svc_hdl, UAUDIO_STREAM_SERVICE_ID_V01, |
| UAUDIO_STREAM_SERVICE_VERS_V01, 0); |
| |
| uaudio_svc = svc; |
| |
| qc_usb_audio_offload_init_qmi_dev(); |
| uaudio_qdev->auxdev = auxdev; |
| |
| ret = snd_usb_register_platform_ops(&offload_ops); |
| if (ret < 0) |
| goto release_qmi; |
| |
| snd_usb_rediscover_devices(); |
| |
| return 0; |
| |
| release_qmi: |
| qc_usb_audio_cleanup_qmi_dev(); |
| qmi_handle_release(svc->uaudio_svc_hdl); |
| free_svc: |
| kfree(svc); |
| |
| return ret; |
| } |
| |
| static void qc_usb_audio_remove(struct auxiliary_device *auxdev) |
| { |
| struct uaudio_qmi_svc *svc = uaudio_svc; |
| int idx; |
| |
| /* |
| * Remove all connected devices after unregistering ops, to ensure |
| * that no further connect events will occur. The disconnect routine |
| * will issue the QMI disconnect indication, which results in the |
| * external DSP to stop issuing transfers. |
| */ |
| snd_usb_unregister_platform_ops(); |
| for (idx = 0; idx < SNDRV_CARDS; idx++) |
| qc_usb_audio_offload_disconnect(uadev[idx].chip); |
| |
| qc_usb_audio_cleanup_qmi_dev(); |
| |
| qmi_handle_release(svc->uaudio_svc_hdl); |
| kfree(svc); |
| uaudio_svc = NULL; |
| } |
| |
| static const struct auxiliary_device_id qc_usb_audio_table[] = { |
| { .name = "q6usb.qc-usb-audio-offload" }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(auxiliary, qc_usb_audio_table); |
| |
| static struct auxiliary_driver qc_usb_audio_offload_drv = { |
| .name = "qc-usb-audio-offload", |
| .id_table = qc_usb_audio_table, |
| .probe = qc_usb_audio_probe, |
| .remove = qc_usb_audio_remove, |
| }; |
| module_auxiliary_driver(qc_usb_audio_offload_drv); |
| |
| MODULE_DESCRIPTION("QC USB Audio Offloading"); |
| MODULE_LICENSE("GPL"); |