ASoC DAPM Architecture: Widget Registration, Path Construction, and Route Management

Core Concepts

ASoC Dynamic Audio Power Management (DAPM) organizes audio hardware into power-controllable units called widgets, which are interconnected to form signal paths. These connections determine how power states propagate across the audio subsystem during stream activity changes.

Each widget belongs to a DAPM context, representing a logical power domain. Contexts align with hardware boundaries:

  • One per codec component
  • One per platform component
  • One per machine (sound card)

The snd_soc_dapm_context structure encapsulates bias level management, event callbacks, and membership in the sound card’s global DAPM infrastructure:

struct snd_soc_dapm_context {
	enum snd_soc_bias_level bias_level;
	unsigned int idle_bias_off:1;
	unsigned int suspend_bias_off:1;
	void (*seq_notifier)(struct snd_soc_dapm_context *,
			     enum snd_soc_dapm_type, int);
	struct device *dev;
	struct snd_soc_component *component;
	struct snd_soc_card *card;
	enum snd_soc_bias_level target_bias_level;
	struct list_head list;
	int (*stream_event)(struct snd_soc_dapm_context *, int);
	int (*set_bias_level)(struct snd_soc_dapm_context *,
			      enum snd_soc_bias_level);
	struct snd_soc_dapm_wcache path_sink_cache;
	struct snd_soc_dapm_wcache path_source_cache;
#ifdef CONFIG_DEBUG_FS
	struct dentry *debugfs_dapm;
#endif
};

Bias levels define operational states:

  • SND_SOC_BIAS_OFF: Full power-down
  • SND_SOC_BIAS_STANDBY: Low-power idle, fast wake-up (<10ms)
  • SND_SOC_BIAS_PREPARE: Pre-stream setup
  • SND_SOC_BIAS_ON: Active operation

Widgets reference their context via w->dapm, and all contexts link into card->dapm_list for unified traversal.

Widget Registration Mechanisms

Widgets are instantiated from driver-defined templates using snd_soc_dapm_new_controls(). This function iterates over an array of snd_soc_dapm_widget descriptors and delegates creation to snd_soc_dapm_new_control_unlocked().

Explicit Widget Creation

Drivers declare static widget arrays—commonly using macros like SOC_DAPM_INPUT() or SOC_DAPM_SPK()—and register them at initialization time:

  • Codec & Platform: Registered via component->driver->dapm_widgets or component->driver->probe() during soc_probe_link_components()
  • Machine: Registered directly on card->dapm after snd_card_new()

The core allocation logic resides in snd_soc_dapm_new_control_unlocked():

struct snd_soc_dapm_widget *
snd_soc_dapm_new_control_unlocked(struct snd_soc_dapm_context *dapm,
				  const struct snd_soc_dapm_widget *template)
{
	struct snd_soc_dapm_widget *w;

	w = dapm_cnew_widget(template);
	if (!w)
		return ERR_PTR(-ENOMEM);

	// Handle special widget types (regulator, clock, pinctrl)
	switch (w->id) {
	case snd_soc_dapm_regulator_supply:
		w->regulator = devm_regulator_get(dapm->dev, w->name);
		break;
	case snd_soc_dapm_clock_supply:
		w->clk = devm_clk_get(dapm->dev, w->name);
		break;
	// ...
	}

	// Apply name prefix if present
	const char *prefix = soc_dapm_prefix(dapm);
	if (prefix)
		w->name = kasprintf(GFP_KERNEL, "%s %s", prefix, template->name);
	else
		w->name = kstrdup_const(template->name, GFP_KERNEL);

	// Assign endpoint and power-check behavior
	switch (w->id) {
	case snd_soc_dapm_mic:
		w->is_ep = SND_SOC_DAPM_EP_SOURCE;
		w->power_check = dapm_generic_check_power;
		break;
	case snd_soc_dapm_spk:
	case snd_soc_dapm_hp:
		w->is_ep = SND_SOC_DAPM_EP_SINK;
		w->power_check = dapm_generic_check_power;
		break;
	case snd_soc_dapm_supply:
	case snd_soc_dapm_regulator_supply:
		w->is_supply = 1;
		w->power_check = dapm_supply_check_power;
		break;
	// ...
	}

	w->dapm = dapm;
	INIT_LIST_HEAD(&w->list);
	INIT_LIST_HEAD(&w->dirty);
	list_add_tail(&w->list, &dapm->card->widgets);

	for_each_dapm_direction(dir) {
		INIT_LIST_HEAD(&w->edges[dir]);
		w->endpoints[dir] = -1;
	}

	w->connected = 1;
	return w;
}

Key outcomes:

  • Memory is allocated and template data copied
  • Endpoint flags (is_ep) indicate signal flow direction (source/sink)
  • power_check callbacks govern dynamic power decisions
  • Widgets are inserted into card->widgets for later enumeration

DAI Widget Generation

Digital Audio Interface (DAI) widgets are generated dynamically during component registration. Each DAI contributes two widgets:

  • snd_soc_dapm_dai_in: For playback streams
  • snd_soc_dapm_dai_out: For capture streams

These are created by snd_soc_dapm_new_dai_widgets():

int snd_soc_dapm_new_dai_widgets(struct snd_soc_dapm_context *dapm,
				 struct snd_soc_dai *dai)
{
	struct snd_soc_dapm_widget template = { .reg = SND_SOC_NOPM };
	struct snd_soc_dapm_widget *w;

	if (dai->driver->playback.stream_name) {
		template.id = snd_soc_dapm_dai_in;
		template.name = dai->driver->playback.stream_name;
		template.sname = template.name;

		w = snd_soc_dapm_new_control_unlocked(dapm, &template);
		if (IS_ERR(w))
			return PTR_ERR(w);

		w->priv = dai;
		dai->playback_widget = w;
	}

	if (dai->driver->capture.stream_name) {
		template.id = snd_soc_dapm_dai_out;
		template.name = dai->driver->capture.stream_name;
		template.sname = template.name;

		w = snd_soc_dapm_new_control_unlocked(dapm, &template);
		if (IS_ERR(w))
			return PTR_ERR(w);

		w->priv = dai;
		dai->capture_widget = w;
	}

	return 0;
}

Each DAI widget stores its parent DAI in w->priv, enabling bidirectional lookup between DAPM and ASoC core structures.

Endpoint Identification Logic

Endpoint status (is_ep) is not static—it evolves based on routing topology. Three mechanisms update it:

  1. Initial assignment during widget creation (e.g., snd_soc_dapm_input → EP_SOURCE)
  2. Dynamic adjustment in dapm_update_widget_flags() when routes are added/removed:
static void dapm_update_widget_flags(struct snd_soc_dapm_widget *w)
{
	switch (w->id) {
	case snd_soc_dapm_input:
		if (w->dapm->card->fully_routed)
			return;
		// Check if connected to micbias/mic/etc.
		// Clear EP_SOURCE if downstream sink exists
		break;
	case snd_soc_dapm_output:
		// Similar logic for EP_SINK
		break;
	case snd_soc_dapm_line:
		// Derive EP flags from edge presence
		break;
	}
	w->is_ep = ep;
}
  1. Runtime toggling during stream events via soc_dapm_dai_stream_event():
  • SND_SOC_DAPM_STREAM_START: Sets is_ep to EP_SOURCE/EP_SINK
  • SND_SOC_DAPM_STREAM_STOP: Clears is_ep

Path and Route Construction

Paths represent directed connections between widgets. Routes are declarative specifications used to instantiate those paths.

Explicit Route Registration

Routes are defined as arrays of snd_soc_dapm_route, then registered via snd_soc_dapm_add_routes():

int snd_soc_dapm_add_routes(struct snd_soc_dapm_context *dapm,
			    const struct snd_soc_dapm_route *route, int num)
{
	int i, ret = 0;

	mutex_lock_nested(&dapm->card->dapm_mutex, SND_SOC_DAPM_CLASS_RUNTIME);
	for (i = 0; i < num; i++) {
		ret = snd_soc_dapm_add_route(dapm, route++);
		if (ret < 0)
			break;
	}
	mutex_unlock(&dapm->card->dapm_mutex);
	return ret;
}

snd_soc_dapm_add_route() locates source and sink widgets, then invokes snd_soc_dapm_add_path() to construct the connection.

Path construction validates constraints:

  • Supply widgets cannot be sinks for non-supply sources
  • Conditional controls (control != NULL) require compatible widget types (mux/demux/mixer)
  • Static paths (control == NULL) always connect
