驱动框架入门: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,结果系统直接卡死。血的教训。

三者的协作关系

这三个层不是孤立的。我举个例子,当应用开始播放音频时:

  1. ALSA层收到open请求,分配PCM运行时结构体
  2. 应用设置hw_params,ALSA层调用你的hw_params回调
  3. 你在hw_params里,通过I2C/DDC层读取EDID,确认显示器支持当前格式
  4. 然后通过HDA层配置Codec的音频输出参数
  5. 应用调用trigger(start),你启动DMA,音频数据开始流到HDMI接口

你看,三个层配合得挺紧密的。任何一个环节出问题,声音都出不来。

我个人建议,调试的时候从下往上查。先确认I2C/DDC能读到EDID,再确认HDA寄存器配置正确,最后看ALSA的回调有没有被正确调用。这样排查问题最快。

好,这一章就到这里。下一章咱们会深入ALSA的PCM接口,看看每个回调函数到底该怎么写。到时候我会拿一个真实的HDMI驱动代码来拆解。