4. Linux音频子系统架构:ASoC框架组成

好,咱们今天聊聊ASoC。这玩意儿全称叫ALSA System on Chip,是Linux内核专门为嵌入式音频设备打造的一套框架。说白了,它就是帮你把音频驱动写得更有条理,而不是一团乱麻。

我刚开始接触音频驱动时,面对的是各种芯片手册和寄存器。那时候的驱动代码,Codec、DMA、CPU接口全揉在一个文件里。换一个Codec,得重写大半。后来ASoC出现了,把这些问题拆得清清楚楚。

4.1 为什么需要ASoC?

传统的ALSA驱动,把所有硬件细节都堆在一个驱动里。你想想看,一个嵌入式音频系统,至少有三部分:

  • CPU端的I2S接口 —— 负责把数据从内存搬出来,通过I2S总线发出去
  • Codec芯片 —— 负责数模转换、音量控制、路径切换
  • 板级连接 —— 比如用了哪个I2S引脚、有没有外部时钟、用了哪路DMA

这三部分如果写在一起,换一个Codec就得动整个驱动。我在一个项目里就吃过这个亏,当时换了一颗WM8960,结果因为原来的驱动把Codec和CPU接口耦合得太紧,改了一个星期才跑通。

ASoC的解决思路很简单:分而治之。它把音频驱动拆成三个独立的组件:Codec、Platform、Machine。每个组件只管自己的事,通过标准接口互相通信。

核心思想:ASoC通过分层解耦,让Codec驱动、CPU DMA驱动、板级配置各自独立。换Codec不用改Platform,换CPU不用改Codec。

4.2 ASoC三大组件

咱们一个一个来看。这三个组件,我习惯叫它们「三驾马车」。

4.2.1 Codec组件

Codec组件,顾名思义,就是管理音频编解码芯片的。它负责的事情包括:

  • 控制DAC/ADC的开关和增益
  • 管理音频路径(比如从MIC到ADC,从DAC到耳机)
  • 处理音量、静音、EQ等控制
  • 管理电源域(省电模式)

在ASoC里,Codec驱动通过 snd_soc_codec_driver 结构体注册。这个结构体里,最重要的就是 controls 和 dapm_widgets。

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),
    .dapm_routes = wm8960_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(wm8960_dapm_routes),
};

这里我特别想强调一下DAPM(Dynamic Audio Power Management)。这是ASoC的一大亮点。它可以根据音频路径的开关状态,自动控制Codec内部各个模块的电源。比如,你只用了耳机输出,那DAPM会自动关掉扬声器功放的电源,省电。

个人经验:调试DAPM时,我建议先检查 /sys/kernel/debug/asoc/codec_name/dapm 下的状态文件。它能告诉你每个widget当前是开还是关,路径是否连通。我曾经靠这个文件,半小时就定位了一个音频路径不通的问题。

4.2.2 Platform组件

Platform组件,管的是CPU这一侧的事情。它主要处理:

  • DMA引擎 —— 把音频数据从内存搬运到I2S发送FIFO,或者从接收FIFO搬回内存
  • CPU的DAI接口 —— 比如I2S、PCM、AC97等数字音频接口的配置
  • 时钟和系统时钟 —— 比如I2S的位时钟BCLK、帧时钟LRCK的生成

Platform驱动通过 snd_soc_platform_driver 注册。它和Codec之间通过DAI link连接。

static struct snd_soc_platform_driver imx_pcm_platform = {
    .ops = &imx_pcm_ops,
    .pcm_new = imx_pcm_new,
    .pcm_free = imx_pcm_free,
};

这里有个容易混淆的点:Platform组件里的「DAI」和Codec组件里的「DAI」是两回事。Platform的DAI是CPU端的数字音频接口,Codec的DAI是Codec端的数字音频接口。它们通过I2S总线连接。

注意:Platform组件不负责音频数据的编解码。它只负责数据的搬运和接口时序。编解码是Codec的事。别搞混了。

4.2.3 Machine组件

Machine组件,是板级配置的「胶水」。它把Codec和Platform连接起来,告诉系统:

  • CPU的哪个I2S接口连到了哪个Codec
  • 用了哪些GPIO做复位、中断、检测
  • 时钟源从哪里来(外部晶振还是CPU内部生成)
  • 音频路径的初始状态

