首頁 > 軟體

Linux ALSA詳解

2020-06-16 16:40:25

1. 介紹

ALSA(即Advanced Linux Sound Architecture), 是目前Linux的主流音訊體系結構, 提供了音訊和MIDI的支援, 其架構圖如下所示

TIP: 筆者的程式碼分析基於linux-4.14.19

2. 初始化

系統啟動中ALSA初始化過程如下

alsa_sound_init()
  /* 註冊alsa字元裝置 */
  register_chrdev(116, "alsa", &snd_fops)
  /* 建立/proc/asound目錄及下屬version、devices、cards、modules等檔案 */
  snd_info_init()
  
const struct file_operations snd_fops =
{
    .owner =    THIS_MODULE,
    .open =     snd_open,
    .llseek =   noop_llseek,
};

從使用者空間開啟PCM裝置過程如下

snd_pcm_open("default", SND_PCM_STREAM_PLAYBACK)  // alsa-lib介面
  open("/dev/snd/controlC0")         // 開啟控制裝置; 主裝置116, 次裝置0
  open("/dev/snd/pcmC0D0p")          // 開啟PCM裝置; 主裝置116, 次裝置16
    snd_open()                       // 根據主裝置號找到該入口
      snd_minors[minor]              // 根據次裝置號找到對應操作集
        snd_ctl_f_ops::open()        // 控制裝置開啟方法
          snd_ctl_open()
        snd_pcm_f_ops::open()        // PCM裝置開啟方法
          snd_pcm_playback_open()
            snd_lookup_minor_data()  // 根據次裝置號查詢對應PCM裝置(snd_pcm)
            snd_pcm_open()           // 開啟PCM播放子流

3. 核心層

核心層為使用者空間提供邏輯裝置介面, 同時為驅動提供介面來驅動硬體裝置, 主要位於sound/core目錄下

3.1 資料結構

該層包含的主要資料結構包括

- snd_card      表示一個音效卡範例, 包含多個音效卡裝置
- snd_device    表示一個音效卡裝置部件
- snd_pcm       表示一個PCM裝置, 音效卡裝置的一種, 用於播放和錄音
- snd_control   表示Control裝置, 音效卡裝置的一種, 用於控制音效卡
- snd_pcm_str   表示PCM流, 分為playback和capture
- snd_pcm_substream PCM子流, 用於音訊的播放或錄製
- snd_pcm_ops   PCM流操作集

各結構體之間主要關係圖如下所示

snd_card主要欄位如下

struct snd_card {
    int number;             /* 索引 */
    char id[16];            /* 識別符號 */

    char driver[16];        /* 驅動名稱 */
    char shortname[32];     /* 短名 */
    char longname[80];      /* 名字 */

    void *private_data;     /* 音效卡私有資料*/
    void (*private_free) (struct snd_card *); /* 私有資料釋放回撥 */

    struct list_head devices;     /* 該音效卡下所有裝置*/
    struct list_head controls;    /* 該音效卡下所有控制裝置*/

    struct list_head files_list;  /* 音效卡管理檔案 */
    struct device *dev;           /* 音效卡相關的device */
    struct device card_dev;       /* 用於sysfs, 代表該音效卡 */
    bool registered;              /* 是否註冊標記 */
};

snd_device主要欄位如下

struct snd_device {
    struct list_head list;        /* 所有註冊的音效卡裝置連結串列 */
    struct snd_card *card;        /* 裝置所屬音效卡 */
    enum snd_device_state state;  /* 裝置狀態*/
    enum snd_device_type type;    /* 裝置型別*/
    void *device_data;            /* 指向具體的音效卡裝置, 如snd_pcm */
    struct snd_device_ops *ops;   /* 裝置操作集*/
};

snd_pcm主要欄位如下

struct snd_pcm {
    struct snd_card *card;   /* 該PCM裝置所屬音效卡*/
    struct list_head list;   /* 所有註冊的PCM裝置連結串列 */
    int device;              /* PCM索引 */
    unsigned int info_flags; /* SNDRV_PCM_INFO_ */
    char id[64];             /* PCM裝置標識 */
    char name[80];           /* PCM裝置名 */
    struct snd_pcm_str streams[2];  /* 指向PCM裝置的capture(1)和playback(0)流 */
    void *private_data;      /* PCM裝置私有資料*/
    void (*private_free) (struct snd_pcm *); /* 私有資料釋放回撥 */
};

3.2 介面

該層主要介面如下

