ASoC & DAPM
ASoC와 DAPM을 임베디드 오디오 전력 최적화 관점에서 심층 분석합니다. Codec/Platform/Machine 드라이버 분리 구조, DAI 링크 구성과 포맷 협상, DAPM 위젯·라우트 그래프를 통한 자동 전원 게이팅, 클럭/레귤레이터/잭 감지 연동, Device Tree 카드 바인딩과 멀티코덱 설계, low-power 오디오 경로 구성, pop-noise 방지 및 경로 전환 안정화, debugfs와 오디오 계측 기반 튜닝까지 보드 레벨 오디오 스택 구현에 필요한 실전 내용을 다룹니다.
핵심 요약
- 3계층 분리 — Codec, Platform(CPU DAI+DMA), Machine 드라이버를 독립적으로 개발하고 DAI 링크로 연결합니다.
- DAI 포맷 협상 — I2S/TDM/PDM 포맷, 마스터/슬레이브, 클럭 극성을 양쪽 DAI가 합의합니다.
- DAPM 자동 전원 — 위젯 그래프를 추적하여 활성 경로의 컴포넌트만 전원을 켜고 나머지는 자동 차단합니다.
- 클럭/레귤레이터 연동 — MCLK, BCLK, 전원 레귤레이터를 DAPM 이벤트에 맞춰 관리합니다.
- 잭 감지 — 헤드폰/마이크 삽입을 감지하여 DAPM 경로를 동적으로 전환합니다.
단계별 이해
- ASoC 아키텍처 파악
Codec/Platform/Machine 3계층 구조와 각 드라이버의 역할을 이해합니다. - DAI 링크 구성
Machine 드라이버에서 CPU DAI와 Codec DAI를 연결하고, 포맷/클럭 모드를 설정합니다. - DAPM 위젯과 라우트 정의
오디오 경로를 위젯 그래프로 모델링하고 전원 시퀀싱을 구현합니다. - 디버깅(Debugging)과 튜닝
debugfs로 DAPM 상태를 확인하고, pop-noise 방지와 전력 최적화를 수행합니다.
ASoC 프레임워크
ASoC (ALSA System on Chip)는 임베디드/SoC 플랫폼을 위한 고수준 오디오 프레임워크입니다. 코덱, 플랫폼 (CPU DAI + DMA), 머신 (보드 특화) 드라이버를 분리하여 재사용성을 극대화합니다.
ASoC 아키텍처
ASoC는 세 계층으로 구성됩니다:
- Codec Driver: 오디오 코덱 칩 (WM8731, RT5640 등). I2C/SPI로 제어, I2S/TDM으로 데이터 전송.
- Platform Driver: SoC의 오디오 인터페이스 (I2S, PCM, AC97). DMA 엔진 포함.
- Machine Driver: 보드별 연결 정보 (어떤 코덱이 어떤 CPU DAI에 연결되었는지, GPIO 설정 등).
Codec Driver 작성
/* sound/soc/codecs/wm8731.c 스타일 간단 예제 */
#include <sound/soc.h>
#include <sound/tlv.h>
#include <linux/regmap.h>
/* Codec 레지스터 맵 */
#define WM8731_LINVOL 0x00
#define WM8731_RINVOL 0x01
#define WM8731_LOUT1V 0x02
#define WM8731_ROUT1V 0x03
#define WM8731_APANA 0x04
#define WM8731_APDIGI 0x05
#define WM8731_PWR 0x06
#define WM8731_IFACE 0x07
#define WM8731_SRATE 0x08
#define WM8731_ACTIVE 0x09
#define WM8731_RESET 0x0f
/* Regmap 설정 */
static const struct regmap_config wm8731_regmap = {
.reg_bits = 7,
.val_bits = 9,
.max_register = WM8731_RESET,
.cache_type = REGCACHE_RBTREE,
};
/* Kcontrols */
static const DECLARE_TLV_DB_SCALE(in_tlv, -3450, 150, 0);
static const DECLARE_TLV_DB_SCALE(out_tlv, -7300, 100, 1);
static const struct snd_kcontrol_new wm8731_controls[] = {
SOC_DOUBLE_R_TLV("Capture Volume", WM8731_LINVOL, WM8731_RINVOL,
0, 31, 0, in_tlv),
SOC_DOUBLE_R_TLV("Headphone Playback Volume", WM8731_LOUT1V,
WM8731_ROUT1V, 0, 127, 0, out_tlv),
SOC_SINGLE("Mic Boost (+20dB)", WM8731_APANA, 0, 1, 0),
};
/* DAPM widgets (나중에 DAPM 섹션에서 상세 설명) */
static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = {
SND_SOC_DAPM_DAC("DAC", "Playback", WM8731_PWR, 3, 1),
SND_SOC_DAPM_ADC("ADC", "Capture", WM8731_PWR, 2, 1),
SND_SOC_DAPM_OUTPUT("LHPOUT"),
SND_SOC_DAPM_OUTPUT("RHPOUT"),
SND_SOC_DAPM_INPUT("LLINEIN"),
SND_SOC_DAPM_INPUT("RLINEIN"),
};
/* DAPM routes */
static const struct snd_soc_dapm_route wm8731_routes[] = {
{"LHPOUT", NULL, "DAC"},
{"RHPOUT", NULL, "DAC"},
{"ADC", NULL, "LLINEIN"},
{"ADC", NULL, "RLINEIN"},
};
/* DAI ops */
static int wm8731_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct snd_soc_component *component = dai->component;
unsigned int iface = 0;
/* 샘플 포맷 설정 */
switch (params_width(params)) {
case 16:
break;
case 20:
iface |= 0x04;
break;
case 24:
iface |= 0x08;
break;
case 32:
iface |= 0x0c;
break;
}
snd_soc_component_write(component, WM8731_IFACE, iface);
/* 샘플레이트 설정 (생략: 복잡한 클럭 계산) */
return 0;
}
static int wm8731_set_dai_fmt(struct snd_soc_dai *dai,
unsigned int fmt)
{
struct snd_soc_component *component = dai->component;
unsigned int iface = 0;
/* Format: I2S, Left-justified, Right-justified, DSP mode A/B */
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
iface |= 0x02;
break;
case SND_SOC_DAIFMT_LEFT_J:
break;
case SND_SOC_DAIFMT_RIGHT_J:
iface |= 0x01;
break;
case SND_SOC_DAIFMT_DSP_A:
iface |= 0x03;
break;
default:
return -EINVAL;
}
/* Clock master/slave */
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBM_CFM: /* Codec is master */
iface |= 0x40;
break;
case SND_SOC_DAIFMT_CBS_CFS: /* Codec is slave */
break;
default:
return -EINVAL;
}
snd_soc_component_update_bits(component, WM8731_IFACE, 0x4f, iface);
return 0;
}
static const struct snd_soc_dai_ops wm8731_dai_ops = {
.hw_params = wm8731_hw_params,
.set_fmt = wm8731_set_dai_fmt,
};
/* Codec DAI 정의 */
static struct snd_soc_dai_driver wm8731_dai = {
.name = "wm8731-hifi",
.playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_96000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
},
.capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_96000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
},
.ops = &wm8731_dai_ops,
};
/* Component driver */
static const struct snd_soc_component_driver soc_component_dev_wm8731 = {
.controls = wm8731_controls,
.num_controls = ARRAY_SIZE(wm8731_controls),
.dapm_widgets = wm8731_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(wm8731_dapm_widgets),
.dapm_routes = wm8731_routes,
.num_dapm_routes = ARRAY_SIZE(wm8731_routes),
};
/* I2C probe */
static int wm8731_i2c_probe(struct i2c_client *i2c)
{
struct regmap *regmap;
int ret;
regmap = devm_regmap_init_i2c(i2c, &wm8731_regmap);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
ret = devm_snd_soc_register_component(&i2c->dev,
&soc_component_dev_wm8731,
&wm8731_dai, 1);
return ret;
}
static const struct i2c_device_id wm8731_i2c_id[] = {
{ "wm8731", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, wm8731_i2c_id);
static struct i2c_driver wm8731_i2c_driver = {
.driver = {
.name = "wm8731",
},
.probe_new = wm8731_i2c_probe,
.id_table = wm8731_i2c_id,
};
module_i2c_driver(wm8731_i2c_driver);
Platform Driver 작성
/* CPU DAI + DMA (간단화된 예제) */
static int my_i2s_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct my_i2s *i2s = snd_soc_dai_get_drvdata(dai);
unsigned int rate = params_rate(params);
unsigned int channels = params_channels(params);
unsigned int width = params_width(params);
/* I2S 컨트롤러 클럭 설정 */
unsigned int bclk = rate * channels * width;
my_i2s_set_clk(i2s, bclk);
return 0;
}
static int my_i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
struct my_i2s *i2s = snd_soc_dai_get_drvdata(dai);
u32 ctrl = 0;
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
ctrl |= I2S_MODE_I2S;
break;
case SND_SOC_DAIFMT_LEFT_J:
ctrl |= I2S_MODE_LEFT_J;
break;
default:
return -EINVAL;
}
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBS_CFS: /* CPU is master */
ctrl |= I2S_MASTER;
break;
case SND_SOC_DAIFMT_CBM_CFM: /* Codec is master */
break;
default:
return -EINVAL;
}
writel(ctrl, i2s->regs + I2S_CTRL);
return 0;
}
static const struct snd_soc_dai_ops my_i2s_dai_ops = {
.hw_params = my_i2s_hw_params,
.set_fmt = my_i2s_set_fmt,
};
static struct snd_soc_dai_driver my_i2s_dai = {
.playback = {
.channels_min = 2,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
.capture = {
.channels_min = 2,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
.ops = &my_i2s_dai_ops,
};
/* Platform component (DMA) */
static const struct snd_soc_component_driver my_platform_component = {
.name = "my-platform",
.pcm_construct = my_pcm_new, /* DMA 버퍼 할당 */
};
static int my_i2s_probe(struct platform_device *pdev)
{
struct my_i2s *i2s;
int ret;
i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
if (!i2s)
return -ENOMEM;
i2s->regs = devm_platform_ioremap_resource(pdev, 0);
platform_set_drvdata(pdev, i2s);
ret = devm_snd_soc_register_component(&pdev->dev,
&my_platform_component,
&my_i2s_dai, 1);
return ret;
}
Machine Driver 작성
/* sound/soc/fsl/imx-wm8731.c 스타일 */
static struct snd_soc_dai_link my_board_dai_link = {
.name = "WM8731",
.stream_name = "WM8731 HiFi",
.cpus = &(struct snd_soc_dai_link_component){
.dai_name = "my-i2s-dai",
},
.num_cpus = 1,
.codecs = &(struct snd_soc_dai_link_component){
.dai_name = "wm8731-hifi",
},
.num_codecs = 1,
.platforms = &(struct snd_soc_dai_link_component){
.name = "my-platform",
},
.num_platforms = 1,
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS, /* I2S, CPU is master */
};
static struct snd_soc_card my_board_card = {
.name = "MyBoard-WM8731",
.owner = THIS_MODULE,
.dai_link = &my_board_dai_link,
.num_links = 1,
};
static int my_board_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct device_node *cpu_np, *codec_np;
/* Device Tree에서 DAI 노드 읽기 */
cpu_np = of_parse_phandle(np, "cpu-dai", 0);
codec_np = of_parse_phandle(np, "audio-codec", 0);
my_board_dai_link.cpus->of_node = cpu_np;
my_board_dai_link.codecs->of_node = codec_np;
my_board_dai_link.platforms->of_node = cpu_np;
my_board_card.dev = &pdev->dev;
return devm_snd_soc_register_card(&pdev->dev, &my_board_card);
}
static const struct of_device_id my_board_dt_ids[] = {
{ .compatible = "myvendor,myboard-audio", },
{ }
};
static struct platform_driver my_board_driver = {
.driver = {
.name = "myboard-audio",
.of_match_table = my_board_dt_ids,
},
.probe = my_board_probe,
};
module_platform_driver(my_board_driver);
DAI 포맷
| 포맷 | 설명 | 용도 |
|---|---|---|
SND_SOC_DAIFMT_I2S |
I2S (Philips) | 가장 일반적인 스테레오 포맷 |
SND_SOC_DAIFMT_LEFT_J |
Left Justified | MSB가 LRCLK 엣지에 정렬 |
SND_SOC_DAIFMT_RIGHT_J |
Right Justified | LSB가 LRCLK 엣지에 정렬 |
SND_SOC_DAIFMT_DSP_A |
DSP Mode A | 1 BCLK 지연(Latency) |
SND_SOC_DAIFMT_DSP_B |
DSP Mode B | 지연 없음 |
SND_SOC_DAIFMT_AC97 |
AC'97 | 레거시 AC'97 버스(Bus) |
SND_SOC_DAIFMT_PDM |
PDM (Pulse Density Modulation) | MEMS 마이크 |
Device Tree Binding 예제
/* arch/arm/boot/dts/myboard.dts */
i2s0: i2s@40030000 {
compatible = "myvendor,my-i2s";
reg = <0x40030000 0x1000>;
interrupts = <45>;
clocks = <&clk_i2s>;
dmas = <&dma 10>, <&dma 11>;
dma-names = "tx", "rx";
#sound-dai-cells = <0>;
};
wm8731: codec@1a {
compatible = "wlf,wm8731";
reg = <0x1a>;
#sound-dai-cells = <0>;
clocks = <&clk_mclk>;
clock-names = "mclk";
};
sound {
compatible = "myvendor,myboard-audio";
cpu-dai = <&i2s0>;
audio-codec = <&wm8731>;
};
또는 simple-audio-card 사용:
sound {
compatible = "simple-audio-card";
simple-audio-card,name = "MyBoard Audio";
simple-audio-card,format = "i2s";
simple-audio-card,mclk-fs = <256>;
simple-audio-card,cpu {
sound-dai = <&i2s0>;
};
simple-audio-card,codec {
sound-dai = <&wm8731>;
};
};
DAPM
DAPM (Dynamic Audio Power Management)는 ASoC의 핵심 기능으로, 오디오 경로를 기반으로 전원 도메인을 자동으로 켜고 끄는 지능형 전원 관리(Power Management) 시스템입니다. Widget 그래프와 오디오 경로 추적으로 사용하지 않는 컴포넌트의 전원을 자동 차단하여 전력 소비를 최소화합니다.
DAPM Widget 타입
| Widget 타입 | 설명 | 예시 |
|---|---|---|
SND_SOC_DAPM_INPUT |
입력 핀 (외부) | Mic, Line In |
SND_SOC_DAPM_OUTPUT |
출력 핀 (외부) | Headphone, Speaker |
SND_SOC_DAPM_MIC |
마이크 바이어스 포함 | Internal Mic |
SND_SOC_DAPM_HP |
헤드폰 출력 | Headphone Jack |
SND_SOC_DAPM_SPK |
스피커 출력 | External Speaker |
SND_SOC_DAPM_LINE |
라인 입출력(I/O) | Line Out, Line In |
SND_SOC_DAPM_ADC |
Analog-to-Digital Converter | Left ADC, Right ADC |
SND_SOC_DAPM_DAC |
Digital-to-Analog Converter | Left DAC, Right DAC |
SND_SOC_DAPM_MIXER |
믹서 (여러 입력 합성) | Output Mixer |
SND_SOC_DAPM_MUX |
멀티플렉서 (하나 선택) | Input Mux (Line/Mic/CD) |
SND_SOC_DAPM_DEMUX |
디멀티플렉서 | Output Router |
SND_SOC_DAPM_PGA |
Programmable Gain Amplifier | Mic Boost, Volume Control |
SND_SOC_DAPM_SUPPLY |
전원 공급 (다른 widget 의존) | VREF, Clock, Bias |
SND_SOC_DAPM_REGULATOR_SUPPLY |
Regulator 전원 | AVDD, DVDD |
SND_SOC_DAPM_CLOCK_SUPPLY |
클럭 소스 | MCLK |
SND_SOC_DAPM_AIF_IN |
오디오 인터페이스 입력 | I2S RX |
SND_SOC_DAPM_AIF_OUT |
오디오 인터페이스 출력 | I2S TX |
SND_SOC_DAPM_PRE |
스트림 시작 전 이벤트 | Pre-charge circuit |
SND_SOC_DAPM_POST |
스트림 종료 후 이벤트 | Pop noise reduction |
SND_SOC_DAPM_SWITCH |
On/Off 스위치 | Capture Switch |
DAPM Route 정의
Route는 widget 간 오디오 경로를 정의합니다:
static const struct snd_soc_dapm_widget wm8731_widgets[] = {
/* Outputs */
SND_SOC_DAPM_OUTPUT("LHPOUT"),
SND_SOC_DAPM_OUTPUT("RHPOUT"),
SND_SOC_DAPM_OUTPUT("LOUT"),
SND_SOC_DAPM_OUTPUT("ROUT"),
/* Inputs */
SND_SOC_DAPM_INPUT("LLINEIN"),
SND_SOC_DAPM_INPUT("RLINEIN"),
SND_SOC_DAPM_INPUT("MICIN"),
/* DACs */
SND_SOC_DAPM_DAC("DAC", "Playback", WM8731_PWR, 3, 1),
/* ADCs */
SND_SOC_DAPM_ADC("ADC", "Capture", WM8731_PWR, 2, 1),
/* Mixers */
SND_SOC_DAPM_MIXER("Output Mixer", WM8731_PWR, 4, 1, NULL, 0),
/* Input Mux */
SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0, &wm8731_input_mux),
/* Mic Bias */
SND_SOC_DAPM_SUPPLY("Mic Bias", WM8731_PWR, 1, 0, NULL, 0),
};
static const struct snd_soc_dapm_route wm8731_routes[] = {
/* Playback path */
{"Output Mixer", NULL, "DAC"},
{"LHPOUT", NULL, "Output Mixer"},
{"RHPOUT", NULL, "Output Mixer"},
{"LOUT", NULL, "Output Mixer"},
{"ROUT", NULL, "Output Mixer"},
/* Capture path */
{"Input Mux", "Line", "LLINEIN"},
{"Input Mux", "Line", "RLINEIN"},
{"Input Mux", "Mic", "MICIN"},
{"ADC", NULL, "Input Mux"},
/* Mic bias */
{"MICIN", NULL, "Mic Bias"},
};
DAPM 전원 시퀀싱 이벤트
Widget은 전원 상태 변화 시 이벤트 콜백(Callback)을 받을 수 있습니다:
static int my_amp_event(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol,
int event)
{
struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm);
switch (event) {
case SND_SOC_DAPM_PRE_PMU: /* Power-up 전 */
dev_dbg(component->dev, "Amp powering up\\n");
/* Pre-charge 회로 활성화 */
break;
case SND_SOC_DAPM_POST_PMU: /* Power-up 후 */
/* 앰프 언뮤트 (pop noise 방지 위해 지연) */
msleep(50);
snd_soc_component_update_bits(component, AMP_CTRL, MUTE_BIT, 0);
break;
case SND_SOC_DAPM_PRE_PMD: /* Power-down 전 */
/* 앰프 뮤트 (pop noise 방지) */
snd_soc_component_update_bits(component, AMP_CTRL, MUTE_BIT, MUTE_BIT);
msleep(50);
break;
case SND_SOC_DAPM_POST_PMD: /* Power-down 후 */
dev_dbg(component->dev, "Amp powered down\\n");
break;
}
return 0;
}
SND_SOC_DAPM_PGA_E("Headphone Amp", SND_SOC_NOPM, 0, 0, NULL, 0,
my_amp_event,
SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU |
SND_SOC_DAPM_PRE_PMD | SND_SOC_DAPM_POST_PMD),
DAPM Controls (동적 경로)
DAPM mixer와 mux는 오디오 경로를 동적으로 변경할 수 있습니다:
/* Input Mux 정의 */
static const char * const input_mux_texts[] = {
"Line", "Mic", "CD", "Aux"
};
static SOC_ENUM_SINGLE_DECL(input_mux_enum, INPUT_MUX_REG, 0,
input_mux_texts);
static const struct snd_kcontrol_new input_mux_control =
SOC_DAPM_ENUM("Route", input_mux_enum);
SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0, &input_mux_control),
/* Mixer 정의 (여러 입력 합성) */
static const struct snd_kcontrol_new output_mixer_controls[] = {
SOC_DAPM_SINGLE("DAC Switch", MIXER_REG, 0, 1, 0),
SOC_DAPM_SINGLE("Line Bypass Switch", MIXER_REG, 1, 1, 0),
SOC_DAPM_SINGLE("Mic Sidetone Switch", MIXER_REG, 2, 1, 0),
};
SND_SOC_DAPM_MIXER("Output Mixer", SND_SOC_NOPM, 0, 0,
output_mixer_controls,
ARRAY_SIZE(output_mixer_controls)),
DAPM 그래프 시각화
DAPM Bias Level
DAPM은 코덱 전체의 전원 상태를 4단계 바이어스 레벨로 관리합니다. 바이어스 레벨은 코덱의 set_bias_level 콜백을 통해 전환되며, 활성 스트림과 위젯 상태에 따라 DAPM 코어가 자동으로 결정합니다.
| Bias Level | 상수 | 설명 | 전력 소비 |
|---|---|---|---|
| ON | SND_SOC_BIAS_ON |
활성 스트림 존재. 모든 전원 도메인 활성화 | 최대 |
| PREPARE | SND_SOC_BIAS_PREPARE |
스트림 시작/종료 전환 중. 클럭 활성화, 바이어스 전압 준비 | 높음 |
| STANDBY | SND_SOC_BIAS_STANDBY |
활성 스트림 없음. 최소 바이어스 유지 (빠른 복귀 가능) | 낮음 |
| OFF | SND_SOC_BIAS_OFF |
코덱 완전 차단. 레귤레이터/클럭 비활성화 | 0 |
/* Codec의 set_bias_level 콜백 구현 예제 */
static int my_codec_set_bias_level(struct snd_soc_component *component,
enum snd_soc_bias_level level)
{
struct my_codec_priv *priv = snd_soc_component_get_drvdata(component);
switch (level) {
case SND_SOC_BIAS_ON:
/* 스트림 활성: 추가 작업 불필요 (이미 PREPARE에서 준비) */
break;
case SND_SOC_BIAS_PREPARE:
if (snd_soc_component_get_bias_level(component) == SND_SOC_BIAS_STANDBY) {
/* STANDBY → PREPARE: MCLK 활성화 */
clk_prepare_enable(priv->mclk);
}
break;
case SND_SOC_BIAS_STANDBY:
if (snd_soc_component_get_bias_level(component) == SND_SOC_BIAS_OFF) {
/* OFF → STANDBY: 레귤레이터 활성화, 소프트 리셋 */
regulator_bulk_enable(ARRAY_SIZE(priv->supplies), priv->supplies);
regcache_sync(priv->regmap);
} else if (snd_soc_component_get_bias_level(component) == SND_SOC_BIAS_PREPARE) {
/* PREPARE → STANDBY: MCLK 비활성화 */
clk_disable_unprepare(priv->mclk);
}
break;
case SND_SOC_BIAS_OFF:
/* 완전 차단: 레귤레이터 비활성화 */
regulator_bulk_disable(ARRAY_SIZE(priv->supplies), priv->supplies);
regcache_mark_dirty(priv->regmap);
break;
}
return 0;
}
클럭과 레귤레이터 관리
임베디드 오디오에서 클럭과 전원은 DAPM과 긴밀하게 연동됩니다. MCLK(Master Clock)은 코덱의 내부 PLL/FLL 기준 클럭이며, BCLK(Bit Clock)은 DAI 데이터 전송 클럭입니다. 레귤레이터는 코덱 IC의 아날로그/디지털 전원을 공급합니다.
/* Machine 드라이버에서 MCLK/레귤레이터 관리 */
struct my_audio_data {
struct clk *mclk;
struct regulator *amp_supply;
unsigned int mclk_freq;
};
static int my_board_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
struct snd_soc_dai *codec_dai = snd_soc_rtd_to_codec(rtd, 0);
struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0);
struct my_audio_data *data = snd_soc_card_get_drvdata(rtd->card);
unsigned int sample_rate = params_rate(params);
unsigned int mclk_freq;
int ret;
/* MCLK = 256 * fs (일반적인 오버샘플링 비율) */
mclk_freq = sample_rate * 256;
/* MCLK 주파수 설정 */
ret = clk_set_rate(data->mclk, mclk_freq);
if (ret) {
dev_err(rtd->card->dev, "MCLK rate %u 설정 실패: %d\n", mclk_freq, ret);
return ret;
}
/* 코덱에 sysclk 전달 */
ret = snd_soc_dai_set_sysclk(codec_dai, 0, mclk_freq, SND_SOC_CLOCK_IN);
if (ret) {
dev_err(rtd->card->dev, "Codec sysclk 설정 실패: %d\n", ret);
return ret;
}
/* CPU DAI에 sysclk 전달 */
ret = snd_soc_dai_set_sysclk(cpu_dai, 0, mclk_freq, SND_SOC_CLOCK_OUT);
if (ret) {
dev_err(rtd->card->dev, "CPU DAI sysclk 설정 실패: %d\n", ret);
return ret;
}
return 0;
}
static const struct snd_soc_ops my_board_ops = {
.hw_params = my_board_hw_params,
};
assigned-clocks와 assigned-clock-rates 속성으로 부팅 시 기본 클럭을 설정할 수도 있습니다.
잭 감지 (Jack Detection)
헤드폰이나 마이크 잭의 삽입/제거를 감지하여 DAPM 경로를 동적으로 전환하는 것은 임베디드 오디오의 핵심 기능입니다.
ASoC는 snd_soc_jack 구조체(Struct)를 통해 잭 상태를 관리하고, GPIO/IRQ/코덱 내부 감지 메커니즘을 지원합니다.
/* Machine 드라이버에서 잭 감지 설정 */
static struct snd_soc_jack headphone_jack;
static struct snd_soc_jack mic_jack;
/* 잭과 DAPM 핀 매핑 */
static struct snd_soc_jack_pin headphone_jack_pins[] = {
{
.pin = "Headphone Jack",
.mask = SND_JACK_HEADPHONE,
},
};
static struct snd_soc_jack_pin mic_jack_pins[] = {
{
.pin = "Mic Jack",
.mask = SND_JACK_MICROPHONE,
},
};
/* GPIO 기반 잭 감지 */
static struct snd_soc_jack_gpio headphone_jack_gpio = {
.name = "hp-detect",
.report = SND_JACK_HEADPHONE,
.debounce_time = 200, /* 바운스 방지 200ms */
.invert = 0, /* GPIO HIGH = 잭 삽입 */
};
static int my_board_late_probe(struct snd_soc_card *card)
{
struct snd_soc_pcm_runtime *rtd = snd_soc_get_pcm_runtime(card,
&card->dai_link[0]);
struct snd_soc_component *component = snd_soc_rtd_to_codec(rtd, 0)->component;
int ret;
/* 헤드폰 잭 등록 */
ret = snd_soc_card_jack_new_pins(card, "Headphone Jack",
SND_JACK_HEADPHONE,
&headphone_jack,
headphone_jack_pins,
ARRAY_SIZE(headphone_jack_pins));
if (ret)
return ret;
/* GPIO 잭 감지 연결 */
headphone_jack_gpio.gpiod_dev = component->dev;
ret = snd_soc_jack_add_gpios(&headphone_jack, 1, &headphone_jack_gpio);
if (ret)
dev_warn(card->dev, "GPIO 잭 감지 설정 실패: %d\n", ret);
/* 마이크 잭 등록 (코덱 내부 감지 사용) */
ret = snd_soc_card_jack_new_pins(card, "Mic Jack",
SND_JACK_MICROPHONE,
&mic_jack,
mic_jack_pins,
ARRAY_SIZE(mic_jack_pins));
if (ret)
return ret;
/* 코덱 내부 잭 감지 설정 (코덱 의존적) */
my_codec_set_jack_detect(component, &mic_jack);
return 0;
}
"Headphone Jack")은 DAPM 위젯 이름과 정확히 일치해야 합니다.
Machine 드라이버에서 SND_SOC_DAPM_HP("Headphone Jack", NULL) 위젯을 정의하고, 잭이 삽입되면 해당 핀이 활성화되어 연결된 경로의 모든 위젯에 전원이 공급됩니다.
잭이 빠지면 핀이 비활성화되어 해당 경로의 불필요한 위젯이 자동으로 꺼집니다.
DPCM (Dynamic PCM)
DPCM (Dynamic PCM)은 단일 프론트엔드 PCM 디바이스를 여러 백엔드 DAI에 동적으로 연결할 수 있게 하는 ASoC 확장입니다. SoC 내부에서 오디오 경로가 DSP를 경유하거나, 하나의 PCM 스트림이 상황에 따라 다른 물리적 출력으로 라우팅(Routing)되어야 할 때 사용합니다.
/* DPCM Machine 드라이버: Frontend + Backend DAI 링크 */
static struct snd_soc_dai_link my_dpcm_dai_links[] = {
/* Frontend DAI link: 사용자 공간과 연결 */
{
.name = "Playback Frontend",
.stream_name = "Playback",
.dynamic = 1, /* DPCM frontend 표시 */
.dpcm_playback = 1, /* 재생 방향 활성 */
.trigger = {
SND_SOC_DPCM_TRIGGER_POST,
SND_SOC_DPCM_TRIGGER_POST,
},
SND_SOC_DAILINK_REG(fe_playback), /* CPU DAI */
},
{
.name = "Capture Frontend",
.stream_name = "Capture",
.dynamic = 1,
.dpcm_capture = 1,
.trigger = {
SND_SOC_DPCM_TRIGGER_POST,
SND_SOC_DPCM_TRIGGER_POST,
},
SND_SOC_DAILINK_REG(fe_capture),
},
/* Backend DAI links: 물리적 코덱과 연결 */
{
.name = "Headphone Backend",
.no_pcm = 1, /* DPCM backend 표시 (PCM 없음) */
.dpcm_playback = 1,
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS,
SND_SOC_DAILINK_REG(be_headphone),
},
{
.name = "Speaker Backend",
.no_pcm = 1,
.dpcm_playback = 1,
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS,
SND_SOC_DAILINK_REG(be_speaker),
},
{
.name = "DMIC Backend",
.no_pcm = 1,
.dpcm_capture = 1,
.dai_fmt = SND_SOC_DAIFMT_PDM,
SND_SOC_DAILINK_REG(be_dmic),
},
};
dai_link가 CPU DAI와 Codec DAI를 1:1로 고정 연결합니다.
DPCM에서는 Frontend(사용자 공간(User Space) PCM)와 Backend(물리적 DAI)를 분리하여, DAPM 위젯 경로에 따라 런타임에 동적으로 연결합니다.
이는 SoC 내부 DSP/믹서를 경유하는 복잡한 오디오 라우팅에 필수적입니다.
멀티코덱과 audio-graph-card
하나의 보드에 여러 코덱을 연결하는 구성은 임베디드 오디오에서 흔합니다 (예: 통화용 코덱 + 미디어 재생용 코덱, 또는 스피커 앰프 IC + 헤드폰 코덱). Device Tree 기반의 audio-graph-card는 Machine 드라이버 C 코드 없이 DT만으로 오디오 카드를 구성할 수 있는 범용 드라이버입니다.
/* audio-graph-card: DT만으로 오디오 카드 구성 */
sound {
compatible = "audio-graph-card";
label = "MyBoard Audio";
/* 여러 DAI 링크를 widgets 속성으로 선언 */
widgets = "Headphone", "Headphone Jack",
"Speaker", "External Speaker",
"Microphone", "Internal Mic";
routing = "Headphone Jack", "LHPOUT",
"Headphone Jack", "RHPOUT",
"External Speaker", "LSPK",
"External Speaker", "RSPK",
"LLINEIN", "Internal Mic";
/* DAI 링크: CPU 포트 → 코덱 엔드포인트 */
dais = <&cpu_port0>, <&cpu_port1>;
};
/* CPU 측 I2S 컨트롤러 */
i2s0: i2s@40030000 {
compatible = "myvendor,my-i2s";
/* ... */
#sound-dai-cells = <0>;
port {
cpu_port0: endpoint {
remote-endpoint = <&codec_a_ep>;
dai-format = "i2s";
bitclock-master;
frame-master;
};
};
};
i2s1: i2s@40031000 {
compatible = "myvendor,my-i2s";
/* ... */
#sound-dai-cells = <0>;
port {
cpu_port1: endpoint {
remote-endpoint = <&codec_b_ep>;
dai-format = "i2s";
bitclock-master;
frame-master;
};
};
};
/* 코덱 A: 헤드폰 코덱 (I2C 버스) */
codec_a: codec@1a {
compatible = "wlf,wm8731";
reg = <0x1a>;
#sound-dai-cells = <0>;
port {
codec_a_ep: endpoint {
remote-endpoint = <&cpu_port0>;
};
};
};
/* 코덱 B: 스피커 앰프 (I2C 버스) */
codec_b: codec@34 {
compatible = "ti,tas5720";
reg = <0x34>;
#sound-dai-cells = <0>;
port {
codec_b_ep: endpoint {
remote-endpoint = <&cpu_port1>;
};
};
};
| Machine 드라이버 방식 | 장점 | 단점 | 사용 시점 |
|---|---|---|---|
| C 코드 Machine 드라이버 | 완전한 제어, 복잡한 로직 구현 가능 | 보드마다 별도 코드 필요 | 복잡한 잭 감지, DPCM, 커스텀 이벤트 |
| simple-audio-card | DT만으로 구성, 간단 | 제한적 기능 (단일 DAI 링크) | 단순한 1:1 코덱 연결 |
| audio-graph-card | DT 포트/엔드포인트 모델, 멀티코덱 | 중간 수준 복잡도 | 멀티코덱, DT 표준 연결 |
| audio-graph-card2 | DPCM, 멀티 CPU DAI 지원 | 최신 커널 필요 (5.19+) | DPCM + 멀티코덱 |
Pop-Noise 방지와 전원 시퀀싱
오디오 경로의 전원을 켜거나 끌 때 발생하는 팝 노이즈(pop/click noise)는 사용자 경험에 큰 영향을 미칩니다. DAPM은 위젯의 전원 시퀀스를 자동으로 관리하지만, 하드웨어 특성에 따라 추가적인 지연과 순서 제어가 필요합니다.
/* Pop-noise 방지: Headphone Charge Pump 이벤트 핸들러 */
static int hp_charge_pump_event(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol,
int event)
{
struct snd_soc_component *comp = snd_soc_dapm_to_component(w->dapm);
struct my_codec_priv *priv = snd_soc_component_get_drvdata(comp);
switch (event) {
case SND_SOC_DAPM_PRE_PMU:
/* 1단계: Soft-start 활성화 (전류 제한) */
snd_soc_component_update_bits(comp, HP_CTRL,
HP_SOFT_START, HP_SOFT_START);
/* 2단계: Charge pump 활성화 */
snd_soc_component_update_bits(comp, HP_CTRL,
HP_CP_EN, HP_CP_EN);
break;
case SND_SOC_DAPM_POST_PMU:
/* 충전 완료 대기 (코덱 데이터시트 참조) */
msleep(100);
/* 출력 뮤트 해제 */
snd_soc_component_update_bits(comp, HP_CTRL,
HP_MUTE, 0);
break;
case SND_SOC_DAPM_PRE_PMD:
/* 출력 뮤트 (팝 방지) */
snd_soc_component_update_bits(comp, HP_CTRL,
HP_MUTE, HP_MUTE);
msleep(50);
break;
case SND_SOC_DAPM_POST_PMD:
/* Charge pump 비활성화 */
snd_soc_component_update_bits(comp, HP_CTRL,
HP_CP_EN | HP_SOFT_START, 0);
break;
}
return 0;
}
/* DAPM 위젯에 이벤트 핸들러 연결 */
SND_SOC_DAPM_SUPPLY_S("HP Charge Pump", 1, /* subseq=1: 전원 순서 지정 */
SND_SOC_NOPM, 0, 0,
hp_charge_pump_event,
SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU |
SND_SOC_DAPM_PRE_PMD | SND_SOC_DAPM_POST_PMD),
msleep() 값이 너무 작으면 팝 노이즈가 발생하고, 너무 크면 오디오 시작 지연이 발생합니다.
SND_SOC_DAPM_SUPPLY_S의 두 번째 인자(subseq)로 같은 레벨의 SUPPLY 위젯 간 순서를 제어할 수 있습니다.
Suspend/Resume과 전원 관리
시스템 Suspend/Resume 시 ASoC는 자동으로 오디오 카드의 전원 상태를 관리합니다. 코덱 레지스터(Register)는 regcache를 통해 저장/복원되며, DAPM 바이어스 레벨은 SND_SOC_BIAS_OFF로 전환됩니다. 그러나 일부 코덱은 추가적인 Suspend/Resume 처리가 필요합니다.
/* Codec component의 suspend/resume 콜백 */
static int my_codec_suspend(struct snd_soc_component *component)
{
struct my_codec_priv *priv = snd_soc_component_get_drvdata(component);
/* 레지스터 캐시 저장 (regmap이 자동 처리) */
regcache_cache_only(priv->regmap, true);
regcache_mark_dirty(priv->regmap);
/* 클럭 비활성화 */
clk_disable_unprepare(priv->mclk);
return 0;
}
static int my_codec_resume(struct snd_soc_component *component)
{
struct my_codec_priv *priv = snd_soc_component_get_drvdata(component);
int ret;
/* 클럭 재활성화 */
ret = clk_prepare_enable(priv->mclk);
if (ret) {
dev_err(component->dev, "MCLK 활성화 실패: %d\n", ret);
return ret;
}
/* 레지스터 캐시 → 하드웨어 동기화 */
regcache_cache_only(priv->regmap, false);
ret = regcache_sync(priv->regmap);
if (ret) {
dev_err(component->dev, "레지스터 복원 실패: %d\n", ret);
return ret;
}
return 0;
}
static const struct snd_soc_component_driver my_codec_component = {
.suspend = my_codec_suspend,
.resume = my_codec_resume,
.set_bias_level = my_codec_set_bias_level,
/* ... controls, widgets, routes ... */
};
pm_runtime_enable()을 probe에서 호출하고, SET_RUNTIME_PM_OPS()로 콜백을 등록하면 DAPM 바이어스가 SND_SOC_BIAS_OFF가 될 때 자동으로 Runtime Suspend가 트리거됩니다.
debugfs를 이용한 DAPM 디버깅
ASoC와 DAPM은 debugfs를 통해 풍부한 디버깅 정보를 제공합니다. /sys/kernel/debug/asoc/ 아래에서 카드, 컴포넌트, DAPM 위젯의 상태를 실시간(Real-time)으로 확인할 수 있습니다.
# ASoC 카드 목록 확인
$ cat /sys/kernel/debug/asoc/components
wm8731.1-001a
my-platform
my-i2s-dai
# DAPM 위젯 전원 상태 확인
$ cat /sys/kernel/debug/asoc/MyBoard-WM8731/dapm/codec/wm8731.1-001a/DAC
DAC: On (power: 1, connected: 1)
in "Playback" "AIF1 Playback"
out "Output Mixer" ""
# 전체 DAPM 위젯 상태 요약
$ cat /sys/kernel/debug/asoc/MyBoard-WM8731/dapm_pop_time
5ms
# 현재 바이어스 레벨 확인
$ cat /sys/kernel/debug/asoc/MyBoard-WM8731/dapm/codec/wm8731.1-001a/bias_level
On
# DAPM 위젯 목록과 전원 상태
$ cat /proc/asound/card0/codec#0/dapm_widgets
ADC: Off
DAC: On
Output Mixer: On
LHPOUT: On
RHPOUT: On
LLINEIN: Off
RLINEIN: Off
Input Mux: Off
Mic Bias: Off
# 코덱 레지스터 덤프 (regmap debugfs)
$ cat /sys/kernel/debug/regmap/1-001a/registers
00: 017
01: 017
02: 079
03: 079
04: 012
05: 008
06: 09f
07: 00a
08: 000
09: 001
# 실시간 DAPM 이벤트 추적 (ftrace)
$ echo 1 > /sys/kernel/debug/tracing/events/asoc/snd_soc_dapm_widget_power/enable
$ echo 1 > /sys/kernel/debug/tracing/events/asoc/snd_soc_bias_level_done/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
aplay-1234 [001] .... 12345.678: snd_soc_dapm_widget_power: widget=DAC power=1
aplay-1234 [001] .... 12345.679: snd_soc_dapm_widget_power: widget=Output Mixer power=1
aplay-1234 [001] .... 12345.680: snd_soc_bias_level_done: card=MyBoard-WM8731 level=On
| debugfs 경로 | 내용 | 용도 |
|---|---|---|
/sys/kernel/debug/asoc/components |
등록된 모든 ASoC 컴포넌트 | 드라이버 로딩 확인 |
.../dapm/<widget_name> |
개별 위젯 전원 상태, 연결 정보 | 경로 활성화 확인 |
.../dapm_pop_time |
팝 노이즈 방지 지연 시간 | Pop 관련 타이밍 튜닝 |
.../bias_level |
코덱 바이어스 레벨 | 전원 상태 확인 |
/sys/kernel/debug/regmap/<dev>/registers |
코덱 레지스터 값 | 하드웨어 상태 확인 |
ftrace asoc/snd_soc_dapm_* |
DAPM 이벤트 실시간 추적 | 전원 시퀀스 디버깅 |
- 소리 없음: DAPM 위젯 상태 확인 → 경로의 모든 위젯이 "On"인지 확인. 하나라도 "Off"이면 해당 위젯의 전원 조건(레귤레이터, 클럭, SUPPLY 의존성) 점검.
- 팝 노이즈:
dapm_pop_time값 조정,PRE_PMU/PRE_PMD이벤트에서 지연 시간 확인. - Suspend 후 소리 없음:
regcache_sync()호출 확인, 클럭 재활성화 순서 점검. - 잭 감지 안됨: GPIO 방향/풀업/풀다운 설정 확인,
/proc/interrupts에서 IRQ 카운트 확인.
커널 소스 구조
ASoC 관련 소스 코드는 커널 트리의 sound/soc/ 디렉터리에 위치합니다:
| 디렉터리/파일 | 역할 |
|---|---|
sound/soc/soc-core.c |
ASoC 코어: 카드/DAI/컴포넌트 등록, DAPM 통합 |
sound/soc/soc-dapm.c |
DAPM 코어: 위젯 그래프, 전원 시퀀싱, 경로 추적 |
sound/soc/soc-pcm.c |
PCM 연산: hw_params, trigger, DPCM 관리 |
sound/soc/soc-jack.c |
잭 감지: GPIO/IRQ 잭 이벤트 처리 |
sound/soc/soc-component.c |
컴포넌트 연산 래퍼 |
sound/soc/codecs/ |
코덱 드라이버 (wm8731.c, rt5640.c, tas2562.c 등) |
sound/soc/generic/ |
simple-audio-card, audio-graph-card 범용 드라이버 |
sound/soc/fsl/ |
NXP/Freescale SoC 오디오 (i.MX I2S, SAI) |
sound/soc/samsung/ |
Samsung Exynos SoC 오디오 |
sound/soc/rockchip/ |
Rockchip SoC 오디오 |
sound/soc/intel/ |
Intel HD Audio / SOF 통합 |
include/sound/soc.h |
ASoC 핵심 구조체/매크로(Macro) 정의 |
include/sound/soc-dapm.h |
DAPM 위젯/라우트 매크로 정의 |
struct snd_soc_dapm_widget 필드 분석
DAPM의 핵심 자료구조인 snd_soc_dapm_widget은 오디오 경로 그래프의 노드(Node)를 표현합니다. 커널 소스 include/sound/soc-dapm.h에 정의되어 있으며, 각 필드가 전원 시퀀싱과 경로 추적에 어떻게 사용되는지 이해하는 것이 DAPM 내부 동작 파악의 출발점입니다.
/* include/sound/soc-dapm.h — 주요 필드 발췌 */
struct snd_soc_dapm_widget {
enum snd_soc_dapm_type id; /* 위젯 타입: INPUT, OUTPUT, DAC, ADC, MIXER 등 */
const char *name; /* 위젯 이름 (debugfs 표시, route 매칭 키) */
const char *sname; /* 스트림 이름: "Playback", "Capture" 등 */
struct snd_soc_dapm_context *dapm; /* 소속 DAPM 컨텍스트 (컴포넌트별) */
struct snd_soc_component *codec; /* (레거시) 소속 코덱 컴포넌트 */
struct snd_soc_component *platform; /* (레거시) 소속 플랫폼 컴포넌트 */
/* 레지스터 제어 */
int reg; /* 전원 제어 레지스터 오프셋 (-1이면 비가상) */
unsigned char shift; /* 레지스터 내 비트 위치 */
unsigned int mask; /* 비트 마스크 */
unsigned int on_val; /* 전원 ON 시 기록할 값 */
unsigned int off_val; /* 전원 OFF 시 기록할 값 */
unsigned char invert; /* 비트 반전 여부 (active-low) */
/* 전원 상태 */
unsigned int power:1; /* 현재 전원 상태 (0=OFF, 1=ON) */
unsigned int active:1; /* 스트림 활성 여부 */
unsigned int connected:1; /* 유효 경로 연결 여부 */
unsigned int new_power:1; /* 다음 동기화 시 적용할 전원 상태 */
unsigned int force:1; /* 강제 전원 ON (debugfs override) */
/* 그래프 연결 (경로 리스트) */
struct list_head edges[2]; /* [0]=source 경로, [1]=sink 경로 */
/* 엔드포인트 캐시 */
int endpoints[2]; /* [0]=sink 도달, [1]=source 도달 (-1=미계산) */
/* 이벤트 콜백 */
int (*event)(struct snd_soc_dapm_widget *,
struct snd_kcontrol *, int);
unsigned short event_flags; /* PRE_PMU | POST_PMU | PRE_PMD | POST_PMD */
/* 시퀀싱 */
int subseq; /* 동일 타입 내 세부 순서 */
struct list_head dirty; /* 변경된 위젯 리스트 (dapm_dirty) */
struct list_head power_list; /* 전원 시퀀싱 리스트 */
};
코드 설명
id— 위젯의 종류를 나타내는 열거형(Enum)입니다.snd_soc_dapm_input,snd_soc_dapm_dac,snd_soc_dapm_mixer등의 값이 있으며, 이 값에 따라 DAPM 코어가 전원 시퀀싱 순서와 처리 방식을 결정합니다.name과sname—name은 위젯의 고유 이름으로 route 매칭과 debugfs 출력에 사용됩니다.sname(스트림 이름)은 PCM 스트림과 위젯을 연결하는 키입니다. 예를 들어 DAC 위젯의sname이"Playback"이면 재생 스트림 시작 시 이 위젯이 활성화됩니다.reg,shift,mask— 하드웨어 레지스터의 특정 비트를 조작하여 위젯의 전원을 제어합니다.reg가SND_SOC_NOPM(-1)이면 소프트웨어 전용 위젯으로 레지스터 쓰기 없이 상태만 추적합니다.power,new_power—power는 현재 하드웨어에 반영된 전원 상태이고,new_power는dapm_power_widgets()가 그래프 워크로 계산한 다음 상태입니다. 두 값이 다르면 실제 전원 전환이 발생합니다.edges[2]— 위젯에 연결된 경로(snd_soc_dapm_path)의 이중 연결 리스트입니다.edges[SND_SOC_DAPM_DIR_IN]은 이 위젯으로 들어오는 경로,edges[SND_SOC_DAPM_DIR_OUT]은 나가는 경로입니다.endpoints[2]— 그래프 워크 최적화용 캐시입니다. 양쪽 엔드포인트(소스/싱크)에 도달 가능한지를 캐시하여 매번 전체 그래프를 탐색하지 않아도 됩니다.-1은 아직 계산되지 않았음을 의미합니다.dirty— 상태가 변경된 위젯은 이 리스트에 추가되어 다음snd_soc_dapm_sync()호출 시 처리됩니다.
struct snd_soc_dapm_path 필드 분석
snd_soc_dapm_path는 두 위젯 사이의 오디오 경로(간선)를 표현합니다. Route 정의에서 생성되며, DAPM 그래프의 간선(Edge) 역할을 합니다.
/* include/sound/soc-dapm.h — snd_soc_dapm_path 주요 필드 */
struct snd_soc_dapm_path {
const char *name; /* 경로 이름 (MUX 선택지, MIXER 스위치명) */
/* 연결된 위젯 */
struct snd_soc_dapm_widget *source; /* 신호 출발 위젯 (오디오 소스) */
struct snd_soc_dapm_widget *sink; /* 신호 도착 위젯 (오디오 싱크) */
/* 연결 상태 */
unsigned int connect:1; /* 현재 연결 여부 (MUX/MIXER 선택 상태) */
unsigned int walking:1; /* 그래프 워크 중 순환 방지 플래그 */
unsigned int weak:1; /* 약한 경로: 전원 결정에 영향 없음 */
unsigned int is_supply:1; /* SUPPLY 위젯 연결 경로 */
/* 그래프 리스트 */
struct list_head list_node[2]; /* source/sink 위젯의 edges[] 연결 */
struct list_head list; /* 카드 전체 경로 리스트 */
/* kcontrol 연결 (MUX/MIXER용) */
struct snd_kcontrol *kcontrol; /* 이 경로를 제어하는 ALSA control */
};
코드 설명
source와sink— 오디오 신호의 방향을 정의합니다. 예를 들어 route{"Output Mixer", NULL, "DAC"}에서source는 DAC 위젯,sink는 Output Mixer 위젯입니다. DAPM은 sink에서 source 방향으로 역추적하여 활성 경로를 판별합니다.connect— MUX의 현재 선택이나 MIXER 스위치의 on/off 상태를 반영합니다. 직접 연결(NULL control)은 항상1입니다. 이 값이0이면 그래프 워크 시 이 경로를 건너뜁니다.walking— 그래프 순회(Graph Walk) 중 순환(Cycle)을 방지하는 플래그입니다. 경로를 따라갈 때 설정하고, 탐색이 끝나면 해제합니다.weak— 약한 연결은 엔드포인트 도달 가능성에 영향을 주지 않습니다. 보조 모니터링 경로 등에 사용됩니다.is_supply— SUPPLY 위젯(전원, 클럭)과의 연결을 표시합니다. 공급 경로는 오디오 신호 경로와 별도로 처리되어 의존성 기반 전원 제어에 사용됩니다.list_node[2]— source 위젯의edges[SND_SOC_DAPM_DIR_OUT]과 sink 위젯의edges[SND_SOC_DAPM_DIR_IN]에 각각 연결되어 양방향 탐색을 가능하게 합니다.
struct snd_soc_dapm_context 주요 필드
snd_soc_dapm_context는 컴포넌트별 DAPM 상태를 관리하는 컨텍스트 구조체입니다. 각 코덱/플랫폼 컴포넌트마다 하나씩 존재하며, 카드 전체에도 하나의 루트 컨텍스트가 있습니다.
/* include/sound/soc-dapm.h — snd_soc_dapm_context 주요 필드 */
struct snd_soc_dapm_context {
struct device *dev; /* 소속 디바이스 (로깅용) */
enum snd_soc_bias_level bias_level; /* 현재 바이어스 레벨: OFF/STANDBY/PREPARE/ON */
enum snd_soc_bias_level suspend_bias_level; /* suspend 전 바이어스 레벨 */
unsigned int idle_bias_off:1; /* 1이면 유휴 시 BIAS_OFF (기본: STANDBY 유지) */
unsigned int suspend_bias_off:1; /* 1이면 suspend 시 BIAS_OFF */
struct snd_soc_dapm_update *update; /* 진행 중인 kcontrol 업데이트 정보 */
struct list_head list; /* 카드의 dapm_list에 연결 */
struct snd_soc_component *component; /* 소속 컴포넌트 */
struct snd_soc_card *card; /* 소속 카드 */
int (*set_bias_level)(struct snd_soc_dapm_context *,
enum snd_soc_bias_level); /* 바이어스 레벨 전환 콜백 */
};
코드 설명
bias_level— 코덱/컴포넌트의 현재 전원 상태를 4단계로 관리합니다.dapm_power_widgets()가 활성 위젯 유무에 따라 이 레벨을 자동 전환합니다.idle_bias_off— 기본적으로 DAPM은 유휴 상태에서SND_SOC_BIAS_STANDBY를 유지하여 빠른 복귀를 보장합니다. 이 플래그를 설정하면 유휴 시BIAS_OFF로 전환하여 전력을 더 절약하지만 재시작이 느려집니다. 모바일 코덱에서 배터리 절약을 위해 자주 사용됩니다.suspend_bias_off— 시스템 suspend 시BIAS_OFF로 전환할지 결정합니다. DAPM은snd_soc_suspend()에서 이 플래그를 참조하여 코덱 전원을 완전히 차단합니다.update— MIXER/MUX kcontrol 변경 시 해당 정보를 담아dapm_power_widgets()에 전달합니다. 레지스터 쓰기를 전원 시퀀싱 과정에서 적절한 시점에 수행하기 위한 메커니즘입니다.component와card— DAPM 컨텍스트가 어떤 컴포넌트에 속하는지, 그리고 전체 카드 레벨의 동기화에 참여하기 위한 참조입니다.
dapm_power_widgets() 구현 분석
dapm_power_widgets()는 DAPM의 핵심 엔진입니다. 위젯 그래프를 순회하여 각 위젯의 전원 상태를 결정하고, 올바른 순서로 전원을 켜거나 끄는 시퀀싱을 수행합니다. 이 함수는 snd_soc_dapm_sync()에서 호출됩니다.
/* sound/soc/soc-dapm.c — dapm_power_widgets() 핵심 로직 (간략화) */
static int dapm_power_widgets(struct snd_soc_dapm_context *dapm, int event)
{
struct snd_soc_card *card = dapm->card;
struct snd_soc_dapm_widget *w;
LIST_HEAD(up_list); /* 전원 ON 위젯 리스트 */
LIST_HEAD(down_list); /* 전원 OFF 위젯 리스트 */
trace_snd_soc_dapm_start(card);
/* 1단계: 변경된 위젯부터 그래프 워크로 전원 상태 계산 */
list_for_each_entry(w, &card->dapm_dirty, dirty) {
dapm_power_one_widget(w, &up_list, &down_list);
}
/* 2단계: 각 DAPM 컨텍스트의 바이어스 레벨 결정 */
list_for_each_entry(d, &card->dapm_list, list) {
if (dapm_idle_bias_off(d))
d->target_bias_level = SND_SOC_BIAS_OFF;
else
d->target_bias_level = SND_SOC_BIAS_STANDBY;
}
/* 활성 위젯이 있는 컨텍스트는 BIAS_ON */
list_for_each_entry(w, &up_list, power_list)
w->dapm->target_bias_level = SND_SOC_BIAS_ON;
/* 3단계: BIAS_PREPARE 전환 (ON으로 올라가는 컨텍스트) */
list_for_each_entry(d, &card->dapm_list, list)
if (d->target_bias_level == SND_SOC_BIAS_ON)
dapm_pre_sequence_async(d, event); /* → set_bias_level(PREPARE) */
/* 4단계: 전원 시퀀싱 실행 — 순서대로 ON/OFF */
dapm_seq_run(card, &down_list, event, false); /* 먼저 OFF */
dapm_seq_run(card, &up_list, event, true); /* 다음 ON */
/* 5단계: BIAS 최종 전환 */
list_for_each_entry(d, &card->dapm_list, list)
dapm_post_sequence_async(d, event); /* → set_bias_level(ON/STANDBY/OFF) */
/* dirty 리스트 정리 */
list_for_each_entry_safe(w, n, &card->dapm_dirty, dirty)
list_del_init(&w->dirty);
trace_snd_soc_dapm_done(card);
return 0;
}
코드 설명
- 1단계 (그래프 워크) —
dapm_dirty리스트에 있는 변경된 위젯들을 순회합니다. 각 위젯에서dapm_power_one_widget()을 호출하여 그래프를 양방향으로 탐색하고, 소스(입력)와 싱크(출력) 엔드포인트에 모두 도달 가능한 위젯만new_power = 1로 설정합니다. 전원이 변경될 위젯은up_list또는down_list에 추가됩니다. - 2단계 (바이어스 결정) — 각 컴포넌트의 DAPM 컨텍스트에 대해 목표 바이어스 레벨을 결정합니다. 활성 위젯이 하나라도 있으면
BIAS_ON, 없으면idle_bias_off설정에 따라BIAS_OFF또는BIAS_STANDBY가 됩니다. - 3단계 (PREPARE 전환) —
BIAS_ON으로 올라가는 컨텍스트는 먼저BIAS_PREPARE로 전환합니다. 이 단계에서 MCLK 활성화 등 준비 작업이 수행됩니다. - 4단계 (시퀀싱) —
dapm_seq_run()이 위젯 타입 순서(dapm_up_seq[]/dapm_down_seq[])에 따라 레지스터를 일괄 기록합니다. OFF를 먼저 처리한 후 ON을 처리하여 전원 과도기에 불필요한 전력 소비를 방지합니다. - 5단계 (BIAS 최종) — 모든 위젯 전원 전환이 끝난 후 바이어스 레벨을 최종 목표로 전환합니다.
DAPM 전원 시퀀싱 호출 체인
DAPM의 전원 동기화는 사용자 공간의 ALSA control 변경이나 스트림 시작/종료에서 시작되어 최종적으로 코덱 레지스터 쓰기까지 이어집니다. 아래 다이어그램은 전체 호출 체인을 보여줍니다.
코드 설명
- 트리거 — 사용자 공간에서 ALSA mixer control을 변경하거나(예: 볼륨 조절, MUX 선택 변경), PCM 스트림을 시작/종료하면 관련 위젯이
dapm_dirty리스트에 추가되고snd_soc_dapm_sync()가 호출됩니다. - dapm_power_one_widget() — 각 위젯에서 양방향 그래프 워크를 수행합니다. 위젯이 소스 엔드포인트(예: INPUT, MIC)와 싱크 엔드포인트(예: OUTPUT, HP, SPK) 모두에 연결된 활성 경로가 있으면
new_power = 1로 설정합니다. - dapm_seq_run() — 위젯 타입별 우선순위 배열(
dapm_up_seq[]/dapm_down_seq[])에 따라 정렬된 순서로 전원을 전환합니다. 같은 레지스터를 사용하는 위젯들은dapm_seq_run_coalesced()에서 비트를 병합하여 단일 I2C/SPI 트랜잭션으로 처리합니다. - 이벤트 콜백 —
SND_SOC_DAPM_PRE_PMU이벤트는 레지스터 쓰기 전에,SND_SOC_DAPM_POST_PMU는 쓰기 후에 호출됩니다. pop-noise 방지를 위한 지연이나 외부 앰프 제어 등을 이 콜백에서 수행합니다.
DAPM Route/Path 연결 내부
snd_soc_dapm_add_routes()는 드라이버가 정의한 route 배열을 받아 위젯 그래프에 경로(Path)를 생성합니다. 내부적으로 snd_soc_dapm_add_route()를 반복 호출하여 snd_soc_dapm_path 구조체를 할당하고 위젯 간 간선을 연결합니다.
/* sound/soc/soc-dapm.c — route 추가 내부 로직 (간략화) */
static int snd_soc_dapm_add_route(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_route *route)
{
struct snd_soc_dapm_widget *wsource, *wsink;
struct snd_soc_dapm_path *path;
/* 위젯 이름으로 source/sink 검색 */
wsink = dapm_find_widget(dapm, route->sink, true);
wsource = dapm_find_widget(dapm, route->source, true);
if (!wsink || !wsource)
return -ENODEV;
/* path 구조체 할당 */
path = kzalloc(sizeof(*path), GFP_KERNEL);
path->source = wsource;
path->sink = wsink;
path->connected = 1; /* 직접 연결은 기본 활성 */
/* 위젯 타입에 따른 연결 방식 */
switch (wsink->id) {
case snd_soc_dapm_mux:
/* MUX: kcontrol 현재 선택과 비교하여 connect 결정 */
dapm_connect_mux(dapm, path, route->control, wsource);
break;
case snd_soc_dapm_mixer:
case snd_soc_dapm_mixer_named_ctl:
/* MIXER: 스위치 kcontrol과 연결 */
dapm_connect_mixer(dapm, path, route->control);
break;
case snd_soc_dapm_supply:
/* SUPPLY: 전원 의존성 경로 표시 */
path->is_supply = 1;
break;
default:
/* 직접 연결: connect = 1 유지 */
break;
}
/* 양방향 간선 등록 */
list_add(&path->list_node[SND_SOC_DAPM_DIR_IN],
&wsink->edges[SND_SOC_DAPM_DIR_IN]);
list_add(&path->list_node[SND_SOC_DAPM_DIR_OUT],
&wsource->edges[SND_SOC_DAPM_DIR_OUT]);
list_add(&path->list, &dapm->card->paths);
/* 영향받는 위젯을 dirty 리스트에 추가 */
dapm_mark_dirty(wsource, "route added");
dapm_mark_dirty(wsink, "route added");
return 0;
}
코드 설명
dapm_find_widget()— route의 sink/source 문자열과 일치하는 위젯을 DAPM 컨텍스트에서 검색합니다. 두 번째 인자true는 카드 전체 범위에서 검색하도록 하여, 서로 다른 컴포넌트 간의 연결(예: CPU DAI → Codec DAI)도 지원합니다.dapm_connect_mux()— MUX 위젯의 경우 현재 kcontrol 선택 값을 읽어 이 경로가 활성인지 판별합니다. 예를 들어 Input Mux의 현재 선택이 "Mic"이면 "Line" 경로의connect는0이 됩니다.dapm_connect_mixer()— MIXER 위젯의 경우 해당 스위치 kcontrol을 경로에 연결합니다. 스위치 ON/OFF에 따라connect상태가 동적으로 변경됩니다.is_supply— SUPPLY 타입 위젯과의 연결은 오디오 신호 경로가 아니라 전원 의존성을 나타냅니다. DAPM은 이 경로를 별도로 처리하여 의존 위젯이 활성화될 때 공급 위젯을 먼저 켭니다.list_add()— 경로를 source 위젯의 출력 간선 리스트(edges[DIR_OUT])와 sink 위젯의 입력 간선 리스트(edges[DIR_IN])에 동시에 등록하여 양방향 탐색이 가능하게 합니다.dapm_mark_dirty()— 경로가 추가된 위젯을 dirty로 표시하여 다음snd_soc_dapm_sync()에서 전원 상태를 재계산하도록 합니다.
위젯 타입별 전원 제어
DAPM은 위젯 타입에 따라 전원 시퀀싱 순서를 다르게 적용합니다. 커널 소스의 dapm_up_seq[]와 dapm_down_seq[] 배열이 이 순서를 정의합니다.
| 위젯 타입 | Power-Up 순서 | Power-Down 순서 | 전원 제어 방식 |
|---|---|---|---|
SUPPLY |
0 (가장 먼저) | 14 (가장 나중) | 의존 위젯보다 먼저 ON, 나중에 OFF |
REGULATOR_SUPPLY |
1 | 13 | regulator_enable() / disable() |
CLOCK_SUPPLY |
1 | 13 | clk_prepare_enable() / disable_unprepare() |
DAC |
5 | 10 | 레지스터 비트: reg + shift |
ADC |
6 | 9 | 레지스터 비트: reg + shift |
MIXER |
7 | 8 | 레지스터 비트 + kcontrol 스위치 |
PGA |
8 | 7 | 레지스터 비트 (볼륨/게인 앰프) |
MUX |
7 | 8 | 레지스터 비트 + kcontrol 선택 |
OUTPUT / HP / SPK |
10 | 5 | 소프트웨어 전용 (핀 상태 추적) |
INPUT / MIC |
10 | 5 | 소프트웨어 전용 (핀 상태 추적) |
PRE |
2 | 12 | 이벤트 콜백만 (pre-charge 등) |
POST |
11 | 2 | 이벤트 콜백만 (post-cleanup 등) |
아래 코드는 dapm_seq_run()이 위젯 타입별 순서에 따라 전원을 제어하는 핵심 로직입니다.
/* sound/soc/soc-dapm.c — dapm_seq_run() 핵심 로직 (간략화) */
static void dapm_seq_run(struct snd_soc_card *card,
struct list_head *list,
int event, bool power_up)
{
struct snd_soc_dapm_widget *w, *n;
int *seq = power_up ? dapm_up_seq : dapm_down_seq;
int cur_sort = -1;
int cur_reg = SND_SOC_NOPM;
LIST_HEAD(pending);
/* 리스트는 (sort_order, reg) 기준으로 사전 정렬됨 */
list_for_each_entry_safe(w, n, list, power_list) {
/* 정렬 순서나 레지스터가 바뀌면 이전 배치 실행 */
if (seq[w->id] != cur_sort || w->reg != cur_reg) {
if (!list_empty(&pending))
dapm_seq_run_coalesced(card, &pending);
cur_sort = seq[w->id];
cur_reg = w->reg;
}
/* PRE 이벤트 콜백 */
if (w->event && (w->event_flags &
(power_up ? SND_SOC_DAPM_PRE_PMU : SND_SOC_DAPM_PRE_PMD)))
w->event(w, NULL,
power_up ? SND_SOC_DAPM_PRE_PMU : SND_SOC_DAPM_PRE_PMD);
list_move_tail(&w->power_list, &pending);
}
/* 마지막 배치 실행 */
if (!list_empty(&pending))
dapm_seq_run_coalesced(card, &pending);
}
코드 설명
- 시퀀싱 배열 —
dapm_up_seq[]는 Power-Up 시 위젯 타입별 순서를 정의합니다. SUPPLY(0) → PRE(2) → DAC(5) → ADC(6) → MIXER(7) → PGA(8) → OUTPUT(10) → POST(11) 순서로 전원을 켭니다.dapm_down_seq[]는 반대 순서입니다. - 레지스터 병합 — 같은 레지스터를 사용하는 연속된 위젯들을
pending리스트에 모아둡니다.dapm_seq_run_coalesced()에서 이 위젯들의 비트를 OR 연산으로 병합하여 단일 레지스터 쓰기로 처리합니다. I2C/SPI 버스 트랜잭션 수를 최소화하는 핵심 최적화입니다. - 이벤트 콜백 — 레지스터 쓰기 전(
PRE_PMU/PRE_PMD)에 위젯의 이벤트 콜백을 호출합니다. 레지스터 쓰기 후(POST_PMU/POST_PMD)는dapm_seq_run_coalesced()내부에서 호출됩니다. - Power-Up 순서의 의미 — SUPPLY → DAC/ADC → MIXER → PGA → OUTPUT 순서로 켜는 이유는 신호 경로의 하류(downstream)부터 전원을 안정화한 후 상류(upstream)로 진행하여 pop-noise와 글리치(Glitch)를 방지하기 위함입니다. Power-Down은 반대로 출력부터 끕니다.
DAPM 위젯 상세
DAPM 위젯은 오디오 경로 그래프(Audio Path Graph)의 노드(Node)입니다. 각 위젯 타입은 하드웨어 블록의 특성에 따라 전원 제어 방식이 다르며, 매크로(Macro)를 통해 선언적으로 정의합니다.
위젯 매크로와 파라미터
모든 DAPM 위젯 매크로는 공통 파라미터를 공유합니다.
| 파라미터 | 설명 | 예시 |
|---|---|---|
wname | 위젯 고유 이름 (라우트에서 참조) | "Left DAC" |
wreg | 전원 제어 레지스터 주소 | WM8731_PWR, SND_SOC_NOPM |
wshift | 레지스터 내 비트 위치 | 4 |
winvert | 비트 극성 반전 (1=OFF, 0=ON) | 1 |
wevent | 이벤트 콜백 함수 포인터 | dac_event |
wflags | 이벤트 플래그 (PRE_PMU, POST_PMD 등) | SND_SOC_DAPM_PRE_PMU |
/* 위젯 타입별 선언 매크로 예시 */
/* INPUT: 외부 입력 핀 — 레지스터 제어 없음 (소프트웨어 전용) */
SND_SOC_DAPM_INPUT("MICIN"),
/* OUTPUT: 외부 출력 핀 */
SND_SOC_DAPM_OUTPUT("HPOUT"),
/* MIC: 마이크 바이어스(Mic Bias) 전원 포함 */
SND_SOC_DAPM_MIC("Internal Mic", mic_bias_event),
/* HP: 헤드폰 — 잭 감지와 연동 */
SND_SOC_DAPM_HP("Headphone Jack", NULL),
/* SPK: 스피커 출력 — 앰프 제어와 연동 */
SND_SOC_DAPM_SPK("Speaker", spk_amp_event),
/* DAC: D/A 변환기 — 레지스터 비트로 전원 제어 */
SND_SOC_DAPM_DAC("Left DAC", "Playback",
WM8731_PWR, 4, 1), /* reg, shift, invert */
/* ADC: A/D 변환기 */
SND_SOC_DAPM_ADC("Right ADC", "Capture",
WM8731_PWR, 1, 1),
/* PGA: Programmable Gain Amplifier */
SND_SOC_DAPM_PGA("Mic Boost",
CODEC_BOOST_REG, 0, 0,
NULL, 0), /* kcontrols, num_kcontrols */
/* MIXER: 여러 입력을 혼합 — 각 입력에 kcontrol 스위치 */
SND_SOC_DAPM_MIXER("Output Mixer",
CODEC_MIX_REG, 5, 0,
output_mixer_controls,
ARRAY_SIZE(output_mixer_controls)),
/* MUX: 여러 입력 중 하나만 선택 */
SND_SOC_DAPM_MUX("Input Mux",
SND_SOC_NOPM, 0, 0,
&input_mux_control),
/* SUPPLY: 전원 공급 위젯 — 의존 위젯보다 먼저 ON */
SND_SOC_DAPM_SUPPLY("VREF",
CODEC_PWR_REG, 7, 0,
vref_event,
SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD),
/* REGULATOR_SUPPLY: Linux Regulator Framework 연동 */
SND_SOC_DAPM_REGULATOR_SUPPLY("AVDD", 0, 0),
/* CLOCK_SUPPLY: Common Clock Framework 연동 */
SND_SOC_DAPM_CLOCK_SUPPLY("MCLK"),
/* AIF_IN/AIF_OUT: DAI 인터페이스 스트림 연결 */
SND_SOC_DAPM_AIF_IN("AIF1RX", "Playback",
0, SND_SOC_NOPM, 0, 0),
SND_SOC_DAPM_AIF_OUT("AIF1TX", "Capture",
0, SND_SOC_NOPM, 0, 0),
/* SWITCH: 단일 ON/OFF 스위치 */
SND_SOC_DAPM_SWITCH("HP Switch",
SND_SOC_NOPM, 0, 0,
&hp_switch_control),
/* PRE/POST: 스트림 전후 이벤트 전용 (레지스터 없음) */
SND_SOC_DAPM_PRE("Pre-charge", precharge_event),
SND_SOC_DAPM_POST("Post-cleanup", postcleanup_event),
코드 설명
- SND_SOC_NOPM —
wreg에 이 값을 넣으면 DAPM 코어가 레지스터 쓰기를 건너뛰고 이벤트 콜백으로만 제어합니다. 소프트웨어 전용 위젯이나 GPIO 제어 위젯에 사용합니다. - invert 파라미터 — 일부 코덱은 "Power Down" 비트를 사용합니다 (1=OFF).
invert=1로 설정하면 DAPM이 비트를 반전하여 올바르게 제어합니다. - 스트림 이름 — DAC/ADC/AIF 위젯의 두 번째 문자열은 ALSA PCM 스트림 이름과 매칭됩니다.
"Playback"이나"Capture"를 지정하면 해당 PCM 스트림이 열릴 때 자동으로 활성화됩니다. - MIXER vs MUX — MIXER는 여러 입력을 동시에 혼합(합산)하고, MUX는 하나만 선택합니다. MIXER의 각 입력은 kcontrol 스위치로 개별 ON/OFF되고, MUX는 enum kcontrol로 하나만 선택됩니다.
- SUPPLY 이벤트 — SUPPLY 위젯의 PRE_PMU 콜백에서 안정화 대기 시간(settling time)을 삽입하고, POST_PMD 콜백에서 방전(discharge)을 수행하는 패턴이 일반적입니다.
이벤트 콜백 상세
이벤트 콜백은 전원 전환의 각 단계에서 호출되며, 하드웨어 초기화/정리, 안정화 지연, 글리치 방지 등을 처리합니다.
| 이벤트 플래그 | 호출 시점 | 주요 용도 |
|---|---|---|
SND_SOC_DAPM_PRE_PMU | Power-Up 레지스터 쓰기 전 | 클럭 활성화, 레귤레이터 ON, 프리차지 |
SND_SOC_DAPM_POST_PMU | Power-Up 레지스터 쓰기 후 | 안정화 대기, 캘리브레이션, 언뮤트 |
SND_SOC_DAPM_PRE_PMD | Power-Down 레지스터 쓰기 전 | 뮤트, 소프트 램프다운 |
SND_SOC_DAPM_POST_PMD | Power-Down 레지스터 쓰기 후 | 클럭 비활성화, 레귤레이터 OFF, 방전 |
SND_SOC_DAPM_PRE_REG | 레지스터 쓰기 직전 | 특수 레지스터 시퀀스 |
SND_SOC_DAPM_POST_REG | 레지스터 쓰기 직후 | 레지스터 쓰기 후 검증 |
/* 실전 예: HP 앰프 이벤트 콜백 — pop-noise 방지 */
static int hp_amp_event(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol, int event)
{
struct snd_soc_component *comp = snd_soc_dapm_to_component(w->dapm);
switch (event) {
case SND_SOC_DAPM_PRE_PMU:
/* 프리차지: 출력을 VMID로 바이어스 */
snd_soc_component_update_bits(comp,
HP_CTRL_REG, HP_PRECHARGE, HP_PRECHARGE);
msleep(20); /* 캐패시터 충전 대기 */
break;
case SND_SOC_DAPM_POST_PMU:
/* 프리차지 해제 후 정상 출력 */
snd_soc_component_update_bits(comp,
HP_CTRL_REG, HP_PRECHARGE, 0);
msleep(10);
/* 언뮤트 */
snd_soc_component_update_bits(comp,
HP_CTRL_REG, HP_MUTE, 0);
break;
case SND_SOC_DAPM_PRE_PMD:
/* 뮤트 → 소프트 램프다운 */
snd_soc_component_update_bits(comp,
HP_CTRL_REG, HP_MUTE, HP_MUTE);
msleep(5);
break;
case SND_SOC_DAPM_POST_PMD:
/* 출력 방전 */
snd_soc_component_update_bits(comp,
HP_CTRL_REG, HP_DISCHARGE, HP_DISCHARGE);
msleep(30);
snd_soc_component_update_bits(comp,
HP_CTRL_REG, HP_DISCHARGE, 0);
break;
}
return 0;
}
DAPM 라우팅 상세
snd_soc_dapm_route 구조체는 위젯 간의 오디오 경로를 정의합니다. DAPM 코어는 이 라우트 배열(Route Array)을 파싱하여 방향 그래프(Directed Graph)를 구축하고, 활성 경로(Active Path) 탐색의 기반으로 사용합니다.
라우트 구조체
struct snd_soc_dapm_route {
const char *sink; /* 목적지 위젯 이름 */
const char *control; /* kcontrol 이름 (MIXER/MUX용, NULL이면 직접 연결) */
const char *source; /* 소스 위젯 이름 */
int (*connected)(struct snd_soc_dapm_widget *src,
struct snd_soc_dapm_widget *sink);
/* 동적 연결 판단 콜백 (선택) */
};
/* 라우트 정의 예시 */
static const struct snd_soc_dapm_route codec_routes[] = {
/* 직접 연결: Source → Sink */
{ "Left ADC", NULL, "Input PGA" },
/* MIXER 연결: control 이름으로 스위치 매칭 */
{ "Output Mixer", "DAC", "Left DAC" },
{ "Output Mixer", "Line", "LINEIN" },
{ "Output Mixer", "Mic", "Mic Boost" },
/* MUX 연결: enum 선택지 이름으로 매칭 */
{ "Input Mux", "Line", "LINEIN" },
{ "Input Mux", "Mic", "MICIN" },
/* SUPPLY 의존성: 전원 공급 경로 */
{ "Left DAC", NULL, "VREF" },
{ "Left DAC", NULL, "AVDD" },
{ "Left DAC", NULL, "MCLK" },
/* 동적 연결 콜백: 런타임에 경로 활성 여부 결정 */
{ "Speaker", NULL, "SPK Amp",
.connected = spk_connected },
};
머신 드라이버 오디오 맵
머신 드라이버(Machine Driver)는 보드 레벨의 물리적 연결을 오디오 맵(Audio Map)으로 정의합니다. 코덱의 외부 핀(INPUT/OUTPUT 위젯)과 보드의 잭/마이크/스피커를 연결합니다.
/* 머신 드라이버 오디오 맵 — 보드 레벨 연결 */
static const struct snd_soc_dapm_route board_routes[] = {
/* 보드 잭 → 코덱 입력 핀 */
{ "MICIN", NULL, "Headset Mic" },
{ "LINEIN", NULL, "Line In Jack" },
/* 코덱 출력 핀 → 보드 잭 */
{ "Headphone Jack", NULL, "HPOUT" },
{ "Ext Speaker", NULL, "SPKOUT" },
};
/* 머신 드라이버에서 등록 */
static struct snd_soc_card my_card = {
.name = "my-board",
.dapm_routes = board_routes,
.num_dapm_routes = ARRAY_SIZE(board_routes),
.dapm_widgets = board_widgets,
.num_dapm_widgets = ARRAY_SIZE(board_widgets),
.fully_routed = true, /* 미연결 핀 자동 비활성화 */
};
코드 설명
- fully_routed = true — 이 플래그를 설정하면 라우트에 등장하지 않는 코덱 핀은 자동으로 비활성화됩니다. 보드에서 사용하지 않는 코덱 출력이 있을 때 불필요한 전력 소비를 방지합니다.
- 라우트 방향 —
{ sink, control, source }순서입니다. 신호는 source에서 sink 방향으로 흐릅니다. 재생 경로에서는 CPU DAI → DAC → Mixer → Amp → 출력 핀 순이고, 캡처 경로에서는 입력 핀 → PGA → ADC → CPU DAI 순입니다. - 동적 연결 콜백 —
.connected콜백이 0을 반환하면 경로가 비활성화됩니다. GPIO 상태나 런타임 조건에 따라 경로를 동적으로 결정할 때 사용합니다.
동적 라우트 추가/삭제
런타임에 라우트를 추가하거나 삭제하여 오디오 경로를 동적으로 변경할 수 있습니다.
/* 런타임 라우트 추가 */
static const struct snd_soc_dapm_route ext_amp_route[] = {
{ "Ext Speaker", NULL, "SPK Amp" },
};
/* 외부 앰프 보드가 감지되면 경로 추가 */
snd_soc_dapm_add_routes(&card->dapm,
ext_amp_route, ARRAY_SIZE(ext_amp_route));
snd_soc_dapm_sync(&card->dapm);
/* 경로 삭제 */
snd_soc_dapm_del_routes(&card->dapm,
ext_amp_route, ARRAY_SIZE(ext_amp_route));
snd_soc_dapm_sync(&card->dapm);
전력 시퀀싱
DAPM의 전력 시퀀싱(Power Sequencing)은 위젯 타입별 정렬 순서, 이벤트 콜백 타이밍, 레지스터 병합 최적화의 세 가지 메커니즘이 결합되어 동작합니다.
바이어스 레벨 전환과 시퀀싱
DAPM은 위젯 전원 상태에 따라 코덱 전체의 바이어스 레벨을 자동으로 전환합니다. 바이어스 전환은 위젯 시퀀싱 전후에 발생합니다.
| 바이어스 레벨 | 전환 조건 | 시퀀싱 관계 |
|---|---|---|
SND_SOC_BIAS_ON | 스트림 활성 (PCM RUNNING) | 모든 위젯 Power-Up 후 전환 |
SND_SOC_BIAS_PREPARE | 스트림 시작/종료 과도기 | Power-Up 전, Power-Down 후 거침 |
SND_SOC_BIAS_STANDBY | 스트림 없음, 코덱 깨어있음 | 최소 전력으로 빠른 복귀 가능 |
SND_SOC_BIAS_OFF | 완전 비활성 | 모든 위젯 Power-Down 후 전환 |
코덱 드라이버 DAPM 구현
실전 코덱 드라이버에서 DAPM을 구현하는 전체 패턴을 살펴봅니다. WM8731 코덱을 예시로 위젯 정의, 라우트 정의, 이벤트 콜백, 바이어스 레벨 제어까지 완전한 구현을 보여줍니다.
/* 코덱 드라이버 DAPM 구현 완전 예제 (WM8731 기반) */
/* 1. kcontrol 정의: MIXER 입력 스위치 */
static const struct snd_kcontrol_new output_mixer_controls[] = {
SOC_DAPM_SINGLE("Line Bypass Switch", WM8731_APANA, 3, 1, 0),
SOC_DAPM_SINGLE("Mic Sidetone Switch", WM8731_APANA, 5, 1, 0),
SOC_DAPM_SINGLE("HiFi Playback Switch", WM8731_APANA, 4, 1, 0),
};
/* 2. MUX enum 정의: 입력 선택 */
static const char *const input_sel_text[] = {
"Line", "Mic",
};
static SOC_ENUM_SINGLE_DECL(input_sel_enum,
WM8731_APANA, 2, input_sel_text);
static const struct snd_kcontrol_new input_mux_ctrl =
SOC_DAPM_ENUM("Input Select", input_sel_enum);
/* 3. DAPM 위젯 배열 */
static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = {
/* 전원 공급 */
SND_SOC_DAPM_SUPPLY("VMID", WM8731_PWR, 0, 1,
NULL, 0),
SND_SOC_DAPM_SUPPLY("Mic Bias", WM8731_PWR, 1, 1,
NULL, 0),
/* 입력 */
SND_SOC_DAPM_INPUT("MICIN"),
SND_SOC_DAPM_INPUT("LLINEIN"),
SND_SOC_DAPM_INPUT("RLINEIN"),
/* 입력 선택 */
SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0,
&input_mux_ctrl),
/* ADC */
SND_SOC_DAPM_ADC("ADC", "HiFi Capture",
WM8731_PWR, 2, 1),
/* DAC */
SND_SOC_DAPM_DAC("DAC", "HiFi Playback",
WM8731_PWR, 3, 1),
/* 출력 믹서 */
SND_SOC_DAPM_MIXER("Output Mixer", WM8731_PWR, 4, 1,
output_mixer_controls,
ARRAY_SIZE(output_mixer_controls)),
/* 출력 */
SND_SOC_DAPM_OUTPUT("LHPOUT"),
SND_SOC_DAPM_OUTPUT("RHPOUT"),
SND_SOC_DAPM_OUTPUT("LOUT"),
SND_SOC_DAPM_OUTPUT("ROUT"),
};
/* 4. DAPM 라우트 배열 */
static const struct snd_soc_dapm_route wm8731_intercon[] = {
/* 전원 의존성 */
{ "ADC", NULL, "VMID" },
{ "DAC", NULL, "VMID" },
{ "Output Mixer", NULL, "VMID" },
/* 입력 경로 */
{ "Input Mux", "Line", "LLINEIN" },
{ "Input Mux", "Mic", "MICIN" },
{ "MICIN", NULL, "Mic Bias" },
{ "ADC", NULL, "Input Mux" },
/* 출력 경로 */
{ "Output Mixer", "HiFi Playback Switch", "DAC" },
{ "Output Mixer", "Line Bypass Switch", "LLINEIN" },
{ "Output Mixer", "Mic Sidetone Switch", "MICIN" },
/* 출력 핀 */
{ "LHPOUT", NULL, "Output Mixer" },
{ "RHPOUT", NULL, "Output Mixer" },
{ "LOUT", NULL, "Output Mixer" },
{ "ROUT", NULL, "Output Mixer" },
};
/* 5. set_bias_level 콜백 */
static int wm8731_set_bias_level(struct snd_soc_component *comp,
enum snd_soc_bias_level level)
{
switch (level) {
case SND_SOC_BIAS_ON:
break;
case SND_SOC_BIAS_PREPARE:
break;
case SND_SOC_BIAS_STANDBY:
if (snd_soc_component_get_bias_level(comp) ==
SND_SOC_BIAS_OFF) {
/* OFF → STANDBY: 기본 클럭/VMID 활성화 */
regcache_sync(wm8731->regmap);
}
break;
case SND_SOC_BIAS_OFF:
/* 모든 전원 OFF, 레지스터 캐시만 유지 */
snd_soc_component_write(comp, WM8731_PWR, 0xffff);
regcache_mark_dirty(wm8731->regmap);
break;
}
return 0;
}
/* 6. 컴포넌트 드라이버에 DAPM 등록 */
static const struct snd_soc_component_driver wm8731_component = {
.set_bias_level = wm8731_set_bias_level,
.dapm_widgets = wm8731_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(wm8731_dapm_widgets),
.dapm_routes = wm8731_intercon,
.num_dapm_routes = ARRAY_SIZE(wm8731_intercon),
.suspend_bias_off = 1,
.idle_bias_on = 1,
.use_pmdown_time = 1,
.endianness = 1,
};
코드 설명
- suspend_bias_off — 이 플래그가 1이면 시스템 Suspend 시 바이어스를 OFF까지 낮춥니다. 코덱이 완전 전원 차단을 지원할 때 사용합니다.
- idle_bias_on — 1이면 스트림이 없어도 바이어스를 STANDBY에 유지합니다. 바이어스 전환 지연이 큰 코덱에서 빠른 재생 시작을 위해 사용합니다.
- use_pmdown_time — PCM 닫힌 후 일정 시간(기본 5초) 동안 전원을 유지하여 빠른 연속 재생 시 pop-noise를 방지합니다.
- regcache_sync — OFF → STANDBY 전환 시 레지스터 캐시를 하드웨어에 복원합니다. regmap 프레임워크가 변경된 레지스터만 I2C/SPI로 전송합니다.
머신 드라이버 DAPM 실전
머신 드라이버는 보드 레벨의 물리적 연결, 잭 감지, 외부 앰프 제어를 담당합니다. 코덱과 CPU DAI 사이의 연결과 보드 고유의 DAPM 위젯/라우트를 정의합니다.
보드 레벨 위젯과 라우트
/* 보드 레벨 DAPM 위젯 */
static const struct snd_soc_dapm_widget board_widgets[] = {
SND_SOC_DAPM_HP("Headphone Jack", NULL),
SND_SOC_DAPM_SPK("Ext Speaker", ext_spk_event),
SND_SOC_DAPM_MIC("Headset Mic", NULL),
SND_SOC_DAPM_MIC("Internal Mic", NULL),
SND_SOC_DAPM_LINE("Line In", NULL),
};
/* 보드 레벨 라우트: 코덱 핀 ↔ 보드 잭 */
static const struct snd_soc_dapm_route board_routes[] = {
{ "Headphone Jack", NULL, "LHPOUT" },
{ "Headphone Jack", NULL, "RHPOUT" },
{ "Ext Speaker", NULL, "LOUT" },
{ "Ext Speaker", NULL, "ROUT" },
{ "MICIN", NULL, "Headset Mic" },
{ "LLINEIN", NULL, "Line In" },
{ "RLINEIN", NULL, "Line In" },
};
잭 감지와 DAPM 핀 연동
/* 잭 감지 콜백 — 잭 상태에 따라 DAPM 핀 활성화/비활성화 */
static struct snd_soc_jack hp_jack;
static struct snd_soc_jack_pin hp_jack_pins[] = {
{
.pin = "Headphone Jack",
.mask = SND_JACK_HEADPHONE,
},
{
.pin = "Ext Speaker",
.mask = SND_JACK_HEADPHONE,
.invert = 1, /* HP 삽입 시 스피커 OFF */
},
{
.pin = "Headset Mic",
.mask = SND_JACK_MICROPHONE,
},
};
/* 머신 드라이버 init 콜백에서 잭 등록 */
static int my_board_init(struct snd_soc_pcm_runtime *rtd)
{
struct snd_soc_card *card = rtd->card;
/* 잭 생성 */
snd_soc_card_jack_new_pins(card, "Headphone Jack",
SND_JACK_HEADSET,
&hp_jack, hp_jack_pins,
ARRAY_SIZE(hp_jack_pins));
/* GPIO 기반 잭 감지 */
struct snd_soc_jack_gpio hp_jack_gpio = {
.name = "hp-detect",
.report = SND_JACK_HEADSET,
.debounce_time = 200, /* ms */
.invert = 0,
};
snd_soc_jack_add_gpios(&hp_jack, 1, &hp_jack_gpio);
/* 초기 상태: 미연결 핀 비활성화 */
snd_soc_dapm_disable_pin(&card->dapm, "Line In");
snd_soc_dapm_sync(&card->dapm);
return 0;
}
코드 설명
- snd_soc_jack_pin.invert —
invert=1이면 잭 보고 상태의 반대로 핀을 제어합니다. 헤드폰이 삽입되면(SND_JACK_HEADPHONE보고) 스피커 핀이 비활성화됩니다. 이것이 헤드폰/스피커 자동 전환의 핵심입니다. - debounce_time — GPIO 바운싱(Bouncing)으로 인한 오탐을 방지합니다. 200ms는 일반적인 3.5mm 잭의 접촉 안정화 시간입니다.
- snd_soc_dapm_disable_pin — 사용하지 않는 핀을 비활성화하면 해당 핀으로 이어지는 경로의 위젯이 불필요하게 전원을 소비하지 않습니다.
DAPM 디버깅
DAPM 디버깅은 위젯 전원 상태, 경로 연결 상태, 바이어스 레벨, 이벤트 콜백 실행 순서를 종합적으로 분석하는 과정입니다.
debugfs 상세 활용
# DAPM debugfs 위치
ls /sys/kernel/debug/asoc/
# 카드별 DAPM 상태
ls /sys/kernel/debug/asoc/my-board/
# 전체 위젯 전원 상태 확인
cat /sys/kernel/debug/asoc/my-board/dapm_pop_time
# → pop-noise 방지 타이밍 설정
# 코덱 컴포넌트의 위젯 상태
cat /sys/kernel/debug/asoc/my-board/wm8731.1-001a/dapm/ADC
# 출력 예시:
# ADC: On in 2 out 1
# stream ADC Active
# in "Input Mux" "VMID"
# out "AIF1TX"
# 특정 위젯의 경로 추적
cat /sys/kernel/debug/asoc/my-board/wm8731.1-001a/dapm/Output\ Mixer
# Output Mixer: On in 3 out 4
# in "DAC" "HiFi Playback Switch" [connected]
# in "LLINEIN" "Line Bypass Switch" [not connected]
# in "MICIN" "Mic Sidetone Switch" [not connected]
# out "LHPOUT"
# out "RHPOUT"
# out "LOUT"
# out "ROUT"
# 바이어스 레벨 확인
cat /sys/kernel/debug/asoc/my-board/wm8731.1-001a/dapm_bias_level
# → On / Prepare / Standby / Off
# 핀 상태 확인
grep -r "" /sys/kernel/debug/asoc/my-board/wm8731.1-001a/dapm/ 2>/dev/null | \
grep -E "(On|Off)" | sort
ftrace로 DAPM 이벤트 추적
# DAPM 관련 커널 함수 추적
echo 1 > /sys/kernel/tracing/tracing_on
# DAPM 전원 변경 이벤트 추적
echo 'snd_soc_dapm*' > /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
# 오디오 재생 시작/종료 후 트레이스 확인
cat /sys/kernel/tracing/trace | head -30
# aplay-1234 [001] .... 123.456789: snd_soc_dapm_sync_unlocked
# aplay-1234 [001] .... 123.456790: dapm_power_widgets
# aplay-1234 [001] .... 123.456795: dapm_seq_run
# regmap 레지스터 쓰기 추적 (코덱 I2C/SPI 트래픽)
echo 'regmap*' > /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
aplay -D hw:0,0 /dev/zero -d 1
cat /sys/kernel/tracing/trace | grep -i "regmap_write\|regmap_read"
일반적인 DAPM 문제와 해결
| 증상 | 원인 | 진단 방법 | 해결 |
|---|---|---|---|
| 소리가 나오지 않음 | 경로의 위젯 중 하나가 OFF | debugfs에서 전체 경로의 위젯 ON/OFF 확인 | 누락된 라우트 추가 또는 핀 활성화 |
| Pop-noise 발생 | 시퀀싱 순서 또는 타이밍 부적절 | ftrace로 이벤트 콜백 순서/시간 확인 | PRE_PMU/POST_PMD 콜백에 msleep 추가 |
| 위젯이 항상 ON | SUPPLY 의존성 루프 또는 비활성화 안 됨 | debugfs에서 위젯의 in/out 카운트 확인 | 불필요한 라우트 제거, 핀 disable |
| 바이어스 레벨 전환 안 됨 | set_bias_level 콜백 오류 | dapm_bias_level 파일 확인 | 콜백에서 레지스터 쓰기 순서 점검 |
| Suspend 후 소리 안 남 | regcache 동기화 누락 | Resume 후 레지스터 값 비교 | STANDBY 전환 시 regcache_sync 호출 |
동적 오디오 라우팅과 UCM
UCM (Use Case Manager)은 ALSA 사용자 공간 도구로, 런타임에 DAPM 경로를 전환하는 표준화된 방법을 제공합니다. 코덱의 kcontrol과 DAPM 핀을 조합하여 다양한 사용 사례(Use Case)를 정의합니다.
UCM 개념
| UCM 용어 | 설명 | DAPM 매핑 |
|---|---|---|
| Verb | 사용 시나리오 (HiFi, Voice, VoIP) | PCM 스트림과 기본 라우팅 |
| Device | 오디오 장치 (Speaker, Headphones, DMIC) | DAPM 핀 활성화/비활성화 |
| Modifier | 추가 기능 (CaptureVoice, EchoCancellation) | kcontrol 변경, 추가 경로 활성화 |
| SectionSequence | 설정 순서 (cdev, EnableSequence) | regmap 쓰기, msleep, DAPM sync |
# UCM 설정 파일 위치
ls /usr/share/alsa/ucm2/
# 현재 카드의 UCM 상태 확인
alsaucm -c my-board listverbs
alsaucm -c my-board list _devices/HiFi
# Verb 설정: HiFi 모드 활성화
alsaucm -c my-board set _verb HiFi
# Device 활성화: 헤드폰 출력
alsaucm -c my-board set _enadev Headphones
# Device 전환: 스피커로 변경
alsaucm -c my-board set _disdev Headphones
alsaucm -c my-board set _enadev Speaker
UCM 설정 파일 예시
# /usr/share/alsa/ucm2/my-board/HiFi.conf
SectionVerb {
EnableSequence [
cdev "hw:my-board"
# DAPM 핀 활성화
cset "name='Left DAC Switch' on"
cset "name='Right DAC Switch' on"
]
DisableSequence [
cdev "hw:my-board"
cset "name='Left DAC Switch' off"
cset "name='Right DAC Switch' off"
]
Value {
PlaybackPCM "hw:my-board,0"
CapturePCM "hw:my-board,0"
}
}
SectionDevice."Headphones" {
EnableSequence [
cdev "hw:my-board"
cset "name='Headphone Jack' on"
cset "name='Speaker' off"
]
DisableSequence [
cdev "hw:my-board"
cset "name='Headphone Jack' off"
]
Value {
PlaybackChannels 2
JackControl "Headphone Jack"
JackHWMute "Speaker"
}
}
SectionDevice."Speaker" {
EnableSequence [
cdev "hw:my-board"
cset "name='Speaker' on"
cset "name='Headphone Jack' off"
]
DisableSequence [
cdev "hw:my-board"
cset "name='Speaker' off"
]
Value {
PlaybackChannels 2
}
}
참고 링크
- Kernel Docs — ASoC (ALSA System on Chip) — ASoC 프레임워크 공식 문서 색인
- ASoC Overview — 코덱, 플랫폼, 머신 드라이버 3계층 구조 개요
- ASoC Codec Driver — 코덱 드라이버 개발 가이드
- ASoC Platform Driver — DMA/CPU DAI 플랫폼 드라이버
- ASoC Machine Driver — 보드별 머신 드라이버, DAI 링크 설정
- ASoC DAPM — Dynamic Audio Power Management 상세 설명
- ASoC DPCM — Dynamic PCM (FE/BE 라우팅) 프레임워크
- Codec-to-Codec Links — 코덱 간 직접 연결 (CPU 개입 없는 오디오 경로)
- ASoC Jack Detection — 잭 감지 및 버튼 이벤트 처리
- ASoC Clocking — MCLK, BCLK, LRCLK 클럭 설정
- ALSA Project 공식 사이트 — ASoC가 기반하는 ALSA 프레임워크
- alsa-lib API Reference — UCM(Use Case Manager) 포함 유저스페이스 API
- PipeWire — 차세대 오디오/비디오 서버 (임베디드 오디오 지원)
- alsa-devel 메일링 리스트 — ASoC/DAPM 커널 패치 및 리뷰 토론
- LWN.net — Sound — 리눅스 오디오 관련 심층 기술 기사
- I2S Bus Specification — 디지털 오디오 데이터 전송 표준 (ASoC DAI 기반)
- Kernel Docs — Regmap API — 코덱 레지스터 접근 추상화 계층
- Device Tree Specification — 오디오 카드/코덱 DT 바인딩 기반
sound/soc/soc-core.c— ASoC 코어 (카드 등록, DAI 링크 바인딩)sound/soc/soc-dapm.c— DAPM 엔진 (위젯 그래프, 전원 시퀀싱)sound/soc/soc-pcm.c— ASoC PCM 오퍼레이션, DPCM 런타임sound/soc/soc-component.c— 코덱/플랫폼 컴포넌트 공통 APIsound/soc/soc-topology.c— 토폴로지(Topology) 로더 (UCM/DSP 파이프라인)sound/soc/codecs/— 코덱 드라이버 모음 (rt5682, wcd938x, cs42l42 등)sound/soc/generic/— simple-card, audio-graph-card 범용 머신 드라이버include/sound/soc.h— ASoC 코어 API 헤더include/sound/soc-dapm.h— DAPM 위젯/루트 매크로 정의
관련 문서
- ALSA — ASoC의 기반이 되는 리눅스 오디오 서브시스템 (PCM, Control, Timer)
- 디바이스 드라이버 — 드라이버 모델, probe/remove 생명주기
- Device Tree — 오디오 카드 바인딩, 포트/엔드포인트 모델
- I2C / SPI / GPIO — 코덱 제어 버스 (I2C/SPI), 잭 감지 GPIO
- DMA Engine — 오디오 DMA 전송, dmaengine_pcm 통합
- 전원 관리 — Runtime PM, Suspend/Resume, 레귤레이터
- 인터럽트(Interrupt) — 오디오 DMA 완료, 코덱 IRQ, 잭 감지 인터럽트
- Regmap — 코덱 레지스터 접근 추상화 (디바이스 드라이버 문서의 regmap 섹션 참조)