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 小结
这一节我们干了三件事:
- 用
snd_pcm_new()创建 PCM 设备的骨架 - 用
snd_pcm_set_ops()给播放和录音方向分别绑定操作函数 - 理解了 playback 和 capture 的本质区别——数据流动方向不同
下一节我们会深入 snd_pcm_ops 里的每个回调函数,看看它们到底该怎么实现。到时候我会分享一些我在调试 DMA 传输时遇到的奇葩问题,保证让你少走弯路。