我对linux理解之alsa

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处: amingriyue.blog.chinaunix.net
------------------------------------------

我们以imx51为平台,去分析alsa的架构。
有两个文件跟平台具体相关的:
一个是跟cpu的音频接口相关的:sound/soc/imx/imx-3stack-wm8994.c;
另一个是跟codec芯片有关的:sound/soc/codecs/wm8994.c
我们先看imx-3stack-wm8994.c中的初始化:
static int __init imx_3stack_init(void)
{
    int ret;

    ret = platform_driver_register(&imx_3stack_wm8994_audio_driver);//注册audio接口驱动
    if (ret)
        return -ENOMEM;

    imx_3stack_snd_device = platform_device_alloc("soc-audio", 2);//名字是soc-audio
    if (!imx_3stack_snd_device)
        return -ENOMEM;
    platform_set_drvdata(imx_3stack_snd_device, &imx_3stack_snd_devdata);//设置data
    imx_3stack_snd_devdata.dev = &imx_3stack_snd_device->dev;
    ret = platform_device_add(imx_3stack_snd_device);//将与soc_core中的platform匹配
    if (ret)
        platform_device_put(imx_3stack_snd_device);

    return ret;
}

这里面主要有两个工作:
1,platform_driver_register(&imx_3stack_wm8994_audio_driver);//注册audio接口驱动
2,platform_device_add(imx_3stack_snd_device);//将与soc_core中的platform匹配

我们先分析第一个工作,platform_driver_register(&imx_3stack_wm8994_audio_driver):
imx_3stack_wm8994_audio_driver定义:
static struct platform_driver imx_3stack_wm8994_audio_driver = {
    .probe = imx_3stack_wm8994_probe,
    .remove = imx_3stack_wm8994_remove,
    .driver = {
           .name = "imx-3stack-wm8994",
           },
};
对应它的device:
static struct platform_device mxc_wm8994_device = {
    .name = "imx-3stack-wm8994",
};
mxc_register_device(&mxc_wm8994_device, &wm8994_data);//设置了mxc_wm8994_device的data了
wm8994_data定义:
static struct mxc_audio_platform_data wm8994_data = {
    .ssi_num = 1,
    .src_port = 2,
    .ext_port = 4,
    .hp_irq = IOMUX_TO_IRQ_V3(F101_HEADSET_DET),
    .vdda_reg = "VGEN3",
    .vddd_reg = "VIOHI",
    .vdda = 1800000,
    .vddd = 2775000,
    .sysclk =24000000,
    .hp_status = wm8994_headset_det_status,
    .amp_enable = mxc_wm8994_amp_enable,
    .init = mxc_wm8994_plat_init,
    .finit = mxc_wm8994_plat_finit,
};
这样我们确认系统中device跟imx_3stack_wm8994_audio_driver对应,那下面会执行probe函数:
imx_3stack_wm8994_probe:
static int __devinit imx_3stack_wm8994_probe(struct platform_device *pdev)
{
    struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
    struct imx_3stack_priv *priv = &card_priv;
    struct snd_soc_dai *wm8994_cpu_dai;
    int ret = 0;

    priv->pdev = pdev;

    imx_3stack_init_dam(plat->src_port, plat->ext_port);//初始化ssi和dai口子

    if (plat->src_port == 2)//由wm8994_data定义知道src_port=2
        wm8994_cpu_dai = imx_ssi_dai[2];
    else
        wm8994_cpu_dai = imx_ssi_dai[0];

    imx_3stack_dai.cpu_dai = wm8994_cpu_dai;//设定cpu的音频接口
    ret = driver_create_file(pdev->dev.driver, &driver_attr_headphone);//创建耳机的属性文件
    if (ret < 0) {
        pr_err("%s:failed to create driver_attr_headphone\n", __func__);
        goto sysfs_err;
    }
    if (plat->init && plat->init())//如果plat的init定义,则执行
        goto err_plat_init;
   
    if (plat->hp_status())//根据耳机的初始状态设定中断
        ret = request_irq(plat->hp_irq,
              imx_headphone_detect_handler,
              IRQ_TYPE_EDGE_RISING, pdev->name, priv);
    else
        ret = request_irq(plat->hp_irq,
              imx_headphone_detect_handler,
              IRQ_TYPE_EDGE_FALLING, pdev->name, priv);

    if (ret < 0) {
        pr_err("%s: request irq failed\n", __func__);
        goto err_card_reg;
    }
    priv->sysclk = plat->sysclk;

    /* The WM8994 has an internal reset that is deasserted 8 SYS_MCLK
       cycles after all power rails have been brought up. After this time
       communication can start */
    wm8994_jack_func = 1;
    wm8994_spk_func = 1;//默认是speaker功能
    wm8994_line_in_func = 0;
    wm8994_modem1_func = 0;
    wm8994_modem2_func = 0;
   
    wm8994_psdev = kzalloc(sizeof(struct switch_dev), GFP_KERNEL);//申请一个switch_dev空间
    if (wm8994_psdev == NULL) {
        ret = -ENOMEM;
        goto err_card_reg;
    }

    wm8994_psdev->name = "h2w";
    wm8994_psdev->print_name = h2w_print_name;

    ret = switch_dev_register(wm8994_psdev);//注册一个switch_dev
    if (ret < 0) {
        pr_err("%s:failed to register switch device\n", __func__);
        goto err_switchdev;
    }

    return 0;

 err_switchdev:
    kfree(wm8994_psdev);
err_card_reg:
    if (plat->finit)
        plat->finit();
err_plat_init:
    driver_remove_file(pdev->dev.driver, &driver_attr_headphone);
sysfs_err:
    return ret;
}

imx_3stack_init的第一个工作就结束了,下面我们看下第二个工作:platform_device_add(imx_3stack_snd_device),我们由
imx_3stack_snd_device组成过程知道,它的名字是"soc-audio",data是imx_3stack_snd_devdata。这个data是重中之重。
我们看下它的定义过程:
static struct snd_soc_device imx_3stack_snd_devdata = {
    .card = &snd_soc_card_imx_3stack,//1
    .codec_dev = &soc_codec_dev_wm8994,//2
};
(1)card定义如下:
static struct snd_soc_card snd_soc_card_imx_3stack = {
    .name = "imx-3stack",
    .platform = &imx_soc_platform,//1-1
    .dai_link = &imx_3stack_dai,//连接了cpu的dai和codec的dai,1-2
    .num_links = 1,
    .remove = imx_3stack_card_remove,
};
(1-1)
struct snd_soc_platform imx_soc_platform = {//主要是pcm的处理
    .name = "imx-audio",
    .pcm_ops = &imx_pcm_ops,//1-1-2
    .pcm_new = imx_pcm_new,
    .pcm_free = imx_pcm_free_dma_buffers,
};
(1-1-2)
struct snd_pcm_ops imx_pcm_ops = {//pcm的ops
    .open = imx_pcm_open,
    .close = imx_pcm_close,
    .ioctl = snd_pcm_lib_ioctl,
    .hw_params = imx_pcm_hw_params,
    .hw_free = imx_pcm_hw_free,
    .prepare = imx_pcm_prepare,
    .trigger = imx_pcm_trigger,
    .pointer = imx_pcm_pointer,
    .mmap = imx_pcm_mmap,
};
(1-2)
static struct snd_soc_dai_link imx_3stack_dai = {//连接codec和cpu
    .name = "WM8994",
    .stream_name = "WM8994",
    .codec_dai = &wm8994_dai,//1-2-1
    .init = imx_3stack_wm8994_init,
    .ops = &imx_3stack_ops,//1-2-2
};
在第一个工作中的probe函数里有这样的语句imx_3stack_dai.cpu_dai = wm8994_cpu_dai,它定义了dai_link的cpu_dai,我们放到1-3中分析。
(1-2-1)
struct snd_soc_dai wm8994_dai = {
    .name = "WM8994",
    .playback = {
             .stream_name = "Playback",
             .channels_min = 2,
             .channels_max = 2,
             .rates = WM8994_RATES,
             .formats = WM8994_FORMATS,
             },
    .capture = {
            .stream_name = "Capture",
            .channels_min = 1,
            .channels_max = 2,
            .rates = WM8994_RATES,
            .formats = WM8994_FORMATS,
            },
    .ops = &wm8994_ops,//1-2-1-1
    .symmetric_rates = 1,
};
(1-2-1-1),
struct snd_soc_dai_ops wm8994_ops = {
    .prepare = wm8994_pcm_prepare,
    .startup = wm8994_pcm_startup,
    .trigger = wm8994_pcm_trigger,
    .hw_free = wm8994_pcm_free,
    .shutdown = wm8994_pcm_shutdown,
    .hw_params = wm8994_pcm_hw_params,
    .digital_mute = wm8994_digital_mute,
    .set_fmt = wm8994_set_dai_fmt,
//    .set_pll = wm8994_set_fll,
    .set_sysclk    = wm8994_set_dai_sysclk,
};
(1-2-2)
static struct snd_soc_ops imx_3stack_ops = {
    .startup = imx_3stack_startup,
    .shutdown = imx_3stack_shutdown,
    .hw_params = imx_3stack_audio_hw_params,
};

