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设备注册的完整初始化流程:

PCM设备初始化流程图 PCM设备初始化完整流程 snd_card_new() snd_pcm_new() 初始化 struct snd_pcm private_data / name / nonatomic snd_pcm_set_ops() snd_card_register() PCM设备就绪 关键说明 • snd_pcm_new()自动分配substream • 必须在register前完成所有配置 • private_data指向硬件上下文 常见错误 • 未设置private_data导致ops回调 无法获取硬件上下文 • 未调用snd_pcm_set_ops导致 打开设备时内核崩溃 • 在register后修改ops无效

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缓冲区的完整对象层次:

PCM设备对象层次结构图 PCM设备对象层次结构 struct snd_card struct snd_pcm streams[PLAYBACK] streams[CAPTURE] struct snd_pcm_substream struct snd_pcm_substream runtime (DMA缓冲区) runtime (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设备状态,这是定位问题最有效的手段。