驱动框架入门:Linux ALSA音频驱动框架概述
好,咱们今天聊聊ALSA。说实话,我刚接触Linux音频驱动那会儿,看到ALSA这几个字母,第一反应是——这又是个什么缩写怪物?
ALSA,全称是Advanced Linux Sound Architecture。翻译过来就是高级Linux声音架构。但你别被「高级」两个字吓到。说白了,它就是Linux内核里负责音频的那一套框架。我个人的理解是:它帮我们屏蔽了底层硬件的差异,让上层应用可以统一地播放和录音。
ALSA的核心组件
ALSA框架里,有几个东西你必须要搞清楚。我列个表,方便你对照着看:
| 组件 | 作用 | 我的一点体会 |
|---|---|---|
| Card | 代表一个声卡设备 | 一个HDMI接口通常对应一个Card |
| Device | Card下的子设备,比如播放、录音 | HDMI音频一般用Device 0 |
| PCM | 数字音频数据流 | 你听到的声音,本质上就是PCM数据 |
| Control | 控制接口,调节音量、静音等 | 这个在HDMI里用得不多,但也要知道 |
嗯,这里要注意:ALSA的Card和硬件声卡不是一一对应的。我在项目中就遇到过,一个物理声卡可以拆成多个Card,反过来也行。具体怎么分,看驱动怎么写。
ALSA驱动的分层结构
ALSA驱动本身是分层的。从上到下,大概是这么个结构:
应用层 (aplay, arecord, 播放器)
↓
ALSA库 (libasound)
↓
ALSA核心层 (kernel/sound/core)
↓
硬件驱动层 (你的HDMI音频驱动)
↓
硬件 (HDMI芯片、Codec)
你想想看,应用层调用aplay播放一个WAV文件,数据是怎么流到HDMI接口的?
首先,ALSA库把应用的数据格式转换好。然后交给内核的ALSA核心层。核心层负责调度、缓冲管理。最后,你的硬件驱动层负责把数据写到I2S总线上,或者通过HDA总线送出去。
我个人习惯把硬件驱动层再拆成三块:
- I2C/DDC部分:用来和HDMI的EDID通信,读显示器的音频能力
- HDA部分:如果用的是Intel HDA总线,这部分负责和Codec交互
- ALSA部分:注册PCM设备、Control设备,实现回调函数
核心要点:ALSA驱动说白了就是三件事——注册设备、实现回调、操作硬件。别想复杂了。
HDMI音频驱动的分层结构
好,现在咱们聚焦到HDMI音频。HDMI音频驱动在ALSA框架下,是怎么分层的?
我画个图给你看(用文字描述):
+------------------+
| ALSA PCM层 | ← 注册snd_pcm_ops,实现open/close/hw_params/prepare/trigger/pointer
+------------------+
| HDMI音频逻辑层 | ← 处理音频格式、通道数、采样率,和视频驱动交互
+------------------+
| I2C/DDC通信层 | ← 读取EDID,获取显示器支持的音频能力
+------------------+
| HDA/硬件接口层 | ← 操作硬件寄存器,启动DMA传输
+------------------+
| 物理HDMI芯片 | ← 你的硬件
+------------------+
每一层都有它的活。我分别说一下:
I2C/DDC层
这一层负责和显示器的EDID通信。EDID里存着显示器支持哪些音频格式——比如支持2声道LPCM,还是支持Dolby Digital 5.1。
我曾经踩过一个坑:某款显示器EDID里明明写着支持8声道,但实际只能输出2声道。查了半天,发现是EDID的Speaker Allocation数据块解析错了。嗯,从那以后我每次解析EDID都会多打印几行日志。
小技巧:调试EDID可以用 edid-decode 工具,把原始数据dump出来看。别光信驱动里的解析结果。
HDA层
HDA是Intel主导的高清音频总线规范。如果你的平台用的是HDA,那HDMI音频驱动其实是在HDA框架下工作的。
HDA层主要做这些事:
- 初始化Codec(HDMI芯片在这里被当作一个HDA Codec)
- 配置音频输出格式(采样率、位深、通道数)
- 启动DMA传输
我记得第一次调HDA的HDMI音频,死活不出声。后来发现是Codec的AFG(音频功能组)的默认配置不对。HDA的寄存器操作,一步都不能省。
ALSA层
这一层是给上层应用看的。你需要注册一个snd_pcm_ops结构体,实现这些回调:
static const struct snd_pcm_ops hdmi_pcm_ops = {
.open = hdmi_pcm_open,
.close = hdmi_pcm_close,
.hw_params = hdmi_pcm_hw_params,
.hw_free = hdmi_pcm_hw_free,
.prepare = hdmi_pcm_prepare,
.trigger = hdmi_pcm_trigger,
.pointer = hdmi_pcm_pointer,
};
每个回调函数都有它的职责。比如hw_params里,你要根据应用请求的格式,去配置HDA或者I2S的寄存器。trigger里启动或停止DMA传输。pointer返回当前的DMA位置,ALSA核心用它来计算缓冲区的填充情况。
警告:pointer回调里千万别做耗时操作!它是在中断上下文或者原子上下文中调用的。我曾经在里面加了个printk,结果系统直接卡死。血的教训。
三者的协作关系
这三个层不是孤立的。我举个例子,当应用开始播放音频时:
- ALSA层收到open请求,分配PCM运行时结构体
- 应用设置hw_params,ALSA层调用你的hw_params回调
- 你在hw_params里,通过I2C/DDC层读取EDID,确认显示器支持当前格式
- 然后通过HDA层配置Codec的音频输出参数
- 应用调用trigger(start),你启动DMA,音频数据开始流到HDMI接口
你看,三个层配合得挺紧密的。任何一个环节出问题,声音都出不来。
我个人建议,调试的时候从下往上查。先确认I2C/DDC能读到EDID,再确认HDA寄存器配置正确,最后看ALSA的回调有没有被正确调用。这样排查问题最快。
好,这一章就到这里。下一章咱们会深入ALSA的PCM接口,看看每个回调函数到底该怎么写。到时候我会拿一个真实的HDMI驱动代码来拆解。