3、ALSA核心概念:声卡(Card)、设备(Device)、子设备(Subdevice)、PCM流的概念

好,咱们今天来聊聊ALSA里最基础、也最绕不开的几个概念。说实话,我刚入行那会儿,对着ALSA的文档看了三天,脑子里还是一团浆糊。什么Card、Device、Subdevice,还有PCM流,感觉像俄罗斯套娃一样一层套一层。后来在项目里调了两年驱动,才慢慢摸清楚它们到底是怎么回事。

嗯,咱们今天就把它彻底讲明白。你想想看,搞音频驱动,本质上就是在跟硬件打交道。而ALSA这套框架,就是帮我们把复杂的硬件抽象成几个简单的层次。说白了,就是给硬件“建档立卡”。

3.1 声卡(Card)—— 物理存在的“大管家”

声卡,就是Card,代表一个物理的或者逻辑的音频硬件实体。在车载Linux系统里,一个Card通常对应一颗Codec芯片,或者一个SoC内部的音频控制器。

我个人习惯把Card想象成一个“大管家”。它管理着所有跟这块硬件相关的资源。比如,你的车机上有一颗TI的TAS6424功放芯片,那它就是一个Card。如果还有一颗NXP的音频DSP,那可能就是另一个Card。

在系统里,每个Card都有一个编号,从0开始。你可以通过 /proc/asound/cards 看到它们:

cat /proc/asound/cards
 0 [tegrahda       ]: HDA-Intel - HDA Tegra
                      HDA Tegra at 0x3518000 irq 67
 1 [tegraxbar      ]: XBAR - Tegra XBAR
                      Tegra XBAR

你看,这里有两个Card。0号是HDA接口的,1号是Tegra内部的XBAR。每个Card都是独立的,它们之间互不干扰。

我的经验: 在车载项目中,经常遇到多个Card的情况。比如一个Card负责蓝牙电话,另一个Card负责导航提示。我曾经在一个项目里,因为两个Card的PCM时钟不同步,导致切换音源时出现“啪”的一声爆音。后来我强制让两个Card共享同一个MCLK(主时钟),才解决了这个问题。

3.2 设备(Device)—— 功能单元的“部门经理”

一个Card下面,可以有多个Device。Device代表的是这个声卡上的不同功能单元。比如,一个Codec芯片,它可能同时支持播放(Playback)和录音(Capture),那它至少就有两个Device。

你想想看,一个Card是“大管家”,那Device就是“部门经理”。有的部门管输出,有的部门管输入,有的部门管混音。

在ALSA里,Device也是从0开始编号的。比如:

  • Device 0:通常用于主音频播放(比如音乐、导航)
  • Device 1:可能用于语音通话(比如蓝牙电话)
  • Device 2:可能用于录音(比如麦克风阵列)

你可以用 aplay -l 来查看:

aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: tegrahda [HDA Tegra], device 0: HDA Tegra HDMI/DP 0 [HDMI/DP 0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 0: tegrahda [HDA Tegra], device 3: HDA Tegra HDMI/DP 1 [HDMI/DP 1]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: tegraxbar [Tegra XBAR], device 0: Tegra XBAR PCM [Tegra XBAR PCM]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

看到了吗?Card 0下面有device 0和device 3,分别对应不同的HDMI输出口。Card 1下面只有device 0。

注意: Device的编号不一定连续。比如上面例子中,device 0和device 3之间跳过了1和2。这是因为硬件设计上,有些通道被预留给了其他功能(比如I2S输入)。千万别想当然地认为device 0、1、2、3一定都存在。

3.3 子设备(Subdevice)—— 通道的“工位”

Subdevice,子设备,这个概念最容易被忽略。它代表的是同一个Device下的多个独立通道。说白了,就是同一个“部门经理”手下,有几个“工位”,每个工位可以独立干活。

举个例子,一个支持8通道输出的Codec,它可能只有一个Device(device 0),但下面有8个Subdevice。每个Subdevice对应一个物理通道(比如左前、右前、左后、右后...)。

在ALSA里,Subdevice的表示方式是 hw:X,Y,Z,其中X是Card号,Y是Device号,Z是Subdevice号。比如 hw:0,0,0 表示Card 0、Device 0、Subdevice 0。

我记得有一次,我在调试一个车载功放,发现左右声道声音不一样大。查了半天,原来是应用层打开的是 hw:1,0,0,但硬件上左声道是Subdevice 0,右声道是Subdevice 1。应用层只打开了Subdevice 0,所以只有左声道有声音。嗯,这种坑,你不看Subdevice配置根本发现不了。

核心要点: Subdevice的数量由驱动决定。大多数消费级声卡只有一个Subdevice(即Subdevice #0)。但在专业音频设备或车载多通道系统中,Subdevice的数量可能很多。驱动中通过 snd_pcm_substream 来管理每个Subdevice。

3.4 PCM流 —— 数据的“高速公路”

最后,咱们聊聊PCM流。PCM流,说白了就是音频数据流动的通道。它分为两种方向:

  • Playback(播放):数据从内存流向硬件(比如播放音乐)
  • Capture(捕获):数据从硬件流向内存(比如录音)

每个Device下,可以有多个PCM流。比如一个Device同时支持播放和录音,那它就有两个PCM流。在ALSA的术语里,每个PCM流对应一个 snd_pcm_substream 对象。

你想想看,PCM流就像一条高速公路。数据在高速公路上跑,需要遵守交通规则:采样率、位深、通道数。这些参数就是高速公路的“限速”和“车道数”。

在驱动开发中,我们最常打交道的就是PCM流的配置。比如:

// 设置PCM流的硬件参数
snd_pcm_hw_params_t *params;
snd_pcm_hw_params_any(handle, params);
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(handle, params, 2);
snd_pcm_hw_params_set_rate(handle, params, 48000);

这段代码,就是告诉PCM流:“嘿,你要用16位小端格式、双通道、48kHz采样率来跑数据。”

避坑指南: 我曾经在一个项目中,因为PCM流的缓冲区设置太小,导致在高负载时出现Xrun(欠载或过载)。现象就是声音断断续续,像卡带一样。后来我把缓冲区大小从1024帧改成了4096帧,问题就解决了。记住,缓冲区大小要根据系统负载和音频延迟要求来权衡。

3.5 它们之间的关系 —— 一张图说清楚

好了,咱们把四个概念串起来。你可以这样理解:

层级 类比 说明
Card 公司 一个物理音频硬件,比如一颗Codec芯片
Device 部门 一个功能单元,比如播放、录音、混音
Subdevice 工位 一个独立的通道,比如左声道、右声道
PCM流 高速公路 数据流动的通道,有方向(播放/捕获)

在驱动代码里,它们的创建顺序通常是:先注册Card,然后在Card上添加Device,每个Device下再创建Subdevice,最后通过PCM流来传输数据。

嗯,这里要注意:并不是所有Device都有Subdevice。有些简单的Device只有一个Subdevice(即Subdevice #0),那它其实就退化成了一对一的映射关系。但理解了这个层次结构,你以后看任何ALSA驱动的代码,都不会再晕了。

总结一下: Card是硬件实体,Device是功能单元,Subdevice是独立通道,PCM流是数据通道。这四个概念构成了ALSA驱动的基本骨架。搞懂了它们,你就掌握了ALSA的“世界观”。

好了,这一章就到这里。下一章咱们会深入PCM流的内部机制,讲讲DMA、环形缓冲区、以及Xrun的处理。到时候我会分享一个我在车载项目中遇到的“声音延迟”的案例,挺有意思的。