1. ALSA驱动框架初探

任务目标

编写一个虚拟声卡驱动,不依赖真实硬件,向 ALSA 核心注册一个 PCM 设备。加载驱动后,用户空间可通过 aplay -l 或 cat /proc/asound/cards 查看到该声卡,并可通过 aplay 播放标准 WAV 文件验证数据通路。

环境与依赖

  • Linux 内核源码树(本案例基于 5.15+,接口向下兼容)
  • 已配置的内核构建环境(make oldconfig 后至少启用 CONFIG_SND 与 CONFIG_SND_PCM)
  • 用户空间工具:alsa-utils(提供 aplay/arecord)
  • 开发机需具备 root 权限用于加载模块

核心实现

驱动通过 snd_pcm_new() 创建 PCM 实例,并注册一个仅支持播放的虚拟 substream。关键点在于:snd_pcm_set_ops() 绑定 open/close/hw_params/prepare/trigger 等回调,其中 trigger 回调中直接以静音数据填充用户缓冲区,从而在无硬件中断的情况下模拟数据消费,保证 aplay 能正常完成写入与播放流程。

// virtual_card.c — 最小虚拟声卡驱动
#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/initval.h>

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

static int vpcm_open(struct snd_pcm_substream *substream) { return 0; }
static int vpcm_close(struct snd_pcm_substream *substream) { return 0; }
static int vpcm_hw_params(struct snd_pcm_substream *substream,
                          struct snd_pcm_hw_params *params) { return 0; }
static int vpcm_prepare(struct snd_pcm_substream *substream) { return 0; }

static int vpcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct snd_pcm_runtime *rt = substream->runtime;
    snd_pcm_sframes_t avail;
    void *buf;
    int i;

    if (cmd != SNDRV_PCM_TRIGGER_START)
        return -EINVAL;

    /* 模拟硬件消费:将用户空间写入的数据全部替换为静音,
       使 aplay 的写入循环能持续推进 */
    avail = snd_pcm_playback_avail(rt);
    if (avail > 0) {
        buf = rt->dma_area;
        for (i = 0; i < avail * rt->frame_bits / 8; i++)
            ((char *)buf)[i] = 0;
        snd_pcm_period_elapsed(substream);
    }
    return 0;
}

static snd_pcm_uframes_t vpcm_pointer(struct snd_pcm_substream *substream)
{
    return 0;  /* 固定位置,配合 trigger 中的 period_elapsed 驱动循环 */
}

static const struct snd_pcm_ops vpcm_ops = {
    .open      = vpcm_open,
    .close     = vpcm_close,
    .hw_params = vpcm_hw_params,
    .prepare   = vpcm_prepare,
    .trigger   = vpcm_trigger,
    .pointer   = vpcm_pointer,
};

static int vpcm_probe(struct platform_device *pdev)
{
    struct snd_pcm_hardware hw;
    int ret;

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

    strcpy(card->driver, "Virtual_Card");
    strcpy(card->shortname, "Virtual ALSA Card");
    strcpy(card->longname, "Virtual ALSA Card for PCM Demo");

    ret = snd_pcm_new(card, "virtual_pcm", 0, 1, 0, &pcm);
    if (ret < 0)
        goto fail;

    /* 定义硬件能力:仅播放,支持 44.1kHz/48kHz,16位,双声道 */
    memset(&hw, 0, sizeof(hw));
    hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
              SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER;
    hw.formats = SNDRV_PCM_FMTBIT_S16_LE;
    hw.rates = SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000;
    hw.rate_min = 44100;
    hw.rate_max = 48000;
    hw.channels_min = 2;
    hw.channels_max = 2;
    hw.buffer_bytes_max = 1024 * 1024;
    hw.period_bytes_min = 256;
    hw.period_bytes_max = 64 * 1024;
    hw.periods_min = 2;
    hw.periods_max = 16;

    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &vpcm_ops);
    snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
                                   NULL, 0, hw.buffer_bytes_max);

    ret = snd_card_register(card);
    if (ret < 0)
        goto fail;

    platform_set_drvdata(pdev, card);
    return 0;

fail:
    snd_card_free(card);
    return ret;
}

static int vpcm_remove(struct platform_device *pdev)
{
    snd_card_free(platform_get_drvdata(pdev));
    return 0;
}

static struct platform_driver vpcm_driver = {
    .driver = {
        .name = "virtual_card",
    },
    .probe  = vpcm_probe,
    .remove = vpcm_remove,
};

static struct platform_device *vpcm_device;

static int __init vpcm_init(void)
{
    int ret;

    ret = platform_driver_register(&vpcm_driver);
    if (ret < 0)
        return ret;

    vpcm_device = platform_device_register_simple("virtual_card", -1, NULL, 0);
    if (IS_ERR(vpcm_device)) {
        platform_driver_unregister(&vpcm_driver);
        return PTR_ERR(vpcm_device);
    }
    return 0;
}

static void __exit vpcm_exit(void)
{
    platform_device_unregister(vpcm_device);
    platform_driver_unregister(&vpcm_driver);
}

module_init(vpcm_init);
module_exit(vpcm_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal virtual ALSA sound card");

编译与加载

# 在内核源码树外编译(需已配置的内核构建环境)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载模块
sudo insmod virtual_card.ko

# 验证设备出现
cat /proc/asound/cards
aplay -l

验证方法

  1. 设备注册检查:cat /proc/asound/cards 应显示 Virtual ALSA Card 及其编号(如 1)。
  2. PCM 设备可见:aplay -l 应列出 card 1: Virtual [Virtual ALSA Card], device 0: virtual_pcm [virtual_pcm]。
  3. 播放功能测试:生成测试音频并播放:
# 生成 1 秒 44.1kHz 16位立体声正弦波
sox -n -r 44100 -c 2 -b 16 test.wav synth 1 sine 440

# 播放(指定声卡编号,假设为 1)
aplay -D hw:1,0 test.wav

通过标准:aplay 正常退出且无错误信息,播放耗时约等于音频时长(1秒),即表示数据通路完整——用户空间写入的数据被驱动正确接收并模拟消费。

常见问题

  • 加载失败(Unknown symbol):确认内核已启用 CONFIG_SND 与 CONFIG_SND_PCM,且模块与当前运行内核版本匹配。
  • aplay 报 Broken pipe:通常因 trigger 回调未正确调用 snd_pcm_period_elapsed(),导致 ALSA 认为硬件未消费数据而溢出。检查 vpcm_trigger 中是否在填充后调用了该函数。
  • 播放无声音(预期行为):本驱动为虚拟设备,不连接物理音频输出,播放静音数据是正常现象,验证重点在于数据流是否完整走通。