(一)AudioArchitecture

经典的音频通路如下图

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 核心 共同完成:

  1. 路由寻路(DAPM):当上层操作 FE 的 PCM 设备时,DAPM 从 FE 的 DAI widget 出发,沿着 DSP 内部的音频路由通路进行拓扑图遍历,最终寻找到当前处于激活状态的 BE DAI widget。
  2. 状态绑定(DPCM):内核通过 snd_soc_dpcm 结构体来管理这种动态寻找出来的 FE 与 BE 配对关系,将激活的 BE 动态绑定到当前的 FE 上。
  3. 控制级联(控制流):完成绑定后,上层对 FE 发起的 hw_params、trigger 等控制命令,会通过 DPCM 机制自动、级联地分发调用到 BE 侧的各个组件驱动(如真实 Codec 的驱动函数),从而实现了应用层透明的、无缝的动态音频切换。

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)。


在一些 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)(开启数字音频接口发送)
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui