Linux音频三部曲(2):Linux ASoC驱动深度开发

本系列每篇文章的链接:

Linux音频三部曲(1):嵌入式 Linux 音频基础与硬件体系 - 知乎

Linux音频三部曲(2):Linux ASoC驱动深度开发 - 知乎

Linux音频三部曲(3):嵌入式 Linux 音频用户态开发:ALSA、PipeWire、WirePlumber 与 GStreamer 实战 - 知乎


本文基于Bootlin文档面向嵌入式Linux内核驱动开发、音频系统集成与硬件调试工程师。内容以相关内核代码、设备树配置、寄存器操作、DAPM电源管理、Regmap抽象为主,覆盖从ALSA到ASoC的演进、三大组件驱动实现、DT配置、DAPM、调试方法。

如有专业词汇翻译不当、内容理解有误的地方,还望指正。

一、从ALSA到ASoC:架构演进与设计目标

1.1 标准ALSA架构的局限

ALSA(Advanced Linux Sound Architecture) 于2002年随Linux 2.5内核合入主线,提供标准化音频API、播放/捕获流、kcontrol控件接口。标准ALSA设计面向PC架构,一张声卡对应一个设备、一个驱动,驱动内部耦合PCI总线、Codec、DMA、接口控制等全部逻辑。

这种架构在嵌入式场景存在明显缺陷:

  1. 代码无法复用:不同SoC与Codec组合需重新编写全量驱动;
  2. 组件耦合严重:Codec驱动与平台驱动绑定,无法独立维护;
  3. 不支持低功耗:缺乏细粒度音频路径电源管理;
  4. 无法适配多组件硬件:SoC+Codec+功放+MUX的模块化结构难以描述。

标准ALSA无法满足嵌入式音频可复用、低功耗、多组件、可配置的核心需求,因此ASoC框架被提出并合入内核。

1.2 ASoC架构定义与设计目标

ASoC全称ALSA System on Chip ,2006年合入主线,定位为标准ALSA之上的嵌入式增强层,对外保持原有用户空间API不变,对内实现硬件解耦与组件化。

ASoC核心设计目标:

  1. 组件化驱动分离:Codec、Platform(CPU DAI)、Machine三层独立;
  2. 跨平台复用:同一Codec驱动可适配任意SoC,同一DAI驱动可搭配任意Codec;
  3. 精细化低功耗:基于音频路径的动态电源管理DAPM;
  4. 设备树化配置:支持simple-audio-card等通用驱动,无需手写C语言机器驱动;
  5. 支持多链路、多通道、TDM、PDM等复杂嵌入式接口。

ASoC不修改用户空间ABI,原有aplay/arecord/alsamixer等工具无需改动即可使用。

1.3 ASoC三大核心组件

ASoC将音频系统严格拆分为三部分,遵循单一职责、解耦复用原则:

  1. Codec Driver:负责音频编解码器本身,包含寄存器配置、DAI格式、控件、DAPM、时钟;
  2. Platform Driver:包含CPU DAI控制器驱动(I2S/SAI/SSI等)与DMA驱动,无板级相关代码;
  3. Machine Driver:板级驱动,负责绑定Codec与CPU DAI、配置时钟主从、音频路由、GPIO、功放等。

Codec与Platform驱动高度复用,仅Machine驱动与具体电路板相关。这一结构是嵌入式音频驱动开发的基础。

二、ASoC核心数据结构与注册流程

2.1 核心结构体总览

ASoC驱动围绕一组标准化结构体展开,内核定义位于:

  • include/sound/soc.h
  • include/sound/soc-dai.h
  • include/sound/soc-component.h
  • include/sound/soc-dapm.h

