3. 音频Platform驱动注册:module_platform_driver宏、probe与remove回调函数实现

在Linux音频驱动中,Platform驱动是连接硬件设备与ALSA(Advanced Linux Sound Architecture)框架的核心桥梁。本章将深入剖析Platform驱动的注册机制,重点讲解module_platform_driver宏的展开过程,以及probe与remove回调函数的实现要点。理解这些基础构件,是后续进行中断处理调优和DMA(Direct Memory Access)配置的前提。

3.1 module_platform_driver宏:从宏定义到驱动注册

module_platform_driver是Linux内核提供的一个便捷宏,用于简化Platform驱动的注册与注销。它封装了module_init、module_exit以及platform_driver_register/platform_driver_unregister的调用。

核心作用: 将驱动开发者从重复的模块入口/出口函数编写中解放出来,只需专注于定义struct platform_driver结构体。

该宏的定义位于 include/linux/platform_device.h,其展开逻辑如下:

/* 宏定义原型 */
#define module_platform_driver(__platform_driver) \
    module_driver(__platform_driver, platform_driver_register, \
                  platform_driver_unregister)

/* 进一步展开为 */
static int __init __platform_driver_init(void) \
{ \
    return platform_driver_register(&(__platform_driver)); \
} \
module_init(__platform_driver_init); \
static void __exit __platform_driver_exit(void) \
{ \
    platform_driver_unregister(&(__platform_driver)); \
} \
module_exit(__platform_driver_exit);

下表总结了宏展开后的关键元素:

元素 说明 生命周期
__platform_driver_init 模块初始化函数,调用platform_driver_register 模块加载时执行
__platform_driver_exit 模块退出函数,调用platform_driver_unregister 模块卸载时执行
platform_driver_register 将驱动注册到内核Platform总线,触发probe匹配 驱动与设备匹配时
platform_driver_unregister 从内核注销驱动,触发remove回调 驱动与设备分离时
最佳实践: 对于音频驱动,建议始终使用module_platform_driver宏,而非手动编写module_init/module_exit。这能避免因忘记注册/注销导致的资源泄漏,并保持代码风格与内核主线一致。

下面通过一个内联SVG图展示module_platform_driver宏的展开流程及其在驱动生命周期中的位置:

module_platform_driver 宏展开与驱动生命周期 module_platform_driver module_init 注册入口 module_exit 注册出口 platform_driver_register platform_driver_unregister 驱动生命周期流程 insmod 加载模块 probe 回调执行 音频设备就绪 remove 回调执行 rmmod 卸载模块 资源释放完成 注:probe 在设备与驱动匹配时自动调用,remove 在设备移除或模块卸载时调用

3.2 probe回调函数:音频驱动的初始化核心

probe回调是Platform驱动的初始化入口,当内核检测到设备树中描述的音频设备与当前驱动匹配时,自动调用此函数。对于音频驱动,probe需要完成以下关键任务:

  1. 获取硬件资源: 解析设备树,获取寄存器基地址、中断号、DMA通道等。
  2. 初始化硬件: 复位音频编解码器、配置时钟、设置I2S(Inter-IC Sound)接口参数。
  3. 分配ALSA组件: 创建struct snd_card、struct snd_pcm、struct snd_soc_dai等核心结构体。
  4. 注册ALSA设备: 调用snd_card_register使音频设备对用户空间可见。
常见陷阱: 在probe中申请资源时,必须使用devm_系列函数(如devm_ioremap_resource、devm_request_irq)。这些函数会在驱动卸载或probe失败时自动释放资源,避免内存泄漏。

以下是一个典型的音频Platform驱动probe实现示例:

static int audio_platform_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct audio_priv *priv;
    struct resource *res;
    int ret;

    /* 1. 分配私有数据结构 */
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    platform_set_drvdata(pdev, priv);

    /* 2. 获取寄存器基地址并映射 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(priv->regs))
        return PTR_ERR(priv->regs);

    /* 3. 获取中断号并注册中断处理函数 */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    ret = devm_request_irq(dev, priv->irq, audio_interrupt_handler,
                           IRQF_TRIGGER_HIGH, "audio_int", priv);
    if (ret) {
        dev_err(dev, "Failed to request IRQ %d\n", priv->irq);
        return ret;
    }

    /* 4. 初始化ALSA声卡 */
    ret = snd_card_new(dev, -1, NULL, THIS_MODULE, 0, &priv->card);
    if (ret < 0) {
        dev_err(dev, "Failed to create sound card\n");
        return ret;
    }

    /* 5. 创建PCM设备(音频数据流通道) */
    ret = snd_pcm_new(priv->card, "audio-pcm", 0, 1, 1, &priv->pcm);
    if (ret < 0) {
        dev_err(dev, "Failed to create PCM device\n");
        goto err_free_card;
    }

    /* 6. 设置PCM操作函数集 */
    snd_pcm_set_ops(priv->pcm, SNDRV_PCM_STREAM_PLAYBACK, &audio_pcm_ops);
    snd_pcm_set_ops(priv->pcm, SNDRV_PCM_STREAM_CAPTURE, &audio_pcm_ops);

    /* 7. 预分配DMA缓冲区 */
    snd_pcm_lib_preallocate_pages_for_all(priv->pcm,
                                          SNDRV_DMA_TYPE_DEV,
                                          dev, 64*1024, 256*1024);

    /* 8. 注册声卡,对用户空间可见 */
    ret = snd_card_register(priv->card);
    if (ret < 0) {
        dev_err(dev, "Failed to register sound card\n");
        goto err_free_card;
    }

    dev_info(dev, "Audio platform driver probed successfully\n");
    return 0;

err_free_card:
    snd_card_free(priv->card);
    return ret;
}

下表总结了probe中关键API的用途与注意事项:

API函数 用途 注意事项
platform_get_resource 获取设备树中定义的寄存器地址、DMA通道等资源 必须检查返回值是否为NULL
devm_ioremap_resource 将物理地址映射到内核虚拟地址空间 自动处理内存区域冲突检测
devm_request_irq 注册中断处理函数 使用IRQF_TRIGGER_*标志指定触发方式
snd_card_new 创建ALSA声卡实例 最后一个参数为私有数据大小,0表示无额外数据
snd_pcm_new 创建PCM设备,表示音频数据流通道 参数依次为:声卡、名称、设备索引、播放流数、捕获流数
snd_card_register 注册声卡,创建/dev/snd/下的设备节点 必须在所有组件初始化完成后调用

3.3 remove回调函数:资源的优雅释放

remove回调是probe的逆操作,在驱动卸载或设备热移除时调用。其核心职责是确保所有已分配资源被正确释放,避免内核崩溃或内存泄漏。

关键原则: 如果probe中全部使用devm_系列函数,remove的实现可以非常简洁,因为内核会在设备分离时自动释放托管资源。但ALSA组件需要显式释放。

典型的remove实现如下:

static int audio_platform_remove(struct platform_device *pdev)
{
    struct audio_priv *priv = platform_get_drvdata(pdev);

    /* 1. 释放ALSA声卡(会自动释放PCM、控制接口等子组件) */
    if (priv->card)
        snd_card_free(priv->card);

    /* 2. 禁用硬件(如关闭时钟、复位编解码器) */
    audio_hw_disable(priv);

    /* 3. 清除驱动私有数据(可选,devm_资源会自动处理) */
    platform_set_drvdata(pdev, NULL);

    dev_info(&pdev->dev, "Audio platform driver removed\n");
    return 0;
}
重要提醒: 不要在remove中手动调用kfree释放devm_kzalloc分配的内存,这会导致双重释放。同样,devm_ioremap_resource和devm_request_irq分配的I/O内存和中断也不应手动释放。

下面通过一个SVG图展示probe与remove在驱动生命周期中的对称关系:

probe 与 remove 的对称资源管理 probe 阶段(资源分配) devm_kzalloc 分配私有数据 devm_ioremap_resource 映射寄存器 devm_request_irq 注册中断 snd_card_new + snd_card_register 加载 卸载 remove 阶段(资源释放) snd_card_free 释放声卡 audio_hw_disable 关闭硬件 devm_资源自动释放(内核管理) platform_set_drvdata 清空数据 注:使用 devm_ 系列函数后,remove 只需处理 ALSA 组件和硬件关闭逻辑

3.4 设备树匹配与驱动绑定

Platform驱动的匹配依赖于设备树中的compatible属性。驱动需要定义一个of_device_id表,并在platform_driver中引用。

/* 设备树匹配表 */
static const struct of_device_id audio_of_match[] = {
    { .compatible = "vendor,audio-controller-v1", },
    { .compatible = "vendor,audio-controller-v2", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, audio_of_match);

/* Platform驱动结构体定义 */
static struct platform_driver audio_platform_driver = {
    .probe  = audio_platform_probe,
    .remove = audio_platform_remove,
    .driver = {
        .name = "audio_platform_drv",
        .of_match_table = audio_of_match,
        .pm = &audio_pm_ops,  /* 电源管理操作集 */
    },
};

/* 使用宏注册驱动 */
module_platform_driver(audio_platform_driver);
调试技巧: 如果驱动加载后probe未被调用,首先检查设备树中compatible字符串是否与audio_of_match表中的值完全一致(包括大小写)。可以使用cat /sys/bus/platform/devices/.../uevent查看内核实际匹配的compatible值。

下表总结了驱动匹配过程中的关键数据结构:

数据结构 作用 典型字段
struct of_device_id 定义驱动支持的设备兼容性字符串 .compatible:设备树匹配字符串
struct platform_driver 描述Platform驱动的核心结构体 .probe、.remove、.driver
struct device_driver 通用驱动结构体,嵌入在platform_driver.driver中 .name、.of_match_table、.pm

3.5 本章小结

本章详细讲解了音频Platform驱动的注册机制,核心要点包括:

  • module_platform_driver宏:自动生成模块入口/出口函数,简化驱动注册流程。
  • probe回调:负责获取硬件资源、初始化ALSA组件、注册声卡,是驱动初始化的核心。
  • remove回调:负责释放ALSA资源、关闭硬件,与probe形成对称的资源管理。
  • 设备树匹配:通过of_device_id表实现驱动与硬件设备的绑定。