理解ALSA:从零写ASoC驱动

Posted 快乐的池塘里面有只小青蛙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解ALSA:从零写ASoC驱动相关的知识,希望对你有一定的参考价值。

文章目录

1 最简单的ASoC声卡驱动

我买了一个AP处理器叫Tualcomm100和一个codec芯片叫BKM100。Tualcomm100音频接口只有一个标准的I2S,BKM100的音频接口也只有一个标准的I2S。输出是一个喇叭,输入是一个麦克风。PCB硬件连接如下。

                    ****************                         ***********
                    *              *                         * <------ * <---- MIC
 RAM <------T_PCM-> * <----------> * <-T_I2S---------B_I2S-> *         *
                    *              *                         * ------> * ----> SPEAKER
                    ****************                         ***********
                      Tualcomm100                              BKM100

那么,我要如何写一个ASoC的驱动来让它工作呢?

供应商Tualcomm已经提供了Tualcomm100的platform driver和cpu dai driver。

struct snd_soc_platform_driver tualcomm_soc_platform = 
        .ops            = &tualcomm_pcm_ops,
        .pcm_new        = tualcomm_pcm_new,
;

snd_soc_register_platform(&pdev->dev, &tualcomm_soc_platform);


struct snd_soc_dai_driver tualcomm_i2s_dai = 
        .name = "T_I2S"
        .probe = tualcomm_i2s_probe,
        .remove = tualcomm_i2s_remove,
        .suspend = tualcomm_i2s_suspend,
        .resume = tualcomm_i2s_resume,
        .playback = 
                .channels_min = 2,
                .channels_max = 2,
                .rates = TUALCOMM_I2S_RATES,
                .formats = SNDRV_PCM_FMTBIT_S16_LE,,
        .capture = 
                .channels_min = 2,
                .channels_max = 2,
                .rates = TUALCOMM_I2S_RATES,
                .formats = SNDRV_PCM_FMTBIT_S16_LE,,
        .ops = &tualcomm_i2s_dai_ops,
        .symmetric_rates = 1,
;

snd_soc_register_component(&pdev->dev, &tualcomm_i2s_component, &tualcomm_i2s_dai, 1);

供应商BKM也已经提供了BKM100的codec driver.

struct snd_soc_dai_driver bkm100_dai = 
        .name = "bkm100-i2s",
        .playback = 
                .stream_name = "Playback",
                .channels_min = 1,
                .channels_max = 2,
                .rates = BKM100_RATES,
                .formats = BKM100_FORMATS,,
        .capture = 
                .stream_name = "Capture",
                .channels_min = 1,
                .channels_max = 2,
                .rates = BKM100_RATES,
                .formats = BKM100_FORMATS,,
        .ops = &bkm100_dai_ops,
        .symmetric_rates = 1,
;

snd_soc_register_component(&dev, &soc_component_bkm100, &bkm100_dai, 1);

好的,我们需要做的就是创建一个machine driver把cpu和codec粘和起来。

/* A Minimal ASoC Sound Card - Single DAI Link */
struct snd_soc_dai_link machine_link = 
        .name = "SPK,MIC",
        .stream_name = "Tualcomm100 T_I2S B_I2S BKM100",
        .platform_name = "T_PCM",
        .cpu_dai_name = "T_I2S",
        .codec_dai_name = "B_I2S",
        .codec_name = "bkm100",
;

struct snd_soc_card card = 
        .dai_link = &machine_link,
        .num_links = 1,
;

snd_soc_register_card(&card);

开机,

$ ls /dev/snd/pcm*
pcmC0D0p pcmC0D0c
$ cat /proc/asound/pcm
00-00: SPK,MIC : Tualcomm100 T_I2S B_I2S BKM100 : playback 1 : capture 1

注:这里的打印格式为

printf("%02i-%02i: %s : %s", pcm->card->number, pcm->device, pcm->id, pcm->name);
printf(" : playback %i", pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream_count);
printf(" : capture %i", pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream_count);

