在做usb audio设备驱动开发前我还不知道有usb audiodevice class,以为这是个HID类型的驱动,开发起来应该容易实现,后来才发现原来自己进入了一个未知领域。幸亏之前有开发过mass storage固件程序,又了解过OV511的usb camera驱动,所以尽管多花了点时间,中间也出现了波折但还是完成了开发。完成开发后回头一看发现自己对USB协议、音频处理有了进一步的了解,这也算是收获一吧;因为之前没做过,在开发过程中我有40%的时间是用在思考如何进行、10%的时间用于将思路文档化、50%的时间编码,发现这样效率还不错,人也没那么累,也算是收获二吧。
一、在总结开发经验的时候,先温习一些基础知识点做铺垫。
1、usb协议
(1)USB设备通过描述符来描述其功能,标准的USB设备有5种USB描述符:设备描述符,配置描述符,字符串描述符,接口描述符,端点描述符。
(2)USB设备可以看作提供了多个串口的设备,依据USB的规范,我们将每个串口称作端点(Endpoint),要和这个端点通信,我们就要打开到这个端点的连接,这个连接就是管道(Pipe)。
(3)由于一个设备可能要适应多种情况,端点的设置会有多套,以备使用。端点设置称为接口(Interface)。USB设备展现给我们能够找到的东西就是这些Interface,我们选择要用的Interface,就可以找到Endpoint,再打开Endpoint,就可以传输数据了。所以,在驱动程序开始的时候,需要记录下这些Interface。
(4)打开端点之后,就可以像串口一样进行数据传输了。USB有4种不同类型的传输方式:控制传输(ControlTransfer),批量传输(Bulk Transfer),中断传输(InterruptTransfer)和实时传输(IsochTransfer)。象audio和viedo这种对实时性要求高的就要用实时传输。
2、usb设备驱动
(1)HID、usb audio class device、usb video class device、mass storage这四种设备可以开发通用的设备驱动,只需要修改VID和PID即可。
(2)对于一个USB设备的流驱动,除开实现流驱动接口外,还需要实现USBInstallDriver、USBDeviceAttach、USBUnInstallDriver这三个函数以及设备拔出的回调函数。
3、音频处理
(1)PCM编码方式:脉冲编码调制PCM(Pulse Code Modulation),其实就是将声音数字化进行传输。也是最常见的方式。具体原理可以从网上查询。
(2)PCM文件:模拟音频信号经模数转换(A/D变换)直接形成的二进制序列,该文件没有附加的文件头和文件结束标志。
(3)傅立叶变换:数字信号处理领域一种很重要的算法。傅立叶原理表明:任何连续测量的时序或信号,都可以表示为不同频率的正弦波信号的无限叠加。而根据该原理创立的傅立叶变换算法利用直接测量到的原始信号,以累加方式来计算该信号中不同正弦波信号的频率、振幅和相位。和傅立叶变换算法对应的是反傅立叶变换算法。该反变换从本质上说也是一种累加处理,这样就可以将单独改变的正弦波信号转换成一个信号。因此,可以说,傅立叶变换将原来难以处理的时域信号转换成了易于分析的频域信号(信号的频谱),可以利用一些工具对这些频域信号进行处理、加工。最后还可以利用傅立叶反变换将这些频域信号转换成时域信号。
在数学可以认为:任何波形都可以用多项式来表示,就是说可以进行数学运算。
(4)人耳能听到的声波范围是200hz-20Khz,声音信号中的噪音一般是在低频段(比如500hz以下),通过傅立叶变换后将此部分消除再逆转换回去,就可以达到去噪的目的
(5)WINDOWS下对音频的处理,大致可分为两部分,即音频的输入、输出,和ACM压缩处理。
4、音频驱动
(1)音频驱动通过一个wave api manager的模块与上层应用打交道。具体结构图可参考MSDN
(2)音频驱动处理的就是PCM数据流。因为设备的I/O采样率已经是预先设置好一个值了,因此对于上层应用要求的不同采样率要进行转换,此时用线性插值算法来保证转换后声音不失真。
(3)AC97音频驱动结构功能分析。
二、设备分析
通过将此设备接到PC上,用工具软件bushound和usbview分析出此设备的信息,它有四个接口(端点配置)如下:
1、接口0:音频控制,使用端点0,用于调节音量等操作,class为1(Audio),subclass为1(AudioControl)
音量调节过程:缺省是接口0,发送SET CUR指令及音量大小值。因为改变音量后会响一下,发送SETINTERFACE从接口0切换到接口2,然后往接口2发送音频数据(PCM格式的WAVE文件),最后发送SETINTERFACE从接口2切换回接口1。
2、接口1、2:音频流输出(SPEAK),使用端点1,class为1(Audio),subclass为2(AudioStreaming),前者bAlternateSetting和bNumEndpoints均为0(zerobandwidth),后者均为1
3、接口3、4:音频输入(MIC),使用端点2,class为1(Audio),subclass为2(AudioStreaming),前者bAlternateSetting和bNumEndpoints均为0(zerobandwidth),后者均为1
4、接口5:HID,使用端点3。未使用,class为3(HID),subclass为0
5、Data Format Type
输入是单声道、8Khz采样率、16位的PCM,端点最大传输值是16字节;输出是双声道、48Khz采样率、16位的PCM,端点最大传输值是192字节
6、接口1、2和3、4有Alternative接口,是为了启动/停止MIC或SPK而准备,usb audiodevice class规范要求实现zero bandwidth的接口。因此要实现此设备的录音和播放只需要操作这几个接口即可。
三、实现步骤及思路
第一步:参照usb print实现驱动,在驱动中提供自定义的IoControl控制码供应用程序调用来实现操作设备的目的,因为它跳过了waveapi所以不具有通用性,这主要是为了测试设备使用,同时加深对USB设备开发的理解并积累经验。
第二步:在第一步的基础上参照wavedev驱动,这样做出的驱动应用程序只需要调用waveapi系列函数就能实现操作设备的目的,具有通用性。
第三步:实现平台多音频设备的切换。
四、开发中的重点问题
第一步
(1)采样到的PCM文件在PC上播放有加快现象
原因:播放加快是因为数据丢失。因为驱动的读控制码这个地方只是简单地投递一个实时传输请求,应用程序循环调用自然就有缓冲区溢出的问题。改成驱动中用线程结合事件和信号量,最多可投递5个实时传输请求进入内核的异步等待队列,引入双端循环队列来管理缓冲区就基本上不会有数据丢失的问题。需要注意的是dwLen数组赋值不能超过端点的最大传输,这点在MSDN也是有说明的。
(2)采样到的PCM文件在PC上播放有噪音现象
原因一:此设备一次传送960字节(即10ms),因为输入端点的包大小是100,所以我的Frame数组是100*10,但是数据到来是并不是由Frame[0]开始填,而是每个Frame都填96,这样在生成PCM文件的时候就会每隔96字节多出4个0,将Frame数组改成96*10后,噪音要小多了。
原因二:此音频设备是USB的5V供电,开发板的5V电源还接了其它使用,受硬件原因的影响有噪音存在。换到产品设备上试验基本上无噪音。
(3)多拔插几次就会出异常
在设备拔出的回调函数中,对于之前分配的内存、事件、信号量及打开的管道没有做回收或关闭操作。
第二步
对于wavedev结构的驱动的修改,我们只需要关注与硬件相关的几个部分,那就是hwctxt、wavemain部分,其它通用部分中只需要修改devctxt中的设备名即可。
(1)几个重要函数的说明
SetRate:设置进行线性插值算法的初始值
Render2:线性插值算法,分别对8位、单声道、立体声进行采样率转换。
TransferBuffers:对于输入将数据从DMA缓冲经采样率转换后搬移到应用缓冲,对于输出将应用缓冲经采样率转换后搬移到DMA缓冲
(2)WAV_INIT参数的修改
对于缺省的音频设备在系统启动的时候就会加载。根据WAV_INIT参数可以得知设备管理器是通过RegisterDevice来加载的,但对于USB音频设备因为是通过USBDeviceAttach动态加载的,它跳过了设备管理器,加载驱动用的ActivateDevice会传递不同的参数,因此需要修改WAV_INIT参数(或者改成用RegisterDevice,不过MSDN推荐用前者)
注:此处修改一下,对于WAV_INIT参数的理解是错误的,设备管理器实际上通过ActivateDeviceEx来加载,传递给流驱动接口XXX_INIT其实都是一个Active的注册表项路径。
(3)DMA操作的修改
不管是AC97还是IIS音频驱动,其过程都是通过有限状态机来进行状态切换。数据传输有一个重要机制就是使用双缓冲,作用是一个由DMA负责传送,一个由CPU做数据搬移。第一次启动的时候两个缓冲区都填满,但只有一个交给DMA,在IST中进行接下来的数据转换和搬移,这样就可以保证传送的连续性,避免数据搬移的时候DMA空置停机。因为DMA传输是不占用CPU的,所以在CPU进行数据转换和搬移的时候DMA也在进行,这样codec一直有数据播放就不会有播放停顿现象。
在Usb音频驱动中是没有DMA而是实时传输,如果想完全照搬DMA的工作原理,通过创建一个传输线程来模拟DMA,不管传输缓冲区开多大,在传送回调函数激活IST进行数据转换和搬移时,codec已经播放完数据,IST完成数据转换和搬移并通知传送线程投递新请求这整个过程都会占用CPU其中是有延时的,因此会造成播放停顿现象。因此只能完全抛开DMA的工作机制,采用第一步中实现机制,这样就不会有问题了。对于起停DMA只需要保留其接口,内部实现改为切换USB的接口就能实现起/停音频设备。
第三步
可以用waveInMessage、waveOutMessage来实现音频设备的切换,另外如果确定设备已连接可以在waveInOpen、waveOutOpen中直接指定设备id。关于这个可以参照WINCE自带的Bluetooth中关于音频设备切换的代码部分。
五、存在问题
1、想在插入的时候在WAV_INIT切换为默认,拔出的时候在WAV_DEINIT切换回去,但是没有起作用。估计此时驱动还没加载完,还是要放到应用程序中,通过其它方法来动态切换了。
2、在USB音频设备拔出的消息通知函数中把默认设备切换回去,再重新插入从调试信息看驱动加载,WODM_OPEN也调用了,但是播放无声音。如果多拔插几次在Active下生成的项不会被删除,只是把项下面的值删除了,我看了PRIVATE目录下DeactivateDevice源码注释中说明:如果项不能删除,就会删除项下面的键值以确保此项意外地被重复利用,但是没说明为什么不能删除项。而且多拔插几次后device.exe和coredll.dll就会出现熟悉的大堆DataAbort错误提示。是不是设备管理器或者USBD没有完全释放完与设备有关的内核资源?
关于此问题的说明及解决方案:
(1)刚开始一直以为是内存泄露问题,甚至认为OHCI驱动有BUG。从出错的地方定位看来,主要是出现在usbd、ohci的驱动如PIPE、USB设备链表或device.exe的设备链表的访问出错,分析可能是拔出的时候内存没有回收就插入申请新内存,而我这个USB设备因为有好几个管道要打开内存资源可能不够用,我加大PDD层中OHCI的DMA内存问题依旧。
(2)接下来我发现在插上USB音频设备加载驱动后会接着调用WAV_OPEN,但是我不知道这是谁在打开设备(是WAVEAPI还是PM?),在拔出的时候却没有调用WAV_CLOSE,我觉得可能是因为缺省的音频设备一直在运行,所以CreateFile后不需要CloseHandle。这样的话USB音频驱动通过WAV_OPEN打开的句柄一直得不到释放,是不是因为这个原因才造成多次拔插后出出异常?
(3)通过分析WAVEAPI,终于明白之前的猜测是对的:为什么会有WAV_OPEN了?第一次是PM,它取设备的电源属性,然后WAV_CLOSE。第二次是WAVEAPI,它需要设备句柄,因为WAVEAPI和音频驱动是通过DeviceIoControl通讯的,所以打开后是不会关闭的。
我从一个国外的网站看到有人说的,原文如下:Note that in general CE hasn't been well testedfor hot-unplug ofaudio devices。 原因可能就是因为WAVEPAI打开音频设备后是不会释放句柄的。
(4)通过借鉴mass storage的方法,我把USB音频驱动按功能拆分成两个DLL:WAV.DLL和USB.DLL,前者是音频处理实现,系统启动时加载;后者是USB处理部分,提供USB设备操作函数接口给WAV.DLL(延迟加载)在设备插入的时候由USB.DLL通过fastcall方式将USB设备上下文传递给WAV.DLL,拔出的时候将WAV.DLL中的USB设备上下文赋为0。
这样的话在WAV部分是一直存在并与waveapi交互,当涉及到USB设备操作的时候先判断USB设备上下文是否不为空,否则不做处理直接返回TRUE,设备无关部分因为符合wavedev结构也不会有问题。即使USB设备拔下,在应用程序切换两个音频设备并播放的时候不会有问题,录音的话可能要做一点改动。
这样做效率可能会降低,但是目前也想不到其它好办法。另外我考虑在WAV.DLL中增加一个线程来判断USB的拔出和插入,这样可能实现自动切换而不需要应用程序来切换。
3、录音的音量播放出来偏小,如果每次都要到控制面板去改很麻烦。考虑用API或者USB的命令来实现
关于此问题的说明及解决方案:
因应用程序需要的是8Khz+8bit+mono的数据,而设备是16位所以在进行转换后变成unsigned的PCM,其值是110到140之间,振幅只有30;通过增益处理:现值=(原值-100)*4,振幅达到120达到音量放大效果。进行增益的时候要注意不要溢出
4、此设备为USB组合设备,功能是音频(usb audioclass)和HID,音频功能包括一个接口实现录音,一个接口实现播放;HID包括一个接口实现键盘,一个接口实现鼠标。HID这一部分通过调试发现并没有被系统识别,现象是USBHID.dll、KBDHID.dll没有加载,将这个设备放到PC则一切工作正常,另外一个相同功能的复合设备HID工作正常。有可能wince不支持组合设备吗?
六、引用文章