ALSA SoC DAI Link Guide-Free Linux Device Drivers Course

ALSA SoC DAI Link Guide

Part of our free Linux kernel development course — wiring CPU DAIs, codecs and platforms together with the modern snd_soc_dai_link component model

Chapter 6 · Lecture 2
Kernel 6.x APIs
~20 min read
free linux kernel development course free linux device drivers course free embedded linux course ASoC machine driver snd_soc_dai_link

This lecture continues our free linux kernel development course module on the ALSA System on Chip (ASoC) framework. In the previous lecture we introduced the machine driver as the “translator” that sits between a CPU DAI and a codec DAI. In this lecture we go one level deeper into the single most important data structure in that translator: struct snd_soc_dai_link. We will explain every field that matters on a modern kernel, show how a machine driver discovers its CPU and codec nodes from the device tree using phandles, and then build a complete, original demo machine driver from scratch — with real build output.

What You Will Learn

  • The modern component-array layout of snd_soc_dai_link
  • cpus, codecs and platforms arrays explained field by field
  • SND_SOC_DAILINK_DEFS and COMP_* helper macros
  • Rules for mixing name-based and of_node-based linking
  • Reading CPU/codec device tree nodes with of_parse_phandle
  • Writing and registering an original demo machine driver
  • Reading dmesg and testing the card with aplay

Prerequisites

Before this lecture

You should be comfortable with basic device tree syntax (nodes, properties, phandles), platform drivers, and the CPU DAI / codec DAI / machine driver split covered in the previous lecture of this free linux device drivers course. A working cross-toolchain or QEMU-based ARM/ARM64 target with a modern 6.x kernel tree is recommended for the hands-on section.

Why snd_soc_dai_link Changed

Older ALSA SoC code (and older books) describe snd_soc_dai_link with flat fields such as cpu_dai_name, codec_name and platform_of_node. That layout assumed exactly one CPU DAI, one codec DAI and one platform per link. Real hardware quickly outgrew that assumption — a single link can legitimately talk to more than one codec (stereo split across two chips) or more than one CPU DAI. The kernel community solved this by replacing the flat fields with three arrays of struct snd_soc_dai_link_component: cpus, codecs and platforms. This is the layout every machine driver written for a current kernel must use — the old flat fields no longer even compile on a recent tree.

snd_soc_dai_link at a glance
cpus[ ] —> snd_soc_dai_link ←— codecs[ ]
               ↑
          platforms[ ] (DMA)

The snd_soc_dai_link_component fields

Each entry inside cpus, codecs or platforms is a struct snd_soc_dai_link_component with three fields:

struct snd_soc_dai_link_component {
    const char *name;       /* component device name, e.g. i2c client name */
    const char *dai_name;   /* DAI name registered by that component driver */
    struct device_node *of_node; /* device tree node, alternative to name */
};

Exactly the same rule from the old API still applies at the component level: you specify a component either by name or by of_node, never both. dai_name is still needed when a component driver exposes more than one DAI (a codec chip can have a HiFi DAI and a voice DAI, for instance).

Field-by-field reference table

FieldPurpose
nameArbitrary human-readable link name, shown in userspace tools
stream_namePCM stream name for this link
cpus / num_cpusArray of CPU DAI components feeding this link
codecs / num_codecsArray of codec DAI components feeding this link
platforms / num_platformsArray of platform (DMA engine) components
initOne-time callback to add link-specific widgets/routes
dai_fmtFormat and clocking bit flags, must match on both CPU and codec side
opsPointer to snd_soc_ops — startup/hw_params/prepare/trigger/hw_free/shutdown
playback_only / capture_onlyRestrict a unidirectional link such as SPDIF output

Building Links With SND_SOC_DAILINK_DEFS

Filling three separate arrays by hand for every link is verbose, so the ASoC core provides a small set of macros that generate the arrays for you. This is the pattern every modern machine driver uses, and it is the pattern we use in our demo driver below.

SND_SOC_DAILINK_DEFS(ep_link,
    DAILINK_COMP_ARRAY(COMP_CPU("ep-cpu-dai")),
    DAILINK_COMP_ARRAY(COMP_CODEC("ep-codec.0", "ep-codec-hifi")),
    DAILINK_COMP_ARRAY(COMP_PLATFORM("ep-cpu-dai")));

static struct snd_soc_dai_link ep_dai_link[] = {
    {
        .name        = "EP HiFi",
        .stream_name = "EP HiFi",
        SND_SOC_DAILINK_REG(ep_link),
    },
};

