你好!这里是风筝的博客,
欢迎和我一起交流。
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)
理想情况下,大小为Count的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他回到起始位置。在实际应用中,我们通常都会把这个大小为Count的缓冲区虚拟成一个大小为n*Count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver也使用了该方法对dma buffer进行管理:
- 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:
1 2 3 4 5 6 7 8 9 | 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跟新。
.
log演示
这里我们通过配置XRUN_DEBUG和TRACE,用trace工具抓取一段hw_ptr更新过程的log:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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的更新过程,可以结合我们的代码一起看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 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说下:
1 2 3 4 5 6 7 | 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