(一)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)(开启数字音频接口发送)
相关推荐
AFinalStone5 小时前
Android12 U盘插拔链路源码全解析(四):Framework层(上) —— UsbHostManager
android·frameworks
qq3621967055 小时前
第三方安卓应用商店安全评测 2026:Appteka、Aptoide、APKPure 等 7 家横评
android·网络·人工智能·安全·chatgpt·智能手机
coderhuo6 小时前
JibarOS 简介:Android AICore 开源实现方案
android·ai编程
故渊at6 小时前
第十五板块:Android 系统调试与逆向工程 | 第三十六篇:Smali 字节码语义与 Dalvik 指令集
android·指令集·dalvik·smali·字节码语义
J2虾虾6 小时前
Android支持Java语言的标准
android·java·开发语言
charlee447 小时前
Unity在安卓端如何调试输出信息
android·unity·adb·游戏引擎·真机调试
法欧特斯卡雷特7 小时前
从 Kotlin 编译器 API 的变化开始: 2.4.0
android·开源·github
贾艺驰7 小时前
实战Android Framework: 新增一个系统服务
android·源码
火山上的企鹅7 小时前
Codex实战:APP远程升级服务搭建(五)App端远程升级接入
android·服务器·远程升级·qgc
BreezeDove7 小时前
【Android】Flutter3.35项目启动超时问题
android·flutter