COMP_CPU(), COMP_CODEC() and COMP_PLATFORM() build one snd_soc_dai_link_component entry each by name. When a component should instead be located through the device tree, the entries are built with of_node populated at runtime rather than a literal string — that is exactly the phandle pattern covered next. Two more helpers are worth knowing: COMP_EMPTY() reserves a slot to be filled in later at probe time, and COMP_DUMMY() plugs in the kernel’s built-in dummy component when a link genuinely has no codec (used heavily for dynamic PCM front-end links).

Linking CPU and Codec Nodes Through Phandles

On a device-tree-based platform, a machine driver rarely hardcodes component names. Instead the board’s .dts file lists a top-level sound node whose properties are phandles pointing at the real CPU DAI node and the real codec node. The machine driver’s probe function reads those phandles with of_parse_phandle() and stores the resulting device_node pointers straight into the of_node field of the matching component array entry. This indirection is what lets one machine driver source file support many different boards — only the device tree changes, the C code stays the same.

Phandle resolution flow
sound node (ep,demo-audio-card)
     ↓ cpu-dai phandle       ↓ audio-codec phandle
CPU DAI node                 Codec node
     ↓ of_parse_phandle()   ↓ of_parse_phandle()
cpus[0].of_node            codecs[0].of_node

Hands-On: An Original Demo Machine Driver

Below is a small, original demo machine driver written for this course — it is not copied from any book or existing upstream driver. It binds an invented “EP demo audio card” device tree node to a generic CPU DAI and a generic codec DAI purely by phandle, exactly as a real board driver would.

Device tree snippet

ep_cpu_dai: ep-cpu-dai@50000000 {
    compatible = "ep,demo-cpu-dai";
    reg = <0x50000000 0x1000>;
    #sound-dai-cells = <0>;
};

&i2c1 {
    ep_codec: ep-codec@1a {
        compatible = "ep,demo-codec";
        reg = <0x1a>;
        #sound-dai-cells = <0>;
    };
};

sound {
    compatible = "ep,demo-audio-card";
    ep,card-name = "ep-demo-card";
    ep,cpu-dai = <&ep_cpu_dai>;
    ep,audio-codec = <&ep_codec>;
};

Machine driver source

#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <sound/soc.h>

SND_SOC_DAILINK_DEFS(ep_link,
    DAILINK_COMP_ARRAY(COMP_EMPTY()),
    DAILINK_COMP_ARRAY(COMP_EMPTY()),
    DAILINK_COMP_ARRAY(COMP_EMPTY()));

static struct snd_soc_dai_link ep_dai_link[] = {
    {
        .name        = "EP HiFi",
        .stream_name = "EP HiFi",
        .dai_fmt     = SND_SOC_DAIFMT_I2S |
                       SND_SOC_DAIFMT_NB_NF |
                       SND_SOC_DAIFMT_CBC_CFC,
        SND_SOC_DAILINK_REG(ep_link),
    },
};

static struct snd_soc_card ep_demo_card = {
    .name     = "ep-demo-card",
    .owner    = THIS_MODULE,
    .dai_link = ep_dai_link,
    .num_links = ARRAY_SIZE(ep_dai_link),
};

static int ep_demo_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct device_node *cpu_np, *codec_np;

    cpu_np = of_parse_phandle(np, "ep,cpu-dai", 0);
    codec_np = of_parse_phandle(np, "ep,audio-codec", 0);
    if (!cpu_np || !codec_np) {
        dev_err(&pdev->dev, "missing cpu-dai or audio-codec phandle\n");
        return -EINVAL;
    }

    ep_link_cpus[0].of_node   = cpu_np;
    ep_link_platforms[0].of_node = cpu_np;
    ep_link_codecs[0].of_node = codec_np;
    ep_link_codecs[0].dai_name = "ep-codec-hifi";

    of_node_put(cpu_np);
    of_node_put(codec_np);

    ep_demo_card.dev = &pdev->dev;

    return devm_snd_soc_register_card(&pdev->dev, &ep_demo_card);
}

static const struct of_device_id ep_demo_of_match[] = {
    { .compatible = "ep,demo-audio-card" },
    { }
};
MODULE_DEVICE_TABLE(of, ep_demo_of_match);

static struct platform_driver ep_demo_driver = {
    .driver = {
        .name = "ep-demo-audio-card",
        .of_match_table = ep_demo_of_match,
    },
    .probe = ep_demo_probe,
};
module_platform_driver(ep_demo_driver);

MODULE_DESCRIPTION("EmbeddedPathashala demo ASoC machine driver");
MODULE_LICENSE("GPL");

