ASoC & DAPM

ASoC와 DAPM을 임베디드 오디오 전력 최적화 관점에서 심층 분석합니다. Codec/Platform/Machine 드라이버 분리 구조, DAI 링크 구성과 포맷 협상, DAPM 위젯·라우트 그래프를 통한 자동 전원 게이팅, 클럭/레귤레이터/잭 감지 연동, Device Tree 카드 바인딩과 멀티코덱 설계, low-power 오디오 경로 구성, pop-noise 방지 및 경로 전환 안정화, debugfs와 오디오 계측 기반 튜닝까지 보드 레벨 오디오 스택 구현에 필요한 실전 내용을 다룹니다.

전제 조건: 디바이스 드라이버DMA 기초, ALSA 서브시스템 개요를 먼저 읽으세요. ASoC는 ALSA 위에 구축된 프레임워크이므로, PCM/Control 기본 구조와 DMA 버퍼(Buffer) 흐름을 먼저 파악해야 합니다.
일상 비유: ASoC는 오케스트라와 비슷합니다. Machine 드라이버가 지휘자(악보와 배치 결정), Codec 드라이버가 악기 연주자(소리 변환), Platform 드라이버가 무대 장비(소리 전달 경로)입니다. DAPM은 조명 감독처럼 연주 중인 파트만 조명을 켜고 쉬는 파트는 끕니다.

핵심 요약

  • 3계층 분리 — Codec, Platform(CPU DAI+DMA), Machine 드라이버를 독립적으로 개발하고 DAI 링크로 연결합니다.
  • DAI 포맷 협상 — I2S/TDM/PDM 포맷, 마스터/슬레이브, 클럭 극성을 양쪽 DAI가 합의합니다.
  • DAPM 자동 전원 — 위젯 그래프를 추적하여 활성 경로의 컴포넌트만 전원을 켜고 나머지는 자동 차단합니다.
  • 클럭/레귤레이터 연동 — MCLK, BCLK, 전원 레귤레이터를 DAPM 이벤트에 맞춰 관리합니다.
  • 잭 감지 — 헤드폰/마이크 삽입을 감지하여 DAPM 경로를 동적으로 전환합니다.

단계별 이해

  1. ASoC 아키텍처 파악
    Codec/Platform/Machine 3계층 구조와 각 드라이버의 역할을 이해합니다.
  2. DAI 링크 구성
    Machine 드라이버에서 CPU DAI와 Codec DAI를 연결하고, 포맷/클럭 모드를 설정합니다.
  3. DAPM 위젯과 라우트 정의
    오디오 경로를 위젯 그래프로 모델링하고 전원 시퀀싱을 구현합니다.
  4. 디버깅(Debugging)과 튜닝
    debugfs로 DAPM 상태를 확인하고, pop-noise 방지와 전력 최적화를 수행합니다.

ASoC 프레임워크

ASoC (ALSA System on Chip)는 임베디드/SoC 플랫폼을 위한 고수준 오디오 프레임워크입니다. 코덱, 플랫폼 (CPU DAI + DMA), 머신 (보드 특화) 드라이버를 분리하여 재사용성을 극대화합니다.

ASoC 아키텍처

ASoC는 세 계층으로 구성됩니다:

Machine Driver (Board-specific) snd_soc_card, dai_link[], DAPM routes, jack detection Example: sound/soc/fsl/imx-wm8731.c Platform Driver (CPU DAI) SoC Audio Interface I2S/PCM/TDM controller DMA engine integration Clock management Codec Driver Audio Codec Chip Regmap (I2C/SPI control) DAPM widgets Kcontrols (mixer) Hardware SoC I2S/TDM/PCM DMA Controller Hardware Codec IC (WM8731, RT5640...) DAC/ADC, Amplifier I2S/TDM Bus (BCLK, LRCLK, SDATA) I2C/SPI Control Machine driver는 dai_link로 CPU DAI와 Codec DAI를 연결하여 하나의 PCM 디바이스 생성

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 그래프 시각화

