1. ALSA架构概览:音频子系统在Linux内核中的位置

大家好,我是老李。做嵌入式Linux音频驱动开发也有些年头了。今天咱们来聊聊ALSA——这个在Linux世界里几乎统治了音频领域的家伙。

说实话,我刚接触Linux音频时,也被那一堆术语搞得头晕。什么OSS、ALSA、PCM、DMA...嗯,别急,咱们一步步来。

1.1 音频子系统在Linux内核中的位置

先问个问题:音频子系统在内核里到底算老几?

Linux内核是个庞然大物,音频子系统属于字符设备驱动这一层。它位于内核的中间位置——上面是用户空间的应用程序(比如你播放音乐用的mpg123、aplay),下面是具体的硬件(Codec芯片、DMA控制器、I2S总线)。

我画了个图,帮你理解这个层次关系:

Linux音频子系统分层架构 用户空间 (User Space) 应用程序 (aplay, arecord, mpg123, PulseAudio, ALSA lib) 内核空间 (Kernel Space) ALSA Core (核心层) snd_card, snd_pcm, snd_control PCM设备管理、DMA缓冲区管理 硬件驱动层 (Driver) I2S控制器驱动、DMA驱动 Codec驱动、平台驱动 硬件层 (Hardware) Codec芯片 (WM8960, ES8316, MAX98357等) 数据流方向 应用程序 → ALSA lib → 内核ALSA核心 → 硬件驱动 → 物理硬件

你看,用户空间的应用程序通过ALSA库(libasound)发起调用,经过系统调用陷入内核,然后由ALSA核心层调度,最终交给硬件驱动去操作真实的音频设备。

关键点:ALSA在内核中属于sound/core目录,它不直接操作硬件,而是提供一套统一的框架。真正的硬件操作由底层的SoC平台驱动和Codec驱动完成。

1.2 ALSA与OSS的对比——为什么ALSA赢了?

老工程师可能还记得OSS(Open Sound System)。我入行那会儿,OSS还是主流。但后来ALSA全面取代了它。为什么?

我列个表,你一看就明白:

对比项 OSS ALSA
设计理念 简单粗暴,每个音频设备对应一个设备文件 层次化、模块化设计,支持复杂拓扑
设备文件 /dev/dsp/dev/mixer /dev/snd/pcmC0D0p/dev/snd/controlC0
多路音频 不支持,一个设备一次只能被一个进程使用 原生支持,通过dmix插件实现多路混音
API复杂度 简单,ioctl为主 复杂但功能强大,提供完整API
内核集成 独立子系统 从2.6内核开始成为标准音频子系统
硬件支持 有限,商业驱动居多 广泛,大量开源驱动
现状 已被废弃,仅用于兼容旧硬件 Linux标准音频框架

说白了,OSS就像个老式收音机——只能一个人听,换台还得拧旋钮。ALSA则像个现代音响系统——支持多路输入输出,还能做各种音效处理。

我的经验:如果你在维护老项目,可能还会碰到OSS代码。但新项目千万别碰OSS了。我曾经接手过一个用OSS的项目,改到ALSA费了不少劲。嗯,那滋味不好受。

1.3 ALSA的软件分层架构

ALSA的分层设计,说白了就是「各司其职」。咱们从上往下捋:

1.3.1 用户空间层

  • ALSA库(libasound):这是用户空间的核心。应用程序通过它来操作音频设备。它封装了底层的系统调用,提供了snd_pcm_open()snd_pcm_writei()这些友好的API。
  • ALSA插件系统:比如dmix(多路混音)、dsnoop(多路录音)、rate(采样率转换)。这些插件让ALSA变得非常灵活。
  • 工具集aplayarecordamixeralsamixer等。调试时我经常用它们。

1.3.2 内核空间层

  • ALSA核心(sound/core):提供框架和通用功能。包括PCM设备管理、Control接口、Timer定时器等。
  • SoC层(sound/soc):针对嵌入式系统的ASoC(ALSA System on Chip)框架。把驱动拆成了三部分——Platform驱动、Codec驱动、Machine驱动。这个后面会详细讲。
  • 硬件驱动:具体的I2S控制器驱动、DMA驱动、Codec芯片驱动。

1.3.3 硬件层

  • Codec芯片(音频编解码器)
  • I2S/PCM总线
  • DMA控制器
  • 扬声器、麦克风等物理器件

记住这个分层:用户空间(应用+库)→ 内核空间(ALSA核心+驱动)→ 硬件。数据流是双向的,播放时从上往下,录音时从下往上。

1.4 ALSA核心数据结构初步认识

做驱动开发,数据结构就是你的「武器」。ALSA里有几个核心结构体,你必须得认识它们。

