ALSA SoC Volume Controls and DAPM-Free Linux Device Drivers Course
Lecture 6 of the ASoC (ALSA System on Chip) Framework module — part of Embeddedpathashala’s free Linux kernel development course
In the previous lecture of this free Linux kernel development course we built our first
snd_kcontrol_new array by hand, wired up a TLV (Threshold Limit Value) table, and registered
it with snd_soc_add_component_controls(). In this lecture we go one level deeper: we will
build every practical control macro an ASoC codec driver actually ships — plain switches, dB-aware
volume controls, stereo controls, mixer controls, and enumerated controls — using a brand new
imaginary codec, ep_codec, so every register bit and every number in this lecture is
original and traceable back to first principles rather than copied from any datasheet. We will close
by introducing DAPM (Dynamic Audio Power Management), the layer that sits on top of
kcontrols and finally gives the ASoC core the power-awareness that plain kcontrols cannot provide on
their own.
Series Keywords
free linux device drivers course
free embedded linux course
ALSA SoC framework
DAPM widgets
What You Will Learn
- How
SOC_SINGLEturns one register bit into a userspace ALSA switch - How TLV (dB) metadata makes
SOC_SINGLE_TLVreport real decibel values toamixer - The difference between
SOC_DOUBLEandSOC_DOUBLE_Rfor stereo controls - How to build a mixer control array that routes multiple inputs into one output
- The modern, single-line
SOC_ENUM_SINGLE_DECLmacro for enumerated controls - Why plain kcontrols cannot manage power on their own, and how DAPM widgets solve that gap
- How to verify every control you write using
amixeron real or QEMU-emulated hardware
Prerequisites
This lecture assumes you have already gone through the earlier lectures of this free linux device
drivers course covering the ASoC machine/platform/codec split, struct snd_soc_component_driver,
and the basic anatomy of struct snd_kcontrol_new. You should be comfortable cross-compiling
and loading a kernel module, and have alsa-utils (amixer, aplay)
available on your target board or a QEMU ALSA-capable rootfs.
Why Control Macros Exist at All
Every ALSA mixer control that a user sees in amixer scontrols is, underneath, a
struct snd_kcontrol_new populated with an .info, .get, and
.put callback plus a packed private_value describing which register and which
bits the control touches. Writing that structure out by hand for every single switch in a codec driver
is repetitive and error-prone, so the ASoC core ships a family of macros that expand into a fully-formed
snd_kcontrol_new entry from a handful of arguments. Understanding what each macro expands to
is far more valuable than memorising its argument order, because it tells you exactly which generic
snd_soc_get_volsw / snd_soc_put_volsw style callback will run when userspace
touches that control through regmap.
SOC_SINGLE — The Plain Register-Bit Switch
The simplest control macro exposes exactly one bit of one register as an on/off switch. Internally it
expands to a snd_kcontrol_new entry whose .info callback is
snd_soc_info_volsw and whose .get/.put callbacks are
snd_soc_get_volsw/snd_soc_put_volsw — both of which internally call
regmap_update_bits() against the component’s regmap, so as a driver author you never touch
raw I2C/SPI transfers yourself.
/* ep_codec.h - register map (original, not from any real datasheet) */
#define EP_CODEC_OUTPUT_CTRL 0x12
#define EP_CODEC_INPUT_CTRL 0x13
#define EP_CODEC_MIXER_CTRL 0x14
#define EP_CODEC_OUT_VOL 0x15
#define EP_CODEC_LDAC_VOL 0x16
#define EP_CODEC_RDAC_VOL 0x17
#define EP_CODEC_LOUT_CTRL 0x18
#define EP_CODEC_ROUT_CTRL 0x19
/* ep_codec.c - a plain switch control */
static const struct snd_kcontrol_new ep_codec_snd_controls[] = {
SOC_SINGLE("EP Codec High Pass Filter Switch",
EP_CODEC_OUTPUT_CTRL, 3, 1, 0),
};
Here bit 3 of EP_CODEC_OUTPUT_CTRL is treated as a one-bit field
(max = 1) that is not inverted (invert = 0). Once this array is registered,
running amixer sset 'EP Codec High Pass Filter Switch' on in userspace results in a single
regmap bit-field write to register 0x12 — nothing more.
SOC_SINGLE_TLV — Giving a Control Real Decibel Meaning
A plain SOC_SINGLE control only knows a raw integer range. For a volume control that raw
range is meaningless to a user unless it is mapped to decibels. TLV (Threshold Limit Value) metadata is
what lets amixer print -46.50dB instead of a bare register value. The mapping
is declared separately from the control itself:
/* Each step is 0.5 dB (150 in 1/100ths of a dB), 32 steps (5-bit field),
* range runs from -46.50 dB up to 0 dB:
* -4650 + (150 * 31) = 0
*/
static const DECLARE_TLV_DB_SCALE(ep_out_tlv, -4650, 150, 0);
static const struct snd_kcontrol_new ep_codec_snd_controls[] = {
SOC_SINGLE("EP Codec High Pass Filter Switch",
EP_CODEC_OUTPUT_CTRL, 3, 1, 0),
SOC_SINGLE_TLV("EP Codec Output Volume",
EP_CODEC_OUT_VOL, 0, 31, 0, ep_out_tlv),
};
The three arguments to DECLARE_TLV_DB_SCALE are the starting value in hundredths of a
decibel, the fixed per-step size in hundredths of a decibel, and a mute flag — set that flag to 1 instead
of 0 when step 0 of the control means “muted” rather than “quietest audible level”, which shifts where the
scale actually starts from the userspace point of view.
SOC_DOUBLE and SOC_DOUBLE_R — Stereo In One Control
Real codecs almost always need a left/right pair. ASoC gives you two macros depending on how the
hardware laid out the two channels:
| Macro | Left/Right storage | When to use it |
|---|---|---|
SOC_DOUBLE |
Two bit-fields inside the same register | Codec packs both channels into one register |
SOC_DOUBLE_R |
Two separate registers, one per channel | Codec gives each channel its own register (more common on real silicon) |
/* Both channels muted from the same bit position (bit 4) in two
* separate registers - a very common real-world layout. */
static const struct snd_kcontrol_new ep_codec_snd_controls[] = {
SOC_SINGLE("EP Codec High Pass Filter Switch",
EP_CODEC_OUTPUT_CTRL, 3, 1, 0),
SOC_SINGLE_TLV("EP Codec Output Volume",
EP_CODEC_OUT_VOL, 0, 31, 0, ep_out_tlv),
SOC_DOUBLE_R("EP Codec DAC Mute Switch",
EP_CODEC_LOUT_CTRL, EP_CODEC_ROUT_CTRL, 4, 1, 0),
};
And when the two channels each need dB-scaled volume rather than a plain switch, reach for
SOC_DOUBLE_R_TLV, the stereo counterpart of SOC_SINGLE_TLV:
/* 128 steps, 0.5 dB each, first step (0) means mute -> last argument is 1 */
static const DECLARE_TLV_DB_SCALE(ep_dac_tlv, -6350, 50, 1);
SOC_DOUBLE_R_TLV("EP Codec DAC Playback Volume",
EP_CODEC_LDAC_VOL, EP_CODEC_RDAC_VOL,
0, 127, 0, ep_dac_tlv),
Mixer Controls — Many Inputs, One Output
A mixer control is not one macro but an array of SOC_SINGLE entries, one bit per
input source, all living in a single register. Userspace does not see this as one widget yet — at the pure
kcontrol level it is just a group of independent switches that happen to share a register:
static const struct snd_kcontrol_new ep_output_mixer_controls[] = {
SOC_SINGLE("DAC Playback Switch", EP_CODEC_MIXER_CTRL, 0, 1, 0),
SOC_SINGLE("Line In Switch", EP_CODEC_MIXER_CTRL, 1, 1, 0),
SOC_SINGLE("Mic Boost Switch", EP_CODEC_MIXER_CTRL, 2, 1, 0),
};
Bits 0, 1 and 2 of EP_CODEC_MIXER_CTRL each independently gate one input path into the
shared output summing node. Later in this lecture you will see why this exact array is what gets handed
to a DAPM mixer widget instead of being registered directly as a top-level control.
SOC_ENUM_SINGLE_DECL — The Modern Enumerated Control
Older ASoC code split an enumerated control into two steps: declare a struct soc_enum
with SOC_ENUM_SINGLE, then wrap it a second time with SOC_ENUM("name", enum_var)
inside the kcontrol array. Current kernels give you a single combined macro,
SOC_ENUM_SINGLE_DECL, which declares the backing struct soc_enum and its text
array reference in one line — fewer places for the register/shift/mask to drift out of sync:
static const char * const ep_input_select_text[] = {
"Mic", "Line In", "Digital Mic",
};
static SOC_ENUM_SINGLE_DECL(ep_input_select_enum,
EP_CODEC_INPUT_CTRL, 4,
ep_input_select_text);
static const struct snd_kcontrol_new ep_codec_snd_controls[] = {
SOC_SINGLE("EP Codec High Pass Filter Switch",
EP_CODEC_OUTPUT_CTRL, 3, 1, 0),
SOC_SINGLE_TLV("EP Codec Output Volume",
EP_CODEC_OUT_VOL, 0, 31, 0, ep_out_tlv),
SOC_DOUBLE_R("EP Codec DAC Mute Switch",
EP_CODEC_LOUT_CTRL, EP_CODEC_ROUT_CTRL, 4, 1, 0),
SOC_DOUBLE_R_TLV("EP Codec DAC Playback Volume",
EP_CODEC_LDAC_VOL, EP_CODEC_RDAC_VOL,
0, 127, 0, ep_dac_tlv),
SOC_ENUM("EP Codec Input Select", ep_input_select_enum),
};
Registering the Array the Modern Way
Manually calling snd_soc_add_component_controls() from your .probe callback
still works, but current driver style prefers letting the ASoC core register the array automatically by
pointing .controls/.num_controls in struct snd_soc_component_driver
directly at it — one less thing to forget in probe:
static const struct snd_soc_component_driver ep_codec_component_driver = {
.controls = ep_codec_snd_controls,
.num_controls = ARRAY_SIZE(ep_codec_snd_controls),
.name = "ep_codec",
};
static int ep_codec_probe(struct i2c_client *i2c)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(i2c, &ep_codec_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
return devm_snd_soc_register_component(&i2c->dev,
&ep_codec_component_driver,
NULL, 0);
}
Build and Verify on Target
Once the module is cross-compiled against your kernel headers and loaded, the new controls should
appear immediately in the ALSA control interface:
$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_codec.ko
$ dmesg | tail -n 4
[ 41.220117] ep_codec 1-001a: EP Codec component registered
[ 41.220481] ep_codec 1-001a: 5 controls added
$ amixer scontrols
Simple mixer control 'EP Codec High Pass Filter Switch',0
Simple mixer control 'EP Codec Output Volume',0
Simple mixer control 'EP Codec DAC Mute Switch',0
Simple mixer control 'EP Codec DAC Playback Volume',0
Simple mixer control 'EP Codec Input Select',0
$ amixer sset 'EP Codec Output Volume' -- -20dB
Simple mixer control 'EP Codec Output Volume',0
Limits: Playback -46.50dB - 0.00dB
Front Left: Playback 18 [58%] [-20.00dB]
Front Right: Playback 18 [58%] [-20.00dB]
Notice that amixer prints real decibel values and limits automatically — that entire
output comes from the TLV metadata you declared, not from anything you had to print yourself.
Why Kcontrols Alone Are Not Enough
Everything above gives userspace a way to change register values. None of it tells the kernel
anything about power. A kcontrol has no concept of “this switch, when off, means an entire amplifier block
downstream can be powered down.” On a battery-powered embedded audio device that gap matters: leaving an
unused DAC or amplifier permanently powered wastes real, measurable current. Four concrete gaps show up
once you try to use kcontrols for power management directly:
- No connection awareness — a kcontrol only knows its own register; it cannot
describe that it sits between a DAC and a speaker amplifier. - No power hooks — there is no callback that fires “power this block on” or
“power this block off” as playback starts and stops. - No sequencing — nothing enforces the order blocks must power up/down in to avoid
audible pop/click artifacts. - Manual teardown — when an audio path becomes invalid, nothing automatically
tears it down; userspace would have to do it by hand every time.
DAPM: Widgets as an Upgrade Over Kcontrols
Dynamic Audio Power Management lives inside the ASoC core itself, which means the power-switching
decisions are made in-kernel and are completely transparent to userspace — an application just opens a
PCM device and plays audio; it never has to know which internal blocks got powered on to make that
possible. DAPM’s basic unit is the widget. A widget wraps a kcontrol (or several) together
with power-domain awareness and a place in the audio signal graph, so the core can reason about it as one
node that has neighbours, not an isolated register bit.
→
register bit read/write only
→
kcontrol + power state + graph position
The three things every DAPM widget adds on top of a raw kcontrol are: a node in the audio
graph (so the core knows what feeds into it and what it feeds out to), a power state
machine (so the core can walk the graph on every stream start/stop and power exactly the blocks
that are actually in use), and a sequencing rule that governs the order neighbouring
widgets are powered to avoid the classic pop/click most people have heard from cheap audio hardware.
DAC widget →
Output Mixer widget →
Speaker Output widget
Only the widgets on an active path like this one get powered on;
every other widget in the codec stays off until a stream needs it.
The mixer array you wrote earlier in this lecture,
ep_output_mixer_controls[], is exactly what gets handed to a DAPM mixer widget instead of
being registered as a standalone top-level control — this is the connecting thread between the two halves
of this lecture, and it is exactly where the next lecture in this free linux kernel development course
picks up: writing SND_SOC_DAPM_* widget macros and connecting them with
snd_soc_dapm_route entries.
Common Mistakes and Troubleshooting
- Wrong TLV step size units —
DECLARE_TLV_DB_SCALEtakes hundredths
of a dB, not tenths. A 0.5 dB step is50, not5. - max off by the mute step — if step 0 means mute, your usable range and the
mute flag inDECLARE_TLV_DB_SCALEneed to agree, oramixerwill print a
misleading dB floor. - Registering a mixer’s sub-controls twice — once a control array is handed to a
DAPM mixer widget, do not also list it in the component’s top-level.controlsarray; the
two will fight over the same regmap bits and confuse userspace. - Forgetting
ARRAY_SIZE()— a hardcoded control count that drifts
from the actual array size silently drops or overruns controls at registration time.
Best Practices
- Keep every register/bit definition in a private header, never inline magic numbers in the
control array itself. - Prefer
SOC_ENUM_SINGLE_DECLover the older two-step enum declaration in new code. - Let
.controls/.num_controlson the component driver do registration
instead of a manual call in.probe, unless you genuinely need conditional registration. - Design mixer sub-control arrays with DAPM in mind from the start — it avoids a rewrite once you
add widgets in the next lecture.
Summary and Key Takeaways
SOC_SINGLEexposes one register bit as a plain switch.SOC_SINGLE_TLVadds dB metadata soamixerreports real decibel values.SOC_DOUBLE_R/SOC_DOUBLE_R_TLVhandle stereo pairs stored in separate
registers, the layout most real codecs use.- Mixer arrays are just grouped
SOC_SINGLEentries that later feed a DAPM mixer widget. SOC_ENUM_SINGLE_DECLis the modern one-line way to declare enumerated controls.- Plain kcontrols cannot manage power, sequencing, or graph connectivity — DAPM widgets exist to
add exactly those three things on top of the kcontrols you already know how to write.
That closes the control-macro half of the ASoC framework module in this free linux kernel development
course. You now have every building block needed to expose a codec’s switches, volumes, and selectors to
userspace correctly, and you understand precisely why the ASoC core still needs a further abstraction —
DAPM widgets — to make power management automatic rather than manual. The next lecture builds directly
on the ep_output_mixer_controls[] array from this one to construct real
SND_SOC_DAPM_* widgets and wire them together with routes.
Frequently Asked Questions
What is the difference between SOC_DOUBLE and SOC_DOUBLE_R?
SOC_DOUBLE packs both channels into bit-fields of a single register, while
SOC_DOUBLE_R uses two separate registers, one per channel — the layout most real-world
codecs actually use.
Why does amixer show dB values for some controls but not others?
Only controls built with a _TLV macro and a DECLARE_TLV_DB_SCALE table
carry dB metadata. Plain SOC_SINGLE/SOC_DOUBLE_R switches only report raw
integer values.
Is snd_soc_add_component_controls() still valid in current kernels?
Yes, it still works, but pointing .controls/.num_controls directly on
struct snd_soc_component_driver is the more common modern pattern since the core handles
registration for you automatically.
Can a mixer control array be registered both directly and via a DAPM widget?
No — doing both causes two consumers to fight over the same regmap bits. Once an array feeds a
DAPM mixer widget, it should not also appear in the top-level .controls array.
What problem does DAPM solve that plain kcontrols cannot?
DAPM adds graph connectivity, automatic power state tracking, and power sequencing — none of
which a plain kcontrol has any concept of.
What units does DECLARE_TLV_DB_SCALE expect?
Both the starting value and the step size are given in hundredths of a decibel — a 0.5 dB step is
written as 50.
Is DAPM specific to embedded Linux, or used on desktop audio too?
DAPM lives in the mainline ASoC core and is used across embedded SoC audio, but its power-saving
motivation is most visible on battery-powered embedded and mobile devices.
Does SOC_ENUM_SINGLE_DECL replace SOC_ENUM_SINGLE entirely?
SOC_ENUM_SINGLE still exists and is used internally, but new driver code should
prefer the combined SOC_ENUM_SINGLE_DECL to declare the enum in one line.
Continue the Free Linux Kernel Development Course
Next up: writing real DAPM widget macros and routes to complete the power-aware audio path you
started building in this lecture.