(1-3)
imx_3stack_dai.cpu_dai = wm8994_cpu_dai,从第一个工作中分析知道它对应 imx_ssi_dai[2]。
这个imx_ssi_dai在sound/soc/imx/imx-ssi.c的probe函数里有赋值。

(2).codec_dev = &soc_codec_dev_wm8994:
定义在sound/soc/codecs/wm8994.c中:
struct snd_soc_codec_device soc_codec_dev_wm8994 = {
    .probe = wm8994_probe,
    .remove = wm8994_remove,
    .suspend = wm8994_suspend,
    .resume = wm8994_resume,
};

第2个工作platform_device_add(imx_3stack_snd_device)执行的时候它会寻找与自己名字相同的driver去匹配,然后去执行它们的probe探测函数。那么它对应的driver在哪呢?
我们通过名字"soc-audio"可以在sound/soc/soc-core.c中找到对应的驱动定义:
/* ASoC platform driver */
static struct platform_driver soc_driver = {
    .driver        = {
        .name        = "soc-audio",
        .owner        = THIS_MODULE,
    },
    .probe        = soc_probe,
    .remove        = soc_remove,
    .suspend        = soc_suspend,
    .resume         = soc_resume,
};
soc_probe:
static int soc_probe(struct platform_device *pdev)
{
    int ret = 0;
    struct snd_soc_device *socdev = platform_get_drvdata(pdev);//得到pdev的data,即imx_3stack_snd_devdata
    struct snd_soc_card *card = socdev->card;//从soc_device中得到card

    /* Bodge while we push things out of socdev */
    card->socdev = socdev;//在card中保存soc_device

    /* Bodge while we unpick instantiation */
    card->dev = &pdev->dev;
    ret = snd_soc_register_card(card);//注册card,添加到card列表里面
    if (ret != 0) {
        dev_err(&pdev->dev, "Failed to register card\n");
        return ret;
    }

    return 0;
}
我们看snd_soc_register_card(card):
static int snd_soc_register_card(struct snd_soc_card *card)
{
    if (!card->name || !card->dev)
        return -EINVAL;

    INIT_LIST_HEAD(&card->list);
    card->instantiated = 0;

    mutex_lock(&client_mutex);
    list_add(&card->list, &card_list);//添加到声卡列表
    snd_soc_instantiate_cards();//初始化声卡
    mutex_unlock(&client_mutex);

    dev_dbg(card->dev, "Registered card '%s'\n", card->name);

    return 0;
}
我们从函数定义中知道snd_soc_register_card主要就是将card添加到card_list中,然后执行snd_soc_instantiate_cards():
static void snd_soc_instantiate_cards(void)
{
    struct snd_soc_card *card;
    list_for_each_entry(card, &card_list, list) //对card列表中的每个声卡
        snd_soc_instantiate_card(card);//执行card初始化动作
}
对card_list中每一个card都执行snd_soc_instantiate_card(card):
static void snd_soc_instantiate_card(struct snd_soc_card *card)
{
    struct platform_device *pdev = container_of(card->dev,//对应soc_probe中的pdev->dev
                            struct platform_device,
                            dev);
    struct snd_soc_codec_device *codec_dev = card->socdev->codec_dev;//从soc_probe函数中分析知道对应soc_codec_dev_wm8994
    struct snd_soc_platform *platform;
    struct snd_soc_dai *dai;
    int i, found, ret, ac97;

    if (card->instantiated)//已经初始化则退出
        return;

    found = 0;
    list_for_each_entry(platform, &platform_list, list)//对platform列表中的每个platform
        if (card->platform == platform) {//对应imx_soc_platform,检查是否能找到,代表是否已经注册
            found = 1;
            break;
        }
    if (!found) {
        dev_dbg(card->dev, "Platform %s not registered\n",
            card->platform->name);
        return;
    }
    ac97 = 0;
    for (i = 0; i < card->num_links; i++) {//num_links=1
        found = 0;
        list_for_each_entry(dai, &dai_list, list) {
            if (card->dai_link[i].cpu_dai == dai) {//检查cpu dai是否注册,对应imx_ssi_dai[2]
                found = 1;
                break;
            }
        }
        if (!found) {
            dev_dbg(card->dev, "DAI %s not registered\n",
                card->dai_link[i].cpu_dai->name);
            return;
        }

        if (card->dai_link[i].cpu_dai->ac97_control)//i.mx51没定义
            ac97 = 1;
    }

    /* If we have AC97 in the system then don't wait for the
     * codec.  This will need revisiting if we have to handle
     * systems with mixed AC97 and non-AC97 parts.  Only check for
     * DAIs currently; we can't do this per link since some AC97
     * codecs have non-AC97 DAIs.
     */
    if (!ac97)
        for (i = 0; i < card->num_links; i++) {
            found = 0;
            list_for_each_entry(dai, &dai_list, list)
                if (card->dai_link[i].codec_dai == dai) {//检查codec dai,对应wm8994_dai
                    found = 1;
                    break;
                }
            if (!found) {
                dev_dbg(card->dev, "DAI %s not registered\n",
                    card->dai_link[i].codec_dai->name);
                return;
            }
        }

    /* Note that we do not current check for codec components */

    dev_dbg(card->dev, "All components present, instantiating\n");

    /* Found everything, bring it up */
    if (card->probe) {//在i.mx51中没有定义
        ret = card->probe(pdev);//执行声卡的probe函数
        if (ret < 0)
            return;
    }
    for (i = 0; i < card->num_links; i++) {
        struct snd_soc_dai *cpu_dai = card->dai_link[i].cpu_dai;
        if (cpu_dai->probe) {//这个有定义,在imx-ssi中,对应了imx_ssi_probe
            ret = cpu_dai->probe(pdev, cpu_dai);//执行probe函数
            if (ret < 0)
                goto cpu_dai_err;
        }
    }

    if (codec_dev->probe) {
        ret = codec_dev->probe(pdev);//对应wm8994_probe
        if (ret < 0)
            goto cpu_dai_err;
    }
    if (platform->probe) {//这里platform对应imx_soc_platform,没有定义probe
        ret = platform->probe(pdev);
        if (ret < 0)
            goto platform_err;
    }

    /* DAPM stream work */
    INIT_DELAYED_WORK(&card->delayed_work, close_delayed_work);
#ifdef CONFIG_PM
    /* deferred resume work */
    INIT_WORK(&card->deferred_resume_work, soc_resume_deferred);
#endif
    card->instantiated = 1;//已经初始化

    return;

platform_err:
    if (codec_dev->remove)
        codec_dev->remove(pdev);

cpu_dai_err:
    for (i--; i >= 0; i--) {
        struct snd_soc_dai *cpu_dai = card->dai_link[i].cpu_dai;
        if (cpu_dai->remove)
            cpu_dai->remove(pdev, cpu_dai);
    }

    if (card->remove)
        card->remove(pdev);
}
在这个函数中我们接触到platform_list和dai_list,这些list是asla中特有的。我们下面探寻一下他们形成的过程。
platform_list:
我们在sound/soc/imx/imx-pcm.c中找到imx_soc_platform定义:
struct snd_soc_platform imx_soc_platform = {
    .name = "imx-audio",
    .pcm_ops = &imx_pcm_ops,
    .pcm_new = imx_pcm_new,
    .pcm_free = imx_pcm_free_dma_buffers,
};
EXPORT_SYMBOL_GPL(imx_soc_platform);
在初始化函数里注册了:
static int __init imx_pcm_init(void)
{
    return snd_soc_register_platform(&imx_soc_platform);//注册了soc_platform,添加到platform列表里
}
我们看下snd_soc_register_platform(&imx_soc_platform):
int snd_soc_register_platform(struct snd_soc_platform *platform)
{
    if (!platform->name)
        return -EINVAL;
    INIT_LIST_HEAD(&platform->list);

    mutex_lock(&client_mutex);
    list_add(&platform->list, &platform_list);//添加到platform列表里面,这个platform_list是属于soc子系统的
    snd_soc_instantiate_cards();//初始化声卡,上面已分析
    mutex_unlock(&client_mutex);

    pr_debug("Registered platform '%s'\n", platform->name);

    return 0;
}
将imx_soc_platform添加到了platform_list里面了。

