當app在調用snd_pcm_writei時,alsa core將app傳來的數據搬到HW buffer(即DMA buffer)中,alsa driver從HW buffer中讀取數據傳輸到硬件播放。
ALSA buffer是采用ring buffer來實現的。ring buffer有多個HW buffer組成。
HW buffer一般是在alsa driver的hw_params函數中分配的一塊大小為buffer size的DMA buffer.
之所以采用多個HW buffer來組成ring buffer,是防止讀寫指針的前后位置頻繁的互換(即寫指針到達HW buffer邊界時,就要回到HW buffer起始點)。
ring buffer = n * HW buffer.通常這個n比較大,在數據讀寫的過程中,很少會出現讀寫指針互換的情況。
下圖是ALSA buffer的實現以及讀寫指針更新的方法,
hw_ptr_base是當前HW buffer在Ring buffer中的起始位置。當讀指針到達HW buffer尾部時,hw_ptr_base按buffer size移動.
hw_ptr即HW buffer的讀指針。alsa driver將數據從HW buffer中讀走並送到聲卡硬件時,hw_ptr就會移動到新位置。
appl_ptr即HW buffer的寫指針。app在調用snd_pcm_write寫數據,alsa core將數據copy到HW buffer后,appl_ptr就更新。
boundary即Ring buffer邊界。
hw_ofs是讀指針在當前HW buffer中的位置。由alsa driver的pointer()返回。
appl_ofs是寫指針在當前HW buffer中的位置。
hw_ptr的更新是通過調用snd_pcm_update_hw_ptr0完成。此函數在app寫數據時會調用,也會在硬件中斷時通過snd_pcm_peroid_elapsed調用。
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,在此函數中將更新hw_ptr
/*
* group pointer, time and jiffies reads to allow for more
* accurate correlations/corrections.
* The values are stored at the end of this routine after
* corrections for hw_ptr position
*/
pos = substream->ops->pointer(substream);//獲取hw_ptr在當前HW buffer中的偏移
curr_jiffies = jiffies;
if (runtime->tstamp_mode == SNDRV_PCM_TSTAMP_ENABLE) {//獲取當前的time stamp
if ((substream->ops->get_time_info) &&
(runtime->audio_tstamp_config.type_requested != SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT)) {
substream->ops->get_time_info(substream, &curr_tstamp,&audio_tstamp,&runtime->audio_tstamp_config,&runtime->audio_tstamp_report);
/* re-test in case tstamp type is not supported in hardware and was demoted to DEFAULT */
if (runtime->audio_tstamp_report.actual_type == SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT)
snd_pcm_gettime(runtime, (struct timespec *)&curr_tstamp);
} else
snd_pcm_gettime(runtime, (struct timespec *)&curr_tstamp);
}
if (pos == SNDRV_PCM_POS_XRUN) {//發生XRUN
xrun(substream);
return -EPIPE;
}
if (pos >= runtime->buffer_size) {//pos大於buffer 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;
trace_hwptr(substream, pos, in_interrupt);
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;
if (delta > new_hw_ptr) {//如果本次通過中斷位置加上period_size計算出來的hw_ptr比當前hw_ptr大的話,則說明上一次中斷沒處理,有可能hw_base需要更新到下一個HW buffer的基地址。
/* 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;
if (hw_base >= runtime->boundary) {
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 */
if (new_hw_ptr < old_hw_ptr) {//如果當前的hw_ptr比上一次的hw_ptr小,hw_ptr超過了HW buffer邊界。hw_base需要更新到下一個HW buffer的基地址。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;
}
__delta:
delta = new_hw_ptr - old_hw_ptr;
if (delta < 0)//如果當前的hw_ptr任然比上一的hw_ptr小,說明hw_ptr走完了Ring buffer一圈。
delta += runtime->boundary;
if (runtime->no_period_wakeup) {//如果硬件處理完一個peroid數據后不產生中斷,就不更新hw_ptr
snd_pcm_sframes_t xrun_threshold;
/*
* Without regular period interrupts, we have to check
* the elapsed time to detect xruns.
*/
jdelta = curr_jiffies - runtime->hw_ptr_jiffies;
if (jdelta < runtime->hw_ptr_buffer_jiffies / 2)
goto no_delta_check;
hdelta = jdelta - delta * HZ / runtime->rate;
xrun_threshold = runtime->hw_ptr_buffer_jiffies / 2 + 1;
while (hdelta > xrun_threshold) {
delta += runtime->buffer_size;
hw_base += runtime->buffer_size;
if (hw_base >= runtime->boundary) {
hw_base = 0;
crossed_boundary++;
}
new_hw_ptr = hw_base + pos;
hdelta -= runtime->hw_ptr_buffer_jiffies;
}
goto no_delta_check;
}
/* 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;
}
/* Do jiffies check only in xrun_debug mode */
if (!xrun_debug(substream, XRUN_DEBUG_JIFFIESCHECK))
goto no_jiffies_check;
/* Skip the jiffies check for hardwares with BATCH flag.
* Such hardware usually just increases the position at each IRQ,
* thus it can't give any strange position.
*/
if (runtime->hw.info & SNDRV_PCM_INFO_BATCH)
goto no_jiffies_check;
hdelta = delta;
if (hdelta < runtime->delay)
goto no_jiffies_check;
hdelta -= runtime->delay;
jdelta = curr_jiffies - runtime->hw_ptr_jiffies;
if (((hdelta * HZ) / runtime->rate) > jdelta + HZ/100) {
delta = jdelta /(((runtime->period_size * HZ) / runtime->rate)+ HZ/100);
/* move new_hw_ptr according jiffies not pos variable */
new_hw_ptr = old_hw_ptr;
hw_base = delta;
/* use loop to avoid checks for delta overflows */
/* the delta value is small or zero in most cases */
while (delta > 0) {
new_hw_ptr += runtime->period_size;
if (new_hw_ptr >= runtime->boundary) {
new_hw_ptr -= runtime->boundary;
crossed_boundary--;
}
delta--;
}
/* align hw_base to buffer_size */
hw_ptr_error(substream, in_interrupt, "hw_ptr skipping",
"(pos=%ld, delta=%ld, period=%ld, jdelta=%lu/%lu/%lu, hw_ptr=%ld/%ld)\n",
(long)pos, (long)hdelta,
(long)runtime->period_size, jdelta,
((hdelta * HZ) / runtime->rate), hw_base,
(unsigned long)old_hw_ptr,
(unsigned long)new_hw_ptr);
/* reset values to proper state */
delta = 0;
hw_base = new_hw_ptr - (new_hw_ptr % runtime->buffer_size);
}
no_jiffies_check:
if (delta > runtime->period_size + runtime->period_size / 2) {//interupt丟失,delta(如果當前hw_ptr比較上一次之差)>1.5個peroid size
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沒變化
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);//通過本次更新的 件指針,檢查是否XRUN,或喚醒等待在他上面的隊列如poll,lib_write1
}