ALSA DAI Callback Classes Explained-Free Linux Device Drivers Course
Free Linux Kernel Development Course — ALSA SoC Framework, Lecture 4
free linux device drivers course
free embedded linux course
ALSA SoC framework
snd_soc_dai_ops
If you are following our free linux kernel development course, you already know that a codec driver in the ALSA SoC (ASoC) framework exposes its behaviour through a table of function pointers called snd_soc_dai_ops. In the previous lecture of this free linux device drivers course we went through every single callback in that table one by one. What we did not do yet is explain why the kernel developers organised these callbacks the way they did, and what happens internally when ALSA moves a PCM stream from silence to sound and back. That is exactly what this lecture, part of our free embedded linux course, is going to fix — and along the way we will also open the door to ASoC controls, the mechanism that lets userspace tweak volume, mute, and routing on a real codec.
What You Will Learn
- Why the ASoC framework groups DAI callbacks into three logical classes
- The PCM common state machine (off, standby, prepare, on) and which callback fires at each transition
- How to describe playback/capture hardware capability with
struct snd_soc_pcm_stream, including the modernsubformatsfield - A complete, original demo codec driver that groups its callbacks by class and declares its stream capability
- Building, loading, and testing the driver with
aplay/arecordand reading the resultingdmesgtrace - A first look at ASoC controls:
snd_kcontrol_new, regmap-backed register access, and the four control types
Prerequisites
- Lecture 3 of this series — the individual
snd_soc_dai_opscallbacks (set_sysclk,set_fmt,trigger, and so on) - Basic ALSA driver terminology — DAI, machine driver, platform driver, codec/component driver
- Comfort building and loading out-of-tree kernel modules
Why Group DAI Callbacks Into Classes?
The snd_soc_dai_ops table can hold more than a dozen function pointers. Reading them as one flat list makes it hard to reason about who calls what and when. The ASoC maintainers effectively split them into three functional classes based on the caller and the purpose of the call. Once you see the split, the whole table stops looking like a random collection of hooks and starts looking like three small, purposeful APIs.
Three Classes of DAI Callbacks
1. Clock Configuration
set_sysclk, set_pll, set_clkdiv
Called once by the machine driver to feed the DAI its master/bit/frame clocks.
2. Format Configuration
set_fmt, set_tdm_slot, set_channel_map, set_tristate
Called by the machine driver to describe the wire protocol and slot layout of the DAI.
3. PCM Stream Operations
startup, shutdown, hw_params, hw_free, prepare, trigger, mute_stream
Called by the ASoC core itself, on every open/configure/start/stop of a PCM stream.
Classes 1 and 2 are configuration-time calls: the machine driver invokes them once, typically from its own hw_params callback, to tell the DAI how it is wired to the rest of the audio path. Class 3 is different — the ASoC core drives these automatically as a PCM substream moves through its lifecycle, which is exactly the state machine we cover next.
The PCM Common State Machine
Every PCM substream in ALSA — whether playback or capture — moves through four states: off, standby, prepare, and on. The ASoC core walks this state machine on every open, configure, start, stop, and suspend/resume cycle, and each transition invokes one or more callbacks from the third class above.
PCM State Transitions and Their Callbacks
startup → hw_params → prepare → trigger(START))trigger(STOP))prepare → trigger(RESUME))hw_free → shutdown)Understanding this diagram is the single most useful thing you can take from this lecture: whenever your driver misbehaves at a particular point (silence on first playback, a glitch after resume, a leak after close), the state machine tells you exactly which callback to put your pr_debug() statements in.
Describing Stream Capability: struct snd_soc_pcm_stream
Every DAI advertises what it can actually do — sample formats, sample rates, channel counts — separately for its playback direction and its capture direction. This is done by filling one struct snd_soc_pcm_stream per direction inside the snd_soc_dai_driver. On current kernels the structure looks like this:
struct snd_soc_pcm_stream {
const char *stream_name;
u64 formats;
u32 subformats; /* newer field: sub-format bitmask, e.g. MSBITS */
unsigned int rates;
unsigned int rate_min;
unsigned int rate_max;
unsigned int channels_min;
unsigned int channels_max;
unsigned int sig_bits;
};
The subformats field is the one thing here that did not exist in older kernel releases — it lets a driver be precise about container-vs-sample-width combinations (for example 24-bit audio packed inside a 32-bit container) instead of only being able to say “S24_LE” at the format level. Everything else keeps the meaning you would expect: formats is an SNDRV_PCM_FMTBIT_* bitmask, rates is an SNDRV_PCM_RATE_* bitmask, and the _min/_max pairs bound channel count and sample rate for drivers that support a continuous range rather than a fixed set.
Original Demo: A Minimal Codec DAI With Grouped Callbacks
Let’s put both ideas — callback classes and stream capability — into one small, original codec driver. This driver is a software-only stub (it does not talk to real hardware), so you can build and load it on any x86 development machine without special hardware. It is intentionally named with the ep_ prefix so you never confuse it with a vendor driver from any book or datasheet.
// ep_dummy_codec.c — EmbeddedPathashala demo ALSA codec driver
#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/soc.h>
#include <sound/pcm.h>
static int ep_set_sysclk(struct snd_soc_dai *dai, int clk_id,
unsigned int freq, int dir)
{
dev_info(dai->dev, "ep_codec: sysclk requested = %u Hz\n", freq);
return 0;
}
static int ep_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
dev_info(dai->dev, "ep_codec: dai format = 0x%x\n", fmt);
return 0;
}
static int ep_startup(struct snd_pcm_substream *substream,
struct snd_soc_dai *dai)
{
dev_info(dai->dev, "ep_codec: startup, stream=%s\n",
substream->stream == SNDRV_PCM_STREAM_PLAYBACK ?
"playback" : "capture");
return 0;
}
static int ep_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
dev_info(dai->dev, "ep_codec: hw_params, rate=%u channels=%u\n",
params_rate(params), params_channels(params));
return 0;
}
static int ep_trigger(struct snd_pcm_substream *substream, int cmd,
struct snd_soc_dai *dai)
{
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
dev_info(dai->dev, "ep_codec: trigger START\n");
break;
case SNDRV_PCM_TRIGGER_STOP:
dev_info(dai->dev, "ep_codec: trigger STOP\n");
break;
default:
break;
}
return 0;
}
static void ep_shutdown(struct snd_pcm_substream *substream,
struct snd_soc_dai *dai)
{
dev_info(dai->dev, "ep_codec: shutdown\n");
}
/* Callbacks grouped by class, exactly as explained above */
static const struct snd_soc_dai_ops ep_dai_ops = {
/* class 1: clock configuration */
.set_sysclk = ep_set_sysclk,
/* class 2: format configuration */
.set_fmt = ep_set_fmt,
/* class 3: PCM stream operations */
.startup = ep_startup,
.hw_params = ep_hw_params,
.trigger = ep_trigger,
.shutdown = ep_shutdown,
};
static struct snd_soc_dai_driver ep_dai_driver = {
.name = "ep-dummy-dai",
.playback = {
.stream_name = "Playback",
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 1,
.channels_max = 2,
},
.capture = {
.stream_name = "Capture",
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 1,
.channels_max = 2,
},
.ops = &ep_dai_ops,
};
static const struct snd_soc_component_driver ep_component_driver = {
.name = "ep-dummy-codec",
};
static int ep_codec_probe(struct platform_device *pdev)
{
return devm_snd_soc_register_component(&pdev->dev, &ep_component_driver,
&ep_dai_driver, 1);
}
static struct platform_driver ep_codec_platform_driver = {
.driver = {
.name = "ep-dummy-codec",
},
.probe = ep_codec_probe,
};
module_platform_driver(ep_codec_platform_driver);
MODULE_DESCRIPTION("EmbeddedPathashala demo ALSA SoC codec driver");
MODULE_LICENSE("GPL");
Notice how the ep_dai_ops table is written and commented in exactly the three groups we discussed — that grouping is a habit worth keeping in your own drivers, purely for readability. It costs nothing at compile time and saves real time during debugging.
Build and Run Walkthrough
To try this on a development machine, register a matching platform device (or bind through Device Tree/ACPI on real hardware) and build the module against your running kernel headers:
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_dummy_codec.ko
$ dmesg | tail -n 5
Once the component is registered and bound into a sound card by a machine driver, playing a file will walk the state machine from off all the way to on, and each hop is visible in the kernel log:
$ aplay -D hw:0,0 test.wav
[ 102.441823] ep_codec: startup, stream=playback
[ 102.441901] ep_codec: dai format = 0x4002
[ 102.442015] ep_codec: sysclk requested = 12288000 Hz
[ 102.443220] ep_codec: hw_params, rate=48000 channels=2
[ 102.443980] ep_codec: trigger START
[ 105.201122] ep_codec: trigger STOP
[ 105.201400] ep_codec: shutdown
This trace is the state machine made visible: startup and hw_params take you from off to prepare, trigger START takes you to on, trigger STOP drops you back to standby, and shutdown returns you to off when the application closes the device node.
A First Look at ASoC Controls
So far every callback we have covered configures the data path — clocks, formats, PCM state. Real codecs also need a way for userspace to change things like output volume or input mute while the stream is running, without tearing anything down. That is the job of ASoC controls.
Every control the codec exposes to userspace is described by one struct snd_kcontrol_new instance and registered with the ALSA core when the component probes. Under the hood, most controls end up reading and writing hardware registers through the component’s regmap, using a small set of helper functions instead of talking to I2C/SPI directly:
int snd_soc_component_write(struct snd_soc_component *component,
unsigned int reg, unsigned int val);
unsigned int snd_soc_component_read(struct snd_soc_component *component,
unsigned int reg);
int snd_soc_component_update_bits(struct snd_soc_component *component,
unsigned int reg,
unsigned int mask, unsigned int val);
int snd_soc_component_test_bits(struct snd_soc_component *component,
unsigned int reg,
unsigned int mask, unsigned int value);
One detail worth flagging if you are cross-referencing older material: on modern kernels snd_soc_component_read() returns the register value directly as its return value, instead of writing it through an output pointer argument the way some very old ASoC code used to. Always check the signature in your own kernel tree with grep -R "snd_soc_component_read" include/sound/ before assuming either style.
There are four broad control shapes you will meet as you build codec drivers:
- Simple switch — one on/off bit in one register (e.g. a mute bit)
- Stereo control — the same switch duplicated for left and right channels
- Mixer control — combines several simple inputs into one summed output
- MUX control — like a mixer, but selects exactly one input among many rather than summing them
We will build real examples of each of these control types, field by field through struct snd_kcontrol_new, in the next lecture of this free linux device drivers course.
Common Mistakes
- Putting clock/format setup logic inside
hw_paramsinstead of the dedicated class-1/class-2 callbacks — it works by accident but breaks the moment the machine driver needs to reconfigure clocks independently of stream parameters - Forgetting that
triggerruns in atomic/interrupt context on many platforms — never call sleeping APIs (like blocking regmap I/O over a slow bus) directly from it - Leaving
channels_min/channels_maxorrate_min/rate_maxunset, which silently allows userspace to request configurations your hardware cannot actually produce - Assuming
snd_soc_component_read()has an output-pointer signature because an old book or blog post shows it that way
Best Practices
- Keep the three callback classes visually grouped and commented in your driver source, exactly as shown in the demo above
- Log every PCM state transition during development, then strip the logging back to
dev_dbg()once the driver is stable - Always double-check current struct layouts (like the
subformatsfield) against your actual kernel tree rather than trusting older reference material - Prefer the
snd_soc_component_*regmap-backed helpers over hand-rolled I2C/SPI calls for control I/O — they give you locking and caching for free
Summary and Key Takeaways
In this lecture of our free linux kernel development course we reorganised what you already knew about snd_soc_dai_ops into three functional classes — clock configuration, format configuration, and PCM stream operations — and connected the third class directly to ALSA’s off/standby/prepare/on state machine. We then filled in struct snd_soc_pcm_stream for both playback and capture in a complete, original demo driver, built and ran it, and read the resulting trace against the state diagram. Finally we opened the door to ASoC controls: snd_kcontrol_new, the regmap-backed snd_soc_component_* helpers, and the four control shapes — switch, stereo, mixer, and MUX — that the next lecture will implement in full.
Frequently Asked Questions
Why does ASoC split DAI callbacks into three classes instead of one flat list?
Because the caller and timing differ: clock/format callbacks are configuration-time calls made once by the machine driver, while PCM stream callbacks are driven automatically by the ASoC core on every open, start, stop, and close of a substream.
Which callback fires when I first open an ALSA playback device?
The sequence is startup, then hw_params, then prepare, then trigger with SNDRV_PCM_TRIGGER_START — moving the stream from off through standby and prepare into the on state.
What is the difference between hw_params and prepare?
hw_params configures the stream’s format, rate, and channel count for the current open; prepare is called every time the stream is about to start (including after a stop/restart) and sets up DMA transfer parameters based on those already-negotiated parameters.
What is the subformats field in struct snd_soc_pcm_stream?
It is a newer bitmask field that lets a DAI describe sub-format details — such as sample bit-width packed inside a wider container — more precisely than the formats field alone can express.
Does trigger run in interrupt context?
On many platforms, yes — the ALSA core can call trigger from atomic context, so it must never block or sleep; any slow hardware access should be prepared ahead of time in prepare or hw_params.
What does snd_soc_component_read() return on current kernels?
It returns the register value directly as an unsigned int return value. Older reference material sometimes shows an output-pointer style signature — always verify against your own kernel tree.
What is the difference between a mixer control and a MUX control?
A mixer control sums multiple enabled inputs into one output, while a MUX control selects exactly one input among several — think “combine” versus “choose.”
Do I need regmap to implement ASoC controls?
Not strictly, but it is strongly recommended — regmap gives the snd_soc_component_* helpers built-in locking and optional register caching, which saves you from writing that plumbing yourself.
Continue This Free Linux Device Drivers Course
Next up: implementing switch, stereo, mixer, and MUX controls with struct snd_kcontrol_new, field by field, in a real codec driver.
