技术笔记 · 音频与语音

编写你自己的 CODEC 驱动 — 第 2 篇:DAC 播放

硬件侧把 DAC 接上功放就能出声——无需驱动。那么软件侧,要出声到底需要做什么?翻译并注释自 罗总(罗总), Rockchip — "罗总开发笔记" 微信

Attribution & authorization
Original author: Luo Zong (罗总), Rockchip · Published on the WeChat public account "罗总开发笔记" on 2026-08-25.
Original URL: https://mp.weixin.qq.com/s/CqBo2NonYYGqC99ojL--4Q
Republished here with authorization, translated into English and annotated by Bestom. This is Part 2 of the series "How to write your own CODEC driver" — Part 1 (PDM microphone capture) is here.

接上 DAC 就能出声——软件需要做什么?

上一篇文章讲了 PDM 麦克风——芯片无需控制,配置好 DTS 就能录音。这次我们调转方向——播放。

很多 DAC 芯片同样"甩手不管":无需控制接口,接上 I2S 数据线与电源,就输出声音。硬件工程师喜欢这类——零工作量。那么在 Linux 上,如果我们把这样的 DAC 配上一个 dummy-codec,要播放出声需要写多少行驱动代码?

答案和上次一样:零行。但这次我们调转了 DAI 的方向。

百斯通注: 这篇文章与我们音频 SoM 的工作天然契合——参见 解决方案 → 音频与语音 与 M08D 模组(RK2108D + RK962),用于智能面板与声霸。罗总描述的 dummy-codec 模式,正是我们在真实 Codec 驱动到位前,为早期 bring-up 与客户参考设计搭建音频通路的手法。

要点

  1. 播放方向 = DAI 声明 .playback,声卡暴露 /dev/snd/pcmC0D0p
  2. 像 PCM5102A 这样的 DAC 内置 PLL——它们从 BCLK 自行派生时钟,因此 无需 MCLK。真正的零控制播放。
  3. dummy-codec 中的 .playback 能力声明正是本文覆盖的内容。DTS 仍是同样的三件套。aplay 直接播放。

1. 首先,方向:PLAYBACK 还是 CAPTURE

第 1 篇(PDM 麦克风)是 capture 方向——数据流入 SoC——声卡暴露 pcmC0D0c。这次我们翻到另一边——playback——数据流出 SoC:

Playback (PLAYBACK) Capture (CAPTURE) ┌──────────────┐ ┌──────────────┐ │ Speaker / │ │ Microphone │ │ Headphone │ │ │ ▲ │ │ ▼ │ DAC │ │ ADC │ ▲ │ │ ▼ │ I2S data │ │ PDM / I2S │ │ ▼ │ ▼ SoC audio IF ← data OUT SoC audio IF ← data IN

回顾第 1 篇的 DAI 能力小结:

struct snd_soc_dai_driver {
        .playback = { ... },    /* declared = this stream exists = /dev/snd/pcmC0D0p */
        .capture  = { ... },    /* declared = this stream exists = /dev/snd/pcmC0D0c */
};

2. 主角:一颗完全无需控制的 DAC

以 TI 的 PCM5102A 为例——HiFi 圈的熟面孔,用于许多音频板。看看它的硬件"需求清单":

RequirementNeeded?
I2S data lines (BCLK / LRCK / DATA)Yes — 3 wires
MCLK master clockNo — internal PLL derives the clock from BCLK
I2C / SPI control interfaceNo — the chip has no such pins
Register configurationNo — there are no registers

上电、喂 I2S 数据,就出声。比 PDM 麦克风还省心——你甚至不用想"我得给它时钟"(BCLK 随 I2S 自带)。

内核已有这颗芯片的专用驱动——sound/soc/codecs/pcm5102a.c,总共 57 行。核心只是一句能力声明:

static struct snd_soc_dai_driver pcm5102a_dai = {
  .name = "pcm5102a-hifi",
  .playback = {                              /* NOTE: playback only! */
    .channels_min = 2,
    .channels_max = 2,
    .rates = SNDRV_PCM_RATE_8000_384000,
    .formats = SNDRV_PCM_FMTBIT_S16_LE |
         SNDRV_PCM_FMTBIT_S24_LE |
         SNDRV_PCM_FMTBIT_S32_LE
  },
};

static int pcm5102a_probe(struct platform_device *pdev)
{
  return devm_snd_soc_register_component(&pdev->dev, &soc_component_dev_pcm5102a,
      &pcm5102a_dai, 1);
}

仅此而已:声明"我能播 2 声道",然后注册。但你甚至不需要它——dummy-codec 同时声明了 .playback.capture,所以它绰绰有余地覆盖了 PCM5102A 的槽位。芯片本就无需控制,所以 Codec 驱动只是占位符——任何占位符都行。

dummy_dai 中的 .playback 部分

第 1 篇看了 dummy-codec.c.capture(第 60–70 行)。本文对称地看它的播放声明(源码第 49–59 行):

