Linux 音频子系统完整梳理:ALSA、ASoC、DAPM、Codec、Machine、es8389 与 rk‑multicodecs 全解析

一、整体大图:ALSA → ASoC → DAPM → Codec/Machine

先给你一个总的结构图:

ALSA(用户空间接口)
 ├── PCM(aplay/arecord)
 ├── Mixer(amixer)
 └── Controls(音量、开关、路由)

ASoC(SoC 音频框架)
 ├── CPU DAI(I2S/PCM/TDM 控制器)
 ├── Codec DAI(ES8389 等音频编解码器)
 └── Machine Driver(板级 glue:rk-multicodecs.c)
      └── DAPM(Dynamic Audio Power Management)
           ├── Widgets(节点:DAC/ADC/HP/SPK/Power)
           └── Routes(边:谁连谁)
  • ALSA:对用户空间暴露统一接口(aplay、amixer 用的就是它)。
  • ASoC:专门为 SoC/嵌入式设计的 ALSA 扩展,负责把 CPU DAI、Codec、板级硬件 glue 在一起。
  • DAPM:ASoC 里的“动态音频电源管理”,负责音频路径、电源、功放的自动开关。
  • Codec Driver(es8389.c):描述 Codec 芯片内部的音频结构和寄存器。
  • Machine Driver(rk‑multicodecs.c):描述板级硬件(耳机、喇叭、功放 GPIO、耳机检测等)。

二、ALSA 与 ASoC:谁管什么?

1. ALSA(Advanced Linux Sound Architecture)

  • 提供:
    • PCM:播放/录音(aplay/arecord)
    • Mixer/Controls:音量、开关、路由(amixer)
  • 不关心:
    • 板子上是 ES8389 还是 ES8316
    • 喇叭是哪个 GPIO 控制
    • 耳机插孔怎么接

2. ASoC(ALSA System‑on‑Chip)

ASoC 把一个 SoC 音频系统拆成三块:

  • CPU DAI:SoC 内部的 I2S/PCM 控制器(比如 Rockchip I2S)
  • Codec DAI:外部音频 Codec(比如 ES8389)
  • Machine Driver:把 CPU + Codec + 板级硬件 glue 在一起(rk‑multicodecs.c)

你现在看到的两个文件:

  • es8389.c → Codec Driver
  • rk-multicodecs.c → Machine Driver

就是这两层的典型实现。


三、Codec Driver vs Machine Driver:谁负责什么?

1. Codec Driver(以 es8389.c 为例)