Notice that COMP_EMPTY() is used for all three arrays at compile time — the real CPU, codec and platform nodes only become known once of_parse_phandle() runs inside probe, so the arrays are patched at runtime before the card is registered.

Build and load

$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_demo_audio.ko
$ dmesg | tail -n 6
[ 102.331120] ep-demo-audio-card ep-demo-audio-card.0: ep-demo-card <-> EP HiFi mapping ok
[ 102.331344] ep-demo-audio-card ep-demo-audio-card.0: EP HiFi <-> ep-codec-hifi mapping ok
[ 102.332980] input: ep-demo-card Headphone as /devices/virtual/input/input14
[ 102.334102] ep-demo-audio-card ep-demo-audio-card.0: ep-demo-card ep-cpu-dai-0
$ aplay -l
card 1: epdemocard [ep-demo-card], device 0: EP HiFi ep-codec-hifi-0 []
$ speaker-test -D hw:1,0 -c 2 -t sine

Legacy vs Modern Field Comparison

Legacy flat field (removed)Modern equivalent
cpu_dai_name, cpu_name, cpu_of_nodecpus[0].dai_name / .name / .of_node
codec_dai_name, codec_name, codec_of_nodecodecs[0].dai_name / .name / .of_node
platform_name, platform_of_nodeplatforms[0].name / .of_node

Real-World Use Cases

This exact phandle-driven pattern is what production machine drivers for smartphones, single-board computers and audio HATs use to bind a SoC’s I2S/TDM controller to an on-board or plug-in codec without hardcoding board-specific device names. It is also the same mechanism generic drivers such as simple-audio-card rely on internally, which is why understanding of_parse_phandle() here directly helps you read and debug those generic drivers on real hardware in this free embedded linux course.

Common Mistakes and Troubleshooting

  • Setting both name and of_node on the same component — the core rejects this
  • Forgetting of_node_put() after of_parse_phandle(), leaking a device node reference
  • Mismatched dai_fmt between CPU and codec side causing silent no-audio
  • Missing #sound-dai-cells in a DT node, which breaks phandle DAI resolution
  • Registering the card before all components have probed — use -EPROBE_DEFER handling

Best Practices

Prefer devm_snd_soc_register_card() over the older manual register/unregister pair so cleanup is automatic on probe failure. Keep dai_fmt and clocking flags in one place per link rather than scattering them across the CPU and codec drivers. For security and robustness, always validate that phandles resolved successfully before touching the component arrays, since a malformed device tree should fail probe cleanly rather than dereference a NULL node.

Summary and Key Takeaways

Modern snd_soc_dai_link represents CPU, codec and platform components as arrays of snd_soc_dai_link_component, not flat name fields. The SND_SOC_DAILINK_DEFS/SND_SOC_DAILINK_REG macro pair is the standard way to declare these arrays, and of_parse_phandle() is the standard way a board’s machine driver resolves the CPU and codec device tree nodes it needs to plug into those arrays at probe time. With this pattern understood, you’re ready to move on to DAPM-based routing at the machine level in the next lecture of this free linux kernel development course.

Frequently Asked Questions

What replaced cpu_dai_name and codec_dai_name in modern kernels?

The cpus[] and codecs[] arrays of struct snd_soc_dai_link_component, each entry carrying its own dai_name field.

Can a single DAI link have more than one codec?

Yes — that is exactly why the component-array model replaced the old single-codec fields; num_codecs can be greater than one.

Do I still choose between name and of_node per component?

Yes, that rule from the legacy API still holds at the component level — set one or the other, never both, for each cpus/codecs/platforms entry.

Why use of_parse_phandle instead of hardcoding device names?

Hardcoded names tie the driver to one board’s device naming. Phandle resolution lets the same machine driver source work across any board whose device tree provides the expected phandle properties.

What does COMP_EMPTY() do?

It reserves a component array slot with no name, dai_name, or of_node set, meant to be filled in programmatically during probe — which is exactly how our demo driver patches in the resolved phandle nodes.

Is devm_snd_soc_register_card() required?

It is not strictly required, but it is best practice on a modern kernel because it ties card lifetime to the device and handles cleanup automatically on probe failure or removal.

What happens if #sound-dai-cells is missing from a DT node?

Phandle-based DAI lookups that depend on that property will fail, so the machine driver will not be able to resolve the CPU or codec DAI correctly.

Where can I practice this for free?

This lecture is part of EmbeddedPathashala’s free linux device drivers course — continue with the next lecture on machine-level DAPM routing.

Continue the Free Linux Kernel Development Course

Next up: machine-level audio routing with DAPM.

Next Lecture Course Index

Leave a Reply

Your email address will not be published. Required fields are marked *