实现细节
本章将介绍组成lib_xua的各个组件的实现方式。还将讨论依赖项和支持库的集成。
音频中心(Audio Hub)
音频中心会执行多个功能。它通过通道与解耦器(Decoupler)或混音器(Mixer)核心交换数据,即接收和传输音频样本。
它还驱动多个输入和输出的I2S/TDM通道,与CODEC、DAC、ADC等外部音频硬件进行数据交换。从现在开始,这些外部设备将被称为“音频硬件”。
如果固件配置了xCORE作为I2S Master,则还会从该任务驱动所需的时钟线。它还负责转发和接收来自其他音频相关任务/核心(如S/PDIF任务、ADAT等)的样本。

在 Master 模式下,xCORE生成I2S的“连续串行时钟(SCK)” (或称 位时钟 BCLK) 以及 “字选择时钟(WS)” (或称 左右时钟 LRCLK)信号。任何支持I2S协议的CODEC或DAC/ADC组合都可以使用这一模式。
左右时钟、位时钟和数据都是从输入的主时钟分频出来的(主时钟通常来源于外部晶振或PLL的输出)。这不是I2S标准的一部分,但通常包含在内,用于同步模拟/数字转换器的内部操作。
音频中心任务的实现在xua_audiohub.xc文件中。
图27显示了在XMOS设备和外部音频硬件之间通信所使用的信号。
| 信号 | 描述 |
|---|---|
| LRCLK | 字时钟,在一个采样值开始时跳变 |
| BCLK | 位时钟,用于对齐输入和输出数据 |
| SDIN | 采样数据输入(从CODEC/ADC到XMOS设备) |
| SDOUT | 采样数据输出(从XMOS设备到CODEC/DAC) |
| MCLK | 驱动CODEC/DAC/ADC的主时钟 |
位时钟控制着数据与外部音频硬件之间传输的速率。 如果xCORE设备是 Master,则它将MCLK分频以生成所需的BCLK和LRCLK信号,其中BCLK用于时钟数据的输入(SDIN)和输出(SDOUT)。
图28显示了不同采样率下的一些示例时钟的频率和分频系数:
对于xCORE-200系列设备,主时钟必须由外部源提供,例如时钟发生器、固定晶振、PLL等。xCORE.ai设备可以使用集成的次级PLL(APP PLL),通过Tile1上的1D端口输出MCLK。
为了支持44.1kHz和48kHz音频频率(例如11.2896/22.5792MHz和12.288/24.576MHz),需要两个主时钟频率。对于xCORE-200系列,这需要两个外来时钟源。然后,将此主时钟输入提供给外部音频硬件和xCORE设备。
| 采样率(kHz) | MCLK(MHz) | BCLK(MHz) | 分频系数 |
|---|---|---|---|
| 44.1 | 11.2896 | 2.819 | 4 |
| 88.2 | 11.2896 | 5.638 | 2 |
| 176.4 | 11.2896 | 11.2896 | 1 |
| 48 | 24.576 | 3.072 | 8 |
| 96 | 24.576 | 6.144 | 4 |
| 192 | 24.576 | 12.288 | 2 |
端口配置(xCORE Master )
默认软件配置是xCORE作为I2S Master 。也就是说,XMOS设备提供BCLK和LRCLK信号给外部音频硬件。
xCORE的端口和XMOS的时钟提供了许多有价值的功能来实现I2S。本节描述了如何配置和使用这些端口和时钟来驱动I2S接口。
配置端口和时钟的代码位于ConfigAudioPorts()函数中。开发人员不需要修改此代码。
xCORE输入MCLK并将其分频以生成BCLK和LRCLK。
为了实现这一点,MCLK通过1位端口p_mclk输入到设备中。它连接到时钟块clk_audio_mclk,并用于时钟端口p_bclk的时钟。BCLK用于时钟数据输入(p_sdin)和输出(p_sdout)。 同样,使用一个时钟块(clk_audio_bclk),该时钟块的输入是p_bclk,并用于时钟端口p_lrclk、p_sdin和p_sdout的时钟。上述图示显示了端口和时钟块的连接方式。
p_sdin和p_sdout被配置为带有32位传输宽度的带缓冲端口,因此所有32位数据在一个输入语句中进行输入。这允许软件输入、处理和输出32位字,而端口则将其序列化和反序列化到与每个端口连接的单个I/O引脚。
与之前的xCORE架构不同,xCORE-200(XS2)和xCORE.ai(XS3)系列设备具有将外部时钟分频的能力,可以在时钟块中进行分频。
位时钟每个采样输出32个时钟周期。在特殊情况下,如果分频为1(即位时钟频率等于主时钟频率),则将p_bclk端口设置为特殊模式,其中它简单地输出其时钟输入(即p_mclk)。有关详细信息,请参阅xs1.h中的configure_port_clock_output()函数。p_lrclk由p_bclk进行时钟同步。在I2S模式下,该端口重复输出0x7fffffff和0x80000000。这样可以得到一个信号,在数据之前有一个位时钟的跳变(符合I2S标准要求),并且对于左右声道的音频,高低电平交替变化。
更改音频采样率
当主机更改采样率时,新的采样率会被通过端点0(通过缓冲核心和混音器 )发送到音频驱动程序核心。
首先,新的采样率会通过XC通道发送,来报告采样率的更改。音频核心通过检查通道上是否存在控制token来检测到这一点。
在接收到采样率更改请求后,音频核心停止I2S/TDM接口并调用CODEC/端口配置函数。
完成此操作后,会以新的采样率重新启动I2S/TDM接口(即AudioHub中的主循环)。
端点0:管理和控制
所有USB设备必须支持一个强制性的控制端点,即端点0。这个端点控制着USB设备的管理任务。
这些任务通常可以分为枚举、音频配置和固件升级请求。
枚举
当设备首次连接到主机时,会进行枚举。这个过程涉及主机向设备询问其功能。设备通过通过一组描述符向主机呈现多个接口来实现这一点。 在枚举过程中,主机将向设备发出各种命令,包括在总线上为设备分配唯一地址。 端点0的代码在其自己的核心中运行,并遵循lib_xud中USB设备示例(例如HID鼠标演示)的类似格式。也就是说,调用USB_GetSetupPacket()来接收来自主机的命令。来自主机的命令会填充一个USB_SetupPacket_t结构体,然后进行解析。
根据USB规范,USB设备必须支持许多强制性请求。由于这些请求对所有设备都是必需的,以便正常工作,因此提供了一个USB_StandardRequests()函数(参见xud_device.xc),该函数实现了所有这些请求。其中包括以下内容:
- 请求标准描述符(设备描述符、配置描述符等)和字符串描述符
- USB GET/SET INTERFACE请求
- USB GET/SET_CONFIGURATION请求
- USB SET_ADDRESS请求
有关更多信息和完整文档,请参阅lib_xud,其中包括简单设备的完整示例。
USB_StandardRequests()函数将设备的各种描述符作为参数,这些描述符从xud_ep0_descriptors.h文件中的数据结构传递。这些数据结构根据设计使用的各种定义进行完全定制。
USB_StandardRequests()函数返回一个XUD_Result_t。XUD_RESULT_OKAY表示请求已经完全处理且没有错误,不需要进一步操作 - 设备应该继续接收来自主机的下一个请求(通过USB_GetSetupPacket())。如果USB_StandardRequests()函数无法识别请求并发出STALL,则返回XUD_RES_ERR。
如果主机发出总线复位并从XUD传递到端点0,则该函数还可能返回XUD_RES_RST。由于USB_StandardRequests()函数会对未知请求发出STALL,因此端点0代码必须首先解析USB_SetupPacket_t结构以处理设备特定的请求,然后根据需要调用USB_StandardRequests()。
覆盖标准请求
USB音频设计“覆盖”了USB_StandardRequests()处理的一些请求,例如使用SET_INTERFACE请求指示主机是否向设备流式传输音频。在这种情况下,解析设置数据包,执行相关操作,并仍然调用USB_StandardRequests()以处理对主机的响应。
类请求
在调用USB_StandardRequests()之前,将解析设置数据包以处理类请求。这些请求在AudioClassRequests_1()、AudioClassRequests_2()、DFUDeviceRequests()等函数中进行处理,具体取决于请求的类型。任何设备特定的请求都会被处理 - 在这种情况下是音频类、MIDI类、DFU请求等。
现在将讨论一些常见的音频类请求及其相关行为。
音频请求
当主机发出音频请求(例如采样率或音量更改)时,它会向端点0发送一个命令。与所有请求一样,这是从USB_GetSetupPacket()返回的。经过一些解析(即作为音频接口的类请求),该请求由AudioClassRequests_1()或AudioClassRequests_2()函数处理(取决于设备是否运行在音频类1.0或2.0模式下)。
注意,音频类1.0采样率更改被发送到相关端点,而不是接口 - 这在端点0请求解析中作为特殊情况处理,调用了AudioEndpointRequests_1()。
AudioClassRequests_X()函数进一步解析请求以确定要执行的正确音频操作。
音频请求:设置采样率
AudioClassRequests_2()函数解析传递的USB_SetupPacket_t结构,以获取对设备拓扑中的时钟单元的CUR类型SAM_FREQ_CONTROL请求。提取新的采样率,并通过通道传递给设计的其余部分 - 通过缓冲代码最终传递给音频中心(I2S)核心。AudioClassRequests_2()函数等待握手信号在系统中传播回来,然后向主机发出请求已成功完成的 信号。请注意,在此期间,USB库正在NAK主机,从而阻止进一步的流量/请求,直到采样率更改完全完成。
音频请求:音量控制
当主机请求改变音量时,它会向端点0发送一个音频接口请求。在端点0核心中维护了一个数组,用于更新此类请求。
当改变音量时,端点0应用主音量和通道音量,为每个通道生成一个单一的音量值。这些值被存储在数组中。
音量的处理方式可以由解耦核心或者混音器组件(如果使用了混音器组件)来完成。将音量处理交给混音器可以使解耦器有更多的性能来处理更多的通道。
如果音量控制数组对音频输入和输出的影响是由解耦器实现的,则解耦器核心从该数组中读取音量值。请注意,该数组在端点0和解耦器核心之间是共享的。这样做是安全的,因为只有端点0可以对数组进行写入,核心之间的字更新是原子的,并且解耦器核心只从数组中读取(在这种情况下,写入和读取之间的顺序不重要)。解耦器核心使用内联汇编来访问数组,避免了XC并行使用检查。
如果音量控制是在混音器中实现的,端点0会向混音器发送一个混音器命令来改变音量。混音器命令在数字混音器中有描述。
音频端点(端点缓冲区和解耦器)
端点缓冲区
除了端点0外,所有其他端点都由一个核心处理,该核心实现在文件 ep_buffer.xc 中。该核心直接与XUD库进行通信。USB缓冲核心负责基于USB帧起始(SOF)通知的反馈计算,并从连接到主时钟的端口计数器中读取数据。
解耦器
解耦器为USB缓冲核心提供用于向主机传输/接收音频数据的缓冲区。它将这些缓冲区编组成FIFO。然后,从FIFO中的数据通过XC通道发送到系统的其他部分。在异步模式下,该核心还确定发送到主机的每个音频数据包的大小,使音频速率与USB数据包速率匹配。解耦器实现在文件 decouple.xc 中。
音频缓冲方案
此方案通过缓冲核心、解耦核心和XUD库之间的协作来执行。
对于从设备到主机的数据:
- 解耦核心从音频中心接收样本并将其放入FIFO中。当输入数据时,该FIFO会被分割成数据包。数据包以字节长度及其后的数据格式存储。
- 当端点缓冲核心需要发送到XUD核心的缓冲区(在发送前一个缓冲区后),通过共享内存标志向解耦核心发出信号。
- 在接收到来自端点缓冲核心的信号后,解耦核心将下一个数据包从FIFO传递给端点缓冲核心。它还向XUD库发出信号,表示端点缓冲核心可以发送数据包。
- 当端点缓冲核心发送完此缓冲区后,它向解耦核心发出信号,表示缓冲区已发送,并且解耦核心移动FIFO的读指针 。
对于从主机到设备的数据:
- 解耦核心将指向数据FIFO的端点缓冲核心的指针传递给XUD库,表示端点缓冲核心准备好接收数据。
- 端点缓冲核心然后从USB数据包中读取一个数据到FIFO,并向解耦核心发出信号,表示数据包已读取。
- 在接收到此信号后,解耦核心更新FIFO的写指针,并为端点缓冲核心提供一个新的指针以填充。
- 在音频中心的请求下,解耦核心通过从FIFO中读取样本向音频中心发送样本。
解耦器/音频核心交互
为了满足音频系统(即音频中心/混音器)的时序要求,解耦核心必须响应音频系统发送/接收样本的请求。在解耦核心中设置了一个中断处理程序来实现这一点,该中断处理程序实现在函数 handle_audio_request 中。
音频系统通过通道向解耦核心发送一个字,以请求使用内置的 outuint() 函数进行样本传输。在通道中接收到该字后,将触发 handle_audio_request 中断。
中断处理程序的第一个操作是发送一个字以确认请求。如果有样本频率的更改,则会发送控制令牌,音频系统使用 testct() 检查该令牌。
现在可以进行样本传输。首先,解耦核心从主机向设备发送样本,然后音频子系统传输发送到主机的样本。这些传输始终以通道数大小的块进行(即 NUM_USB_CHAN_OUT 和 NUM_USB_CHAN_IN)。例如,如果设备具有10个输出通道和8个输入通道,则每次中断从解耦核心发送10个样本并接收8个样本。
完整的通信方案如下表所示(非样本频率更改情况):
| 解耦 核心 | 音频系统 | 备注 |
|---|---|---|
| outuint() | 音频系统请求样本交换 | |
| inuint() | 触发中断并执行 inuint() | |
| outuint() | 解耦核心发送确认(ACK) | |
| testct() | 检查CT以指示样本频率更改 | |
| inuint() | 字指示ACK输入(无样本频率更改) | |
| inuint() | outuint() | 样本传输(设备到主机) |
| inuint() | outuint() | |
| inuint() | outuint() | |
| ... | ||
| outuint() | inuint() | 样本传输(主机到设备) |
| outuint() | inuint() | |
| outuint() | inuint() | |
| outuint() | inuint() | |
| ... |
向解耦核心和音频系统发送的请求和确认是“输出欠流”样本值。在PCM模式下,它将为0;在DSD模式下,它将为DSD静音。这允许缓冲系统输出适当的欠流值,而无需了解流的格式(在DSD over PCM(DoP)情况下尤其有优势)。
异步反馈
在异步模式 下,设备使用反馈端点按照USB 2.0规范报告音频输出/输入到/从外部音频接口/设备的速率。计算得出的反馈值也用于确定发送给主机的数据包大小。
这种异步时钟方案意味着设备是时钟主控,可以使用高质量的本地主时钟或数字输入流作为时钟源。
在每次接收到USB帧起始(SOF)令牌后,缓冲核心从一个由主时钟同步的端口获取时间戳。通过减去上一个SOF时刻的时间戳,计算出自上一个SOF以来的主时钟周期数。根据此计算,可以确定两个SOF之间的样本数量(作为定点数)。这个计数在128个SOF中累积,并用作反馈值的基础。
通过显式的反馈IN端点,反馈数据也由端点缓冲核心处理并发送给主机。
如果同时启用输入和输出,则可以根据发送给主机的音频流隐式生成反馈。然而,在实践中,通常会使用显式的反馈端点,这是由于Microsoft Windows操作系统的限制(参见UAC_FORCE_FEEDBACK_EP)。
USB速率控制
设备必须按照所选采样频率的正确速率从USB主机消耗数据并向USB主机提供数据。在异步模式下,根据USB 2.0规范,USB数据包的最大变化范围可以是每个USB帧+/- 1个样本。在同步模式下,除了满足不能整除USB SOF周期的采样率所需的变化外(例如44.1kHz),没有其他变化。
高速USB帧以8kHz发送,因此对于48kHz,每个数据包包含每个声道六个样本。
在异步模式下,音频时钟可能会漂移,比主机运行得更快或更慢。因此,如果音频时钟稍微快了,设备偶尔可能输入/输出七个样本而不是六个。或者,它可能稍微慢,并且输入/输出五个样本而不是六个。图31显示了在异步模式下每个示例音频频率的数据包中允许的样本数。
在同步模式下,音频时钟与USB主机的SOF时钟同步。因此,在48kHz下,设备始终从主机接收六个样本,并始终向主机发送相同数量的样本。
请参阅《USB设备音频数据格式定义v2.0》第2.3.1.1节,获取完整的详细信息。
| 频率 (kHz) | 最小数据包 | 最大数据包 |
|---|---|---|
| 44.1 | 5 | 6 |
| 48 | 5 | 7 |
| 88.2 | 10 | 11 |
| 96 | 11 | 13 |
| 176.4 | 20 | 21 |
| 192 | 23 | 25 |
为了实现这一控制,Decoupler核心使用在EP Buffering核心中计算得到的反馈值。该值用于确定将插入音频FIFO的下一个数据包的大小。
在同步模式下,使用相同的系统,但反馈值仅使用固定值,而不是从主时钟端口派生的值。
XMOS USB设备(XUD)库
所有与USB主机的低级通信都由XMOS USB设备(XUD)库 - lib_xud 处理。
XUD_Main() 函数运行在自己的核心中,并通过共享内存和通道通信与端点核心进 行通信。
有关更多详细信息和完整的XUD API文档,请参考 lib_xud。
图表 5 显示了XUD库与其他两个核心的通信:
- Endpoint 0:该核心控制USB设备的枚举/配置任务。
- Endpoint Buffer:该核心从XUD库发送/接收数据包。该核心从AudioHub接收音频数据,从MIDI核心接收MIDI数据等。
恢复外部时钟(时钟发生器)
为了提供音频主时钟,应用程序可以使用可选择的振荡器、时钟生成IC,或者在使用xCORE.ai设备的情况下,集成的APP PLL可以生成固定的主时钟频率。
它还可以使用外部PLL/时钟倍频器,根据xCORE的参考信号生成主时钟。使用外部PLL/时钟倍频器允许异步模式设计锁定到来自数字流(例如S/PDIF或ADAT输入)的外部时钟源。代码库支持Cirrus Logic CS2100设备来实现此目的。其他设备可以通过代码修改来支持。
预计在将来的版本中,xCORE.ai设备中的辅助PLL,结合相关软件更改,将能够替代大多数设计中的CS2100部分。
时钟发生器核心(Clock Gen)负责生成供CS2100设备使用的参考频率,进而生成整个设计中使用的主时钟。该核心还作为ADAT和S/PDIF接收核心与音频中心核心之间的较小缓冲区。
当以内部时钟模式运行时,该核心仅使用本地定时器根据XMOS参考时钟生成此时钟。
当以外部时钟模式运行时(即“S/PDIF Clock”或“ADAT Clock”模式),从S/PDIF和/或ADAT接收核心接收样本。通过计算给定时间段内的样本数来计算外部频率。然后,基于接收到这些样本,生成供CS2100使用的参考时钟。
如果外部流变得无效,内部时钟定时器事件将触发,以确保即使出现电缆拔插等情况,仍能继续生成有效的主时钟。努力保证这些时钟之间的切换相对平滑。此外,还努力尽可能降低参考时钟的抖动,无论Clock Gen核心的活动水平如何。这是通过使用端口时间表来调度引脚切换而不是直接输出到端口来实现的。
Clock Gen核心通过c_clk_ctl通道从Endpoint 0接收时钟选择Get/Set命令。该核心还记录外部时钟的有效性,也可以通过相同通道从Endpoint 0查询。请注意,始终将报告当前设备采样率,而不管所查询的时钟是什么。这样可以改善大多数驱动程序/操作系统组合的用户体验。
为了通知主机任何状态更改,Clock Gen核心还可以导致Decouple核心在时钟有效性更改时请求中断数据包。该功能基于音频类2.0状态/中断端点特性。
在同步模式下,目前不支持外部数字输入流。这样的功能需要进行采样率转换,将其从S/PDIF或ADAT时钟域转换为USB主机时钟域。因此,在同步模式设备中不使用该核心。
数字混音器
混音器核心从Decouple核心接收输出音频,从Audio Hub核心接收输入音频。它对每个通道应用音量,并将传入的音频传递给Decouple核心,传出的音频传递给Audio Hub。音量更新使用内置的32位到64位有符号乘累加函数(macs)实现。混音器在mixer.xc文件中实现。
混音器会占用(最多)2个核心,并且可以在高达96kHz的采样率下执行8个混音,以及在更高采样率下执行2个混音,每个混音最多有18个输入。当以较高采样率运行时,组件会自动恢复为生成2个混音。
混音器可以从以下任一输入获取输入:
- 主机的USB输出 - 这些样本来自Decouple核心。
- 设备上的音频接口输入 - 这些样本来自Audio Hub核心,并包括来自数字输入流的样本。
由于这些输入的总和可能超过每个混音器的18个可能混音输入,因此存在从所有可能输入到混音器输入的映射。
混音发生后,创建最终输出。每个混音都有两个可能的输出目标。
- 发送到主机的USB输入 - 这些样本被发送到Decouple核心。
- 发送到设备上的音频接口输出 - 这些样本被发送到Audio Hub核心。
对于设备的每个可能输出,都存在一个映射来通知混音器其来源。可能的来源包括来自USB主机的输出、来自Audio Hub核心的输入或来自混音的输出。
基本上,混音器/路由器可以配置为任何设备输入都可以用作任何混音的输入,或直接路由到任何设备输出。此外,任何设备输出都可以源自任何混音输出或任何设备输入。
如音频请求:音量控制所述,混音器还可以处理处理或音量控制。如果将混音器配置为处理音量,但将混音数设置为零(以便核心仅执行音量设置),则组件将仅使用1个核心。这对于通道数量较多的设备而言有时是一种有用的配置。
USB混音器控制
混音器可以通过USB控制请求从主机接收控制命令,发送到Endpoint 0。Endpoint 0核心通过通道(c_mix_ctl)将这些命令传递给混音器核心。这些命令在图表32中描述。
| 命令 | 描述 |
|---|---|
| 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音频输出的音量乘法器。 |
主机控制
可以通过向Endpoint 0发送请求来从主机PC控制混音器。XMOS提供了一个基于命令行的简单示例应用程序,演示了如何控制混音器。这旨在作为向自己的控制应用程序添加混音器控制的示例。它不打算暴露给最终用户。
有关详细信息,请参阅host_usb_mixer_control目录中的README文件。也可以使用以下命令查看参数列表:
$ ./xmos_mixer --help
此控制实用程序的主要要求是:
- 设置输入通道到混音器的映射
- 为每个混音器输出设置系数和输入
- 设置物理输出的映射,可以直接来自输入或通过混音器
在这个配置空间内存在多种产生所需结果的灵活性。产品开发者可能只想向最终用户公开部分功能。
在使用XMOS主机控制示例应用程序时,考虑将混音器设置为从模拟输入1和2回路传送到模拟输出1和2的示例。
下面所示命令为一个示例;实际输出将取决于混音器的配置。
下面将显示每个设备输出的索引以及当前映射到它的通道。在这个例子中,模拟输出1和2对应的目录分别是0和1:
$ ./xmos_mixer --display-aud-channel-map
Audio Output Channel Map
------------------------
0 (DEVICE OUT - Analogue 1) source is 0 (DAW OUT - Analogue 1)
1 (DEVICE OUT - Analogue 2) source is 1 (DAW OUT - Analogue 2)
2 (DEVICE OUT - SPDIF 1) source is 2 (DAW OUT - SPDIF 1)
3 (DEVICE OUT - SPDIF 2) source is 3 (DAW OUT - SPDIF 2)
可以使用以下命令查看DAW输出映射:
$ ./xmos_mixer --display-daw-channel-map
DAW Output To Host Channel Map
------------------------
0 (DEVICE IN - Analogue 1) source is 4 (DEVICE IN - Analogue 1)
1 (DEVICE IN - Analogue 2) source is 5 (DEVICE IN - Analogue 2)
在这两种情况下,默认情况下,它们绕过混音器。
以下命令将列出可以从音频输出通道映射中映射到设备输出的通道。请注意,在此示例中,模拟输入1和2分别是源4和5,Mix 1和2是源6和7:
$ ./xmos_mixer --display-aud-channel-map-sources
Audio Output Channel Map Source List
------------------------------------
0 (DAW OUT - Analogue 1)
1 (DAW OUT - Analogue 2)
2 (DAW OUT - SPDIF 1)
3 (DAW OUT - SPDIF 2)
4 (DEVICE IN - Analogue 1)
5 (DEVICE IN - Analogue 2)
6 (MIX - Mix 1)
7 (MIX - Mix 2)
使用前面命令中的索引,我们现在将重新映射前两个混音器通道(Mix 1和Mix 2)到设备输出1和2:
$ ./xmos_mixer --set-aud-channel-map 0 6
$ ./xmos_mixer --set-aud-channel-map 1 7
您可以通过重新检查映射来确认此效果:
$ ./xmos_mixer --display-aud-channel-map
Audio Output Channel Map
------------------------
0 (DEVICE OUT - Analogue 1) source is 6 (MIX - Mix 1)
1 (DEVICE OUT - Analogue 2) source is 7 (MIX - Mix 2)
2 (DEVICE OUT - SPDIF 1) source is 2 (DAW OUT - SPDIF 1)
3 (DEVICE OUT - SPDIF 2) source is 3 (DAW OUT - SPDIF 2)
现在,模拟输出1和2的源已更改为混音器(MIX - Mix 1和MIX - Mix 2),而不是直接从USB获取。然而,由于默认情况下混音器被映射为将USB通道直接传递到输出,因此功能上没有变化。
USB音频参考设计只有一个单元,因此mixer_id参数应始终为0。
需要单独设置混音器节点。可以使用以下命令显示mixer_id 0中的节点:
$ ./xmos_mixer --display-mixer-nodes 0
Mixer Values (0)
----------------
Mixer outputs
1 2
DAW - Analogue 1 0:[0000.000] 1:[-inf]
DAW - Analogue 2 2:[-inf] 3:[0000.000]
DAW - SPDIF 1 4:[-inf] 5:[-inf]
DAW - SPDIF 2 6:[-inf] 7:[-inf]
AUD - Analogue 1 8:[-inf] 9:[-inf]
AUD - Analogue 2 10:[-inf] 11:[-inf]
将混音器输出1和2映射到设备输出模拟1和2后,要将模拟输入的音频传递到设备输出,需要将mixer_id 0节点8和节点11设置为0dB:
$ ./xmos_mixer --set-value 0 8 0
$ ./xmos_mixer --set-value 0 11 0
同时,可以将原始混音器输出静音:
$ ./xmos_mixer --set-value 0 0 -inf
$ ./xmos_mixer --set-value 0 3 -inf
现在,模拟输入1和2上的音频应分别可以听到在输出1和2上。如上所述,混音器的灵活性使得有多种方法可以创建特定的混音。创建相同路由的另一种选择是更改混音器的源,使混音器输出1和2来自模拟输入1和2。
为了演示这一点,首先撤销上面的更改(或者简单地重置设备):
$ ./xmos_mixer --set-value 0 8 -inf
$ ./xmos_mixer --set-value 0 11 -inf
$ ./xmos_mixer --set-value 0 0 0
$ ./xmos_mixer --set-value 0 3 0
现在,混音器应恢复默认值。可以使用“Audio Output Channel Map Source List”中的索引来更改混音器0输出1和2的源:
$ ./xmos_mixer --set-mixer-source 0 0 4
Set mixer (0) input 0 to device input 4 (AUD - Analogue 1)
$ ./xmos_mixer --set-mixer-source 0 1 5
Set mixer (0) input 1 to device input 5 (AUD - Analogue 2)
如果重新运行以下命令,那么第一列现在显示为“AUD - Analogue 1和2”,而不是“DAW(数字音频工作站,即主机)- Analogue 1和2”,确认了新的映射。再次通过模拟输入1/2播放音频,可以听到它们被回路传送到模拟输出1/2:
$ ./xmos_mixer --display-mixer-nodes 0
S/PDIF传输
通过使用lib_spdif,lib_xua支持开发具有S/PDIF传输功能的设备。XMOS S/PDIF发射器组件在单个核心中运行,并支持高达192kHz的采样率。
S/PDIF发射器核心通过通道接收PCM音频样本,并将其以S/PDIF格式输出到端口。通过查表将音频数据编码为所需的格式。
它每次从音频I/O核心接收两个样本(左声道和右声道)。对于每个样本,它对每个字节进行查找,生成16位的编码数据,并将其输出到端口。
S/PDIF以帧的形式发送数据,每个帧包含左声道和右声道的192个样本。音频样本被封装成S/PDIF字(添加前导码、奇偶校验、通道状态和有效性位),并以双相标记编码(BMC)的方式传输,与外部主时钟同步。
请注意,对SpdifTransmitPortConfig函数进行微小更改可以启用内部主时钟生成(例如,当时钟源已锁定到所需的音频时钟时)。
- 采样率:44.1kHz、48kHz、88.2kHz、96kHz、176.4kHz、192kHz
- 主时钟倍率:128x、256x、512x
- 库:lib_spdif
列表 33:S/PDIF兼容性
时钟
图 34: D-Type 抖动抑制
S/PDIF信号的输出速率由外部主时钟决定。主时钟必须是BMC位速率的1倍、2倍或4倍(即音频采样率的128倍、256倍或512倍)。例如,对于192kHz,最小主时钟频率为24.576MHz。
这将使主时钟重新采样到其时钟域(晶振),从而在S/PDIF信号上引入2.5-5 ns的抖动。典型的抖动减少方案是使用从主时钟触发的外部D型触发器(如上图所示)。
使用方法
与S/PDIF发射器核心的接口是通过一个带有流式内置函数(outuint、inuint)的普通通道。数据格式应为左对齐的24位数据在32位字中:0x12345600
在通道上使用以下协议:
- outct:新采样率命令
- outuint:采样频率(Hz)
- outuint:主时钟频率(Hz)
- outuint:左声道样本
- outuint:右声道样本
- outuint:左声道样本
- outuint:右声道样本 ...
图35: S/PDIF 组件的协议
这些通信被封装在lib_spdif提供的API函数中。
输出流结构
流由具有以下结构的字组成,如图36所示。通道状态位为0x0nc07A4,其中c=1表示左声道,c=2表示右声道,n表示采样频率,如图37所示。
- Bits 0-3: 前导码,正确的B M W顺序,从样本0开始
- Bits 4-27:音频样本,给定字的高24位
- Bits 28:有效性位,始终为0
- Bits 29:子代码数据(用户位),未使用,设置为0
- Bits 30:通道状态,参见下图
- Bits 31:奇偶校验,确保位4-30的正确奇偶校验
图36: S/PDIF 流结构
| 频率(kHz) | n |
|---|---|
| 44.1 | 0x0 |
| 48 | 0x2 |
| 88.2 | 0x8 |
| 96 | 0xA |
| 176.4 | 0xC |
| 192 | 0xE |
图37: 表示通道状态的Bits位
S/PDIF 接收
XMOS设备可以支持高达192kHz的S/PDIF接收 - 有关完整规格,请参阅lib_spdif。
S/PDIF接收模块使用一个时钟块和一个用于缓冲的一位端口。时钟块是由100 MHz的参考时钟分频得到的。一位端口被缓冲为4位。接收代码使用该时钟对输入数据进行过采样。
接收器通过流式通道输出音频样本,可以使用内置的输入操作符输入数据。lib_spdif还提供了封装此通信的API函数。
S/PDIF接收函数永远没有返回值。通道输入的32位值包括:
| 位 | 注释 |
|---|---|
| 0:3 | 标签(见下文) |
| 4:28 | PCM编码的样本值 |
| 29:31 | 用户位(奇偶校验等) |
图38: S/PDIF RX 的字结构
标签有三个可能的值:
| 标签 | 含义 |
|---|---|
| FRAME_X | 通道0上的样本(立体声的左声道) |
| FRAME_Y | 另一个通道上的样本(如果是立体声,则为右声道) |
| FRAME_Z | 通道0上的样本,并且是帧的第一个样本(如果需要重构用户位,则可使用) |
图39: S/PDIF RX 标签
有关格式、用户位等的详细信息,请参阅 S/PDIF, IEC 60958-3:2006 规范。
使用与集成
由于S/PDIF是数字流,设备的主时钟必须与之同步。这通常通过外部设备完成。请参阅恢复外部时钟。
由于需要时钟恢复,S/PDIF接收只能在异步模式下使用。
S/PDIF接收函数与时钟生成核心进行通信,后者将音频数据传递给音频中心核心。时钟生成核心还处理与S/PDIF时钟源的锁定(见§8.5)。
理想情况下,应检查每个接收到的字/样本的奇偶校验。可以使用内置的crc32函数来实现(见xs1.h):
/* 返回1表示奇偶校验错误,否则返回0 */
static inline int badParity(unsigned x)
{
unsigned X = (x >> 4);
crc32(X, 0, 1);
return X & 1;
}
如果检测到奇偶校验错误,则忽略该字/样本;否则,检查标签以确定通道(即左声道或右声道),并存储样本。
下面的代码片段演示了如何使用S/PDIF接收组件的输出:
while (1)
{
c_spdif_rx :> data;
if (badParity(data))
continue;
tag = data & 0xF;
/* 提取24位音频样本 */
sample = (data << 4) & 0xFFFFFF00;
switch (tag)
{
case FRAME_X:
case FRAME_Y:
// 存储左声道
break;
case FRAME_Z:
// 存储右声道
break;
}
}
时钟生成核心在将样本传递给音频中心核心之前,将样本存储在一个小的FIFO中。
ADAT 接收
ADAT接收组件以44.1kHz或48kHz的采样率接收多达8个音频通道。调用接收器函数的API在XM-005512-PC中有描述。
该组件输出32位的单词,分为九个字帧。这些帧按照以下方式排列:
- 控制字节
- 通道0样本
- 通道1样本
- 通道2样本
- 通道3样本
- 通道4样本
- 通道5样本
- 通道6样本
- 通道7样本
下面是一个读取ADAT组件输出的示例:
control = inuint(oChan);
for (int i = 0; i < 8; i++)
{
sample[i] = inuint(oChan);
}
样本是24位值,包含在单词的低24位中。
控制字由 位[11..8] 中的4个控制位和 位[7..0] 中的值0b00000001组成。该控制字在更高级别上启用同步,即在通道上始终先读取一个奇数字,然后是8个数据字。
使用和集成
由于ADAT是数字流,设备的主时钟必须与之同步。ADAT接收的集成与S/PDIF接收非常相似,即ADAT接收函数与时钟发生器核心进行通信。然后,时钟发生器核心将音频数据传递给音频中心核心。它还处理与ADAT时钟源的锁定。
与S/PDIF集成相比,ADAT集成有一些小的差异,这是因为ADAT通常具有8个通道,而S/PDIF只有两个通道。
时钟发生器核心还处理SMUX II(例如96kHz的4个通道)和SMUX IV(例如192kHz的2个通道),并根据需要填充样本FIFO。SMUX模式通过c_clk_ctl通道从端点0向时钟发生器核心传递。使用适当的通道数,SMUX模式通过备选接口暴露给USB主机,用于流式输入端点。
MIDI
MIDI核心实现了一个31250波特率的UART,用于输入和输出。当从端点缓冲核心接收到32位的USB MIDI事件时,它会解析这些事件并将其转换为8位的MIDI消息,然后通 过UART发送出去。类似地,传入的8位MIDI消息会被聚合成32位的USB MIDI事件,并传递给端点缓冲核心。MIDI核心的实现在usb_midi.xc文件中。
端点缓冲核心实现了两个批量端点(一个输入端点和一个输出端点),并与每个端点的小型共享内存FIFO进行交互。
PDM麦克风
XMOS USB音频参考设计固件可以与PDM麦克风集成。来自麦克风的PDM流将被转换为PCM并通过USB输出到主机。
使用XMOS麦克风阵列库(lib_mic_array)进行PDM麦克风的接口。lib_mic_array旨在允许与PDM麦克风进行接口,并进行高效的抽取以选择用户可选的输出采样率。
lib_mic_array库仅适用于xCORE-200与xCORE.AI系列设备。
该库使用以下组件:
- PDM接口
- 四通道抽取滤波器
每个高通道计数的PDM接口(mic_array_pdm_rx())最多可以连接16个PDM麦克风。每个处理任务(mic_array_decimate_to_pcm_4ch())可以处理最多四个通道。对于1-4个通道,库需要2个逻辑核心:

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

最左边的任务mic_array_pdm_rx()对最多8个麦克风进行采样,并对数据进行滤波处理,提供最多八个384kHz的数据流,分成两个四通道的数据流。处理线程将 信号抽取到用户选择的采样率(48kHz、24kHz、16kHz、12kHz或8kHz之一)。
通过增加专用于PDM任务的核心数量,可以支持更多的通道。然而,当前PDM麦克风集成到lib_xua中的通道数限制为8个。
在将信号抽取到输出采样率后,还会进行其他各种步骤,例如消除直流偏移、增益校正和补偿等。有关更多实现细节和完整功能集,请参阅随lib_mic_array提供的文档。
PDM麦克风硬件特性
PDM麦克风需要一个时钟输入,并在数据输出上提供PDM信号。所有的PDM麦克风必须共享同一个时钟信号(在PCB上适当地进行缓冲),并输出到连接到单个8位端口的八根数据线上:
| 信号 | 描述 |
|---|---|
| CLOCK | 时钟线,PDM麦克风用于驱动数据输出的时钟。 |
| DQ_PDM | 来自PDM麦克风的数据,通过一个8位端口传输。 |
传递给lib_mic_array的唯一端口是8位数据端口。该库假设输入端口使用PDM时钟进行时钟同步,并不需要了解PDM时钟源的信息。
麦克风的输入时钟可以以多种方式生成。例如,可以在电路板上生成3.072MHz的时钟,或者xCORE可以将12.288MHz的主时钟分频。或者,如果时钟精度不重要,可以将内部的100MHz参考时钟分频以提供近似的时钟信号。
使用和集成
从main()函数调用一个PDM麦克风包装器 ,并将一个通道参数连接到系统的其他部分:
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);
}