dai_list:
我们在sound/soc/imx/imx-ssi.c中找到imx_ssi_dai赋值过程。首先看它的初始化
static int __init imx_ssi_init(void)
{
    return platform_driver_register(&imx_ssi_driver);
}
static struct platform_driver imx_ssi_driver = {
    .probe = imx_ssi_dev_probe,
    .remove = __devexit_p(imx_ssi_dev_remove),
    .driver = {
           .name = "mxc_ssi",
           },
};
我们可以找到它的device定义:
struct platform_device mxc_ssi2_device = {
    .name = "mxc_ssi",
    .id = 1,
    .num_resources = ARRAY_SIZE(ssi2_resources),
    .resource = ssi2_resources,
};
这样它们匹配后将会执行probe函数:
static int imx_ssi_dev_probe(struct platform_device *pdev)
{
    int fifo0_channel = pdev->id * 2;
    struct snd_soc_dai *dai;
    struct imx_ssi *priv;
    int fifo, channel;
    struct resource *res;
    int ret;

    BUG_ON(fifo0_channel >= MAX_SSI_CHANNELS);

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENODEV;

    priv = kzalloc(sizeof(struct imx_ssi), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* Each SSI block has 2 fifos which share the same
       private data (struct imx_ssi) */
    priv->baseaddr = res->start;
    priv->ioaddr = ioremap(res->start, 0x5C);
    priv->irq = platform_get_irq(pdev, 0);
    priv->ssi_clk = clk_get(&pdev->dev, "ssi_clk");
    priv->pdev = pdev;

    for (fifo = 0; fifo < 2; fifo++) {
        channel = (pdev->id * 2) + fifo;

        dai = kzalloc(sizeof(struct snd_soc_dai), GFP_KERNEL);//申请snd_soc_dai空间
        if (IS_ERR(dai)) {
            ret = -ENOMEM;
            goto DAI_ERR;
        }

        dai->name = kasprintf(GFP_KERNEL, "imx-ssi-%d-%d",
                      pdev->id + 1, fifo);
        if (IS_ERR(dai->name)) {
            kfree(dai);
            ret = -ENOMEM;
            goto DAI_ERR;
        }
        //一些列初始化
        dai->probe = imx_ssi_probe;
        dai->suspend = imx_ssi_suspend;
        dai->remove = imx_ssi_remove;
        dai->resume = imx_ssi_resume;

        dai->playback.channels_min = 1;
        dai->playback.channels_max = 2;
        dai->playback.rates = IMX_SSI_RATES;
        dai->playback.formats = IMX_SSI_FORMATS;

        dai->capture.channels_min = 1;
        dai->capture.channels_max = 2;
        dai->capture.rates = IMX_SSI_RATES;
        dai->capture.formats = IMX_SSI_FORMATS;

        dai->ops = &imx_ssi_dai_ops;

        dai->private_data = priv;

        dai->id = channel;
        imx_ssi_dai[channel] = dai;//赋值给imx_ssi_dai,由上面知道,最后赋值给了imx_3stack_dai.cpu_dai,这就是cpu_dai的由来过程

        ret = snd_soc_register_dai(dai);//注册dai
        if (ret < 0) {
            kfree(dai->name);
            kfree(dai);
            goto DAI_ERR;
        }
    }

    return 0;

DAI_ERR:
    if (fifo == 1) {
        dai = imx_ssi_dai[fifo0_channel];
        snd_soc_unregister_dai(dai);
        kfree(dai->name);
        kfree(dai);
    }

    clk_put(priv->ssi_clk);
    iounmap(priv->ioaddr);
    kfree(priv);
    return ret;
}
我们看下snd_soc_register_dai(dai):
int snd_soc_register_dai(struct snd_soc_dai *dai)
{
    if (!dai->name)
        return -EINVAL;

    /* The device should become mandatory over time */
    if (!dai->dev)
        printk(KERN_WARNING "No device for DAI %s\n", dai->name);

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

    INIT_LIST_HEAD(&dai->list);//初始化dai列表

    mutex_lock(&client_mutex);
    list_add(&dai->list, &dai_list);//添加到dai列表里面
    snd_soc_instantiate_cards();//初始化,这个上面以分析
    mutex_unlock(&client_mutex);

    pr_debug("Registered DAI '%s'\n", dai->name);

    return 0;
}
我们看到cpu_dai添加到了alsa的dai_list里了。

到此为止我们主要分析了cpu端的音频初始化工作,下面我们看下codec端的初始化工作:

static const struct i2c_device_id wm8994_id[] = {
    {"wm8994_i2c", 0},
    {},
};

MODULE_DEVICE_TABLE(i2c, wm8994_id);

static struct i2c_driver wm8994_i2c_driver = {
    .driver = {
           .name = "wm8994_i2c",
           .owner = THIS_MODULE,
           },
    .probe = wm8994_i2c_probe,
    .remove = wm8994_i2c_remove,
    .id_table = wm8994_id,
};

static int __init wm8994_modinit(void)
{
    int ret;

    ret = i2c_add_driver(&wm8994_i2c_driver);
    if (ret != 0)
        pr_err("WM8994: Unable to register I2C driver: %d\n", ret);

    return ret;
}

在device定义里有:
static struct i2c_board_info mxc_i2c0_board_info[] __initdata = {
    {
     .type = "wm8994_i2c",
     .addr = 0x1a,
     .platform_data = &wm8994_data,
     },
......
}
i2c驱动和设备匹配成功后将执行probe函数:
static int wm8994_i2c_probe(struct i2c_client *client,
                    const struct i2c_device_id *id)
{
    struct wm8994_priv *wm8994;
    struct snd_soc_codec *codec;//编解码器结构
    struct mxc_audio_platform_data *plat = client->dev.platform_data;
    struct regulator *reg;
    int ret = 0;

    if (wm8994_codec) {
        dev_err(&client->dev,
            "Multiple WM8994 devices not supported\n");
        return -ENOMEM;
    }
    codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);
    if (codec == NULL)
        return -ENOMEM;
    wm8994 = kzalloc(sizeof(struct wm8994_priv), GFP_KERNEL);
    if (wm8994 == NULL) {
        kfree(codec);
        return -ENOMEM;
    }

    codec->private_data = wm8994;//赋值codec的私有数据
    mutex_init(&codec->mutex);
    INIT_LIST_HEAD(&codec->dapm_widgets);//dapm,动态音频电源管理
    INIT_LIST_HEAD(&codec->dapm_paths);

    i2c_set_clientdata(client, codec);//将codec赋值为client私有数据
    codec->control_data = client;//将client赋值给codec的control_data

    if (plat->vddio_reg) {
        reg = regulator_get(&client->dev, plat->vddio_reg);
        if (IS_ERR(reg))
            goto err_reg_vddio;
        wm8994->reg_vddio = reg;
    }
    if (plat->vdda_reg) {
        reg = regulator_get(&client->dev, plat->vdda_reg);
        if (IS_ERR(reg))
            goto err_reg_vdda;
        wm8994->reg_vdda = reg;
    }
    if (plat->vddd_reg) {
        reg = regulator_get(&client->dev, plat->vddd_reg);
        if (IS_ERR(reg))
            goto err_reg_vddd;
        wm8994->reg_vddd = reg;
    }

    if (wm8994->reg_vdda) {
        ret = regulator_set_voltage(wm8994->reg_vdda,
                        plat->vdda, plat->vdda);
        regulator_enable(wm8994->reg_vdda);
    }
    if (wm8994->reg_vddio) {
        regulator_set_voltage(wm8994->reg_vddio,
                      plat->vddio, plat->vddio);
        regulator_enable(wm8994->reg_vddio);
    }
    if (wm8994->reg_vddd) {
        regulator_set_voltage(wm8994->reg_vddd,
                      plat->vddd, plat->vddd);
        regulator_enable(wm8994->reg_vddd);
    }

    if (plat->amp_enable)
        plat->amp_enable(1);
    mdelay(10);
    wm8994_write(codec, WM8994_SOFTWARE_RESET, 0xFFFF);
    mdelay(50);
    wm8994->cur_mode = WM8994_MODE_NORMAL;
    wm8994->old_mode = MODE_END;
    wm8994->playback  = 0;
    wm8994->capture    = 0;
    wm8994->start_count = 0;
    wm8994->revision = wm8994_hw_read(codec, WM8994_SOFTWARE_RESET);
    printk("WM8994 revision %x\n", wm8994->revision);
    if (wm8994->revision == 0)
        goto err_codec_reg;
    //codec初始化工作
    codec->dev = &client->dev;
    codec->name = "WM8994";
    codec->owner = THIS_MODULE;
    codec->read = wm8994_read;
    codec->write = wm8994_write;
    codec->hw_write = (hw_write_t)i2c_master_send;
    codec->bias_level = SND_SOC_BIAS_OFF;
    codec->set_bias_level = wm8994_set_bias_level;
    codec->dai = &wm8994_dai;
    codec->num_dai = 1;
    codec->reg_cache_size = sizeof(wm8994_regs_default);
    codec->reg_cache_step = 2;
    codec->reg_cache = (void *)&wm8994_regs_default;

    wm8994_codec = codec;
    wm8994_dai.dev = &client->dev;

    ret = snd_soc_register_codec(codec);//添加到codec列表,然后初始化声卡
    if (ret != 0) {
        dev_err(codec->dev, "Failed to register codec: %d\n", ret);
        goto err_codec_reg;
    }

    ret = snd_soc_register_dai(&wm8994_dai);//添加到dai列表里,然后执行初始化声卡函数
    if (ret != 0) {
        dev_err(codec->dev, "Failed to register DAIs: %d\n", ret);
        goto err_codec_reg;
    }

    return 0;

err_codec_reg:
    if (wm8994->reg_vddd)
        regulator_put(wm8994->reg_vddd);
err_reg_vddd:
    if (wm8994->reg_vdda)
        regulator_put(wm8994->reg_vdda);
err_reg_vdda:
    if (wm8994->reg_vddio)
        regulator_put(wm8994->reg_vddio);
err_reg_vddio:
    kfree(wm8994);
    kfree(codec);
    return ret;
}
我们看下snd_soc_register_codec(codec):
int snd_soc_register_codec(struct snd_soc_codec *codec)
{
    int i;

    if (!codec->name)
        return -EINVAL;
    /* The device should become mandatory over time */
    if (!codec->dev)
        printk(KERN_WARNING "No device for codec %s\n", codec->name);

    INIT_LIST_HEAD(&codec->list);//初始化codec列表

    for (i = 0; i < codec->num_dai; i++) {
        fixup_codec_formats(&codec->dai[i].playback);
        fixup_codec_formats(&codec->dai[i].capture);
    }

    mutex_lock(&client_mutex);
    list_add(&codec->list, &codec_list);//添加到codec列表里面
    snd_soc_instantiate_cards();//初始化声卡,上面已做分析
    mutex_unlock(&client_mutex);

    pr_debug("Registered codec '%s'\n", codec->name);

    return 0;
}
我们再看下snd_soc_register_dai(&wm8994_dai):
int snd_soc_register_dai(struct snd_soc_dai *dai)
{
    if (!dai->name)
        return -EINVAL;

    /* The device should become mandatory over time */
    if (!dai->dev)
        printk(KERN_WARNING "No device for DAI %s\n", dai->name);

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

    INIT_LIST_HEAD(&dai->list);//初始化dai列表

    mutex_lock(&client_mutex);
    list_add(&dai->list, &dai_list);//添加到dai列表里面
    snd_soc_instantiate_cards();//初始化,上面已做分析
    mutex_unlock(&client_mutex);

    pr_debug("Registered DAI '%s'\n", dai->name);

    return 0;
}
我们看到codec的dai也添加到dai_list中了。

