4. PCM周期与缓冲区管理

任务目标

在ALSA驱动中,PCM周期(period)与缓冲区(buffer)直接决定DMA传输的粒度与延迟。本案例实现一个虚拟声卡驱动,完成:配置period size与buffer size、实现prepared回调、在回调中正确计算单次DMA传输字节数。

核心概念与计算关系

  • buffer size:DMA缓冲区总大小(以帧为单位),必须为period size的整数倍。
  • period size:一次中断/DMA传输的帧数,通常为2的幂次。
  • period bytes = period size × 帧字节数(channels × sample_bits / 8)。
  • DMA传输大小:在prepared回调中,实际需要告知DMA引擎的单次搬运量即period_bytes,而缓冲区整体划分为多个period,由硬件中断循环触发。

完整驱动实现

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

#define DRV_NAME "demo-pcm"
#define BUFFER_MAX 65536   /* 最大buffer:65536帧 */
#define PERIOD_MAX  8192   /* 最大period:8192帧  */

static struct snd_pcm_hardware demo_pcm_hw = {
    .info = SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_MMAP,
    .formats = SNDRV_PCM_FMTBIT_S16_LE,
    .rates = SNDRV_PCM_RATE_48000,
    .rate_min = 48000,
    .rate_max = 48000,
    .channels_min = 2,
    .channels_max = 2,
    .buffer_bytes_max = BUFFER_MAX * 4,  /* 2ch × 2bytes */
    .period_bytes_min = 256,
    .period_bytes_max = PERIOD_MAX * 4,
    .periods_min = 2,
    .periods_max = 16,
};

static int demo_pcm_prepare(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *rt = substream->runtime;
    size_t period_bytes, buffer_bytes;

    /* 内核已根据应用层参数完成约束校验,
     * 此处直接读取实际配置值 */
    period_bytes = frames_to_bytes(rt, rt->period_size);
    buffer_bytes = frames_to_bytes(rt, rt->buffer_size);

    /* 校验:buffer必须是period的整数倍 */
    if (buffer_bytes % period_bytes != 0) {
        dev_err(substream->pcm->dev,
            "buffer(%zu) not aligned to period(%zu)\n",
            buffer_bytes, period_bytes);
        return -EINVAL;
    }

    /* 真实驱动在此配置DMA控制器:
     * 源/目的地址、传输方向、单次传输大小 = period_bytes
     * 传输次数 = buffer_bytes / period_bytes,由硬件中断循环触发 */
    dev_info(substream->pcm->dev,
        "prepared: period=%zu bytes, buffer=%zu bytes, periods=%zu\n",
        period_bytes, buffer_bytes,
        buffer_bytes / period_bytes);

    return 0;
}

static int demo_pcm_hw_params(struct snd_pcm_substream *substream,
                  struct snd_pcm_hw_params *params)
{
    /* 硬件参数已在框架层校验,
     * 此处可预留DMA缓冲区分配;
     * 本示例使用虚拟内存,无需额外操作 */
    return 0;
}

static int demo_pcm_open(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *rt = substream->runtime;

    rt->hw = demo_pcm_hw;
    /* 设置DMA内存访问回调(虚拟示例直接使用内核内存) */
    snd_pcm_set_managed_buffer(substream,
                   SNDRV_DMA_TYPE_VMALLOC, NULL, 0, 0);
    return 0;
}

static int demo_pcm_close(struct snd_pcm_substream *substream)
{
    return 0;
}

static int demo_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    /* 真实驱动在此启动/停止DMA;
     * 虚拟示例仅返回成功 */
    return 0;
}

static snd_pcm_uframes_t demo_pcm_pointer(struct snd_pcm_substream *substream)
{
    /* 返回当前DMA硬件位置(帧数);
     * 虚拟示例固定返回0,实际应从DMA控制器读取 */
    return 0;
}

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

static int demo_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
    struct snd_pcm *pcm = rtd->pcm;
    /* 注册PCM设备:播放+捕获 */
    return snd_pcm_add_substream(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                     NULL, 0, 1, &demo_pcm_ops);
}

static const struct snd_soc_component_driver demo_component = {
    .name = DRV_NAME,
    .pcm_new = demo_pcm_new,
};

static int demo_probe(struct platform_device *pdev)
{
    return devm_snd_soc_register_component(&pdev->dev,
                          &demo_component, NULL, 0);
}

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

module_platform_driver(demo_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("ALSA PCM period/buffer management demo");

编译与加载

# 内核源码树内编译(放入sound/soc/目录后)
make -C /lib/modules/$(uname -r)/build M=$PWD modules

# 加载
insmod demo-pcm.ko

# 确认设备节点生成
cat /proc/asound/cards | grep demo

验证方法

  1. 参数约束验证:使用aplay播放时指定非对齐参数,应返回错误:
# 期望失败:period=1000帧,buffer=3000帧(非整数倍)
aplay -D hw:0,0 -f S16_LE -c 2 -r 48000 \
      --buffer-size=3000 --period-size=1000 /dev/zero
# 内核日志应显示 "buffer not aligned to period"

# 期望成功:period=1024,buffer=4096(4倍关系)
aplay -D hw:0,0 -f S16_LE -c 2 -r 48000 \
      --buffer-size=4096 --period-size=1024 /dev/zero
# dmesg应输出 prepared: period=4096 bytes, buffer=16384 bytes, periods=4
  1. DMA大小计算核对:period_bytes = 1024 × 2ch × 2bytes = 4096,与日志输出一致,即单次DMA传输大小。
  2. 运行时验证:播放过程中cat /proc/asound/card0/pcm0p/sub0/hw_params可查看实际生效的period/buffer参数。

关键说明

  • snd_pcm_set_managed_buffer使用VMALLOC仅用于演示,真实驱动应使用SNDRV_DMA_TYPE_DEV_SG或连续DMA内存。
  • demo_pcm_pointer返回0会导致应用层认为缓冲区未推进,仅用于验证参数配置流程;实际驱动需从DMA控制器读取硬件位置。
  • 本示例聚焦prepare回调中的参数读取与校验,DMA启动逻辑在trigger中留空,不影响参数配置验证。