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
验证方法
- 设备注册检查:
cat /proc/asound/cards应显示Virtual ALSA Card及其编号(如1)。 - PCM 设备可见:
aplay -l应列出card 1: Virtual [Virtual ALSA Card], device 0: virtual_pcm [virtual_pcm]。 - 播放功能测试:生成测试音频并播放:
# 生成 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中是否在填充后调用了该函数。- 播放无声音(预期行为):本驱动为虚拟设备,不连接物理音频输出,播放静音数据是正常现象,验证重点在于数据流是否完整走通。