Capture Path LLINEIN RLINEIN MICIN Mic Bias (SUPPLY) Input Mux Line / Mic / CD (MUX) Input PGA (Volume) ADC (A/D Conv) AIF IN (I2S to CPU) Playback Path AIF OUT (I2S from CPU) DAC (D/A Conv) Output Mixer DAC + Bypass + Sidetone (MIXER) Line Bypass HP Amp (PGA) SPK Amp (PGA) LHPOUT (Headphone) RHPOUT (Headphone) LSPK (Speaker) RSPK (Speaker) DAPM은 활성 경로의 모든 widget만 전원 ON (사용 안 하는 DAC/ADC/Amp 자동 OFF → 전력 절감) Powered ON Powered OFF External I/O Audio Interface
자동 전원 관리: 사용자가 재생 또는 녹음을 시작하면 DAPM은 활성 경로를 추적하여 필요한 widget만 전원을 켭니다. 예: 헤드폰 재생 시 DAC → Output Mixer → HP Amp → LHPOUT/RHPOUT 경로의 widget만 ON, 스피커 앰프와 ADC는 OFF 상태 유지.

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,
};
MCLK 비율 가이드: 일반적으로 MCLK = 256 × fs (샘플레이트)를 사용합니다. 48kHz 재생 시 MCLK = 12.288MHz, 44.1kHz 시 11.2896MHz입니다. 코덱 내부 PLL/FLL을 사용하면 단일 고정 클럭(예: 24.576MHz)에서 여러 샘플레이트를 지원할 수 있습니다. Device Tree의 assigned-clocksassigned-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;
}
잭 감지와 DAPM 핀: 잭 핀 이름("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: Frontend ↔ Backend 동적 연결 User Space PCM Device 0 (Playback) PCM Device 1 (Capture) Frontend DAI 0 (CPU ↔ DSP) Frontend DAI 1 (CPU ↔ DSP) DSP Mixing EQ / Effects Routing Backend DAI 0 (I2S → Headphone) Backend DAI 1 (I2S → Speaker) Backend DAI 2 (PDM → DMIC) Codec A Codec B DMIC DAPM 위젯 경로에 따라 Backend 동적 연결/해제 Frontend DSP Backend Codec/HW
/* 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),
    },
};
DPCM vs 일반 PCM: 일반 ASoC에서는 하나의 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 방지: 전원 ON 시퀀스 t 1. VMID/VREF 바이어스 전압 안정화 ~50ms 2. DAC ON 디지털 출력 시작 ~10ms 3. Mixer ON 경로 활성화 ~5ms 4. HP Amp ON 출력 드라이버 활성화 ~100ms (charge) 5. Unmute 출력 뮤트 해제 소리 출력 시작 Power-down: 역순 시퀀스 Mute → HP Amp OFF → Mixer OFF → DAC OFF → VMID/VREF OFF (PRE_PMD 이벤트에서 뮤트 후 지연, POST_PMD에서 전원 차단) Pop-Noise 방지 핵심 원칙 1. 전원 ON: 내부 → 외부 순서 (VREF 먼저, 출력 마지막) 2. 전원 OFF: 외부 → 내부 순서 (출력 먼저 뮤트, VREF 마지막) 3. 각 단계 사이 충분한 지연 (코덱 데이터시트 참조, 보통 10~100ms)
/* 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),
Pop-noise 디버깅 주의: 코덱 데이터시트에 명시된 전원 시퀀싱 지연 시간을 반드시 준수하세요. 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 ... */
};
Runtime PM과 ASoC: 시스템 Suspend 외에도 Runtime PM을 활용하면 오디오 스트림이 없을 때 코덱을 자동으로 저전력 모드로 전환할 수 있습니다. 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 코어가 전원 시퀀싱 순서와 처리 방식을 결정합니다.
  • namesnamename은 위젯의 고유 이름으로 route 매칭과 debugfs 출력에 사용됩니다. sname(스트림 이름)은 PCM 스트림과 위젯을 연결하는 키입니다. 예를 들어 DAC 위젯의 sname"Playback"이면 재생 스트림 시작 시 이 위젯이 활성화됩니다.
  • reg, shift, mask — 하드웨어 레지스터의 특정 비트를 조작하여 위젯의 전원을 제어합니다. regSND_SOC_NOPM(-1)이면 소프트웨어 전용 위젯으로 레지스터 쓰기 없이 상태만 추적합니다.
  • power, new_powerpower는 현재 하드웨어에 반영된 전원 상태이고, new_powerdapm_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 */
};
코드 설명
  • sourcesink — 오디오 신호의 방향을 정의합니다. 예를 들어 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()에 전달합니다. 레지스터 쓰기를 전원 시퀀싱 과정에서 적절한 시점에 수행하기 위한 메커니즘입니다.
  • componentcard — 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 변경이나 스트림 시작/종료에서 시작되어 최종적으로 코덱 레지스터 쓰기까지 이어집니다. 아래 다이어그램은 전체 호출 체인을 보여줍니다.

