天天看點

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

1. Overview

硬體平台及軟體版本:

  • Kernel - 3.4.5
  • SoC - Samsung exynos
  • CODEC - WM8994
  • Machine - goni_wm8994
  • Userspace - tinyalsa

Linux ALSA 音頻系統架構大緻如下:

+--------+  +--------+  +--------+
              |tinyplay|  |tinycap |  |tinymix |
              +--------+  +--------+  +--------+
                   |           ^           ^ 
                   V           |           V
              +--------------------------------+
              |        ALSA Library API        |
              |      (tinyalsa, alsa-lib)      |
              +--------------------------------+
  user space                   ^
-------------------------------|---------------------
  kernel space                 V
              +--------------------------------+
              |           ALSA CORE            |
              | +-------+ +-------+ +------+   |
              | |  PCM  | |CONTROL| | MIDI |...|
              | +-------+ +-------+ +------+   |
              +--------------------------------+
                               |
              +--------------------------------+
              |           ASoC CORE            |
              +--------------------------------+
                               |
              +--------------------------------+
              |        hardware driver         |
              |  +-------+ +--------+ +-----+  |
              |  |Machine| |Platform| |Codec|  |
              |  +-------+ +--------+ +-----+  |
              +--------------------------------+
           
  • Native ALSA Application:tinyplay/tinycap/tinymix,這些使用者程式直接調用 alsa 使用者庫接口來實作放音、錄音、控制
  • ALSA Library API:alsa 使用者庫接口,常見有 tinyalsa、alsa-lib
  • ALSA CORE:alsa 核心層,向上提供邏輯裝置(PCM/CTL/MIDI/TIMER/…)系統調用,向下驅動硬體裝置(Machine/I2S/DMA/CODEC)
  • ASoC CORE:asoc 是建立在标準 alsa core 基礎上,為了更好支援嵌入式系統和應用于移動裝置的音頻 codec 的一套軟體體系
  • Hardware Driver:音頻硬體裝置驅動,由三大部分組成,分别是 Machine、Platform、Codec

本主題不遵循自頂而下的原則,而先從硬體裝置驅動說起,畢竟這些是看得見摸得着聽得到的東西,容易對其有着直覺的了解。

//

// 聲明:本文由 http://blog.csdn.net/zyuanyun 原創,轉載請注明出處,謝謝!

//

ALSA/ASoC 中硬體裝置關系:

+------------------------------------------+
|                 Machine                  |
|  +--------------+      +--------------+  |
|  |   Platform   |      |     Codec    |  |
|  |              | I2S  |              |  |
|  |       cpu_dai|<---->|codec_dai     |  |
|  |              |      |              |  |
|  +--------------+      +--------------+  |
+------------------------------------------+
           
  • Platform:指某款 SoC 平台的音頻子產品,如 exynos、omap、qcom 等等。Platform 又可細分兩部分:
    • cpu dai:在嵌入式系統裡面通常指 SoC 的 I2S、PCM 總線控制器,負責把音頻資料從 I2S tx FIFO 搬運到 CODEC(這是回放的情形,錄制則方向相反)。cpu_dai 通過

      snd_soc_register_dai()

      來注冊。注:DAI 是 Digital Audio Interface 的簡稱,分為 cpu_dai 和 codec_dai,這兩者通過 I2S/PCM 總線連接配接;AIF 是 Audio Interface 的簡稱,嵌入式系統中一般是 I2S 和 PCM 接口。
    • pcm dma:負責把 dma buffer 中的音頻資料搬運到 I2S tx FIFO。值得留意的是:某些情形下是不需要 dma 操作的,比如 Modem 和 CODEC 直連,因為 Modem 本身已經把資料送到 FIFO 了,這時隻需啟動 codec_dai 接收資料即可;該情形下,Machine 驅動 dai_link 中需要設定

      .platform_name = "snd-soc-dummy",

      這是虛拟 dma 驅動,實作見

      sound/soc/soc-utils.c

      。音頻 dma 驅動通過

      snd_soc_register_platform()

      來注冊,故也常用 platform 來指代音頻 dma 驅動(這裡的 platform 需要與 SoC Platform 區分開)。
  • Codec:對于回放來說,userspace 送過來的音頻資料是經過采樣量化的數字信号,在 codec 經過 DAC 轉換成模拟信号然後輸出到外放或耳機,這樣我們就可以聽到聲音了。Codec 字面意思是編解碼器,但晶片裡面的功能部件很多,常見的有 AIF、DAC、ADC、Mixer、PGA、Line-in、Line-out,有些高端的 codec 晶片還有 EQ、DSP、SRC、DRC、AGC、Echo-Canceller、Noise-Suppression 等部件。
  • Machine:指某款機器,通過配置 dai_link 把 cpu_dai、codec_dai、modem_dai 各個音頻接口給鍊結成一條條音頻鍊路,然後注冊

    snd_soc_card

    。和上面兩個不一樣,Platform 和 CODEC 驅動一般是可以重用的,而 Machine 有它特定的硬體特性,幾乎是不可重用的。所謂的硬體特性指:SoC Platform 與 Codec 的差異;DAIs 之間的鍊結方式;通過某個 GPIO 打開 Amplifier;通過某個 GPIO 檢測耳機插拔;使用某個時鐘如 MCLK/External-OSC 作為 I2S、CODEC 的時鐘源等等。

從上面的描述來看,對于回放的情形,PCM 資料流向大緻是:

copy_from_user           DMA                 I2S           DAC
              ^                   ^                   ^             ^
+---------+   |    +----------+   |   +-----------+   |   +-----+   |   +------+
|userspace+-------->DMA Buffer+------->I2S TX FIFO+------->CODEC+------->SPK/HP|
+---------+        +----------+       +-----------+       +-----+       +------+
           

幾個音頻實體鍊路的概念:

dai_link:machine 驅動中定義的音頻資料鍊路,它指定鍊路用到的 codec、codec_dai、cpu_dai、platform。比如對于 goni_wm8994 平台的 media 鍊路:

codec="wm8994-codec"、codec_dai="wm8994-aif1"、cpu_dai="samsung-i2s"、platform="samsung-audio"

,這四者就構成了一條音頻資料鍊路用于多媒體聲音的回放和錄制。一個系統可能有多個音頻資料鍊路,比如 media 和 voice,是以可以定義多個 dai_link 。如 WM8994 的典型設計,有三個 dai_link,分别是

AP<>AIF1

的 “HIFI”(多媒體聲音鍊路),

BP<>AIF2

的 “Voice”(通話語音鍊路),以及

BT<>AIF3

(藍牙 SCO 語音鍊路)。

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

代碼如下:

static struct snd_soc_dai_link goni_dai[] = {
{
    .name = "WM8994",
    .stream_name = "WM8994 HiFi",
    .cpu_dai_name = "samsung-i2s.0",
    .codec_dai_name = "wm8994-aif1",
    .platform_name = "samsung-audio",
    .codec_name = "wm8994-codec.0-001a",
    .init = goni_wm8994_init,
    .ops = &goni_hifi_ops,
}, {
    .name = "WM8994 Voice",
    .stream_name = "Voice",
    .cpu_dai_name = "goni-voice-dai",
    .codec_dai_name = "wm8994-aif2",
    .codec_name = "wm8994-codec.0-001a",
    .ops = &goni_voice_ops,
},
};
           

hw constraints:指平台本身的硬體限制,如所能支援的通道數/采樣率/資料格式、DMA 支援的資料周期大小(period size)、周期次數(period count)等,通過

snd_pcm_hardware

結構體描述:

static const struct snd_pcm_hardware dma_hardware = {
    .info           = SNDRV_PCM_INFO_INTERLEAVED |
                    SNDRV_PCM_INFO_BLOCK_TRANSFER |
                    SNDRV_PCM_INFO_MMAP |
                    SNDRV_PCM_INFO_MMAP_VALID |
                    SNDRV_PCM_INFO_PAUSE |
                    SNDRV_PCM_INFO_RESUME,
    .formats        = SNDRV_PCM_FMTBIT_S16_LE |
                    SNDRV_PCM_FMTBIT_U16_LE |
                    SNDRV_PCM_FMTBIT_U8 |
                    SNDRV_PCM_FMTBIT_S8,
    .channels_min       = 2,
    .channels_max       = 2,
    .buffer_bytes_max   = 128*1024,
    .period_bytes_min   = PAGE_SIZE,
    .period_bytes_max   = PAGE_SIZE*2,
    .periods_min        = 2,
    .periods_max        = 128,
    .fifo_size      = 32,
};
           

hw params:使用者層設定的硬體參數,如 channels、sample rate、pcm format、period size、period count;這些參數受 hw constraints 限制。

sw params:使用者層設定的軟體參數,如 start threshold、stop threshold、silence threshold。

2. ASoC

ASoC:ALSA System on Chip,是建立在标準 ALSA 驅動之上,為了更好支援嵌入式系統和應用于移動裝置的音頻 codec 的一套軟體體系,它依賴于标準 ALSA 驅動架構。核心文檔

Documentation/alsa/soc/overview.txt

中詳細介紹了 ASoC 的設計初衷,這裡不一一引用,簡單陳述如下:

  • 獨立的 codec 驅動,标準的 ALSA 驅動架構裡面 codec 驅動往往與 SoC/CPU 耦合過于緊密,不利于在多樣化的平台/機器上移植複用
  • 友善 codec 與 SoC 通過 PCM/I2S 總線建立連結
  • 動态音頻電源管理 DAPM,使得 codec 任何時候都工作在最低功耗狀态,同時負責音頻路由的建立
  • POPs 和 click 音抑制弱化處理,在 ASoC 中通過正确的音頻部件上下電次序來實作
  • Machine 驅動的特定控制,比如耳機、麥克風的插拔檢測,外放功放的開關

