ALSA子系统(十二)------ALSA Buffer的更新

你好!这里是风筝的博客,
欢迎和我一起交流。

PCM 数据管理可以说是 ALSA 系统中最核心的部分。
不管是录音还是播放,都要用到buffer管理数据。

  • 播放:copy_from_user 把用户态的音频数据拷贝到 buffer 中,启动 dma 设备把音频数据从 buffer 传送到 I2S tx FIFO。
  • 录音:启动 dma 设备把音频数据从 I2S rx FIFO 传送到 buffer, copy_to_user 把 buffer 中音频数据拷贝到用户态。
    在这里插入图片描述

ALSA buffer是采用ring buffer来实现的。ring buffer有多个HW buffer(虚拟)组成。
之所以采用多个HW buffer来组成ring buffer,是防止读写指针的前后位置频繁的互换(即写指针到达HW buffer边界时,就要回到HW buffer起始点)。
这里采用droidphone博客里的一段话作为描述:
(本来想去alsa官网找下解释的,结果里面就只有这个:https://www.alsa-project.org/wiki/PCM_Ring_Buffer)
ALSA子系统(十二)------ALSA Buffer的更新_第1张图片
理想情况下,大小为Count的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他回到起始位置。在实际应用中,我们通常都会把这个大小为Count的缓冲区虚拟成一个大小为n*Count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver也使用了该方法对dma buffer进行管理:
ALSA子系统(十二)------ALSA Buffer的更新_第2张图片

  • hw_ptr_base:当前HW buffer在Ring buffer中的起始位置。当读指针到达HW buffer尾部时,hw_ptr_base按buffer size移动.
  • hw_ptr:硬件逻辑位置,播放时相当于读指针,录音时相当于写指针。
  • appl_ptr:应用逻辑位置,播放时相当于写指针,录音时相当于读指针。
  • boundary:扩展后的逻辑缓冲区大小,通常是(2^n)*size。
  • buffer_size:HW buffer的大小,大小为period_size * period_count 。
  • avail:HW buffer中空闲的地址,我们可以稳定的通过一个公式获取avail:
static inline snd_pcm_uframes_t snd_pcm_playback_avail(struct snd_pcm_runtime *runtime)
{
	snd_pcm_sframes_t avail = runtime->status->hw_ptr + runtime->buffer_size - runtime->control->appl_ptr;
	if (avail < 0)
		avail += runtime->boundary;
	else if ((snd_pcm_uframes_t) avail >= runtime->boundary)
		avail -= runtime->boundary;
	return avail;
}

HW buffer的size可以通过ALSA library的API进行修改,即修改period_size 和 period_count。
如果buffer设得太大,那么一次数据的传输需要的延迟会增加,为了解决这个问题,ALSA将buffer分为一系列的period(在OSS/Free语境中称为fragment),然后以period为单位进行数据的传输。
关于period的介绍在alsa官网有,我之前也有翻译过:Frames Periods

HW buffer的硬件逻辑指针(hw_ptr)主要由 snd_pcm_update_hw_ptr0函数跟新。

  • DMA传输完成一个period_size之后通过在中断里snd_pcm_period_elapsed调用snd_pcm_update_hw_ptr0跟新。
  • 数据读/写/重置(snd_pcm_lib_read1/snd_pcm_lib_write1/snd_pcm_lib_ioctl_reset)时通过snd_pcm_update_hw_ptr调用snd_pcm_update_hw_ptr0跟新。
  • snd_pcm_playback_forward/snd_pcm_capture_forward通过调用snd_pcm_update_hw_ptr跟新。
  • snd_pcm_do_pause暂停时通过调用snd_pcm_update_hw_ptr跟新。

HW buffer的应用逻辑指针(appl_ptr)更新有两种:

  • 用户空间调用write函数往缓冲区中写入数据时, 在内核层snd_pcm_write -> snd_pcm_lib_write -> snd_pcm_lib_write1函数会计算appl_ptr的新位置, 并更新该参数。
  • 用户空间通过mmap的方式往缓冲区中写入数据时, 在mmap方式下, 内核并不知道用户空间何时完成写入了, 因此用户空间完成写入时需要通过某种方式告知内核. alsa提供了ioctl SNDRV_PCM_IOCTL_SYNC_PTR, 供用户空间通知内核更新appl_ptr, 例如tinyalsa中的pcm_sync_ptr采用的就是这种方式. 在内核层, snd_pcm_common_ioctl1 -> snd_pcm_sync_ptr 会最终更新该参数。
    .

log演示

这里我们通过配置XRUN_DEBUG和TRACE,用trace工具抓取一段hw_ptr更新过程的log:

        tinyplay-2528  [000] d..2   587.028041: hwptr: pcmC0D0p/sub0: POS: pos=32, old=0, base=0, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.048548: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=32, base=0, period=1024, buf=4096
           Sadbd-2531  [000] d.h4   587.069895: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=1024, base=0, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.091223: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=2048, base=0, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.112541: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=3072, base=0, period=1024, buf=4096
        tinyplay-2528  [000] d..2   587.112764: hwptr: pcmC0D0p/sub0: POS: pos=0, old=4096, base=4096, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.133875: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=4096, base=4096, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.155209: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=5120, base=4096, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.176541: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=6144, base=4096, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.197872: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=7168, base=4096, period=1024, buf=4096
        tinyplay-2528  [000] d..2   587.198069: hwptr: pcmC0D0p/sub0: POS: pos=0, old=8192, base=8192, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.219212: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=8192, base=8192, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.240541: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=9216, base=8192, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.261876: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=10240, base=8192, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.283201: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=11264, base=8192, period=1024, buf=4096
  • hwptr: pcmC0D0p/sub0: POS:代表用户层读写数据等操作时更新hw_ptr的log。
  • hwptr: pcmC0D0p/sub0: IRQ:代表DMA传输中断时更新hw_ptr的log。

这段log里面实时记录了pos、old_hw_ptr、hw_ptr_base、period_size、buf_size的更新过程,可以结合我们的代码一起看:

static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
                                  unsigned int in_interrupt)
{
		struct snd_pcm_runtime *runtime = substream->runtime;
        snd_pcm_uframes_t pos;
        snd_pcm_uframes_t old_hw_ptr, new_hw_ptr, hw_base;
        snd_pcm_sframes_t hdelta, delta;
        unsigned long jdelta;
        unsigned long curr_jiffies;
        struct timespec curr_tstamp;
        struct timespec audio_tstamp;
        int crossed_boundary = 0;

        old_hw_ptr = runtime->status->hw_ptr;//保存上一次的hw_ptr

		pos = substream->ops->pointer(substream);//DMA以及搬运了的数据量,正常情况下pos每次递增period_size,最大为buf_size,但是递增到buf_size时pos会清零,因为pos=buf_size-DMA搬运数据量。
        curr_jiffies = jiffies;
        //......
        if (pos == SNDRV_PCM_POS_XRUN) {//发生XRUN
                xrun(substream);
                return -EPIPE;
        }
        if (pos >= runtime->buffer_size) {//按pos计算的描述,理论上pos不会>=buf_size,否则出现异常
                if (printk_ratelimit()) {
                        char name[16];
                        snd_pcm_debug_name(substream, name, sizeof(name));
                        pcm_err(substream->pcm,
                                "invalid position: %s, pos = %ld, buffer size = %ld, period size = %ld\n",
                                name, pos, runtime->buffer_size,
                                runtime->period_size);
                }
                pos = 0;
        }
        pos -= pos % runtime->min_align;//pos地址对齐
        trace_hwptr(substream, pos, in_interrupt);//通过trace打印调试
        hw_base = runtime->hw_ptr_base;//当前的hw_base
        new_hw_ptr = hw_base + pos;//当前的hw_ptr
        if (in_interrupt) {//如果是在中断中调用此函数
                /* we know that one period was processed */
                /* delta = "expected next hw_ptr" for in_interrupt != 0 */
                delta = runtime->hw_ptr_interrupt + runtime->period_size;//期望下一个hw_ptr的值
                if (delta > new_hw_ptr) {//如果期望的hw_ptr比当前计算出来的hw_ptr大的话,则说明上一次中断没处理
                        /* check for double acknowledged interrupts */
                        hdelta = curr_jiffies - runtime->hw_ptr_jiffies;
                        if (hdelta > runtime->hw_ptr_buffer_jiffies/2 + 1) {//距离上一次的jiffies大于整个buffer 的jiffies的一半
                                hw_base += runtime->buffer_size;//hw_base需要更新到下一个HW buffer的基地址
                                if (hw_base >= runtime->boundary) {//超过Ring Buffer总和
                                        hw_base = 0;
                                        crossed_boundary++;
                                }
                                new_hw_ptr = hw_base + pos;
                                goto __delta;
                        }
                }
        }
        /* new_hw_ptr might be lower than old_hw_ptr in case when */
        /* pointer crosses the end of the ring buffer */
        //传输完成一个buf_size的话,pos此时为0,hw_ptr超过了HW buffer边界,此条件则成立。hw_base需要更新到下一个HW buffer的基地址。
        if (new_hw_ptr < old_hw_ptr) {
                hw_base += runtime->buffer_size;
                if (hw_base >= runtime->boundary) {//如果hw_base > boundary,那hw_base回跳到Ring Buffer起始位置
                        hw_base = 0;
                        crossed_boundary++;
                }
                new_hw_ptr = hw_base + pos;//重新更新正确的new_hw_ptr
        }
__delta:
        delta = new_hw_ptr - old_hw_ptr;//hw_ptr相较上一次的偏移值,理论上为period_size
        if (delta < 0)//如果当前计算出来的hw_ptr任然比上一的hw_ptr小,说明hw_ptr走完了Ring buffer一圈
                delta += runtime->boundary;
        //......
        /* something must be really wrong */
        if (delta >= runtime->buffer_size + runtime->period_size) {//如果当前hw_ptr比较上一次相差buffer size + peroid size,说明有错误
                hw_ptr_error(substream, in_interrupt, "Unexpected hw_ptr",
                             "(stream=%i, pos=%ld, new_hw_ptr=%ld, old_hw_ptr=%ld)\n",
                             substream->stream, (long)pos,
                             (long)new_hw_ptr, (long)old_hw_ptr);
                return 0;
        }
		//......
no_jiffies_check:
		//delta(如果当前hw_ptr比较上一次之差)>1.5个peroid size,可能是interupt丢失?理论上delta == period_size
        if (delta > runtime->period_size + runtime->period_size / 2) {
                hw_ptr_error(substream, in_interrupt,
                             "Lost interrupts?",
                             "(stream=%i, delta=%ld, new_hw_ptr=%ld, old_hw_ptr=%ld)\n",
                             substream->stream, (long)delta,
                             (long)new_hw_ptr,
                             (long)old_hw_ptr);
        }

 no_delta_check:
        if (runtime->status->hw_ptr == new_hw_ptr) {//hw_ptr没变化,直接返回,等待下一次更新pos
                update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
                return 0;
        }
        
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
            runtime->silence_size > 0)
                snd_pcm_playback_silence(substream, new_hw_ptr);//播放silence静音
                
		if (in_interrupt) {//更新hw_ptr_interrupt
                delta = new_hw_ptr - runtime->hw_ptr_interrupt;
                if (delta < 0)
                        delta += runtime->boundary;
                delta -= (snd_pcm_uframes_t)delta % runtime->period_size;
                runtime->hw_ptr_interrupt += delta;
                if (runtime->hw_ptr_interrupt >= runtime->boundary)
                        runtime->hw_ptr_interrupt -= runtime->boundary;
        }
        runtime->hw_ptr_base = hw_base;//将更新后的所有值保存到runtime中
        runtime->status->hw_ptr = new_hw_ptr;
        runtime->hw_ptr_jiffies = curr_jiffies;
        if (crossed_boundary) {
        	snd_BUG_ON(crossed_boundary != 1);
        	runtime->hw_ptr_wrap += runtime->boundary;update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
		
		return snd_pcm_update_state(substream, runtime);

主要流程参考注释,这里简单对着之前的log说下:

        tinyplay-2528  [000] d..2   587.028041: hwptr: pcmC0D0p/sub0: POS: pos=32, old=0, base=0, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.048548: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=32, base=0, period=1024, buf=4096
           Sadbd-2531  [000] d.h4   587.069895: hwptr: pcmC0D0p/sub0: IRQ: pos=2048, old=1024, base=0, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.091223: hwptr: pcmC0D0p/sub0: IRQ: pos=3072, old=2048, base=0, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.112541: hwptr: pcmC0D0p/sub0: IRQ: pos=0, old=3072, base=0, period=1024, buf=4096
        tinyplay-2528  [000] d..2   587.112764: hwptr: pcmC0D0p/sub0: POS: pos=0, old=4096, base=4096, period=1024, buf=4096
          <idle>-0     [000] d.h3   587.133875: hwptr: pcmC0D0p/sub0: IRQ: pos=1024, old=4096, base=4096, period=1024, buf=4096

一开始log是hwptr: pcmC0D0p/sub0: POS,表明是write里面调用snd_pcm_update_hw_ptr跟新hw_ptr,
此时write里面发送了32frames,pos也就是32,上一次hw_ptr是0,HW buffer基地址base是0,推知当前hw_ptr是32,period_size是1024,period_count是4,buf_size是4096。

接下来就是DMA中断产生,在中断里调用snd_pcm_update_hw_ptr0函数跟新hw_ptr:

第一次中断,传输了period_size,所以pos是1024,old是上一次的hw_ptr,也就是32,HW buffer基地址base还是是0。
第二次中断,再次传输了period_size,所以pos是2048,old是上一次的hw_ptr,也就是1024,HW buffer基地址base还是是0。
第三次中断,再次传输了period_size,所以pos是3072,old是上一次的hw_ptr,也就是2048,HW buffer基地址base还是是0。
第四次中断,再次传输了period_size,此时dma数据传完了(因为buf是4096,一次传1024,一共传4次)所以pos是0,old是上一次的hw_ptr,也就是3072,HW buffer基地址base还是是0。

接下来的log就不是由DMA中断里跟新hw_ptr了,因为hwptr: pcmC0D0p/sub0: POS
所以这条log里面,pos还是0,没更新,但是old是上一次的hw_ptr,是4096,HW buffer基地址base就变成4096了!!!
然后往复循环,周而复始~~

至此,alsa dma buffer里hw_ptr的更新梳理就到此结束了,完结撒花~


trace文件在这:sound/core/pcm_trace.h

参考:
Linux ALSA声卡驱动之八:ASoC架构中的Platform
ALSA driver–HW Buffer
ALSA & ASOC

你可能感兴趣的:(音频子系统,alsa,asoc,alsa,buffer)