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
验证方法
- 参数约束验证:使用
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
- DMA大小计算核对:
period_bytes = 1024 × 2ch × 2bytes = 4096,与日志输出一致,即单次DMA传输大小。 - 运行时验证:播放过程中
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中留空,不影响参数配置验证。