static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
				 struct snd_soc_dapm_widget *wsource,
				 struct snd_soc_dapm_widget *wsink,
				 const char *control,
				 int (*connected)(...))
{
	struct snd_soc_dapm_path *path;

	// Validation checks omitted

	path = kzalloc(sizeof(*path), GFP_KERNEL);
	path->node[SND_SOC_DAPM_DIR_IN] = wsource;
	path->node[SND_SOC_DAPM_DIR_OUT] = wsink;

	if (!control) {
		path->connect = 1;
	} else {
		switch (wsource->id) {
		case snd_soc_dapm_demux:
			dapm_connect_mux(dapm, path, control, wsource);
			break;
		}
		switch (wsink->id) {
		case snd_soc_dapm_mux:
			dapm_connect_mux(dapm, path, control, wsink);
			break;
		case snd_soc_dapm_mixer:
			dapm_connect_mixer(dapm, path, control);
			break;
		}
	}

	list_add(&path->list, &dapm->card->paths);
	list_add(&path->list_node[SND_SOC_DAPM_DIR_IN], &wsource->edges[SND_SOC_DAPM_DIR_IN]);
	list_add(&path->list_node[SND_SOC_DAPM_DIR_OUT], &wsink->edges[SND_SOC_DAPM_DIR_OUT]);

	// Update endpoint flags and mark dirty
	for_each_dapm_direction(dir) {
		dapm_update_widget_flags(widgets[dir]);
		dapm_mark_dirty(widgets[dir], "Route added");
	}

	return 0;
}

Critical side effects:

  • Paths are added to card->paths
  • Forward/backward edges are linked into widget->edges[dir]
  • Widgets are marked dirty for subsequent power-state evaluation

Inter-Component Path Linking

Three automated linking phases complete the full audio graph:

  1. Codec DAI ↔ Codec Internal Widgets Executed by snd_soc_dapm_link_dai_widgets() during card instantiation. It matches DAI widgets with internal widgets sharing the same sname (stream name), then creates unconditional paths.

  2. CPU DAI ↔ Codec DAI Handled by snd_soc_dapm_connect_dai_link_widgets(). For each BE (back-end) DAI link, it connects corresponding playback/capture widgets between CPU and codec components.

  3. Platform DAI ↔ Platform Internal Widgets Covered under explicit route registration since platform drivers declare these routes statically.

KControl Initialization

After all widgets and paths exist, snd_soc_dapm_new_widgets() constructs associated ALSA controls:

int snd_soc_dapm_new_widgets(struct snd_soc_card *card)
{
	struct snd_soc_dapm_widget *w;

	mutex_lock_nested(&card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);
	list_for_each_entry(w, &card->widgets, list) {
		if (w->new)
			continue;

		if (w->num_kcontrols) {
			w->kcontrols = kcalloc(w->num_kcontrols,
					sizeof(struct snd_kcontrol *), GFP_KERNEL);
		}

		switch (w->id) {
		case snd_soc_dapm_switch:
		case snd_soc_dapm_mixer:
			dapm_new_mixer(w);
			break;
		case snd_soc_dapm_mux:
			dapm_new_mux(w);
			break;
		case snd_soc_dapm_pga:
			dapm_new_pga(w);
			break;
		}

		// Read initial hardware state
		if (w->reg >= 0) {
			soc_dapm_read(w->dapm, w->reg, &val);
			w->power = ((val >> w->shift) & w->mask) == w->on_val;
		}

		w->new = 1;
		dapm_mark_dirty(w, "new widget");
	}

	dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);
	mutex_unlock(&card->dapm_mutex);
	return 0;
}

Widget-specific helpers create controls:

  • dapm_new_mixer() iterates over w->edges[SND_SOC_DAPM_DIR_IN], matching paths by name to w->kcontrol_news[i].name, then calls dapm_create_or_share_kcontrol()
  • dapm_new_mux() handles single-control muxes, linking all paths to one control
  • dapm_new_pga() follows similar patterns for programmable gain amplifiers

The dapm_create_or_share_kcontrol() functon builds the final snd_kcontrol, applies naming conventions (e.g., "Widget Name Control Name"), allocates private data, and registers it with the ALSA control subsystem.

At completion, the DAPM graph contains fully connected widgets, validated paths, and user-controllable switches/mixers—ready for runtime power-state propagation.

Tags: Linux ALSA asoc dapm audio-driver

Posted on Thu, 06 Aug 2026 17:02:55 +0000 by sarun