战列舰 发表于 2018-2-14 11:58:47

【关于所谓的高品质USB输出专机及DDC原理】鉴于紫檀水准每况日下,转一篇隔壁绿檀...

原作者,绿檀aarwwefdds
原贴地址:http://www.erji.net/forum.php?mod=viewthread&tid=1987631


USB是通用串行总线的意思,本质上并不是专门用来传输音频数据的。这里先简单列举USB2.0几个特性:双绞线、带电源、数据速率与传输频率无关、自同步(不需要单独传输时钟)、Token(令牌)轮询特性。双绞线的特性使得USB天生具有抗共模干扰的能力,带供电使得它带的设备可以不需要电源,令牌轮询特性用于“交通管制”。

USB是主从模式的总线,Host控制器决定它下面所有设备一切事务的发送/接收时机。全速下,Host每1ms±500μs生成一个“帧”(frame)。高速下,每125us±0.0625μs生成一个“帧”。一个“帧”以SOF包打头,包含发送给同一个Host控制器下的不同设备的若干个USB数据包。因此同一个控制器下的USB设备带宽共享:

http://www.erji.net/data/attachment/forum/201704/23/185928sd3n911ttwr9dvwq.png
在同一个USB控制器上的全速设备(FullSpeed/FS)、低速设备(LowSpeed/LS)设备之间共享带宽。高速设备(HighSpeed/HS)之间共享带宽,不与全速/低速设备共享,甚至很多南桥会单独设计高速USB2.0控制器。这种共享带宽,在下行中是这样实现的:Host设备发给高速设备 H1的数据包,高速设备H2同样会收到相同的数据包,但是因为接收地址与自身地址不符,H2设备会无视该数据包。同样的,Host发给F1全速设备的数据包,F2全速设备和L1/L2慢速设备也会收到同样的包,但是会将其无视。

上行带宽共享则是由主机控制:除非主机在“帧”中发送了IN包告诉这个设备准许发送数据,否则设备不允许发送数据。这样就避免了冲突产生。

USB有四种传输通道类型(Endpoint):控制(Control)、中断(Interrupt)、批量(Bulk)、同步(Isochronous)。
Control用于USB总线控制等(所有设备都有),Interrupt多用于鼠标键盘的数据传输(其实在USB令牌轮询的特性下这是个伪Interrupt),Bulk多用于U盘数据传输。
前三种类型都有重传,各有特点这里略过。但USB音频类(USB Audio Class,简称UAC)采用的音频数据传输是第四种端口,也就是Isochronous,拥有错误检测机制,但是没有重传。

为何没有重传呢。
一是音频/视频是很讲究“实时性”的,否则会音画不同步,带来的恶果是很蛋疼的。因此音频/视频设备本质上都需要与主机同步,重传会打乱同步。
二是同步(Isochronous)类型因为要求低延迟,不像别的传输类型有握手过程,同一个"传输端口"内数据包是单向移动的(要么入要么出),Host不会给一个OUT方向的同步传输通道发送IN包,没有办法引入重传机制(发生错误时无法通知主机)。
另外还有一点,为了“同步”,每个“帧”中的每个同步(Isochronous)通道只会传送一次数据。

这里有人要问了 用一个IN和一个OUT的Isochronous通道不就可以解决错误处理了?(ASYNC异步下也确实有一对IN和OUT通道,具体作用下文会解释,反正不是用来“错误检测”)

事实上,不可能。Host在一个Frame中轮询各个Endpoint的顺序是不可知的,很可能Host在一个Frame中先轮询了你的IN通道,但此时Host还没给你的OUT通道传送数据。这样就产生了:数据还没发送 怎么能确认数据收到了呢。

但这是否值得担忧呢?USB自身抗干扰特性(双绞线/屏蔽层)使得正常/正确使用的情况下,产生错误的概率非常非常低。真产生错误了,设备因为CRC校验失败,会将错误的USB数据包丢弃,而这会导致USB界面缓冲区欠载,导致短暂静音/爆音


