ALSA子系統 | ALSA Buffer的更新

佈道師peter 發佈 2022-11-28T07:43:45.795797+00:00

PCM 數據管理可以說是 ALSA 系統中最核心的部分。不管是錄音還是播放,都要用到buffer管理數據。播放:copy_from_user 把用戶態的音頻數據拷貝到 buffer 中,啟動 dma 設備把音頻數據從 buffer 傳送到 I2S tx FIFO。

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起始點)。

理想情況下,大小為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為單位進行數據的傳輸。

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的更新梳理就到此結束了,完結撒花~

關鍵字: