- 瞭解ASoC 參考
1.概述
ASoC是Alsa System on Chip的縮寫,用於實現那些集成聲音控制器的CPU,它的設計目標如下:
- 解耦codec, codec的驅動不依賴具體的平臺。
- 簡單易用的I2S/PCM配置接口,讓soc和codec的配置相匹配。
- 動態的電源管理DAPM,實現對用戶空間透明的電源管理,各個widget按需供電,實現功耗最小化。
- 消除pop音,控制各個widget上下電的順序消除pop音。
- 添加平臺相關的控制,如earphone, speaker。
2.ASoC架構
以播放爲例, 在這樣一個硬件結構下, 涉及到幾個模塊:
-
DMA : 負責把用戶空間的音頻數據搬移至I2S的FIFO.
-
I2S : 負責以某個採樣頻率、採樣深度、通道數發送音頻數據, 也叫dai (Digital Audio Interface).
-
AFIx : 負責以某個採樣頻率、採樣深度、通道數接收音頻數據, 也稱作dai.
-
DAC : 並把數據通過DAC轉換後送給耳機等播放.
爲了解決複用性問題, 內核引入了ASoC架構,在底層ASoC抽象瞭如下三個模塊:
- Platform : 該模塊負責DMA的控制和I2S的控制, 由CPU廠商負責編寫此部分代碼.
- Codec : 該模塊負責AFIx的控制和DAC部分的控制(也可以說是芯片自身的功能的控制), 由Codec廠商負責編寫此部分代碼.
- Machine : 用於描述一塊電路板, 它指明此塊電路板上用的是哪個Platform和哪個Codec, 由電路板商負責編寫此部分代碼.
從數據結構的角度來說, ASoC核心層內部定義瞭如下數據結構, 注意它們是由核心層內部創建和維護的:
-
struct snd_soc_platform : 用於抽象一個platform, 作用是描述一個CPU的DMA設備及操作函數. 系統中可能有多個platforms, 它們都掛載在全局鏈表頭static LIST_HEAD(platform_list)下面, 不同的platform以name區分.
-
struct snd_soc_codec : 用於抽象一顆Codec. 一顆Codec可能會有多個dai接口, 該結構體的作用是描述與具體dai無關的、Codec內部的工作邏輯, 例如控件/微件/音頻路由的描述信息、時鐘配置、IO 控制等. 系統中可能有多個Codecs, 它們都掛載在全局鏈表頭static LIST_HEAD(codec_list) 下面, 不同的Codec以name區分.
-
struct snd_soc_dai : 用於描述一個dai, 既可以是CPU側的dai(I2S), 也可以是Codec側的dai(AFIx). 一顆CPU可能有多個I2S, 一個Codec也可能有多個AFIx, 因此係統中會有很多個dai, 它們都掛載在全局鏈表頭static LIST_HEAD(dai_list)下面, 不同的dai以name區分.
從底層驅動的角度來說, ASoC定義了一些需要底層實現的interface以及相應的註冊函數:
-
針對DMA (platform): CPU廠商需要填充struct snd_soc_platform_driver 和struct snd_pcm_ops, 然後調用snd_soc_register_platform向ASoC核心層註冊, 例如atmel-pcm-pdc.c.
-
針對I2S (cpu_dai): CPU廠商需要填充struct snd_soc_dai_driver 和 struct snd_soc_dai_ops, 然後調用snd_soc_register_dai向ASoC核心層註冊, 例如atmel_ssc_dai.c.
-
針對Codec (codec_dai) : Codec廠商需要填充struct snd_soc_codec_driver(用於描述Codec內部工作邏輯)和struct snd_soc_dai_driver、struct snd_soc_dai_ops(用於描述AFIx), 然後調用snd_soc_register_codec向ASoC核心層註冊, 例如wm9081.c.
-
針對Machine (codec): 電路板商需要填充struct snd_soc_dai_link、struct snd_soc_ops, 然後準備一個struct snd_soc_card把dai_link包裹起來, 然後調用snd_soc_register_card註冊此snd_soc_card. 例如atmel_wm8904.c.
當底層調用了snd_soc_register_card時, ASoC核心層會從全局鏈表中找到dai_link指定的platform、cpu_dai、codec_dai、codec, 並建立一個struct snd_soc_pcm_runtime來保存這些對應關係. 然後核心層會snd_card_new創建聲卡, snd_pcm_new創建pcm邏輯設備, 最後snd_card_register註冊聲卡. 此後, 用戶空間就可以看到設備節點了. 當用戶空間訪問設備節點時, 最終由ASoC核心層響應, 核心層會通過snd_soc_pcm_runtime找到對應的platform、cpu_dai、codec_dai、codec, 並根據需要回調它們實現的接口函數.
ASoC系統架構也在不停的演化, 從v4.18開始, 雖然ASoC還是劃分爲platform、cpu_dai、codec_dai、codec這幾個模塊, 但描述它們的數據結構發生了較大變化:
-
首先, ASoC用統一的數據結構來描述platform和codec : 核心層用struct snd_soc_component替代原來的struct snd_soc_platform(描述platform)和struct snd_soc_codec(描述codec); 底層驅動則用struct snd_soc_component_driver替換原來的struct snd_soc_platform_driver和struct snd_soc_codec_driver.
-
其次, 描述dai的數據結構雖然沒有變化, 核心層依然用struct snd_soc_dai, 底層驅動依然用struct snd_soc_dai_driver、struct snd_soc_dai_ops. 但dai相關的數據結構被包裹到snd_soc_component下來統一管理了. 當底層驅動註冊一個dai時, 核心層會創建一個snd_soc_component, 然後把dai掛載到這個數據結構下面.
-
也就是說, 不管是platform、codec還是dai, 核心層現在都統一用struct snd_soc_component來管理了, 一個component對應一個模塊. 因此原先的platform_list、codec_list、dai_list這3個表頭都不存在了, 只有一個static LIST_HEAD(component_list)表頭.
-
最後, 核心層提供給底層的註冊API也統一了, 不管是platform、codec還是dai, 現在都統一用devm_snd_soc_register_component / snd_soc_register_component註冊即可.
-
Machine層面沒有什麼變化, machine驅動還是構建一個struct snd_soc_card然後調用snd_soc_register_card註冊即可. 而且核心層在註冊函數的實現邏輯上也沒有太大變化.
ASoC架構如下圖:
ASoC對於Alsa來說,就是分別註冊PCM/CONTROL類型的snd_device設備,並實現相應的操作方法集。圖中DAI是數字音頻接口,用於配置音頻數據格式等。
- Codec驅動向ASoC註冊snd_soc_codec和snd_soc_dai設備。
- Platform驅動向ASoC註冊snd_soc_platform和snd_soc_dai設備。
- Machine驅動通過snd_soc_dai_link綁定codec/dai/platform。
Widget是各個組件內部的小單元。處在活動通路上電,不在活動通路下電。ASoC的DAPM 正是通過控制這些Widget的上下電達到動態電源管理的效果。
- path描述與其它widget的連接關係。
- event用於通知該widget的上下電狀態。
- power指示當前的上電狀態。
- control實現空間用戶接口用於控制widget的音量/通路切換等。
對驅動開者來說,就可以很好的解耦了:
- codec驅動的開發者,實現codec的IO讀寫方法,描述DAI支持的數據格式/操作方法和Widget的連接關係就可以了;
- soc芯片的驅動開發者,Platform實現snd_pcm的操作方法集和DAI的配置如操作 DMA,I2S/AC97/PCM的設定等;
- 板級的開發者,描述Machine上codec與platform之間的總線連接, earphone/Speaker的佈線情況就可以了。
2.1.struct snd_soc_card
include/sound/soc.h:
struct snd_soc_card {
const char *name;
const char *long_name;
const char *driver_name;
struct device *dev;
struct snd_card *snd_card; //card結構體,最終是要註冊card
struct module *owner;
struct list_head list;
struct mutex mutex;
struct mutex dapm_mutex;
bool instantiated;
int (*probe)(struct snd_soc_card *card);
int (*late_probe)(struct snd_soc_card *card);
int (*remove)(struct snd_soc_card *card);
/* the pre and post PM functions are used to do any PM work before and
* after the codec and DAI's do any PM work. */
int (*suspend_pre)(struct snd_soc_card *card);
int (*suspend_post)(struct snd_soc_card *card);
int (*resume_pre)(struct snd_soc_card *card);
int (*resume_post)(struct snd_soc_card *card);
/* callbacks */
int (*set_bias_level)(struct snd_soc_card *,
struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
int (*set_bias_level_post)(struct snd_soc_card *,
struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
long pmdown_time;
/* CPU <--> Codec DAI links */----------------附屬部件鏈接表
struct snd_soc_dai_link *dai_link;
int num_links;
struct snd_soc_pcm_runtime *rtd;
int num_rtd;
/* optional codec specific configuration */
struct snd_soc_codec_conf *codec_conf;
int num_configs;
/*
* optional auxiliary devices such as amplifiers or codecs with DAI
* link unused
*/
struct snd_soc_aux_dev *aux_dev;
int num_aux_devs;
struct snd_soc_pcm_runtime *rtd_aux;
int num_aux_rtd;
const struct snd_kcontrol_new *controls;
int num_controls;
/*
* Card-specific routes and widgets.----------------------DAPM內容
*/
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;
bool fully_routed;
struct work_struct deferred_resume_work;
/* lists of probed devices belonging to this card */--------附屬部件鏈表
struct list_head codec_dev_list;
struct list_head platform_dev_list;
struct list_head dai_dev_list;
struct list_head widgets;
struct list_head paths;
struct list_head dapm_list;
struct list_head dapm_dirty;
/* Generic DAPM context for the card */---------------DAPM內容
struct snd_soc_dapm_context dapm;
struct snd_soc_dapm_stats dapm_stats;
#ifdef CONFIG_DEBUG_FS
struct dentry *debugfs_card_root;
struct dentry *debugfs_pop_time;
#endif
u32 pop_time;
void *drvdata;
};
2.2.struct snd_card
struct snd_card可以說是整個ALSA音頻驅動最頂層的一個結構, 整個聲卡的軟件邏輯結構開始於該結構, 幾乎所有與聲音相關的邏輯設備都是在snd_card的管理之下,聲卡驅動的第一個動作通常就是創建一個snd_card結構體。
include/sound/core.h:
/* main structure for soundcard */
struct snd_card {
int number; /* number of soundcard (index to
snd_cards) */
char id[16]; /* id string of this card */
char driver[16]; /* driver name */
char shortname[32]; /* short name of this soundcard */
char longname[80]; /* name of this soundcard */
char mixername[80]; /* mixer name */
char components[128]; /* card components delimited with
space */
struct module *module; /* top-level module */
void *private_data; /* private data for soundcard */
void (*private_free) (struct snd_card *card); /* callback for freeing of
private data */
struct list_head devices; /* devices */
unsigned int last_numid; /* last used numeric ID */
struct rw_semaphore controls_rwsem; /* controls list lock */
rwlock_t ctl_files_rwlock; /* ctl_files list lock */
int controls_count; /* count of all controls */
int user_ctl_count; /* count of all user controls */
struct list_head controls; /* all controls for this card */
struct list_head ctl_files; /* active control files */
struct snd_info_entry *proc_root; /* root for soundcard specific files */
struct snd_info_entry *proc_id; /* the card id */
struct proc_dir_entry *proc_root_link; /* number link to real id */
struct list_head files_list; /* all files associated to this card */
struct snd_shutdown_f_ops *s_f_ops; /* file operations in the shutdown
state */
spinlock_t files_lock; /* lock the files for this card */
int shutdown; /* this card is going down */
int free_on_last_close; /* free in context of file_release */
wait_queue_head_t shutdown_sleep;
struct device *dev; /* device assigned to this card */
#ifndef CONFIG_SYSFS_DEPRECATED
struct device *card_dev; /* cardX object for sysfs */
#endif
#ifdef CONFIG_PM
unsigned int power_state; /* power state */
struct mutex power_lock; /* power lock */
wait_queue_head_t power_sleep;
#endif
#if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_MIXER_OSS_MODULE)
struct snd_mixer_oss *mixer_oss;
int mixer_oss_change_count;
#endif
};
- struct list_head devices 記錄該聲卡下所有邏輯設備的鏈表
- struct list_head controls 記錄該聲卡下所有的控制單元的鏈表
- void *private_data 聲卡的私有數據,可以在創建聲卡時通過參數指定數據的大小
2.3.card註冊
通常有兩種方法註冊一個card:
-
在machine驅動中註冊一個名爲 “soc-audio” 的 platform device,並將 struct snd_soc_card 結構變量作爲這個device的私有數據,在 soc-core.c 調用 名爲 “soc-audio” 的 platform drv的probe函數,probe中調用 snd_soc_register_card 註冊這個card.
-
直接在machine驅動中調用 snd_soc_register_card 函數註冊card.
snd_soc_register_card流程如下:
- A:綁定系統中相關的platform/cedec
- B:聲卡對象的創建
- C:聲卡註冊
- control:這是control相關的一些初始化函數
2.4.邏輯設備創建與註冊
struct snd_device 隸屬於card, 通常用來實現某一功能,一個邏輯設備一般來說會對應用戶空間的一個或多個設備節點(也有某些僅在內核使用的邏輯設備不會創建設備節點)。
- struct snd_device:
struct snd_device {
struct list_head list; /* list of registered devices */
struct snd_card *card; /* card which holds this device */
enum snd_device_state state; /* state of the device */
enum snd_device_type type; /* device type */
void *device_data; /* device structure */
struct snd_device_ops *ops; /* operations */
};
- struct snd_device_ops
每個邏輯設備都有一個對應的snd_device_ops, 在新增一個邏輯設備時, 需要爲此設備準備好該數據結構。
struct snd_device_ops {
int (*dev_free)(struct snd_device *dev);
int (*dev_register)(struct snd_device *dev);
int (*dev_disconnect)(struct snd_device *dev);
};
2.4.1.API
- snd_device_new
該API用於創建一個邏輯設備,主要內容是分配一個struct snd_device空間, 初始化相關字段, 並把該邏輯設備添加到card->devices鏈表下。 - snd_device_register
一般在註冊聲卡時(snd_card_register)會自動調用此處的API; 不過也可以在card註冊完畢後, 在手動調用此API註冊一個新的邏輯設備。、 - snd_device_register_all
相當於對snd_device_register的封裝: 針對card下的每一個邏輯設備, 調用__snd_device_register註冊此邏輯設備。 - snd_device_disconnect
一般在調用snd_card_disconnect時會調用此API.
2.4.2. 字符設備驅動的創建
邏輯設備與用戶空間是通過字符設備驅動交互的。一個邏輯設備可以創建一個或多個字符設備節點,節點創建的時機如下:
當聲卡註冊時(snd_card_register), 會針對其下的每一個邏輯設備調用snd_device_register,後者會回調邏輯設備的回調函數(snd_device_ops->dev_register)。邏輯設備在實現dev_register時, 一般會調用snd_register_device, 後者會通過device_add創建一個設備節點. 所以調用幾次snd_register_device, 就會存在幾個設備節點。
所有ALSA字符設備的主設備號都是CONFIG_SND_MAJOR (116), ALSA系統在初始化時, 會在alsa_sound_init中調用register_chrdev,定義此類字符設備的統一處理函數snd_fops。snd_fops中只實現了snd_open函數, 當用戶空間打開任一ALSA設備節點時, 都會進入到該open函數中。
在snd_open中, 會根據次設備號(每個邏輯設備都有自己的次設備號), 選擇對應的邏輯設備的ops函數, 然後替換file->f_op, 此後用戶空間的任何操作都是直接與邏輯設備的ops函數交互的。ALSA系統中有一個全局數組snd_minors[], 數組的下標就是次設備號, 每個元素對應一個邏輯設備, snd_minor[i]-> f_ops存儲的就是邏輯設備自己的ops函數.有了它, snd_open的實現就比較容易了, 只需用次設備號作爲下標, 從snd_minors[]中找到對應的元素, 然後用元素的f_ops替換file->f_op即可.
2.4.3.邏輯設備中間層
在當前的系統中, 有很多邏輯設備中間層, 它們相當於對《邏輯設備創建與註冊》中的步驟進行了封裝, 然後對外提供了更加簡潔的API來創建對應的邏輯設備。
除了Info設備外, 其它幾個中間層都是對snd_device_new進行了一次封裝. 當調用這些中間層提供的API時, 最終會在card下添加一個新的邏輯設備。
在card的註冊階段, 會掃描它下面的每一個邏輯設備, 然後調用snd_device_register來註冊該邏輯設備. 當邏輯設備註冊完成後, 核心層會回調snd_device_ops.dev_register函數, 如果dev_register裏面調用了snd_register_device, 則會在用戶空間出現字符設備節點.
重點介紹:PCM邏輯設備
- PCM邏輯設備是以字符設備的形式呈現給用戶空間;
- 內核實現了一個‘PCM中間層’, 該中間層對上向ALSA核心層註冊, 將自己註冊爲一個邏輯設備, 並最終創建了字符設備節點, 負責與用戶空間交互(snd_pcm_f_ops); 對下則提供API給底層驅動, 供底層驅動註冊一個PCM設備.
從功能的角度來說, PCM邏輯設備的作用是供用戶空間播放/錄製PCM音頻. PCM音頻本質上來說就是一塊Buffer, ‘PCM中間層’的作用就是從用戶空間接收這塊Buffer, 然後把Buffer的數據轉給底層驅動播放. 底層驅動一般對應的是I2S控制器, 它收到數據後, 會通過I2S總線把數據傳給codec, 然後codec經過DA轉換後送到喇叭播放.
除了Buffer的傳輸, 還要解決配置的問題, 也就是配置I2S控制器和Codec芯片, 告訴它們應該按照什麼樣的clock、採樣深度、通道數等來播放Buffer中的數據. 因此‘PCM中間層’也提供了一些ioctl給用戶空間, 供它們設置這些參數.
從分工的角度來說, PCM中間層和底層驅動各自應該專注於哪些事情?
PCM中間層主要完成一些公共性的事情, 這些事情一般與具體的硬件無關(例如存儲音頻數據時, 肯定需要一個Buffer, 這個Buffer很顯然用環形緩衝區描述比較好, 怎麼管理這個環形緩衝區的讀寫指針? 另外, 用戶空間設置的音頻參數要檢查其合法性. 還有, 用戶空間可能啓停音頻播放/錄製, 這意味着我們最好實現一個狀態機, 來管理用戶空間的切換操作. 等等). 對於與具體硬件相關的操作, PCM中間層會pass給底層驅動去處理(這就意味着PCM中間層要定義interface, 讓底層驅動去實現這些interface).
底層驅動則處理與具體硬件相關的事情, 例如I2S控制器的配置, Codec芯片的配置, 時鐘的初始化等等.
refer to
- http://www.mysixue.com/?p=134