完成了。让我们测一下:

tinyplay test.wav -D 0 -d 0

声音从喇叭放出来了!

tinycap cap.wav -D 0 -d 0

麦克风的声音被录到了文件cap.wav!

由于我的声卡只支持喇叭输出,在安静的环境下(例如图书馆)这会打扰到其他人。我需要让code支持耳机输出。我在网上找了一下,发现BKM刚刚发布了新产品BKM200,它支持喇叭输出,耳机输出和麦克风输入。这不正是我要的吗!跟BKM100比较,BKM200增加了第二组I2S接口用来输出声音到耳机。

                    ****************                         ***********
                    *              *                         * <------ * <---- MIC
 RAM <------T_PCM-> * <----------> * <-T_I2S--------B_I2S1-> *         *
                    *              *            |            * ------> * ----> SPEAKER
                    ****************            |            *         *
                      Tualcomm100               |            *         *
                                                 ---B_I2S2-> * ------> * ----> HEADPHONE
                                                             ***********
                                                               BKM200

供应商BKM同样提供了BKM200的codec driver。

struct snd_soc_dai_driver bkm200_dais[] = 
        
                .name = "B_I2S1",
                .playback = 
                        .stream_name = "Playback",
                        .channels_min = 1,
                        .channels_max = 2,
                        .rates = BKM200_RATES,
                        .formats = BKM200_FORMATS,,
                .capture = 
                        .stream_name = "Capture",
                        .channels_min = 1,
                        .channels_max = 1,
                        .rates = BKM200_RATES,
                        .formats = BKM200_FORMATS,,
                .ops = &bkm200_dai_ops,
                .symmetric_rates = 1,
        ,
        
                .name = "B_I2S2",
                .playback = 
                        .stream_name = "Playback",
                        .channels_min = 1,
                        .channels_max = 2,
                        .rates = BKM200_RATES,
                        .formats = BKM200_FORMATS,,
                .ops = &bkm200_dai_ops,
        
;

snd_soc_register_component(&dev, &soc_component_bkm200, bkm200_dais, 2);

我的machine driver需要跟着修改:

struct snd_soc_dai_link machine_links[] = 
        
                .name = "SPK,MIC",
                .stream_name = "Tualcomm100 T_I2S B_I2S1 BKM200",
                .platform_name = "T_PCM",
                .cpu_dai_name = "T_I2S",
                .codec_dai_name = "B_I2S1",
                .codec_name = "bkm200",
        ,
        
                .name = "HEADPHONE",
                .stream_name = "Tualcomm100 T_I2S B_I2S2 BKM200",
                .platform_name = "T_PCM",
                .cpu_dai_name = "T_I2S",
                .codec_dai_name = "B_I2S2",
                .codec_name = "bkm200",
        ,
;

struct snd_soc_card card = 
        .dai_link = machine_links,
        .num_links = 2,
;

snd_soc_register_card(&card);

开机后:

$ ls /dev/snd/pcm*
pcmC0D0p pcmC0D0c pcmC0D1p
$ cat /proc/asound/pcm
00-00: SPK,MIC : Tualcomm100 T_I2S B_I2S1 BKM200 : playback 1 : capture 1
00-01: HEADPHONE : Tualcomm100 T_I2S B_I2S2 BKM200 : playback 1

完成了。让我们测一下:

tinyplay test.wav -D 0 -d 0

声音从喇叭放出来了。

tinycap cap.wav -D 0 -d 0

麦克风的声音被录到了文件cap.wav。

tinyplay test.wav -D 0 -d 1

声音从耳机放出来了!

成功了!

