经典的音频通路如下图

DMA把音频数据写到I2S控制器的TX FIFO中,通过I2S总线传送给外设Codec,Codec将接收到的数据做音效处理(增益等)后DAC为模拟信号,给到连接的Speaker或者Headset。
1. ASOC要素
DAI(Digital Audio Interface)是ASoC中对数字音频总线接口的统一抽象层,I2S是其中最常用的协议之一。DAI同样封装了TDM、PCM、SPDIF、DMIC、LVDS等接口,本文中DAI是I2S。
1.1 无DSP
声音通过I2S总线从SOC芯片传送到Codec芯片,使用dai link来连接SOC侧和Codec侧的DAI,它包含cpu_dai name, codec_dai name, codec name, platform name。
- cpu_dai_name:即I2S控制器驱动中注册的DAI; platform component driver。
- codec_dai_name:Codec驱动注册的DAI,如wm8960-hifi。
- codec_name:对应Codec驱动组件名(用于匹配snd_soc_codec_driver)。
- platform_name:对应Platform组件,职责抽象为DMA通道管理、PCM Runtime缓冲区分配、硬件参数配置。

在没有 Audio DSP 的传统场景下,音频流与物理硬件通路是强绑定的。 系统运行时,一个物理 PCM 流唯一对应一个 runtime 实例。当上层应用调用 open / prepare / hw_params / trigger 时,ASoC 框架通过该 runtime 回溯到 snd_soc_pcm_runtime (rtd) 结构体,进而找到该 PCM 流绑定的固定的 dai_link,最终遍历并调用该通路上的 platform、cpu_dai、codec 等组件的驱动函数。
这种硬编码的绑定导致路由切换极不灵活。例如在播放音乐时插入耳机,系统必须先关闭当前的 PCM 流,再重新打开并配置指向耳机的全新 PCM 流。由于必须经历"销毁流再重建流"的过程,音频输出容易出现爆音或露音,无法做到无缝切换。
1.2 有DSP
在手机、电视、车机等现代产品中,音频场景极其复杂,CPU 与 Codec 之间通常会加入 DSP 进行音效处理和动态音频路由。为了适配这种带 DSP 的动态路由需求,ASoC 引入了 Dynamic PCM (DPCM) 机制。
如下图所示,DPCM 的核心思想是将原本一条完整的硬件音频通路拆分为"前端(FE)"和"后端(BE)"两段独立的 dai_link,分别描述不同物理阶段的连接:
-
FE (Front End) DAI Link:面向应用层,描述 CPU 与 DSP 的连接
- cpu_dai_name:指向 CPU 的 I2S/PCM 接口。
- platform_name:指向负责 DMA 传输的平台驱动。
- codec_name:通常指向 "snd-soc-dummy",因为此阶段不直接连接真实 Codec。
- 核心配置:将 dynamic 字段置 1。ASoC 框架只会为 FE 创建物理 PCM 设备(如 pcmCxDx),作为供应用层读写的统一接口。
-
BE (Back End) DAI Link:面向硬件层,描述 DSP 与 Codec 的连接
- cpu_dai_name:指向 DSP 的 DAI 接口(此时 DSP 扮演"CPU"角色来驱动音频总线)。
- platform_name:指向 DSP 驱动,由其管理面向 Codec 的 DMA 或数据搬运。
- codec_name:指向真实的硬件 Codec 芯片。
- 核心配置:将 no_pcm 字段置 1。内核不会为 BE 创建 PCM 设备文件,它仅作为后端硬件通路存在。

