4. PCM操作集实现

任务目标

在ALSA驱动中,struct snd_pcm_ops是PCM设备与内核ALSA框架之间的核心接口。本案例实现一个最小但完整的PCM操作集,包含open、close、hw_params、hw_free、prepare、trigger回调,并通过ALSA框架的标准调用流程验证各回调按预期顺序被触发。

核心实现

#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

#define DRV_NAME "demo-pcm-ops"

static int demo_pcm_open(struct snd_pcm_substream *substream)
{
    pr_info("%s: open called\n", DRV_NAME);
    return 0;
}

static int demo_pcm_close(struct snd_pcm_substream *substream)
{
    pr_info("%s: close called\n", DRV_NAME);
    return 0;
}

static int demo_pcm_hw_params(struct snd_pcm_substream *substream,
                              struct snd_pcm_hw_params *params)
{
    pr_info("%s: hw_params called, rate=%d, channels=%d\n",
            DRV_NAME, params_rate(params), params_channels(params));
    return 0;
}

static int demo_pcm_hw_free(struct snd_pcm_substream *substream)
{
    pr_info("%s: hw_free called\n", DRV_NAME);
    return 0;
}

static int demo_pcm_prepare(struct snd_pcm_substream *substream)
{
    pr_info("%s: prepare called\n", DRV_NAME);
    return 0;
}

static int demo_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    pr_info("%s: trigger called, cmd=%d\n", DRV_NAME, cmd);
    return 0;
}

static struct snd_pcm_ops demo_pcm_ops = {
    .open       = demo_pcm_open,
    .close      = demo_pcm_close,
    .hw_params  = demo_pcm_hw_params,
    .hw_free    = demo_pcm_hw_free,
    .prepare    = demo_pcm_prepare,
    .trigger    = demo_pcm_trigger,
};

static int demo_pcm_new(struct snd_card *card, struct snd_device *device)
{
    struct snd_pcm *pcm;
    int err;

    err = snd_pcm_new(card, "demo-pcm", 0, 0, 1, &pcm);
    if (err < 0)
        return err;

    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &demo_pcm_ops);
    return 0;
}

static int demo_probe(struct platform_device *pdev)
{
    struct snd_card *card;
    int err;

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

    err = demo_pcm_new(card, NULL);
    if (err < 0)
        goto fail;

    strcpy(card->driver, DRV_NAME);
    strcpy(card->shortname, "Demo PCM Ops");
    strcpy(card->longname, "Demo PCM Ops Driver");

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

    platform_set_drvdata(pdev, card);
    return 0;

fail:
    snd_card_free(card);
    return err;
}

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

static struct platform_driver demo_driver = {
    .probe  = demo_probe,
    .remove = demo_remove,
    .driver = {
        .name = DRV_NAME,
    },
};

module_platform_driver(demo_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALSA Course");
MODULE_DESCRIPTION("Demo PCM ops implementation");

编译与加载

# Makefile
obj-m += demo_pcm_ops.o

# 编译
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载
sudo insmod demo_pcm_ops.ko

# 确认设备节点生成
cat /proc/asound/cards
# 应看到类似: 0 [Demo            ]: Demo - Demo PCM Ops

验证调用流程

# 使用arecord触发PCM操作(播放方向用aplay)
arecord -D hw:0,0 -f S16_LE -r 44100 -c 2 -d 2 /dev/null

# 观察内核日志
dmesg | tail -20

预期日志顺序(与代码逻辑严格对应):

demo-pcm-ops: open called
demo-pcm-ops: hw_params called, rate=44100, channels=2
demo-pcm-ops: prepare called
demo-pcm-ops: trigger called, cmd=1   # SNDRV_PCM_TRIGGER_START
demo-pcm-ops: trigger called, cmd=0   # SNDRV_PCM_TRIGGER_STOP
demo-pcm-ops: hw_free called
demo-pcm-ops: close called

判定标准

  • 加载模块后/proc/asound/cards出现新声卡,无错误信息;
  • 执行arecord后,dmesg中按上述顺序出现全部7条回调日志;
  • 回调参数正确:hw_params中的rate和channels与arecord参数一致;
  • trigger收到start(1)和stop(0)两个命令。

常见问题

  • 无设备节点:确认snd_pcm_new的playback参数为1(最后一个参数),且snd_pcm_set_ops设置了PLAYBACK方向;
  • hw_params未被调用:arecord必须指定格式参数(如-f S16_LE),否则ALSA可能跳过参数协商;
  • trigger只出现一次:若使用-d 2指定时长,停止时才会触发stop命令;直接Ctrl+C可能不触发stop。