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的架构讲得很清楚。
从上图你能看到,用户空间的音频数据,经过ALSA Core,进入ASoC Core。ASoC Core根据Machine的配置,把数据路由到对应的Platform和Codec。Platform负责把数据通过DMA搬进I2S FIFO,Codec负责把数字信号转成模拟信号输出。
4.4 组件间的数据流
咱们以播放为例,走一遍数据流:
- 用户空间:aplay 把WAV文件数据写入ALSA设备节点
- ALSA Core:把数据交给PCM中间层,创建PCM流
- Platform组件:启动DMA传输,把数据从内存搬到CPU的I2S发送FIFO
- CPU DAI:按照I2S协议,把数据通过BCLK、LRCK、SDIN引脚发送出去
- Codec DAI:接收I2S数据,送入DAC
- 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驱动可以直接复用。