/* 建立和初始化音效卡結構體 */
int snd_card_new(struct device *parent, int idx, const char *xid, struct module *, int extra_size, struct snd_card **card_ret);
/* 釋放音效卡結構體 */
int snd_card_free(struct snd_card * card);
/* 註冊音效卡 */
int snd_card_register(struct snd_card * card);

/* 建立音效卡裝置部件, 通常由snd_pcm_new和snd_card_new自動完成 */
int snd_device_new(struct snd_card *, enum snd_device_type type, void *device_data, struct snd_device_ops *ops);
/* 註冊音效卡裝置部件, 通常由snd_card_register自動完成 */
int snd_device_register(struct snd_card *card, void *device_data);

/* 建立PCM裝置 */
int snd_pcm_new(struct snd_card *, const char *id, int device, int playback_count, int capture_count, struct snd_pcm **rpcm);
/* 建立PCM流, 通常snd_pcm_new會自動建立capture和playback兩個PCM流 */
int snd_pcm_new_stream(struct snd_pcm * pcm, int stream, int substream_count);
/* 設定PCM裝置操作集 */
void snd_pcm_set_ops(struct snd_pcm *, int direction, const struct snd_pcm_ops *ops);

snd_card_new完成了如下事宜
1. 分配snd_card+extra_size空間大小
2. 如果extra_size大於0,將private_data指向extra_size所在首地址
3. 如果指定了xid, 將其拷貝至snd_card::id中, 即音效卡識別符號
4. 根據idx獲取可用的音效卡索引並賦值給snd_card::number
5. 分別將parent、module賦值給snd_card::dev、snd_card::module
6. 初始化連結串列snd_card::devices、snd_card::controls、snd_card::ctl_files、snd_card::files_list
7. 呼叫device_initialize()初始化snd_card::card_dev, 並設定snd_card::card_dev相關成員變數, 用於sysfs
8. 呼叫snd_ctl_create()建立控制介面
8.1 呼叫snd_device_initialize初始化snd_card::ctl_dev, 並設定相關成員變數, 用於sysfs
8.2 呼叫snd_device_new(SNDRV_DEV_CONTROL, ops)建立音效卡控制裝置部件

    static struct snd_device_ops ops = {
        .dev_free = snd_ctl_dev_free,
        .dev_register = snd_ctl_dev_register,
        .dev_disconnect = snd_ctl_dev_disconnect,
    };

9. 呼叫snd_info_card_create()建立proc對應檔案系統

snd_card_register完成了如下事宜
1. 如果音效卡未註冊(snd_card::registered), 呼叫device_add(snd_card::card_dev)將音效卡新增到sysfs
2. 呼叫snd_device_register_all(snd_card)註冊該音效卡下所有音效卡裝置(即snd_card::devices連結串列), 即完成snd_device_register相同的功能
2.1 遍歷snd_card::devices連結串列, 依次呼叫__snd_device_register註冊音效卡裝置
2.1.1 呼叫snd_device::snd_device_ops::dev_register註冊該裝置, 對於Control裝置, 即snd_ctl_dev_register; 對於PCM裝置, 即snd_pcm_dev_register; 最終則都會呼叫snd_register_device
2.1.1.1 snd_ctl_dev_register: 呼叫snd_register_device(snd_ctl_f_ops)註冊該Control裝置
2.1.1.2 snd_pcm_dev_register: 呼叫snd_pcm_add將該PCM裝置新增至全域性PCM連結串列snd_pcm_devices中, 然後呼叫snd_register_device(snd_pcm_f_ops)註冊該PCM裝置
2.1.1.x.1 snd_register_device: 分配snd_minor空間, 設定type、card、device、f_ops、card_ptr等成員變數; 通過snd_find_free_minor找到合適的minor並通過MKDEV(116, minor)建立裝置節點, 然後通過device_add向系統新增該裝置; 最後將該音效卡裝置新增至全域性音效卡主裝置的次裝置陣列snd_minors中
3. 將該音效卡放入全域性靜態音效卡陣列snd_cards中
4. 呼叫init_info_for_card()向proc檔案系統註冊該音效卡

snd_pcm_new完成了如下事宜
1. 分配snd_pcm空間, 並設定snd_pcm::card、snd_pcm::device等成員變數
2. 呼叫snd_pcm_new_stream(SNDRV_PCM_STREAM_PLAYBACK)建立playback_count個子流用於播放
3. 呼叫snd_pcm_new_stream(SNDRV_PCM_STREAM_CAPTURE)建立capture_count個子流用於錄製
4. 呼叫snd_device_new(SNDRV_DEV_PCM, ops)新增PCM裝置

    static struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register = snd_pcm_dev_register,
        .dev_disconnect = snd_pcm_dev_disconnect,
    };

