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宏的展开流程及其在驱动生命周期中的位置:
3.2 probe回调函数:音频驱动的初始化核心
probe回调是Platform驱动的初始化入口,当内核检测到设备树中描述的音频设备与当前驱动匹配时,自动调用此函数。对于音频驱动,probe需要完成以下关键任务:
- 获取硬件资源: 解析设备树,获取寄存器基地址、中断号、DMA通道等。
- 初始化硬件: 复位音频编解码器、配置时钟、设置I2S(Inter-IC Sound)接口参数。
- 分配ALSA组件: 创建
struct snd_card、struct snd_pcm、struct snd_soc_dai等核心结构体。 - 注册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在驱动生命周期中的对称关系:
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表实现驱动与硬件设备的绑定。