4. PCM设备创建:PCM设备注册与结构体初始化
PCM(Pulse Code Modulation)设备是ALSA声卡驱动中最核心的音频数据通道。本章将深入讲解如何通过snd_pcm_new()注册PCM设备,以及如何正确初始化struct snd_pcm结构体中的关键成员。掌握这些内容是驱动移植适配的基础。
核心概念: 一个声卡(card)可以包含多个PCM设备(playback/capture),每个PCM设备又包含一个或多个PCM substream(子流)。驱动开发者的主要工作就是创建PCM设备并配置其操作函数集。
4.1 PCM设备注册流程
PCM设备的注册遵循“创建 → 初始化 → 设置操作集 → 注册”的固定流程。核心API函数为snd_pcm_new(),其原型如下:
int snd_pcm_new(struct snd_card *card,
const char *id,
int device,
int playback_count,
int capture_count,
struct snd_pcm **rpcm);
| 参数 | 说明 | 典型值 |
|---|---|---|
card |
指向声卡对象的指针,由snd_card_new()返回 |
驱动私有数据中的card指针 |
id |
PCM设备的标识字符串,用于proc文件系统 | "plughw:0,0" 或 "default" |
device |
设备编号,从0开始递增 | 0, 1, 2 ... |
playback_count |
播放子流数量,0表示不支持播放 | 1(单声道)或 2(立体声) |
capture_count |
录音子流数量,0表示不支持录音 | 1 或 0 |
rpcm |
输出参数,返回创建的PCM设备指针 | &pcm |
典型调用示例:
struct snd_pcm *pcm;
int ret;
ret = snd_pcm_new(card, "My Audio Device", 0, 1, 1, &pcm);
if (ret < 0) {
dev_err(card->dev, "Failed to create PCM device: %d\n", ret);
return ret;
}
4.2 struct snd_pcm 结构体深度解析
struct snd_pcm是ALSA框架中描述PCM设备的核心结构体,其定义位于include/sound/core.h。驱动开发者需要重点关注以下成员:
| 成员 | 类型 | 说明 | 初始化时机 |
|---|---|---|---|
card |
struct snd_card * |
所属声卡指针 | 由snd_pcm_new()自动赋值 |
device |
int |
设备编号 | 由snd_pcm_new()自动赋值 |
streams[2] |
struct snd_pcm_str |
播放(SNDRV_PCM_STREAM_PLAYBACK=0)和录音(SNDRV_PCM_STREAM_CAPTURE=1)子流数组 | 由snd_pcm_new()分配,驱动需进一步配置substream |
ops |
struct snd_pcm_ops * |
PCM操作函数集,驱动必须实现 | 调用snd_pcm_set_ops()设置 |
private_data |
void * |
驱动私有数据指针,通常指向硬件描述结构体 | 驱动手动赋值 |
name |
char[64] |
PCM设备名称,显示在/proc/asound中 |
驱动手动赋值 |
nonatomic |
unsigned int |
标志位:若置1,表示ops回调可能在非原子上下文中调用 | 驱动手动赋值 |
常见陷阱: 很多初学者忘记设置
pcm->private_data,导致在ops回调中无法获取硬件上下文。务必在创建PCM设备后立即将驱动私有数据指针赋值给private_data。
4.3 PCM操作函数集(snd_pcm_ops)
struct snd_pcm_ops定义了PCM设备的核心操作,驱动必须实现这些回调函数。其结构如下:
struct snd_pcm_ops {
int (*open)(struct snd_pcm_substream *substream);
int (*close)(struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_pcm_substream *substream,
unsigned int cmd, void *arg);
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);
int (*hw_free)(struct snd_pcm_substream *substream);
int (*prepare)(struct snd_pcm_substream *substream);
int (*trigger)(struct snd_pcm_substream *substream, int cmd);
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
int (*copy_user)(struct snd_pcm_substream *substream,
int channel, unsigned long pos,
void __user *buf, unsigned long count);
int (*copy_kernel)(struct snd_pcm_substream *substream,
int channel, unsigned long pos,
void *buf, unsigned long count);
int (*fill_silence)(struct snd_pcm_substream *substream,
int channel, unsigned long pos,
unsigned long count);
struct page *(*page)(struct snd_pcm_substream *substream,
unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream,
struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
};
| 回调函数 | 调用时机 | 必须实现 | 典型操作 |
|---|---|---|---|
open |
应用层打开PCM设备时 | 是 | 初始化硬件、分配DMA缓冲区、设置默认参数 |
close |
应用层关闭PCM设备时 | 是 | 释放硬件资源、停止DMA传输 |
hw_params |
应用层设置硬件参数时 | 是 | 验证采样率、格式、通道数,配置硬件寄存器 |
prepare |
每次启动传输前 | 是 | 重置DMA指针、配置环形缓冲区 |
trigger |
启动/停止/暂停传输时 | 是 | 操作DMA控制器启动或停止 |
pointer |
ALSA定时查询当前DMA位置 | 是 | 返回当前硬件DMA指针位置 |
设置操作函数集:
static const struct snd_pcm_ops my_pcm_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,
};
// 在probe函数中设置
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_pcm_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_pcm_ops);
4.4 PCM设备初始化完整流程
以下SVG图展示了从声卡创建到PCM设备注册的完整初始化流程:
4.5 完整代码示例:PCM设备创建与初始化
以下代码展示了在声卡驱动probe函数中创建并初始化PCM设备的完整过程:
static int my_audio_probe(struct platform_device *pdev)
{
struct snd_card *card;
struct snd_pcm *pcm;
struct my_chip *chip;
int ret;
// 1. 创建声卡
ret = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE,
sizeof(struct my_chip), &card);
if (ret < 0)
return ret;
chip = card->private_data;
chip->card = card;
// 2. 创建PCM设备:1个播放子流,1个录音子流
ret = snd_pcm_new(card, "my_audio_device", 0, 1, 1, &pcm);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to create PCM: %d\n", ret);
goto err_free_card;
}
// 3. 初始化PCM结构体
pcm->private_data = chip; // 关联硬件上下文
strcpy(pcm->name, "My Audio PCM"); // 设置设备名称
pcm->nonatomic = 1; // 允许非原子上下文
// 4. 设置操作函数集
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_pcm_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_pcm_ops);
// 5. 设置硬件参数约束(可选,但推荐)
// snd_pcm_hw_constraint_list() 等
// 6. 注册声卡
ret = snd_card_register(card);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to register card: %d\n", ret);
goto err_free_card;
}
platform_set_drvdata(pdev, card);
return 0;
err_free_card:
snd_card_free(card);
return ret;
}
4.6 PCM Substream 结构体关系
理解PCM设备与substream的层级关系对驱动开发至关重要。以下SVG图展示了从声卡到DMA缓冲区的完整对象层次:
关键理解: 每个
struct snd_pcm_substream对应一个独立的音频数据流。驱动在open回调中通过substream->runtime访问DMA缓冲区配置。一个PCM设备可以同时支持多个substream,但通常嵌入式设备每个方向只使用一个substream。
4.7 常见问题与调试技巧
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 打开PCM设备时内核崩溃 | 未设置snd_pcm_set_ops()或ops回调为NULL |
检查所有必须实现的回调函数是否已赋值 |
| 录音/播放无声音 | hw_params中未正确配置硬件寄存器 |
在hw_params回调中添加寄存器dump调试 |
| DMA指针位置异常 | pointer回调返回错误的位置值 |
确保返回的是帧数(frames)而非字节数 |
| 设备名称不显示 | 未设置pcm->name |
在snd_pcm_new()后立即赋值 |
| 多实例冲突 | 多个PCM设备使用了相同的device编号 | 确保每个PCM设备的device参数唯一 |
调试建议: 在
open和hw_params回调中添加dev_dbg()打印,通过cat /proc/asound/card0/pcm0p/sub0/status查看PCM设备状态,这是定位问题最有效的手段。