1. 程式人生 > >嵌入式Linux音訊驅動開發

嵌入式Linux音訊驅動開發

1.嵌入式音訊系統硬體連線

下圖所示的嵌入式裝置使用IIS將音訊資料傳送給編解碼器。對編解碼器的I/O暫存器的程式設計通過IIC匯流排進行。

2.音訊體系結構-ALSA

ALSA是Advanced Linux Sound Architecture 的縮寫,目前已經成為了linux的主流音訊體系結構
在核心裝置驅動層,ALSA提供了alsa-driver,同時在應用層,ALSA為我們提供了alsa-lib,應用程式只要呼叫alsa-lib提供的API,即可以完成對底層音訊硬體的控制。


3.ALSA裝置檔案

ALSA驅動核心會建立和管理一些裝置節點,比如:

/dev/snd/controlC0: 一個控制結點,(應用程式用它來控制音效卡,例如通道選擇,音量的控制等)

/dev/snd/pcmC0D0p:用於播放的pcm裝置

/dev/snd/pcmC0D0c:用於錄音的pcm裝置

C0D0代表的是音效卡0中的裝置0,最後一個c代表capture,最後一個p代表playback。

4.音效卡的建立流程
第一步,建立snd_card的一個例項
snd_card可以說是整個ALSA音訊驅動最頂層的一個結構,整個音效卡的軟體邏輯結構開始於該結構,幾乎所有與聲音相關的邏輯裝置都是在snd_card的管理之下.
第二步,建立音效卡的功能部件(邏輯裝置),例如PCM, Mixer等,並將邏輯裝置與步驟一建立的音效卡繫結
通常, alsa-driver的已經提供了一些常用的部件的建立函式,PCM ---- snd_pcm_new()、CONTROL -- snd_ctl_create()
第三步,將音效卡註冊進ALSA框架


經過以上的建立步驟之後,音效卡的邏輯結構如下圖所示:


5.PCM裝置的建立

對於一個pcm裝置,可以生成兩個裝置檔案,一個用於playback,一個用於capture,程式碼中也確定了他們的命名規則:

  • playback -- pcmCxDxp,通常系統中只有一個音效卡和一個pcm,它就是pcmC0D0p
  • capture -- pcmCxDxc,通常系統中只有一個音效卡和一個pcm,它就是pcmC0D0c

新建一個pcm裝置的過程:

  • snd_card_create ,pcm是音效卡下的一個裝置(部件),所以第一步是要建立一個音效卡
  • snd_pcm_new, 呼叫該api建立一個pcm,在該api中會做以下事情:
建立playback stream,相應的substream也同時建立
建立capture stream,相應的substream也同時建立
呼叫snd_device_new()把該pcm掛到音效卡中,引數ops中的dev_register欄位指向了函式snd_pcm_dev_register,這個回撥函式會在音效卡的註冊階段被呼叫
  • snd_pcm_set_ops, 設定操作該pcm的控制/操作介面函式,引數中的snd_pcm_ops結構中的函式通常就是我們驅動要實現的函式
  • snd_card_register 註冊音效卡,在這個階段會遍歷音效卡下的所有邏輯裝置,並且呼叫各裝置的註冊回撥函式,對於pcm,就是第二步提到的snd_pcm_dev_register函式,該回調函式建立了和使用者空間應用程式( alsa-lib)通訊所用的裝置檔案節點:/dev/snd/pcmCxxDxxp和/dev/snd/pcmCxxDxxc
6.Control裝置的建立

Control裝置和PCM裝置一樣,都屬於音效卡下的邏輯裝置。使用者空間的應用程式通過alsa-lib訪問該Control裝置,讀取或設定control的狀態,從而達到控制音訊Codec進行各種Mixer等控制操作。

要自定義一個Control,我們首先要定義3各回調函式:info,get和put。然後,定義一個snd_kcontrol_new結構:

static struct snd_kcontrol_new my_control __devinitdata = {  
    .iface = SNDRV_CTL_ELEM_IFACE_MIXER,  
    .name = "PCM Playback Switch",  
    .index = 0,  
    .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,  
    .private_value = 0xffff,  
    .info = my_control_info,  
    .get = my_control_get,  
    .put = my_control_put  
}; 
iface欄位指出了control的型別
name欄位是該control的名字
index欄位用於儲存該control的在該卡中的編號。
access欄位包含了該control的訪問型別。
private_value欄位包含了一個任意的長整數型別值。

info回撥函式用於獲取control的詳細資訊
get回撥函式用於讀取control的當前值,並返回給使用者空間的應用程式。
put回撥函式用於把應用程式的控制值設定到control中。

我們需要在我們的驅動程式初始化時主動呼叫snd_pcm_new()函式建立pcm裝置,而control裝置則在snd_card_create()內被建立,snd_card_create()通過呼叫snd_ctl_create()函式建立control裝置節點。所以我們無需顯式地建立control裝置,只要建立音效卡,control裝置被自動地建立。