关键结构体:

  1. struct snd_soc_card:代表一张声卡,是Machine驱动的核心载体;
  2. struct snd_soc_dai_link:描述一条CPU DAI与Codec DAI的连接链路;
  3. struct snd_soc_component:组件抽象,统一表示Codec、CPU DAI、功放、MUX;
  4. struct snd_soc_dai_driver:DAI接口驱动,包含格式、时钟、回调;
  5. struct snd_soc_dapm_widget :DAPM电源管理节点与路径;
  6. struct snd_kcontrol_new:ALSA控件(音量、静音、开关、枚举);
  7. struct regmap_config:I2C/SPI寄存器抽象配置。

2.2 声卡注册流程

ASoC声卡注册入口为Machine驱动,标准流程:

  1. 定义并填充struct snd_soc_card
  2. 定义struct snd_soc_dai_link,绑定CPU DAI与Codec DAI;
  3. 实现必要回调(hw_params、set_sysclk等);
  4. 调用devm_snd_soc_register_card注册声卡;
  5. 内核自动匹配并加载Codec与Platform组件;
  6. 初始化DAPM图、控件、路由;
  7. 向ALSA核心注册声卡,导出至用户空间。

注册失败常见原因:DAI匹配失败、时钟不可用、寄存器访问错误、DAPM路由未定义。

三、Codec驱动开发完整实现

Codec是音频硬件核心,Codec驱动是ASoC中最复杂的组件,负责寄存器控制、DAI通信、模拟路由、时钟管理、控件、DAPM

3.1 Codec驱动基本结构

现代内核(≥4.17)统一使用struct snd_soc_component_driver替代旧struct snd_soc_codec_driver,驱动通过devm_snd_soc_register_component注册。

典型注册代码:

复制代码
static const struct snd_soc_component_driver xxx_codec_driver = {
    .name           = "xxx-codec",
    .probe          = xxx_codec_probe,
    .remove         = xxx_codec_remove,
    .controls       = xxx_controls,
    .num_controls   = ARRAY_SIZE(xxx_controls),
    .dapm_widgets   = xxx_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(xxx_dapm_widgets),
    .dapm_routes    = xxx_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(xxx_dapm_routes),
    .set_sysclk     = xxx_set_sysclk,
    .set_pll        = xxx_set_pll,
    .set_bias_level = xxx_set_bias_level,
};

3.2 DAI驱动描述

每个Codec至少实现一个DAI,用于与CPU通信:

复制代码
static struct snd_soc_dai_driver xxx_dai = {
    .name = "xxx-hifi",
    .playback = {
        .stream_name    = "Playback",
        .channels_min   = 1,
        .channels_max   = 2,
        .rates          = SNDRV_PCM_RATE_8000_48000,
        .formats        = SNDRV_PCM_FMTBIT_S16_LE |
                          SNDRV_PCM_FMTBIT_S24_LE,
    },
    .capture = {
        .stream_name    = "Capture",
        .channels_min   = 1,
        .channels_max   = 2,
        .rates          = SNDRV_PCM_RATE_8000_48000,
        .formats        = SNDRV_PCM_FMTBIT_S16_LE,
    },
    .ops = &xxx_dai_ops,
};

3.3 DAI操作回调(dai_ops)

DAI ops是Codec与平台交互的核心接口,必须正确实现:

复制代码
struct snd_soc_dai_ops {
    int (*set_sysclk)(struct snd_soc_dai *dai, int clk_id,
                       unsigned int freq, int dir);
    int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
                    unsigned int freq_in, unsigned int freq_out);
    int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);
    int (*hw_params)(struct snd_pcm_substream *substream,
                     struct snd_pcm_hw_params *params,
                     struct snd_soc_dai *dai);
    int (*trigger)(struct snd_pcm_substream *substream,
                    int cmd, struct snd_soc_dai *dai);
    int (*mute_stream)(struct snd_soc_dai *dai, int mute, int stream);
};

关键回调说明:

  • set_fmt:配置接口格式(I2S/Left-J/DSP-A/TDM)与时钟主从;
  • hw_params:根据采样率、位深、通道数配置Codec;
  • set_sysclk:设置MCLK频率与来源;
  • trigger:处理START/STOP等流事件,用于复位、同步;
  • mute_stream:实现软硬件静音,抑制爆音。