负责:

  • Codec 内部的 DAPM Widgets
    • DACL / DACR
    • ADCL / ADCR
    • OUTL MUX / OUTR MUX
    • HPOL / HPOR
    • ADC Mixer / IF DACL Mixer
  • Codec 内部的 Routes
    • DACL → IF DACL1 → IF DACL Mixer → OUTL MUX → HPOL
  • Codec 的 寄存器配置
    • 采样率、位宽、时钟分频(coeff_div
    • 上电/下电序列(es8389_set_bias_level()
    • 音量寄存器(DACL Playback Volume 等)
  • DAI 格式:
    • I2S / Left Justified / DSP_A / DSP_B
  • MCLK / SYSCLK 设置

一句话:

Codec Driver 描述的是“芯片内部世界”。


2. Machine Driver(以 rk‑multicodecs.c 为例)

负责:

  • 板级 DAPM Widgets
    SND_SOC_DAPM_HP("Headphone", NULL),
    SND_SOC_DAPM_SPK("Speaker", NULL),
    SND_SOC_DAPM_MIC("Main Mic", NULL),
    SND_SOC_DAPM_MIC("Headset Mic", NULL),
    SND_SOC_DAPM_SUPPLY("Speaker Power", ..., mc_spk_event, ...),
    SND_SOC_DAPM_SUPPLY("Headphone Power", ..., mc_hp_event, ...),
    
  • 板级 功放电源控制
    • mc_spk_event() / mc_hp_event() 里通过 GPIO 控制功放上电/断电
  • 板级 耳机检测 / ADC 按键
    • hp_det_gpio + adc + input_dev
  • 声卡整体 glue:
    • snd_soc_card
    • snd_soc_dai_link
    • CPU DAI ↔ Codec DAI 连接
  • DAPM 路由中与板级相关的部分(通常通过 audio-routing DT 属性描述)

一句话:

Machine Driver 描述的是“板子外部世界”:耳机孔、喇叭、功放、电源、检测。


四、DAPM:动态音频电源管理的核心概念

DAPM 是 ASoC 的核心,它管理:

  • 哪些模块需要上电
  • 哪些路径需要打开
  • 功放什么时候开/关
  • 避免 pop noise(“啪”的一声)
  • 自动省电(不用时自动断电)

1. DAPM Widget(节点)

你看到的这些宏:

SND_SOC_DAPM_DAC("DACL", ...)
SND_SOC_DAPM_ADC("ADCL", ...)
SND_SOC_DAPM_MIXER("ADC Mixer", ...)
SND_SOC_DAPM_MUX("OUTL MUX", ...)
SND_SOC_DAPM_HP("Headphone", ...)
SND_SOC_DAPM_SPK("Speaker", ...)
SND_SOC_DAPM_SUPPLY("Headphone Power", ...)

本质上都是:

音频拓扑图里的“节点”
有的传音频(DAC/ADC/Mixer/MUX),有的是电源(Supply),有的是外设(HP/SPK/MIC)。

2. DAPM Route(边)

Codec 里:

{"OUTL MUX", "normal", "IF DACL Mixer"},
{"HPOL", NULL, "OUTL MUX"},

Machine 里(通常通过 DT audio-routing):

Headphone → HPOL
Speaker → SPK_OUT

ASoC 会把 Codec + Machine 的所有 Widgets 和 Routes 合并成一个完整的 DAPM 图。


五、DAPM Supply:为什么要用它,而不是自己写 GPIO?

你问过一个非常关键的问题:

“我直接在 IRQ 或 poll 里控制 GPIO 不就行了?
为什么还要搞一个 SND_SOC_DAPM_SUPPLY 这么复杂?”

1. Supply 的定义(以 Headphone Power 为例)

SND_SOC_DAPM_SUPPLY("Headphone Power",
    SND_SOC_NOPM, 0, 0,
    mc_hp_event,
    SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD),

含义:

  • 定义一个名为 "Headphone Power" 的电源节点
  • 不绑定具体寄存器(SND_SOC_NOPM),由回调控制
  • 当 DAPM 决定“耳机路径需要上电”时:
    • 触发 POST_PMU → 调用 mc_hp_event() → 打开功放 GPIO
  • 当 DAPM 决定“耳机路径不再使用”时:
    • 触发 PRE_PMD → 调用 mc_hp_event() → 关闭功放 GPIO

2. 为什么不能只用 IRQ/poll?

因为音频电源时序必须严格:

  • 播放开始:
    1. Codec bias 上电
    2. DAC 上电
    3. Mixer/MUX 打开
    4. 最后 打开功放(避免 pop)
  • 播放结束:
    1. 关功放
    2. 再关 Mixer/DAC
    3. 最后关 bias

你自己写 GPIO 根本拿不到这些状态,只能“猜时机”;
而 DAPM 是整个音频系统的状态机,它知道:

  • 播放/录音是否开始
  • 哪条路径被使用
  • 哪些 Widget 需要上电/断电

Supply 的意义就是:

把“功放电源”这个动作挂到 DAPM 状态机里,让它在正确的时机自动执行。


六、DAPM 事件:POST_PMU / PRE_PMD 是什么?

你看到的:

SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD

它们不是函数,而是 事件标志(bit flags),定义在 include/sound/soc-dapm.h

#define SND_SOC_DAPM_PRE_PMU   0x1
#define SND_SOC_DAPM_POST_PMU  0x2
#define SND_SOC_DAPM_PRE_PMD   0x4
#define SND_SOC_DAPM_POST_PMD  0x8

含义:

  • PRE_PMU:上电前
  • POST_PMU:上电后
  • PRE_PMD:断电前
  • POST_PMD:断电后

DAPM 状态机在执行电源切换时,会根据这些标志调用你的回调:

mc_hp_event(widget, SND_SOC_DAPM_POST_PMU);
mc_hp_event(widget, SND_SOC_DAPM_PRE_PMD);

你在回调里做的通常是:

case SND_SOC_DAPM_POST_PMU:
    gpiod_set_value_cansleep(hp_ctl_gpio, 1);
    break;
case SND_SOC_DAPM_PRE_PMD:
    gpiod_set_value_cansleep(hp_ctl_gpio, 0);
    break;

七、Codec 上电:最终一定会落到 es8389 的寄存器

你问的最后一个关键问题是:

“那最后还是会调用到 es8389 的寄存器?完成上电?”

答案是:一定会。

完整链路是这样的:

播放开始时:

  1. 用户空间:aplay test.wav
  2. ALSA PCM:打开播放流
  3. ASoC DAPM:
    • 发现 Playback 路径被使用
    • 激活相关 Widgets(DACL、OUTL MUX、Headphone 等)
  4. DAPM 状态机:
    • 调用 Codec Driver 的 es8389_set_bias_level(SND_SOC_BIAS_ON)
    • Codec Driver 里:
      clk_prepare_enable(mclk);
      regmap_write(... RESET_REG00, 0x01);
      regmap_write(... CLK_OFF1_REG03, 0xC3);
      regmap_write(... DAC_RESET_REG4D, 0x00);
      
    • 调用 Machine Driver 的 mc_hp_event(POST_PMU) 打开功放 GPIO

播放结束时:

  1. PCM 关闭播放流
  2. DAPM 状态机:
    • 调用 mc_hp_event(PRE_PMD) 关闭功放
    • 调用 es8389_set_bias_level(SND_SOC_BIAS_STANDBY/OFF) 关闭 Codec 内部模块

所以:

DAPM 决定“什么时候上电/下电”,
Codec Driver 决定“上电/下电时具体写哪些寄存器”。


八、amixer 在这套体系里扮演什么角色?

amixer 操作的是 ALSA Controls,这些控件来自:

  • Codec Driver 暴露的控件(es8389_snd_controls[]
  • Codec/Machine 暴露的 DAPM 控件(Widgets/MUX/Mixer)

例如:

static const struct snd_kcontrol_new es8389_snd_controls[] = {
    SOC_SINGLE_TLV("DACL Playback Volume", ES8389_DACL_VOL_REG46, ...),
    SOC_ENUM("ADC MUX", es8389_dmic_mux_enum),
    ...
};

这些控件会出现在:

amixer -c 0 controls
amixer -c 0 scontents

amixer 能做的事情包括:

  • 改 Codec 寄存器(音量、增益、MUX 选择)
  • 改 DAPM Widget 状态(比如 enable/disable Headphone)
  • 间接触发 DAPM 路由变化 → 触发功放电源事件

所以你看到:

amixer 里既有 Codec 的控件(DACL Playback Volume),
也有 Machine 的控件(Headphone、Speaker、Speaker Power)。


九、常见实战操作:你已经在做的几件事

1. 不插耳机也能播放:强制打开 DAPM pin

在 Machine Driver 的 probe 里:

snd_soc_dapm_force_enable_pin(&card->dapm, "Headphone");
snd_soc_dapm_force_enable_pin(&card->dapm, "Speaker");
snd_soc_dapm_sync(&card->dapm);

效果:

  • 不再依赖 hp-det GPIO
  • Headphone/Speaker 路径永远视为“启用”
  • 播放时 DAPM 会自动打开 Codec + 功放

2. 修改默认音量:推荐在 Codec Driver 里改

es8389_probe() 里:

regmap_write(es8389->regmap, ES8389_DACL_VOL_REG46, 0xA0);
regmap_write(es8389->regmap, ES8389_DACR_VOL_REG47, 0xA0);

或者用 amixer 在用户空间设置,并写入启动脚本。

3. 打印完整 DAPM 路由图

ls /sys/kernel/debug/asoc/
cat /sys/kernel/debug/asoc/<card>/dapm
cat /sys/kernel/debug/asoc/<card>/widgets
cat /sys/kernel/debug/asoc/<card>/routes

可以看到 Codec + Machine 合并后的完整拓扑。


十、最后给你一个“脑内模型”

你可以把整个系统想象成这样:

  • Codec Driver(es8389.c)
    “我负责芯片内部的世界——DAC/ADC/Mixer/MUX/寄存器/上电时序。”

  • Machine Driver(rk‑multicodecs.c)
    “我负责板子外部的世界——耳机孔、喇叭、功放 GPIO、耳机检测、ADC 按键。”

  • DAPM
    “我负责全局状态机——谁在用、谁要上电、谁要断电、什么时候开功放、什么时候关。”

  • ALSA / amixer / aplay
    “我负责让用户空间有一个统一的接口——你不用关心底下有多少 Codec、有多少 GPIO。”

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