1. ALSA驱动框架初探

任务目标

编写一个最小的虚拟声卡驱动,向 ALSA 核心注册 snd_card,并在该声卡上创建一个 PCM 播放设备。驱动加载后,系统应出现 /dev/snd/pcmC0D0p 设备节点,且可以通过 ALSA 工具或应用程序打开该设备进行基本的播放操作。

环境与依赖

  • Linux 内核源码树(本案例基于 5.15 版本验证,其他版本接口基本一致)
  • 内核头文件与编译工具链(gcc、make)
  • 目标系统已启用 ALSA 框架(CONFIG_SND)
  • 需要 root 权限加载/卸载模块

实现原理

ALSA 驱动核心对象是 snd_card,每个声卡对应一个 card 实例。PCM 设备通过 snd_pcm_new() 创建并挂载到 card 上。本驱动不涉及真实硬件,仅分配内存作为 DMA 缓冲区,并实现 playback 方向的 open/close/hw_params/prepare/trigger 回调,使设备能够被打开并完成基本的数据流操作。

关键完整代码

// minimal_alsa_card.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/initval.h>

#define DRV_NAME "minialsa"

static struct snd_card *card;
static struct snd_pcm *pcm;
static void *dma_area;

static int snd_mini_pcm_open(struct snd_pcm_substream *substream)
{
    return 0;
}

static int snd_mini_pcm_close(struct snd_pcm_substream *substream)
{
    return 0;
}

static int snd_mini_pcm_hw_params(struct snd_pcm_substream *substream,
                                  struct snd_pcm_hw_params *hw_params)
{
    return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));
}

static int snd_mini_pcm_hw_free(struct snd_pcm_substream *substream)
{
    return snd_pcm_lib_free_pages(substream);
}

static int snd_mini_pcm_prepare(struct snd_pcm_substream *substream)
{
    return 0;
}

static int snd_mini_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    return 0;
}

static snd_pcm_uframes_t snd_mini_pcm_pointer(struct snd_pcm_substream *substream)
{
    return 0;
}

static const struct snd_pcm_ops snd_mini_pcm_ops = {
    .open      = snd_mini_pcm_open,
    .close     = snd_mini_pcm_close,
    .hw_params = snd_mini_pcm_hw_params,
    .hw_free   = snd_mini_pcm_hw_free,
    .prepare   = snd_mini_pcm_prepare,
    .trigger   = snd_mini_pcm_trigger,
    .pointer   = snd_mini_pcm_pointer,
};

static int snd_mini_probe(struct platform_device *pdev)
{
    int err;

    err = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE, 0, &card);
    if (err < 0)
        return err;

    err = snd_pcm_new(card, "mini PCM", 0, 1, 0, &pcm);
    if (err < 0)
        goto fail_pcm;

    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_mini_pcm_ops);
    pcm->dma_device = &pdev->dev;

    err = snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
                                                &pdev->dev, 64 * 1024, 64 * 1024);
    if (err < 0)
        goto fail_pcm;

    strcpy(card->driver, DRV_NAME);
    strcpy(card->shortname, "Mini ALSA Card");
    strcpy(card->longname, "Mini Virtual ALSA Card");

    err = snd_card_register(card);
    if (err < 0)
        goto fail_register;

    platform_set_drvdata(pdev, card);
    return 0;

fail_register:
    snd_pcm_lib_preallocate_free_for_all(pcm);
fail_pcm:
    snd_card_free(card);
    return err;
}

static int snd_mini_remove(struct platform_device *pdev)
{
    card = platform_get_drvdata(pdev);
    snd_card_free(card);
    return 0;
}

static struct platform_driver mini_driver = {
    .driver = {
        .name = DRV_NAME,
    },
    .probe  = snd_mini_probe,
    .remove = snd_mini_remove,
};

static struct platform_device *mini_device;

static int __init mini_init(void)
{
    int err;

    err = platform_driver_register(&mini_driver);
    if (err < 0)
        return err;

    mini_device = platform_device_register_simple(DRV_NAME, -1, NULL, 0);
    if (IS_ERR(mini_device)) {
        platform_driver_unregister(&mini_driver);
        return PTR_ERR(mini_device);
    }

    return 0;
}

static void __exit mini_exit(void)
{
    platform_device_unregister(mini_device);
    platform_driver_unregister(&mini_driver);
}

module_init(mini_init);
module_exit(mini_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALSA Course");
MODULE_DESCRIPTION("Minimal virtual ALSA sound card");

编译与加载

# 在内核源码树外编译(需已配置内核头文件)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载模块
sudo insmod minimal_alsa_card.ko

# 验证设备节点
ls -l /dev/snd/pcmC0D0p

# 查看声卡注册信息
cat /proc/asound/cards

验证方法

  1. 加载模块后执行 ls /dev/snd/pcmC0D0p,若存在该节点则说明 PCM 设备已成功注册。
  2. 执行 cat /proc/asound/cards,应能看到编号为 0 的声卡,名称包含 "Mini ALSA Card"。
  3. 使用 aplay -D hw:0,0 /usr/share/sounds/alsa/Front_Center.wav 播放测试音频(若系统无此文件,可用 speaker-test -D hw:0,0 -c 2 -t wav 替代)。命令能正常执行且无 I/O 错误,即表示设备可被 ALSA 应用层打开并完成数据提交。

常见问题

  • 设备节点未出现:检查 dmesg 是否有错误输出,确认 snd_card_register 是否成功;确认系统声卡编号是否已被其他声卡占用(若 card 编号不为 0,节点名会相应变化)。
  • aplay 报 no such device:确认 PCM 设备方向正确(本驱动只创建了 playback 方向),使用 aplay -D hw:0,0 而非 arecord。
  • 编译报错:确认内核头文件版本与运行内核一致,且 CONFIG_SND 已启用。