前文已经可以得到结论,USB总线比较复杂,并不是为音频传输特别设置的传输方式。一个控制器通常需要对应多个设备,PC也有很多任务,其传输延迟会相对不那么稳定。

但是声卡设备需要与主机同步,否则会产生音画不同步问题,DAC也需要喂按采样率喂数据,因此就有了大家熟知的三种同步方式:SYNC、自适应、ASYNC模式(俗称“异步” 注意不要和Endpoint类型搞混)。顺带一提 很多人有个误区,以为ASYNC是UAC2引入的,其实并不是。早在UAC1就已经支持ASYNC,但因为各种原因(开发困难/成本高/效果一般)并没有被大规模采用。在UAC2时才因为XMOS等界面的出现降低了开发难度简化了实现而且效果好而被广为采用。

SYNC是将输出时钟与每个Frame的SOF包同步,但前文可以看到SOF包本身就允许较大的抖动。自适应是根据Host传送数据的速率调整输出频率(有点像SPDIF的工作方式不是么)。这两种同步方式下USB界面都是被动适应Host端的发送节奏,本身没有反馈机制,产生的Jitter受USB总线影响较大。当然芯片厂商为了减少影响也是绞尽脑汁,例如TI的所谓“SpAct时钟恢复”引入8xPLL等尽可能减少影响。最后根据某个外国人测出来大概PCM270x能把Jitter压到低于1us(还是挺高的按照现在的标准)

ASYNC下,USB界面会额外申请一条Feedback传输端口。这里有两种实现,一种是显式Feedback,一种是隐式Feedback。

显式Feedback下,USB界面会将单位时间内该传多少Samples回传给Host(实际有一套相当复杂的计算方式,此处略),让Host计算并知道之后该“补”多少或者该“少”多少采样传给USB界面,这样就能与USB界面主时钟同步并保持不溢出/欠载的缓冲区。UAC1使用数据格式10.14(因为是1ms),UAC2使用数据格式16.16(因为是125us),有所不同

这里以俗知的Amanero界面为例:


[*]Endpoint Descriptor:
[*]------------------------------
[*]0x07      bLength
[*]0x05      bDescriptorType
[*]0x05      bEndpointAddress   (OUT Endpoint) #主机端->USB界面
[*]0x05      bmAttributes      (Transfer: Isochronous / Synch: Asynchronous / Usage: Data) #传输类型Isochronous 同步方式Asynchronous
[*]0x0400      wMaxPacketSize   (1024 Bytes) #最大包大小
[*]0x01      bInterval #传输间隔 2^(1-1) x 125。也就是125us传输一次
[*]
[*]Endpoint Descriptor:
[*]------------------------------
[*]0x07      bLength
[*]0x05      bDescriptorType
[*]0x81      bEndpointAddress   (IN Endpoint) #USB界面->主机端
[*]0x11      bmAttributes      (Transfer: Isochronous / Synch: None / Usage: Feedback) #传输类型Isochronous 用于Feedback
[*]0x0004      wMaxPacketSize   (4 Bytes)#最大包大小4字节
[*]0x06      bInterval #传输间隔 2^(6-1) x 125。也就是4000us(4ms)传输一次

复制代码


这是一个典型的显式Feedback的ASYNC,上行反馈Endpoint,每4ms传输一次,最大包大小是4字节(因为只是反馈Rate不需要太大)。再来看看Linux的UAC驱动是如何处理反馈的
节选:http://lxr.free-electrons.com/source/sound/usb/endpoint.c