snd_pcm_new_stream完成了如下事宜
1. 設定snd_pcm::stream[playback or catpure]對應stream、pcm、substream_count成員變數
2. 呼叫snd_device_initialize()初始化snd_pcm::stream::dev, 並設定相關成員變數, 用於sysfs
3. 呼叫snd_pcm_stream_proc_init(snd_pcm_str)初始化對應proc檔案系統
4. 分配substream_count個snd_pcm_substream並進行相應初始化

3.3 實現

核心驅動的一般實現步驟如下
1. 呼叫snd_card_create建立音效卡範例(struct snd_card)
2. 定義音效卡的私有結構體用於存放該音效卡的一些資源資訊, 如中斷資源、IO資源、DMA資源等
3. 硬體初始化, 包括數位音訊介面初始化、DMA控制器初始化、編解碼器初始化
4. 呼叫snd_pcm_new建立邏輯裝置, 並實現其操作集snd_pcm_ops
5. 呼叫snd_card_register註冊音效卡範例及音效卡裝置

具體範例可參考sound/atmel/ac97csound/spi/at73c213的實現

4. ASOC層

在移動裝置中, 為了更好的提供ALSA支援, 在核心層的基礎上出現了ASOC(ALSA System on Chip)層
ASOC層程式碼位於sound/soc/*, 主要由如下三部分組成
- Codec: 負責設定編解碼器提供音訊捕獲和回放功能
- Platform: 主要負責SoC平台音訊DMA和音訊介面的設定和控制, 包括時鐘、DMA、I2S、PCM等
- Machine: Codec、Platform、輸入輸出裝置提供了一個載體

4.1 DAI

DAI(Digital Audio Interfaces), 即數位音訊介面
ASOC支援三種主流DAI: AC97、I2S和PCM

AC97: 通常用於PC音效卡, 為5線介面, 每個AC97幀為21uS長, 被分為13個時隙
- BCLK: 由AC97驅動, 為12.288 MHz
- SYNC: 同步信號, 由Controler驅動, 為48 kHz
- SDATDIN: 用於capture, AC97->Controler
- SDATAOUT: 用於playback, Controler->AC97
- RESET: 由Controler生成, 用於喚醒AC97

I2S是HiFi、STB和便攜式裝置中常用的4線DAI
- SCLK: 序列時鐘
- LRCK: 也稱WS, 聲道選擇線
- Tx: 用於傳輸音訊資料
- Rx: 用於接收音訊資料

PCM是另一種4線介面, 與I2S非常相似, 可以支援更靈活的協定
- BCLK: 位時鐘, 根據取樣率而變化
- SYNC: 同步信號
- Tx: 用於傳輸音訊資料
- Rx: 用於接收音訊資料

4.2 Codec

Codec驅動應該實現為通用與硬體無關的,用於設定編解碼器、FM、MODEM、BT或外部DSP, 以提供playback和capture, 這部分程式碼通常位於sound/soc/codecs/*

每個Codec驅動必須??供如下功能
1. Codec DAI和PCM設定
2. 使用RegMap實現的Codec控制IO
3. Mixers和Audio控制
4. Codec音訊操作
5. DAPM描述
6. DAPM事件處理
7. DAC靜音控制(可選)

4.2.1 資料結構

Codec層主要結構體包括snd_soc_codec、snd_soc_codec_driver、snd_soc_dai、snd_soc_dai_driver

snd_soc_codec代表一個Codec裝置, 其主要欄位如下

struct snd_soc_codec {
    struct device *dev;        /* 指向Codec裝置的指標 */
    const struct snd_soc_codec_driver *driver;  /* 該Codec對應的驅動 */
    struct list_head list;

    /* runtime */
    unsigned int cache_init:1; /* 指示Codec cache是否初始化 */

    /* codec IO */
    void *control_data;        /* 控制IO資料 */
    hw_write_t hw_write;       /* 控制IO函數 */
    void *reg_cache;

    /* component */
    struct snd_soc_component component;
};

snd_soc_codec_driver代表一個Codec驅動, 其主要欄位如下

struct snd_soc_codec_driver {
    /* 操作集 */
    int (*probe)(struct snd_soc_codec *);
    int (*remove)(struct snd_soc_codec *);
    int (*suspend)(struct snd_soc_codec *);
    int (*resume)(struct snd_soc_codec *);
    struct snd_soc_component_driver component_driver;