在概述中已經介紹了 ASoC 硬體裝置驅動的三大構成:Codec、Platform 和 Machine,下面列舉各驅動的功能構成:

ASoC Codec Driver:

  • Codec DAI 和 PCM 的配置資訊
  • Codec 的控制接口,如 I2C/SPI
  • Mixer 和其他音頻控件
  • Codec 的音頻接口函數,見

    snd_soc_dai_ops

    結構體定義
  • DAPM 描述資訊
  • DAPM 事件處理句柄
  • DAC 數字靜音控制

ASoC Platform Driver: 包括 dma 和 cpu_dai 兩部分:

  • dma 驅動實作音頻 dma 操作,具體見

    snd_pcm_ops

    結構體定義
  • cpu_dai 驅動實作音頻數字接口控制器的描述和配置

ASoC Machine Driver:

  • 作為鍊結 Platform 和 Codec 的載體,它必須配置 dai_link 為音頻資料鍊路指定 Platform 和 Codec
  • 處理機器特有的音頻控件和音頻事件,例如回放時打開外放功放

硬體裝置驅動相關結構體:

  • snd_soc_codec_driver:音頻編解碼晶片描述及操作函數,如控件/微件/音頻路由的描述資訊、時鐘配置、IO 控制等
  • snd_soc_dai_driver:音頻資料接口描述及操作函數,根據 codec 端和 soc 端,分為 codec_dai 和 cpu_dai
  • snd_soc_platform_driver:音頻 dma 裝置描述及操作函數
  • snd_soc_dai_link:音頻鍊路描述及闆級操作函數

下面是 goni_wm8994 類圖,從這個類圖中,我們可以大緻了解 goni_wm8994 整個音頻驅動組成:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

3. Codec

上一章提到 codec_drv 的幾個組成部分,下面逐一介紹,基本是以核心文檔

Documentation/sound/alsa/soc/codec.txt

中的内容為脈絡來分析的。Codec 的作用,之前已有描述,本章主要羅列下 Codec driver 中重要的資料結構及注冊流程。

我們先看看 Codec 的硬體框圖,以 WM8994 為例:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

其中有着各種功能部件,包括但不限于 :

Widget Description
ADC 把麥克風拾取的模拟信号轉換成數字信号
DAC 把音頻接口過來的數字信号轉換成模拟信号
AIF 音頻數字接口,用于 Codec 與其他器件(如AP、BB等)之間的資料傳輸
MIXER 混音器,把多路輸入信号混合成單路輸出
DRC 動态範圍調節
LHPF 高低通濾波

3.1. Codec DAI and PCM configuration

codec_dai 和 pcm 配置資訊通過結構體

snd_soc_dai_driver

描述,包括 dai 的能力描述和操作接口,

snd_soc_dai_driver

最終會被注冊到 soc-core 中。

/*
 * Digital Audio Interface Driver.
 *
 * Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97
 * operations and capabilities. Codec and platform drivers will register this
 * structure for every DAI they have.
 * This structure covers the clocking, formating and ALSA operations for each
 * interface.
 */
struct snd_soc_dai_driver {
    /* DAI description */
    const char *name;
    unsigned int id;
    int ac97_control;

    /* DAI driver callbacks */
    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);

    /* ops */
    const struct snd_soc_dai_ops *ops;

    /* DAI capabilities */
    struct snd_soc_pcm_stream capture;
    struct snd_soc_pcm_stream playback;
    unsigned int symmetric_rates:1;

    /* probe ordering - for components with runtime dependencies */
    int probe_order;
    int remove_order;
};
           
  • name:codec_dai 的名稱辨別,dai_link 通過配置 codec_dai_name 來找到對應的 codec_dai;
  • probe:codec_dai 的初始化函數,注冊聲霸卡時回調;
  • playback:回放能力描述,如回放裝置所支援的聲道數、采樣率、音頻格式;
  • capture:錄制能力描述,如錄制裝置所支援聲道數、采樣率、音頻格式;
  • ops:codec_dai 的操作函數集,這些函數集非常重要,用于 dai 的時鐘配置、格式配置、硬體參數配置。

例子,wm8994 有三個 dai,這裡隻列其一:

static const struct snd_soc_dai_ops wm8994_aif1_dai_ops = {
    .set_sysclk = wm8994_set_dai_sysclk,
    .set_fmt    = wm8994_set_dai_fmt,
    .hw_params  = wm8994_hw_params,
    .shutdown   = wm8994_aif_shutdown,
    .digital_mute   = wm8994_aif_mute,
    .set_pll    = wm8994_set_fll,
    .set_tristate   = wm8994_set_tristate,
};

static struct snd_soc_dai_driver wm8994_dai[] = {
    {
        .name = "wm8994-aif1",
        .id = 1,
        .playback = {
            .stream_name = "AIF1 Playback",
            .channels_min = 1,
            .channels_max = 2,
            .rates = WM8994_RATES,
            .formats = WM8994_FORMATS,
            .sig_bits = 24,
        },
        .capture = {
            .stream_name = "AIF1 Capture",
            .channels_min = 1,
            .channels_max = 2,
            .rates = WM8994_RATES,
            .formats = WM8994_FORMATS,
            .sig_bits = 24,
         },
        .ops = &wm8994_aif1_dai_ops,
    },
    // ......
           

3.2. Codec control IO

移動裝置的音頻 Codec,其控制接口一般是 I2C 或 SPI,控制接口用于讀寫 codec 的寄存器。在

snd_soc_codec_driver

結構體中,有如下字段描述 Codec 的控制接口:

/* codec IO */
    unsigned int (*read)(struct snd_soc_codec *, unsigned int);
    int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);
    int (*display_register)(struct snd_soc_codec *, char *,
                size_t, unsigned int);
    int (*volatile_register)(struct snd_soc_codec *, unsigned int);
    int (*readable_register)(struct snd_soc_codec *, unsigned int);
    int (*writable_register)(struct snd_soc_codec *, unsigned int);
    unsigned int reg_cache_size;
    short reg_cache_step;
    short reg_word_size;
    const void *reg_cache_default;
    short reg_access_size;
    const struct snd_soc_reg_access *reg_access_default;
    enum snd_soc_compress_type compress_type;
           
  • read:讀寄存器;
  • write:寫寄存器;
  • volatile_register:判斷指定的寄存器是否是 volatile 屬性;假如是,則讀取寄存器時不是讀 cache,而直接通路硬體;
  • readable_register:判斷指定的寄存器是否可讀;
  • reg_cache_default:寄存器的預設值;
  • reg_cache_size:預設的寄存器值數組大小;
  • reg_word_size:寄存器寬度。

在 Linux-3.4.5 中,很多 codec 的控制接口都改用 regmap 了。soc-core 中判斷是否用的是 regmap,如果是,則調用 regmap 接口,見如下函數:

int snd_soc_update_bits(struct snd_soc_codec *codec, unsigned short reg,
                unsigned int mask, unsigned int value)
{
    bool change;
    unsigned int old, new;
    int ret;

    if (codec->using_regmap) {
        // 目前使用 regmap,調用 regmap 接口,其中 codec->control_data 是 regmap 私有資料
        ret = regmap_update_bits_check(codec->control_data, reg,
                           mask, value, &change);
    } else {
        // 非 regmap,調用 snd_soc_codec_driver 實作的 read/write 回調
        ret = snd_soc_read(codec, reg);
        if (ret < 0)
            return ret;

        old = ret;
        new = (old & ~mask) | (value & mask);
        change = old != new;
        if (change)
            ret = snd_soc_write(codec, reg, new);
    }

    if (ret < 0)
        return ret;

    return change;
}
           

使用 regmap,使得控制接口抽象化,codec_drv 不用關心目前控制方式是什麼;regmap 線上調試目錄是

/sys/kernel/debug/regmap

。關于 wm8994 的 regmap 描述,請自行查閱

driver/mfd/wm8994-regmap.c

3.3. Mixers and audio controls

音頻控件多用于部件開關和音量的設定,音頻控件可通過

soc.h

中的宏來定義,例如單一型控件:

#define SOC_SINGLE(xname, reg, shift, max, invert) \
{   .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
    .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
    .put = snd_soc_put_volsw, \
    .private_value =  SOC_SINGLE_VALUE(reg, shift, max, invert) }
           

這種控件隻有一個設定量,一般用于部件開關。宏定義的參數說明:

  • xname:控件的名稱辨別;
  • reg:控件對應的寄存器位址;
  • shift:控件控制位在寄存器中的偏移;
  • max:控件設定值範圍;
  • invert:設定值是否取反。

其他類型控件類似,不一一介紹了。

上述隻是宏定義,音頻控件真正的結構是

snd_kcontrol_new

struct snd_kcontrol_new {
    snd_ctl_elem_iface_t iface; /* interface identifier */
    unsigned int device;        /* device/client number */
    unsigned int subdevice;     /* subdevice (substream) number */
    const unsigned char *name;  /* ASCII name of item */
    unsigned int index;     /* index of item */
    unsigned int access;        /* access rights */
    unsigned int count;     /* count of same elements */
    snd_kcontrol_info_t *info;
    snd_kcontrol_get_t *get;
    snd_kcontrol_put_t *put;
    union {
        snd_kcontrol_tlv_rw_t *c;
        const unsigned int *p;
    } tlv;
    unsigned long private_value;
};
           

Codec 初始化時,通過

snd_soc_add_codec_controls()

把所有定義好的音頻控件注冊到 alsa-core ,上層可以通過 tinymix、alsa_amixer 等工具檢視修改這些控件的設定。

3.4. Codec audio operations

Codec 音頻操作接口通過結構體

snd_soc_dai_ops

描述:

