Skip to main content
欢迎来到PAWPAW技术文档网站了解更多信息

软件架构 - USB Audio

以下部分描述了XMOS USB音频框架的软件架构。

XMOS USB音频解决方案以框架形式提供,参考设计应用可定制和扩展该框架以提供所需功能。这些应用在参考硬件平台上执行。

3.1 USB音频系统架构

XMOS USB音频平台由一系列的通信组件组成。每个系统都需要具备图7中所列的共享组件

组件描述
XMOS USB Device Driver (XUD)处理低等级的USB I/O
端点0
(Endpoint 0)
为端点0提供逻辑,处理设备的枚举和控制,包括DFU相关请求
端点缓冲区
(Endpoint Buffer)
缓冲从主机来或到主机去的端点数据包
解耦器
(Decoupler)
管理端点缓冲区组件和音频组件之间的音频数据包传输。同时也能够处理音量控制。
音频驱动
(Audio Driver)
处理基于I2S的音频I/O,管理从其他数字音频I/O组件来,或到其他音频I/O组件去的音频数据

图7: 共享框架

此外,图8显示了可以被添加到设计中的组件:

组件描述
混音器 (Mixer)允许输入通道和输出通道的数字混音,它也能够处理音量控制而不需要用到解耦器。
S/PDIF发送器
(S/PDIF Transmitter)
输出一个S/PDIF数字音频接口的样本
S/PDIF接收器
(S/PDIF Receiver)
输入一个S/PDIF数字音频接口的样本(需要时钟源组件)
ADAT接收器
(ADAT Receiver)
输入一个ADAT数字音频接口的样本(需要时钟源组件)
时钟源
(Clockgen)
驱动一个外置的频率生成器(PLL),管理在内部时钟和数字输入产生的外部时钟之间的变化
MIDI通过一个串行UART接口输入/输出MIDI
PDM麦克风从麦克风接收PDM数据然后进行从PDM到PCM的转换

图8:可选组件

图9显示了组件之间的相互作用。绿色的圆圈代表核,箭头表示内核间通信。

本节将进一步详细研究这些组件。

3.2 XMOS USB Device (XUD) 库

所有与USB主机的低级通信都由XMOS USB设备(XUD)库处理。

XUD_Manager()函数在它自己的内核中运行,并通过共享内存和通道通信的混合的方式与端点内核进行通信。

image-20220408163709271

图9:USB音频核心图

更多细节和完整的XUD API文档,请参考 XMOS USB Device (XUD) Library

图9显示了XUD库与其他两个内核的通信:

  • 端点0:这个核心控制USB设备的枚举/配置任务
  • 端点缓冲区:这个核心发送/接收来自XUD库的数据包。该核心从解耦器核心接收音频数据,从MIDI核心接收MIDI数据等

3.3 端点0:管理与控制

所有的USB设备都必须支持一个强制性的控制端点,即端点0。这控制了USB设备的管理任务。

这些任务一般可分为枚举、音频配置和固件升级请求。

3.3.1 枚举

当设备第一次连接到主机上时,会发生枚举。这个过程包括主机询问设备的功能。设备通过一组描述符向主机提供几个接口来实现这一目的。

在枚举过程中,主机将向设备发出各种命令,包括为设备分配一个总线上的唯一地址。

端点0的代码在它自己的内核中运行,并遵循与sc_usb_device中的USB设备例子类似的格式(例如:HID鼠标Demo)。就是说,调用USB_GetSetupPacket()来接收来自主机的命令。 这将填充一个USB_SetupPacket_t结构,然后对其进行解析。按照USB规范的要求,USB设备必须支持许多强制性要求。由于这些是所有设备都需要的功能,所以提供了一个USB_StandardRequests()函数(见module_usb_device),它实现了所有这些请求。这包括以下项目:

  • 请求标准描述符(设备描述符、配置描述符请求,等)和字符串描述符
  • USB GET/SET 接口请求
  • USB GET_CONFIGURATION/SET_CONFIGURATION 请求
  • USB SET_ADDRESS 请求

了解更多信息以及获取完整文件,包括简单设备上的完整工作实例,请参考 XMOS USB Device Design Guide.

USB_StandardRequests()函数将设备的各种描述符作为参数,这些参数来自descriptors.h文件中的数据结构。这些数据结构是根据设计如何使用各种定义进行配置而完全定制的(见§7.1)。

USB_StandardRequests()函数返回一个XUD_Result_tXUD_RESULT_OKAY表示请求被完全处理,没有错误,不需要进一步的行动 - 设备应该转到接收主机的下一个请求(通过USB_GetSetupet())。

如果请求没有被USB_StandardRequests()函数识别,函数返回XUD_RES_ERRUSB_StandardRequests()函数不能识别,并且已经发出STALL。

如果主机向总线发出了总线复位,并从XUD传送到端点0,该函数也可能返回XUD_RES_RST