3.4 Regmap寄存器抽象

Codec几乎全部通过I2C/SPI配置,内核使用Regmap框架统一抽象,简化读写、缓存、位操作。

典型Regmap配置:

复制代码
const struct regmap_config xxx_regmap_config = {
    .reg_bits       = 16,
    .val_bits       = 8,
    .max_register   = 0x7F,
    .writeable_reg  = xxx_writeable_reg,
    .readable_reg   = xxx_readable_reg,
    .volatile_reg   = xxx_volatile_reg,
    .cache_type     = REGCACHE_RBTREE,
};

Regmap提供统一接口:

  • regmap_read / regmap_write
  • regmap_update_bits:读---改---写原子操作
  • regcache_sync:缓存同步至硬件

Regmap大幅减少驱动重复代码,兼容I2C/SPI/MMIO。

3.5 Codec时钟配置

Codec依赖稳定时钟才能正常工作,时钟配置流程:

  1. Machine驱动通过snd_soc_dai_set_sysclk传入MCLK;
  2. Codec驱动在set_sysclk中保存频率与来源;
  3. hw_params中根据采样率配置PLL与分频;
  4. 生成正确BCLK与LRCLK。

时钟错误会导致无声、爆音等现象。

3.6 控件(kcontrol)实现

kcontrol是用户空间控制音频参数的统一接口,ASoC提供大量宏简化定义:

常用宏:

  • SOC_SINGLE:单比特开关/单通道控制;
  • SOC_DOUBLE:左右声道双控;
  • SOC_ENUM:枚举选择(去加重、通道选择);
  • SOC_SINGLE_TLV:带dB映射的音量控制;
  • SOC_DOUBLE_R_RANGE_TLV:左右分离、带范围音量。

示例:

复制代码
static const DECLARE_TLV_DB_SCALE(dac_tlv, -10050, 50, 1);
SOC_SINGLE_TLV("Master Playback Volume", REG_DAC_VOL, 0, 255, 0, dac_tlv),
SOC_DOUBLE("ADC1 Mute Switch", REG_ADC_MUTE, 0, 1, 1, 0),

控件命名遵循ALSA约定,如Master Playback VolumeCapture Volume,便于alsamixer自动识别。

四、Platform(CPU DAI)驱动开发

Platform驱动包含CPU DAI控制器DMA两部分,负责数字接口时序与数据搬运。

4.1 CPU DAI驱动职责

  1. 实现I2S/SAI/SSI/McASP等控制器时序;
  2. 提供时钟生成与分频;
  3. 配置格式、位宽、通道、TDM时隙;
  4. 与DMA联动,实现FIFO读写;
  5. 提供set_sysclkset_fmthw_params回调。

典型CPU DAI:NXP SAI、Atmel SSC、TI McASP、Rockchip I2S、Allwinner DAI。

4.2 DMA处理

ASoC推荐使用标准DMAengine框架,驱动只需:

  1. 定义DMA通道地址与FIFO位置;
  2. 调用snd_soc_dai_init_dma_data绑定DMA参数;
  3. 调用devm_snd_dmaengine_pcm_register注册PCM设备。

示例:

复制代码
struct snd_dmaengine_dai_dma_data playback_dma = {
    .addr = 0xXXXXXXX,
    .maxburst = 1,
};
snd_soc_dai_init_dma_data(dai, &playback_dma, &capture_dma);

DMA配置错误会导致数据中断等现象。

4.3 TDM与时隙配置

多通道场景使用TDM,驱动需实现set_tdm_slot回调:

复制代码
int (*set_tdm_slot)(struct snd_soc_dai *dai,
                    unsigned int tx_mask, unsigned int rx_mask,
                    int slots, int slot_width);
  • slots:总时隙数(4/8/16);
  • slot_width:每时隙位宽(16/24/32);
  • tx_mask/rx_mask:激活时隙位图。