DAPM Power Sync Call Chain kcontrol / stream event snd_soc_dapm_sync() dapm_power_widgets() 1. dapm_power_one_widget() 그래프 워크 → new_power 계산 2. target_bias_level 결정 OFF / STANDBY / ON 3. dapm_pre_sequence_async() set_bias_level(PREPARE) 4. dapm_seq_run() down_list (OFF 먼저) 타입 순서: OUT→MIX→PGA→ADC→DAC up_list (ON 다음) 타입 순서: SUPPLY→DAC→ADC→MIX→PGA dapm_seq_run_coalesced() 같은 레지스터 → 비트 병합 soc_dapm_update_bits() 5. dapm_post_sequence_async() set_bias_level(ON/STANDBY/OFF) widget->event() 콜백 PRE_PMU → reg write → POST_PMU 이벤트 콜백은 시퀀싱 중 호출
코드 설명
  • 트리거 — 사용자 공간에서 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" 경로의 connect0이 됩니다.
  • 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_NOPMwreg에 이 값을 넣으면 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_PMUPower-Up 레지스터 쓰기 전클럭 활성화, 레귤레이터 ON, 프리차지
SND_SOC_DAPM_POST_PMUPower-Up 레지스터 쓰기 후안정화 대기, 캘리브레이션, 언뮤트
SND_SOC_DAPM_PRE_PMDPower-Down 레지스터 쓰기 전뮤트, 소프트 램프다운
SND_SOC_DAPM_POST_PMDPower-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)은 위젯 타입별 정렬 순서, 이벤트 콜백 타이밍, 레지스터 병합 최적화의 세 가지 메커니즘이 결합되어 동작합니다.

Power-Up 시퀀스 (좌 → 우) SUPPLY 순서: 0 REG/CLK 순서: 1 PRE 순서: 2 DAC/ADC 순서: 5-6 MIX/MUX 순서: 7 PGA 순서: 8 OUT/HP/SPK 순서: 10 Power-Down 시퀀스 (좌 → 우, Power-Up의 역순) OUT/HP/SPK 순서: 5 PGA 순서: 7 MIX/MUX 순서: 8 ADC/DAC 순서: 9-10 POST 순서: 2 REG/CLK 순서: 13 SUPPLY 순서: 14 Power-Up: 전원 공급 → 변환기 → 신호 경로 → 출력 (pop-noise 방지) Power-Down: 출력 → 신호 경로 → 변환기 → 전원 공급 (역순으로 안전 종료) 이벤트 콜백 타이밍 PRE_PMU 레지스터 쓰기 전 레지스터 쓰기 (비트 ON/OFF) POST_PMU 레지스터 쓰기 후 같은 레지스터의 위젯들은 pending 리스트에 모아 단일 쓰기로 병합

바이어스 레벨 전환과 시퀀싱

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.invertinvert=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
    }
}
UCM / PulseAudio set _verb HiFi set _enadev Speaker ALSA kcontrol amixer cset ... 핀 enable/disable DAPM Core dapm_power_widgets() 경로 탐색 → 전원 결정 dapm_seq_run() DAC → Mixer → SPK Amp ON (활성 경로) DAC → Mixer → HP Amp OFF (핀 비활성) 잭 감지 이벤트 흐름 GPIO IRQ snd_soc_jack_report() 핀 상태 변경 dapm_sync() 위젯 전원 전환 HP 삽입 → Speaker OFF, HP ON | HP 제거 → HP OFF, Speaker ON (자동) 활성 경로 비활성 경로 제어 흐름

참고 링크

커널 공식 문서:
외부 참고 자료:
커널 소스 참고 경로:
  • 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 — 코덱/플랫폼 컴포넌트 공통 API
  • sound/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 위젯/루트 매크로 정의