由于USB_StandardRequests()函数STALL了一个未知的请求,终端0代码必须解析USB_SetupPacket_t结构来处理设备的特定请求,然后根据需要调用USB_StandardRequests()。这一点将在接下来描述。

3.3.2 凌驾于标准请求之上

USB音频设计 "覆盖 "了一些由USB_StandardRequests()处理的请求,例如,它使用SET_INTERFACE请求来指示它是否主机正在向设备传输音频。在这种情况下,设置数据包被解析,采取相关行动,调用USB_StandardRequests()来处理对主机的响应等等。

3.3.3 类请求

在调用USB_StandardRequests()之前,设置数据包被解析为类请求。这些被处理在诸如AudioClasRequests_2(), AudioClassRequests_2, DFUDeviceRequests()等函数中,取决于请求的类型。

任何设备的特定请求都会被处理--在这种情况下包括音频类、MIDI类、DFU 请求等。

现在我们将研究一些常见的音频类请求和它们的相关行为。

3.3.3.1 音频请求

当主机发出一个音频请求(例如采样率或音量改变)时,它向端点0发送一个命令。 像所有的请求一样,这是从USB_GetSetupPacket()中返回的。经过一些解析(即作为音频接口的类请求),请求被AudioClassRequests_1()AudioClassRequests_2()函数处理(基于设备是在音频类1.0还是2.0模式下运行)。

注意,音频类1.0的采样率变化会被发送到相关的端点,而不是接口--这在端点0请求解析中被作为一种特殊情况处理,其中AudioEndpointRequests_1()被调用。

AudioClassRequests_X()函数会对请求做进一步解析,以确定要执行的正确音频操作。

3.3.3.2 音频请求:设置采样率

AudioClassRequests_2()函数用于解析被传递的USB_SetupPacket_t结构,以获取对设备拓扑中的时钟单元的SAM_FREQ_CNTROL类型的CUR请求(如设备描述符中所述)。

新的采样频率被提取出来,并通过通道传递给设计的其他部分--通过缓冲代码,最终传递给音频IO/I2S核心。AudioClassRequests_2()函数在向主机发出请求已成功完成的信号前,会先等待与系统的握手回传。注意,在这段时间里,USB库会向主机发出NAK信号,基本上是在采样率改变完全完成之前,暂停进一步的通信/请求。

3.3.3.3 音频请求:音量控制

当主机要求改变音量时,它向端点0发送一个音频接口请求。在端点0的核心中维护着一个数组,它随着请求而更新。

当音量改变时,端点0应用主音量和通道音量,为每个通道产生一个音量值。这些值都存储在数组中。

音量将由解耦器核心或混音器组件(前提是使用了混音器组件)来处理。在混音器中处理音量可以使解耦器有更多的性能来处理更多的通道。

如果音量控制阵列对音频输入和输出的影响是由解耦器实现的,那么解耦器核心会从这个阵列中读取音量值。注意,这个数组是在端点0和解耦器核心之间共享的。这是以一种安全的方式进行的,因为只有端点0可以写到这个数组,字的更新在内核之间是具有原子性的,而解耦器内核只从数组中读取(在这种情况下,写和读之间的排序是不重要的)。解耦器内核使用内联汇编来访问数组,避免了XC的并行使用检查。