CPU DAI与Codec必须配置完全一致的TDM参数。

五、Machine驱动与设备树(DT)配置

Machine驱动连接Codec与CPU DAI。现代内核优先使用设备树+通用simple-audio-card,无需手写C语言驱动。

5.1 simple-audio-card标准绑定

simple-audio-card是内核通用音频声卡驱动,驱动路径:sound/soc/generic/simple-card.c

必备属性:

  • compatible = "simple-audio-card"
  • simple-audio-card,name:声卡名称;
  • simple-audio-card,format:音频格式(i2s/left_j/dsp-a/tdm);
  • simple-audio-card,mclk-fs:MCLK与采样率比率(256/384);
  • cpu子节点:sound-dai = <&dai>
  • codec子节点:sound-dai = <&codec>
  • bitclock-master/frame-master:时钟主从配置。

5.2 时钟主从配置

时钟主从决定BCLK/LRCLK来源,通过dai_fmt配置:

  • SND_SOC_DAIFMT_CBP_CFP:Codec提供位时钟+帧时钟(推荐);
  • SND_SOC_DAIFMT_CBC_CFC:CPU提供全部时钟。

设备树中直接使用:

复制代码
bitclock-master = <&codec_dai>;
frame-master = <&codec_dai>;

主从配置错误是嵌入式音频最常见故障。

5.3 设备树完整示例(i.MX6UL + ADAU1372)

复制代码
&i2c1 {
    adau1372: codec@3c {
        compatible = "adi,adau1372";
        reg = <0x3c>;
        #sound-dai-cells = <0>;
        clocks = <&clk IMX6UL_CLK_SAI2>;
        clock-names = "mclk";
    };
};

&sai2 {
    status = "okay";
    pinctrl-0 = <&pinctrl_sai2>;
    fsl,sai-mclk-direction-output;
    assigned-clocks = <&clks IMX6UL_CLK_SAI2>;
    assigned-clock-rates = <24576000>;
};

sound {
    compatible = "simple-audio-card";
    simple-audio-card,name = "imx6ul-adau1372";
    simple-audio-card,format = "i2s";
    simple-audio-card,mclk-fs = <512>;
    bitclock-master = <&adau1372_dai>;
    frame-master = <&adau1372_dai>;

    simple-audio-card,dai-link@0 {
        cpu {
            sound-dai = <&sai2>;
        };
        codec {
            sound-dai = <&adau1372>;
        };
    };
};

5.4 音频路由(Routing)配置

Routing用于描述板级模拟连接,如麦克风→ADC、DAC→耳机。

设备树配置:

复制代码
simple-audio-card,widgets =
    "Microphone", "Built-in Mic",
    "Headphone", "Headphone Jack";

simple-audio-card,routing =
    "MICIN", "Built-in Mic",
    "Headphone Jack", "HPOUTL",
    "Headphone Jack", "HPOUTR";

Routing直接决定DAPM路径是否连通。

5.5 手写C语言Machine驱动

简单平台可手写C语言Machine驱动,核心是struct snd_soc_cardstruct snd_soc_dai_link

复制代码
static struct snd_soc_dai_link atmel_wm8904_dailink = {
    .name = "WM8904",
    .stream_name = "WM8904 PCM",
    .dai_fmt = SND_SOC_DAIFMT_I2S |
                SND_SOC_DAIFMT_NB_NF |
                SND_SOC_DAIFMT_CBP_CFP,
    .ops = &atmel_asoc_wm8904_ops,
};

static struct snd_soc_card atmel_asoc_wm8904_card = {
    .name = "atmel-asoc-wm8904",
    .dai_link = &atmel_asoc_wm8904_dailink,
    .num_links = 1,
};

static int atmel_asoc_wm8904_probe(struct platform_device *pdev)
{
    card->dev = &pdev->dev;
    return devm_snd_soc_register_card(&pdev->dev, card);
}