struct snd_soc_dai_ops {
    /*
     * DAI clocking configuration, all optional.
     * Called by soc_card drivers, normally in their hw_params.
     */
    int (*set_sysclk)(struct snd_soc_dai *dai,
        int clk_id, unsigned int freq, int dir);
    int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
        unsigned int freq_in, unsigned int freq_out);
    int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);

    /*
     * DAI format configuration
     * Called by soc_card drivers, normally in their hw_params.
     */
    int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);
    int (*set_tdm_slot)(struct snd_soc_dai *dai,
        unsigned int tx_mask, unsigned int rx_mask,
        int slots, int slot_width);
    int (*set_channel_map)(struct snd_soc_dai *dai,
        unsigned int tx_num, unsigned int *tx_slot,
        unsigned int rx_num, unsigned int *rx_slot);
    int (*set_tristate)(struct snd_soc_dai *dai, int tristate);

    /*
     * DAI digital mute - optional.
     * Called by soc-core to minimise any pops.
     */
    int (*digital_mute)(struct snd_soc_dai *dai, int mute);

    /*
     * ALSA PCM audio operations - all optional.
     * Called by soc-core during audio PCM operations.
     */
    int (*startup)(struct snd_pcm_substream *,
        struct snd_soc_dai *);
    void (*shutdown)(struct snd_pcm_substream *,
        struct snd_soc_dai *);
    int (*hw_params)(struct snd_pcm_substream *,
        struct snd_pcm_hw_params *, struct snd_soc_dai *);
    int (*hw_free)(struct snd_pcm_substream *,
        struct snd_soc_dai *);
    int (*prepare)(struct snd_pcm_substream *,
        struct snd_soc_dai *);
    int (*trigger)(struct snd_pcm_substream *, int,
        struct snd_soc_dai *);
    /*
     * For hardware based FIFO caused delay reporting.
     * Optional.
     */
    snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,
        struct snd_soc_dai *);
};
           

注釋比較詳細的了,Codec 音頻操作接口分為 5 大部分:時鐘配置、格式配置、數字靜音、PCM 音頻接口、FIFO 延遲。着重說下時鐘配置及格式配置接口:

  • set_sysclk:codec_dai 系統時鐘設定,當上層打開 pcm 裝置時,需要回調該接口設定 Codec 的系統時鐘,Codec 才能正常工作;
  • set_pll:Codec FLL 設定,Codec 一般接了一個 MCLK 輸入時鐘,回調該接口基于 MCLK 來産生 Codec FLL 時鐘,接着 codec_dai 的 sysclk、bclk、lrclk 均可從 FLL 分頻出來(假設 Codec 作為 master);
  • set_fmt:codec_dai 格式設定,具體見

    soc-dai.h

    • SND_SOC_DAIFMT_I2S

      :音頻資料是 I2S 格式,常用于多媒體音頻;
    • SND_SOC_DAIFMT_DSP_A

      :音頻資料是 PCM 格式,常用于通話語音;
    • SND_SOC_DAIFMT_CBM_CFM

      :Codec 作為 master,BCLK 和 LRCLK 由 Codec 提供;
    • SND_SOC_DAIFMT_CBS_CFS

      :Codec 作為 slave,BCLK 和 LRCLK 由 SoC/CPU 提供;
  • hw_params:codec_dai 硬體參數設定,根據上層設定的聲道數、采樣率、資料格式,來配置 codec_dai 相關寄存器。

以上接口一般在 Machine 驅動中回調,我們看看 Machine 驅動

goni_wm8994.c

goni_hifi_hw_params()

函數:

static int goni_hifi_hw_params(struct snd_pcm_substream *substream,
        struct snd_pcm_hw_params *params)
{
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
    struct snd_soc_dai *codec_dai = rtd->codec_dai;
    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
    unsigned int pll_out = 24000000; // 這是 MCLK 的時鐘頻率,Codec 的源時鐘
    int ret = 0;

    /* set the cpu DAI configuration */
    ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S |
            SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM);
    if (ret < 0)
        return ret;

    /* set codec DAI configuration */
    ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S |
            SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM);
    if (ret < 0)
        return ret;

    /* set the codec FLL */
    ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, 0, pll_out,
            params_rate(params) * 256);
    if (ret < 0)
        return ret;

    /* set the codec system clock */
    ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1,
            params_rate(params) * 256, SND_SOC_CLOCK_IN);
    if (ret < 0)
        return ret;

    return 0;
}
           

其中

snd_soc_dai_set_fmt()

實際上會調用 cpu_dai 或 codec_dai 的

set_fmt()

回調,

snd_soc_dai_set_pll()

snd_soc_dai_set_sysclk()

也類似。

  • MCLK 作為 Codec 的源時鐘,頻率為 24Mhz;
  • 設定 cpu_dai 和 codec_dai 格式:資料格式是 I2S;Codec 作為 master,BCLK 和 LRCLK 由 Codec 提供;
  • 設定 codec_dai 的 FLL1:時鐘源是 MCLK,時鐘源頻率是 24Mhz,目的時鐘頻率是 256fs(fs 是采樣頻率);
  • 設定 codec_dai 的系統時鐘:時鐘源是 FLL1,系統時鐘頻率是 256fs。

對于 dai(codec_dai 和 cpu_dai),都要非常留意時鐘設定,它很關鍵又複雜,設定錯誤将會導緻很多問題,典型如下:

  • 系統無聲:檢查 Codec 系統時鐘、codec_dai 位時鐘和幀時鐘是否使能;
  • 聲音失真:檢查音頻資料的采樣率是否和 codec_dai 幀時鐘一緻;
  • 斷續破音:檢查 Codec 系統時鐘和位時鐘、幀時鐘是否同步,出現這種情況,可能是因為 sysclk 和 BCLK/LRCLK 不是由同一個時鐘源分頻出來的。

如下是一個典型的音頻系統時鐘設定(Codec works as master mode):

+---------------------------------------------------------------------- -+
|                                 CODEC                                  |            +-----------+
|           +---------+                                                  |            |           |
| SLIMCLK+-->         |                                                  |            |           |
|           |         |                                                  |            |           |
| AIFnBCLK+->         |                                    +---------+   |            |           |
|           |         |   +-----+                          |         |   |            |           |
| AIFnLRCLK+> FLL_SRC +---> FLL +---+                      |         +---->AIFnBCLK+-->           |
|           |         |   +-----+   |                      |         |   |            |           |
|  MCLK1+--->         |             |       +---------+    |         +---->AIFnLRCLK+->           |
|   ^       |         |             +------->         |    |         |   |            | Processor |
|   | MCLK2+>         |                     |         |    |   AIFn  |   |            |           |
|   |  ^    +---------+           SLIMCLK+-->         |    |         |   |            |           |
|   |  |                                    |         +---->         <----+AIFnRX<----+           |
|   |  |                          AIFnBCLK+-> SYSCLK  |    |         |   |            |           |
|   |  |                                    |         |    |         +----+AIFnTX+---->           |
|   |  |                          MCLK1+---->         |    |         |   |            |           |
|   |  |                                    |         |    +---------+   |            |           |
|   |  |                          MCLK2+---->         |                  |            |           |
|   |  |                                    +---------+                  |            |           |
+------+-----------------------------------------------------------------+            +-----------+
    |
  +-+----------+
  | Oscillator |
  +------------+
                      AIF Master Mode, Using MCLK and FLL as Reference
           
  • MCLK1 由外部晶振提供時鐘
  • Codec FLL 選取 MCLK1 作為時鐘源,分出想要的時鐘頻率
  • SYSCLK 選取 FLL 作為時鐘源,産生系統時鐘
  • BCLK、LRCLK 由 Codec 産生提供,即 Codec 作為 Master

3.5. DAPM description

概念:Dynamic Audio Power Management,動态音頻電源管理,為移動 Linux 裝置設計,使得音頻系統任何時候都工作在最低功耗狀态。

目的:使能最少的必要的部件,令音頻系統正常工作。

原理:當音頻路徑發生改變(比如上層使用 tinymix 工具設定音頻通路)時,或發生資料流事件(比如啟動或停止播放)時,都會觸發 dapm 去周遊所有鄰近的音頻部件,檢查是否存在完整的音頻路徑(complete path:滿足條件的音頻路徑,該路徑上任意一個部件往前周遊能到達輸入端點如 DAC/Mic/Linein,往後周遊能到達輸出端點如 ADC/HP/SPK),如果存在完整的音頻路徑,則該路徑上面的所有部件都是需要上電的,其他部件則下電。

部件上下電都是 dapm 根據政策自主要制的,外部無法幹預,可以說 dapm 是一個專門為音頻系統設計的自成體系的電源管理子產品,獨立于 Linux 電源管理之外。即使 SoC 休眠了,Codec 仍可以在正常工作,試想下這個情景:語音通話,modem_dai 連接配接到 codec_dai,語音資料不經過 SoC,是以這種情形下 SoC 可以進入睡眠以降低功耗,隻保持 Codec 正常工作就行了。

dapm 原理及實作非常精妙,我認為是 ALSA/ASoC 中最值得鑽研的一個點了。

如下是多媒體外放回放通路:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

在這個例子中,codec 中的音頻通路是:

AIF1>DAC1>OUTMIXER>SPKOUT

;AIF1 是輸入端點,SPKOUT 是輸出端點,是以這條通路是一個 complete path,這通路上的所有部件都是需要上電的,與此同時,其他部件需要下電。

而音頻部件由于上下電瞬間的瞬态沖擊會産生爆破音,我們稱之為 POPs。POPs 是電氣特性,我們無法徹底消除,隻能硬體軟體上優化削弱到人耳辨識不出的程度。DAPM 中,部件的上下電有嚴格的順序以抑制爆破音,總的來說:上電次序是從輸入端點到輸出端點,下電次序是從輸出端點到輸入端點。

驅動中如何建立 dapm widget 和 dapm route?以 最典型的 mixer widget 為例:Mixes several analog signals into a single analog signal. 它可以把幾路模拟信号混合到一路輸出,如 WM8994 的 SPKMIXL:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

如圖,SPKMIXL 有 5 路輸入,分别是:MIXINL、IN1LP、DAC1L、DAC2L、MIXEROUTL,是以這裡可以構成 5 條通路。

  • 如下 5 個控件控制 SPKMIXL 輸入:
static const struct snd_kcontrol_new left_speaker_mixer[] = {
SOC_DAPM_SINGLE("DAC2 Switch", WM8994_SPEAKER_MIXER, 9, 1, 0),
SOC_DAPM_SINGLE("Input Switch", WM8994_SPEAKER_MIXER, 7, 1, 0),
SOC_DAPM_SINGLE("IN1LP Switch", WM8994_SPEAKER_MIXER, 5, 1, 0),
SOC_DAPM_SINGLE("Output Switch", WM8994_SPEAKER_MIXER, 3, 1, 0),
SOC_DAPM_SINGLE("DAC1 Switch", WM8994_SPEAKER_MIXER, 1, 1, 0),
};
           
  • 定義 SPKMIXL 的 dapm widget:
SND_SOC_DAPM_MIXER_E("SPKL", WM8994_POWER_MANAGEMENT_3, 8, 0,
             left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer),
             late_enable_ev, SND_SOC_DAPM_PRE_PMU),
           

留意

WM8994_POWER_MANAGEMENT_3

寄存器的 bit8 正是控制 SPKMIXL 上下電的。

  • 定義 SPKMIXL 相關路由:
static const struct snd_soc_dapm_route intercon[] = {
    // ...
    { "SPKL", "DAC1 Switch", "DAC1L" },
    { "SPKL", "DAC2 Switch", "DAC2L" },
           

最終上層會看到兩個控件:“SPKL DAC1 Switch”,“SPKL DAC2 Switch”;前者用于 “SPKL” 選中 “DAC1L” 作為輸入,後者用于 “SPKL” 選中 “DAC2L” 作為輸入。

但控件 “SPKLDAC1 Switch” 或 “SPKL DAC2 Switch” 的打開,不代表能使得 “SPKL” 上電。隻有當 “SPKL” 位于完整的音頻路徑中時,“SPKL” 才會上電。

3.6. Codec register

當 platform_driver:

static struct platform_driver wm8994_codec_driver = {
    .driver = {
        .name = "wm8994-codec",
        .owner = THIS_MODULE,
        .pm = &wm8994_pm_ops,
    },
    .probe = wm8994_probe,
    .remove = __devexit_p(wm8994_remove),
};
           

.name = "wm8994-codec"

的 platform_device(該 platform_device 在

driver/mfd/wm8994-core.c

中注冊)比對後,立即回調

wm8994_probe()

注冊 Codec:

static int __devinit wm8994_probe(struct platform_device *pdev)
{
    struct wm8994_priv *wm8994;

    wm8994 = devm_kzalloc(&pdev->dev, sizeof(struct wm8994_priv),
                  GFP_KERNEL);
    if (wm8994 == NULL)
        return -ENOMEM;
    platform_set_drvdata(pdev, wm8994);

    wm8994->wm8994 = dev_get_drvdata(pdev->dev.parent);
    wm8994->pdata = dev_get_platdata(pdev->dev.parent);

    return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_wm8994,
            wm8994_dai, ARRAY_SIZE(wm8994_dai));
}
           

snd_soc_register_codec

:将 codec_driver 和 codec_dai_driver 注冊到 soc-core。

/**
 * snd_soc_register_codec - Register a codec with the ASoC core
 *
 * @codec: codec to register
 */
int snd_soc_register_codec(struct device *dev,
               const struct snd_soc_codec_driver *codec_drv,
               struct snd_soc_dai_driver *dai_drv,
               int num_dai)
           
  • 建立一個

    snd_soc_codec

    執行個體,包含 codec_drv(

    snd_soc_dai_driver

    )相關資訊,封裝給 soc-core 使用,相關代碼段如下:
struct snd_soc_codec *codec;

    dev_dbg(dev, "codec register %s\n", dev_name(dev));

    codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);
    if (codec == NULL)
        return -ENOMEM;

    /* create CODEC component name */
    codec->name = fmt_single_name(dev, &codec->id);
    if (codec->name == NULL) {
        kfree(codec);
        return -ENOMEM;
    }

    // 初始化 Codec 的寄存器緩存配置及讀寫接口
    codec->write = codec_drv->write;
    codec->read = codec_drv->read;
    codec->volatile_register = codec_drv->volatile_register;
    codec->readable_register = codec_drv->readable_register;
    codec->writable_register = codec_drv->writable_register;
    codec->ignore_pmdown_time = codec_drv->ignore_pmdown_time;
    codec->dapm.bias_level = SND_SOC_BIAS_OFF;
    codec->dapm.dev = dev;
    codec->dapm.codec = codec;
    codec->dapm.seq_notifier = codec_drv->seq_notifier;
    codec->dapm.stream_event = codec_drv->stream_event;
    codec->dev = dev;
    codec->driver = codec_drv;
    codec->num_dai = num_dai;
    mutex_init(&codec->mutex);
           
  • 把以上 codec 執行個體插入到

    codec_list

    連結清單中(聲霸卡注冊時會周遊該連結清單,找到 dai_link 聲明的 codec 并綁定):
  • 把 codec_drv 中的

    snd_soc_dai_driver

    (wm8994 有 3 個 dai,分别是 aif1、aif2、aif3)注冊到 soc-core:
/* register any DAIs */
    if (num_dai) {
        ret = snd_soc_register_dais(dev, dai_drv, num_dai);
        if (ret < 0)
            goto fail;
    }
           

snd_soc_register_dais()

會把 dai 插入到

dai_list

連結清單中(聲霸卡注冊時會周遊該連結清單,找到 dai_link 聲明的 codec_dai 并綁定):

最後順便提下 codec 和 codec_dai 的差別:codec 指音頻晶片共有的部分,包括 codec 初始化函數、控制接口、寄存器緩存、控件、dapm 部件、音頻路由、偏置電壓設定函數等描述資訊;而 codec_dai 指 codec 上的音頻接口驅動描述,包括時鐘配置、格式配置、能力描述等等,各個接口的描述資訊不一定都是一緻的,是以每個音頻接口都有着各自的驅動描述。

我開始時認為:codec_dai 從屬于 codec,dai_link 沒有必要同時聲明 codec 和 codec_dai,應該可以實作codec_dai 就能找到它對應的父裝置 codec 的方法。後來想到系統上如果有兩個以上的 codec,而恰好不同 codec 上的 codec_dai 有重名的話,此時就必須同時聲明 codec 和 codec_dai 才能找到正确的音頻接口了。

4. Platform

概述中提到音頻 Platform 驅動主要用于音頻資料傳輸,這裡又細分為兩步:

  • 啟動 dma 裝置,把音頻資料從 dma buffer 搬運到 cpu_dai FIFO,這部分驅動用

    snd_soc_platform_driver

    描述,後面分析用 pcm_dma 指代它。
  • 啟動數字音頻接口控制器(I2S/PCM/AC97),把音頻資料從 cpu_dai FIFO 傳送到 codec_dai,這部分驅動用

    snd_soc_dai_driver

    描述,後面分析用 cpu_dai 指代它。

那麼 dma buffer 中的音頻資料從何而來?保留這個問題,在後面章節 pcm native 分析。

我們浏覽下 platform_drv 中的幾個重要結構體,其中淺藍色部分是 cpu_dai 相關的,淺綠色部分是 pcm_dma 相關的。

snd_soc_dai

是 cpu_dai 注冊時所建立的 dai 執行個體,

snd_soc_platform

是 pcm_dma 注冊時所建立的 platform 執行個體,這些執行個體友善 soc-core 管理。

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

4.1. cpu dai

一個典型的 I2S 總線控制器框圖:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine

各子產品描述如下,摘自 S3C44B0 的資料手冊:

Bus interface, register bank, and state machine(BRFC) - Bus interface logic and FIFO access are controlled by the state machine.

3-bit dual prescaler(IPSR) - One prescaler is used as the master clock generator of the IIS bus interface and the other is used as the external CODEC clock generator.

16-byte FIFOs(TXFIFO, RXFIFO) - In transmit data transfer, data are written to TXFIFO, and, in the receive data transfer, data are read from RXFIFO.

Master IISCLK generaor(SCLKG) - In master mode, serial bit clock is generated from the master clock.

Channel generator and state machine(CHNC) - IISCLK and IISLRCK are generated and controlled by the channel state machine.

16-bit shift register(SFTR) - Parallel data is shifted to serial data output in the transmit mode, and serial data input is shifted to parallel data in the receive mode.

再回顧下 I2S 總線協定,這是音頻驅動開發最基本的内容了:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine
  • BCLK:位時鐘,對應數字音頻的每一位資料;BCLK = 聲道數 * 采樣頻率 * 采樣位數;
  • LRCLK:幀時鐘,構成一個完整的聲音單元;雙聲道的情況下,LRCLK = 0 時表示是左聲道的資料,= 1 時表示是右聲道的資料;LRCLK = 采樣頻率;
  • DACDAT:下行資料;
  • ADCDAT:上行資料;
  • 資料的最高位總是出現在 LRCLK 跳變後的第 2 個 BCLK 脈沖處。

對于 cpu_dai 驅動,從上面的類圖我們可知,主要工作有:

  • 實作 dai 操作函數,見

    snd_soc_dai_ops

    定義,用于配置和操作音頻數字接口控制器,如時鐘配置

    set_sysclk()

    、格式配置

    set_fmt()

    、硬體參數配置

    hw_params()

    、啟動/停止資料傳輸

    trigger()

    等;
  • 實作

    probe

    函數(初始化)、

    remove

    函數(解除安裝)、

    suspend/resume

    函數(電源管理);
  • 初始化

    snd_soc_dai_driver

    執行個體,包括回放和錄制的能力描述、dai 操作函數集、

    probe/remove

    回調、電源管理相關的

    suspend/resume

    回調;
  • 通過

    snd_soc_register_dai()

    把初始化完成的

    snd_soc_dai_driver

    注冊到 soc-core:首先建立一個

    snd_soc_dai

    執行個體,然後把該

    snd_soc_dai

    執行個體插入到

    dai_list

    連結清單(聲霸卡注冊時會周遊該連結清單,找到 dai_link 聲明的 cpu_dai 并綁定)。
/**
 * snd_soc_register_dai - Register a DAI with the ASoC core
 *
 * @dai: DAI to register
 */
int snd_soc_register_dai(struct device *dev,
        struct snd_soc_dai_driver *dai_drv)
{
    struct snd_soc_dai *dai;

    dai = kzalloc(sizeof(struct snd_soc_dai), GFP_KERNEL);
    if (dai == NULL)
        return -ENOMEM;

    /* create DAI component name */
    dai->name = fmt_single_name(dev, &dai->id);
    if (dai->name == NULL) {
        kfree(dai);
        return -ENOMEM;
    }

    dai->dev = dev;
    dai->driver = dai_drv;
    if (!dai->driver->ops)
        dai->driver->ops = &null_dai_ops;

    mutex_lock(&client_mutex);
    list_add(&dai->list, &dai_list);
    mutex_unlock(&client_mutex);

    return 0;
}
           

dai 操作函數的實作是 cpu_dai 驅動的主體,需要配置好相關寄存器讓 I2S/PCM 總線控制器正常運轉,

snd_soc_dai_ops

字段的詳細說明見

3.4. Codec audio operations

章節。

cpu_dai 驅動應該算是這個系列中最簡單的一環,是以不多花費筆墨在這裡了。倒是某些平台上,dma 裝置資訊(總線位址、通道号、傳輸單元大小)是在這裡初始化的,這點要留意,這些 dma 裝置資訊在 pcm_dma 驅動中用到。以 Exynos 平台為例,代碼位置

sound/soc/samsung/i2s.c

Samsung Exynos 平台的音頻 dma 裝置資訊用

s3c_dma_params

結構體描述:

struct s3c_dma_params {
    struct s3c2410_dma_client *client;  /* stream identifier */
    int channel;                /* Channel ID */
    dma_addr_t dma_addr;
    int dma_size;           /* Size of the DMA transfer */
    unsigned ch;
    struct samsung_dma_ops *ops;
};
           
  • client:流辨別符
  • channel:通道号
  • dma_addr:裝置的總線位址,這裡通常指向 I2S tx FIFO 或 I2S rx FIFO 首位址
  • dma_size:dma 傳輸單元大小
  • ops:平台 dma 操作函數

sound/soc/samsung/i2s.c

中設定 dma 裝置資訊的相關代碼片段:

struct i2s_dai {
    // ...
    /* Driver for this DAI */
    struct snd_soc_dai_driver i2s_dai_drv;
    /* DMA parameters */
    struct s3c_dma_params dma_playback; // playback dma 描述資訊
    struct s3c_dma_params dma_capture;  // capture dma 描述資訊
    struct s3c_dma_params idma_playback;// playback idma 描述資訊,idma 僅用于回放,用于三星平台的 LPA(低功耗音頻)模式
    // ...
};

static __devinit int samsung_i2s_probe(struct platform_device *pdev)
{
    // ...
    // 從 platform_device 中取得 resource,得到 playback dma 通道号
    res = platform_get_resource(pdev, IORESOURCE_DMA, 0);
    if (!res) {
        dev_err(&pdev->dev, "Unable to get I2S-TX dma resource\n");
        return -ENXIO;
    }
    dma_pl_chan = res->start; // dma_pl_chan 中的 pl 是 playback 簡寫

    // 從 platform_device 中取得 resource,得到 capture dma 通道号
    res = platform_get_resource(pdev, IORESOURCE_DMA, 1);
    if (!res) {
        dev_err(&pdev->dev, "Unable to get I2S-RX dma resource\n");
        return -ENXIO;
    }
    dma_cp_chan = res->start; // dma_cp_chan 中的 cp 是 capture 的簡寫

    // 從 platform_device 中取得 resource,得到 playback idma 通道号
    res = platform_get_resource(pdev, IORESOURCE_DMA, 2);
    if (res)
        dma_pl_sec_chan = res->start;
    else
        dma_pl_sec_chan = 0;

    // 從 platform_device 中取得 resource,得到 I2S 的基位址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "Unable to get I2S SFR address\n");
        return -ENXIO;
    }

    if (!request_mem_region(res->start, resource_size(res),
                            "samsung-i2s")) {
        dev_err(&pdev->dev, "Unable to request SFR region\n");
        return -EBUSY;
    }
    regs_base = res->start;
    
    // ...
    pri_dai->dma_playback.dma_addr = regs_base + I2STXD; // 設定 playback dma 裝置位址為 I2S tx FIFO 位址
    pri_dai->dma_capture.dma_addr = regs_base + I2SRXD; // 設定 capture dma 裝置位址為 I2S rx FIFO 位址
    pri_dai->dma_playback.client =
        (struct s3c2410_dma_client *)&pri_dai->dma_playback;
    pri_dai->dma_capture.client =
        (struct s3c2410_dma_client *)&pri_dai->dma_capture;
    pri_dai->dma_playback.channel = dma_pl_chan; // 設定 playback dma 通道号
    pri_dai->dma_capture.channel = dma_cp_chan; // 設定 capture dma 通道号
    pri_dai->src_clk = i2s_cfg->src_clk;
    pri_dai->dma_playback.dma_size = 4; // 設定 playback dma 傳輸單元大小為 4 個位元組
    pri_dai->dma_capture.dma_size = 4; // 設定 capture dma 傳輸單元大小為 4 個位元組
           

我們再看看 Board 初始化時,如何設定這些 resource,檔案

arch/arm/mach-exynos/dev-audio.c

static struct resource exynos4_i2s0_resource[] = {
    [0] = {
        .start  = EXYNOS4_PA_I2S0, // start 字段儲存的是 I2S 基位址
        .end    = EXYNOS4_PA_I2S0 + 0x100 - 1,
        .flags  = IORESOURCE_MEM,  // 辨別為 MEM 資源
    },
    [1] = {
        .start  = DMACH_I2S0_TX,   // start 字段儲存的是用于回放的 dma 通道号
        .end    = DMACH_I2S0_TX,
        .flags  = IORESOURCE_DMA,  // 辨別為 DMA 資源
    },
    [2] = {
        .start  = DMACH_I2S0_RX,   // start 字段儲存的是用于錄制的 dma 通道号
        .end    = DMACH_I2S0_RX,
        .flags  = IORESOURCE_DMA,  // 辨別為 DMA 資源
    },
    [3] = {
        .start  = DMACH_I2S0S_TX,  // start 字段儲存的是用于回放的 idma 通道号
        .end    = DMACH_I2S0S_TX,
        .flags  = IORESOURCE_DMA,  // 辨別為 DMA 資源
    },
};

struct platform_device exynos4_device_i2s0 = {
    .name = "samsung-i2s", // platform_device 名稱辨別為 "samsung-i2s",與 i2s.c 中的samsung_i2s_driver 比對
    .id = 0,
    .num_resources = ARRAY_SIZE(exynos4_i2s0_resource),
    .resource = exynos4_i2s0_resource,
    .dev = {
        .platform_data = &i2sv5_pdata,
    },
};
           

當 samsung_i2s_driver 初始化時,通過

platform_get_resource()

函數來擷取 platform_device 聲明的 resource。

struct resource 結構中我們通常關心 start、end 和 flags 這 3 個字段,分别标明資源的開始值、結束值和類型。flags 可以為 IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORESOURCE_DMA 等。start、end 的含義會随着 flags 而變更,如當 flags 為 IORESOURCE_MEM 時,start、end 分别表示該 platform_device 占據的記憶體的開始位址和結束位址;當 flags 為 IORESOURCE_IRQ 時,start、end 分别表示該 platform_device 使用的中斷号的開始值和結束值,如果隻使用了 1 個中斷号,開始和結束值相同。對于同種類型的資源而言,可以有多份,譬如說某裝置占據了 2 個記憶體區域,則可以定義 2 個 IORESOURCE_MEM 資源。

摘自:http://21cnbao.blog.51cto.com/109393/337609

4.2. pcm dma

PCM 資料管理可以說是 ALSA 系統中最核心的部分,這部分的工作有兩個(回放情形):

  • copy_from_user

    把使用者态的音頻資料拷貝到 dma buffer 中;
  • 啟動 dma 裝置把音頻資料從 dma buffer 傳送到 I2S tx FIFO。

當資料送到 I2S tx FIFO 後,剩下的是啟動 I2S 控制器把資料傳送到 Codec,然後 DAC 把音頻數字信号轉換成模拟信号,再輸出到 SPK/HP。關于 I2S (cpu_dai) 和 Codec,在以上兩章已經描述清楚了。

為什麼要使用 dma 傳輸?兩個原因:首先在資料傳輸過程中,不需要 cpu 的參與,節省 cpu 的開銷;其次傳輸速度快,提高硬體裝置的吞吐量。對于 ARM,它不能直接把資料從 A 位址搬運到 B 位址,隻能把資料從 A 位址搬運到一個寄存器,然後再從這個寄存器搬運到 B 位址;而 dma 有突發(Burst)傳輸能力,這種模式下一次能傳輸幾個甚至十幾個位元組的資料,尤其适合大資料的高速傳輸。一個 dma 傳輸塊裡面,可以劃分為若幹個周期,每傳輸完一個周期産生一個中斷。

寫這個文檔的初衷是為了描述清楚 pcm 資料流向,這裡先剖析 pcm_dma 驅動,以便後面 pcm native 的分析。以 Exynos 平台為例,代碼位置

sound/soc/samsung/dma.c

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine
  • 淺綠色:pcm_dma 驅動共有的結構及接口定義
  • 淺灰色:samsung exynos 平台特有的實作
  • 淺紫色:pcm native 關鍵結構
  • 淺橙色:

    snd_soc_platform

    是pcm_dma 注冊時所建立的 platform 執行個體

snd_pcm_substream

是 pcm native 關鍵結構體,上圖可以看出這個結構體包含了音頻資料傳輸所需的重要資訊:pcm ops 操作函數集和 dma buffer。

我們先看看 dma 裝置相關的結構,對于回放來說,dma 裝置把記憶體緩沖區的音頻資料傳送到 I2S tx FIFO;對于錄制來說,dma 裝置把 I2S rx FIFO 的音頻資料傳送到記憶體緩存區。是以在 dma 裝置傳輸之前,必須确定 data buffer 和 I2S FIFO 的資訊。

snd_dma_buffer:資料緩存區,用于儲存從使用者态拷貝過來的音頻資料;包含 dma buffer 的實體首位址,虛拟首位址、大小等資訊;其中實體位址用于設定 dma 傳輸的源位址(回放情形)或目的位址(錄制情形),虛拟位址用于與使用者态之間的音頻資料拷貝。

s3c_dma_params:dma 裝置描述,包括裝置總線位址(回放情形下為 I2S tx FIFO 首位址,設定為 dma 傳輸的目的位址)、dma 通道号、dma 傳輸單元大小,這些資訊在

i2s.c

中初始化(具體見上一小節)。

runtime_data:dma 運作期資訊

  • state:記錄 dma 裝置狀态,啟動或停止;
  • dma_loaded:dma 裝載計數,每當啟動一次 dma 傳輸,該計數加一;每當完成一次 dma 傳輸,該計數減一;
  • dma_period:dma 周期資料大小;
  • dma_start:指向 dma buffer 實體首位址;
  • dma_pos:記錄 dma buffer 目前指針位置,當 dma 每傳輸一次,都會更新該指針;
  • dma_end:dma buffer 結束位置;
  • params:dma 裝置描述資訊,包括裝置總線位址、dma 通道号、傳輸單元大小。

4.2.1. pcm operations

操作函數的實作是本子產品的主體,見

snd_pcm_ops

結構體描述:

struct snd_pcm_ops {
    int (*open)(struct snd_pcm_substream *substream);
    int (*close)(struct snd_pcm_substream *substream);
    int (*ioctl)(struct snd_pcm_substream * substream,
             unsigned int cmd, void *arg);
    int (*hw_params)(struct snd_pcm_substream *substream,
             struct snd_pcm_hw_params *params);
    int (*hw_free)(struct snd_pcm_substream *substream);
    int (*prepare)(struct snd_pcm_substream *substream);
    int (*trigger)(struct snd_pcm_substream *substream, int cmd);
    snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
    int (*copy)(struct snd_pcm_substream *substream, int channel,
            snd_pcm_uframes_t pos,
            void __user *buf, snd_pcm_uframes_t count);
    int (*silence)(struct snd_pcm_substream *substream, int channel, 
               snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
    struct page *(*page)(struct snd_pcm_substream *substream,
                 unsigned long offset);
    int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
    int (*ack)(struct snd_pcm_substream *substream);
};
           

下面介紹幾個重要的接口:

  • open:打開 pcm 邏輯裝置時,回調該函數設定 dma 裝置的硬體限制;并申請一個私有結構,儲存 dma 裝置資源如通道号、傳輸單元、緩沖區資訊、IO 資訊等,儲存在

    runtime->private_data

    。代碼如下:
static const struct snd_pcm_hardware dma_hardware = {
    .info           = SNDRV_PCM_INFO_INTERLEAVED |
                    SNDRV_PCM_INFO_BLOCK_TRANSFER |
                    SNDRV_PCM_INFO_MMAP |
                    SNDRV_PCM_INFO_MMAP_VALID |
                    SNDRV_PCM_INFO_PAUSE |
                    SNDRV_PCM_INFO_RESUME,
    .formats        = SNDRV_PCM_FMTBIT_S16_LE |
                    SNDRV_PCM_FMTBIT_U16_LE |
                    SNDRV_PCM_FMTBIT_U8 |
                    SNDRV_PCM_FMTBIT_S8,
    .channels_min       = 2,
    .channels_max       = 2,
    .buffer_bytes_max   = 128*1024,
    .period_bytes_min   = PAGE_SIZE,
    .period_bytes_max   = PAGE_SIZE*2,
    .periods_min        = 2,
    .periods_max        = 128,
    .fifo_size      = 32,
};

static int dma_open(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct runtime_data *prtd;

    pr_debug("Entered %s\n", __func__);

    // 設定 dma 裝置的硬體限制
    snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
    snd_soc_set_runtime_hwparams(substream, &dma_hardware);

    // 為 runtime_data 配置設定記憶體,用于儲存 dma 資源,包括緩沖區資訊、IO 裝置資訊、通道号、傳輸單元大小 
    prtd = kzalloc(sizeof(struct runtime_data), GFP_KERNEL);
    if (prtd == NULL)
        return -ENOMEM;

    spin_lock_init(&prtd->lock);

    // runtime 的私有資料指向 runtime_data 
    runtime->private_data = prtd;
    return 0;
}
           
  • hw_params:設定硬體參數時(

    cmd=SNDRV_PCM_IOCTL_HW_PARAMS

    ),回調該函數初始化 dma 資源,包括通道号、傳輸單元、緩沖區資訊、IO 裝置資訊等。代碼如下:
static int dma_hw_params(struct snd_pcm_substream *substream,
    struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct runtime_data *prtd = runtime->private_data;
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
    unsigned long totbytes = params_buffer_bytes(params); 
    struct s3c_dma_params *dma =
        snd_soc_dai_get_dma_data(rtd->cpu_dai, substream); // 從 cpu_dai 驅動 i2s.c 取得 dma 裝置資源
    struct samsung_dma_info dma_info;

    /* return if this is a bufferless transfer e.g.
     * codec <--> BT codec or GSM modem -- lg FIXME */
    if (!dma)
        return 0;

    /* this may get called several times by oss emulation
     * with different params -HW */
    if (prtd->params == NULL) {
        /* prepare DMA */
        prtd->params = dma; // 該字段儲存的是 dma 裝置資源,如 I2S tx FIFO 位址、dma 通道号、dma 傳輸單元等

        prtd->params->ops = samsung_dma_get_ops(); // 平台的 dma 操作函數,這些操作函數實作見:arch/arm/plat-samsung/dma-ops.c

        //...
        prtd->params->ch = prtd->params->ops->request(
                prtd->params->channel, &dma_info);
    }

    snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); // 這裡把 dma buffer 相關資訊賦給 substream runtime,注意 dma_buffer 在建立 pcm 邏輯裝置時配置設定

    runtime->dma_bytes = totbytes;

    spin_lock_irq(&prtd->lock);
    prtd->dma_loaded = 0;
    prtd->dma_period = params_period_bytes(params);
    prtd->dma_start = runtime->dma_addr; // dma buffer 實體首位址
    prtd->dma_pos = prtd->dma_start;
    prtd->dma_end = prtd->dma_start + totbytes;
    spin_unlock_irq(&prtd->lock);

    return 0;
}
           
  • prepare:當資料已準備好(

    cmd=SNDRV_PCM_IOCTL_PREPARE

    ),回調該函數告知 dma 裝置資料已就緒。代碼如下:
static int dma_prepare(struct snd_pcm_substream *substream)
{
    struct runtime_data *prtd = substream->runtime->private_data;
    int ret = 0;

    pr_debug("Entered %s\n", __func__);

    /* return if this is a bufferless transfer e.g.
     * codec <--> BT codec or GSM modem -- lg FIXME */
    if (!prtd->params)
        return 0;

    /* flush the DMA channel */
    prtd->params->ops->flush(prtd->params->ch);

    prtd->dma_loaded = 0; // 初始化 dma 裝載計數
    prtd->dma_pos = prtd->dma_start; // 設定 dma buffer 目前指針為 dma buffer 首位址

    /* enqueue dma buffers */
    dma_enqueue(substream); // 插入到 dma 傳輸隊列中

    return ret;
}
           

dma_enqueue()

函數,把目前 dma buffer 插入到 dma 傳輸隊列中。當觸發

trigger()

啟動 dma 裝置傳輸後,将會把 dma buffer 資料傳送到 FIFO(回放情形)。

注意:每次 dma 傳輸完一個周期的資料後,都要調用

snd_pcm_period_elapsed()

告知 pcm native 一個周期的資料已經傳送到 FIFO 上了,然後再次調用

dma_enqueue()

,dma 傳輸…如此循環,直到觸發

trigger()

停止 dma 傳輸。

  • trigger:資料傳送 開始/停止/暫停/恢複 時,回調該函數啟動或停止 dma 傳輸(當上層第一次調用

    pcm_write()

    時,觸發

    trigger()

    啟動 dma 傳輸;當上層調用

    pcm_stop()

    pcm_drop()

    時,觸發

    trigger()

    停止 dma 傳輸)。

    trigger()

    函數裡面的操作必須是原子的,不能調用可能睡眠的操作,并且應盡量簡單。代碼如下:
static int dma_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct runtime_data *prtd = substream->runtime->private_data;
    int ret = 0;

    pr_debug("Entered %s\n", __func__);

    spin_lock(&prtd->lock);

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        prtd->state |= ST_RUNNING;
        prtd->params->ops->trigger(prtd->params->ch); // 啟動 dma 傳輸
        break;

    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        prtd->state &= ~ST_RUNNING;
        prtd->params->ops->stop(prtd->params->ch); // 停止 dma 傳輸
        break;

    default:
        ret = -EINVAL;
        break;
    }

    spin_unlock(&prtd->lock);

    return ret;
}
           
  • pointer:dma 每完成一次傳輸,都會調用該函數獲得傳輸資料的目前位置,這樣 pcm native 可計算 dma buffer 指針位置及可用空間。該函數也是原子的。代碼如下:
static snd_pcm_uframes_t
dma_pointer(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct runtime_data *prtd = runtime->private_data;
    unsigned long res;

    res = prtd->dma_pos - prtd->dma_start; // 目前位置減去首位址,其實就是已傳輸資料的大小

    /* we seem to be getting the odd error from the pcm library due
     * to out-of-bounds pointers. this is maybe due to the dma engine
     * not having loaded the new values for the channel before being
     * called... (todo - fix )
     */
    if (res >= snd_pcm_lib_buffer_bytes(substream)) {
        if (res == snd_pcm_lib_buffer_bytes(substream))
            res = 0;
    }

    return bytes_to_frames(substream->runtime, res); // 機關轉化為 frames
}
           

4.2.2. dma buffer allocation

4.2.1. pcm operations

小節,數次提及 dma buffer,即 dma 資料緩沖區。dma buffer 的配置設定,一般發生在 pcm_dma 驅動初始化階段

probe()

或 pcm 邏輯裝置建立階段

pcm_new()

。代碼如下:

static int preallocate_dma_buffer(struct snd_pcm *pcm, int stream)
{
    struct snd_pcm_substream *substream = pcm->streams[stream].substream;
    struct snd_dma_buffer *buf = &substream->dma_buffer;
    size_t size = dma_hardware.buffer_bytes_max; // 緩沖區大小不得超過 hardware 中的buffer_bytes_max

    buf->dev.type = SNDRV_DMA_TYPE_DEV;
    buf->dev.dev = pcm->card->dev;
    buf->private_data = NULL;
    buf->area = dma_alloc_writecombine(pcm->card->dev, size,
                       &buf->addr, GFP_KERNEL); // area 字段是 dma buffer 虛拟首位址,addr 字段是 dma buffer 實體首位址
    if (!buf->area)
        return -ENOMEM;
    buf->bytes = size;
    return 0;
}

static int dma_new(struct snd_soc_pcm_runtime *rtd)
{
    struct snd_card *card = rtd->card->snd_card;
    struct snd_pcm *pcm = rtd->pcm;
    int ret = 0;

    if (!card->dev->dma_mask)
        card->dev->dma_mask = &dma_mask;
    if (!card->dev->coherent_dma_mask)
        card->dev->coherent_dma_mask = DMA_BIT_MASK(32);

    if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
        ret = preallocate_dma_buffer(pcm, // 為回放子流配置設定的 dma buffer
            SNDRV_PCM_STREAM_PLAYBACK);
        if (ret)
            goto out;
    }

    if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
        ret = preallocate_dma_buffer(pcm, // 為錄制子流配置設定的 dma buffer
            SNDRV_PCM_STREAM_CAPTURE);
        if (ret)
            goto out;
    }
out:
    return ret;
}

static struct snd_soc_platform_driver samsung_asoc_platform = {
    .ops        = &dma_ops,
    .pcm_new    = dma_new,
    .pcm_free   = dma_free_dma_buffers,
};
           

當 soc-core 調用

soc_new_pcm()

建立 pcm 邏輯裝置時,會回調

pcm_new()

完成 dma buffer 記憶體配置設定,注意回放子流和錄制子流有着各自的 dma buffer。

4.2.3. pcm dma register

上兩個小節,我們介紹了 pcm_dma 接口函數的作用及實作和 dma buffer 的配置設定,本小節分析 pcm_dma 注冊過程。

當 platform_driver:

static struct platform_driver asoc_dma_driver = {
    .driver = {
        .name = "samsung-audio",
        .owner = THIS_MODULE,
    },
    .probe = samsung_asoc_platform_probe,
    .remove = __devexit_p(samsung_asoc_platform_remove),
};
           

.name = "samsung-audio"

的 platform_device(該 platform_device 在

arch/arm/plat-samsung/devs.c

中注冊)比對後,系統會回調

samsung_asoc_platform_probe()

注冊 platform:

static struct snd_soc_platform_driver samsung_asoc_platform = {
    .ops        = &dma_ops,
    .pcm_new    = dma_new,
    .pcm_free   = dma_free_dma_buffers,
};

static int __devinit samsung_asoc_platform_probe(struct platform_device *pdev)
{
    return snd_soc_register_platform(&pdev->dev, &samsung_asoc_platform);
}
           

snd_soc_register_platform:将 platform_drv 注冊到 soc-core。

  • 建立一個

    snd_soc_platform

    執行個體,包含 platform_drv(

    snd_soc_platform_driver

    )的相關資訊,封裝給 soc-core 使用;
  • 把以上建立的 platform 執行個體插入到

    platform_list

    連結清單上(聲霸卡注冊時會周遊該連結清單,找到 dai_link 聲明的 platform 并綁定)。

代碼實作:

/**
 * snd_soc_register_platform - Register a platform with the ASoC core
 *
 * @platform: platform to register
 */
int snd_soc_register_platform(struct device *dev,
        struct snd_soc_platform_driver *platform_drv)
{
    struct snd_soc_platform *platform;

    platform = kzalloc(sizeof(struct snd_soc_platform), GFP_KERNEL);
    if (platform == NULL)
        return -ENOMEM;

    /* create platform component name */
    platform->name = fmt_single_name(dev, &platform->id);
    if (platform->name == NULL) {
        kfree(platform);
        return -ENOMEM;
    }

    platform->dev = dev;
    platform->driver = platform_drv;
    platform->dapm.dev = dev;
    platform->dapm.platform = platform;
    platform->dapm.stream_event = platform_drv->stream_event;
    mutex_init(&platform->mutex);

    mutex_lock(&client_mutex);
    list_add(&platform->list, &platform_list);
    mutex_unlock(&client_mutex);

    return 0;
}
           

至此,完成了 Platform 驅動的實作。回放情形下,pcm_dma 裝置負責把 dma buffer 中的資料搬運到 I2S tx FIFO,I2S 總線控制器負責把 I2S tx FIFO 中的資料傳送到 Codec。至于 alsa 如何把音頻資料從 userspace 拷貝到 dma buffer,如何管理 dma buffer,如何啟動 I2S 和 DMA 傳輸,這裡面一環扣一環,見後續 pcm native 分析。

5. Machine

章節

3. Codec

4. Platform

介紹了 Codec、Platform 驅動,但僅有 Codec、Platform 驅動是不能工作的,需要一個角色把 codec、codec_dai、cpu_dai、platform 給鍊結起來才能構成一個完整的音頻鍊路,這個角色就由 machine_drv 承擔了。如下是一個典型的智能手機音頻框圖:

+------------+        +---------------------+        +------------+
|            |        |                     |        |            |
|            |        +        CODEC        +        |            |
|     AP     +------>AIF1                 AIF3+------>     PA     +->SPK
|            |        +   +-----+ +-----+   +        |            |
|            |        |   | DSP | | DAC |   |        |            |
+------------+        |   +-----+ +-----+   |        +------------+
                      |   +-----+ +-----+   |
                      |   | DSP | | DAC |   |
                      |   +-----+ +-----+   |
+------------+        |   +-----+ +-----+   |        +------------+
|            |        |   | DSP | | ADC |   |        |            |
|            |        +   +-----+ +-----+   +        |            |
|     BB     +------>AIF2 +-----+ +-----+ AIF4+------>    BTSCO   |
|            |        +   | DSP | | ADC |   +        |            |
|            |        |   +-----+ +-----+   |        |            |
+------------+        +----------+----------+        +------------+
                          |      |     |
                          +MIC   +HP   +EARP
           

組成了 4 個音頻鍊路(dai_link):

  • AP<>AIF1:AP(應用處理器)與 Codec 之間的鍊路,多媒體聲音
  • BB<>AIF2:BB(基帶處理器)與 Codec 之間的鍊路,通話語音
  • PA<>AIF3:PA(智能功率放大器)與 Codec 之間的鍊路,外放輸出
  • BTSCO<>AIF4:BTSCO(藍牙)與 Codec 之間的鍊路,藍牙耳機輸出

snd_soc_dai_link

結構體:

struct snd_soc_dai_link {
    /* config - must be set by machine driver */
    const char *name;           /* Codec name */
    const char *stream_name;        /* Stream name */
    const char *codec_name;     /* for multi-codec */
    const struct device_node *codec_of_node;
    const char *platform_name;  /* for multi-platform */
    const struct device_node *platform_of_node;
    const char *cpu_dai_name;
    const struct device_node *cpu_dai_of_node;
    const char *codec_dai_name;

    unsigned int dai_fmt;           /* format to set on init */

    /* Keep DAI active over suspend */
    unsigned int ignore_suspend:1;

    /* Symmetry requirements */
    unsigned int symmetric_rates:1;

    /* pmdown_time is ignored at stop */
    unsigned int ignore_pmdown_time:1;

    /* codec/machine specific init - e.g. add machine controls */
    int (*init)(struct snd_soc_pcm_runtime *rtd);

    /* machine stream operations */
    struct snd_soc_ops *ops;
};
           

注釋比較詳細,重點介紹如下幾個字段:

  • codec_name:音頻鍊路需要綁定的 codec 名稱,聲霸卡注冊時會周遊

    codec_list

    ,找到同名的 codec 并綁定;
  • platform_name:音頻鍊路需要綁定的 platform 名稱,聲霸卡注冊時會周遊

    platform_list

    ,找到同名的 platform 并綁定;
  • cpu_dai_name:音頻鍊路需要綁定的 cpu_dai 名稱,聲霸卡注冊時會周遊

    dai_list

    ,找到同名的 dai 并綁定;
  • codec_dai_name:音頻鍊路需要綁定的 codec_dai 名稱,聲霸卡注冊時會周遊

    dai_list

    ,找到同名的 dai 并綁定;
  • ops:重點留意 hw_params() 回調,一般來說這個回調是要實作的,用于配置 codec、codec_dai、cpu_dai 的資料格式和系統時鐘。在

    3.4. Codec audio operations

    小節中有描述。

goni_wm8994.c

中的 dai_link 定義,兩個音頻鍊路分别用于 Media 和 Voice:

static struct snd_soc_dai_link goni_dai[] = {
{
    .name = "WM8994",
    .stream_name = "WM8994 HiFi",
    .cpu_dai_name = "samsung-i2s.0",
    .codec_dai_name = "wm8994-aif1",
    .platform_name = "samsung-audio",
    .codec_name = "wm8994-codec.0-001a",
    .init = goni_wm8994_init,
    .ops = &goni_hifi_ops,
}, {
    .name = "WM8994 Voice",
    .stream_name = "Voice",
    .cpu_dai_name = "goni-voice-dai",
    .codec_dai_name = "wm8994-aif2",
    .codec_name = "wm8994-codec.0-001a",
    .ops = &goni_voice_ops,
},
};
           

除了 dai_link,機器中一些特定的音頻控件和音頻事件也可以在 machine_drv 定義,如耳機插拔檢測、外部功放打開關閉等。

我們再分析 machine_drv 初始化過程:

static struct snd_soc_card goni = {
    .name = "goni",
    .owner = THIS_MODULE,
    .dai_link = goni_dai,
    .num_links = ARRAY_SIZE(goni_dai),

    .dapm_widgets = goni_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(goni_dapm_widgets),
    .dapm_routes = goni_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(goni_dapm_routes),
};

static int __init goni_init(void)
{
    int ret;

    goni_snd_device = platform_device_alloc("soc-audio", -1);
    if (!goni_snd_device)
        return -ENOMEM;

    /* register voice DAI here */
    ret = snd_soc_register_dai(&goni_snd_device->dev, &voice_dai);
    if (ret) {
        platform_device_put(goni_snd_device);
        return ret;
    }

    platform_set_drvdata(goni_snd_device, &goni);
    ret = platform_device_add(goni_snd_device);

    if (ret) {
        snd_soc_unregister_dai(&goni_snd_device->dev);
        platform_device_put(goni_snd_device);
    }

    return ret;
}
           
  • 建立一個

    .name="soc-audio"

    的 platform_device 執行個體;
  • 設定 platform_device 的私有資料

    snd_soc_card

  • 然後注冊 platform_device 到系統中;
  • 再然後呢?好像沒有了…

但是真的沒有了嗎?别忘了,platform_device 還有個好夥伴 platform_driver 跟它配對。而

.name="soc-audio"

的 platform_driver 定義在

soc-core.c

中:

/* ASoC platform driver */
static struct platform_driver soc_driver = {
    .driver     = {
        .name       = "soc-audio",
        .owner      = THIS_MODULE,
        .pm     = &snd_soc_pm_ops,
    },
    .probe      = soc_probe,
    .remove     = soc_remove,
};

static int __init snd_soc_init(void)
{
    // ...
    snd_soc_util_init();
    return platform_driver_register(&soc_driver);
}
module_init(snd_soc_init);
           

兩者比對後,

soc_probe()

會被調用,繼而調用

snd_soc_register_card()

注冊聲霸卡。由于該過程很冗長,這裡不一一貼代碼分析了,但整個流程是比較簡單的,流程圖如下:

Linux ALSA 音頻系統:實體鍊路篇1. Overview2. ASoC3. Codec4. Platform5. Machine
  • 取出 platform_device 的私有資料,該私有資料就是

    snd_soc_card

  • snd_soc_register_card()

    為每個 dai_link 配置設定一個

    snd_soc_pcm_runtime

    執行個體,别忘了之前提過

    snd_soc_pcm_runtime

    是 ASoC 的橋梁,儲存着 codec、codec_dai、cpu_dai、platform 等硬體裝置執行個體。
  • 随後的工作都在

    snd_soc_instantiate_card()

    進行:
  • 周遊

    dai_list

    codec_list

    platform_list

    連結清單,為每個音頻鍊路找到對應的 cpu_dai、codec_dai、codec、platform;找到的 cpu_dai、codec_dai、codec、platform 儲存到

    snd_soc_pcm_runtime

    ,完成音頻鍊路的裝置綁定;
  • 調用

    snd_card_create()

    建立聲霸卡;
  • soc_probe_dai_link()

    依次回調 cpu_dai、codec、platform、codec_dai 的

    probe()

    函數,完成各音頻裝置的初始化,随後調用

    soc_new_pcm()

    建立 pcm 邏輯裝置(因為涉及到本系列的重點内容,後面具體分析這個函數);
  • 最後調用

    snd_card_register()

    注冊聲霸卡。

soc_new_pcm

源碼分析:

/* create a new pcm */
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
    struct snd_soc_codec *codec = rtd->codec;
    struct snd_soc_platform *platform = rtd->platform;
    struct snd_soc_dai *codec_dai = rtd->codec_dai;
    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
    struct snd_pcm_ops *soc_pcm_ops = &rtd->ops;
    struct snd_pcm *pcm;
    char new_name[64];
    int ret = 0, playback = 0, capture = 0;

    // 初始化 snd_soc_pcm_runtime 的 ops 字段,成員函數其實依次調用 machine、codec_dai、cpu_dai、platform 的回調;如 soc_pcm_hw_params:
    // |-> rtd->dai_link->ops->hw_params() 
    // |-> codec_dai->driver->ops->hw_params() 
    // |-> cpu_dai->driver->ops->hw_params()
    // |-> platform->driver->ops->hw_params()
    // 在這裡把底層硬體的操作接口抽象起來,pcm native 不用知道底層硬體細節
    soc_pcm_ops->open   = soc_pcm_open;
    soc_pcm_ops->close  = soc_pcm_close;
    soc_pcm_ops->hw_params  = soc_pcm_hw_params;
    soc_pcm_ops->hw_free    = soc_pcm_hw_free;
    soc_pcm_ops->prepare    = soc_pcm_prepare;
    soc_pcm_ops->trigger    = soc_pcm_trigger;
    soc_pcm_ops->pointer    = soc_pcm_pointer;

    /* check client and interface hw capabilities */
    snprintf(new_name, sizeof(new_name), "%s %s-%d",
            rtd->dai_link->stream_name, codec_dai->name, num);

    if (codec_dai->driver->playback.channels_min)
        playback = 1;
    if (codec_dai->driver->capture.channels_min)
        capture = 1;

    // 建立 pcm 邏輯裝置
    ret = snd_pcm_new(rtd->card->snd_card, new_name,
            num, playback, capture, &pcm);
    if (ret < 0) {
        printk(KERN_ERR "asoc: can't create pcm for codec %s\n", codec->name);
        return ret;
    }

    /* DAPM dai link stream work */
    INIT_DELAYED_WORK(&rtd->delayed_work, close_delayed_work);

    rtd->pcm = pcm;
    pcm->private_data = rtd; // pcm 的私有資料指向 snd_soc_pcm_runtime
    if (platform->driver->ops) {
        // 初始化 snd_soc_pcm_runtime 的 ops 字段,這些與 pcm_dma 操作相關,一般我們隻用留意 pointer 回調
        soc_pcm_ops->mmap = platform->driver->ops->mmap;
        soc_pcm_ops->pointer = platform->driver->ops->pointer;
        soc_pcm_ops->ioctl = platform->driver->ops->ioctl;
        soc_pcm_ops->copy = platform->driver->ops->copy;
        soc_pcm_ops->silence = platform->driver->ops->silence;
        soc_pcm_ops->ack = platform->driver->ops->ack;
        soc_pcm_ops->page = platform->driver->ops->page;
    }

    if (playback)
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, soc_pcm_ops); // 把 soc_pcm_ops 賦給 playback substream 的 ops 字段

    if (capture)
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, soc_pcm_ops); // 把 soc_pcm_ops 賦給 capture substream 的 ops 字段

    // 回調 dma 驅動的 pcm_new(),進行 dma buffer 記憶體配置設定和 dma 裝置初始化
    if (platform->driver->pcm_new) {
        ret = platform->driver->pcm_new(rtd);
        if (ret < 0) {
            pr_err("asoc: platform pcm constructor failed\n");
            return ret;
        }
    }

    pcm->private_free = platform->driver->pcm_free;
    printk(KERN_INFO "asoc: %s <-> %s mapping ok\n", codec_dai->name,
        cpu_dai->name);
    return ret;
}
           

可見

soc_new_pcm()

最主要的工作是建立 pcm 邏輯裝置,建立回放子流和錄制子流執行個體,并初始化回放子流和錄制子流的 pcm 操作函數(資料搬運時,需要調用這些函數來驅動 codec、codec_dai、cpu_dai、dma 裝置工作)。

繼續閱讀