4、Linux ALSA驱动框架:核心数据结构与架构概述

好,我们进入第4章。这一章是理解整个音频子系统的基石——ALSA驱动框架。说白了,就是Linux内核怎么把声卡、编解码器、DMA这些硬件抽象成一套统一的软件接口。我刚开始接触ALSA时,也被那一堆snd_soc_xxx结构体搞得晕头转向。后来调了几个板子才明白,其实核心就那么几个数据结构,搞清楚了,整个框架就通了。

4.1 ALSA架构概述

ALSA在Linux音频系统中处于中间层。往上,它给用户空间提供/dev/snd/下的设备节点和alsa-lib库;往下,它直接操作I2C、I2S、DMA等硬件接口。

在嵌入式领域,我们主要跟ASoC(ALSA System on Chip)打交道。ASoC把音频驱动拆成了三个角色:

  • Codec驱动:管理编解码芯片本身,比如WM8960的寄存器、PLL配置、ADC/DAC路径。
  • Platform驱动:负责DMA传输和CPU侧的音频接口(I2S、PCM)。
  • Machine驱动:把Codec和Platform绑在一起,描述板级硬件连接(比如用了哪个I2S口、哪个时钟源)。

这种分层设计,说白了就是为了复用。换一颗Codec,只要换Codec驱动;换一个CPU平台,只要换Platform驱动。Machine驱动则像胶水,把两者粘起来。

核心思想:ASoC通过 snd_soc_card 作为顶层容器,把 dai_link、codec、platform 组织在一起。你写驱动时,大部分时间就是在填充这些结构体。

ALSA ASoC 架构分层与核心数据结构关系图 ALSA ASoC 架构分层与核心数据结构 用户空间 (alsa-lib / tinyalsa) 内核 ALSA Core (snd) ASoC 核心 (snd_soc_card / soc-core) Machine 驱动 snd_soc_card / snd_soc_dai_link Codec 驱动 snd_soc_codec / snd_soc_dai Platform 驱动 snd_soc_platform / DMA 硬件层 (I2C / I2S / GPIO / 时钟) 板级描述 (胶水层)

4.2 核心数据结构详解

我们逐个来看。这些结构体定义在 include/sound/soc.h 里,建议你打开内核源码对照着看。

4.2.1 snd_soc_card —— 声卡顶层容器

snd_soc_card 代表一张“声卡”。它不直接对应硬件,而是描述一个音频子系统。比如你的板子上有一颗WM8960,那通常就只有一个card。

关键字段:

字段 类型 说明
name const char * 声卡名称,用户空间通过它识别
dai_link struct snd_soc_dai_link * 指向DAI链接数组,描述数据通路
num_links int dai_link数组的元素个数
dev struct device * 关联的设备节点
probe int (*probe)(struct snd_soc_card *) card初始化回调,常用于配置GPIO、时钟

我的习惯:在Machine驱动的probe函数里,我会先检查card->dai_link的配置是否正确。曾经有个项目,因为dai_link的name字段拼写错误,导致Codec和Platform匹配不上,音频死活不出声。查了两天才发现是字符串不匹配。

4.2.2 snd_soc_dai_link —— 数据通路的描述

这个结构体描述了一条完整的音频数据路径:从CPU的I2S接口到Codec的数字音频接口(DAI)。

关键字段:

字段 说明
name 链路名称,用于匹配和调试
stream_name 流名称,PCM流创建时使用
cpu_dai_name CPU侧DAI的名称,对应Platform驱动
codec_dai_name Codec侧DAI的名称
codec_name Codec设备名称,用于查找已注册的Codec
platform_name Platform设备名称,用于DMA传输
ops 指向snd_soc_ops,包含startup/shutdown/hw_params/prepare等回调

嗯,这里要注意:cpu_dai_name 和 codec_dai_name 必须与DAI驱动中注册的名称完全一致。我见过有人把 wm8960-hifi 写成了 wm8960-hifi-0,结果内核报 Failed to bind。

4.2.3 snd_soc_codec —— 编解码器抽象

这个结构体代表一颗Codec芯片。WM8960的驱动最终会注册一个snd_soc_codec实例。

关键字段:

