PCM接口
ALSA的PCM中间层非常有用,每个驱动只需要实现底层的功能来访问硬件。要使用PCM层,你需要先引用 <sound/pcm.h>头文件。此外,如果你要使用和hw_param相关的函数,<sound/pcm_params.h>也是必须的。
每个声卡设备最多拥有4个PCM实例。一个PCM实例对应一个PCM设备文件。实例数量的约束来自linux设备序号可用的位大小。当64bit的设备序号开始使用时,我们就可以支持更多的PCM实例。
PCM实例包含PCM回放和录制流。每个PCM流包含一个或更多的PCM子流。一些声卡支持多路回放功能。比如,emu10k1 支持32路立体声播放子流。这种情况下,每次打开动作,一路空闲的子流会被自动选择并打开。如果只有一路子流存在并且已经打开过,那么后续的打开动作将会阻塞或者返回错误码EAGAIN 。但在驱动中不需要关注这些细节。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;
}调用snd_pcm_new()函数可以获得PCM实例。可以为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要绑定的声卡。也就是上篇文章提到并获得的snd_card实例。第二个参数是ID字符串。第三个参数是新PCM的索引。第四个和第五个参数是播放和录制子流的个数。当没有播放和录制子流可用的时候,对应的参数传0.如果芯片支持多路播放或者录制子流,你可以指定更大的数。但在open,close,回调等函数内部要做好处理。当你需要知道在处理哪个子流时,你可以从每个回调函数的参数中获取,参考下面的示例:
struct snd_pcm_substream *substream;
int index = substream->number;当PCM实例创建后,你需要为每个PCM流设置操作结构体。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,
};操作结构体的包含了所有回调函数。
设置完操作结构体后,可以预分配缓冲区(buffer)并设置管理分配模式。执行下面的代码即可。
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,&chip->pci->dev,64*1024, 64*1024);这段代码会默认会分配最多64kB的buffer。另外,pcm->info_flags还可以包含更多的信息。 <sound/asound.h>中定义的SNDRV_PCM_INFO_XXX类型的宏都可以添加到flag中。运行时指针---------PCM的主要信息
当PCM子流被打开,一个PCM运行时就被创建好并分配给子流。通过 substream->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中间层可以修改更新他们。唯一的例外时硬件描述中的DMA buffer信息以及私有数据。此外,如果你使用标准管理buffer分配模式,你不需要设置DMA buffer信息。
硬件描述信息
硬件描述符(结构体snd_pcm_hardware)定义了基础的硬件配置信息。首先,你需要在PCM open的回调函数里面定义它。运行时实例持有的是一份这个描述的拷贝,而不是指针。在open回调函数里面。如果你需要,你可以修改这份拷贝的描述符 (runtime->hw)。比如,在一些芯片模式下,最大声道数支持1,依然可以使用同样的硬件描述符并且在后续的代码中修改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类型的宏定义了info字段的类型。是否支持mmap和支持什么交织类型必须指定。SNDRV_PCM_INFO_MMAP 标志为表示驱动支持MMAP,SNDRV_PCM_INFO_INTERLEAVED 标志位表示支持交织PCM,SNDRV_PCM_INFO_NONINTERLEAVED 表示支持非交织PCM。如果交织和非交织都支持,则可以两者都设置。
上面的实例代码中, OSS mmap mode指定了BLOCK_TRANSFER 和MMAP_VALID 。这两个标志位一般都是同时指定的。只有当驱动真正支持mmap的时候才能设置MMAP_VALID 。
SNDRV_PCM_INFO_PAUSE 和SNDRV_PCM_INFO_RESUME表示PCM支持pause/resume。如果设置了这两个标志位,那么在trigger 要做响应的处理。