如果音量控制是在混音器中实现的,端点0会向混音器发送一个混音器命令来改变音量。混音器命令在[§3.6](#3.6 数字混频器)中描述。

3.4 音频端点(端点缓冲区和解耦器)

3.4.1 端点缓冲区

除了端点0以外的所有端点都在一个核心中处理。这个核心在文件usb_buffer.xc中实现。这个核心直接与XUD库进行通信。

USB缓冲区核心还负责根据USB起始帧(SOF)通知进行反馈计算,并从连接到主时钟的端口计数器中读取数据。

3.4.2 解耦器

解耦器为USB缓冲核心提供缓冲区,以便向/从主机发送/接收音频数据。它将这些缓冲区编入FIFO。然后,FIFO中的数据在需要时通过XC通道被发送到系统的其他部分。这个内核还决定了发送至主机的每个音频包的大小(从而使音频速率与USB包速率相匹配)。解耦器在文件decouple.xc中实现。

3.4.3 音频缓冲方案

这个方案由缓冲核心、解耦核心和XUD库之间的合作来执行。

对于从设备到主机的数据,采用了以下方案:

  1. 解耦核接收来自音频核的样本并将其放入一个FIFO。当数据被输入时,这个FIFO被分割成数据包。数据包的存储格式是由其长度(字节)和数据组成。
  2. 当缓冲区核心需要一个缓冲区来发送至XUD核心时(在发送完前一个缓冲区后),解耦核心会得到信号(通过一个共享内存Flag)。
  3. 在缓冲区核心发出这个信号后,解耦核心将下一个数据包从FIFO传给缓冲区核心。它还向XUD库发出信号,表明缓冲核心能够发送一个数据包。
  4. 当缓冲区核心发送完这个缓冲区后,它向解耦器发出信号说缓冲区已经发送,解耦器核心就会移动FIFO的读指针。

对于从主机到设备的数据,使用以下方案:

  1. 解耦核心将一个指向数据FIFO的指针传递给缓冲核心,并向XUD库发出缓冲核心准备接收的信号。
  2. 然后,缓冲核心将一个USB数据包读入FIFO,并向解耦器发出信号,表明数据包已被读取。
  3. 收到这个信号后,解耦器核心更新FIFO的写指针,并提供一个新的指针给缓冲核心来填充。
  4. 根据音频核心的要求,解耦器核心通过从FIFO中读出样本来向音频核心发送样本。

3.4.4 解耦器/音频核之间的交互

为了满足音频系统的时间要求,解耦核必须及时响应从音频系统发送/接收samples的请求。为了做到这一点,在解耦器内核中设置了一个中断处理程序。中断处理程序是在函数handle_audio_request中实现的。

音频系统通过一个通道向解耦核心发送一个word来请求样本传输(使用内置的outuint函数)。在通道中收到这个字会导致handle_audio_request中断的发生。

中断处理程序所做的第一个操作是发回一个确认请求的字(如果有一个采样频率的变化,将发送一个控制令牌--音频系统使用testct()来检查这种情况)。

现在可以进行样本传输了。首先,音频子系统将样本传送给主机,然后解耦核心将样本从主机传送到设备。这些传输总是以通道数大小的块(即NUM_USB_CHAN_OUTNUM_USB_CHAN_IN)进行的。也就是说,如果设备有10个输出通道和8个输入通道,那么每次中断都有10个样本从解耦核心发送,8个样本被接收。

完整的通信方案如下表所示(对于非采样频率变化的情况):

解耦器音频系统备注
outuint()音频系统请求交换样本
intuint()启动中断并开始执行intuint()
outuint()解耦器发送ACK
testct()检查指示采样率变化的CT值
intuint()字指示 ACK输入(未发生采样率变化)
intuint()outuint()样本传输(从设备到主机)
intuint()outuint()
intuint()outuint()
...
outuint()intuint()样本传输(从主机到设备)
outuint()intuint()
outuint()intuint()
outuint()intuint()
...

图10:解耦/音频系统通道通信

重要

从音频系统发送到解耦器/从解耦器到音频系统的请求和确认是一个 "输出下溢 "的样本值。如果在PCM模式下,它将是0,在DSD模式下,它将是DSD静音。这允许缓冲系统在不知道数据流格式的情况下输出一个合适的下溢值(这在DSD over PCM(DoP)的情况下特别有利)

3.4.4.1 异步反馈

设备使用一个反馈端点来报告音频输出/输入到外部音频接口/设备的速率。这个反馈是符合USB2.0规范的。

这种异步时钟方案,意味着设备是主时钟,因此可以使用高质量的本地主时钟源。

在每次收到USB SOF(Start Of Frame)的token后,缓冲核心都会从主时钟上的一个端口获取一个time-stamp。通过减去上一个SOF的time-stamp,就可以计算出自上一个SOF以来的主时钟刻度数(the number of master clock ticks)。由此可以计算出SOF之间的样本数(作为一个固定数字)。将这个数字在128个SOF中汇总,作为反馈值的基础。

hint

两个相邻SOF之间的时间戳(time-stamp)的差值,即为一个data frame传输所需的时间,即主时钟的刻度数。异步传输时,时钟频率保持不变,那么我们就能通过频率x时间算出每个frame包含的样本数,再以128个样本数的集合作为一组,成为反馈值的基础。从而根据缓存的负载情况,实时的控制USB口的传输速率,从而保证缓存不会溢出或者断流

设备向主机发送反馈信息,也是通过一个明确的反馈IN端点,在USB缓冲核心中处理的。如果(异步)输入和输出都被启用,那么反馈是基于发送到主机的音频流的隐式反馈。

3.4.4.2 USB 速率控制

音频核必须处理从USB来的数据,然后将数据以正确的速率提供给USB,这个速率是以所选的采样频率为基准的。USB2.0标准声明,传输时USB Packet最大的速率变化不得超过+/- 1个样本每USB frame。USB frame是以8kHz的速率被传输的,也就是说,对于48kHz (的音频),每个packet需要包含6个USB frame。设备使用的是异步模式,所以音频时钟可能会漂移,导致运行的比主机快/慢。因此,如果音频时钟稍微有一点快,设备可能会偶尔输入/输出7个sample而不是6个。同时,也可能会有一点慢,偶尔输出/输入5个sample而不是6个。图11显示出对于每个音频采样率,每个packet被允许传输的sample范围。

image-20211230152314154

图11. 每个packet允许传输的sample数

解耦核使用缓冲核中计算出的反馈值来实现控制。这个反馈值会被用于计算下一个将要被插入音频的FIFO中的packet的大小。

3.5 音频驱动

音频驱动通过一个XC channel,在它与解耦器(decoupler)或混频器(mixer)之间传输或者接收sample。然后它会驱动几个输入或输出的I2S/TDM channels。如果固件通过CODEC(编解码器)被设置为从(slave),它将同样驱动这个核中的字或者字节时钟。字时钟与位时钟以及数据都来自传入的主时钟(通常是外置晶振或者PLL输出的时钟)。音频驱动在文件audio.xc中实现。

音频驱动通过I2S捕捉和播放音频数据。它还将相关的音频数据转发到S/PDIF传输核心

音频核心必须和支持I2S的CODEC连接(其他的模式,例如左调整可以通过一些固件改动来支持)。在从模式下,XMOS设备作主,产生BCLK(Bit Clock)以及LRCLK(Left-Right Clock或Word Clock)信号。任何支持I2S的CODEC或者DAC/ADC组合,都可以使用。

图12显示了用于在XMOS设备和CODEC之间进行音频通信的信号。

image-20211230160629857

图12. I2S信号

位时钟控制从CODEC传出或传入数据的速率。在XMOS设备作主的情况下,它会将MCLK划分为BCLK和LRCLK需求的信号,然后BCLK会被用来为CODEC的数据输入(SDIN)和数据输出(SDOUT)提供时钟。

图13展示了一些不同采样率的时钟频率和分频的例子(注意:这反映的是只有单个tile的L系列参考板的设置):

image-20211230161725733

图13. 在单tile的L系列参考设计中使用的时钟分频器

主时钟必须通过外部来源提供,例如时钟发生器、固定的晶振、PLL等,以产生两种频率来支持44.1kHz和48kHz音频频率(例如分别为11.2896/22.5792MHz 和 12.288/24.576MHz)。这个主时钟输出之后会被提供给CODEC和XMOS设备

3.5.1 端口设置(xCORE作主)

默认的软件设置是CODEC作从(xCORE作主)。意思是,XMOS设备会提供BLCK和LRCLK信号给CODEC。

XS1端口和XMOS时钟为实现I2S提供了很多宝贵的功能。这一节会描述这些功能如何配置和使用,以驱动I2S接口。

image-20211230162957485

图14. 端口与时钟(CODEC作从)

代码中配置端口和时钟的是ConfigAudioPorts()函数。开发者不需要修改此函数。

XMOS设备输入MCLK然后将其划分生成BCLK和LRCLK。

为了实现时钟的分割,MCLK通过1-bit端口p_mclk输入到设备中。它被连接到时钟块clk_audio_mclk,后者又被用来为BCLK端口p_bclk提供时钟。BCLK被用作为LRCLK(p_lrclk)和数据信号SDIN(p_sdin)和SDOUT(p_sdout)提供时钟。同样,另一个时钟块(clk_audio_bclk)以p_bclk作为输入,用于为端口p_lrclkp_sdinp_sdout提供时钟。前面的图14显示了端口和时钟块之间的连接情况。

p_sdinp_sdout被设置为一个传输宽度为32bit的缓冲端口,因此所有的32bit都在一个输入语句中被输入。这就使得软件可以输入、处理以及输出32bit的word,同时端口串行化和反串行化到连接到每个端口的单一I/O引脚。

xCORE-200系列设备能够在一个时钟块中分割一个外部时钟。然而,基于XS1的设备则不具备这种功能。为了在XS1设备上实现所需的主时钟到位时钟或LR时钟的分割,传输宽度为32bit的缓冲端口也被用于p_bclkp_lrclk。位时钟是通过对p_bclk执行一个特定模式的输出来产生的,以实现用期望的速率来切换输出。这个模式依赖于主时钟和位时钟之间的划分。下面的表展示了对于不同的分割值,所需的输出模式是什么。

image-20211230171254810

图15. 输出模式

无论如何,位时钟的每个sample都会输出32个时钟周期(一个bit表示一个周期)。特殊情况下,分频为1(例如位时钟频率等于主时钟频率的情况,p_bclk端口被设置为一种特殊模式,它只是输出其时钟输入(即p_mclk)。详情见xs1.h中的configure_port_clock_output()

hint

术语 "位时钟(bit clock)"用于描述数字音频设备内用于内部同步的每个sample一个周期的 "方波 "信号。

image-20211230175249995

时钟分频,即对于高频率的主时钟,通过将时钟的每n个周期合并为1个周期,称为n分频,以产生不同频率的时钟。如上图,是一种四分频。

p_lrclkp_bclk提供时钟。在I2S模式下,端口重复输出0x7fffffff0x80000000的模式。这就提供了一个信号,在数据之前有一个位时钟的转换(按照I2S标准的要求),并且在高电平和低电平之间交替,用于音频的左右声道。

3.5.2 改变时钟采样率

当主机改变采样率时,一个新的频率就通过Endpoint 0被发送给了音频驱动核(audio driver core)(通过缓冲核以及混频器)。

首先,通过XC通道发送新的频率来报告采样率的变化。音频核心通过检查通道上是否存在control token来检测这一点。

收到改变采样频率的请求以后,音频核会停止I2S/TDM接口并调用CODEC/端口配置功能。

一旦完成后,I2S/TDM接口会重新以新的频率开始。

3.6 数字混频器

混频器核心接收从解耦器核心传出的音频,以及从音频驱动核心传入的音频。然后它为每个通道设置对应的音量,并将传入的音频传给解耦器,传出的音频传给音频驱动。音量的更新是通过内置的32bit到64bit的有符号的乘积函数(macs)实现的。混频器在文件mixer.xc中实现。

混频器核心需要两个核来驱动。运行8个混频器时,最高支持96kHz采样率下的18路输入;当需要支持更高的采样率时,仅运行两个混频器,同样最高支持18路输入。当切换到高采样率时,该组件会自动下移到两个混频器。

hint

混频器的作用是将不同的音频通道混合在一起,混合后的通道可以提供给主机或者DAC等等。每个混频器之间都是独立的。 当我们需要混合高于96kHz采样率的音频时,由于资源有限,XMOS在软件实现中,仅能使用两个混频器。当采样率低于或等于96kHz时,则可以使用全部的8个混频器。

混频器可以从下列任一项中获取输入:

  • 从主机获取的USB输出 - 这些samples来自解耦器核心
  • 从设备上的音频接口获取的输入 - 这些samples来自音频驱动

由于这些输入的总和可能多于每个混频器的18个可能的混合输入,所以有一个从所有可能的输入到混频器输入的映射。

在混频发生后,最终的输出就被创造出来。这里有两个输出的去向:

  • 通过USB输入到主机 - 这些samples会被送去解耦器核心
  • 通过设备上的音频接口输出 - 这些samples会被送去音频驱动

对于每一个可能的输出,存在一个映射,去告诉混频器他们的来源是哪里。可能的来源是来自主机的USB输出、音频接口的输入或混频器单元的输出。

§3.3.3.3中提到过的,混频器同样可以处理音量设置。如果混频器被设置来处理音量但混频(mix)的数量被设置为0(所以该组件只做音量设置),之后该组件将仅使用一个核心。

3.6.1 控制

混频器可以接受下列来自端点0,通过一个通道传输来的控制命令:

命令描述
SET_SAMPLES_TO_HOST_MAP设置通往主机的一个音频流的来源。
SET_SAMPLES_TO_DEVICE_MAP设置进入音频驱动器的一个音频流的来源。
SET_MIX_MULT设置混频器的一个输入的乘数
SET_MIX_MAP设置混频器的一个输入源
SET_MIX_IN_VOL如果音量调整是在混频器中进行的,该命令会设置一个USB音频输入的音量倍率
SET_MIX_OUT_VOL如果音量调整是在混频器中进行的,该命令会设置一个USB音频输出的音量倍率

图16. 混频器组件命令

3.6.2 主机控制

通过从主机PC发送请求到端点0,可以控制混频器。XMOS提供了一个简单的基于命令行的示例应用程序,演示如何控制混频器。

详情请查阅host_usb_mixer_control 目录下的README文件。

这种控制的主要要求是

  • 设置从输入通道到混频器的映射
  • 设置每个混频器输入的输出系数
  • 设置物理输出的映射,物理输出可以直接来自输入或通过混频器。

这种配置有足够的灵活性,往往有多种方法来创造所需的解决方案。

在使用XMOS主机控制示例应用程序时,考虑将混频器设置为从模拟输入1/2到模拟输出1/2进行回环。

首先来看看混频器的输入:

./xmos_mixer --display -aud -channel -map 0

显示哪些通道被映射到哪些混频器输入:

./xmos_mixer --display -aud -channel -map -sources 0

显示哪些通道可能被映射到混频器输入。注意,模拟输入1/2是在混频器输入10和11上。 现在检查一下音频输出的映射:

./xmos_mixer --display -aud -channel -map 0

显示哪些通道被映射到哪些输出。默认情况下,所有这些都会绕过混音器。我们还可以看到所有可能的映射是什么:

./xmos_mixer --display -aud -channel -map -sources 0

所以现在把前两个混音器的输出映射到物理输出1/2:

./xmos_mixer --set -aud -channel -map 0 26
./xmos_mixer --set -aud -channel -map 1 27

你可以通过重新检查映射来确认是否生效:

./xmos_mixer --display -aud -channel -map 0

现在,这使得模拟输出1/2来自混音器,而不是直接来自USB。然而,混音器仍然被映射为将USB通道传递给输出,所以在功能上仍然没有变化。 混音器的节点需要单独设置。它们可以用以下方式显示。

./xmos_mixer --display -mixer -nodes 0

为了从模拟输入端获得音频到输出1/2,需要设置节点80和89:

./xmos_mixer --set -value 0 80 0
./xmos_mixer --set -value 0 89 0

同一时间,原始的混频器输出也可以被静音:

./xmos_mixer --set -value 0 0 -inf 
./xmos_mixer --set -value 0 9 -inf

现在模拟1/2上的音频输出可以在输出1/2上被听到了。

就像上面提到的,混频器的灵活性决定了会有多种方式来创建一个特定的混频。另外一个创建同样的路由的选择是改变混频器的来源,比如混频器1/2的输出来自模拟输入。

为了证明这一点,首先撤销上面的修改:

./xmos_mixer --set -value 0 80 -inf 
./xmos_mixer --set -value 0 89 -inf
./xmos_mixer --set -value 0 0 0
./xmos_mixer --set -value 0 9 0

混频器现在应当使用的是默认的值。现在可以改变混频器1/2的来源了:

./xmos_mixer --set -mixer -source 0 0 10 
./xmos_mixer --set -mixer -source 0 1 11

如果你再运行一次:

./xmos_mixer --display -mixer -nodes 0

第一栏现在有AUD-模拟1/2,而不是DAW(数字音频工作站,即主机)-模拟1/2会确认新的映射。同样,通过向模拟输入1/2播放音频,可以听到循环到模拟输出1/2。

hint

AUD即Audio,DAW即Digital Audio Workstation。此处的demo主要是用于演示mixer的一些应用,如果需要尝试可以在XTC中使用。

3.7 S/PDIF 传输

XMOS设备可以支持S/PDIF传输,最高至192kHz。XMOS S/PDIF传输组件在一个核上运行,具体的代码可以在路径sc_spdif/module_spdif_tx中找到。

S/PDIF传输核心通过一个通道接收PCM音频samples,然后将他们通过一个端口以S/PDIF的格式输出。这里使用一个查找表来将音频数据编码成所需的格式。

它从音频I/O核心一次接收两个样本(用于左和右声道)。对于每个样本,它对每个字节进行查找,产生16位编码的 数据,并将其输出到一个端口。

S/PDIF以帧为单位发送数据,每个帧包含192个左右声道的样本。

音频样本被封装成S/PDIF字(加入前导、奇偶校验、通道状态和有效性位),并以双相标记编码(biphase-mark encoding, BMC)相对于外部主时钟传输。

注意:对SpdifTransmitPortConfig函数稍作改动,就可以启用内部主时钟生成(例如,当时钟源已经锁定在所需的音频时钟上时)。

keyvalue
采样率44.1, 48, 88.2, 96, 176.4, 192 kHz
主时钟倍率128x, 256x, 512x
模式module_spdif_tx

图17. S/PDIF的兼容性

3.7.1 时钟

D-Type Jitter Reduction

图18:减少D-Type抖动

S/PDIF信号的输出速率由外部主时钟(MCLK)决定。主时钟必须是双向标记编码(BMC- Biphase Mark Code)比特率的1倍2倍或4倍(即分别是128倍256倍或512倍的音频采样率)。例如,192kHz的最小主时钟频率是24.576MHz。

这就把主时钟重新采样到它的时钟域(晶振),在S/PDIF信号上引入了2.5-5ns的抖动。一个典型的抖动减少方案是使用一个由主时钟时钟化的外部D型触发器(如图18所示)。

HInt

关于如何计算最小主时钟频率:

S/PDIF用于传输数据使用双相标记编码,其原理是使用一个两倍于传输位率的时钟频率做为基准,把原来一位数据拆成两份,允许从信号本身提取原始字时钟(BCLK)。故我们需要传输的数据量也是原来的两倍。

以上文的192kHz为例,如果传输的是PCM,我们至少需要传输一个2通道,24bit(放在8个bytes里,即32bit)的数据。而对于S/PDIF,我们需要传输两倍的原始数据。故计算为:

192kHz × 2ch × 32bit × 2(for BMC) = 24.576MHz`

3.7.2 使用方法

S/PDIF传输核心的接口是通过一个带有内置流(outuint,inuint)的常规通道实现的。数据格式应为一个32bit的word中的24bit左对齐:例如0x12345600。

下列协议将在以下通道中使用

S/PDIF stream structure image

图19. S/PDIF组件协议

3.7.3 输出流的结构

如同图20所表示的,流是由words组成的。通道的状态bits是0x0nc07A4,当c为1时表示左通道,c为2表示右通道。n则表示采样率,如图21所示。

S/PDIF Stream
Structure table

图20:S/PDIF流结构

Channel Status Bits table

图21:通道状态bits

3.8 S/PDIF接收

XMOS设备最高可支持192kHz的S/PDIF输入。

S/PDIF接收模块使用了一个时钟区和一个缓冲1bit端口。时钟区由一个100MHz的参考时钟分频而来。1bit端口被缓冲到4bits。接收器代码使用这个时钟对输入数据进行过度采样。

提示

对于为什么要缓冲到4bit: 通过前四个bits,我们可以得知 S/PDIF RX Word 的标签(tag)值,即可判断frame的类型

接收器通过一个流媒体通道端输出音频样本,数据可以使用内置的输入运算器输入。 S/PDIF接收功能从不返回。来自通道输入的32位值包括:

S/PDIF RX Word
Structure

图22:S/PDIF RX Word结构

标签具有以下三个值之一:

S/PDIF RX Tags

图23:S/PDIF RX标签

关于格式、用户bits等进一步的细节,请参见S/PDIF规范。

3.8.1 使用和集成

因为S/PDIF是一个数字流,而设备的主时钟必须与它同步。这通常是通过一个外部的Fractional-N时钟倍频器完成的。见时钟恢复(§3.10)

Hint

小数 N 频率合成器提供了一种启用小步长的方法,同时保持较高的比较频率以提高合成器的性能。

S/PDIF接收功能与clockGen组件通信,将音频数据传递给音频驱动器,并在需要时处理对S/PDIF时钟源的锁定(见外部时钟恢复)。 理想情况下,应检查所收到的每个word/sample的奇偶性。这可以通过使用内置的crc32函数来完成(见xs1.h)。

/* Returns 1 for bad parity, else 0 */ 
static inline int badParity(unsigned x) {
unsigned X = (x>>4);
crc32(X, 0, 1);
return X & 1;
}

如果检测到坏的奇偶校验,则word/sample将被忽略,否则将检查标签的通道(即左或右通道)并存储样本。

下面的代码片断说明了如何使用S/PDIF接收组件的输出:

while(1){
c_spdif_rx :> data;
if(badParity(data)
continue;
tag = data & 0xF;
/* Extract 24bit audio sample */
sample = (data << 4) & 0xFFFFFF00;
switch(tag){
case FRAME_X:
case FRAME_Y:
// Store left
break;
case FRAME_Z:
// Store right
break;
}
}

3.9 ADAT接收

ADAT接收组件以44.1kHz或48kHz的采样率接收最多八个通道的音频。调用接收器功能的API描述在 §7.3.

该组件输出32bit word,分成9个字框。这些字框是以下列方式排列的:

Control byte

Channel 0 sample

Channel 1 sample

Channel 2 sample

Channel 3 sample

Channel 4 sample

Channel 5 sample

Channel 6 sample

Channel 7 sample

关于如何读取ADAT组件的输出的代码示例,如下所示:

control = inuint(oChan); 
for(int i = 0; i < 8; i++) {
sample[i] = inuint(oChan);
}

Samples是包含在word里的24bit的24bit值。控制bit包括位[11...8]的四个控制bits和bits[7...0]的值0b00000001。这个控制bit可以在更高的层次上实现同步,在通道上总是先读一个奇数bit,然后再读八个data bits。

3.9.1 集成

由于ADAT是一个数字流,设备的主时钟必须与它同步。这通常是通过一个外部的Fractional-N时钟倍频器实现的。

ADAT接收功能与clockGen组件进行通信,该组件将音频数据传递给音频驱动器,并在需要时处理对ADAT时钟源的锁定。

3.10 外部时钟恢复(ClockGen)

应用程序既可以通过可选择的振荡器,时钟生成集成电路等提供固定的时钟源,也可以使用外部的PLL/时钟乘法器,在XMOS器件的参考基础上生成主时钟。

使用外部PLL/时钟乘法器可以使设计电路锁定来自数字流(如S/PDIF或ADAT输入)的外部时钟源。

时钟恢复核心(ClockGen)负责生成参考频率给Fraction-N时钟倍频器。这反过来又产生了整个设计电路中使用的主时钟。

当运行在内部时钟模式下时,该内核只需使用一个基于XMOS参考时钟的本地计时器来生成时钟。

当运行在外部时钟模式(即S/PDIF时钟 "或 "ADAT时钟 "模式)时,时钟样本来源于S/PDIF和/或ADAT接收核心。

通过计数一个给定周期内的样本数,可以计算外部频率。而Fractional-N时钟倍频器的参考时钟是基于这个外部流生成的。如果这个流变得不可用,计时器事件将会启动,以确保继续生成有效的主时钟,而不考虑断电拔出等原因。

ClockGen核心从端点0通过c_clk_ctl通道获取时钟选择的Get/Set命令。这个核心也会记录外部时钟的有效性,这也是通过同一通道从端点0查询的。

这个核心还可以使解耦核心在时钟有效性改变时请求一个中断包。这个功能是基于Audio Class 2.0 状态/中断端点的特性实现的。

3.11 MIDI

MIDI驱动实现了31250波特率UART接口的输入输出。当从缓冲核接收32-bit的USB MIDI时间时,它会将这些数据解析然后将它们翻译成8-bit的MIDI信息,然后通过UART传输。同样,传入的8位MIDI信息也会被聚合成32-bit的USB-MIDI事件,并传递给缓冲核。MIDI核心在文件usb_midi.xc中实现。

3.12 PDM 麦克风

3.12.1 PDM实现概述

此处的设计能够集成PDM麦克风。从麦克风采集的PDM流被转化为PCM信号,然后通过USB输出给主机。

与PDM麦克风的交互是通过使用XMOS麦克风阵列库(lib_mic_array)来完成的。lib_mic_array被设计为允许与PDM麦克风交互,辅以高效数字信号降采样(decimation),达到用户可选择的预期输出采样率。

lib_mic_array库只适用于xCORE-200系列设备。

该库使用了以下组件:

  • PDM接口
  • 四通道降采样

对于每个高通道数PCM接口(mic_array_pdm_rx())可以连接多达16个PDM麦克风。对于1-4个处理任务,使用mic_array_decimate_to_pcm_4ch(),每个任务最多处理4个通道。对于1-4个通道,该库需要两个逻辑核心:

One to four channel count PDM interface

图24:1到2个通道数的PDM接口

对于5-8个通道,需要使用3个逻辑核,如下图所示:

image-20220120114328635

图24:5到8个通道数的PDM接口

最左边的任务,mic_array_pdm_rx(),对多达8个麦克风进行采样,并对数据进行过滤,以提供多达8个384kHz的数据流,然后分成两个四通道的流。然后处理现场将信号分解为用户所选择的采样率(48、24、16、12或8KHz等之一)。

通过增加专门用于PDM任务的内核数量,可以支持更多的通道。然而,目前集成到USB Audio中的PDM麦克风的数量最多被限制在8个以内。

在对输出采样率进行降采样后,还需要进行其他各种步骤,例如消除直流偏移、增益校正和补偿等。请参阅lib_mic_array文件,了解进一步的实现细节和完整的功能设置。

3.12.2 PDM麦克风硬件特性

PDM麦克风需要一个时钟输入,并在数据输出上提供PDM信号。所有的PDM麦克风共享相同的时钟信号(在PCB上进行适当的缓冲),并输出到八根数据线上,这些数据线连接到一个8-bit的端口:

image-20220120200704916

唯一一个需被传递到lib_mic_array里的端口是8-bit的数据端口。该库假定输入端口使用PDM时钟,而不需要知道PDM时钟源。

麦克风的输入时钟可以通过多种方法来生成。例如,一个3.072MHz的时钟可以在电路板上生成,或者xCORE可以将12.288MHz的主时钟分频生成。或者,如果时钟准确度不重要,内部的100MHz参考时钟可以用于分频,以提供一个近似的时钟。

3.12.3 将PDM麦克风集成到USB音频中

PDM麦克风包装器从main()调用,然后接收一个通道参数,将其与系统其他部分连接起来:

pcm_pdm_mic(c_pdm_pcm);

这个函数的实现可以在文件pcm_pdm_mics.xc中找到。

这个函数的首要工作是配置麦克风的端口/时钟,它将外部音频主时钟输入(在端口p_mclk上)进行分频,并通过端口p_pdm_clk将分频后的时钟输出给麦克风:

configure_clock_src_divide(pdmclk, p_mclk, MCLK_TO_PDM_CLK_DIV); 
configure_port_clock_output(p_pdm_clk, pdmclk);
configure_in_port(p_pdm_mics, pdmclk);
start_clock(pdmclk);

之后它会运行前面讨论的PDM接口和PDM到PCM转换所需的各种内核:

par {
mic_array_pdm_rx(p_pdm_mics, c_4x_pdm_mic_0, c_4x_pdm_mic_1);
mic_array_decimate_to_pcm_4ch(c_4x_pdm_mic_0, c_ds_output[0]);
mic_array_decimate_to_pcm_4ch(c_4x_pdm_mic_1, c_ds_output[1]);
pdm_process(c_ds_output, c_pcm_out);
}

pdm_process()任务包括主要的集成代码,它从lib_mic_array核中获取音频,对其进行缓冲,然后执行可选的本地处理并输出到音频驱动(TDM/I2S核)。

这个函数简单地调用mic_array_get_next_time_domain_frame()以便从麦克风获得一帧PCM音频。然后,它会等待来自音频/I2S/TDM核心的音频sample请求,并通过该通道将音频帧送回。

注意,这里假设系统共享一个全局主时钟,因此不需要额外的缓冲或者速率匹配/转换。

3.13 资源使用情况

下表详细说明了参考设计软件中每个组件的资源使用情况:

image-20220121103101899

图27:资源使用情况

注意

这些资源的估算是基于多通道参考设计,并启用该设计的所有选项。对于较少的通道,资源的使用可能会减少

特别注意

XUD库需要一个80MIPS的内核才能正常运行(即在一个500MHz的部件上只能运行六个内核)

不可用的资源 ❌

ULPI端口是L系列设备上的一组固定端口。当使用这些端口时,当ULPI激活时,其他端口是不可用的。更多细节请参见XS1-L硬件设计检查表。

写于2022年1月21日:该检查表在XMOS上已经不可用,XS1-L属于XMOS第一代产品,目前使用非常少,不需要特别关注。