由于 FE 和 BE 在声明时是互不干涉的独立链路,系统运行时的动态串联需要依靠 DAPM(动态音频电源管理) 和 DPCM 核心 共同完成:
- 路由寻路(DAPM):当上层操作 FE 的 PCM 设备时,DAPM 从 FE 的 DAI widget 出发,沿着 DSP 内部的音频路由通路进行拓扑图遍历,最终寻找到当前处于激活状态的 BE DAI widget。
- 状态绑定(DPCM):内核通过 snd_soc_dpcm 结构体来管理这种动态寻找出来的 FE 与 BE 配对关系,将激活的 BE 动态绑定到当前的 FE 上。
- 控制级联(控制流):完成绑定后,上层对 FE 发起的 hw_params、trigger 等控制命令,会通过 DPCM 机制自动、级联地分发调用到 BE 侧的各个组件驱动(如真实 Codec 的驱动函数),从而实现了应用层透明的、无缝的动态音频切换。
2. DAI link
2.1 定义
在面对复杂音频系统中大量的音频链路时,一般会通过 Mechine静态数组定义属性 + DTS定义硬件的方式,如下:
1. Machine 驱动静态数组中的字段解析
c
static struct snd_soc_dai_link soc_dai[100] = {
{
.name = "DMA0",
.stream_name = "DMA0",
.dynamic = 1, // FE(前端)
.ignore_suspend = 1,
.trigger = {
SND_SOC_DPCM_TRIGGER_POST,
SND_SOC_DPCM_TRIGGER_PRE
},
.ops = &dma_ops,
.dpcm_playback = 1,
},
这一段主要定义了该链路在 ASoC 核心框架中的控制策略、流向和行为特性:
-
.name = "DMA0" 与 .stream_name = "DMA0"
- 含义:该 DAI Link 的识别名称和音频流名称。
- 作用:在内核日志(如 /sys/kernel/debug/asoc/)中作为唯一标识,同时用户态在通过 Alsa-lib 寻址时(如指定 hw:0,0),内核会将其映射到这个名字上。此外,stream name也是关联Dai widget和Dai link的纽带。
-
.dynamic = 1
- 含义:声明该链路是一个 DPCM 前端 (FE) 链路。
- 作用:告诉 ASoC 框架,"我不是直接连到物理 Codec 的,我是一个虚拟的、面向应用层的流"。框架看到这个标志后,会为它创建物理 PCM 设备文件(如 /dev/snd/pcmC0D0p),允许应用层对其进行 open/write。
-
.ignore_suspend = 1
- 含义:忽略休眠挂起。
- 作用:在系统进入低功耗深度睡眠(Suspend)时,如果该音频流仍在播放(例如语音唤醒监听、车机后台导航播报、手机锁屏音乐),系统不会强制关闭这条音频链路,允许 AUDIO DSP 继续运行。
-
.trigger = { SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_PRE }
- 含义:DPCM 控制流的级联触发顺序。
- 作用:当应用层下发 START(开始播放)或 STOP(停止)等 trigger 命令时,FE 与 BE 的执行先后顺序。
- 第一个参数(针对 Start):SND_SOC_DPCM_TRIGGER_POST 意味着先启动后端 BE(物理通路和 Codec),最后再启动前端 FE。防止产生爆音。
- 第二个参数(针对 Stop):SND_SOC_DPCM_TRIGGER_PRE 意味着先停止前端 FE(断开流输入),然后再停止后端 BE。同样是为了平滑过渡。
-
.ops = &dma_ops
- 含义:该控制链路的函数操作集(回调函数指针)。
- 作用:指向由平台/CPU 驱动实现的 struct snd_soc_ops 结构体。当上层调用 hw_params、prepare 时,会执行 rdma_ops 里面对应的 C 函数,用来配置 AUDIO DSP 内部 RDMA 的硬件寄存器(如 DMA 缓冲区大小、周期等)。
-
.dpcm_playback = 1
- 含义:声明该前端流支持音频播放(Playback/输出)。
- 作用:如果是录音流,则会配置为 .dpcm_capture = 1。这决定了内核创建的 PCM 设备是只读、只写还是双向的。
2. DTS 设备树中的字段解析
dts
dma0 {
cpu {
sound-dai = <&audio_dma_0 0>;};
platform {
sound-dai = <&audio_dma_0 0>;};
};
这一段负责描述该链路在硬件拓扑上的真实归属:DMA 是 audio DSP 内部负责从内存搬运音频数据到 DSP 核心的硬件模块。
- dma0 { ... }
- 含义:节点名称。Machine 驱动在 probe 时,会通过字符串匹配或索引,把这个 DTS 节点和 C 语言数组里的 exynos_dai0(即 .name = "DMA0")关联起来。
- cpu { sound-dai = <&audio_dma_0 0>; };
- 含义:指定该链路的 CPU DAI(数据发送源头)。
- 作用:&audio_dma_0 指向了 EXYNOS88XX 驱动的设备树节点,后面的 0 是索引(Index),代表使用的是 EXYNOS88XX 内部的第 0 号 RDMA 硬件通道。
- platform { sound-dai = <&audio_dma_0 0>; };
- 含义:指定该链路的 Platform 驱动(负责 DMA 内存管理的驱动)。
在 ASoC 传统架构中,CPU 驱动(管接口)和 Platform 驱动(管内存)是分开的。但在现代 SoC(如三星 EXYNOS88XX、高通 QDSP)中,这二者高度集成。这里让它们都指向 <&audio_dma_0 0>,意味着 第 0 号 RDMA 模块既充当数字音频接口(CPU DAI),又自己负责管理它自己的 DMA 内存搬运(Platform)。
2.2 Topology 动态定义 FE DAI Link
在一些 SoC 音频架构中,FE (Front-End) DAI Link 不仅可以静态定义在 Machine 驱动中(.dynamic = 1),还可以通过 ALSA Topology 固件动态创建。
yaml
┌────────────────────────────────────────────────────────┐
│ struct snd_soc_card │
└───────────────────────────┬────────────────────────────┘
│
┌──────────────────────┴──────────────────────┐
▼ ▼
【 路径 A:静态 DTS 骨架 】 【 路径 B:动态 Topology 注入 】
读取 Machine Driver + DTS 设备树 由音频 DSP 驱动加载并解析 alsa_tplg.bin
│ │
▼ ▼
静态创建物理 FE DAI Link 动态创建虚拟 FE DAI Link
(例如:对不需经DSP处理的通道,如按键音) (例如:对需要经过 DSP 深度处理的通道)
└─► PCM 0: Direct Hardware Playback └─► PCM 1: Deep Buffer Playback
└─► PCM 2: VoIP Voice Call Stream
└─► PCM 3: Smart Assistant Wakeup
以一段 Topology 配置文件为例:
bash
SectionPCMCapabilities."virtual_0 playback" {
formats "S16_LE, S24_LE, S24_3LE, S32_LE"
rates "KNOT"
rate_min "8000"
rate_max "384000"
channels_min "1"
channels_max "8"
periods_min "2"
periods_max "4096"
period_size_min "4"
period_size_max "262144"
buffer_size_max "524288"
}
SectionPCM."virtual_0" {
index "1"
id "100"
dai."virtual_0" {
id "100"
}
pcm."playback" {
capabilities "virtual_0 playback"
}
}
加载后,系统中会出现 pcmC0D100p 设备。这个 PCM 背后的 DAI Link 是如何建立的?
2.1.1 Topology 加载流程
vendor_tplg_probe() → 加载 vendor_tplg.bin(由 vendor_tplg.conf 编译生成) → snd_soc_tplg_component_load(component, &vendor_tplg_ops, firmware) → ASoC Topology Parser 逐段解析 bin 文件
当 Parser 遇到 SectionPCM 时,会回调 vendor_tplg_ops 中的 dai_load 和 link_load 两个回调函数,这是 DAI Link 动态创建的核心。
2.1.2 两个关键回调
- dai_load:创建空壳 DAI Driver
c
static int vendor_tplg_dai_load(struct snd_soc_component *c, int idx,
struct snd_soc_dai_driver *dai_drv,
struct snd_soc_tplg_pcm *pcm, struct snd_soc_dai *dai)
{
// 从 SectionPCMCapabilities 提取 hardware 参数
if (pcm->playback) {
caps = &pcm->caps[SNDRV_PCM_STREAM_PLAYBACK];
playback->formats = caps->formats;
playback->rates = caps->rates;
playback->rate_min = caps->rate_min;
playback->channels_min = caps->channels_min;
...
}
// 注册虚拟 DMA component,返回 platform device
dev_platform = vendor_vdma_register_component(dev, dai_drv->id,
dai_drv->name, &playback, &capture);
// 保存 dev_platform,供后续 link_load 使用
data->dev_platform = dev_platform;
list_add_tail(&data->list, &dai_list);
}
Parser 创建的 snd_soc_dai_driver 的 name 为 "virtual_0",id 为 100。这个 DAI driver 本身不提供任何 DAI ops,它是一个空壳,作用是:
markdown
- 命名 --- 为 DAI Link 提供 stream_name "virtual_0"
- 承载 hardware 能力 --- formats、rates、channels 等参数由 Parser 通过 SectionPCMCapabilities 注入
- link_load:绑定 Platform
c
static int vendor_tplg_link_load(struct snd_soc_component *c, int idx,
struct snd_soc_dai_link *link,
struct snd_soc_tplg_link_config *cfg)
{
// 在 dai_list 中找到 dai_load 阶段保存的数据
list_for_each_entry(dai, &c->dai_list, list) {
if (strcmp(dai->name, link->cpus->dai_name) == 0) {
data = dai->driver->dobj->private;
link->platforms->name = dev_name(data->dev_platform);
link->ignore_suspend = 1;
break;
}
}
}
这一步将 link->platforms->name 设为 "vdma.100",使 ASoC 在后续绑定 DAI Link 时能正确找到 Platform component。
2.1.3 FE DAI Link 的完整组成
bash
┌─────────────────┬─────────────────────────┬────────────────────────────┐
│ 字段 │ 值 │ 来源 │
├─────────────────┼─────────────────────────┼────────────────────────────┤
│ DAI Link name │ "virtual_0" │ SectionPCM 名称 │
├─────────────────┼─────────────────────────┼────────────────────────────┤
│ DAI Link id │ 100 │ id "100" │
├─────────────────┼─────────────────────────┼────────────────────────────┤
│ CPU DAI name │ "virtual_0" │ dai."virtual_0" │
├─────────────────┼─────────────────────────┼────────────────────────────┤
│ CPU DAI driver │ 空壳(Parser 自动创建) │ 挂在 topology component 下 │
├─────────────────┼─────────────────────────┼────────────────────────────┤
│ Platform name │ "vdma.100" │ dev_platform → dev_name() │
├─────────────────┼─────────────────────────┼────────────────────────────┤
│ Platform driver │ 虚拟 DMA component │ 注册在 vdma.100 设备上 │
└─────────────────┴─────────────────────────┴────────────────────────────┘
2.1.4 dev_platform 的作用
理解 dev_platform 是理解整个架构的关键。
系统中存在两组 Component:
sql
┌────────────────────┬───────────────────────────────────────────────────────────────────────┐
│ Component │ 职责 │
├────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ Topology Component │ 解析 topology 固件,注册 Widget / Kcontrol / DAI(空壳) │
├────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ 虚拟 DMA Component │ 实现所有 PCM 操作(open / close / hw_params / trigger / pointer / copy │
└────────────────────┴───────────────────────────────────────────────────────────────────────┘
虚拟 DMA component 注册时不带任何 DAI:devm_snd_soc_register_component(dev, &vendor_vdma_component, NULL, 0);
因此 DAI Link 的结构为:
bash
FE DAI Link
├── CPU DAI → 空壳,挂在 alsa topology component 下,只提供 name 和 id
└── Platform → vdma.{id},所有 PCM ops 的实际实现者
dev_platform 就是连接这两者的桥梁:在 dai_load 中保存、在 link_load 中写入 DAI Link,告诉 ASoC "这个 DAI 的实际 PCM 实现在 vdma.{id} 这个 component 里"。
可以理解为 这个FE DAI LINK的dai不在其platform component中定义,而是在abox_tplg中的component定义,他只是保存一些能力信息和名字,没有实际的回调等。比如写数据时,platform component driver中调用IPC通知DSP读数据。
3. 函数层级调用
3.1 设置硬件参数链路(hw_params)
当用户层调用 alsa-lib 接口配置音频参数(如采样率、通道数)时:
scss
1. 用户空间 API
snd_pcm_hw_params() (alsa-lib)
2. 内核系统调用入口 (ALSA Core)
└── snd_pcm_playback_ioctl1() (sound/core/pcm_native.c)
└── snd_pcm_hw_params() --> substream->ops->hw_params(substream, params);
3. ASoC 核心中转层 (ASoC Core)
└── soc_pcm_hw_params() (sound/soc/soc-pcm.c)
该函数是 ASoC 的核心分发器,内部会同时敲击 Component 和 DAI:
└── for_each_rtd_codec_dais(rtd, i, codec_dai)
└── for_each_rtd_cpu_dais(rtd, i, cpu_dai)
└── snd_soc_pcm_component_hw_params(substream, params);
4. 具体驱动层 (Vendor Driver)
├── snd_soc_pcm_component_hw_params() ──► 触发 Component 驱动的 .hw_params(如 dmaengine_pcm_hw_params 分配 Ring Buffer)
└── snd_soc_dai_hw_params() ──► 触发 DAI 驱动的 .hw_params(如芯片厂商的 I2S 驱动配置时钟)
3.2 音频流启动传输链路(trigger START)
当用户层调用接口正式启动音频流,硬件 DMA 开始搬运数据时:
scss
1. 用户空间 API
snd_pcm_start() 或写满数据自动触发 (alsa-lib)
2. 内核系统调用入口 (ALSA Core)
└── snd_pcm_playback_ioctl1() (sound/core/pcm_native.c)
└── snd_pcm_action_start()
└── snd_pcm_do_start()
调用 substream->ops->trigger 指向 ASoC 入口:
3. ASoC 核心中转层 (ASoC Core)
└── soc_pcm_trigger() (sound/soc/soc-pcm.c)
开始纵向分发 START 命令:
4. 具体驱动层 (Vendor Driver)
├── snd_soc_pcm_component_trigger() ──► 触发 Component 驱动的 .trigger(START)(开启 DMA 控制器寄存器)
└── snd_soc_dai_trigger() ──► 触发 DAI 驱动的 .trigger(START)(开启数字音频接口发送)