Believe youself!
分类: LINUX
2015-06-14 23:52:58
PCM信息运行时指针
当打开一个一个PCM子流的时候,PCM运行时实例就会分配给这个子流。这个指针可以通过substream->runtime获得。运行时指针拥有多种信息:hw_params和sw_params的配置的拷贝,缓冲区指针,mmap记录,自旋锁等等。几乎你想控制PCM的所有信息都可以在这里得到。
Struct _snd_pcm_runtime {
/*状态*/
struct snd_pcm_substream *trigger_master;
snd_timestamp_t trigger_tstamp;/*触发时间戳*/、
int overrange;
snd_pcm_uframes_t avail_max;
snd_pcm_uframes_t hw_ptr_base /*缓冲区复位时的位置*/
snd_pcm_uframes_t hw_ptr_interrupt;/*中断时的位置*/
/*硬件参数*/
snd_pcm_access_t access; /*存取模式*/
snd_pcm_format_t format; /*SNDRV_PCM_FORMAT_* */
snd_pcm_subformat_t subformat; /*子格式*/
unsigned int rate; /*rate in HZ*/
unsigned int channels; /*通道*/
snd_pcm_uframe_t period_size; /*周期大小*/
unsigned int periods /*周期数*/
snd_pcm_uframes_t buffer_size; /*缓冲区大小*/
unsigned int tick_time; /*tick time滴答时间*/
snd_pcm_uframes_t min_align; /*格式对应的最小对齐*/
size_t byte_align;
unsigned int frame_bits;
unsigned int sample_bits;
unsigned int info;
unsigned int rate_num;
unsigned int rate_den;
/*软件参数*/
struct timespec tstamp_mode; /*mmap时间戳被更新*/
unsigned int sleep_min; /*睡眠的最小节拍数*/
snd_pcm_uframes_t xfer_align; /*xfer的大小需要是成倍数的*/
snd_pcm_uframes_t start_threshold;
snd_pcm_uframes_t stop_threshold;
snd_pcm_uframes_t silence_threshold;/*silence填充阀值*/
snd_pcm_uframes_t silence_size; /*silence填充大小*/
snd_pcm_uframes_t boundary;
snd_pcm_uframes_t silenced_start;
snd_pcm_uframes_t silenced_size;
snd_pcm_sync_id_t sync; /*硬件同步ID*/
/*mmap*/
volatile struct snd_pcm_mmap_status *status;
volatile struct snd_pcm_mmap_control *control;
atomic_t mmap_count;
/*锁/调度*/
spinlock_t lock;
wait_queue_head_t sleep;
struct timer_list tick_timer;
struct fasync_struct *fasync;
/*私有段*/
void *private_data;
void (*private_free)(struct snd_pcm_runtime *runtime);
/*硬件描述*/
struct snd_pcm_hardware hw;
struct snd_pcm_hw_constraints hw_constraints;
/*中断的回调函数*/
void (*transfer_ack_begin)(struct snd_pcm_substream *substream);
void (*transfer_ack_end)(struct snd_pcm_substream *substream);
/*定时器*/
unsigned int timer_resolution; /*timer resolution*/
/*DMA*/
unsigned char *dma_area;
dma_addr_t dma_addr; /*总线物理地址*/
size_t dma_bytes; /*DMA区域大小*/
struct snd_dma_buffer *dma_buffer_p; /*分配的缓冲区*/
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
struct snd_pcm_oss_runtime oss;
#endif
};
snd_pcm_runtime 对于大部分的驱动程序操作集的函数来说是只读的。仅仅PCM中间层可以改变/更新这些信息。但是硬件描述,中断响应,DMA缓冲区信息和私有数据是例外的。此外,假如你采用标准的内存分配函数snd_pcm_lib_malloc_pages(),就不再需要自己设定DMA缓冲区信息了。
下面几章,会对上面记录的现实进行解释。
硬件描述
硬件描述(struct snd_pcm_hardware)包含了基本硬件配置的定义。如前面所述,你需要在open的时候对它们进行定义。注意runtime实例拥有这个描述符的拷贝而不是已经存在的描述符的指针。换句话说,在open函数中,你可以根据需要修改描述符的拷贝。例如,假如在一些声卡上最大的通道数是1,你仍然可以使用相同的硬件描述符,同时在后面你可以改变最大通道数。
Struct snd_pcm_runtime *runtime = substream->runtime;
....
runtime->hw = snd_mychip_playback_hw; /*通用定义*/
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_FORMAT_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的类型和能力。位标志在
如上面的例子,MMAP_VALID和BLOCK_TRANSFER都是针对OSS mmap模式,通常情况它们都要设定。当然,MMAP_VALID仅仅当mmap真正被支持的时候才会被设定。
其他一些标志位是SNDRV_PCM_INFO_PAUSE和SNDRV_PCM_INFO_RESUME。SNDRV_PCM_INFO_PAUSE标志位意思是pcm支持“暂停”操作,SNDRV_PCM_INFO_RESUME表示是pcm支持“挂起/恢复”操作。假如PAUSE标志位被设定,trigger函数就必须执行一个对应的(暂停 按下/释放)命令。就算没有RESUME标志位,也可以被定义挂起/恢复触发命令。
更详细的部分请参考“电源管理”一章。
当PCM子系统能被同步(如:播放流和录音流的开始/结束的同步)的时候,你可以设定SNDRV_PCM_INFO_SYNC_START标志位。在这种情况下,你必须在trigger函数中检查PCM子流链。下面的章节会想笑介绍这个部分。
formats字段包含了支持格式的标志位(SNDRV_PCM_FMTBIT_XXX)。假如硬件支持超过一个的格式,需要对位标志位进行“或”运算。上面的例子就是支持16bit有符号的小端格式。
rates字段包含了支持的采样率(SNDRV_PCM_RATE_XXX)。当声卡支持多种采样率的时候,应该附加一个CONTINUOUS标志。已经预先定义的典型的采样率,假如你的声卡支持不常用的采样率,你需要加入一个KNOT标志,同时手动的对硬件进行控制(稍后解释)。
rate_min和rate_max定义了最小和最大的采样率。应该和采样率相对应。
channel_min和channel_max定义了最大和最小的通道,以前可能你已看到。
buffer_bytes_max定义了以字节为单位的最大的缓冲区大小。这里没有buffer_bytes_min字段,因为它可以通过最小的period大小和最小的period数量计算得出。同时,period_bytes_min和定义的最小和最大的period。periods_max和periods_min定义了最大和最小的periods。
period信息和OSS中的fragment相对应。period定义了PCM中断产生的周期。这个周期非常依赖硬件。一般来说,一个更短的周期会提供更多的中断和更多的控制。如在录音中,周期大小定义了输入延迟,另外,整个缓存区大小也定义了播放的输出延迟。
字段fifo_size。这个主要是和硬件的FIFO大小有关,但是目前驱动中或alsa-lib中都没有使用。所以你可以忽略这个字段。
PCM配置
OK,让我们再次回到PCM运行时记录。最经常涉及的运行时实例中的记录就是PCM配置了。PCM可以让应用程序通过alsa-lib发送hw_params来配置。有很多字段都是从hw_params和sw_params结构中拷贝过来的。例如:format保持了应用程序选择的格式类型,这个字段包含了enum值SNDRV_PCM_FORMAT_XXX。
其中要注意的一个就是,配置的buffer和period大小被放在运行时记录的“frame”中。在ALSA里,1frame=channel*samples-size。为了在帧和字节之间转换,你可以用下面的函数,frames_to_bytes()和bytes_to_frames()。
period_bytes = frames_to_bytes(runtime,runtime->period_size);
同样,许多的软件参数(sw_params)也存放在frames字段里面。请检查这个字段的类型。snd_pcm_uframes_t是作为表示frames的无符号整数,而snd_pcm_sframes_t是作为表示frames的有符号整数。
DMA缓冲区信息
DMA缓冲区通过下面4个字段定义,dma_area,dma_addr,dma_bytes,dma_private。其中dma_area是缓冲区的指针(逻辑地址)。可以通过memcopy来向这个指针来操作数据。dma_addr是缓冲区的物理地址。这个字段仅仅当缓冲区是线性缓存的时候才要特别说明。dma_bytes是缓冲区的 大小。dma_private是被ALSA的DMA管理用到的。
如果采用ALSA的标准内存分配函数snd_pcm_lib_mallock_pages()分配内存,那些字段会被ALSA的中间层设定,你不能自己改变他们,可以读取而不能写入。而如果你想自己分配内存,你就需要在hw_params回调里面自己管理它们。当内存被mmap之后,你至少要设定dma_bytes和dma_area。但是如果你的驱动不支持mmap,这些字段就不必一定设定.dma_addr也不是强制的,你也可以根据灵活来用dma_private。
运行状态
可以通过runtime->status来获得运行状态。它是一个指向snd_pcm_mmap_status记录的指针。例如,可以通过runtime->status->hw_ptr来得到当前DMA硬件指针。
可以通过runtime->control来查看DMA程序的指针,它是指向snd_pcm_mmap_control记录。但是,不推荐直接存取这些数据。
私有数据
可以为子流分配一个记录,让它保存在runtime->private_data里面。通常可以在open函数中做。不要和pcm->private_data混搅了,pcm->private_data主要是在创建PCM的时候指向chip实例,而runtime->private_data是在PCM open的时候指向一个动态数据。
Struct int snd_xxx_open(struct snd_pcm_substream *substream)
{
struct my_pcm_data *data;
data = kmalloc(sizeof(*data),GFP_KERNEL);
substream->runtime->private_data = data;
....
}
上述分配的对象要在close函数中释放。
中断函数
transfer_ack_begin()和transfer_ack_end()将会在snd_pcm_period_elapsed()的开始和结束。
操作函数
现在让我来详细介绍每个pcm的操作函数吧(ops)。通常每个回调函数成功的话返回0,出错的话返回一个带错误码的负值,如:-EINVAL。
每个函数至少要有一个snd_pcm_substream指针变量。主要是为了从给定的子流实例中得到chip记录,你可以采用下面的宏。
Int xxx(){
struct mychip *chip = snd_pcm_substream_chip(substream);
....
}
open函数
static int snd_xxx_open(struct snd_pcm_substream *substream);
当打开一个pcm子流的时候调用。
在这里,你至少要初始化runtime->hw记录。典型应用如下:
static int snd_xxx_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;
return 0;
}
其中snd_mychip_playback_hw是预先定义的硬件描述。
close函数
static int snd_xxx_close(struct snd_pcm_substream *substream)
显然这是在pcm子流被关闭的时候调用。
所有在open的时候被分配的pcm子流的私有的实例都应该在这里被释放。
Static int snd_xxx_close(struct snd_pcm_substream *substream)
{
...
kfree(substream->runtime->private_data);
...
}
ioctl函数
这个函数主要是完成一些pcm ioctl的特殊功能。但是通常你可以采用通用的ioctl函数snd_pcm_lib_ioctl。
hw_params函数
static int snd_xxx_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_substream *hw_params);
这个函数和hw_free函数仅仅在ALSA0.9.X版本出现。
当pcm子流中已经定义了缓冲区大小,period大小,格式等的时候,应用程序可以通过这个函数来设定硬件参数。
很多的硬件设定都要在这里完成,包括分配内存。
设定的参数可以通过params_xxx()宏得到。对于分配内存,可以采用下面一个有用的函数,
snd_pcm_lib_malloc_pages(substream,params_buffer_bytes(hw_params));
snd_pcm_lib_malloc_pages()仅仅当DMA缓冲区已经被预分配之后才可以用。参考“缓存区类型”一章获得更详细的细节。
注意这个和prepare函数会在初始化当中多次被调用。例如,OSSemulation可能在每次通过ioctl改变的时候都要调用这些函数。
因而,千万不要对一个相同的内存分配多次,可能会导致内存的漏洞!而上面的几个函数是可以被多次调用的,如果它已经被分配了,它会先自动释放之前的内存。
另外一个需要注意的是,这些函数都不是原子的(可以被调到)。这个是非常重要的,因为trigger函数是原子的(不可被调度)。因此,mutex和其他一些和调度相关的功能函数在trigger里面都不需要了。具体参看“原子操作”一节。
hw_free函数
static int snd_xxx_hw_free(struct snd_pcm_substream *substream);
这个函数可以是否通过hw_params分配的资源。例如:通过如下函数释放那些通过snd_pcm_lib_malloc_pages()申请的缓存。
snd_pcm_lib_free_pages(substream)
这个函数要在close调用之前被调用。同时,它也可以被多次调用。它也会知道资源是否已经被分配。
Prepare函数
static int snd_xxx_prepare(struct snd_pcm_substream *substream);
当pcm“准备好了”的时候调用这个函数。可以在这里设定格式类型,采样率等等。和hw_params不同的是,每次调用snd_pcm_prepare()的时候都要调用prepare函数。
注意最近的版本prepare变成了非原子操作的了。这个函数中,你要做一些调度安全性策略。
在下面的函数中,你会涉及到runtime记录的值(substream->runtime)。例如:得到当前的采样率,格式或声道,可以分别存取runtime->rate,runtime->format,runtime->channels。分配的内存的地址放到runtime->dma_area中,内存和period大小分别保存在runtime->buffer_size和runtime->period_size中。
要注意在每次设定的时候都有可能多次调用这些函数。
trigger函数
static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd);
当pcm开始,停止或暂停的时候都会调用这个函数。
具体执行那个操作主要是根据第二个参数,在
switch(cmd){
case SNDRV_PCM_TRIGGER_START:
//启动PCM引擎
break;
case SNDRV_PCM_TRIGGER_STOP:
//停止PCM引擎
break;
default:
break;
}
当pcm支持暂停操作(在hareware表里面有这个),也必须处理PAUSE_PAUSE和PAUSE_RELEASE命令。前者是暂停命令,后者是重新播放命令。
假如pcm支持挂起/恢复操作,不管是全部或部分的挂起/恢复支持,都要处理SUSPEND和RESUME命令。这些命令主要是在电源状态改变的时候需要,通常它们和STOP,START命令放到一起。具体参看“电源管理”一章。
如前面提到的,这个操作上原子的。不要在调用这些函数的时候进入睡眠。而trigger函数要尽量小,甚至仅仅触发DMA。另外的工作如初始化hw_params和prepare应该在之前做好。
pointer函数
static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream);
PCM中间层通过调用这个函数来获得缓冲区中的硬件位置。返回值需要以为frames为单位(在ALSA0.5.X是以字节为单位的),范围从0到buffer_size-1。
一般情况下,在中断程序中,调用snd_pcm_period_elapsed()的时候,在pcm中间层在在更新buffer的程序中调用它。然后,pcm中间层会更新指针位置和计算可用的空间,然后唤醒那些等待的线程。
这个函数也是原子的。
Copy和silence函数
这些函数不是必须的,同时在大部分的情况下是被忽略的。这些函数主要应用在硬件内存不在正常的内存空间的时候。一些声卡有一些没有被影射的自己的内存。在这种情况下,你必须把内存传到硬件的内存空间去。或者,缓冲区在物理内存和虚拟内存中都是不连续的时候就需要它们了。
假如定义了copy和silence,就可以做copy和set-silence的操作了。更详细的描述请参考“缓冲区和内存管理”一章。
Ack函数
这个函数也不是必须的。当在读写操作的时候更新appl_ptr的时候会调用它。一些类似于emu10k1-fx和cs46xx的驱动程序会为了内部缓存来跟踪当前的appl_ptr,这个函数仅仅对于这个情况才会被用到。
这个函数也是原子的。
page函数
这个函数也不是必须的。这个函数主要对那些不连续的缓存区 。mmap会调用这个函数得到内存页的地址。后续章节“缓冲区和内存管理”会有一些例子介绍。
中断处理
下面的pcm工作就是PCM中断处理了。声卡驱动中的PCM中断处理的作用主要是更新缓存的位置,然后在缓冲位置超过预先定义的period大小的时候通知PCM中间层。可以通过调用snd_pcm_period_elapsed()来通知。
声卡有如下几种产生中断。
period(周期)中断
这是一个很常见的类型:硬件会产生周期中断。每次中断都会调用snd_pcm_period_elapsed()。
snd_pcm_period_elapsed()的参数是substream的指针。因为,需要从chip实例中得到substream的指针。例如:在chip记录中定义一个substream字段来保持当前运行的substream指针,在open函数中要设定这个字段而在close函数中要复位这个字段。
假如在中断处理函数中获得了一个自旋锁,如果其他pcm也会调用这个锁,那你必须要在调用snd_pcm_period_elapsed()之前释放这个锁。
典型代码如下:
Example5-3.中断函数处理#1
struct irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
spin_lock(&chip->lock);
....
if (pcm_irq_invoked(chip)){
spin_unlock(&chip->lock);
snd_pcm_period_elapsed(chip->substream);
spin_lock(&chip->lock);
//如果需要的话,可以响应中断
}
....
spin_unlock(&chip->lock);
return IRQ_HANDLED;
}
高频率时钟中断
当硬件不再产生一个period(周期)中断的时候,就需要一个固定周期的timer中断了(例如 es1968,ymfpci驱动)。这时候,在每次中断都要检查当前硬件位置,同时计算已经累积的采样的长度。当长度超过period长度时候,需要调用 snd_pcm_period_elapsed()同时复位计数值。
典型代码如下:
Example5-4.中断函数处理#2
static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
spin_lock(&chip->lock);
....
if (pcm_irq_invoked(chip)){
unsigned int last_ptr, size;
/*得到当前的硬件指针(帧为单位)*/
last_ptr = get_hw_ptr(chip);
/*计算自从上次更新之后又处理的帧*/
if (last_ptr < chip->last_ptr)
{
size = runtime->buffer_size + last_ptr - chip->last_ptr
}else
{
size = last_ptr – chip->chip->last_ptr;
}
//保持上次更新的位置
chip->last_ptr = last_ptr;
/*累加size计数器*/
chip->size += size;
/*超过period的边界?*/
if (chip->size >= runtime->period_size){
/*重置size计数器*/
chip->size %= runtime->period_size;
spin_unlock(&chip->lock);
snd_pcm_period_elapsed(substream);
spin_lock(&chip->lock);
}
//需要的话,要相应中断
}
....
spin_unlock(&chip->lock);
return IRQ_HANDLED;
}
在调用snd_pcm_period_elapsed()的时候
就算超过一个period的时间已经过去,你也不需要多次调用snd_pcm_period_elapsed(),因为pcm层会自己检查当前的硬件指针和上次和更新的状态。
原子操作
在内核编程的时候,一个非常重要(又很难dubug)的问题就是竞争条件。Linux内核中,一般是通过自旋锁和信号量来解决的。通常来说,假如竞争发生在中断函数中,中断函数要具有原子性,你必须采用自旋锁来包含临界资源。假如不是发生在中断部分,同时比较耗时,可以采用信号量。
如我们看到的,pcm的操作函数一些是原子的而一些不是。例如:hw_params函数不是原子的,而trigger函数是原子的。这意味着,后者调用的时候,PCM中间层已经拥有了锁。
在这些函数中申请的自旋锁和信号量要做个计算。
在这些原子的函数中,不能那些可能调用任务切换和进入睡眠的函数。其中信号量和互斥体可能会进入睡眠,因此,在原子操作的函数中(如:trigger函数)不能调用它们。如果在这种函数中调用delay,可以用udelay(),或mdelay()。
约束
假如你的声卡支持不常用的采样率,或仅仅支持固定的采样率,就需要设定一个约束条件。
例如:为了把采样率限制在一些支持的几种之中,就需要用到函数snd_pcm_hw_constraint_list()。需要在open函数中调用它。
Example5-5.硬件约束示例
static unsigned int rates[] =
{4000,10000,22050,44100};
static unsigned snd_pcm_hw_constraint_list constraints_rates = {
.count = ARRAY_SIZE(rates),
.list = rates,
.mask = 0,
};
static int snd_mychip_pcm_open(struct snd_pcm_substream *substream)
{
int err;
....
err = snd_pcm_hw_constraint_list(substream->runtime,0,
SNDRV_PCM_HW_PARAM_RATE,
&constraints_rates);
if (err < 0)
return err;
....
}
有多种不同的约束。请参考sound/pcm.h中的完整的列表。甚至可以定义自己的约束条件。例如,假如my_chip可以管理一个单通道的子流,格式是S16_LE,另外,它还支持snd_pcm_hareware中设定的格式(或其他constraint_list)。可以设定一个:
Example5-6.为通道设定一个硬件规则
static int hw_rule_format_by_channels(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c = hw_params_interval(params,
SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_mask *f = hw_param_mask(params,SNDRV_PCM_HW_PARAM_FORMAT);
struct snd_mask fmt;
snd_mask_any(&fmt); /*初始化结构体*/
if (c->min < 2){
fmt.bits[0] &= SNDRV_PCM_FMTBIT_S16_LE;
return snd_mask_refine(f, &fmt);
}
return 0;
}
之后,需要把上述函数加入到你的规则当中去:
snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,
hw_rule_channels_by_format,0, SNDRV_PCM_HW_PARAM_FORMAT,
-1);
当应用程序设定声道数量的时候会调用上面的规则函数。但是应用程序可以在设定声道数之前设定格式。所以也需要设定对应的规则。
Example5-7.为通道设定一个硬件规则
static int hw_rule_format_by_format(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c = hw_param_interval(params,
SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);
struct snd_interval ch;
snd_interval_any(&ch);
if (f->bits[0] == SNDRV_PCM_FORMAT_S16_LE){
ch.min = ch.max = 1;
ch.integer = 1;
return snd_interval_refine(c, &ch);
}
return 0;
}
在open函数中:
snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,
hw_rule_channels_by_format,0, SNDRV_PCM_HW_PARAM_CHANNELS,
-1);
这里我们不会更详细的描述,我仍然想说“直接看源码吧”。