4、ASoC框架入门:ALSA与ASoC的关系、Codec驱动、Platform驱动、Machine驱动的职责划分与注册流程

说实话,很多刚接触Linux音频驱动的朋友,一上来就被ALSA和ASoC这两个词搞懵了。我当年也一样,翻着内核文档,心里直嘀咕:这俩到底啥关系?别急,咱们今天就把这事儿彻底捋清楚。

ALSA是底层音频基础设施,负责跟硬件打交道。ASoC呢,是建立在ALSA之上的一层框架,专门为嵌入式音频系统设计的。你可以这么理解:ALSA是高速公路,ASoC是跑在高速上的专用卡车,专门拉音频货物。

4.1 ALSA与ASoC:谁管什么?

ALSA(Advanced Linux Sound Architecture)提供的是最基础的音频能力:PCM数据流的读写、Mixer控制、音量调节、设备枚举等等。它定义了一套标准的用户空间API,应用程序通过/dev/snd/下的设备节点来操作音频硬件。

但ALSA有个问题——它太通用了。对于嵌入式平台,Codec芯片五花八门,I2S接口的时序千奇百怪,DMA通道的配置也各不相同。如果每个驱动都从头实现ALSA接口,那代码量就太大了。

ASoC(ALSA System on Chip)就是来解决这个问题的。它把音频驱动拆成了三个部分:Codec驱动、Platform驱动、Machine驱动。各管各的,互不干扰。

核心区别一句话总结:

  • ALSA = 音频子系统 + 用户空间API + 硬件抽象层
  • ASoC = 嵌入式音频框架 + 组件化驱动模型 + 板级适配层

我习惯把ALSA比作操作系统,ASoC比作一个专门的管理软件。操作系统提供基础能力,管理软件帮你把活儿分得更细、更高效。

4.2 三大驱动的职责划分

ASoC框架把音频驱动拆成三个角色,每个角色各司其职。咱们一个一个来看。

4.2.1 Codec驱动

Codec驱动负责管理音频编解码芯片本身。它不关心数据怎么传过来的,只关心芯片内部怎么配置。

具体职责包括:

  • 控制DAC/ADC的开关、采样率、位深
  • 管理Mixer、音量控制、EQ等音频处理模块
  • 处理Codec内部的时钟分频和PLL配置
  • 实现ALSA的kcontrol接口,暴露给用户空间

举个例子,WM8960的Codec驱动里,你会看到这样的注册代码:

static struct snd_soc_dai_driver wm8960_dai = {
    .name = "wm8960-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_S20_3LE |
                   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 |
                   SNDRV_PCM_FMTBIT_S20_3LE |
                   SNDRV_PCM_FMTBIT_S24_LE,
    },
    .ops = &wm8960_dai_ops,
};

你看,这里只声明了Codec本身的能力:支持哪些采样率、哪些格式、哪些通道数。至于数据从哪来、怎么传,它不管。

我的经验:调试Codec驱动时,最常踩的坑是寄存器初始化顺序。WM8960有个坑——必须先给PLL上电,再配置分频系数,否则芯片直接罢工。我曾经因为这个bug折腾了两天,最后翻datasheet才发现。

4.2.2 Platform驱动

Platform驱动负责数据传输这一侧。它管理的是SoC内部的音频接口:I2S控制器、DMA引擎、时钟生成器等。

具体职责包括:

  • 配置I2S接口的引脚、时序、主从模式
  • 管理DMA通道,完成内存到I2S FIFO的数据搬运
  • 处理音频时钟(MCLK、BCLK、LRCLK)的生成和分配
  • 实现ALSA的PCM操作集(open/close/hw_params/prepare/trigger)

Platform驱动通常跟具体的SoC绑定。比如i.MX6的SAI接口、STM32的I2S外设、全志的I2S控制器,每个都有自己的Platform驱动。

这里有个关键点:Platform驱动不关心Codec是什么型号,它只负责把数据按时送到I2S总线上。

4.2.3 Machine驱动

Machine驱动是胶水层,负责把Codec和Platform粘在一起。它描述的是具体的板级硬件连接。

具体职责包括:

  • 指定Codec和Platform的DAI链接(哪个I2S接口连哪个Codec)
  • 配置音频路径(比如:播放路径走哪个Mixer、哪个DAC)
  • 处理板级的GPIO控制(比如功放使能、耳机检测)
  • 注册声卡设备(snd_soc_card)

Machine驱动里最核心的数据结构是snd_soc_dai_link:

static struct snd_soc_dai_link imx_wm8960_dai_link = {
    .name = "WM8960",
    .stream_name = "WM8960 HiFi",
    .codec_dai_name = "wm8960-hifi",
    .codec_name = "wm8960.0-001a",
    .platform_name = "imx-i2s.0",
    .cpu_dai_name = "imx-i2s.0",
    .ops = &imx_wm8960_ops,
    .init = imx_wm8960_init,
};

你看,这里明确说了:Codec是wm8960.0-001a,Platform是imx-i2s.0,它们通过wm8960-hifi这个DAI接口连接。

