PCM 接口:
ALSA 的 PCM 中间层功能非常强大,驱动程序只需实现访问其硬件的底层函数即可。 要访问 PCM 层,首先需要包含头文件 <sound/pcm.h>。此外,如果需要访问与 hw_param 相关的一些函数,还可能需要包含 <sound/pcm_params.h>。
每个声卡设备最多可以有四个 PCM 实例。每个 PCM 实例对应一个 PCM 设备文件。这个实例数量的限制仅来自于 Linux 设备号的位数限制。一旦使用 64 位设备号,就可以支持更多的 PCM 实例。
一个 PCM 实例由 PCM 播放和录音流组成,每个 PCM 流又由一个或多个 PCM 子流(substream)构成。一些声卡支持多个播放功能。例如,emu10k1 声卡拥有多达 32 路立体声 PCM 播放子流。在这种情况下,每次打开设备时,通常会自动选择并打开一个空闲的子流。而当一个子流已被打开,后续打开操作将根据文件打开模式,要么阻塞等待,要么返回 EAGAIN 错误。但你在驱动中无需关心这些细节,PCM 中间层会自动处理这些工作。
下面的示例代码不包含任何硬件访问的例程,仅展示了如何构建 PCM 接口的框架结构 :
#include <sound/pcm.h>
....
/* 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 构造函数
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 设置一些附加信息。可用的取值在 <sound/asound.h> 中以 SNDRV_PCM_INFO_XXX 形式定义,这些标志用于描述硬件特性(将在后文详细介绍)。
当你的声卡芯片仅支持半双工时,可以像下面这样设置:
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 信息的宝库
当 PCM 子流(substream)被打开时,会分配一个 PCM 运行时实例(runtime instance),并将其赋值给该子流。这个指针可以通过 substream->runtime 访问。
该 runtime 指针保存了大部分用于控制 PCM 的关键信息:例如 hw_params 和 sw_params 的副本配置、缓冲区指针、mmap 映射记录、自旋锁等。
运行时实例的定义可以在 <sound/pcm.h> 中找到。以下是该头文件中的相关部分内容:
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 的类型和功能能力。其位标志定义在 <sound/asound.h> 中,形式为 SNDRV_PCM_INFO_XXX。在此字段中,你至少需要指定是否支持 mmap 以及支持哪种交错格式。如果硬件支持 mmap,则需要添加 SNDRV_PCM_INFO_MMAP 标志。如果硬件支持交错格式或非交错格式,则分别需要设置 SNDRV_PCM_INFO_INTERLEAVED 或 SNDRV_PCM_INFO_NONINTERLEAVED 标志;如果两者都支持,也可以同时设置。在上述示例中,还设置了 MMAP_VALID 和 BLOCK_TRANSFER,用于支持 OSS 的 mmap 模式。通常,这两个标志会一起设置。当然,只有在硬件真正支持 mmap 的情况下,才应设置 MMAP_VALID。其他可选的标志还包括 SNDRV_PCM_INFO_PAUSE 和 SNDRV_PCM_INFO_RESUME。PAUSE 标志表示 PCM 支持“暂停”操作,而 RESUME 标志表示 PCM 支持完整的“挂起/恢复”操作。如果设置了 PAUSE 标志,那么后续的 trigger 回调函数中必须处理对应的暂停/恢复(pause push/release)命令。即使没有设置 RESUME 标志,也可以定义挂起/恢复的 trigger 命令。详细说明见后文的“电源管理”章节。当 PCM 子流之间可以同步(通常是播放和录音流的同步启动/停止)时,也可以添加 SNDRV_PCM_INFO_SYNC_START 标志。在这种情况下,你需要在 trigger 回调中检查 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 的运行时结构体记录。运行时实例中最常被访问的部分是 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
2578

被折叠的 条评论
为什么被折叠?