7.移動裝置中的ALSA(ASoC)

ASoC--ALSA System on Chip ,是為了更好地支援嵌入式處理器和移動裝置中的音訊Codec的一套軟體體系。ASoC不能單獨存在,它建立在標準ALSA驅動之上,必須和標準的ALSA驅動框架相結合才能工作。

在軟體層面, ASoC也把嵌入式裝置的音訊系統同樣分為3大部分, Machine, Platform和Codec

Machine驅動:跟單板相關,繫結Platform和Codec驅動,即表明使用的是哪個Platform,哪個CPU DAI、DMA、Codec和Codec DAI。

Platform驅動 :它包含了該SoC平臺的音訊DMA和音訊介面DAI的配置和控制( I2S, PCM等等)

Codec驅動 :它包含了一些音訊的控制元件( Controls),音訊介面, DAMP(動態音訊電源管理)的定義和某些Codec IO功能。所有的Codec驅動都要提供以下特性:

  • Codec DAI 和 PCM的配置資訊;
  • Codec的IO控制方式( I2C, SPI等);
  • Mixer和其他的音訊控制元件;
  • Codec的ALSA音訊操作介面;

8.ASoC架構中的Machine

ASoC把音效卡註冊為Platform Device。
Machine驅動在一個重要的資料結構snd_soc_dai_link中,指定了Platform、 Codec、 codec_dai、 cpu_dai的名字,稍後Machine驅動將會利用這些名字去匹配已經在系統中註冊的platform, codec, dai,這些註冊的部件都是在另外相應的Platform驅動和Codec驅動的程式碼檔案中定義的,這樣看來, Machine驅動的裝置初始化程式碼無非就是選擇合適Platform和Codec以及dai,用他們填充以上幾個資料結構,然後註冊Platform裝置即可。

platform匯流排會匹配這兩個名字相同的device和driver,同時會觸發soc_probe的呼叫,它正是整個ASoC驅動初始化的入口。

soc_probe函式中會完成以下任務:

  • 呼叫標準的alsa函式建立音效卡例項
  • 挨個呼叫了codec, dai和platform驅動的probe函式
  • 呼叫了soc_new_pcm()函式用於建立標準alsa驅動的pcm邏輯裝置
  • 最後則是呼叫標準alsa驅動的音效卡註冊函式對音效卡進行註冊

9.ASoC架構中的Codec

在移動裝置中, Codec的作用可以歸結為4種,分別是:

  • 對PCM等訊號進行D/A轉換,把數字的音訊訊號轉換為模擬訊號
  • 對Mic、 Linein或者其他輸入源的模擬訊號進行A/D轉換,把模擬的聲音訊號轉變CPU能夠處理的數字訊號
  • 對音訊通路進行控制,比如播放音樂,收聽調頻收音機,又或者接聽電話時,音訊訊號在codec內的流通路線是不一樣的
  • 對音訊訊號做出相應的處理,例如音量控制,功率放大, EQ控制等等

描述Codec的最主要的幾個資料結構分別是: 

snd_soc_codec, snd_soc_codec_driver, snd_soc_dai, snd_soc_dai_driver,其中的snd_soc_dai和snd_soc_dai_driver在ASoC的Platform驅動中也會使用到, Platform和Codec的DAI通過snd_soc_dai_link結構,在Machine驅動中進行繫結連線。

Codec驅動的步驟:

  • 定義snd_soc_codec_driver和snd_soc_dai_driver的例項,然後呼叫snd_soc_register_codec函式對Codec進行註冊。
在snd_soc_register_codec函式中,它申請了一個snd_soc_codec結構的例項:確定codec的名字,這個名字很重要, Machine驅動定義的snd_soc_dai_link中會指定每個link的codec和dai的名字,進行匹配繫結時就是通過和這裡的名字比較,從而找到該Codec的!然後初始化snd_soc_codec結構的各個欄位,多數字段的值來自上面定義的snd_soc_codec_driver的例項。
  • 通過snd_soc_register_dais函式對本Codec的dai進行註冊
在snd_soc_register_dais函式中為每個snd_soc_dai例項分配記憶體,確定dai的名字,用snd_soc_dai_driver例項的欄位對它進行必要初始化
  • 最後,它把codec例項連結到全域性連結串列codec_list中,把dai連結到全域性連結串列dai_list中,並且調snd_soc_instantiate_cards函式觸發Machine驅動進行一次匹配繫結操作
