1.dma buffer簡介
播放時,應用程序把音頻數據源源不斷地寫入dma buffer中,然后相應platform的dma操作則不停地從該buffer中取出數據,經dai送往codec中。錄音時則正好相反,codec源源不斷地把A/D轉換好的音頻數據經過dai送入dma buffer中,而應用程序則不斷地從該buffer中讀走音頻數據。
圖6.2.1 環形緩沖區
ALSA buffer是采用ring buffer來實現的。ring buffer有多個HW buffer組成,也是上層的說的一個period。
HW buffer一般是在alsa driver的hw_params函數中分配的一塊大小為buffer size的DMA buffer,這個buffer size一般由應用程序指定.之所以采用多個HW buffer來組成ring buffer,是防止讀寫指針的前后位置頻繁的互換(即寫指針到達HW buffer邊界時,就要回到HW buffer起始點)。ring buffer = n * HW buffer.通常這個n比較大,也是由應用指定,在數據讀寫的過程中,很少會出現讀寫指針互換的情況。
2.dma buffer管理
alsa driver也使用了該方法對dma buffer進行管理:

Buffer size:就是一個HW buffer,也是一個period,DMA每次搬運的基本單位(有時候可能搬運比這個size小),也是一個frames
snd_pcm_runtime結構中,使用了四個相關的字段來完成這個邏輯緩沖區的管理:
- snd_pcm_runtime.hw_ptr_base: 環形緩沖區每一圈的基地址,當讀寫指針越過一圈后,它按buffer size進行移動;
- snd_pcm_runtime.status->hw_ptr: 硬件邏輯位置,播放時相當於讀指針,錄音時相當於寫指針;
- snd_pcm_runtime.control->appl_ptr: 應用邏輯位置,播放時相當於寫指針,錄音時相當於讀指針;
- snd_pcm_runtime.boundary: 擴展后的邏輯緩沖區大小,通常是(2^n)*size, runtime->boundary一般都是較大的數,ALSA中默認接近
LONG_MAX/2.這樣FIFO的出隊入隊指針不是真實的緩沖區的地址偏移,經過轉換才得到 物理緩沖的偏移。這樣做的好處是簡化了緩沖區的管理,只有在更新hw指針的時候才需 要換算到hw_ofs.;
通過這幾個字段,我們可以很容易地獲得緩沖區的有效數據,剩余空間等信息,也可以很容易地把當前邏輯位置映射回真實的dma buffer中。
例如,獲得播放緩沖區的空閑空間:
snd_pcm_sframes_t avail = runtime->status->hw_ptr + runtime->buffer_size - runtime->control->appl_ptr;
這里runtime->status->hw_ptr是讀指針(DMA讀取buf數據,通過dai,寫入到codec),runtime->buffer_size是ring buffer大小,runtime->control->appl_ptr這個是寫指針
(應用寫到ring buffer里面),這樣就可以得到應用可以寫入的空間。
例如,獲得錄音緩沖區的可讀的空間:
snd_pcm_sframes_t avail = runtime->status->hw_ptr - runtime->control->appl_ptr;
這里runtime->status->hw_ptr是寫指針(DMA通過dai,讀取數據到ring buf),runtime->control->appl_ptr這個是讀指針
(應用讀取ring buffer數據的指針),這樣就可以得到應用可以讀取的空間。
所以要想通過snd_pcm_playback_avail等函數獲得正確的信息前,應該先要調用這個api更新指針位置。
以播放(playback)為例,我現在知道至少有3個途徑可以完成對dma buffer的寫入:
- 應用程序調用alsa-lib的snd_pcm_writei、snd_pcm_writen函數;
- 應用程序使用ioctl:SNDRV_PCM_IOCTL_WRITEI_FRAMES或SNDRV_PCM_IOCTL_WRITEN_FRAMES;
- 應用程序使用alsa-lib的snd_pcm_mmap_begin/snd_pcm_mmap_commit;
以上幾種方式最終把數據寫入dma buffer中,然后修改runtime->control->appl_ptr的值。
播放過程中,通常會配置成每一個period size生成一個dma中斷,中斷處理函數最重要的任務就是:
- 更新dma的硬件的當前位置,該數值通常保存在runtime->private_data中;
- 調用snd_pcm_period_elapsed函數,該函數會進一步調用snd_pcm_update_hw_ptr0函數更新上述所說的4個緩沖區管理字段,然后喚醒相應的等待進程。
錄音snd_pcm_lib_read1調試打印:
<4>[ 705.967313] cxw frames=1024, appl_ptr=138240, appl_ofs = 7168, cont=9216
<4>[ 705.967341] cxw buffer_size=16384, runtime->boundary=1073741824, avail = 1024
<4>[ 705.988310] cxw frames=1024, appl_ptr=139264, appl_ofs = 8192, cont=8192
<4>[ 705.988337] cxw buffer_size=16384, runtime->boundary=1073741824, avail = 1024
可以看出:
1.
frames就是一個period,buffer_size是ring buf大小,這里有16個period:1024*16 = 16384;
2.appl_ptr是讀的指針,這個指針會一值增加,直到 runtime->boundary的界限
3.appl_ofs 是相對於buffer_size的偏移
4.cont = buffer_size - appl_ofs
5.avail 是可讀的大小
3.DMA完成一個period后的中斷處理函數
我們這里調用kernel/sound/soc/soc-dmaengine-pcm.c中的dmaengine_pcm_dma_complete
struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream);
prtd->pos += snd_pcm_lib_period_bytes(substream); //得到偏移地址,也就是加上剛才的period_size
if (prtd->pos >= snd_pcm_lib_buffer_bytes(substream)) //如果已經比ring buf大,說明已經饒了一圈了
prtd->pos = 0; //回到起點
snd_pcm_period_elapsed(substream); // update the pcm status for the next period
snd_pcm_update_hw_ptr0(substream, 1) //更新緩沖區管理字段,單獨分析
4.更新緩沖區管理字段
//它是將hw_ofs轉換成FIFO中hw_ptr的過程,同時處理環形緩沖區的回繞,沒有中斷,中斷丟失等情況。
static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream, unsigned int in_interrupt)
old_hw_ptr = runtime->status->hw_ptr; //保存上一次的hw_ptr,在此函數中將更新hw_ptr /*
pos = substream->ops->pointer(substream);//獲取hw_ptr在當前HW buffer中的偏移,調用soc_pcm_pointer函數
if (platform->driver->ops && platform->driver->ops->pointer) //現在是獲取這個
offset = platform->driver->ops->pointer(substream); //hw_ptr在當前HW buffer中的偏移
delay += cpu_dai->driver->ops->delay(substream, cpu_dai); //調用cpu_dai, codec_dai,platform的delay函數
delay += codec_dai->driver->ops->delay(substream, codec_dai);
delay += platform->driver->delay(substream, codec_dai);
if (pos >= runtime->buffer_size)
pos = 0;
pos -= pos % runtime->min_align; //以runtime->min_align對齊這個pos,位置
hw_base = runtime->hw_ptr_base; //獲得基地址
new_hw_ptr = hw_base + pos; //獲得新的地址
if (in_interrupt) //DMA傳輸完成后調用,in_interrupt=1
delta = runtime->hw_ptr_interrupt + runtime->period_size; ///* Position at interrupt time */中斷的位置
if (delta > new_hw_ptr) {//如果本次通過中斷位置加上period_size計算出來的hw_ptr比當前hw_ptr大的話,則說明有可能hw_base需要更新到下一個HW buffer的基地址。
.....細節不分析了.....
if (new_hw_ptr < old_hw_ptr) {//如果當前的hw_ptr比上一次的hw_ptr小,說明hw_base需要更新到下一個HW buffer的基地址。hw_ptr也要同步更新。
hw_base += runtime->buffer_size;
if (hw_base >= runtime->boundary) {//如果hw_base > boundary,那hw_base回跳到Ring Buffer起始位置。
if (hw_base >= runtime->boundary) {//如果hw_base > boundary,那hw_base回跳到Ring Buffer起始位置。
........細節不分析了.....
__delta:
delta = new_hw_ptr - old_hw_ptr;
if (delta < 0)//如果當前的hw_ptr任然比上一的hw_ptr小,說明hw_ptr走完了Ring buffer一圈。
if (delta < 0)//如果當前的hw_ptr任然比上一的hw_ptr小,說明hw_ptr走完了Ring buffer一圈。
........細節不分析.....
if (delta >= runtime->buffer_size + runtime->period_size) {//如果當前hw_ptr比較上一次相差buffer size + peroid size,說明有錯誤。
.......細節不分析.....
no_jiffies_check:
if (delta > runtime->period_size + runtime->period_size / 2) {//interupt丟失,delta(如果當前hw_ptr比較上一次之差)>1.5個peroid size
if (delta > runtime->period_size + runtime->period_size / 2) {//interupt丟失,delta(如果當前hw_ptr比較上一次之差)>1.5個peroid size
.......細節不分析.....
snd_pcm_playback_silence(substream, new_hw_ptr);//播放silence if (in_interrupt) {//更新hw_ptr_interrupt
.......細節不分析.....
5.PCM的狀態轉換
下圖是PCM的狀態的轉換圖

除XRUN狀態之后,其它的狀態大多都由用戶空間的ioctl()顯式的切換。
以TinyAlsa的播放音頻流程為例。
pcm_open()的對應的流程就是:
open(pcm)后綁定一個substream,處於OPEN狀態ioctl(SNDRV_PCM_IOCTL_SW_PARAMS)設定參數pcm_config.配置 runtime 的sw_para.切換到SETUP狀態
Tinyalsa的pcm_wirte()流程:
ioctl(SNDRV_PCM_IOCTL_PREPARE)后,substream切換到PREPARE狀態。ioctl(SNDRV_PCM_IOCTL_WRITEI_FRAMES)后,substream切換到RUNNING狀態。
TinyAlsa的pcm_mmap_write()流程:
ioctl(SNDRV_PCM_IOCTL_PREPARE)后,substream切換到PREPARE狀態。ioctl(SNDRV_PCM_IOCTL_START)后,substream切換到RUNNING狀態。
TinyAlsa pcm_close流程:
ioctl(SNDRV_PCM_IOCTL_DROP)后,切換回SETUP狀態。close()之后,釋放這個設備。
XRUN狀態又分有兩種,在播放時,用戶空間沒及時寫數據導致緩沖區空了,硬件沒有 可用數據播放導致UNDERRUN;錄制時,用戶空間沒有及時讀取數據導致緩沖區滿后溢出, 硬件錄制的數據沒有空閑緩沖可寫導致OVERRUN.

