好久没弄USB的东西,今天TIC的同事又报了一个问题过来,代码一眼望去,感觉依然还是那么的雄兔脚朴素,雌兔眼迷离,双兔傍低走,安能辨我是雄雌(请允许我先晕一会)。

好吧,那就再来理一遍。
以speaker为例,首先要知道,枚举完成,在audio通信阶段,需要做两件事,一是从USB口拿到声音流,一是要把声音流从SAI音频播放口放出去。所以需要做一个蓄水池(buffer),作为缓冲,一方面把USB口来的声音流放进去,一方面要把蓄水池里面的数据拿出来,播放到SAI口。
原理很简单,就这样就已经讲完了,是不是感觉超级清爽,超级easy?不就是个一进一出,小学生纷纷钟都能盘清楚。
不过靠这些是不能解决客户的实际问题,客户的问题是,放着放着,为什么会有噪声?
那我们下面要具体到代码级别看看具体怎么做的,才有可能真正的去分析和解决目前的问题,好,话不多说,开始扒。
首先引入眼帘的是这个宏:AUDIO_PLAY_TRANSFER_SIZE,啥玩意呢?扒完是这样的:

所以设计上,一次SAI的EDMA传输的,刚好就是是一次USB 扔过来的包。
对于上述的情况,这个包的大小是48*2*2 = 192字节。
好了接下来要看内存的管理和组织了,涉及到:


那接下来就要扒一扒startPlayHalfFull了,为什么只有这个为1的时候才做正常的从蓄水池捞水的操作?
等等,我还是先说说, startPlayHalfFull为0的时候干嘛了。 startPlayHalfFull为0表示蓄水池没水了。

audioPlayDMATempBuff就是当蓄水池没水的时候就一直向SAI port发送这个buffer,所以我把它姑且叫做无尽的源泉,如果PC没送数据过来,就一直从这里捞水。虽然每次捞出来都是一样的,但是这就是SAI的玩法。就好比一个人每过1个小时就要喝一瓶可乐,如果到点了送可乐的还没来,就给他一瓶自来水,虽然味道和可乐不一样,但是没办法,因为没可乐。
startPlayHalfFull的字面意思是半满,所以一个简单直观的理解是,蓄水池里面的水如果多于一半,就从蓄水池捞水,否则就从无尽的源泉处捞水。看上去有些怪怪的,但这个是一个平衡算法。因为语音传输的应用,源和目的的时钟很可能不完全一样,这样总会有误差,需要一个算法来平衡两边的时钟差。
好,现在来看什么条件startPlayHalfFull被置1,什么条件startPlayHalfFull被置0?
先看置1的情况:

这个比较简单了,USB收到数据后,看tdReadNumberPlay的位置,如果大于一半的物理就置位。
再看什么时候清0:

所以这个是看如果SAI发送的次数大于USB接收的次数,说明蓄水池没水了,所以要清0.
看到这里感觉有点绕了,整个软件到底是怎么工作的?根据目前已知的,还回答不了。
目前能看到的线索是:
设计了一个蓄水池,如果蓄水池的水有一半以上,会从蓄水池捞水,否则从无尽的源泉捞水。
那蓄水池的水位管理怎么实现的?
这里有一个重要的变量tdReadNumberPlay,这个变量参与了水位的判决,以及捞水后,水位的变化。
水位完全是这个变量决定的吗?想了一下,感觉有点问题,因为蓄水池本质上是一个循环buffer,到顶后会马上归0,只有这个变量应该没法玩的。那就再把这个变量理一理。
撸了一把,结果,彻底晕了。

里面的逻辑好复杂。
于是我喝了一口82年的拉菲,待我先分析一下:
USB_AudioSpeakerBufferSpaceUsed()分析显示,蓄水池在实现上,采用了循环buffer,所以需要head和tail。这个是软件数据结构的基本知识了。要理解这个,首先得知道循环buffer是怎么工作的,这个扯起来就话长了,可以单独讲一篇文章,这里就不详述了。
这个函数分析完,结论就是:
tdReadNumberPlay相当于head
tdWriteNumberPlay相当于tail
入buffer改head, 出buffer改tail
看到这里,就很难理解水位的一半的判决为什么会只看tdReadNumberPlay? 难道是个bug?而且实际测试音质很差,有很明显的变形。如果是我,我会结合tdWriteNumberPlay来判定水位的。

把这个循环buffer撸完,感觉整个系统的结构就已经很清楚了:
水来-->入水库-->水库超过一半要用水就从水库去,否则去自来水公司取。
但是事情还没完结,别忘了上面的水位管理还涉及另外两个变量:
audioSendTimes和usbRecvTimes
所以水位管理除了数据结构的head和tail,还得把这两个变量考虑进去。
接着撸。
先看audioSendTimes:
这个分析完的结论是:这个是从蓄水池捞水的次数,每捞一次就++,如果是连续播放永不清0,小算了一下,1ms加1,有生之年应该不会溢出,所以没啥问题。
再看usbRecvTimes:
这个是从USB那边拿到一个包就++,永不清零,是往蓄水池里面加水的次数。
好了现在再回过来看看水位半满的设定逻辑:
1.一旦捞水的次数比加水的次数多,水位就不到一半,以后都从自来水公司拿水。
2.一旦tdReadNumberPlay过物理地址的一半,就设置水位到一半了。
看到这里感觉还是有点乱,所以我准备再从两个事件入口再回顾一下:
1.SAI发送完成的callback.
2.USB 收到数据后的callback.
这两个入口是分析整个系统怎么run的核心。
不过出于一种敏感度,我决定先看一眼speakerDetachOrNoInput,这个是在把水位置成不到一半的时候同时置的。
这一看我就直接傻了,speakerDetachOrNoInput置位后,code会直接复位,整个audio buffer状态全部丢失。

好了,啥也不想说了,下班。