BKM公司在想,BKM200的成本比BKM100贵多了,因为增加了一组I2S接口。为了节省成本,BKM公司决定改进BKM200。他们移除了第二组I2S接口,在内部增加了一个’Playback Switch’的开关。默认情况此开关为’Off’,用户可以在运行时通过一个kcontrol设定把开关调至’Speaker’/‘Headphone’/'Off’三者之一。新产品BKM300诞生了,它跟BKM100的功能一样,但比BKM200便宜!

                    ****************                         ***********
                    *              *                         * <------ * <---- MIC
 RAM <------T_PCM-> * <----------> * <-T_I2S---------B_I2S-> *         *
                    *              *                         *   / --> * OFF
                    ****************                         * --  --> * ----> SPEAKER
                      Tualcomm100                            *     --> * ----> HEADPHONE
                                                             ***********
                                                               BKM300

BKM300的codec driver:

static struct snd_soc_dai_driver bkm300_dais[] = 
        
                .name = "B_I2S",
                .playback = 
                        .stream_name = "Playback",
                        .channels_min = 1,
                        .channels_max = 2,
                        .rates = BKM300_RATES,
                        .formats = BKM300_FORMATS,,
                .capture = 
                        .stream_name = "Capture",
                        .channels_min = 1,
                        .channels_max = 1,
                        .rates = BKM300_RATES,
                        .formats = BKM300_FORMATS,,
                .ops = &bkm300_dai_ops,
                .symmetric_rates = 1,
        

;

snd_soc_register_component(&dev, &soc_component_bkm300, bkm300_dais, 1);

struct snd_kcontrol_new bkm300_controls[] = 
        SOC_SINGLE_EXT("Playback Switch", SND_SOC_NOPM, 0,
        0xFFFF, 0, bkm300_playback_switch_get,
        bkm300_playback_switch_put),
;

对应地修改machine driver:

struct snd_soc_dai_link machine_links = 
        
                .name = "SPK,MIC,HEADPHONE",
                .stream_name = "Tualcomm100 T_I2S B_I2S BKM300",
                .platform_name = "T_PCM",
                .cpu_dai_name = "T_I2S",
                .codec_dai_name = "B_I2S",
                .codec_name = "bkm300",
        
;

struct snd_soc_card card = 
        .dai_link = machine_links,
        .num_links = 1,
;

snd_soc_register_card(&card);

开机后:

$ ls /dev/snd/pcm*
pcmC0D0p pcmC0D0c
$ cat /proc/asound/pcm
00-00: SPK,MIC,HEADPHONE : Tualcomm100 T_I2S B_I2S BKM300 : playback 1 : capture 1

完成了。让我们测一下:

$ tinyplay test.wav -D 0 -d 0
Error!

什么情况?

tinycap cap.wav -D 0 -d 0

麦克风声音可以被录成文件cap.wav。

原因是默认情况,BKM300里的开关是Off。需要把它设成’Speaker’或’Headphone’。

tinymix 'Playback Switch' 'Speaker'
tinyplay test.wav -D 0 -d 0

声音从喇叭输出了!

tinymix 'Playback Switch' 'Headphone'
tinyplay test.wav -D 0 -d 0

声音从耳机输出了!

$ tinymix 'Playback Switch' 'Off'
$ tinyplay test.wav -D 0 -d 0
Error!

跟预期一样。

2 DPCM(Dynamic PCM)和DAPM(Dynamic Audio Power Management)

前面解释了platform driver, cpu dai, codec dai, codec driver, machine driver等ASoC的概念,然而真实的手机音频系统会更复杂。假设Tualcomm800是一款应用在真实手机里的SoC,它拥有更多的PCM(T_PCM1 ~ T_PCM4)和更多的DAI(T_I2S1T_I2S2T_DAI1T_DAI2T_PDMT_DAI3)。

| Front End PCMs    |   SoC Chip   | Back End DAIs | Audio devices |

                    ****************
RAM <------T_PCM1-> *              * <-T_I2S1--------B_I2S2-> BKM200 Headphone
                    *              *
RAM <------T_PCM2-> *              * <-T_I2S2--------B_I2S1-> BKM200 Speaker
                    * Tualcomm800  *