至此为止,cpu和codec的初始化工作都基本完成,下面才会真正执行上面分析的snd_soc_instantiate_cards(),我们再贴一遍,以便分析:
static void snd_soc_instantiate_cards(void)
{
    struct snd_soc_card *card;
    list_for_each_entry(card, &card_list, list) //对card列表中的每个声卡
        snd_soc_instantiate_card(card);//执行card初始化动作
}
对card_list中每一个card都执行snd_soc_instantiate_card(card):
static void snd_soc_instantiate_card(struct snd_soc_card *card)
{
    struct platform_device *pdev = container_of(card->dev,//对应soc_probe中的pdev->dev
                            struct platform_device,
                            dev);
    struct snd_soc_codec_device *codec_dev = card->socdev->codec_dev;//从soc_probe函数中分析知道对应soc_codec_dev_wm8994
    struct snd_soc_platform *platform;
    struct snd_soc_dai *dai;
    int i, found, ret, ac97;

    if (card->instantiated)//已经初始化则退出
        return;

    found = 0;
    list_for_each_entry(platform, &platform_list, list)//对platform列表中的每个platform
        if (card->platform == platform) {//对应imx_soc_platform,检查是否能找到,代表是否已经注册
            found = 1;
            break;
        }
    if (!found) {
        dev_dbg(card->dev, "Platform %s not registered\n",
            card->platform->name);
        return;
    }
    ac97 = 0;
    for (i = 0; i < card->num_links; i++) {//num_links=1
        found = 0;
        list_for_each_entry(dai, &dai_list, list) {
            if (card->dai_link[i].cpu_dai == dai) {//检查cpu dai是否注册,对应imx_ssi_dai[2]
                found = 1;
                break;
            }
        }
        if (!found) {
            dev_dbg(card->dev, "DAI %s not registered\n",
                card->dai_link[i].cpu_dai->name);
            return;
        }

        if (card->dai_link[i].cpu_dai->ac97_control)//i.mx51没定义
            ac97 = 1;
    }

    /* If we have AC97 in the system then don't wait for the
     * codec.  This will need revisiting if we have to handle
     * systems with mixed AC97 and non-AC97 parts.  Only check for
     * DAIs currently; we can't do this per link since some AC97
     * codecs have non-AC97 DAIs.
     */
    if (!ac97)
        for (i = 0; i < card->num_links; i++) {
            found = 0;
            list_for_each_entry(dai, &dai_list, list)
                if (card->dai_link[i].codec_dai == dai) {//检查codec dai,对应wm8994_dai
                    found = 1;
                    break;
                }
            if (!found) {
                dev_dbg(card->dev, "DAI %s not registered\n",
                    card->dai_link[i].codec_dai->name);
                return;
            }
        }

    /* Note that we do not current check for codec components */

    dev_dbg(card->dev, "All components present, instantiating\n");

    /* Found everything, bring it up */
    if (card->probe) {//在i.mx51中没有定义
        ret = card->probe(pdev);//执行声卡的probe函数
        if (ret < 0)
            return;
    }
    for (i = 0; i < card->num_links; i++) {
        struct snd_soc_dai *cpu_dai = card->dai_link[i].cpu_dai;
        if (cpu_dai->probe) {//这个有定义,在imx-ssi中,对应了imx_ssi_probe
            ret = cpu_dai->probe(pdev, cpu_dai);//执行probe函数
            if (ret < 0)
                goto cpu_dai_err;
        }
    }

    if (codec_dev->probe) {
        ret = codec_dev->probe(pdev);//对应wm8994_probe
        if (ret < 0)
            goto cpu_dai_err;
    }
    if (platform->probe) {//这里platform对应imx_soc_platform,没有定义probe
        ret = platform->probe(pdev);
        if (ret < 0)
            goto platform_err;
    }

    /* DAPM stream work */
    INIT_DELAYED_WORK(&card->delayed_work, close_delayed_work);
#ifdef CONFIG_PM
    /* deferred resume work */
    INIT_WORK(&card->deferred_resume_work, soc_resume_deferred);
#endif
    card->instantiated = 1;//已经初始化

    return;

platform_err:
    if (codec_dev->remove)
        codec_dev->remove(pdev);

cpu_dai_err:
    for (i--; i >= 0; i--) {
        struct snd_soc_dai *cpu_dai = card->dai_link[i].cpu_dai;
        if (cpu_dai->remove)
            cpu_dai->remove(pdev, cpu_dai);
    }

    if (card->remove)
        card->remove(pdev);
}
在都进行了platform_list和dai_list注册后,会执行cpu_dai->probe和codec_dev->probe,它们分别对应imx_ssi_probe和wm8994_probe。我们先看imx_ssi_probe:

static int imx_ssi_probe(struct platform_device *pdev, struct snd_soc_dai *dai)
{
    struct imx_ssi *priv = (struct imx_ssi *)dai->private_data;

    if (priv->irq >= 0) {
        if (request_irq(priv->irq, imx_ssi_irq, IRQF_SHARED,
                pdev->name, priv)) {
            printk(KERN_ERR "%s: failure requesting irq for %s\n",
                   __func__, pdev->name);
            return -EBUSY;
        }
    }

    return 0;
}
我们看到主要注册了ssi的中断函数。

wm8994_probe见下一节。

 

我们接着上一节:
static int wm8994_probe(struct platform_device *pdev)
{
    struct snd_soc_device *socdev = platform_get_drvdata(pdev);
    struct snd_soc_codec *codec = wm8994_codec;
    int ret = 0;

    socdev->card->codec = wm8994_codec;//已经在codec列表里

    /* register pcms */
    ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1);//注册pcms,1
    if (ret < 0) {
        dev_err(codec->dev, "failed to create pcms\n");
        return ret;
    }
   
    snd_soc_add_controls(codec, wm8994_snd_controls,//添加控制,2
                 ARRAY_SIZE(wm8994_snd_controls));
    snd_soc_dapm_new_controls(codec, wm8994_dapm_widgets,//添加dapm的widgets,3
                  ARRAY_SIZE(wm8994_dapm_widgets));

    snd_soc_dapm_add_routes(codec, audio_map, ARRAY_SIZE(audio_map));//添加dapm的路径,4

//    wm8994_audio_start_dac(codec);
//    wm8994_audio_stop_speaker(codec);

    if (tda19989_get_status()) {
        wm8994_update_bit(codec, WM8994_POWER_MANAGEMENT_1,
                0, WM8994_SPKOUTR_ENA_MASK | WM8994_SPKOUTL_ENA_MASK);
        wm8994_update_bit(codec, WM8994_AIF1_DAC1_FILTERS_1,
                WM8994_AIF1DAC1_MUTE, WM8994_AIF1DAC1_MUTE);
        tda19989_pcm_mute((bool)1);
    }
    ret = snd_soc_init_card(socdev);//初始化card,5
    if (ret < 0) {
        printk(KERN_ERR "wm8994: failed to register card\n");
        snd_soc_free_pcms(socdev);
        snd_soc_dapm_free(socdev);
        return ret;
    }
   
    INIT_WORK(&wm8994_trigger_event, wm8994_do_trigger);

    return 0;
}

我们将分为上面标记的5部分进行分析。
1,snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1):
int snd_soc_new_pcms(struct snd_soc_device *socdev, int idx, const char *xid)
{
    struct snd_soc_card *card = socdev->card;
    struct snd_soc_codec *codec = card->codec;
    int ret = 0, i;

    mutex_lock(&codec->mutex);
    /* register a sound card */
    ret = snd_card_create(idx, xid, codec->owner, 0, &codec->card);//注册一个声卡,其中将会有control的ops注册到card的devices里面,1-1
    if (ret < 0) {
        printk(KERN_ERR "asoc: can't create sound card for codec %s\n",
            codec->name);
        mutex_unlock(&codec->mutex);
        return -ENODEV;
    }

    codec->socdev = socdev;
    codec->card->dev = socdev->dev;
    codec->card->private_data = codec;
    strncpy(codec->card->driver, codec->name, sizeof(codec->card->driver));
    /* create the pcms */
    for (i = 0; i < card->num_links; i++) {//根据dai连接数目,决定创建多少个pcm
        ret = soc_new_pcm(socdev, &card->dai_link[i], i);//为每个card新建pcm流,1-2
        if (ret < 0) {
            printk(KERN_ERR "asoc: can't create pcm %s\n",
                card->dai_link[i].stream_name);
            mutex_unlock(&codec->mutex);
            return ret;
        }
    }

    mutex_unlock(&codec->mutex);
    return ret;
}
1-1,snd_card_create(idx, xid, codec->owner, 0, &codec->card):
int snd_card_create(int idx, const char *xid,
            struct module *module, int extra_size,
            struct snd_card **card_ret)
{
    struct snd_card *card;
    int err, idx2;

    if (snd_BUG_ON(!card_ret))
        return -EINVAL;
    *card_ret = NULL;

    if (extra_size < 0)
        extra_size = 0;
    card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL);//申请声卡空间
    if (!card)
        return -ENOMEM;
    if (xid)
        strlcpy(card->id, xid, sizeof(card->id));
    err = 0;
    mutex_lock(&snd_card_mutex);
    if (idx < 0) {//从上面知道idx=-1
        for (idx2 = 0; idx2 < SNDRV_CARDS; idx2++)
            /* idx == -1 == 0xffff means: take any free slot */
            if (~snd_cards_lock & idx & 1<<idx2) {
                if (module_slot_match(module, idx2)) {
                    idx = idx2;//找到对应module名字的minor
                    break;
                }
            }
    }
    if (idx < 0) {//如果上面没找到
        for (idx2 = 0; idx2 < SNDRV_CARDS; idx2++)
            /* idx == -1 == 0xffff means: take any free slot */
            if (~snd_cards_lock & idx & 1<<idx2) {
                if (!slots[idx2] || !*slots[idx2]) {
                    idx = idx2;//找第一个非空的slot
                    break;
                }
            }
    }
    if (idx < 0)//判断idx的合法性
        err = -ENODEV;
    else if (idx < snd_ecards_limit) {
        if (snd_cards_lock & (1 << idx))
            err = -EBUSY;    /* invalid */
    } else if (idx >= SNDRV_CARDS)
        err = -ENODEV;
    if (err < 0) {
        mutex_unlock(&snd_card_mutex);
        snd_printk(KERN_ERR "cannot find the slot for index %d (range 0-%i), error: %d\n",
             idx, snd_ecards_limit - 1, err);
        goto __error;
    }
    snd_cards_lock |= 1 << idx;        /* lock it */
    if (idx >= snd_ecards_limit)
        snd_ecards_limit = idx + 1; /* increase the limit */
    mutex_unlock(&snd_card_mutex);
    card->number = idx;//声卡的序号
    card->module = module;//声卡所属的module
    INIT_LIST_HEAD(&card->devices);//初始化card的设备列表
    init_rwsem(&card->controls_rwsem);
    rwlock_init(&card->ctl_files_rwlock);
    INIT_LIST_HEAD(&card->controls);//初始化card的控制列表
    INIT_LIST_HEAD(&card->ctl_files);
    spin_lock_init(&card->files_lock);
    INIT_LIST_HEAD(&card->files_list);
    init_waitqueue_head(&card->shutdown_sleep);
#ifdef CONFIG_PM
    mutex_init(&card->power_lock);
    init_waitqueue_head(&card->power_sleep);
#endif
    /* the control interface cannot be accessed from the user space until */
    /* snd_cards_bitmask and snd_cards are set with snd_card_register */
    err = snd_ctl_create(card);//创建controlC节点,1-1-1
    if (err < 0) {
        snd_printk(KERN_ERR "unable to register control minors\n");
        goto __error;
    }
    err = snd_info_card_create(card);//在/proc/asound/下创建card0文件夹
    if (err < 0) {
        snd_printk(KERN_ERR "unable to create card info\n");
        goto __error_ctl;
    }
    if (extra_size > 0)
        card->private_data = (char *)card + sizeof(struct snd_card);
    *card_ret = card;//返回card
    return 0;

      __error_ctl:
    snd_device_free_all(card, SNDRV_DEV_CMD_PRE);
      __error:
    kfree(card);
      return err;
}
1-1-1,snd_ctl_create(card):
int snd_ctl_create(struct snd_card *card)
{
    static struct snd_device_ops ops = {
        .dev_free = snd_ctl_dev_free,
        .dev_register =    snd_ctl_dev_register,//这里会注册一个控制节点,但不是在这里执行
        .dev_disconnect = snd_ctl_dev_disconnect,
    };

    if (snd_BUG_ON(!card))
        return -ENXIO;
    return snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);//将ops添加到card devices里面,1-1-1-1
}
1-1-1-1,snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops)
int snd_device_new(struct snd_card *card, snd_device_type_t type,
           void *device_data, struct snd_device_ops *ops)
{
    struct snd_device *dev;

    if (snd_BUG_ON(!card || !device_data || !ops))
        return -ENXIO;
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (dev == NULL) {
        snd_printk(KERN_ERR "Cannot allocate device\n");
        return -ENOMEM;
    }
    dev->card = card;//device的card
    dev->type = type;
    dev->state = SNDRV_DEV_BUILD;
    dev->device_data = device_data;
    dev->ops = ops;//将ops添加到card的device中
    list_add(&dev->list, &card->devices);    //添加到card的设备列表里面/* add to the head of list */
    return 0;
}
snd_soc_new_pcms()的1-1部分主要做了创建声卡,将控制节点添加到card设备列表里面,同时在/proc/asound/下创建card0文件夹。
下面我们看1-2部分soc_new_pcm(socdev, &card->dai_link[i], i):
static int soc_new_pcm(struct snd_soc_device *socdev,
    struct snd_soc_dai_link *dai_link, int num)
{
    struct snd_soc_card *card = socdev->card;
    struct snd_soc_codec *codec = card->codec;
    struct snd_soc_platform *platform = card->platform;
    struct snd_soc_dai *codec_dai = dai_link->codec_dai;
    struct snd_soc_dai *cpu_dai = dai_link->cpu_dai;
    struct snd_soc_pcm_runtime *rtd;
    struct snd_pcm *pcm;
    char new_name[64];
    int ret = 0, playback = 0, capture = 0;

    rtd = kzalloc(sizeof(struct snd_soc_pcm_runtime), GFP_KERNEL);//申请snd_soc_pcm_runtim空间
    if (rtd == NULL)
        return -ENOMEM;

    rtd->dai = dai_link;//将dai_link赋值给pcm_runtime
    rtd->socdev = socdev;//将soc_device赋值给pcm_runtime
    codec_dai->codec = card->codec;//将codec赋值给codec_dai

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

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

    ret = snd_pcm_new(codec->card, new_name, codec->pcm_devs++, playback,//创建一个新的pcm实例,1-2-1
        capture, &pcm);
    if (ret < 0) {
        printk(KERN_ERR "asoc: can't create pcm for codec %s\n",
            codec->name);
        kfree(rtd);
        return ret;
    }

    dai_link->pcm = pcm;//将pcm赋值给dai_link
    pcm->private_data = rtd;
    soc_pcm_ops.mmap = platform->pcm_ops->mmap;//初始化soc_pcm_ops,platform有在imx_pcm.c中定义
    soc_pcm_ops.pointer = platform->pcm_ops->pointer;
    soc_pcm_ops.ioctl = platform->pcm_ops->ioctl;
    soc_pcm_ops.copy = platform->pcm_ops->copy;
    soc_pcm_ops.silence = platform->pcm_ops->silence;
    soc_pcm_ops.ack = platform->pcm_ops->ack;
    soc_pcm_ops.page = platform->pcm_ops->page;

    if (playback)
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &soc_pcm_ops);//设置pcm的ops,pcm->streams[SNDRV_PCM_STREAM_PLAYBACK]->substream->ops= &soc_pcm_ops

    if (capture)
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &soc_pcm_ops);

    ret = platform->pcm_new(codec->card, codec_dai, pcm);//申请playback和capture buffer
    if (ret < 0) {
        printk(KERN_ERR "asoc: platform pcm constructor failed\n");
        kfree(rtd);
        return ret;
    }

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

    return ret;
}
1-2-1,snd_pcm_new(codec->card, new_name, codec->pcm_devs++, playback, capture, &pcm):
int snd_pcm_new(struct snd_card *card, const char *id, int device,
        int playback_count, int capture_count,
            struct snd_pcm ** rpcm)
{
    struct snd_pcm *pcm;
    int err;
    static struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register =    snd_pcm_dev_register,//会注册pcm设备,但是不是现在执行
        .dev_disconnect = snd_pcm_dev_disconnect,
    };

    if (snd_BUG_ON(!card))
        return -ENXIO;
    if (rpcm)
        *rpcm = NULL;
    pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);//申请pcm空间
    if (pcm == NULL) {
        snd_printk(KERN_ERR "Cannot allocate PCM\n");
        return -ENOMEM;
    }
    pcm->card = card;//pcm的card
    pcm->device = device;//pcm的device
    if (id)
        strlcpy(pcm->id, id, sizeof(pcm->id));//将id赋值给pcm->id
    if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)) < 0) {//新的playback stream,1-2-1-1
        snd_pcm_free(pcm);
        return err;
    }
    if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)) < 0) {//capture stream
        snd_pcm_free(pcm);
        return err;
    }
    mutex_init(&pcm->open_mutex);
    init_waitqueue_head(&pcm->open_wait);
    if ((err = snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)) < 0) {//将ops添加到card devices里面,后面将会用到这个ops,与上面添加ctl节点相同
        snd_pcm_free(pcm);
        return err;
    }
    if (rpcm)
        *rpcm = pcm;
    return 0;
}
1-2-1-1,snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count):
int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count)
{
    int idx, err;
    struct snd_pcm_str *pstr = &pcm->streams[stream];
    struct snd_pcm_substream *substream, *prev;

#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
    mutex_init(&pstr->oss.setup_mutex);
#endif
    //这里对应/proc/asound/card0/
    pstr->stream = stream;//stream标记了是PLAYBACK还是CAPTURE
    pstr->pcm = pcm;//对应的pcm
    pstr->substream_count = substream_count;//substream数目
    if (substream_count > 0) {
        err = snd_pcm_stream_proc_init(pstr);// 创建pcm0p或pcm0c目录
        if (err < 0) {
            snd_printk(KERN_ERR "Error in snd_pcm_stream_proc_init\n");
            return err;
        }
    }
    prev = NULL;
    for (idx = 0, prev = NULL; idx < substream_count; idx++) {
        substream = kzalloc(sizeof(*substream), GFP_KERNEL);//对每一个申请substream空间
        if (substream == NULL) {
            snd_printk(KERN_ERR "Cannot allocate PCM substream\n");
            return -ENOMEM;
        }
        substream->pcm = pcm;//该substream对应的pcm
        substream->pstr = pstr;
        substream->number = idx;
        substream->stream = stream;
        sprintf(substream->name, "subdevice #%i", idx);
        snprintf(substream->latency_id, sizeof(substream->latency_id),
             "ALSA-PCM%d-%d%c%d", pcm->card->number, pcm->device,
             (stream ? 'c' : 'p'), idx);
        substream->buffer_bytes_max = UINT_MAX;
        if (prev == NULL)
            pstr->substream = substream;
        else
            prev->next = substream;
        err = snd_pcm_substream_proc_init(substream);//创建sub0文件夹及其内部的一些文件
        if (err < 0) {
            snd_printk(KERN_ERR "Error in snd_pcm_stream_proc_init\n");
            if (prev == NULL)
                pstr->substream = NULL;
            else
                prev->next = NULL;
            kfree(substream);
            return err;
        }
        substream->group = &substream->self_group;
        spin_lock_init(&substream->self_group.lock);
        INIT_LIST_HEAD(&substream->self_group.substreams);
        list_add_tail(&substream->link_list, &substream->self_group.substreams);
        atomic_set(&substream->mmap_count, 0);
        prev = substream;
    }
    return 0;
}               
根据stream的类型(PLAYBACK/CAPTURE)新建substream流,并在/proc/asound/card0/下新建相关文件夹和文件。
1-2部分主要是新建pcm实例,设置pcm的ops,申请playback和capture的dma buffer。
这样我们第一部分就分析到此了,下面看第2部分snd_soc_add_controls(codec, wm8994_snd_controls, ARRAY_SIZE(wm8994_snd_controls)):

2,我们线看wm8994_snd_controls定义:
static const struct snd_kcontrol_new wm8994_snd_controls[] = { 
    SOC_DOUBLE_R_TLV("AIF1ADC1 Volume", WM8994_AIF1_ADC1_LEFT_VOLUME,
                     WM8994_AIF1_ADC1_RIGHT_VOLUME,
                     1, 119, 0, digital_tlv),
    ....//其它的宏类似
};
我们这里就选取这一个control的定义分析,因为其它都是比较类似的。我们看下SOC_DOUBLE_R_TLV定义:
#define SOC_DOUBLE_R_TLV(xname, reg_left, reg_right, xshift, xmax, xinvert, tlv_array) \
{    .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = (xname),\
    .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |\
         SNDRV_CTL_ELEM_ACCESS_READWRITE,\
    .tlv.p = (tlv_array), \
    .info = snd_soc_info_volsw_2r, \
    .get = snd_soc_get_volsw_2r, .put = snd_soc_put_volsw_2r, \
    .private_value = (unsigned long)&(struct soc_mixer_control) \
        {.reg = reg_left, .rreg = reg_right, .shift = xshift, \
        .max = xmax, .invert = xinvert} }
那SOC_DOUBLE_R_TLV("AIF1ADC1 Volume", WM8994_AIF1_ADC1_LEFT_VOLUME, WM8994_AIF1_ADC1_RIGHT_VOLUME, 1, 119, 0, digital_tlv)就变成:
{    .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = ("AIF1ADC1 Volume"),\
    .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |\  //读写权限
         SNDRV_CTL_ELEM_ACCESS_READWRITE,\
    .tlv.p = (digital_tlv), \
    .info = snd_soc_info_volsw_2r, \
    .get = snd_soc_get_volsw_2r, .put = snd_soc_put_volsw_2r, \
    .private_value = (unsigned long)&(struct soc_mixer_control) \
        {.reg = WM8994_AIF1_ADC1_LEFT_VOLUME, .rreg = WM8994_AIF1_ADC1_RIGHT_VOLUME, .shift = 1, \
        .max = 119, .invert = 0} }
我们解释一下各个字段:
(1) iface 字段定义了control 的类型,形式为SNDRV_CTL_ELEM_IFACE_XXX,通常是MIXER,对于不属于mixer的全局控制,使用CARD。如果关联于某类设备,则使用HWDEP、 PCM、RAWMIDI、TIMER 或SEQUENCER。

(2) name 是名称标识字符串,control 的名称非常重要,因为control 的作用由名称来区分。对于名称相同的control,则使用index 区分。name 定义的标准是“SOURCE DIRECTION FUNCTION”即“源、方向、功能”:

SOURCE 定义了control 的源,如“Master”、“PCM”、“CD”和“Line”等

DIRECTION方向则为“Playback”、“Capture”、“Bypass Playback”或“Bypass Capture”,如果方向省略,意味着playback 和capture双向。

FUNCTION可以是“Switch”、“Volume”和“Route”等

iface 和 name 可以通过上层输入amixer命令获取,如下:

# # amixer controls

 

numid=100,iface=CARD,name='BB'

numid=30,iface=MIXER,name='Headphone Playback ZC Switch'

numid=6,iface=MIXER,name='AIF1ADC1 Volume'

(3) info()函数用于获得该control 的详细信息,该函数必须填充传递给它的第二个参数。

(4) get()函数用于得到control 的目前值并返回用户空间。

(5) put()函数用于从用户空间写入值,如果值被改变,该函数返回1,否则返回0;如果发生错误,该函数返回错误码。

get()和put()的第二个参数的类型为snd_ctl_elem_value。snd_ctl_elem_value结构体的内部也包含一个由integer、integer64、enumerated 等组成的值联合体,它的具体类型依赖于control 的类型和info()函数。

对于get()和put()函数而言,如果control 有多于一个元素,即count>1,则每个元素都需要被返回或写入。在使用amixer命令配置声卡时都会调用到get()和put()函数。 get()和put()函数中讲判断设置值与原有值,判断是否配置寄存器。

我们下面看snd_soc_add_controls定义:
int snd_soc_add_controls(struct snd_soc_codec *codec,
    const struct snd_kcontrol_new *controls, int num_controls)
{
    struct snd_card *card = codec->card;
    int err, i;

    for (i = 0; i < num_controls; i++) {//一个一个添加
        const struct snd_kcontrol_new *control = &controls[i];
        err = snd_ctl_add(card, snd_soc_cnew(control, codec, NULL));//添加控制实例到card,2-1,2-2
        if (err < 0) {
            dev_err(codec->dev, "%s: Failed to add %s\n",
                codec->name, control->name);
            return err;
        }
    }

    return 0;
}

2-1,snd_soc_cnew(control, codec, NULL):
struct snd_kcontrol *snd_soc_cnew(const struct snd_kcontrol_new *_template,
    void *data, char *long_name)
{
    struct snd_kcontrol_new template;

    memcpy(&template, _template, sizeof(template));
    if (long_name)
        template.name = long_name;
    template.index = 0;

    return snd_ctl_new1(&template, data);//返回kcontrol,2-1-1
}
2-1-1,snd_ctl_new1(&template, data):
struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,
                  void *private_data)
{
    struct snd_kcontrol kctl;
    unsigned int access;
   
    if (snd_BUG_ON(!ncontrol || !ncontrol->info))
        return NULL;
    memset(&kctl, 0, sizeof(kctl));
    kctl.id.iface = ncontrol->iface;//赋值iface
    kctl.id.device = ncontrol->device;
    kctl.id.subdevice = ncontrol->subdevice;
    if (ncontrol->name) {
        strlcpy(kctl.id.name, ncontrol->name, sizeof(kctl.id.name));
        if (strcmp(ncontrol->name, kctl.id.name) != 0)//如果不等,说明名字被截断
            snd_printk(KERN_WARNING
                   "Control name '%s' truncated to '%s'\n",
                   ncontrol->name, kctl.id.name);
    }
    kctl.id.index = ncontrol->index;
    kctl.count = ncontrol->count ? ncontrol->count : 1;
    access = ncontrol->access == 0 ? SNDRV_CTL_ELEM_ACCESS_READWRITE ://赋值读写权限
         (ncontrol->access & (SNDRV_CTL_ELEM_ACCESS_READWRITE|
                      SNDRV_CTL_ELEM_ACCESS_INACTIVE|
                       SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE|
                       SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK));
    kctl.info = ncontrol->info;//info函数
    kctl.get = ncontrol->get;
    kctl.put = ncontrol->put;
    kctl.tlv.p = ncontrol->tlv.p;
    kctl.private_value = ncontrol->private_value;
    kctl.private_data = private_data;
    return snd_ctl_new(&kctl, access);//新生成控制实例,2-1-1-1
}
2-1-1-1,snd_ctl_new(&kctl, access):
static struct snd_kcontrol *snd_ctl_new(struct snd_kcontrol *control,
                    unsigned int access)
{
    struct snd_kcontrol *kctl;
    unsigned int idx;
   
    if (snd_BUG_ON(!control || !control->count))
        return NULL;
    kctl = kzalloc(sizeof(*kctl) + sizeof(struct snd_kcontrol_volatile) * control->count, GFP_KERNEL);//申请kctl空间
    if (kctl == NULL) {
        snd_printk(KERN_ERR "Cannot allocate control instance\n");
        return NULL;
    }
    *kctl = *control;//拷贝到kctl
    for (idx = 0; idx < kctl->count; idx++)
        kctl->vd[idx].access = access;//访问权限
    return kctl;//返回kctl
}

2-2,snd_ctl_add(card, snd_soc_cnew(control, codec, NULL)):
int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol)
{
    struct snd_ctl_elem_id id;
    unsigned int idx;
    int err = -EINVAL;

    if (! kcontrol)
        return err;
    if (snd_BUG_ON(!card || !kcontrol->info))
        goto error;
    id = kcontrol->id;//id包含iface,device
    down_write(&card->controls_rwsem);
    if (snd_ctl_find_id(card, &id)) {//查找看这个control是否已经在card里面
        up_write(&card->controls_rwsem);
        snd_printd(KERN_ERR "control %i:%i:%i:%s:%i is already present\n",
                    id.iface,
                    id.device,
                    id.subdevice,
                    id.name,
                    id.index);
        err = -EBUSY;
        goto error;
    }
    if (snd_ctl_find_hole(card, kcontrol->count) < 0) {//申请numid
        up_write(&card->controls_rwsem);
        err = -ENOMEM;
        goto error;
    }
    list_add_tail(&kcontrol->list, &card->controls);//将该kcontrol添加到card的control列表里面
    card->controls_count += kcontrol->count;//增加控制的数目
    kcontrol->id.numid = card->last_numid + 1;//更新id的numid
    card->last_numid += kcontrol->count;//更新card的last_numid
    up_write(&card->controls_rwsem);
    for (idx = 0; idx < kcontrol->count; idx++, id.index++, id.numid++)
        snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_ADD, &id);//通知有添加事件产生
    return 0;

 error:
    snd_ctl_free_one(kcontrol);
    return err;
}
我们看到最终是将control转化成kcontrol添加到card->controls列表里面,以备查询。

3,snd_soc_dapm_new_controls(codec, wm8994_dapm_widgets, ARRAY_SIZE(wm8994_dapm_widgets)):
int snd_soc_dapm_new_controls(struct snd_soc_codec *codec,
    const struct snd_soc_dapm_widget *widget,
    int num)
{
    int i, ret;

    for (i = 0; i < num; i++) {
        ret = snd_soc_dapm_new_control(codec, widget);//单个widget控制,3-1
        if (ret < 0) {
            printk(KERN_ERR
                   "ASoC: Failed to create DAPM control %s: %d\n",
                   widget->name, ret);
            return ret;
        }
        widget++;
    }
    return 0;
}

3-1,snd_soc_dapm_new_control(codec, widget):
int snd_soc_dapm_new_control(struct snd_soc_codec *codec,
    const struct snd_soc_dapm_widget *widget)
{
    struct snd_soc_dapm_widget *w;

    if ((w = dapm_cnew_widget(widget)) == NULL)
        return -ENOMEM;

    w->codec = codec;
    INIT_LIST_HEAD(&w->sources);
    INIT_LIST_HEAD(&w->sinks);
    INIT_LIST_HEAD(&w->list);
    list_add(&w->list, &codec->dapm_widgets);//添加到codec的dapm_widgets列表

    /* machine layer set ups unconnected pins and insertions */
    w->connected = 1;
    return 0;
}
最后将widget添加到codec的dapm_widgets列表。

4,snd_soc_dapm_add_routes(codec, audio_map, ARRAY_SIZE(audio_map)):
int snd_soc_dapm_add_routes(struct snd_soc_codec *codec,
                const struct snd_soc_dapm_route *route, int num)
{
    int i, ret;

    for (i = 0; i < num; i++) {
        ret = snd_soc_dapm_add_route(codec, route->sink,//添加单个route,4-1
                         route->control, route->source);
        if (ret < 0) {
            printk(KERN_ERR "Failed to add route %s->%s\n",
                   route->source,
                   route->sink);
            return ret;
        }
        route++;
    }

    return 0;
}
4-1,snd_soc_dapm_add_route(codec, route->sink, route->control, route->source):
static int snd_soc_dapm_add_route(struct snd_soc_codec *codec,
    const char *sink, const char *control, const char *source)
{
    struct snd_soc_dapm_path *path;
    struct snd_soc_dapm_widget *wsource = NULL, *wsink = NULL, *w;
    int ret = 0;

    /* find src and dest widgets */
    list_for_each_entry(w, &codec->dapm_widgets, list) {

        if (!wsink && !(strcmp(w->name, sink))) {
            wsink = w;
            continue;
        }
        if (!wsource && !(strcmp(w->name, source))) {
            wsource = w;
        }
    }

    if (wsource == NULL || wsink == NULL)
        return -ENODEV;

    path = kzalloc(sizeof(struct snd_soc_dapm_path), GFP_KERNEL);//申请snd_soc_dapm_path空间
    if (!path)
        return -ENOMEM;

    path->source = wsource;//赋值source
    path->sink = wsink;//赋值sink
    INIT_LIST_HEAD(&path->list);
    INIT_LIST_HEAD(&path->list_source);
    INIT_LIST_HEAD(&path->list_sink);

    /* check for external widgets */
    if (wsink->id == snd_soc_dapm_input) {//检查是否为外部组件
        if (wsource->id == snd_soc_dapm_micbias ||
            wsource->id == snd_soc_dapm_mic ||
            wsource->id == snd_soc_dapm_line ||
            wsource->id == snd_soc_dapm_output)
            wsink->ext = 1;
    }
    if (wsource->id == snd_soc_dapm_output) {
        if (wsink->id == snd_soc_dapm_spk ||
            wsink->id == snd_soc_dapm_hp ||
            wsink->id == snd_soc_dapm_line ||
            wsink->id == snd_soc_dapm_input)
            wsource->ext = 1;
    }

    /* connect static paths */
    if (control == NULL) {
        list_add(&path->list, &codec->dapm_paths);
        list_add(&path->list_sink, &wsink->sources);
        list_add(&path->list_source, &wsource->sinks);
        path->connect = 1;
        return 0;
    }

    /* connect dynamic paths */
    switch(wsink->id) {//sink的id连接动态路径
    case snd_soc_dapm_adc:
    case snd_soc_dapm_dac:
    case snd_soc_dapm_pga:
    case snd_soc_dapm_input:
    case snd_soc_dapm_output:
    case snd_soc_dapm_micbias:
    case snd_soc_dapm_vmid:
    case snd_soc_dapm_pre:
    case snd_soc_dapm_post:
    case snd_soc_dapm_supply:
    case snd_soc_dapm_aif_in:
    case snd_soc_dapm_aif_out:
        list_add(&path->list, &codec->dapm_paths);
        list_add(&path->list_sink, &wsink->sources);
        list_add(&path->list_source, &wsource->sinks);
        path->connect = 1;
        return 0;
    case snd_soc_dapm_mux:
    case snd_soc_dapm_value_mux:
        ret = dapm_connect_mux(codec, wsource, wsink, path, control,
            &wsink->kcontrols[0]);
        if (ret != 0)
            goto err;
        break;
    case snd_soc_dapm_switch:
    case snd_soc_dapm_mixer:
    case snd_soc_dapm_mixer_named_ctl:
        ret = dapm_connect_mixer(codec, wsource, wsink, path, control);
        if (ret != 0)
            goto err;
        break;
    case snd_soc_dapm_hp:
    case snd_soc_dapm_mic:
    case snd_soc_dapm_line:
    case snd_soc_dapm_spk:
        list_add(&path->list, &codec->dapm_paths);
        list_add(&path->list_sink, &wsink->sources);
        list_add(&path->list_source, &wsource->sinks);
        path->connect = 0;
        return 0;
    }
    return 0;

err:
    printk(KERN_WARNING "asoc: no dapm match for %s --> %s --> %s\n", source,
        control, sink);
    kfree(path);
    return ret;
}
这个函数主要功能是将两个widget连接起来。