字段 说明
name Codec名称,用于dai_link匹配
dev 指向I2C或SPI设备节点
regmap 寄存器缓存,用于读写Codec寄存器
component 指向snd_soc_component,新内核中Codec已合并为Component
dapm DAPM上下文,管理动态电源和音频路径
controls ALSA控制接口,如音量、开关

避坑指南:我曾经在移植WM8960驱动时,忽略了regmap的配置。默认的regmap使用I2C单字节读写,但WM8960有些寄存器需要多字节操作。结果音量控制死活不生效。后来在regmap_config里设置了reg_bits = 7和val_bits = 9才搞定。嗯,WM8960的寄存器地址是7位,数据是9位,这个细节很容易忘。

4.2.4 snd_soc_platform —— DMA与CPU接口抽象

snd_soc_platform 负责CPU侧的音频数据传输。它管理DMA通道、FIFO、I2S控制器等。

关键字段:

字段 说明
name Platform名称
pcm_ops 指向snd_pcm_ops,包含open/close/ioctl/hw_params/trigger/pointer等
pcm_new 创建PCM设备的回调
pcm_free 释放PCM设备的回调

说白了,Platform驱动就是告诉内核:我的CPU怎么把数据从内存搬到I2S接口。DMA描述符怎么配、中断怎么处理,都在这里。

4.3 数据结构之间的关系

这几个结构体不是孤立的。它们通过指针和名称互相引用,形成一个网状关系。我画个简化的关系图:

snd_soc_card
  |
  +-- dai_link[0]
  |     |-- cpu_dai_name  ----> 匹配 Platform 驱动的 DAI
  |     |-- codec_name    ----> 匹配 snd_soc_codec->name
  |     |-- codec_dai_name ----> 匹配 Codec 驱动的 DAI
  |     |-- platform_name ----> 匹配 snd_soc_platform->name
  |     |-- ops           ----> hw_params 回调在这里设置音频格式
  |
  +-- dev                 ----> 设备节点,用于 GPIO、时钟等板级资源

匹配过程是这样的:

  1. 内核解析 dai_link,根据 codec_name 找到已注册的 snd_soc_codec。
  2. 根据 cpu_dai_name 和 codec_dai_name 找到对应的DAI结构。
  3. 根据 platform_name 找到 snd_soc_platform。
  4. 调用 dai_link->ops->hw_params 设置采样率、位宽、时钟极性。
  5. 最终,PCM设备被创建,用户空间可以通过 /dev/snd/pcmC0D0p 访问。

关键点:匹配完全基于字符串名称。所以驱动中 name 字段的命名必须一致且唯一。我习惯在Codec驱动和Machine驱动中用相同的命名规范,比如 "wm8960-codec"、"wm8960-dai",这样不容易出错。

4.4 一个简单的Machine驱动示例

我们来看一个实际例子。假设板子上只有一颗WM8960,通过I2S0连接。Machine驱动的核心代码大概是这样:

static struct snd_soc_dai_link my_board_dai_links[] = {
    {
        .name           = "WM8960 HiFi",
        .stream_name    = "WM8960 HiFi",
        .cpu_dai_name   = "samsung-i2s.0",
        .codec_name     = "wm8960.0-001a",
        .codec_dai_name = "wm8960-hifi",
        .platform_name  = "samsung-i2s.0",
        .ops            = &my_board_ops,
    },
};

static struct snd_soc_card my_board_card = {
    .name       = "my-board-card",
    .owner      = THIS_MODULE,
    .dai_link   = my_board_dai_links,
    .num_links  = ARRAY_SIZE(my_board_dai_links),
};

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

你看,代码量其实不大。大部分工作是在填充结构体。但就是这些字段,决定了音频能否正常工作。

我的经验:调试时,先确认 dai_link 中的 cpu_dai_name 和 codec_dai_name 是否能在 /sys/kernel/debug/asoc/dais 中找到。如果找不到,说明驱动没注册成功,或者名称不匹配。这个debugfs目录是我调试ALSA的第一站。

4.5 小结

这一章我们覆盖了ALSA ASoC框架的四个核心数据结构:

  • snd_soc_card:声卡容器,组织所有链路。
  • snd_soc_dai_link:数据通路描述,连接CPU和Codec。
  • snd_soc_codec:编解码器抽象,管理寄存器、DAPM、控制接口。
  • snd_soc_platform:DMA和CPU接口抽象,负责数据传输。