ALSA 的 PCM 中间层功能非常强大,驱动程序只需实现访问其硬件的底层函数即可。 要访问 PCM 层,首先需要包含头文件
每个声卡设备最多可以有四个 PCM 实例。每个 PCM 实例对应一个 PCM 设备文件。这个实例数量的限制仅来自于 Linux 设备号的位数限制。一旦使用 64 位设备号,就可以支持更多的 PCM 实例。
一个 PCM 实例由 PCM 播放和录音流组成,每个 PCM 流又由一个或多个 PCM 子流(substream)构成。一些声卡支持多个播放功能。例如,emu10k1 声卡拥有多达 32 路立体声 PCM 播放子流。在这种情况下,每次打开设备时,通常会自动选择并打开一个空闲的子流。而当一个子流已被打开,后续打开操作将根据文件打开模式,要么阻塞等待,要么返回 EAGAIN 错误。但你在驱动中无需关心这些细节,PCM 中间层会自动处理这些工作。
下面的示例代码不包含任何硬件访问的例程,仅展示了如何构建 PCM 接口的框架结构 :
#include
....
/* hardware definition */
static struct snd_pcm_hardware snd_mychip_playback_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* hardware definition */
static struct snd_pcm_hardware snd_mychip_capture_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* open callback */
static int snd_mychip_playback_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_playback_hw;
/* more hardware-initialization will be done here */
....
return 0;
}
/* close callback */
static int snd_mychip_playback_close(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
/* the hardware-specific codes will be here */
....
return 0;
}
/* open callback */
static int snd_mychip_capture_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_capture_hw;
/* more hardware-initialization will be done here */
....
return 0;
}
/* close callback */
static int snd_mychip_capture_close(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
/* the hardware-specific codes will be here */
....
return 0;
}
/* hw_params callback */
static int snd_mychip_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
/* the hardware-specific codes will be here */
....
return 0;
}
/* hw_free callback */
static int snd_mychip_pcm_hw_free(struct snd_pcm_substream *substream)
{
/* the hardware-specific codes will be here */
....
return 0;
}
/* prepare callback */
static int snd_mychip_pcm_prepare(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/* set up the hardware with the current configuration
* for example...
*/
mychip_set_sample_format(chip, runtime->format);
mychip_set_sample_rate(chip, runtime->rate);
mychip_set_channels(chip, runtime->channels);
mychip_set_dma_setup(chip, runtime->dma_addr,
chip->buffer_size,
chip->period_size);
return 0;
}
/* trigger callback */
static int snd_mychip_pcm_trigger(struct snd_pcm_substream *substream,
int cmd)
{
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
/* do something to start the PCM engine */
....
break;
case SNDRV_PCM_TRIGGER_STOP:
/* do something to stop the PCM engine */
....
break;
default:
return -EINVAL;
}
}
/* pointer callback */
static snd_pcm_uframes_t
snd_mychip_pcm_pointer(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
unsigned int current_ptr;
/* get the current hardware pointer */
current_ptr = mychip_get_hw_pointer(chip);
return current_ptr;
}
/* operators */
static struct snd_pcm_ops snd_mychip_playback_ops = {
.open = snd_mychip_playback_open,
.close = snd_mychip_playback_close,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
/* operators */
static struct snd_pcm_ops snd_mychip_capture_ops = {
.open = snd_mychip_capture_open,
.close = snd_mychip_capture_close,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
/*
* definitions of capture are omitted here...
*/
/* create a pcm device */
static int snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
int err;
err = snd_pcm_new(chip->card, "My Chip", 0, 1, 1, &pcm);
if (err < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name, "My Chip");
chip->pcm = pcm;
/* set operators */
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
&snd_mychip_capture_ops);
/* pre-allocation of buffers */
/* NOTE: this may fail */
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
&chip->pci->dev,
64*1024, 64*1024);
return 0;
}
PCM 实例是通过 snd_pcm_new() 函数分配的。最好为 PCM 创建一个构造函数,即:
static int snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
int err;
err = snd_pcm_new(chip->card, "My Chip", 0, 1, 1, &pcm);
if (err < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name, "My Chip");
chip->pcm = pcm;
...
return 0;
}
snd_pcm_new() 函数有六个参数。第一个参数是该 PCM 所属的声卡指针,第二个参数是该 PCM 的 ID 字符串。
第三个参数(如上例中的 0)是该新 PCM 的索引,从 0 开始计数。如果你创建多个 PCM 实例,需要为该参数指定不同的数值。例如,第二个 PCM 设备的 index 应为 1。 第四和第五个参数分别表示播放和录音的子流(substream)数量。这里两个参数都使用了 1。如果不需要播放或录音子流,则将对应参数设为 0。
如果芯片支持多个播放或录音通道,你可以传入更大的数值,但在 open/close 等回调函数中必须妥善处理这些子流。当你需要知道当前操作的是哪个子流时,可以通过回调函数中传入的 struct snd_pcm_substream 数据来获取,方式如下:
struct snd_pcm_substream *substream;
int index = substream->number;
在创建 PCM 之后,你需要为每个 PCM 流设置操作函数(operators):
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
&snd_mychip_capture_ops);
操作函数通常定义如下:
static struct snd_pcm_ops snd_mychip_playback_ops = {
.open = snd_mychip_pcm_open,
.close = snd_mychip_pcm_close,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
所有回调函数的说明都在“操作函数(Operators)”小节中。
在设置完操作函数之后,你可能还希望预分配缓冲区,并设置托管的内存分配模式。为此,只需调用以下函数:
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
&chip->pci->dev,
64*1024, 64*1024);
默认情况下,该函数会分配最多 64KB 的缓冲区。缓冲区管理的详细内容将在后续的“缓冲区与内存管理”章节中进行说明。
此外,你还可以通过 pcm->info_flags 为该 PCM 设置一些附加信息。可用的取值在
当你的声卡芯片仅支持半双工时,可以像下面这样设置:
pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;
PCM 实例通常不需要专门的析构函数。因为 PCM 设备会由中间层代码自动释放,所以你无需显式调用析构函数。
只有在你在驱动内部创建了特殊的数据结构,并且需要手动释放它们时,才需要定义析构函数。在这种情况下,可以将析构函数设置为 pcm->private_free:
static void mychip_pcm_free(struct snd_pcm *pcm)
{
struct mychip *chip = snd_pcm_chip(pcm);
/* free your own data */
kfree(chip->my_private_pcm_data);
/* do what you like else */
....
}
static int snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
....
/* allocate your own data */
chip->my_private_pcm_data = kmalloc(...);
/* set the destructor */
pcm->private_data = chip;
pcm->private_free = mychip_pcm_free;
....
}
当 PCM 子流(substream)被打开时,会分配一个 PCM 运行时实例(runtime instance),并将其赋值给该子流。这个指针可以通过 substream->runtime 访问。
该 runtime 指针保存了大部分用于控制 PCM 的关键信息:例如 hw_params 和 sw_params 的副本配置、缓冲区指针、mmap 映射记录、自旋锁等。
运行时实例的定义可以在
struct _snd_pcm_runtime {
/* -- Status -- */
struct snd_pcm_substream *trigger_master;
snd_timestamp_t trigger_tstamp; /* trigger timestamp */
int overrange;
snd_pcm_uframes_t avail_max;
snd_pcm_uframes_t hw_ptr_base; /* Position at buffer restart */
snd_pcm_uframes_t hw_ptr_interrupt; /* Position at interrupt time*/
/* -- HW params -- */
snd_pcm_access_t access; /* access mode */
snd_pcm_format_t format; /* SNDRV_PCM_FORMAT_* */
snd_pcm_subformat_t subformat; /* subformat */
unsigned int rate; /* rate in Hz */
unsigned int channels; /* channels */
snd_pcm_uframes_t period_size; /* period size */
unsigned int periods; /* periods */
snd_pcm_uframes_t buffer_size; /* buffer size */
unsigned int tick_time; /* tick time */
snd_pcm_uframes_t min_align; /* Min alignment for the format */
size_t byte_align;
unsigned int frame_bits;
unsigned int sample_bits;
unsigned int info;
unsigned int rate_num;
unsigned int rate_den;
/* -- SW params -- */
struct timespec tstamp_mode; /* mmap timestamp is updated */
unsigned int period_step;
unsigned int sleep_min; /* min ticks to sleep */
snd_pcm_uframes_t start_threshold;
/*
* The following two thresholds alleviate playback buffer underruns; when
* hw_avail drops below the threshold, the respective action is triggered:
*/
snd_pcm_uframes_t stop_threshold; /* - stop playback */
snd_pcm_uframes_t silence_threshold; /* - pre-fill buffer with silence */
snd_pcm_uframes_t silence_size; /* max size of silence pre-fill; when >= boundary,
* fill played area with silence immediately */
snd_pcm_uframes_t boundary; /* pointers wrap point */
/* internal data of auto-silencer */
snd_pcm_uframes_t silence_start; /* starting pointer to silence area */
snd_pcm_uframes_t silence_filled; /* size filled with silence */
snd_pcm_sync_id_t sync; /* hardware synchronization ID */
/* -- mmap -- */
volatile struct snd_pcm_mmap_status *status;
volatile struct snd_pcm_mmap_control *control;
atomic_t mmap_count;
/* -- locking / scheduling -- */
spinlock_t lock;
wait_queue_head_t sleep;
struct timer_list tick_timer;
struct fasync_struct *fasync;
/* -- private section -- */
void *private_data;
void (*private_free)(struct snd_pcm_runtime *runtime);
/* -- hardware description -- */
struct snd_pcm_hardware hw;
struct snd_pcm_hw_constraints hw_constraints;
/* -- timer -- */
unsigned int timer_resolution; /* timer resolution */
/* -- DMA -- */
unsigned char *dma_area; /* DMA area */
dma_addr_t dma_addr; /* physical bus address (not accessible from main CPU) */
size_t dma_bytes; /* size of DMA area */
struct snd_dma_buffer *dma_buffer_p; /* allocated buffer */
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
/* -- OSS things -- */
struct snd_pcm_oss_runtime oss;
#endif
};
对于每个声卡驱动中的操作函数(回调函数),大多数运行时结构中的字段应被视为只读。它们通常由 PCM 中间层负责修改和更新。
例外情况包括:硬件描述信息(hw)、DMA 缓冲区信息以及私有数据(private_data),这些字段是可以由驱动自行设置的。
此外,如果你使用的是标准的托管缓冲区分配模式(managed buffer allocation mode),则无需手动设置 DMA 缓冲区的信息。
在接下来的章节中,将对这些重要字段进行说明。
硬件描述符(struct snd_pcm_hardware)用于定义底层硬件的基本配置信息。最重要的是,你需要在 PCM 的 open 回调函数中设置这个描述符。
需要注意的是,运行时实例中保存的是描述符的一个副本,而不是对原始描述符的指针。也就是说,在 open 回调中,你可以根据需要修改这个副本(即 runtime->hw)。
例如,如果某些芯片型号上最多只支持 1 个通道,你仍然可以使用相同的硬件描述结构体,只需在 open 回调中修改 channels_max 即可:
struct snd_pcm_runtime *runtime = substream->runtime;
...
runtime->hw = snd_mychip_playback_hw; /* common definition */
if (chip->model == VERY_OLD_ONE)
runtime->hw.channels_max = 1;
通常,你会像下面这样定义一个硬件描述符:
static struct snd_pcm_hardware snd_mychip_playback_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
info 字段用于指定该 PCM 的类型和功能能力。其位标志定义在
formats 字段包含了所支持的音频数据格式的位标志(SNDRV_PCM_FMTBIT_XXX)。如果硬件支持多种格式,则需要使用按位或(|)的方式将所有支持的格式组合起来。在上面的示例中,指定的是 16 位有符号小端格式(signed 16bit little-endian)。
rates 字段包含所支持的采样率的位标志(SNDRV_PCM_RATE_XXX)。如果芯片支持连续的采样率范围,还需要额外添加 SNDRV_PCM_RATE_CONTINUOUS 标志。预定义的采样率位标志只覆盖常见的标准采样率。如果你的芯片支持一些非常规的采样率,还需要添加 SNDRV_PCM_RATE_KNOT 标志,并手动设置相应的硬件约束(后文会详细介绍)。
rate_min 和 rate_max 用于定义支持的最小和最大采样率。它们的取值应与 rates 位标志相对应。
正如你可能已经猜到的,channels_min 和 channels_max 用于定义支持的最小和最大通道数。
buffer_bytes_max 定义了缓冲区的最大字节数。没有 buffer_bytes_min 字段,因为它可以通过最小的 period 大小和最小的 period 数量计算得出。与此同时,period_bytes_min 和 period_bytes_max 分别定义了 period(周期)大小的最小值和最大值(以字节为单位);而 periods_min 和 periods_max 则定义了缓冲区中 period 数量的最小值和最大值。“period” 这个术语在 OSS(Open Sound System)中相当于“碎片(fragment)”。period 决定了何时触发一次 PCM 中断,这通常取决于硬件。一般来说,period 越小,中断次数越多,从而可以更及时地填充或清空缓冲区。在录音(capture)方向上,period 大小决定了输入延迟;而在播放(playback)方向上,整个缓冲区的大小决定了输出延迟。
还有一个名为 fifo_size 的字段,用于指定硬件 FIFO 的大小。但目前该字段既不会被驱动使用,也不会被 alsa-lib 使用,因此你可以忽略它。
接下来,我们回到 PCM 的运行时结构体记录。运行时实例中最常被访问的部分是 PCM 的配置信息。这些配置信息在应用程序通过 alsa-lib 发送 hw_params 数据之后,会被存储到运行时结构体中。很多字段都是从 hw_params 和 sw_params 结构体中复制过来的。
例如,format 字段表示应用程序选择的音频数据格式,它的值是一个枚举类型,即 SNDRV_PCM_FORMAT_XXX。
需要注意的一点是:配置好的缓冲区大小和 period 大小在运行时结构体中是以“帧(frame)”为单位存储的。在 ALSA 中,1 帧 = 通道数 × 每个采样的字节数。
如果你需要在帧数和字节数之间进行转换,可以使用辅助函数 frames_to_bytes() 和 bytes_to_frames()。
period_bytes = frames_to_bytes(runtime, runtime->period_size);
此外,许多软件参数(sw_params)也同样是以帧(frames)为单位进行存储的。请务必检查字段的数据类型。
snd_pcm_uframes_t 表示无符号整型的帧数,而 snd_pcm_sframes_t 表示有符号整型的帧数。
参考学习:
https://kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html#pcm-interface