...
1156         /*
1157          * process after playback sync complete
1158          *
1159          * Full speed devices report feedback values in 10.14 format as samples
1160          * per frame, high speed devices in 16.16 format as samples per
1161          * microframe.
1162          *
1163          * Because the Audio Class 1 spec was written before USB 2.0, many high
1164          * speed devices use a wrong interpretation, some others use an
1165          * entirely different format.
1166          *
1167          * Therefore, we cannot predict what format any particular device uses
1168          * and must detect it automatically.
1169          */
1170
1171         if (urb->iso_frame_desc.status != 0 ||
1172             urb->iso_frame_desc.actual_length < 3)
1173               return;
1174
1175         f = le32_to_cpup(urb->transfer_buffer);
1176         if (urb->iso_frame_desc.actual_length == 3)
1177               f &= 0x00ffffff;
1178         else
1179               f &= 0x0fffffff;
1180
1181         if (f == 0)
1182               return;
1183
1184         if (unlikely(sender->tenor_fb_quirk)) {
1185               /*
1186                  * Devices based on Tenor 8802 chipsets (TEAC UD-H01
1187                  * and others) sometimes change the feedback value
1188                  * by +/- 0x1.0000.
1189                  */
1190               if (f < ep->freqn - 0x8000)
1191                         f += 0xf000;
1192               else if (f > ep->freqn + 0x8000)
1193                         f -= 0xf000;
1194         } else if (unlikely(ep->freqshift == INT_MIN)) {
1195               /*
1196                  * The first time we see a feedback value, determine its format
1197                  * by shifting it left or right until it matches the nominal
1198                  * frequency value.This assumes that the feedback does not
1199                  * differ from the nominal value more than +50% or -25%.
1200                  */
1201               shift = 0;
1202               while (f < ep->freqn - ep->freqn / 4) {
1203                         f <<= 1;
1204                         shift++;
1205               }
1206               while (f > ep->freqn + ep->freqn / 2) {
1207                         f >>= 1;
1208                         shift--;
1209               }
1210               ep->freqshift = shift;
1211         } else if (ep->freqshift >= 0)
1212               f <<= ep->freqshift;
1213         else
1214               f >>= -ep->freqshift;
1215
1216         if (likely(f >= ep->freqn - ep->freqn / 8 && f <= ep->freqmax)) {
1217               /*
1218                  * If the frequency looks valid, set it.
1219                  * This value is referred to in prepare_playback_urb().
1220                  */
1221               spin_lock_irqsave(&ep->lock, flags);
1222               ep->freqm = f;
1223               spin_unlock_irqrestore(&ep->lock, flags);
1224         } else {
1225               /*
1226                  * Out of range; maybe the shift value is wrong.
1227                  * Reset it so that we autodetect again the next time.
1228                  */
1229               ep->freqshift = INT_MIN;
1230         }

这里主要是处理来自于USB界面的反馈(并且应付一些不按标准做的USB界面),将获取的值保存进freqm。之后这个值会在snd_usb_endpoint_next_packet_size函数被使用。

145 int snd_usb_endpoint_next_packet_size(struct snd_usb_endpoint *ep)
146 {
147         unsigned long flags;
148         int ret;
149
150         if (ep->fill_max)
151               return ep->maxframesize;
152
153         spin_lock_irqsave(&ep->lock, flags);
154         ep->phase = (ep->phase & 0xffff)
155               + (ep->freqm << ep->datainterval);
156         ret = min(ep->phase >> 16, ep->maxframesize);
157         spin_unlock_irqrestore(&ep->lock, flags);
158
159         return ret;
160 }

这里将freqm转化为下一次的包大小,并供prepare_playback_urb使用
节选http://lxr.free-electrons.com/source/sound/usb/pcm.c:

1451 static void prepare_playback_urb(struct snd_usb_substream *subs,
1452                                  struct urb *urb)
1453 {
1454         struct snd_pcm_runtime *runtime = subs->pcm_substream->runtime;
1455         struct snd_usb_endpoint *ep = subs->data_endpoint;
1456         struct snd_urb_ctx *ctx = urb->context;
1457         unsigned int counts, frames, bytes;
1458         int i, stride, period_elapsed = 0;
1459         unsigned long flags;
1460
1461         stride = runtime->frame_bits >> 3;
1462
1463         frames = 0;
1464         urb->number_of_packets = 0;
1465         spin_lock_irqsave(&subs->lock, flags);
1466         subs->frame_limit += ep->max_urb_frames;
1467         for (i = 0; i < ctx->packets; i++) {
1468               if (ctx->packet_size)
1469                         counts = ctx->packet_size;
1470               else
1471                         counts = snd_usb_endpoint_next_packet_size(ep);
...

prepare_playback_urb这个函数主要决定了音频回放的USB请求数据块(URB)准备工作,包括该传多少USB数据给界面,而这里可以看到snd_usb_endpoint_next_packet_size对于包大小很重要。除此之外没有什么其它作用

如果仔细读代码,可以发现ASYNC和别的同步方式的最大区别它如何影响主机发送数据的多少,其它是与对待别的同步方式一模一样的。

除显式反馈之外还有隐式反馈(这种Feedback是把主机传给USB设备的数据回传回来 让主机自己计算数据速率)。这里就不说明了

ASYNC的最大好处是,USB界面决定了主机每次Frame中每个包该给多少Samples给它,这样USB界面可以自己决定主时钟并且用这个时钟去“校准”主机发送的数据速率,而不再需要适应Host的发送频率

通常高速需要有125us x 2的Buffer,全速需要1ms x 2的buffer。这些构成了USB音频的最小延迟。配合一定的USB Buffer以及合适的FIFO Buffer,就可以从根本上对USB总线的不稳定时钟“去耦”了。这就好比在线看视频听音乐,你的播放软件会维护一个缓冲区,缓冲网络上的数据,这样你就不需要下载所有数据以后才开始观看,并且播放软件实际上也会反馈给服务器相对应的发送速率(虽然实际情形中反馈方式并不像UAC这样)。我觉得没人会说从网络上在线听音乐会有“jitter”吧?最多是缓冲区欠载产生播放停顿 或者缓冲区溢出程序没处理好造成程序崩溃

对于USB界面自身,需要监控自身主时钟与来自主机的SOF包之间的时间,计算出偏差不断给Host反馈。并且因为Host发送速率和实际播放速率并不一致,USB界面自身需要合成与播放相关的Clock(对于德西架构的DAC 最重要的是MCLK),这个合成实现具体做法十分影响最终出来的效果,这对嵌入式开发者是一个不小的挑战。早在十年前TAS1020就已经有异步模式,但需要自己开发单片机程序,开发难度高于像现在XMOS这样的一体式方案,最终出来的jitter也没有现在的XMOS/Amanero等界面那么优异。

另外还有一个常见的误区,就是异步每次数据包里包含的“采样数”可以变化很大,实际上并不是这样(从代码里就可以看出F有个合理范围,超出会认为错误)。USB规范中最多允许每个USB包包含的samples变化在±1内。因此如果之前数据错误丢失了采样,也不可以因此“索取”更多的“采样”
http://www.erji.net/data/attachment/forum/201704/23/195905yf14puu4y9cicobc.jpg

而在实际应用中,解码器厂商需要做好对USB的电气隔离/地线隔离,以免影响USB界面的工作能力以及把PC传过来的共模干扰带进DAC里。

以上的文章内容来自于我对USB2.0/UAC规范的理解。但为了“相对”严谨起见,我列出以下参考以便印证。如有错误欢迎指正
USB的英文维基百科:https://en.wikipedia.org/wiki/USB
USB协议以及其SOF包时间:http://www.beyondlogic.org/usbnutshell/usb3.shtml
共享带宽的方式:https://www.totalphase.com/support/articles/200349256-USB-Background/#s1.1.2.1
Isochronous的通信方式:http://www.beyondlogic.org/usbnutshell/usb4.shtml
UAC1规范:www.usb.org/developers/docs/devclass_docs/audio10.pdf
PCM2706的jitter测量以及主机电源如何影响时钟稳定性:https://www.audialonline.com/articles/spdif-or-usb/
关于XMOS界面的工作原理:https://www.xmos.com/download/private/USB-Audio-Software-Design-Guide(6.6.0rc5.a).pdf


页: [1]
查看完整版本: 【关于所谓的高品质USB输出专机及DDC原理】鉴于紫檀水准每况日下,转一篇隔壁绿檀...

耳机俱乐部微信
耳机俱乐部微信