10.ASoC架構中的Platform Platform驅動的主要作用是完成音訊資料的管理,最終通過CPU的數字音訊介面( DAI)把音訊資料傳送給Codec進行處理,最終由Codec輸出驅動耳機或者是喇叭的音信訊號。在具體實現上, ASoC有把Platform驅動分為兩個部分: snd_soc_platform_driver和snd_soc_dai_driver。其中, platform_driver負責管理音訊資料,把音訊資料通過dma或其他操作傳送至cpu dai中, dai_driver則主要完成cpu一側的dai的引數配置,同時也會通過一定的途徑把必要的dma等引數與snd_soc_platform_driver進行互動。
snd_soc_platform_driver的註冊
實現該驅動大致可以分為以下幾個步驟:
  • 定義一個snd_soc_platform_driver結構的例項;
  • 在platform_driver的probe回撥中利用ASoC的API: snd_soc_register_platform()註冊上面定義的例項;
  • 實現snd_soc_platform_driver中的各個回撥函式

snd_soc_platform_driver中的ops欄位是一個snd_pcm_ops結構,實現該結構中的各個回撥函式是soc platform驅動的主要工作,他們基本都涉及dma操作以及dma buffer的管理等工作。下面介紹幾個重要的回撥函式:
  • ops.open:當應用程式開啟一個pcm裝置時,該函式會被呼叫
  • ops.hw_params:驅動的hw_params階段,該函式會被呼叫,該函式會通過snd_soc_dai_get_dma_data函式獲得對應的dai的dma引數
  • ops.prepare:正式開始資料傳送之前會呼叫該函式,該函式通常會完成dma操作的必要準備工作。
  • ops.trigger:資料傳送的開始,暫停,恢復和停止時,該函式會被呼叫。
  • ops.pointer:該函式返回傳送資料的當前位置
cpu的snd_soc_dai driver驅動的註冊
dai驅動通常對應cpu的一個或幾個I2S/PCM介面,與snd_soc_platform一樣, dai驅動也是實現為一個platform driver,實現一個dai驅動大致可以分為以下幾個步驟:
  • 定義一個snd_soc_dai_driver結構的例項;
  • 在對應的platform_driver中的probe回撥中通過API: snd_soc_register_dai或者snd_soc_register_dais,注
  • 冊snd_soc_dai例項;
  • 實現snd_soc_dai_driver結構中的probe、 suspend等回撥;
  • 實現snd_soc_dai_driver結構中的snd_soc_dai_ops欄位中的回撥函式
它的ops欄位指向一個snd_soc_dai_ops結構,該結構實際上是一組回撥函式的集合, dai的配置和控制幾乎都是通過這些回撥函式來實現的,這些回撥函式基本可以分為3大類,驅動程式可以根據實際情況實現其中的一部分:
  • 工作時鐘配置函式 通常由machine驅動呼叫
  • 標準的snd_soc_ops回撥 通常由soc-core在進行PCM操作時呼叫
  • 抗pop, pop聲 由soc-core呼叫
11.音訊資料的dma操作
soc-platform驅動的最主要功能就是要完成音訊資料的傳送,大多數情況下,音訊資料都是通過dma來完成的
申請dma buffer
在音效卡的建立階段,pcm_new回撥函式會被呼叫,函式進一步為playback和capture分別呼叫preallocate_dma_buffer函式分配dma記憶體,然後完成substream->dma_buffer的初始化賦值工作

在音效卡的hw_params階段, snd_soc_platform_driver結構的ops->hw_params會被呼叫,在該回呼叫,通常會使用api: snd_pcm_set_runtime_buffer()把substream->dma_buffer的數值拷貝到substream->runtime的相關欄位中( .dma_area, .dma_addr, .dma_bytes),這樣以後就可以通過substream->runtime獲得這些地址和大小資訊了。

dma buffer獲得後,即是獲得了dma操作的源地址,那麼目的地址在哪裡?其實目的地址當然是在dai中,也就是前面介紹的snd_soc_dai結構的playback_dma_data和capture_dma_data欄位中,而這兩個欄位的值也是在hw_params階段,由snd_soc_dai_driver結構的ops->hw_params回撥,利用api: snd_soc_dai_set_dma_data進行設定的。緊隨其後, snd_soc_platform_driver結構的ops->hw_params回撥利用api: snd_soc_dai_get_dma_data獲得這些dai的dma資訊,其中就包括了dma的目的地址資訊。這些dma資訊通常還會被儲存在substream->runtime->private_data中,以便在substream的整個生命週期中可以隨時獲得這些資訊,從而完成對dma的配置和操作
dma buffer管理 播放時,應用程式把音訊資料來源源不斷地寫入dma buffer中,然後相應platform的dma操作則不停地從該buffer中取出資料,經dai送往codec中。錄音時則正好相反, codec源源不斷地把A/D轉換好的音訊資料經過dai送入dmabuffer中,而應用程式則不斷地從該buffer中讀走音訊資料。
以上只是參考別人部落格,概括性的總結了一下Linux音訊子系統,如果想深入瞭解,可以檢視部落格: http://blog.csdn.net/droidphone/article/details/6289712