3. Linux内核音频驱动模型

本章深入剖析Linux内核中音频驱动的核心架构模型。音频驱动并非孤立存在,而是构建在字符设备、platform总线、I2C/SPI协议以及设备树(Device Tree)等多个内核子系统之上。理解这些模型的协作关系,是掌握ALSA框架整合的关键前提。

3.1 字符设备驱动模型:音频设备的用户空间接口

在Linux中,音频设备对用户空间呈现为字符设备(Character Device)。应用程序通过标准的文件操作接口(open、read、write、ioctl、mmap、poll)与音频硬件交互,而无需关心底层硬件的具体细节。

字符设备驱动的核心数据结构是 struct file_operations,它定义了设备文件与内核驱动之间的操作映射表。对于音频设备,关键的file_operations回调包括:

回调函数 用途 ALSA中的典型实现
.open 打开音频设备,初始化硬件和软件状态 snd_pcm_open() / snd_ctl_open()
.release 关闭设备,释放资源 snd_pcm_release()
.read / .write PCM数据流的读写(非mmap模式) snd_pcm_read() / snd_pcm_write()
.ioctl 控制命令,如设置采样率、格式、音量 snd_pcm_ioctl() / snd_ctl_ioctl()
.mmap 将DMA缓冲区映射到用户空间,实现零拷贝 snd_pcm_mmap()
.poll 查询设备是否可读/可写,支持非阻塞I/O snd_pcm_poll()
关键理解: ALSA框架在底层已经封装了标准的字符设备注册流程。驱动开发者通常不需要直接调用 register_chrdev(),而是通过ALSA核心API(如 snd_pcm_new())间接完成字符设备的创建与注册。ALSA会为每个PCM设备分配主设备号(通常为116)和次设备号。

3.2 Platform驱动模型:SoC内部音频控制器的标准框架

对于集成在SoC(System on Chip)内部的音频控制器(如I2S、AC97、PDM接口),Linux内核推荐使用platform驱动模型进行管理。Platform驱动不依赖传统的物理总线(如PCI、USB),而是通过虚拟的platform总线进行设备与驱动的匹配。

Platform驱动模型在音频子系统中的典型应用场景:

  • CPU DAI驱动: 代表SoC内部的数字音频接口(如Samsung I2S、TI McASP、STI FSAudio)。
  • DMA引擎驱动: 负责音频数据的DMA传输,通常也注册为platform驱动。
  • 机器驱动(Machine Driver): 将CPU DAI、Codec和DMA引擎绑定在一起,形成完整的声卡。

Platform驱动的核心数据结构是 struct platform_driver,其 .probe 函数是音频设备初始化的入口点。

// 典型的音频platform驱动模板
static const struct of_device_id audio_of_match[] = {
    { .compatible = "vendor,my-audio-controller" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, audio_of_match);

static struct platform_driver my_audio_driver = {
    .probe  = my_audio_probe,
    .remove = my_audio_remove,
    .driver = {
        .name = "my-audio-controller",
        .of_match_table = audio_of_match,
        .pm = &my_audio_pm_ops,
    },
};

module_platform_driver(my_audio_driver);
最佳实践: 在 .probe 函数中,驱动应完成以下工作:
1. 解析设备树节点,获取寄存器地址、中断号、DMA通道等资源。
2. 映射I/O内存(devm_ioremap_resource())。
3. 初始化硬件时钟、复位和电源管理。
4. 注册DMA引擎(dma_request_slave_channel())。
5. 调用ALSA API创建PCM设备(snd_pcm_new())并设置操作集。

3.3 I2C/SPI设备注册:音频Codec的物理总线连接

大多数外部音频Codec芯片(如WM8960、TLV320AIC3x、ES8316)通过I2C或SPI总线与SoC连接。这些Codec驱动在内核中注册为I2C或SPI设备驱动,负责控制Codec的内部寄存器,实现音量调节、采样率切换、音频路由等功能。

I2C设备驱动的注册流程:

  1. 定义I2C设备ID表: 使用 struct i2c_device_id 和 of_match_table 进行设备匹配。
  2. 实现I2C驱动结构体: 填充 struct i2c_driver 的 .probe、.remove、.id_table 字段。
  3. 在probe中初始化Codec: 读取设备ID、复位芯片、初始化寄存器默认值。
  4. 注册到ALSA SoC框架: 调用 devm_snd_soc_register_component() 将Codec注册为ALSA组件。
// I2C音频Codec驱动注册示例
static const struct i2c_device_id audio_codec_id[] = {
    { "wm8960", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, audio_codec_id);

static const struct of_device_id audio_codec_of_match[] = {
    { .compatible = "wlf,wm8960" },
    { }
};
MODULE_DEVICE_TABLE(of, audio_codec_of_match);

static struct i2c_driver audio_codec_i2c_driver = {
    .driver = {
        .name = "wm8960",
        .of_match_table = audio_codec_of_match,
    },
    .probe    = audio_codec_i2c_probe,
    .remove   = audio_codec_i2c_remove,
    .id_table = audio_codec_id,
};

module_i2c_driver(audio_codec_i2c_driver);
注意: I2C/SPI驱动只负责Codec的控制通路(寄存器读写)。音频数据流(PCM)通常通过独立的I2S或TDM总线传输,不走I2C/SPI。数据通路和控制通路的分离是嵌入式音频设计的核心原则。

3.4 设备树与音频设备:硬件描述的标准化

设备树(Device Tree)是嵌入式Linux中描述硬件拓扑的标准方式。对于音频子系统,设备树节点定义了SoC音频控制器、外部Codec、时钟源、DMA通道以及它们之间的连接关系。

一个典型的音频设备树节点包含以下关键属性:

属性 含义 示例值
compatible 驱动匹配字符串 "wlf,wm8960"
reg I2C从设备地址或SPI片选号 <0x1a>
clocks 音频主时钟(MCLK)的phandle <&clks IMX6QDL_CLK_CKO>
clock-names 时钟名称,用于驱动中获取 "mclk"
#sound-dai-cells DAI链接标识符的单元数 <0> 或 <1>
VDDA-supply 模拟电源 regulator <&reg_audio_3v3>

以下是一个完整的音频设备树片段,展示了CPU DAI、Codec和机器驱动的绑定关系:

// 音频Codec节点
&i2c2 {
    audio_codec: wm8960@1a {
        compatible = "wlf,wm8960";
        reg = <0x1a>;
        clocks = <&clks IMX6QDL_CLK_CKO>;
        clock-names = "mclk";
        #sound-dai-cells = <0>;
        AVDD-supply = <&reg_audio_3v3>;
        DVDD-supply = <&reg_audio_1v8>;
    };
};

// CPU DAI节点(I2S控制器)
&ssi1 {
    status = "okay";
    #sound-dai-cells = <0>;
};

// 机器驱动节点(绑定CPU DAI和Codec)
sound {
    compatible = "my-board,audio-card";
    model = "MyBoard Audio";
    audio-cpu = <&ssi1>;
    audio-codec = <&audio_codec>;
    audio-routing =
        "Headphone Jack", "HP_L",
        "Headphone Jack", "HP_R",
        "MIC_IN", "Mic Jack";
};

3.5 音频驱动模型整体架构图

下图展示了字符设备、platform驱动、I2C/SPI设备注册与设备树在音频驱动中的协作关系:

Linux音频驱动模型整体架构 用户空间应用程序 (ALSA Lib / TinyALSA) 字符设备层 (file_operations) open / read / write / ioctl / mmap / poll ALSA SoC核心层 (ASoC) snd_soc_card / snd_soc_dai_link / snd_soc_component Platform驱动 (CPU DAI) I2S / TDM / AC97 控制器 I2C/SPI驱动 (Codec) WM8960 / TLV320 / ES8316 DMA引擎驱动 dmaengine / SDMA / EDMA 设备树 (Device Tree) compatible / reg / clocks / sound-dai 硬件层 (SoC + 外部Codec + 时钟 + 电源) I2S总线 / MCLK / BCLK / LRCLK / 中断 / DMA 数据流:用户空间 → 字符设备 → ALSA核心 → DMA → 硬件 控制流:用户空间 → 字符设备 → ALSA核心 → I2C/SPI → Codec

3.6 模型协作流程:从设备树到声卡注册

当系统启动时,音频驱动的初始化遵循以下典型流程:

  1. 设备树解析: 内核解析DTB文件,为每个 compatible 匹配的节点创建 platform_device、i2c_client 或 spi_device。
  2. 驱动匹配与probe: 总线驱动(platform、i2c、spi)调用匹配驱动的 .probe 函数。
  3. 资源获取: 驱动通过 of_property_read_* 系列API从设备树节点读取寄存器地址、中断、时钟、DMA通道等资源。
  4. 组件注册: CPU DAI驱动和Codec驱动分别调用 devm_snd_soc_register_component() 注册到ASoC核心。
  5. 声卡绑定: 机器驱动(Machine Driver)在probe中创建 snd_soc_card,并通过 snd_soc_register_card() 将CPU DAI、Codec和DMA引擎绑定在一起。
  6. 字符设备创建: ASoC核心自动为每个PCM DAI创建字符设备(如 /dev/snd/pcmC0D0p),用户空间即可通过标准文件I/O访问音频硬件。
核心要点总结:
• 字符设备模型 提供用户空间访问接口,ALSA封装了底层注册细节。
• Platform驱动 管理SoC内部音频控制器和DMA引擎,通过设备树匹配。
• I2C/SPI驱动 控制外部Codec的寄存器,实现音频参数配置。
• 设备树 是硬件描述的“事实标准”,驱动通过解析设备树获取资源,实现与硬件平台的解耦。
• 四种模型在ASoC框架下协同工作,共同构成完整的嵌入式音频驱动体系。