RAM <------T_PCM3-> *              * <-T_DAI1-----MODEM_DAI-> MODEM
                    *              *
RAM <------T_PCM4-> *              * <-T_DAI2--------BT_DAI-> BT
                    *              *
                    *              * <-T_PDM1------DMIC_PDM-> DMIC
                    *              *
                    *              * <-T_DAI3--------FM_DAI-> FM
                    ****************

PCM能够跟DAI任意互连。

如果我们用前面章节同样的方式来写驱动,理论上可以,但是这样的话,pcm设备的排列组合就会非常多:

  • pcmC0D0p 会是 T_PCM1 —> T_I2S1
  • pcmC0D1p 会是 T_PCM1 —> T_I2S2
  • pcmC0D2p 会是 T_PCM1 —> T_DAI1
  • pcmC0D2c 会是 T_PCM1 <— T_DAI1
  • pcmC0D3p 会是 T_PCM1 —> T_DAI2
  • pcmC0D3c 会是 T_PCM1 <— T_DAI2
  • pcmC0D4p 会是 T_PCM1 <— T_PDM1
  • pcmC0D5p 会是 T_PCM1 <— T_DAI3
  • pcmC0D6p 会是 T_PCM2 —> T_I2S1
  • pcmC0D7p 会是 T_PCM2 —> T_I2S2
  • pcmC0D8p 会是 T_PCM2 —> T_DAI1
  • pcmC0D8c 会是 T_PCM2 <— T_DAI1
  • pcmC0D9p 会是 T_PCM2 —> T_DAI2
  • pcmC0D9c 会是 T_PCM2 <— T_DAI2
  • pcmC0D10p 会是 T_PCM2 <— T_PDM1
  • pcmC0D11p 会是 T_PCM2 <— T_DAI3

不仅如此,音频信号在不同的排列组合中切换需要让用户程序来做。例如,当系统正在通过pcmC0D0p (T_PCM1 —> T_I2S1)播放音乐到耳机,终端用户突然拔出了耳机插口,那么预期的行为应该是音乐从喇叭无缝地播放出来,但是按照现在的做法,用户层音乐播放器程序需要先停止pcmC0D0p播放,然后才可以打开pcmC0D1p去播放。这不是一个好的设计。

DPCM (Dynamic PCM)使得上述情况变得容易。

DPCM的设计要求驱动定义前端(front end)dai links后端(back end)dai links

struct snd_soc_dai_link machine_links[] = 
      
              .name = "Tualcomm PCM1",
              .stream_name = "Tualcomm PCM1",
              .cpu_dai_name = "T_PCM1",
              .platform_name = "tualcomm-pcm",
              .codec_name = "snd-soc-dummy",
              .codec_dai_name = "snd-soc-dummy-dai",
              .dynamic = 1,
              .trigger = SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_POST,
              .dpcm_playback = 1,
      ,
      .....< other FE and BE DAI links here >
;

static struct snd_soc_dai_link machine_links[] = 
      .....< FE DAI links here >
      
              .name = "Tualcomm I2S1",
              .cpu_dai_name = "T_I2S1",
              .platform_name = "snd-soc-dummy",
              .no_pcm = 1,
              .codec_name = "bkm200",
              .codec_dai_name = "B_I2S",
              .ignore_suspend = 1,
              .ignore_pmdown_time = 1,
              .be_hw_params_fixup = bkm200_i2s_fixup,
              .ops = &bkm200_ops,
              .dpcm_playback = 1,
              .dpcm_capture = 1,
      ,
      .....< other BE DAI links here >
;

这种设计下,pcm设备数目会等于前端dai link的数目。 这种情况下,

  • pcmC0D0p 代表前端 T_PCM1
  • pcmC0D0c 代表前端 T_PCM1
  • pcmC0D1p 代表前端 T_PCM2
  • pcmC0D1c 代表前端 T_PCM2
  • pcmC0D2p 代表前端 T_PCM3
  • pcmC0D2c 代表前端 T_PCM3
  • pcmC0D3p 代表前端 T_PCM4
  • pcmC0D3c 代表前端 T_PCM4

仅此而已。

那么当终端用户在音乐播放时突然拔掉耳机插头会发生什么呢?

首先,T_PCM1Headphone的播放会是这样:

                    ****************
RAM =======T_PCM1=> *              * ==T_I2S1========B_I2S2=> BKM200 Headphone
                    *              *
RAM <------T_PCM2-> *              * <-T_I2S2--------B_I2S1-> BKM200 Speaker
                    * Tualcomm800  *
RAM <------T_PCM3-> *              * <-T_DAI1-----MODEM_DAI-> MODEM
                    *              *
RAM <------T_PCM4-> *              * <-T_DAI2--------BT_DAI-> BT
                    *              *
                    *              * <-T_PDM1------DMIC_PDM-> DMIC
                    *              *
                    *              * <-T_DAI3--------FM_DAI-> FM
                    ****************

当耳机插头被移除时,音频路径变成了输出到喇叭:

                    ****************
RAM =======T_PCM1=> *              * <-T_I2S1--------B_I2S2-> BKM200 Headphone
                    *              *
RAM <------T_PCM2-> *              * ==T_I2S2========B_I2S1=> BKM200 Speaker
                    * Tualcomm800  *
RAM <------T_PCM3-> *              * <-T_DAI1-----MODEM_DAI-> MODEM
                    *              *
RAM <------T_PCM4-> *              * <-T_DAI2--------BT_DAI-> BT
                    *              *
                    *              * <-T_PDM1------DMIC_PDM-> DMIC
                    *              *
                    *              * <-T_DAI3--------FM_DAI-> FM
                    ****************

音频驱动会像下面的步骤来处理:

  1. Machine driver收到了插头移除的事件;
  2. Machine driver(或者audio HAL)关掉耳机路径;
  3. DPCM为耳机在T_I2S1上运行PCM的trigger(STOP)hw_free()shutdown()操作,因为现在这条路径正在被关掉;
  4. Machine driver(或者audio HAL)打开喇叭路径;
  5. DPCM为喇叭在T_I2S2上运行PCM的startup()hw_params()prepare()trigger(START)操作,因为这路径正在被打开。

在这个例子中,machine driver(或者用户空间的audio HAL)可以切换路由,DPCM会负责管理DAI上的PCM操作,既可以建立连接也可以断开连接。在此转换过程中,音频播不会停止。

如何打开/关闭一个具体的路径呢?platform driver经常会提供一些kcontrol让用户(machine driver或者audio HAL)来操作。例如:

tinymix 'Connection T_PCM1 to T_I2S1'
tinymix 'Connection T_PCM1 to T_I2S2'
tinymix 'Connection T_PCM1 to T_DAI1'
tinymix 'Connection T_PCM1 to T_DAI2'
tinymix 'Connection T_PCM1 to T_PDM1'
tinymix 'Connection T_PCM1 to T_DAI3'
tinymix 'Connection T_PCM2 to T_I2S1'
tinymix 'Connection T_PCM2 to T_I2S2'
tinymix 'Connection T_PCM2 to T_DAI1'
tinymix 'Connection T_PCM2 to T_DAI2'
tinymix 'Connection T_PCM2 to T_PDM1'
tinymix 'Connection T_PCM2 to T_DAI3'

上面的情况下,当用户(假设audio HAL)收到了插座拔掉的事件,他只需要做

tinymix 'Connection T_PCM1 to T_I2S1' 0
tinymix 'Connection T_PCM1 to T_I2S2' 1

所有事情会在内核自动做掉,用户空间使用pcmC0D0p的音乐播放器不会感知到此事件发生。

这里的kcontrol被设计得非常简单:当用户做tinymix 'Connection T_PCM1 to T_I2S2' 1时,从T_PCM1T_I2S2。然而实际上此路径会被设计得更复杂些以获得更多的功能。让我们考虑当两个用户正在同时分别播放音频到T_PCM1T_PCM2,他们都需要输出声音到喇叭,如何做呢?实际应用中,Tualcomm800已经提供了内部的混音器。

                    ****************
                    *              * <-T_I2S1--------B_I2S2-> BKM200 Headphone
RAM =======T_PCM1=> * ===          *
                    *    + ======> * ==T_I2S2========B_I2S1=> BKM200 Speaker
RAM =======T_PCM2=> * === MIXER    *
                    *              *
RAM <------T_PCM3-> *              * <-T_DAI1-----MODEM_DAI-> MODEM
                    * Tualcomm800  *
RAM <------T_PCM4-> *              * <-T_DAI2--------BT_DAI-> BT
                    *              *
                    *              * <-T_PDM1------DMIC_PDM-> DMIC
                    *              *
                    *              * <-T_DAI3--------FM_DAI-> FM
                    ****************

为了描述这两条路径T_PCM1 —> MIXER —> T_I2S2T_PCM2 —> MIXER —> T_I2S2,ASoC引入新概念:widgetroute.

$KERNEL/include/sound/soc-dapm.h

/* dapm widget types */
enum snd_soc_dapm_type 
        snd_soc_dapm_input = 0,        /* input pin */
        snd_soc_dapm_output,           /* output pin */
        snd_soc_dapm_mux,              /* selects 1 analog signal from many inputs */
        snd_soc_dapm_mixer,            /* mixes several analog signals together */
        snd_soc_dapm_mixer_named_ctl,  /* mixer with named controls */
        snd_soc_dapm_pga,              /* programmable gain/attenuation (volume) */
        snd_soc_dapm_out_drv,          /* output driver */
        snd_soc_dapm_adc,              /* analog to digital converter */
        snd_soc_dapm_dac,              /* digital to analog converter */
        snd_soc_dapm_micbias,          /* microphone bias (power) */
        snd_soc_dapm_mic,              /* microphone */
        snd_soc_dapm_hp,               /* headphones */
        snd_soc_dapm_spk,              /* speaker */
        snd_soc_dapm_line,             /* line input/output */
        snd_soc_dapm_switch,           /* analog switch */
        snd_soc_dapm_vmid,             /* codec bias/vmid - to minimise pops */
        snd_soc_dapm_pre,              /* machine specific pre widget - exec first */
        snd_soc_dapm_post,             /* machine specific post widget - exec last */
        snd_soc_dapm_supply,           /* power/clock supply */
        snd_soc_dapm_regulator_supply, /* external regulator */
        snd_soc_dapm_clock_supply,     /* external clock */
        snd_soc_dapm_aif_in,           /* audio interface input */
        snd_soc_dapm_aif_out,          /* audio interface output */
        snd_soc_dapm_siggen,           /* signal generator */
        snd_soc_dapm_dai_in,           /* link to DAI structure */
        snd_soc_dapm_dai_out,
        snd_soc_dapm_dai_link,         /* link between two DAI structures */
        snd_soc_dapm_kcontrol,         /* Auto-disabled kcontrol */
;

在我们这种情况,

  • T_PCM1是一个snd_soc_dapm_aif_in
  • T_PCM2是一个snd_soc_dapm_aif_in
  • MIXER是一个snd_soc_dapm_mixer
  • T_I2S2是一个snd_soc_dapm_aif_out

路径1会像是T_PCM1 —> MIXER —> T_I2S2

路径2会像是T_PCM2 —> MIXER —> T_I2S2

为了打开/关闭一个具体的路径,ASoC设计了一个kcontrol插在两个widget中间。

$KERNEL/include/sound/soc-dapm.h

/*
 * DAPM audio route definition.
 *
 * Defines an audio route originating at source via control and finishing
 * at sink.
 */
struct snd_soc_dapm_route 
        const char *sink;
        const char *control;
        const char *source;

        /* Note: currently only supported for links where source is a supply */
        int (*connected)(struct snd_soc_dapm_widget *source,
                         struct snd_soc_dapm_widget *sink);
;

路径1会像是T_PCM1kcontrol_a–> MIXERkcontrol_c–> T_I2S2

路径2会像是T_PCM2kcontrol_b–> MIXERkcontrol_c–> T_I2S2

驱动需要实现这些kcontrol来让用户连接/断开这两个widget。驱动也可以把这个kcontrol定义成NULL,让这两个widgets总是连接着的。运行时,当路径中所有的kcontrol都被用户空间的程序打开了,这条路径就被激活了。所有当用户程序A想要从pcmC0D0p到喇叭播放音频,他需要:

tinymix 'kcontrol_a' 1
tinymix 'kcontrol_c' 1
tinyplay music_a.wav -D 0 -d 0

同时,用户程序B想要从pcmC0D1p到喇叭播放音频,他需要:

tinymix 'kcontrol_b' 1
tinymix 'kcontrol_c' 1
tinyplay music_b.wav -D 0 -d 1

播放结束后,用户程序需要关闭相应的kcontrol来让路径处于非激活状态。当一个路径是激活的,这条路径上所有的widget就是打开的,功耗就会增加。当一个路径是非激活的,这条路径上所有的widget就会被关闭,那么功耗就会再降回来。在这种设计下,播放和录音活动总是以最小的功耗工作。这种设计就叫做DAPM (Dynamic Audio Power Management)。

3 实例:MSM8996播放和录音

3.1 播放

播放命令:

tinymix 'PRI_MI2S_RX Audio Mixer MultiMedia1' 1
tinyplay test.wav -D 0 -d 0

播放路径上的widget定义:

$KERNEL/sound/soc/qcom/qdsp6/q6routing.c

