Linux音頻子系統(3) - ALSA 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章