注意:Machine驱动是板级相关的。同一颗WM8960,接在i.MX6上是一个Machine驱动,接在STM32MP1上就是另一个。不要试图复用,老老实实根据硬件原理图写。

4.3 三者的关系图

说了这么多,咱们用一张图把关系理清楚:

ASoC 框架三大驱动关系图 用户空间 (ALSA API) 内核空间 (ASoC 核心层) Codec 驱动 WM8960 / MAX98090 DAC/ADC/Mixer/音量 Platform 驱动 I2S / DMA / 时钟 数据搬运 + 时序控制 Machine 驱动 DAI Link 配置 板级GPIO + 音频路径 三者协作流程 1. Machine驱动注册声卡,指定Codec + Platform的DAI Link 2. Platform驱动配置I2S/DMA,准备数据传输通道 3. Codec驱动配置内部音频路径,处理数据编解码 4. 数据流:用户空间 → ALSA → Platform(DMA) → I2S → Codec → 扬声器

4.4 注册流程分析

了解了职责划分,咱们再来看注册流程。ASoC驱动的注册顺序是有讲究的,不能乱来。

4.4.1 注册顺序

标准的注册流程分三步走:

  1. Codec驱动先注册:通过snd_soc_register_component()或snd_soc_register_codec(),把Codec的能力注册到ASoC核心层。
  2. Platform驱动再注册:通过snd_soc_register_component()或snd_soc_register_platform(),把I2S/DMA的能力注册进去。
  3. Machine驱动最后注册:通过snd_soc_register_card(),把前面两个组件通过DAI Link连接起来,最终生成声卡设备。

我建议:调试时先确认Codec和Platform各自都注册成功了。可以在/sys/kernel/debug/asoc/下查看注册情况。如果Machine注册失败,八成是DAI Link里的名字没对上。

4.4.2 关键数据结构

注册过程中涉及几个核心数据结构,我整理了一个表格:

数据结构 所属驱动 作用
snd_soc_codec_driver Codec 描述Codec的读写函数、寄存器配置、控制接口
snd_soc_dai_driver Codec / Platform 描述DAI接口的能力(采样率、格式、通道数)
snd_soc_platform_driver Platform 描述PCM操作集(open/close/hw_params等)
snd_soc_dai_link Machine 连接Codec和Platform的桥梁,指定DAI名称和回调函数
snd_soc_card Machine 声卡设备描述,包含DAI Link列表、初始化函数

4.4.3 一个完整的注册示例

以WM8960在i.MX平台为例,注册流程大致如下:

// 1. Codec驱动注册(在wm8960.c中)
static struct snd_soc_codec_driver soc_codec_dev_wm8960 = {
    .probe = wm8960_probe,
    .remove = wm8960_remove,
    .controls = wm8960_snd_controls,
    .num_controls = ARRAY_SIZE(wm8960_snd_controls),
    .dapm_widgets = wm8960_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(wm8960_dapm_widgets),
};
snd_soc_register_codec(&i2c_client->dev, &soc_codec_dev_wm8960, &wm8960_dai, 1);

// 2. Platform驱动注册(在imx-i2s.c中)
static struct snd_soc_platform_driver imx_i2s_platform = {
    .ops = &imx_i2s_pcm_ops,
    .pcm_new = imx_i2s_pcm_new,
    .pcm_free = imx_i2s_pcm_free,
};
snd_soc_register_platform(&pdev->dev, &imx_i2s_platform);

// 3. Machine驱动注册(在imx-wm8960.c中)
static struct snd_soc_card imx_wm8960_card = {
    .name = "imx-wm8960",
    .owner = THIS_MODULE,
    .dai_link = imx_wm8960_dai_link,
    .num_links = 1,
    .late_probe = imx_wm8960_late_probe,
};
snd_soc_register_card(&imx_wm8960_card);

你看,三个驱动各自独立注册,互不依赖。ASoC核心层会在Machine注册时,根据DAI Link里的名字去查找已经注册的Codec和Platform,然后自动完成绑定。

我曾经踩过的坑:有一次我改了Codec的DAI名称,但忘了更新Machine驱动里的codec_dai_name字段。结果内核报ASoC: failed to find DAI WM8960,折腾了半天才发现是名字没对上。记住:名字必须完全一致,大小写都不能错。

4.5 调试小技巧

最后分享几个我常用的调试方法:

  • 查看注册状态:cat /sys/kernel/debug/asoc/components 列出所有已注册的组件
  • 查看DAI链接:cat /sys/kernel/debug/asoc/dais 列出所有DAI接口
  • 查看声卡信息:cat /proc/asound/cards 确认声卡是否注册成功
  • 查看Codec寄存器:cat /sys/kernel/debug/regmap/<codec-device>/registers

嗯,ASoC框架的核心思想就是分而治之。把音频驱动拆成三个独立模块,每个模块只关心自己的事。这样不管是换Codec、换平台、还是换板子,改动都局限在对应的驱动里,不会牵一发而动全身。