5,snd_soc_init_card(socdev):
int snd_soc_init_card(struct snd_soc_device *socdev)
{
    struct snd_soc_card *card = socdev->card;
    struct snd_soc_codec *codec = card->codec;
    int ret = 0, i, ac97 = 0, err = 0;

    for (i = 0; i < card->num_links; i++) {
        if (card->dai_link[i].init) {
            err = card->dai_link[i].init(codec);//dai link的init,对应imx_3stack_wm8994_init,5-1
            if (err < 0) {
                printk(KERN_ERR "asoc: failed to init %s\n",
                    card->dai_link[i].stream_name);
                continue;
            }
        }
        if (card->dai_link[i].codec_dai->ac97_control) {
            ac97 = 1;
        }
    }
    snprintf(codec->card->shortname, sizeof(codec->card->shortname),
         "%s",  card->name);
    snprintf(codec->card->longname, sizeof(codec->card->longname),
         "%s (%s)", card->name, codec->name);

    /* Make sure all DAPM widgets are instantiated */
    snd_soc_dapm_new_widgets(codec);//初始化dapm widgets

    ret = snd_card_register(codec->card);//注册card,5-2
    if (ret < 0) {
        printk(KERN_ERR "asoc: failed to register soundcard for %s\n",
                codec->name);
        goto out;
    }

    mutex_lock(&codec->mutex);
#ifdef CONFIG_SND_SOC_AC97_BUS
    /* Only instantiate AC97 if not already done by the adaptor
     * for the generic AC97 subsystem.
     */
    if (ac97 && strcmp(codec->name, "AC97") != 0) {
        ret = soc_ac97_dev_register(codec);
        if (ret < 0) {
            printk(KERN_ERR "asoc: AC97 device register failed\n");
            snd_card_free(codec->card);
            mutex_unlock(&codec->mutex);
            goto out;
        }
    }
#endif

    err = snd_soc_dapm_sys_add(socdev->dev);//创建dapm_widget属性文件
    if (err < 0)
        printk(KERN_WARNING "asoc: failed to add dapm sysfs entries\n");

    err = device_create_file(socdev->dev, &dev_attr_codec_reg);//创建codec_reg属性文件
    if (err < 0)
        printk(KERN_WARNING "asoc: failed to add codec sysfs files\n");

    soc_init_codec_debugfs(codec);
    mutex_unlock(&codec->mutex);
out:
    return ret;
}

5-1,card->dai_link[i].init(codec):
我们在前面已经知道dai_link的定义,所以init对应imx_3stack_wm8994_init:
static int imx_3stack_wm8994_init(struct snd_soc_codec *codec)
{
    int i, ret;
    unsigned int hp;


#if defined(CONFIG_MXC_ASRC) || defined(CONFIG_MXC_ASRC_MODULE)
    for (i = 0; i < ARRAY_SIZE(asrc_controls); i++) {
        ret = snd_ctl_add(codec->card,
                  snd_soc_cnew(&asrc_controls[i], codec, NULL));
        if (ret < 0)
            return ret;
    }
    asrc_ssi_data.output_sample_rate = wm8994_rates[asrc_func];
#endif

    /* Add imx_3stack specific controls */
    for (i = 0; i < ARRAY_SIZE(wm8994_machine_controls); i++) {//添加cpu端的控制
        ret = snd_ctl_add(codec->card,
                  snd_soc_cnew(&wm8994_machine_controls[i],
                           codec, NULL));
        if (ret < 0)
            return ret;
    }

    /* Add imx_3stack specific widgets */
    snd_soc_dapm_new_controls(codec, imx_3stack_dapm_widgets,//添加dapm组件
                  ARRAY_SIZE(imx_3stack_dapm_widgets));

    /* Set up imx_3stack specific audio path audio_map */
    snd_soc_dapm_add_routes(codec, audio_map, ARRAY_SIZE(audio_map));//添加组件路径

    snd_soc_dapm_disable_pin(codec, "Line In Jack");
   
    hp = wm8994_hpdetect_irq(codec);
    if (hp & 0x1) {
        snd_soc_dapm_enable_pin(wm8994_dai.codec, "Headphone Jack");
        if (hp & 0x4) {
            snd_soc_dapm_enable_pin(wm8994_dai.codec,
                        "Headphone Mic");
            card_priv.mic = 1;
        } else
            card_priv.mic = 0;
        card_priv.headset = 1;
    } else {
        card_priv.headset = 0;
        card_priv.mic = 0;
    }
   
    snd_soc_dapm_sync(codec);
   
    if (card_priv.headset == 0)
        switch_set_state(wm8994_psdev, H2W_NO_DEVICE);
    else if (card_priv.mic == 0)
        switch_set_state(wm8994_psdev, H2W_HEADSET_NO_MIC);
    else
        switch_set_state(wm8994_psdev, H2W_HEADSET);
   
    return 0;
}
我们看到init主要添加了cpu端的控制和widgets等,还有一下状态的初始化。

5-2,snd_card_register(codec->card):
int snd_card_register(struct snd_card *card)
{
    int err;

    if (snd_BUG_ON(!card))
        return -EINVAL;
#ifndef CONFIG_SYSFS_DEPRECATED
    if (!card->card_dev) {
        card->card_dev = device_create(sound_class, card->dev,//创建/sys/class/sound/card0
                           MKDEV(0, 0), card,
                           "card%i", card->number);
        if (IS_ERR(card->card_dev))
            card->card_dev = NULL;
    }
#endif
    if ((err = snd_device_register_all(card)) < 0)//注册card devices上的所有设备,5-2-1
        return err;
    mutex_lock(&snd_card_mutex);
    if (snd_cards[card->number]) {
        /* already registered */
        mutex_unlock(&snd_card_mutex);
        return 0;
    }
    snd_card_set_id_no_lock(card, card->id[0] == '\0' ? NULL : card->id);
    snd_cards[card->number] = card;//赋值到snd_cards数组
    mutex_unlock(&snd_card_mutex);
    init_info_for_card(card);
#if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_MIXER_OSS_MODULE)
    if (snd_mixer_oss_notify_callback)
        snd_mixer_oss_notify_callback(card, SND_MIXER_OSS_NOTIFY_REGISTER);
#endif
#ifndef CONFIG_SYSFS_DEPRECATED
    if (card->card_dev) {
        err = device_create_file(card->card_dev, &card_id_attrs);//创建id属性文件
        if (err < 0)
            return err;
        err = device_create_file(card->card_dev, &card_number_attrs);//创建number属性文件
        if (err < 0)
            return err;
    }
#endif
    return 0;
}
5-2-1,snd_device_register_all(card)):
int snd_device_register_all(struct snd_card *card)
{
    struct snd_device *dev;
    int err;
   
    if (snd_BUG_ON(!card))
        return -ENXIO;
    list_for_each_entry(dev, &card->devices, list) {//对于我们之前添加到card的devices列表里的每个device
        if (dev->state == SNDRV_DEV_BUILD && dev->ops->dev_register) {
            if ((err = dev->ops->dev_register(dev)) < 0)//目前对应snd_pcm_dev_register或者snd_ctl_dev_register,这两个函数将生成pcmC0D0c/pcmC0D0p或controlC0节点
                return err;
            dev->state = SNDRV_DEV_REGISTERED;
        }
    }
    return 0;
}
所以可以知道snd_card_register主要是注册了设备节点,还有一些sys下的属性文件。注册的这些设备节点将会在alsa-lib被使用,然后封装成容易使用的api对外使用。

哦,my god,整个初始化过程终于结束了!!! 从初始化过程我们就可以知道内核中alsa的主要架构,从而可以很方便地进行功能开发!

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值