ASoC & DAPM
ASoC와 DAPM을 임베디드 오디오 전력 최적화 관점에서 심층 분석합니다. Codec/Platform/Machine 드라이버 분리 구조, DAI 링크 구성과 포맷 협상, DAPM 위젯·라우트 그래프를 통한 자동 전원 게이팅, 클럭/레귤레이터/잭 감지 연동, Device Tree 카드 바인딩과 멀티코덱 설계, low-power 오디오 경로 구성, pop-noise 방지 및 경로 전환 안정화, debugfs와 오디오 계측 기반 튜닝까지 보드 레벨 오디오 스택 구현에 필요한 실전 내용을 다룹니다.
전제 조건: 디바이스 드라이버와 DMA 문서를 먼저 읽으세요.
멀티미디어/가속기 경로는 대용량 버퍼 이동과 동기화가 성능의 핵심이므로, 메모리 경로와 큐 모델을 먼저 파악해야 합니다.
일상 비유: 이 주제는 영상 제작 파이프라인과 비슷합니다.
촬영·편집·인코딩 단계가 끊기지 않아야 결과가 나오듯이, 버퍼 큐와 하드웨어 스케줄링의 연속성이 중요합니다.
핵심 요약
- 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
- 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
- IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
- 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
- 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.
단계별 이해
- 장치 수명주기 확인
probe부터 remove까지 흐름을 점검합니다. - 비동기 경로 설계
IRQ, 워크큐, 타이머 역할을 분리합니다. - 자원 정합성 검증
DMA/클록/전원 참조를 교차 확인합니다. - 현장 조건 테스트
연결 끊김/복구/부하 상황을 재현합니다.
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 지연 |
SND_SOC_DAIFMT_DSP_B |
DSP Mode B | 지연 없음 |
SND_SOC_DAIFMT_AC97 |
AC'97 | 레거시 AC'97 버스 |
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의 핵심 기능으로, 오디오 경로를 기반으로 전원 도메인을 자동으로 켜고 끄는 지능형 전원 관리 시스템입니다. 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 |
라인 입출력 | 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은 전원 상태 변화 시 이벤트 콜백을 받을 수 있습니다:
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은 활성 경로를 추적하여 필요한 widget만 전원을 켭니다. 예: 헤드폰 재생 시 DAC → Output Mixer → HP Amp → LHPOUT/RHPOUT 경로의 widget만 ON, 스피커 앰프와 ADC는 OFF 상태 유지.
관련 문서
- ALSA — 리눅스 오디오 서브시스템
- I2C — 오디오 Codec 제어
- DMA Engine — 오디오 DMA 전송
- 전원 관리 — 디바이스 전력 제어