struct snd_soc_dai_driver dummy_dai = {
  .name = "dummy_codec",
  .playback = {                           /* ← Playback capability: this article's focus */
    .stream_name = "Dummy Playback",  /* stream name */
    .channels_min = 1,                /* min 1 channel */
    .channels_max = 384,              /* max 384 channels */
    .rates = SNDRV_PCM_RATE_CONTINUOUS,  /* any sample rate */
    .formats = (SNDRV_PCM_FMTBIT_S8 |
          SNDRV_PCM_FMTBIT_S16_LE |
          SNDRV_PCM_FMTBIT_S20_3LE |
          SNDRV_PCM_FMTBIT_S24_LE |
          SNDRV_PCM_FMTBIT_S32_LE),  /* bit depth S8 ~ S32 */
  },
  ...                                     /* .capture was covered in Part 1 */
};

.capture 逐字符对称,仅 stream_name 改为 "Dummy Playback"。有了这段,pcmC0D0p 被创建,aplay 可打开它。

与 PCM5102A 的声明相比,唯一的差别在"肚量":

pcm5102a_daidummy_dai
playback2 channels, 8k–384k (per the datasheet)1–384 channels, any sample rate (no real chip — overshoot is free)
capturenoneyes (reserved for capture scenarios)

真实芯片驱动的能力声明必须贴合 datasheet——过度声明会在打开时让应用崩溃。dummy 不接真实芯片,所以过度声明无妨;真正的约束由 CPU DAI 侧强制。


3. DTS 配置:仍是三件套

与第 1 篇 PDM 同样的配方,只是接口与方向不同:

/* ① Codec DAI: dummy-codec placeholder (same driver as Part 1) */
dummy_codec: dummy-codec {
  status = "okay";
  compatible = "rockchip,dummy-codec";
  #sound-dai-cells = <0>;
};

/* ② CPU DAI: enable the I2S controller + pin mux */
&i2s0 {
  status = "okay";
  pinctrl-names = "default";
  pinctrl-0 = <&i2s0m0_sclk &i2s0m0_lrck
         &i2s0m0_sdi0 &i2s0m0_sdo0>;  /* BCLK / LRCK / data lines */
};

/* ③ Machine: assemble the sound card */
dummy_sound: dummy-sound {
  status = "okay";
  compatible = "rockchip,multicodecs-card";
  rockchip,card-name = "rockchip-dummy";    /* sound-card name */
  rockchip,format = "i2s";                  /* data format */
  rockchip,cpu = <&i2s0>;                   /* CPU DAI = I2S controller */
  rockchip,codec = <&dummy_codec>;          /* Codec DAI = dummy */
};

与第 1 篇相比有两处变化:

Part 1 (PDM capture)This article (DAC playback)
CPU DAI&pdm&i2s0
Data directioncapture (mic → SoC)playback (SoC → DAC)

三件套结构完全一致——这正是框架的价值。换方向、换接口,配方不变。


4. 播放验证

# 1. Sound card registered
cat /proc/asound/cards
#  0 [dummy         ]: rockchip-dummy

# 2. Device node — this time it's the playback (p)
ls /dev/snd/
#  controlC0  pcmC0D0p  timer

# 3. Play
aplay -D hw:0,0 -c 2 -r 48000 -f S16_LE /tmp/sine.wav
# Android: tinyplay /tmp/sine.wav

# 4. Check the file parameters match
ls -l /tmp/sine.wav
# 48000Hz × 2ch × 2 bytes, matches aplay's arguments

数据流与第 1 篇正好相反:

/tmp/sine.wav │ aplay writes /dev/snd/pcmC0D0p ▼ ring buffer in memory │ DMA moves it ▼ I2S controller (rockchip-i2s, outputs BCLK/LRCK + data) │ 3 wires ▼ PCM5102A (internal PLL self-clocks → DAC → analog) │ ▼ Speaker

5. 小结:依然零行驱动代码

两篇文章,一篇 capture 一篇 playback,我们覆盖了第 1 篇的 .capture 与本文的 .playback——dummy-codec 的能力声明现已完整讲清。其余代码(startup / probe)尚未触及,你只写了 DTS。

但并非所有硬件都这么轻松:

这些正是驱动"用信号做点事"的地方。下一篇文章回答:在代码的哪里放 MCLK、SPK-CTL、RESET?dummy-codec.c 剩余的 startupprobe 终于登场。

百斯通注(过渡到真实驱动): 罗总描述的"轻松路径"是早期 bring-up 时让板子出声最快的办法。当你换上真实 Codec(PCM51xx、ES72xx、RK3308 内置 Codec 等),同样的三件套 DTS 不变——变的是 Codec 驱动,它必须处理 MCLK 门控、RESET 与板级 GPIO。百斯通可在 RK3308(专用音频 SoC)与 M08D 模组(RK2108D + RK962) 上,帮你从 dummy-codec 桥接到量产 Codec 驱动,用于智能面板与声霸。

系列预告 — "如何编写你自己的 CODEC 驱动"

本文是该系列的 第 2 篇。源码基于 Rockchip BSP 内核 6.1(v6.1.162)——带你从硬件一路深入到高级 DAPM 架构。

参考源文件:sound/soc/codecs/pcm5102a.c(对比)与 dummy-codec.c