你好!这里是风筝的博客,
欢迎和我一起交流。
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:
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