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); 这里我们不会更详细的描述,我仍然想说“直接看源码吧”。 |
|
来自: dwlinux_gs > 《声卡》