Machine驱动通过 snd_soc_card 结构体注册。它里面最重要的就是 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-pcm-audio",
    .cpu_dai_name = "imx-ssi.0",
    .ops = &imx_wm8960_ops,
    .init = imx_wm8960_init,
};

你看,这个dai_link把Codec的DAI名字、Platform的名字、CPU的DAI名字全串起来了。Machine就是干这个的——搭桥。

4.3 三者的协作关系

为了让你更直观地理解,我画了一张图。这张图我每次培训都会用,因为它把ASoC的架构讲得很清楚。

ASoC 三大组件协作关系图 ASoC 三大组件协作关系 用户空间 (ALSA Lib / aplay / arecord) 内核空间 ALSA Core (snd / pcm / control) ASoC Core (snd_soc) Codec 组件 WM8960 / wm8974 / max98090 Platform 组件 DMA / CPU DAI / 时钟 Machine 组件 板级配置 / dai_link 协作流程 ① Machine 通过 dai_link 指定 Codec 和 Platform 的连接关系 ② Platform 负责 DMA 传输和 CPU DAI 的时序配置 ③ Codec 负责音频编解码、路径切换和电源管理

从上图你能看到,用户空间的音频数据,经过ALSA Core,进入ASoC Core。ASoC Core根据Machine的配置,把数据路由到对应的Platform和Codec。Platform负责把数据通过DMA搬进I2S FIFO,Codec负责把数字信号转成模拟信号输出。

4.4 组件间的数据流

咱们以播放为例,走一遍数据流:

  1. 用户空间:aplay 把WAV文件数据写入ALSA设备节点
  2. ALSA Core:把数据交给PCM中间层,创建PCM流
  3. Platform组件:启动DMA传输,把数据从内存搬到CPU的I2S发送FIFO
  4. CPU DAI:按照I2S协议,把数据通过BCLK、LRCK、SDIN引脚发送出去
  5. Codec DAI:接收I2S数据,送入DAC
  6. Codec内部:DAC把数字信号转成模拟信号,经过音量控制、路径选择,最终输出到耳机或扬声器

整个过程,Machine组件一直在背后协调。它确保每个环节的时钟、格式、采样率都匹配。

关键点:数据流是单向的?不,I2S是全双工的。播放和录音可以同时进行。Platform的DMA通常有独立的发送和接收通道,Codec的DAC和ADC也是独立的。所以,你完全可以一边放歌一边录音。

4.5 组件间的接口约定

三个组件之间,通过几个核心接口通信。我整理了一个表格,方便你对照:

接口 作用 涉及组件
snd_soc_dai_ops 配置DAI的格式、时钟、帧同步 Codec DAI ↔ Platform DAI
snd_soc_ops Machine层对dai_link的回调(如init、startup) Machine ↔ Codec / Platform
snd_pcm_ops PCM操作(open、close、hw_params、trigger) Platform → ALSA Core
snd_kcontrol_new 用户空间的控制接口(音量、开关) Codec → ALSA Core
snd_soc_dapm_widget DAPM电源管理widget Codec内部

这里我特别想提一下 snd_soc_dai_ops。这个接口是Codec和Platform之间最重要的握手协议。它里面定义了 hw_params、set_fmt、set_sysclk 等回调。当Machine启动音频流时,会依次调用这些回调,确保两端的DAI配置一致。

避坑指南:我曾经遇到一个I2S没有声音的问题。查了半天,发现是Codec的DAI格式设成了I2S左对齐,而Platform的DAI格式设成了标准I2S。两边不匹配,数据全丢了。所以,调试时第一件事就是检查 /sys/kernel/debug/asoc/dais/ 下的格式信息。

4.6 小结

ASoC的三大组件,说白了就是:

  • Codec:管音频编解码和路径
  • Platform:管数据搬运和接口时序
  • Machine:管板级配置和组件连接

这种分层设计,让音频驱动的开发变得模块化。你换一个Codec,只需要写一个新的Codec驱动,Platform和Machine基本不用动。反过来,你换一个CPU平台,只需要写新的Platform驱动,Codec驱动可以直接复用。