1.4.1 struct snd_card——声卡实例

每个物理声卡对应一个snd_card。它是ALSA驱动的顶层结构体。你可以把它理解成「声卡的总管家」。

struct snd_card {
    int number;             // 声卡编号,从0开始
    char id[32];            // 声卡标识符
    char driver[32];        // 驱动名称
    char shortname[32];     // 短名称
    char longname[80];      // 长名称
    struct list_head devices; // 设备链表
    struct device *dev;     // 内核设备指针
    // ... 还有很多其他成员
};

我习惯在驱动入口函数里先创建snd_card

static int my_audio_probe(struct platform_device *pdev)
{
    struct snd_card *card;
    int ret;

    // 第一步:创建声卡实例
    ret = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE, 0, &card);
    if (ret < 0) {
        dev_err(&pdev->dev, "snd_card_new failed\n");
        return ret;
    }

    // 设置声卡名称
    strcpy(card->driver, "MyAudio");
    strcpy(card->shortname, "My Audio Driver");
    strcpy(card->longname, "My Audio Driver for Embedded Board");

    // ... 后续注册PCM设备、Control接口等

    // 最后注册声卡
    ret = snd_card_register(card);
    if (ret < 0) {
        snd_card_free(card);
        return ret;
    }

    return 0;
}

注意:创建snd_card后,一定要记得调用snd_card_register()。我曾经犯过这个错——创建了卡但忘了注册,结果用户空间死活看不到设备。排查了半天才发现是少了这步。

1.4.2 struct snd_device——通用设备抽象

snd_device是ALSA内部用来管理各种子设备的通用结构体。PCM设备、Control设备、MIDI设备等都会被封装成snd_device挂到snd_card的设备链表上。

struct snd_device {
    struct list_head list;      // 链表节点
    struct snd_card *card;      // 所属声卡
    snd_device_type_t type;     // 设备类型
    void *device_data;          // 具体设备的数据指针
    struct snd_device_ops *ops; // 操作函数集
};

你不需要直接操作snd_device,ALSA核心会帮你管理。你只需要调用snd_pcm_new()这类API,它内部会自动创建snd_device并挂到卡上。

1.4.3 struct snd_pcm——PCM设备核心

这是咱们打交道最多的结构体。PCM(Pulse Code Modulation)设备负责实际的音频数据流传输。

struct snd_pcm {
    struct snd_card *card;      // 所属声卡
    struct list_head list;      // 链表节点
    unsigned int streams;       // 支持的流方向 (播放/录音)
    struct snd_pcm_str streams[2]; // 播放和录音两个方向
    // ... 还有很多
};

每个snd_pcm可以包含两个snd_pcm_str:一个用于播放(SNDRV_PCM_STREAM_PLAYBACK),一个用于录音(SNDRV_PCM_STREAM_CAPTURE)。

创建PCM设备的典型代码:

struct snd_pcm *pcm;
int ret;

// 创建PCM设备,支持播放和录音
ret = snd_pcm_new(card, "My PCM", 0, 1, 1, &pcm);
if (ret < 0) {
    dev_err(&pdev->dev, "snd_pcm_new failed\n");
    return ret;
}

// 设置PCM操作函数集
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);

我的习惯:创建PCM设备时,我会先确定硬件支持哪些采样率、位深、通道数。然后在hw_constraints里设置好这些参数。这样上层应用打开设备时,ALSA会自动帮它筛选出支持的配置。省心不少。

1.5 三个核心结构体的关系

这三个结构体的关系,我用一句话总结:

一个snd_card包含多个snd_device,其中最重要的snd_device就是snd_pcm

画个图更清楚:

struct snd_card (声卡实例) 包含设备链表、声卡编号、驱动名称等 └─ devices: struct list_head struct snd_device (通用设备抽象) type = SNDRV_DEV_PCM | SNDRV_DEV_CONTROL | ... struct snd_pcm (PCM设备) streams[0]: 播放 | streams[1]: 录音

嗯,这张图应该能帮你理清关系了。

1.6 小结

这一章咱们聊了:

  • 音频子系统在内核中的位置——字符设备驱动层
  • ALSA为什么取代了OSS——功能更强、设计更合理
  • ALSA的三层架构——用户空间、内核空间、硬件层
  • 三个核心数据结构——snd_cardsnd_devicesnd_pcm

说实话,这些概念刚接触时可能会觉得抽象。但别急,后面我们会一步步深入到代码细节。等你亲手写过几个驱动,回头再看这些结构体,就会觉得「哦,原来就这么回事」。

一句话总结:ALSA就是Linux音频的「交通警察」——它负责把应用程序的音频数据,安全、高效地送到硬件手里,或者反过来。


公众号:蓝海资料掘金营,微信deep3321