4. PCM设备创建:snd_pcm_new()、snd_pcm_set_ops(),理解playback和capture方向

好,咱们继续往下走。上一节我们把 PCM 的数据结构理清楚了,这一节要动真格的了——怎么把一个 PCM 设备“造”出来,并且告诉内核它到底是用来播放还是录音。

说白了,你写一个音频驱动,最终目的就是让用户空间的应用程序能通过 ALSA 的 API 来操作你的硬件。而这一切的起点,就是创建 PCM 设备。

4.1 从 snd_pcm_new() 开始

这个函数是 PCM 设备的“构造函数”。我习惯把它叫做“出生证明”——调用它之后,内核里就有了一个 PCM 设备的骨架。

先看原型:

int snd_pcm_new(
    struct snd_card *card,      // 所属的声卡
    const char *id,             // 设备标识,比如 "hw:0,0"
    int device,                 // 设备编号,通常从 0 开始
    int playback_count,         // playback 子流数量
    int capture_count,          // capture 子流数量
    struct snd_pcm **rpcm       // 返回的 PCM 实例指针
);

这里有个关键点:playback_count 和 capture_count 可以同时不为 0。这意味着一个 PCM 设备可以同时支持播放和录音。我在项目中遇到过不少新手,以为一个 PCM 设备只能干一件事,其实不是的。

核心理解:playback_count 和 capture_count 决定了这个 PCM 设备有几个 playback 子流和几个 capture 子流。如果 playback_count = 1,capture_count = 0,那就是纯播放设备。反过来就是纯录音设备。两个都大于 0,那就是全双工设备。

举个例子:

struct snd_pcm *pcm;
int ret;

// 创建一个支持 1 路播放、1 路录音的 PCM 设备
ret = snd_pcm_new(card, "my_audio_device", 0, 1, 1, &pcm);
if (ret < 0) {
    dev_err(card->dev, "创建 PCM 设备失败: %d\n", ret);
    return ret;
}

嗯,这里要注意:device 编号不能乱填。同一个声卡下,每个 PCM 设备的编号必须唯一。你填 0,那就是第一个 PCM 设备,用户空间访问时就是 hw:0,0。填 1 就是 hw:0,1。

4.2 设置操作函数:snd_pcm_set_ops()

光有骨架不行,你得告诉内核怎么操作这个 PCM 设备。比如打开、关闭、准备、触发传输等等。这些操作都封装在 snd_pcm_ops 结构体里。

函数原型很简单:

void snd_pcm_set_ops(
    struct snd_pcm *pcm,        // PCM 实例
    int direction,              // 方向:SNDRV_PCM_STREAM_PLAYBACK 或 SNDRV_PCM_STREAM_CAPTURE
    const struct snd_pcm_ops *ops  // 操作函数集
);

这里有个容易踩坑的地方:direction 参数。你创建 PCM 设备时指定了 playback_count 和 capture_count,但设置操作函数时,需要分别给 playback 和 capture 方向设置不同的 ops。

为什么?因为播放和录音的硬件操作可能完全不同。比如播放需要配置 DAC,录音需要配置 ADC。你想想看,这两个方向的操作函数能一样吗?

我曾经在一个项目中,图省事把 playback 的 ops 直接赋值给了 capture,结果录音时一直没声音。查了两天才发现,capture 的 trigger 函数里调用了播放的启动代码……

看个正确的例子:

static const struct snd_pcm_ops my_playback_ops = {
    .open       = my_pcm_open,
    .close      = my_pcm_close,
    .hw_params  = my_pcm_hw_params,
    .hw_free    = my_pcm_hw_free,
    .prepare    = my_pcm_prepare,
    .trigger    = my_pcm_trigger,
    .pointer    = my_pcm_pointer,
};

static const struct snd_pcm_ops my_capture_ops = {
    .open       = my_pcm_open,
    .close      = my_pcm_close,
    .hw_params  = my_pcm_hw_params,
    .hw_free    = my_pcm_hw_free,
    .prepare    = my_pcm_prepare,
    .trigger    = my_capture_trigger,  // 注意:这里用了不同的 trigger 函数
    .pointer    = my_pcm_pointer,
};

// 分别设置两个方向的操作函数
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);

我的建议:即使 playback 和 capture 的 ops 完全一样,也建议分开定义两个结构体。这样以后修改时不会互相影响,代码可读性也更好。

4.3 理解 playback 和 capture 方向

这两个方向,说白了就是数据流动的方向:

  • Playback(播放):数据从用户空间 -> 内核 -> 硬件(DAC)-> 扬声器。数据是“流出”的。
  • Capture(录音):数据从麦克风 -> 硬件(ADC)-> 内核 -> 用户空间。数据是“流入”的。

在 ALSA 内部,这两个方向用枚举值区分:

enum sndrv_pcm_stream {
    SNDRV_PCM_STREAM_PLAYBACK = 0,
    SNDRV_PCM_STREAM_CAPTURE = 1,
};

你可能会问:为什么 playback 是 0,capture 是 1?嗯,这其实是个历史遗留问题。ALSA 早期设计时,播放被认为是“主要”功能,所以排在了前面。不过咱们写驱动时不用纠结这个,直接用宏定义就行。

4.4 完整的 PCM 设备创建流程

把上面这些串起来,一个典型的 PCM 设备创建流程是这样的:

static int my_card_probe(struct platform_device *pdev)
{
    struct snd_card *card;
    struct snd_pcm *pcm;
    int ret;

    // 1. 创建声卡
    ret = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE, 0, &card);
    if (ret < 0)
        return ret;

    // 2. 创建 PCM 设备:1 路播放,1 路录音
    ret = snd_pcm_new(card, "my_pcm", 0, 1, 1, &pcm);
    if (ret < 0) {
        snd_card_free(card);
        return ret;
    }

    // 3. 设置私有数据(通常指向你的硬件结构体)
    pcm->private_data = my_hardware_data;
    pcm->private_free = my_private_free;

    // 4. 设置操作函数
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_playback_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);

    // 5. 注册声卡
    ret = snd_card_register(card);
    if (ret < 0) {
        snd_card_free(card);
        return ret;
    }

    // 6. 保存声卡指针
    platform_set_drvdata(pdev, card);

    return 0;
}

避坑指南:我曾经在 snd_pcm_new 之后忘记设置 private_free 回调,结果在卸载驱动时发生了内存泄漏。private_free 会在 PCM 设备销毁时自动调用,用来释放你分配的私有数据。千万别忘了它!

4.5 小结

这一节我们干了三件事:

  1. 用 snd_pcm_new() 创建 PCM 设备的骨架
  2. 用 snd_pcm_set_ops() 给播放和录音方向分别绑定操作函数
  3. 理解了 playback 和 capture 的本质区别——数据流动方向不同

下一节我们会深入 snd_pcm_ops 里的每个回调函数,看看它们到底该怎么实现。到时候我会分享一些我在调试 DMA 传输时遇到的奇葩问题,保证让你少走弯路。