Tech Notes · Audio & Voice

Writing Your Own CODEC Driver — Part 2: DAC Playback

Connect a DAC to an amp on the hardware side and it just plays — no driver needed. So on the software side, what does it take to produce sound? Translated and annotated from 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.

Connect a DAC and it just plays — what does the software need to do?

Last article covered the PDM microphone — the chip needs no control, configure DTS and it records. This time we flip the direction — playback.

Many DAC chips are just as "hands-off": no control interface required, connect the I2S data lines and power, and they output sound. Hardware engineers love these — zero effort. So on Linux, if we pair such a DAC with a dummy-codec, how many lines of driver code do we need to write to play audio?

The answer is the same as last time: zero. But this time we flip the DAI direction.

Bestom note: This article pairs naturally with our audio SoM work — see Solutions → Audio & Voice and the M08D module (RK2108D + RK962) for smart panels and soundbars. The dummy-codec pattern Luo Zong describes is exactly how we stand up an audio path for early bring-up and customer reference designs before the real codec driver lands.

Key points

  1. Playback direction = the DAI declares .playback, the sound card exposes /dev/snd/pcmC0D0p.
  2. DACs like PCM5102A have an internal PLL — they derive their own clock from BCLK, so no MCLK is needed. Truly zero-control playback.
  3. The .playback capability declaration in dummy-codec is what this article covers. DTS is still the same three-piece combo. aplay plays directly.

1. First, the direction: PLAYBACK vs CAPTURE

Part 1 (PDM mic) was the capture direction — data flows into the SoC — and the sound card exposed pcmC0D0c. This time we flip to the other side — playback — data flows out of the SoC:

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

Recall the DAI capability summary from Part 1:

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

2. The star: a DAC that needs no control at all

Take TI's PCM5102A — a familiar face in the HiFi world, used on many audio boards. Look at its hardware "requirements list":

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

Power it up, feed it I2S data, it makes sound. Even more hands-off than the PDM mic — you don't even have to think about "I must give it a clock" (BCLK comes with I2S anyway).

The kernel already has a dedicated driver for this chip — sound/soc/codecs/pcm5102a.c, 57 lines total. The core is a single capability declaration:

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);
}

That's it: declare "I can play 2 channels", then register. But you don't even need itdummy-codec declares both .playback and .capture, so it covers PCM5102A's slot more than adequately. The chip needs no control anyway, so the codec driver is just a placeholder — and any placeholder will do.

The .playback portion in dummy_dai

Part 1 looked at dummy-codec.c's .capture (lines 60–70). This article symmetrically looks at its playback declaration (source lines 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 */
};

Symmetric to .capture character-for-character, just the stream_name changes to "Dummy Playback". With this block present, pcmC0D0p is created and aplay can open it.

Compared with PCM5102A's declaration, the only difference is "guts":

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)

A real chip driver's capability declaration must stick to the datasheet — over-declare and applications crash on open. dummy doesn't connect a real chip, so over-declaring is fine; the CPU DAI side enforces the real constraints.


3. DTS configuration: still the three-piece combo

Same recipe as Part 1's PDM, just a different interface and direction:

/* ① 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 */
};

Two things change vs. Part 1:

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

The three-piece structure is identical — that's the value of the framework. Change direction, change interface, the recipe stays.


4. Playback verification

# 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

The data flow is the exact reverse of Part 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. Recap: still zero lines of driver code written

Across both articles, one capture and one playback, we've covered Part 1's .capture and this article's .playbackdummy-codec's capability declarations are now fully explained. The rest of the code (startup / probe) has not been touched, and you've only written DTS.

But not all hardware is this easy:

These are exactly the places where a driver "does something" with signals. The next article answers: where in the code do MCLK, SPK-CTL, RESET live? dummy-codec.c's remaining startup and probe finally come on stage.

Bestom note (transition to real drivers): The "easy path" Luo Zong describes is the fastest way to get audio out of a board during early bring-up. When you move to a real codec (PCM51xx, ES72xx, RK3308 built-in codec, etc.), the same three-piece DTS stays — what changes is the codec driver, which must handle MCLK gating, RESET, and board-level GPIO. Bestom can help bridge from dummy-codec to your production codec driver on RK3308 (dedicated audio SoC) and on the M08D module (RK2108D + RK962) for smart panels and soundbars.

Series preview — "How to write your own CODEC driver"

This article is Part 2 of the series. Source code is based on the Rockchip BSP kernel 6.1 (v6.1.162) — taking you from the hardware all the way up to advanced DAPM architecture.

Source files referenced: sound/soc/codecs/pcm5102a.c (comparison) and dummy-codec.c.