技術筆記 · 音訊與語音

撰寫你自己的 CODEC 驅動 — 第 2 篇:DAC 播放

硬體端把 DAC 接上功放就能出聲——不需要驅動程式。那麼軟體端,要發出聲音需要什麼?翻譯並註解自 羅總(Luo Zong / 罗总),Rockchip——「罗总开发笔记」WeChat

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 驅動到位前,為早期導入與客戶參考設計搭建音訊路徑的方式。

重點

  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 篇相比,兩處改變:

第 1 篇(PDM 擷取)本文(DAC 播放)
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. 總結:仍然零行程式碼

兩篇文章涵蓋一擷取一播放,我們已講完第 1 篇的 .capture 與本文的 .playback——dummy-codec 的能力宣告至此完整說明。其餘程式碼(startup / probe)尚未觸及,你只寫了 DTS。

但並非所有硬體都這麼簡單:

這些正是驅動「用訊號做事」的地方。下一篇文章回答:MCLK、SPK-CTL、RESET 存在於程式碼的哪裡?dummy-codec.c 剩餘的 startupprobe 終於登場。

百斯通註解(過渡到真實驅動): 羅總(罗总)所描述的「簡單路徑」,是早期導入時讓電路板發出聲音最快的方式。當你轉移到真實 Codec(PCM51xx、ES72xx、RK3308 內建 Codec 等)時,同樣的三件式 DTS 不變——改變的是 Codec 驅動,它必須處理 MCLK 閘控、RESET 與板級 GPIO。百斯通能協助你從 dummy-codec 過渡到 RK3308(專用音訊 SoC)與 M08D 模組(RK2108D + RK962) 上的量產 Codec 驅動,用於智慧面板與聲霸。

系列預告——「如何撰寫你自己的 CODEC 驅動」

本文為系列 第 2 篇。原始碼基於 Rockchip BSP kernel 6.1(v6.1.162)——帶你從硬體一路上探到進階 DAPM 架構。

參考原始檔:sound/soc/codecs/pcm5102a.c(比較)與 dummy-codec.c