static const struct snd_soc_dapm_widget msm_qdsp6_widgets[] = 
        ...

        SND_SOC_DAPM_AIF_IN("MM_DL1", "MultiMedia1 Playback", 0, 0, 0, 0

Linux ALSA声卡驱动之五:移动设备中的ALSA(ASoC)

转自http://blog.csdn.net/droidphone/article/details/7165482

1.  ASoC的由来

ASoC--ALSA System on Chip ,是建立在标准ALSA驱动层上,为了更好地支持嵌入式处理器和移动设备中的音频Codec的一套软件体系。在ASoc出现之前,内核对于SoC中的音频已经有部分的支持,不过会有一些局限性:

  •    Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码的重复,例如,仅是wm8731的驱动,当时Linux中有分别针对4个平台的驱动代码。
  •    音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码进行重新对音频路劲进行配置。
  •   当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持通过改变过取样频率和偏置电流来达到省电的目的。

ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树中:sound/soc。ASoC不能单独存在,他只是建立在标准ALSA驱动上的一个它必须和标准的ALSA驱动框架相结合才能工作。

/********************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/********************************************************************************************/

2.  硬件架构

通常,就像软件领域里的抽象和重用一样,嵌入式设备的音频系统可以被划分为板载硬件(Machine)、Soc(Platform)、Codec三大部分,如下图所示:

技术分享图片

                                        图2.1  音频系统结构

  • Machine  是指某一款机器,可以是某款设备,某款开发板,又或者是某款智能手机,由此可以看出Machine几乎是不可重用的,每个Machine上的硬件实现可能都不一样,CPU不一样,Codec不一样,音频的输入、输出设备也不一样,Machine为CPU、Codec、输入输出设备提供了一个载体。
  • Platform  一般是指某一个SoC平台,比如pxaxxx,s3cxxxx,omapxxx等等,与音频相关的通常包含该SoC中的时钟、DMA、I2S、PCM等等,只要指定了SoC,那么我们可以认为它会有一个对应的Platform,它只与SoC相关,与Machine无关,这样我们就可以把Platform抽象出来,使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实际上,把Platform认为是某个SoC更好理解。
  • Codec  字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、A/D、Mixer、PA(功放),通常包含多种输入(Mic、Line-in、I2S、PCM)和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样,是可重用的部件,同一个Codec可以被不同的Machine使用。嵌入式Codec通常通过I2C对内部的寄存器进行控制。 

3.  软件架构

在软件层面,ASoC也把嵌入式设备的音频系统同样分为3大部分,Machine,Platform和Codec。

  • Codec驱动  ASoC中的一个重要设计原则就是要求Codec驱动是平台无关的,它包含了一些音频的控件(Controls),音频接口,DAMP(动态音频电源管理)的定义和某些Codec IO功能。为了保证硬件无关性,任何特定于平台和机器的代码都要移到Platform和Machine驱动中。所有的Codec驱动都要提供以下特性:
    • Codec DAI 和 PCM的配置信息;
    • Codec的IO控制方式(I2C,SPI等);
    • Mixer和其他的音频控件;
    • Codec的ALSA音频操作接口;

必要时,也可以提供以下功能:

    • DAPM描述信息;
    • DAPM事件处理程序;
    • DAC数字静音控制
  • Platform驱动  它包含了该SoC平台的音频DMA和音频接口的配置和控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关的代码。
  • Machine驱动  Machine驱动负责处理机器特有的一些控件和音频事件(例如,当播放音频时,需要先行打开一个放大器);单独的Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起才能完成整个设备的音频处理工作。

4.  数据结构

整个ASoC是由一些列数据结构组成,要搞清楚ASoC的工作机理,必须要理解这一系列数据结构之间的关系和作用,下面的关系图展示了ASoC中重要的数据结构之间的关联方式:

技术分享图片

                                                                                                      图4.1  Kernel-2.6.35-ASoC中各个结构的静态关系

 ASoC把声卡实现为一个Platform Device,然后利用Platform_device结构中的dev字段:dev.drvdata,它实际上指向一个snd_soc_device结构。可以认为snd_soc_device是整个ASoC数据结构的根本,由他开始,引出一系列的数据结构用于表述音频的各种特性和功能。snd_soc_device结构引出了snd_soc_card和soc_codec_device两个结构,然后snd_soc_card又引出了snd_soc_platform、snd_soc_dai_link和snd_soc_codec结构。如上所述,ASoC被划分为Machine、Platform和Codec三大部分,如果从这些数据结构看来,snd_codec_device和snd_soc_card代表着Machine驱动,snd_soc_platform则代表着Platform驱动,snd_soc_codec和soc_codec_device则代表了Codec驱动,而snd_soc_dai_link则负责连接Platform和Codec。

5.  3.0版内核对ASoC的改进

本来写这篇文章的时候参考的内核版本是2.6.35,不过有CSDN的朋友提出在内核版本3.0版本中,ASoC做了较大的变化。故特意下载了3.0的代码,发现确实有所变化,下面先贴出数据结构的静态关系图:

技术分享图片

                                                                                             图5.1   Kernel 3.0中的ASoC数据结构

由上图我们可以看出,3.0中的数据结构更为合理和清晰,取消了snd_soc_device结构,直接用snd_soc_card取代了它,并且强化了snd_soc_pcm_runtime的作用,同时还增加了另外两个数据结构snd_soc_codec_driver和snd_soc_platform_driver,用于明确代表Codec驱动和Platform驱动。

 

后续的章节中将会逐一介绍Machine和Platform以及Codec驱动的工作细节和关联。





以上是关于理解ALSA:从零写ASoC驱动的主要内容,如果未能解决你的问题,请参考以下文章

Linux音频驱动学习之:ASOC分析

Linux ALSA驱动之五:Linux ALSA驱动之Platform源码分析(基于Linux 5.18)

Linux audio驱动模型

Linux ALSA驱动之Platform源码分析(wm8350.c)

Linux ALSA驱动之Platform源码分析(wm8350.c)

Linux ALSA驱动之Platform源码分析(wm8350.c)