    /* codec wide operations */
    int (*set_sysclk)(struct snd_soc_codec *codec,
              int clk_id, int source, unsigned int freq, int dir);
    int (*set_pll)(struct snd_soc_codec *codec, int pll_id, int source,
        unsigned int freq_in, unsigned int freq_out);
    int (*set_jack)(struct snd_soc_codec *codec,
            struct snd_soc_jack *jack,  void *data);

    /* Codec IO相關函數 */
    struct regmap *(*get_regmap)(struct device *);
    unsigned int (*read)(struct snd_soc_codec *, unsigned int);
    int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);
    
    /* 偏置電壓設定函數 */
    int (*set_bias_level)(struct snd_soc_codec *,
                  enum snd_soc_bias_level level);
};

snd_soc_dai代表DAI執行時資料, 其主要欄位如下

struct snd_soc_dai {
    const char *name;    /* 名稱 */
    int id;              /* 索引 */
    struct device *dev;  /* DAI裝置 */

    /* 驅動操作集 */
    struct snd_soc_dai_driver *driver;

    /* DAI執行時資訊 */
    unsigned int capture_active:1;
    unsigned int playback_active:1;
    unsigned int symmetric_rates:1;
    unsigned int symmetric_channels:1;
    unsigned int symmetric_samplebits:1;
    unsigned int probed:1;

    unsigned int active;

    struct snd_soc_dapm_widget *playback_widget;
    struct snd_soc_dapm_widget *capture_widget;

    /* DAI DMA data */
    void *playback_dma_data;  /* 用於管理playback DMA */
    void *capture_dma_data;   /* 用於管理capture DMA */

    /* Symmetry data - only valid if symmetry is being enforced */
    unsigned int rate;
    unsigned int channels;
    unsigned int sample_bits;

    /* parent platform/codec */
    struct snd_soc_codec *codec;           /* 係結的Codec */
    struct snd_soc_component *component;   /* 係結的platform */

    struct list_head list;
};

snd_soc_dai_driver代表一個DAI驅動, 其主要欄位如下

struct snd_soc_dai_driver {
    /* DAI描述 */
    const char *name;
    unsigned int id;
    unsigned int base;
    struct snd_soc_dobj dobj;

    /* DAI驅動回撥 */
    int (*probe)(struct snd_soc_dai *dai);
    int (*remove)(struct snd_soc_dai *dai);
    int (*suspend)(struct snd_soc_dai *dai);
    int (*resume)(struct snd_soc_dai *dai);
    /* compress dai */
    int (*compress_new)(struct snd_soc_pcm_runtime *rtd, int num);
    /* Optional Callback used at pcm creation*/
    int (*pcm_new)(struct snd_soc_pcm_runtime *rtd,
               struct snd_soc_dai *dai);
    /* DAI is also used for the control bus */
    bool bus_control;

    /* 操作集 */
    const struct snd_soc_dai_ops *ops;
    const struct snd_soc_cdai_ops *cops;

    /* DAI能力 */
    struct snd_soc_pcm_stream capture;
    struct snd_soc_pcm_stream playback;
    unsigned int symmetric_rates:1;
    unsigned int symmetric_channels:1;
    unsigned int symmetric_samplebits:1;

    /* probe ordering - for components with runtime dependencies */
    int probe_order;
    int remove_order;
};

4.2.2 介面

int snd_soc_register_codec(struct device *dev, const struct snd_soc_codec_driver *, struct snd_soc_dai_driver *, int num_dai);

snd_soc_register_codec完成了如下事宜
1. 分配snd_soc_codec空間
2. 呼叫snd_soc_component_initialize()初始化snd_soc_codec::snd_soc_component
3. snd_soc_codec::snd_soc_component操作集初始化
4. DAPM相關初始化
5. 呼叫snd_soc_register_dais()註冊num_dai個DAI
6. 將該Codec新增至全域性Codec連結串列codec_list中

4.2.3 實現

Codec的一般實現步驟如下
1. 獲取Codec裝置資源
2. 實現snd_soc_codec_driver結構體
3. 實現snd_soc_dai_driver結構體
4. 實現snd_soc_dai_ops結構體, 並賦值給snd_soc_dai_driver::ops
5. 呼叫snd_soc_register_codec()註冊Codec

4.3 Platform

Platform驅動可分為三個部分: 音訊DMA驅動、SoC DAI驅動和DSP驅動
這些驅動程式碼應該只和SoC CPU有關而和Board無關

FIXME: Later

4.4 Machine

Machine/Board驅動用來將所有的部件驅動(Codecs、Platforms和DAIs)進行關聯

FIXME: Later


IT145.com E-mail:sddin#qq.com