上一篇《骁龙820A汽车与智能设备间进行USB音频分享方案介绍--UAC篇》对涉及到的技术关键细节作进一步的详细分析,这一篇是基于UAC的USB虚拟双声卡。
前两篇文章已经介绍了UAC的基本原理,这次来说说USB虚拟双声卡基本思路。
USB虚拟双声卡的基本思路就是依据USB驱动中的复合描述符,根据驱动的配置,在驱动中用声音子系统创建出了两个声卡。
USB 驱动中的复合描述符
如 USB 规范所述,每个 USB 设备都会提供一组分层描述符来定义其功能。在顶层,每个设备具有一个或多个 USB 配置描述符,其中每一个都具有一个或多个接口描述符。配置是相互排斥的,因此,一次只能选择一种配置来进行操作。
USB复合设备 Compound Device内嵌Hub和多个Function,每个Function都相当于一个独立的USB外设,有自己的PID/VID,复合设备其实就是几个设备通过一个USB Hub形成的单一设备;
USB复合设备一般用Interface Association Descriptor(IAD)实现,就是在要合并的接口前加上IAD描述符。IAD是Interface Association Descriptor,功能是把多个接口定义为一个类设备。
在配置中,接口和接口集合独立进行管理。
IAD描述符简介:
typedef struct _USBInterfaceAssociationDescriptor { BYTE bLength: 0x08 //描述符大小 BYTE bDescriptorType: 0x0B //IAD描述符类型 BYTE bFirstInterface: 0x00 //起始接口 BYTE bInterfaceCount: 0x02 //接口数 BYTE bFunctionClass: 0x0E //类型代码 BYTE bFunctionSubClass: 0x03 //子类型代码 BYTE bFunctionProtocol: 0x00 //协议代码 BYTE iFunction: 0x04 //描述字符串索引 }
bFirstInterface: 表示功能中第一个接口的编号
bInterfaceCount:表示接口集合中有多少个接口。IAD接口集合中的接口必须是连续的(接口编号列表中不能有空格),因此具有第一个接口号的计数足以指定集合中的所有接口。
在描述符级别,每个接口都通过 USB_INTERFACE_DESCRIPTOR 结构中 bInterfaceNumber 成员的唯一值来表示。
接口的函数通过 bInterfaceClass、bInterfaceSubClass 以及同一结构的 bInterfaceProtocol 成员(连同随后的类特定描述符)来表示。
有关描述符的详细信息,请参阅 文章最后的USB 描述符。
驱动中用声音子系统接口创建出了两个声卡:
Create first PCM device、Create second PCM device.
以下为实现虚拟双声卡的关键代码实例:
static int snd_uac2_probe_second(struct platform_device *pdev) { struct snd_uac2_chip_second *uac2 = pdev_to_uac2_second(pdev); struct snd_card *card; struct snd_pcm *pcm; int err; /* Choose any slot, with no id */ err = snd_card_create(-1, NULL, THIS_MODULE, 0, &card); if (err < 0) return err; uac2->card = card; /* * Create first PCM device * Create a substream only for non-zero channel streams */ err = snd_pcm_new(uac2->card, "UAC2 PCM SECOND", 0, p_chmask_second ? 1 : 0, c_chmask_second ? 1 : 0, &pcm); if (err < 0) goto snd_fail; strcpy(pcm->name, "UAC2 PCM SECOND"); pcm->private_data = uac2; uac2->pcm = pcm; snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &uac2_pcm_ops_second); snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &uac2_pcm_ops_second); strcpy(card->driver, "UAC2_Gadget_Second"); strcpy(card->shortname, "UAC2_Gadget_Second"); sprintf(card->longname, "UAC2_Gadget_Second %i", pdev->id); snd_card_set_dev(card, &pdev->dev); snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS, snd_dma_continuous_data(GFP_KERNEL), 0, BUFF_SIZE_MAX_SECOND); err = snd_card_register(card); if (!err) { platform_set_drvdata(pdev, card); return 0; } snd_fail: snd_card_free(card); uac2->pcm = NULL; uac2->card = NULL; return err; } static int snd_uac2_remove_second(struct platform_device *pdev) { struct snd_card *card = platform_get_drvdata(pdev); if (card) return snd_card_free(card); return 0; } static int alsa_uac2_init_second(struct audio_dev_second *agdev) { struct snd_uac2_chip_second *uac2 = &agdev->uac2; int err; uac2->pdrv.probe = snd_uac2_probe_second; uac2->pdrv.remove = snd_uac2_remove_second; uac2->pdrv.driver.name = uac2_name_second; uac2->pdev.id = 0; uac2->pdev.name = uac2_name_second; /* Register snd_uac2 driver */ err = platform_driver_register(&uac2->pdrv); if (err) return err; /* Register snd_uac2 device */ err = platform_device_register(&uac2->pdev); if (err) platform_driver_unregister(&uac2->pdrv); return err; }
在同一条USB线上启用第二块音频卡,一般情况下不需要再定义标志了
这里包括一些Playback 默认的频率,Playback 默认的位
捕捉(USB-OUT)默认立体声, 相关的频率
定义宏,保持两个声卡的同步
驱动程序实现一个简单的UAC_2拓扑, 采集和回放采样率是独立的
由两个时钟源控制
注意:将采样率转换为我们的全速格式 ,否则这将会导致溢出,确定下一个数据包中的帧数
接下来我们不能驱使太多的bad xfers否则将可能导致ISOCH xfer失败。
注意:播放停止后清除缓冲区
往下就就是注册snd_uac2驱动程序
创建第一个PCM设备,仅为非零通道流创建子流
注意:必须为bFirstInterface设置正确的接口号,只有类特定的请求应该到达这里,初始化可配置的参数.
上述就是和USB虚拟双声卡相关的一些基础知识。
[ 附录 ]
USB描述符
USB 描述符信息存储在USB设备中,在枚举过程中,USB主机会向USB设备发送GetDescriptor请求,USB设备在收到这个请求之后,会将USB 描述符信息返回给USB主机,USB主机分析返回来的数据,判断出该设备是哪一种USB设备,建立相应的数据链接通道。那么USB描述符信息到底是一个什 么样的数据呢,USB协议中有详细描述。
通用的USB描述符信息包括设备描述符、配置描述符、接口描述符和端点描述符,具体不同的USB设备还包括其它类型的描述符,例如,USB鼠标、键盘还包括HID描述符和报告描述符,还有可能包括字符串描述符,USB协议中对描述符类型定义如下:
所有的描述符信息都是通过发送GetDescriptor请求得到的,但是USB设备也不知道你要获取的是哪种描述符,所以还需要在GetDescriptor请求中指定描述符的类型以及描述符的长度,这样USB设备才能正确的返回描述符信息。