现代产品优先使用设备树方案,维护更简单。

六、ASoC DAPM动态电源管理

DAPM(Dynamic Audio Power Management) 是ASoC低功耗核心,自动根据音频路径上下电模块,对用户空间完全透明。

6.1 DAPM基本概念

  1. Widget:可独立电源控制的硬件单元(ADC/DAC/MICBIAS/PGA/MUX/OUT);
  2. Route:Widget之间的音频连接;
  3. Path:从输入到输出的完整音频路径;
  4. Power:路径连通则自动上电,断开则自动下电。

DAPM基于图遍历算法,无需驱动手动管理电源。

6.2 Widget类型

  • 端点:SND_SOC_DAPM_INPUTOUTPUTMICSPEAKERHEADPHONE
  • 处理:SND_SOC_DAPM_ADCDACPGAMIXERMUX
  • 电源:SND_SOC_DAPM_SUPPLYMICBIAS
  • 虚拟:SND_SOC_DAPM_STREAM

定义示例:

复制代码
static const struct snd_soc_dapm_widget xxx_dapm_widgets[] = {
    SND_SOC_DAPM_INPUT("AINL"),
    SND_SOC_DAPM_INPUT("AINR"),
    SND_SOC_DAPM_ADC("ADC", "Capture", REG_ADC_PD, 0, 1),
    SND_SOC_DAPM_DAC("DAC", "Playback", REG_DAC_PD, 0, 1),
    SND_SOC_DAPM_OUTPUT("HPOUTL"),
    SND_SOC_DAPM_OUTPUT("HPOUTR"),
    SND_SOC_DAPM_MICBIAS("MICBIAS", REG_MICBIAS, 0, 1),
};

6.3 Route定义

Route表示音频可流通路径:

复制代码
static const struct snd_soc_dapm_route xxx_dapm_routes[] = {
    { "ADC", NULL, "AINL" },
    { "ADC", NULL, "AINR" },
    { "HPOUTL", NULL, "DAC" },
    { "HPOUTR", NULL, "DAC" },
    { "MICBIAS", NULL, "Mic Jack" },
};

Route错误会导致DAPM无法上电,出现无声但寄存器正常的问题。

6.4 DAPM工作流程

  1. 用户打开播放/捕获流;
  2. ALSA触发hw_params;
  3. DAPM遍历所有Widget;
  4. 标记连通路径上的Widget为上电;
  5. 按顺序上电(偏置→模拟→数字→输出);
  6. 停止时逆序下电,抑制爆音。

6.5 DAPM调试工具

  1. debugfs路径:/sys/kernel/debug/asoc/<card>/<component>/dapm/
  2. cat xxx_widget可查看开关状态;
  3. dapm-graphvizdapm生成可视化拓扑图;
  1. 常见问题:Widget始终Off,说明Route未连通或控件未打开。

七、辅助设备驱动:功放、MUX

嵌入式音频常包含功放、模拟开关、MIC MUX等,ASoC提供通用驱动。

7.1 simple-amplifier

GPIO控制功放,无需I2C配置:

复制代码
speaker_amp: audio-amplifier {
    compatible = "simple-audio-amplifier";
    enable-gpios = <&pio 7 7 GPIO_ACTIVE_HIGH>;
    sound-name-prefix = "Speaker Amp";
};

在声卡中通过simple-audio-card,aux-devs挂载。

7.2 simple-mux

GPIO模拟多路选择器:

复制代码
mic_mux: mic-mux {
    compatible = "simple-audio-mux";
    mux-gpios = <&gpio5 5 GPIO_ACTIVE_LOW>;
    sound-name-prefix = "Mic Mux";
};

路由配置:

复制代码
"Mic Mux OUT", "Mic Mux IN1",
"Mic Mux OUT", "Mic Mux IN2"

八、总结

ASoC是嵌入式Linux音频的标准框架,其核心价值在于组件化复用、设备树化配置、DAPM低功耗、标准化API