ALSA driver---register card

通过snd_soc_register_card来注册card,即注册整个machine driver. 

此函数接收一个参数 snd_soc_card:

复制代码

/* SoC card */
struct snd_soc_card {
    const char *name;
    const char *long_name;
    const char *driver_name;
    struct device *dev;
    struct snd_card *snd_card;
    struct module *owner;

    struct mutex mutex;
    struct mutex dapm_mutex;

    bool instantiated;

    int (*probe)(struct snd_soc_card *card);
    int (*late_probe)(struct snd_soc_card *card);
    int (*remove)(struct snd_soc_card *card);

    /* the pre and post PM functions are used to do any PM work before and
     * after the codec and DAI's do any PM work. */
    int (*suspend_pre)(struct snd_soc_card *card);
    int (*suspend_post)(struct snd_soc_card *card);
    int (*resume_pre)(struct snd_soc_card *card);
    int (*resume_post)(struct snd_soc_card *card);

    /* callbacks */
    int (*set_bias_level)(struct snd_soc_card *,
                  struct snd_soc_dapm_context *dapm,
                  enum snd_soc_bias_level level);
    int (*set_bias_level_post)(struct snd_soc_card *,
                   struct snd_soc_dapm_context *dapm,
                   enum snd_soc_bias_level level);

    int (*add_dai_link)(struct snd_soc_card *,
                struct snd_soc_dai_link *link);
    void (*remove_dai_link)(struct snd_soc_card *,
                struct snd_soc_dai_link *link);

    long pmdown_time;

    /* CPU <--> Codec DAI links  */
    struct snd_soc_dai_link *dai_link;  /* predefined links only */
    int num_links;  /* predefined links only */
    struct list_head dai_link_list; /* all links */
    int num_dai_links;

    struct list_head rtd_list;
    int num_rtd;

    /* optional codec specific configuration */
    struct snd_soc_codec_conf *codec_conf;
    int num_configs;

    /*
     * optional auxiliary devices such as amplifiers or codecs with DAI
     * link unused
     */
    struct snd_soc_aux_dev *aux_dev;
    int num_aux_devs;
    struct list_head aux_comp_list;

    const struct snd_kcontrol_new *controls;
    int num_controls;

    /*
     * Card-specific routes and widgets.
     * Note: of_dapm_xxx for Device Tree; Otherwise for driver build-in.
     */
    const struct snd_soc_dapm_widget *dapm_widgets;
    int num_dapm_widgets;
    const struct snd_soc_dapm_route *dapm_routes;
    int num_dapm_routes;
    const struct snd_soc_dapm_widget *of_dapm_widgets;
    int num_of_dapm_widgets;
    const struct snd_soc_dapm_route *of_dapm_routes;
    int num_of_dapm_routes;
    bool fully_routed;

    struct work_struct deferred_resume_work;

    /* lists of probed devices belonging to this card */
    struct list_head codec_dev_list;

    struct list_head widgets;
    struct list_head paths;
    struct list_head dapm_list;
    struct list_head dapm_dirty;

    /* attached dynamic objects */
    struct list_head dobj_list;

    /* Generic DAPM context for the card */
    struct snd_soc_dapm_context dapm;
    struct snd_soc_dapm_stats dapm_stats;
    struct snd_soc_dapm_update *update;

#ifdef CONFIG_DEBUG_FS
    struct dentry *debugfs_card_root;
    struct dentry *debugfs_pop_time;
#endif
    u32 pop_time;

    void *drvdata;
}

复制代码

snd_soc_card结构体成员非常多,但是在定义一个snd_soc_card时,大部分machine driver只需要定义dai_link, controls, machine level的dapm widget和dapm route.其他成员在register card时,在匹配到已经注册的platform和codec时填充。

一个典型的card定义如下:

复制代码

static struct snd_soc_dai_link mt2701_cs42448_dai_links[] = {
    /* FE */
    [DAI_LINK_FE_MULTI_CH_OUT] = {
        .name = "mt2701-cs42448-multi-ch-out",
        .stream_name = "mt2701-cs42448-multi-ch-out",
        .cpu_dai_name = "PCM_multi",
        .codec_name = "snd-soc-dummy",
        .codec_dai_name = "snd-soc-dummy-dai",
        .trigger = {SND_SOC_DPCM_TRIGGER_POST,
                SND_SOC_DPCM_TRIGGER_POST},
        .ops = &mt2701_cs42448_48k_fe_ops,
        .dynamic = 1,
        .dpcm_playback = 1,
    },
    ...
    /* BE */
    [DAI_LINK_BE_I2S0] = {
        .name = "mt2701-cs42448-I2S0",
        .cpu_dai_name = "I2S0",
        .no_pcm = 1,
        .codec_dai_name = "cs42448",
        .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBS_CFS
             | SND_SOC_DAIFMT_GATED,
        .ops = &mt2701_cs42448_be_ops,
        .dpcm_playback = 1,
        .dpcm_capture = 1,
    },
    ...
};

static struct snd_soc_card mt2701_cs42448_soc_card = {
    .name = "mt2701-cs42448",
    .owner = THIS_MODULE,
    .dai_link = mt2701_cs42448_dai_links,
    .num_links = ARRAY_SIZE(mt2701_cs42448_dai_links),
    .controls = mt2701_cs42448_controls,
    .num_controls = ARRAY_SIZE(mt2701_cs42448_controls),
    .dapm_widgets = mt2701_cs42448_asoc_card_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(mt2701_cs42448_asoc_card_dapm_widgets),
};

复制代码

snd_soc_dai_link中,指定了Platform、Codec、codec_dai、cpu_dai的名字,稍后Machine驱动将会利用这些名字去匹配已经在系统中注册的platform,codec,dai,这些注册的部件都是在另外相应的Platform驱动和Codec驱动的代码文件中定义的,这样看来,Machine驱动的设备初始化代码无非就是选择合适Platform和Codec以及dai,用他们填充以上几个数据结构,然后注册Platform设备即可。

复制代码

struct snd_soc_dai_link {
    /* config - must be set by machine driver */
    const char *name;            /* Codec name */
    const char *stream_name;        /* Stream name */
    /*
     * You MAY specify the link's CPU-side device, either by device name,
     * or by DT/OF node, but not both. If this information is omitted,
     * the CPU-side DAI is matched using .cpu_dai_name only, which hence
     * must be globally unique. These fields are currently typically used
     * only for codec to codec links, or systems using device tree.
     */
    const char *cpu_name;
    struct device_node *cpu_of_node;
    /*
     * You MAY specify the DAI name of the CPU DAI. If this information is
     * omitted, the CPU-side DAI is matched using .cpu_name/.cpu_of_node
     * only, which only works well when that device exposes a single DAI.
     */
    const char *cpu_dai_name;
    /*
     * You MUST specify the link's codec, either by device name, or by
     * DT/OF node, but not both.
     */
    const char *codec_name;
    struct device_node *codec_of_node;
    /* You MUST specify the DAI name within the codec */
    const char *codec_dai_name;

    struct snd_soc_dai_link_component *codecs;
    unsigned int num_codecs;

    /*
     * You MAY specify the link's platform/PCM/DMA driver, either by
     * device name, or by DT/OF node, but not both. Some forms of link
     * do not need a platform.
     */
    const char *platform_name;
    struct device_node *platform_of_node;
    int id;    /* optional ID for machine driver link identification */

    const struct snd_soc_pcm_stream *params;
    unsigned int num_params;

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

    enum snd_soc_dpcm_trigger trigger[2]; /* trigger type for DPCM */

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

    /* optional hw_params re-writing for BE and FE sync */
    int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
            struct snd_pcm_hw_params *params);

    /* machine stream operations */
    const struct snd_soc_ops *ops;
    const struct snd_soc_compr_ops *compr_ops;

    /* For unidirectional dai links */
    bool playback_only;
    bool capture_only;

    /* Mark this pcm with non atomic ops */
    bool nonatomic;

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

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

    /* Do not create a PCM for this DAI link (Backend link) */
    unsigned int no_pcm:1;

    /* This DAI link can route to other DAI links at runtime (Frontend)*/
    unsigned int dynamic:1;

    /* DPCM capture and Playback support */
    unsigned int dpcm_capture:1;
    unsigned int dpcm_playback:1;

    /* DPCM used FE & BE merged format */
    unsigned int dpcm_merged_format:1;

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

    struct list_head list; /* DAI link list of the soc card */
    struct snd_soc_dobj dobj; /* For topology */
};

复制代码

进入正题,调用snd_soc_register_card来注册card.

复制代码

/**
 * snd_soc_register_card - Register a card with the ASoC core
 *
 * @card: Card to register
 *
 */
int snd_soc_register_card(struct snd_soc_card *card)
{
    int i, ret;
    struct snd_soc_pcm_runtime *rtd;

    if (!card->name || !card->dev)
        return -EINVAL;

    for (i = 0; i < card->num_links; i++) {
        struct snd_soc_dai_link *link = &card->dai_link[i];

        ret = soc_init_dai_link(card, link);
        if (ret) {
            dev_err(card->dev, "ASoC: failed to init link %s\n",
                link->name);
            return ret;
        }
    }

    dev_set_drvdata(card->dev, card);

    snd_soc_initialize_card_lists(card);

    INIT_LIST_HEAD(&card->dai_link_list);
    card->num_dai_links = 0;

    INIT_LIST_HEAD(&card->rtd_list);
    card->num_rtd = 0;

    INIT_LIST_HEAD(&card->dapm_dirty);
    INIT_LIST_HEAD(&card->dobj_list);
    card->instantiated = 0;
    mutex_init(&card->mutex);
    mutex_init(&card->dapm_mutex);

    ret = snd_soc_instantiate_card(card);
    if (ret != 0)
        return ret;

    /* deactivate pins to sleep state */
    list_for_each_entry(rtd, &card->rtd_list, list)  {
        struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
        int j;

        for (j = 0; j < rtd->num_codecs; j++) {
            struct snd_soc_dai *codec_dai = rtd->codec_dais[j];
            if (!codec_dai->active)
                pinctrl_pm_select_sleep_state(codec_dai->dev);
        }

        if (!cpu_dai->active)
            pinctrl_pm_select_sleep_state(cpu_dai->dev);
    }

    return ret;
}

复制代码

 1.遍历card->dai_link,调用soc_init_dai_link()来初始化dai_link.

在soc_init_dai_link()中,主要调用snd_soc_init_multicodec初始化该dail_link上的codec.并检查该dai_link上是否有codec_name, codec_dai_name,platform_name, cpu_name,cpu_dai_name.

复制代码

static int snd_soc_init_multicodec(struct snd_soc_card *card,
                   struct snd_soc_dai_link *dai_link)
{
    /* Legacy codec/codec_dai link is a single entry in multicodec */
    if (dai_link->codec_name || dai_link->codec_of_node ||
        dai_link->codec_dai_name) {
        dai_link->num_codecs = 1;

        dai_link->codecs = devm_kzalloc(card->dev,
                sizeof(struct snd_soc_dai_link_component),
                GFP_KERNEL);
        if (!dai_link->codecs)
            return -ENOMEM;

        dai_link->codecs[0].name = dai_link->codec_name;
        dai_link->codecs[0].of_node = dai_link->codec_of_node;
        dai_link->codecs[0].dai_name = dai_link->codec_dai_name;
    }

    if (!dai_link->codecs) {
        dev_err(card->dev, "ASoC: DAI link has no CODECs\n");
        return -EINVAL;
    }

    return 0;
}

static int soc_init_dai_link(struct snd_soc_card *card,
                   struct snd_soc_dai_link *link)
{
    int i, ret;

    ret = snd_soc_init_multicodec(card, link);
    if (ret) {
        dev_err(card->dev, "ASoC: failed to init multicodec\n");
        return ret;
    }

    for (i = 0; i < link->num_codecs; i++) {
        /*
         * Codec must be specified by 1 of name or OF node,
         * not both or neither.
         */
        if (!!link->codecs[i].name ==
            !!link->codecs[i].of_node) {
            dev_err(card->dev, "ASoC: Neither/both codec name/of_node are set for %s\n",
                link->name);
            return -EINVAL;
        }
        /* Codec DAI name must be specified */
        if (!link->codecs[i].dai_name) {
            dev_err(card->dev, "ASoC: codec_dai_name not set for %s\n",
                link->name);
            return -EINVAL;
        }
    }

    /*
     * Platform may be specified by either name or OF node, but
     * can be left unspecified, and a dummy platform will be used.
     */
    if (link->platform_name && link->platform_of_node) {
        dev_err(card->dev,
            "ASoC: Both platform name/of_node are set for %s\n",
            link->name);
        return -EINVAL;
    }

    /*
     * CPU device may be specified by either name or OF node, but
     * can be left unspecified, and will be matched based on DAI
     * name alone..
     */
    if (link->cpu_name && link->cpu_of_node) {
        dev_err(card->dev,
            "ASoC: Neither/both cpu name/of_node are set for %s\n",
            link->name);
        return -EINVAL;
    }
    /*
     * At least one of CPU DAI name or CPU device name/node must be
     * specified
     */
    if (!link->cpu_dai_name &&
        !(link->cpu_name || link->cpu_of_node)) {
        dev_err(card->dev,
            "ASoC: Neither cpu_dai_name nor cpu_name/of_node are set for %s\n",
            link->name);
        return -EINVAL;
    }

    return 0;
}

复制代码

2.初始化card的一些list,包括widgets, path, dapm_list, dai_link_list, rtd_list, dapm_dirty.

3.调用snd_soc_instantiate_card来实例化card,大部分注册工作都在此函数内完成。

复制代码

static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
    struct snd_soc_codec *codec;
    struct snd_soc_pcm_runtime *rtd;
    struct snd_soc_dai_link *dai_link;
    int ret, i, order;

    mutex_lock(&client_mutex);
    mutex_lock_nested(&card->mutex, SND_SOC_CARD_CLASS_INIT);

    /* bind DAIs */
    for (i = 0; i < card->num_links; i++) {
        ret = soc_bind_dai_link(card, &card->dai_link[i]);
        if (ret != 0)
            goto base_error;
    }

    /* bind aux_devs too */
    for (i = 0; i < card->num_aux_devs; i++) {
        ret = soc_bind_aux_dev(card, i);
        if (ret != 0)
            goto base_error;
    }

    /* add predefined DAI links to the list */
    for (i = 0; i < card->num_links; i++)
        snd_soc_add_dai_link(card, card->dai_link+i);

    /* initialize the register cache for each available codec */
    list_for_each_entry(codec, &codec_list, list) {
        if (codec->cache_init)
            continue;
        ret = snd_soc_init_codec_cache(codec);
        if (ret < 0)
            goto base_error;
    }

    /* card bind complete so register a sound card */
    ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
            card->owner, 0, &card->snd_card);
    if (ret < 0) {
        dev_err(card->dev,
            "ASoC: can't create sound card for card %s: %d\n",
            card->name, ret);
        goto base_error;
    }

    soc_init_card_debugfs(card);

    card->dapm.bias_level = SND_SOC_BIAS_OFF;
    card->dapm.dev = card->dev;
    card->dapm.card = card;
    list_add(&card->dapm.list, &card->dapm_list);

#ifdef CONFIG_DEBUG_FS
    snd_soc_dapm_debugfs_init(&card->dapm, card->debugfs_card_root);
#endif

#ifdef CONFIG_PM_SLEEP
    /* deferred resume work */
    INIT_WORK(&card->deferred_resume_work, soc_resume_deferred);
#endif

    if (card->dapm_widgets)
        snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,
                      card->num_dapm_widgets);

    if (card->of_dapm_widgets)
        snd_soc_dapm_new_controls(&card->dapm, card->of_dapm_widgets,
                      card->num_of_dapm_widgets);

    /* initialise the sound card only once */
    if (card->probe) {
        ret = card->probe(card);
        if (ret < 0)
            goto card_probe_error;
    }

    /* probe all components used by DAI links on this card */
    for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDER_LAST;
            order++) {
        list_for_each_entry(rtd, &card->rtd_list, list) {
            ret = soc_probe_link_components(card, rtd, order);
            if (ret < 0) {
                dev_err(card->dev,
                    "ASoC: failed to instantiate card %d\n",
                    ret);
                goto probe_dai_err;
            }
        }
    }

    /* probe auxiliary components */
    ret = soc_probe_aux_devices(card);
    if (ret < 0)
        goto probe_dai_err;

    /* Find new DAI links added during probing components and bind them.
     * Components with topology may bring new DAIs and DAI links.
     */
    list_for_each_entry(dai_link, &card->dai_link_list, list) {
        if (soc_is_dai_link_bound(card, dai_link))
            continue;

        ret = soc_init_dai_link(card, dai_link);
        if (ret)
            goto probe_dai_err;
        ret = soc_bind_dai_link(card, dai_link);
        if (ret)
            goto probe_dai_err;
    }

    /* probe all DAI links on this card */
    for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDER_LAST;
            order++) {
        list_for_each_entry(rtd, &card->rtd_list, list) {
            ret = soc_probe_link_dais(card, rtd, order);
            if (ret < 0) {
                dev_err(card->dev,
                    "ASoC: failed to instantiate card %d\n",
                    ret);
                goto probe_dai_err;
            }
        }
    }

    snd_soc_dapm_link_dai_widgets(card);
    snd_soc_dapm_connect_dai_link_widgets(card);

    if (card->controls)
        snd_soc_add_card_controls(card, card->controls, card->num_controls);

    if (card->dapm_routes)
        snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,
                    card->num_dapm_routes);

    if (card->of_dapm_routes)
        snd_soc_dapm_add_routes(&card->dapm, card->of_dapm_routes,
                    card->num_of_dapm_routes);

    snprintf(card->snd_card->shortname, sizeof(card->snd_card->shortname),
         "%s", card->name);
    snprintf(card->snd_card->longname, sizeof(card->snd_card->longname),
         "%s", card->long_name ? card->long_name : card->name);
    snprintf(card->snd_card->driver, sizeof(card->snd_card->driver),
         "%s", card->driver_name ? card->driver_name : card->name);
    for (i = 0; i < ARRAY_SIZE(card->snd_card->driver); i++) {
        switch (card->snd_card->driver[i]) {
        case '_':
        case '-':
        case '\0':
            break;
        default:
            if (!isalnum(card->snd_card->driver[i]))
                card->snd_card->driver[i] = '_';
            break;
        }
    }

    if (card->late_probe) {
        ret = card->late_probe(card);
        if (ret < 0) {
            dev_err(card->dev, "ASoC: %s late_probe() failed: %d\n",
                card->name, ret);
            goto probe_aux_dev_err;
        }
    }

    snd_soc_dapm_new_widgets(card);

    ret = snd_card_register(card->snd_card);
    if (ret < 0) {
        dev_err(card->dev, "ASoC: failed to register soundcard %d\n",
                ret);
        goto probe_aux_dev_err;
    }

    card->instantiated = 1;
    dapm_mark_endpoints_dirty(card);
    snd_soc_dapm_sync(&card->dapm);
    mutex_unlock(&card->mutex);
    mutex_unlock(&client_mutex);

    return 0;

probe_aux_dev_err:
    soc_remove_aux_devices(card);

probe_dai_err:
    soc_remove_dai_links(card);

card_probe_error:
    if (card->remove)
        card->remove(card);

    snd_soc_dapm_free(&card->dapm);
    soc_cleanup_card_debugfs(card);
    snd_card_free(card->snd_card);

base_error:
    soc_remove_pcm_runtimes(card);
    mutex_unlock(&card->mutex);
    mutex_unlock(&client_mutex);

    return ret;
}

复制代码

 3.1遍历每一对dai_link,调用soc_bind_dai_link对codec、platform、dai进行bind。

ASoC定义了三个全局的链表头变量:codec_list、dai_list、platform_list,系统中所有的Codec、DAI、Platform都在注册时连接到这三个全局链表上。soc_bind_dai_link函数逐个扫描这三个链表,根据card->dai_link[]中的名称进行匹配,匹配后把相应的codec,dai和platform实例赋值到card->rtd[]中(snd_soc_pcm_runtime)。经过这个过程后,snd_soc_pcm_runtime:(card->rtd)中保存了本Machine中使用的Codec,DAI和Platform驱动的信息。

在soc_bind_dai_link中,对每个dai_link都通过soc_new_pcm_rutime()创建一个rtd,在函数最后通过调用soc_add_pcm_rutime()将rtd加到card->rtd_list链表中。

复制代码

static int soc_bind_dai_link(struct snd_soc_card *card,
    struct snd_soc_dai_link *dai_link)
{
    struct snd_soc_pcm_runtime *rtd;
    struct snd_soc_dai_link_component *codecs = dai_link->codecs;
    struct snd_soc_dai_link_component cpu_dai_component;
    struct snd_soc_dai **codec_dais;
    struct snd_soc_platform *platform;
    const char *platform_name;
    int i;

    dev_dbg(card->dev, "ASoC: binding %s\n", dai_link->name);

    if (soc_is_dai_link_bound(card, dai_link)) {
        dev_dbg(card->dev, "ASoC: dai link %s already bound\n",
            dai_link->name);
        return 0;
    }

    rtd = soc_new_pcm_runtime(card, dai_link);
    if (!rtd)
        return -ENOMEM;

    cpu_dai_component.name = dai_link->cpu_name;
    cpu_dai_component.of_node = dai_link->cpu_of_node;
    cpu_dai_component.dai_name = dai_link->cpu_dai_name;
    rtd->cpu_dai = snd_soc_find_dai(&cpu_dai_component);
    if (!rtd->cpu_dai) {
        dev_err(card->dev, "ASoC: CPU DAI %s not registered\n",
            dai_link->cpu_dai_name);
        goto _err_defer;
    }

    rtd->num_codecs = dai_link->num_codecs;

    /* Find CODEC from registered CODECs */
    codec_dais = rtd->codec_dais;
    for (i = 0; i < rtd->num_codecs; i++) {
        codec_dais[i] = snd_soc_find_dai(&codecs[i]);
        if (!codec_dais[i]) {
            dev_err(card->dev, "ASoC: CODEC DAI %s not registered\n",
                codecs[i].dai_name);
            goto _err_defer;
        }
    }

    /* Single codec links expect codec and codec_dai in runtime data */
    rtd->codec_dai = codec_dais[0];
    rtd->codec = rtd->codec_dai->codec;

    /* if there's no platform we match on the empty platform */
    platform_name = dai_link->platform_name;
    if (!platform_name && !dai_link->platform_of_node)
        platform_name = "snd-soc-dummy";

    /* find one from the set of registered platforms */
    list_for_each_entry(platform, &platform_list, list) {
        if (dai_link->platform_of_node) {
            if (platform->dev->of_node !=
                dai_link->platform_of_node)
                continue;
        } else {
            if (strcmp(platform->component.name, platform_name))
                continue;
        }

        rtd->platform = platform;
    }
    if (!rtd->platform) {
        dev_err(card->dev, "ASoC: platform %s not registered\n",
            dai_link->platform_name);
        goto _err_defer;
    }

    soc_add_pcm_runtime(card, rtd);
    return 0;

_err_defer:
    soc_free_pcm_runtime(rtd);
    return  -EPROBE_DEFER;
}

复制代码

3.2调用snd_soc_add_dai_link将dai_link添加到card->dai_link_list中。

3.3调用snd_card_new创建card->snd_card.

3.4调用snd_soc_dapm_new_controls创建card->dapm_widgets.

在snd_soc_dapm_new_controls遍历card->dapm_widgets的所有的widgets,通过调用snd_soc_dapm_new_control_unlock()来创建widget,设定每个widget的power_check()函数,并将创建的widget加到card->widget链表中。

复制代码

/**
 * snd_soc_dapm_new_controls - create new dapm controls
 * @dapm: DAPM context
 * @widget: widget array
 * @num: number of widgets
 *
 * Creates new DAPM controls based upon the templates.
 *
 * Returns 0 for success else error.
 */
int snd_soc_dapm_new_controls(struct snd_soc_dapm_context *dapm,
    const struct snd_soc_dapm_widget *widget,
    int num)
{
    struct snd_soc_dapm_widget *w;
    int i;
    int ret = 0;

    mutex_lock_nested(&dapm->card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);
    for (i = 0; i < num; i++) {
        w = snd_soc_dapm_new_control_unlocked(dapm, widget);
        if (IS_ERR(w)) {
            ret = PTR_ERR(w);
            /* Do not nag about probe deferrals */
            if (ret == -EPROBE_DEFER)
                break;
            dev_err(dapm->dev,
                "ASoC: Failed to create DAPM control %s (%d)\n",
                widget->name, ret);
            break;
        }
        if (!w) {
            dev_err(dapm->dev,
                "ASoC: Failed to create DAPM control %s\n",
                widget->name);
            ret = -ENOMEM;
            break;
        }
        widget++;
    }
    mutex_unlock(&dapm->card->dapm_mutex);
    return ret;
}

struct snd_soc_dapm_widget *
snd_soc_dapm_new_control_unlocked(struct snd_soc_dapm_context *dapm,
             const struct snd_soc_dapm_widget *widget)
{
    enum snd_soc_dapm_direction dir;
    struct snd_soc_dapm_widget *w;
    const char *prefix;
    int ret;

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

    switch (w->id) {
    case snd_soc_dapm_regulator_supply:
        w->regulator = devm_regulator_get(dapm->dev, w->name);
        if (IS_ERR(w->regulator)) {
            ret = PTR_ERR(w->regulator);
            if (ret == -EPROBE_DEFER)
                return ERR_PTR(ret);
            dev_err(dapm->dev, "ASoC: Failed to request %s: %d\n",
                w->name, ret);
            return NULL;
        }

        if (w->on_val & SND_SOC_DAPM_REGULATOR_BYPASS) {
            ret = regulator_allow_bypass(w->regulator, true);
            if (ret != 0)
                dev_warn(w->dapm->dev,
                     "ASoC: Failed to bypass %s: %d\n",
                     w->name, ret);
        }
        break;
    case snd_soc_dapm_clock_supply:
#ifdef CONFIG_CLKDEV_LOOKUP
        w->clk = devm_clk_get(dapm->dev, w->name);
        if (IS_ERR(w->clk)) {
            ret = PTR_ERR(w->clk);
            if (ret == -EPROBE_DEFER)
                return ERR_PTR(ret);
            dev_err(dapm->dev, "ASoC: Failed to request %s: %d\n",
                w->name, ret);
            return NULL;
        }
#else
        return NULL;
#endif
        break;
    default:
        break;
    }

    prefix = soc_dapm_prefix(dapm);
    if (prefix)
        w->name = kasprintf(GFP_KERNEL, "%s %s", prefix, widget->name);
    else
        w->name = kstrdup_const(widget->name, GFP_KERNEL);
    if (w->name == NULL) {
        kfree(w);
        return NULL;
    }

    switch (w->id) {
    case snd_soc_dapm_mic:
        w->is_ep = SND_SOC_DAPM_EP_SOURCE;
        w->power_check = dapm_generic_check_power;
        break;
    case snd_soc_dapm_input:
        if (!dapm->card->fully_routed)
            w->is_ep = SND_SOC_DAPM_EP_SOURCE;
        w->power_check = dapm_generic_check_power;
        break;
    case snd_soc_dapm_spk:
    case snd_soc_dapm_hp:
        w->is_ep = SND_SOC_DAPM_EP_SINK;
        w->power_check = dapm_generic_check_power;
        break;
    case snd_soc_dapm_output:
        if (!dapm->card->fully_routed)
            w->is_ep = SND_SOC_DAPM_EP_SINK;
        w->power_check = dapm_generic_check_power;
        break;
    case snd_soc_dapm_vmid:
    case snd_soc_dapm_siggen:
        w->is_ep = SND_SOC_DAPM_EP_SOURCE;
        w->power_check = dapm_always_on_check_power;
        break;
    case snd_soc_dapm_sink:
        w->is_ep = SND_SOC_DAPM_EP_SINK;
        w->power_check = dapm_always_on_check_power;
        break;

    case snd_soc_dapm_mux:
    case snd_soc_dapm_demux:
    case snd_soc_dapm_switch:
    case snd_soc_dapm_mixer:
    case snd_soc_dapm_mixer_named_ctl:
    case snd_soc_dapm_adc:
    case snd_soc_dapm_aif_out:
    case snd_soc_dapm_dac:
    case snd_soc_dapm_aif_in:
    case snd_soc_dapm_pga:
    case snd_soc_dapm_out_drv:
    case snd_soc_dapm_micbias:
    case snd_soc_dapm_line:
    case snd_soc_dapm_dai_link:
    case snd_soc_dapm_dai_out:
    case snd_soc_dapm_dai_in:
        w->power_check = dapm_generic_check_power;
        break;
    case snd_soc_dapm_supply:
    case snd_soc_dapm_regulator_supply:
    case snd_soc_dapm_clock_supply:
    case snd_soc_dapm_kcontrol:
        w->is_supply = 1;
        w->power_check = dapm_supply_check_power;
        break;
    default:
        w->power_check = dapm_always_on_check_power;
        break;
    }

    w->dapm = dapm;
    INIT_LIST_HEAD(&w->list);
    INIT_LIST_HEAD(&w->dirty);
    list_add_tail(&w->list, &dapm->card->widgets);

    snd_soc_dapm_for_each_direction(dir) {
        INIT_LIST_HEAD(&w->edges[dir]);
        w->endpoints[dir] = -1;
    }

    /* machine layer sets up unconnected pins and insertions */
    w->connected = 1;
    return w;
}

复制代码

3.5 如果card有定义probe函数,调用probe

3.6遍历card->rtd_list,调用soc_probe_link_components来probe每个rtd上的component(paltform, cpu_dai,  codec_dai的component).

复制代码

static int soc_probe_link_components(struct snd_soc_card *card,
            struct snd_soc_pcm_runtime *rtd,
                     int order)
{
    struct snd_soc_platform *platform = rtd->platform;
    struct snd_soc_component *component;
    int i, ret;

    /* probe the CPU-side component, if it is a CODEC */
    component = rtd->cpu_dai->component;
    if (component->driver->probe_order == order) {
        ret = soc_probe_component(card, component);
        if (ret < 0)
            return ret;
    }

    /* probe the CODEC-side components */
    for (i = 0; i < rtd->num_codecs; i++) {
        component = rtd->codec_dais[i]->component;
        if (component->driver->probe_order == order) {
            ret = soc_probe_component(card, component);
            if (ret < 0)
                return ret;
        }
    }

    /* probe the platform */
    if (platform->component.driver->probe_order == order) {
        ret = soc_probe_component(card, &platform->component);
        if (ret < 0)
            return ret;
    }

    return 0;
}

复制代码

 

在soc_probe_component()里面,基于该component->dapm_widgets创建widget,将widget加到card->widgets链表中。基于component->dai_list中的dai创建dai widget,也将此widget加到card->widgets链表中。基于component->controls创建kcontrol,将此kcontrol加到card->snd_card->controls链表中。基于component->dapm_routes创建dapm path,并将创建的path加到card->paths.

复制代码

static int soc_probe_component(struct snd_soc_card *card,
    struct snd_soc_component *component)
{
    struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(component);
    struct snd_soc_dai *dai;
    int ret;

    if (!strcmp(component->name, "snd-soc-dummy"))
        return 0;

    if (component->card) {
        if (component->card != card) {
            dev_err(component->dev,
                "Trying to bind component to card \"%s\" but is already bound to card \"%s\"\n",
                card->name, component->card->name);
            return -ENODEV;
        }
        return 0;
    }

    if (!try_module_get(component->dev->driver->owner))
        return -ENODEV;

    component->card = card;
    dapm->card = card;
    soc_set_name_prefix(card, component);

    soc_init_component_debugfs(component);

    if (component->dapm_widgets) {
        ret = snd_soc_dapm_new_controls(dapm, component->dapm_widgets,
            component->num_dapm_widgets);

        if (ret != 0) {
            dev_err(component->dev,
                "Failed to create new controls %d\n", ret);
            goto err_probe;
        }
    }

    list_for_each_entry(dai, &component->dai_list, list) {
        ret = snd_soc_dapm_new_dai_widgets(dapm, dai);
        if (ret != 0) {
            dev_err(component->dev,
                "Failed to create DAI widgets %d\n", ret);
            goto err_probe;
        }
    }

    if (component->probe) {
        ret = component->probe(component);
        if (ret < 0) {
            dev_err(component->dev,
                "ASoC: failed to probe component %d\n", ret);
            goto err_probe;
        }

        WARN(dapm->idle_bias_off &&
            dapm->bias_level != SND_SOC_BIAS_OFF,
            "codec %s can not start from non-off bias with idle_bias_off==1\n",
            component->name);
    }

    /* machine specific init */
    if (component->init) {
        ret = component->init(component);
        if (ret < 0) {
            dev_err(component->dev,
                "Failed to do machine specific init %d\n", ret);
            goto err_probe;
        }
    }

    if (component->controls)
        snd_soc_add_component_controls(component, component->controls,
                     component->num_controls);
    if (component->dapm_routes)
        snd_soc_dapm_add_routes(dapm, component->dapm_routes,
                    component->num_dapm_routes);

    list_add(&dapm->list, &card->dapm_list);

    /* This is a HACK and will be removed soon */
    if (component->codec)
        list_add(&component->codec->card_list, &card->codec_dev_list);

    return 0;

err_probe:
    soc_cleanup_component_debugfs(component);
    component->card = NULL;
    module_put(component->dev->driver->owner);

    return ret;
}

复制代码

3.7遍历card->rtd_list, 调用soc_probe_link_dais来probe每个rtd上的dais(cpu_dai, codec_dai).

复制代码

static int soc_probe_link_dais(struct snd_soc_card *card,
        struct snd_soc_pcm_runtime *rtd, int order)
{
    struct snd_soc_dai_link *dai_link = rtd->dai_link;
    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
    int i, ret;

    dev_dbg(card->dev, "ASoC: probe %s dai link %d late %d\n",
            card->name, rtd->num, order);

    /* set default power off timeout */
    rtd->pmdown_time = pmdown_time;

    ret = soc_probe_dai(cpu_dai, order);
    if (ret)
        return ret;

    /* probe the CODEC DAI */
    for (i = 0; i < rtd->num_codecs; i++) {
        ret = soc_probe_dai(rtd->codec_dais[i], order);
        if (ret)
            return ret;
    }

    /* complete DAI probe during last probe */
    if (order != SND_SOC_COMP_ORDER_LAST)
        return 0;

    /* do machine specific initialization */
    if (dai_link->init) {
        ret = dai_link->init(rtd);
        if (ret < 0) {
            dev_err(card->dev, "ASoC: failed to init %s: %d\n",
                dai_link->name, ret);
            return ret;
        }
    }

    if (dai_link->dai_fmt)
        snd_soc_runtime_set_dai_fmt(rtd, dai_link->dai_fmt);

    ret = soc_post_component_init(rtd, dai_link->name);
    if (ret)
        return ret;

#ifdef CONFIG_DEBUG_FS
    /* add DPCM sysfs entries */
    if (dai_link->dynamic)
        soc_dpcm_debugfs_add(rtd);
#endif

    if (cpu_dai->driver->compress_new) {
        /*create compress_device"*/
        ret = cpu_dai->driver->compress_new(rtd, rtd->num);
        if (ret < 0) {
            dev_err(card->dev, "ASoC: can't create compress %s\n",
                     dai_link->stream_name);
            return ret;
        }
    } else {

        if (!dai_link->params) {
            /* create the pcm */
            ret = soc_new_pcm(rtd, rtd->num);
            if (ret < 0) {
                dev_err(card->dev, "ASoC: can't create pcm %s :%d\n",
                       dai_link->stream_name, ret);
                return ret;
            }
        } else {
            INIT_DELAYED_WORK(&rtd->delayed_work,
                        codec2codec_close_delayed_work);

            /* link the DAI widgets */
            ret = soc_link_dai_widgets(card, dai_link, rtd);
            if (ret)
                return ret;
        }
    }

    return 0;
}

复制代码

 在soc_probe_link_dais()所做的事情如下:

a)首先调用soc_probe_dai()来probe cpu_dai/codec_dai.在soc_probe_dai()中是调用dai driver的probe()函数.

b)如果dai_link定义dai_fmt,则调用snd_soc_runtime_set_dai_fmt将format设置到cpu_dai和codec_dai,即调用dai->driver->ops->set_fmt()。

c)如果dai_link没有定义params,则调用soc_pcm_new来创建pcm device.

在soc_pcm_new中根据dai_link的dynamic(FE), no_pcm(BE)成员来用不同的函数创建pcm,并设置不同的ops.

复制代码

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

    if (rtd->dai_link->dynamic || rtd->dai_link->no_pcm) {
        playback = rtd->dai_link->dpcm_playback;
        capture = rtd->dai_link->dpcm_capture;
    } else {
        for (i = 0; i < rtd->num_codecs; i++) {
            codec_dai = rtd->codec_dais[i];
            if (codec_dai->driver->playback.channels_min)
                playback = 1;
            if (codec_dai->driver->capture.channels_min)
                capture = 1;
        }

        capture = capture && cpu_dai->driver->capture.channels_min;
        playback = playback && cpu_dai->driver->playback.channels_min;
    }

    if (rtd->dai_link->playback_only) {
        playback = 1;
        capture = 0;
    }

    if (rtd->dai_link->capture_only) {
        playback = 0;
        capture = 1;
    }

    /* create the PCM */
    if (rtd->dai_link->no_pcm) {
        snprintf(new_name, sizeof(new_name), "(%s)",
            rtd->dai_link->stream_name);

        ret = snd_pcm_new_internal(rtd->card->snd_card, new_name, num,
                playback, capture, &pcm);
    } else {
        if (rtd->dai_link->dynamic)
            snprintf(new_name, sizeof(new_name), "%s (*)",
                rtd->dai_link->stream_name);
        else
            snprintf(new_name, sizeof(new_name), "%s %s-%d",
                rtd->dai_link->stream_name,
                (rtd->num_codecs > 1) ?
                "multicodec" : rtd->codec_dai->name, num);

        ret = snd_pcm_new(rtd->card->snd_card, new_name, num, playback,
            capture, &pcm);
    }
    if (ret < 0) {
        dev_err(rtd->card->dev, "ASoC: can't create pcm for %s\n",
            rtd->dai_link->name);
        return ret;
    }
    dev_dbg(rtd->card->dev, "ASoC: registered pcm #%d %s\n",num, new_name);

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

    pcm->nonatomic = rtd->dai_link->nonatomic;
    rtd->pcm = pcm;
    pcm->private_data = rtd;

    if (rtd->dai_link->no_pcm) {
        if (playback)
            pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream->private_data = rtd;
        if (capture)
            pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream->private_data = rtd;
        goto out;
    }

    /* ASoC PCM operations */
    if (rtd->dai_link->dynamic) {
        rtd->ops.open        = dpcm_fe_dai_open;
        rtd->ops.hw_params    = dpcm_fe_dai_hw_params;
        rtd->ops.prepare    = dpcm_fe_dai_prepare;
        rtd->ops.trigger    = dpcm_fe_dai_trigger;
        rtd->ops.hw_free    = dpcm_fe_dai_hw_free;
        rtd->ops.close        = dpcm_fe_dai_close;
        rtd->ops.pointer    = soc_pcm_pointer;
        rtd->ops.ioctl        = soc_pcm_ioctl;
    } else {
        rtd->ops.open        = soc_pcm_open;
        rtd->ops.hw_params    = soc_pcm_hw_params;
        rtd->ops.prepare    = soc_pcm_prepare;
        rtd->ops.trigger    = soc_pcm_trigger;
        rtd->ops.hw_free    = soc_pcm_hw_free;
        rtd->ops.close        = soc_pcm_close;
        rtd->ops.pointer    = soc_pcm_pointer;
        rtd->ops.ioctl        = soc_pcm_ioctl;
    }

    if (platform->driver->ops) {
        rtd->ops.ack        = platform->driver->ops->ack;
        rtd->ops.copy        = platform->driver->ops->copy;
        rtd->ops.silence    = platform->driver->ops->silence;
        rtd->ops.page        = platform->driver->ops->page;
        rtd->ops.mmap        = platform->driver->ops->mmap;
    }

    if (playback)
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);

    if (capture)
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &rtd->ops);

    if (platform->driver->pcm_new) {
        ret = platform->driver->pcm_new(rtd);
        if (ret < 0) {
            dev_err(platform->dev,
                "ASoC: pcm constructor failed: %d\n",
                ret);
            return ret;
        }
    }

    pcm->private_free = platform->driver->pcm_free;
out:
    dev_info(rtd->card->dev, "%s <-> %s mapping ok\n",
         (rtd->num_codecs > 1) ? "multicodec" : rtd->codec_dai->name,
         cpu_dai->name);
    return ret;
}

复制代码

 3.8调用snd_soc_dapm_link_dai_widgets来link dai widget和非dai widget.

snd_soc_dapm_link_dai_widgets函数会去遍历每一个dai widgets,然后遍历所有的非dai widgets,如果非dai widgets的stream name与dai widgets的name相同,则把两个widgets进行链接。这也是为什么创建dai widgets时name一定要是stream name的原因之一了。

复制代码

int snd_soc_dapm_link_dai_widgets(struct snd_soc_card *card)
{
    struct snd_soc_dapm_widget *dai_w, *w;
    struct snd_soc_dapm_widget *src, *sink;
    struct snd_soc_dai *dai;

    /* For each DAI widget... */
    list_for_each_entry(dai_w, &card->widgets, list) {
        switch (dai_w->id) {
        case snd_soc_dapm_dai_in:
        case snd_soc_dapm_dai_out:
            break;
        default:
            continue;
        }

        /* let users know there is no DAI to link */
        if (!dai_w->priv) {
            dev_dbg(card->dev, "dai widget %s has no DAI\n",
                dai_w->name);
            continue;
        }

        dai = dai_w->priv;

        /* ...find all widgets with the same stream and link them */
        list_for_each_entry(w, &card->widgets, list) {
            if (w->dapm != dai_w->dapm)
                continue;

            switch (w->id) {
            case snd_soc_dapm_dai_in:
            case snd_soc_dapm_dai_out:
                continue;
            default:
                break;
            }

            if (!w->sname || !strstr(w->sname, dai_w->sname))
                continue;

            if (dai_w->id == snd_soc_dapm_dai_in) {
                src = dai_w;
                sink = w;
            } else {
                src = w;
                sink = dai_w;
            }
            dev_dbg(dai->dev, "%s -> %s\n", src->name, sink->name);
            snd_soc_dapm_add_path(w->dapm, src, sink, NULL, NULL);
        }
    }

    return 0;
}

复制代码

3.9调用snd_soc_dapm_connect_dai_link_widgets() link CPU BE DAI widget 和 codec DAI widget.

复制代码

void snd_soc_dapm_connect_dai_link_widgets(struct snd_soc_card *card)
{
    struct snd_soc_pcm_runtime *rtd;

    /* for each BE DAI link... */
    list_for_each_entry(rtd, &card->rtd_list, list)  {
        /*
         * dynamic FE links have no fixed DAI mapping.
         * CODEC<->CODEC links have no direct connection.
         */
        if (rtd->dai_link->dynamic || rtd->dai_link->params)
            continue;

        dapm_connect_dai_link_widgets(card, rtd);
    }
}

static void dapm_connect_dai_link_widgets(struct snd_soc_card *card,
                      struct snd_soc_pcm_runtime *rtd)
{
    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
    struct snd_soc_dapm_widget *sink, *source;
    int i;

    for (i = 0; i < rtd->num_codecs; i++) {
        struct snd_soc_dai *codec_dai = rtd->codec_dais[i];

        /* connect BE DAI playback if widgets are valid */
        if (codec_dai->playback_widget && cpu_dai->playback_widget) {
            source = cpu_dai->playback_widget;
            sink = codec_dai->playback_widget;
            dev_dbg(rtd->dev, "connected DAI link %s:%s -> %s:%s\n",
                cpu_dai->component->name, source->name,
                codec_dai->component->name, sink->name);

            snd_soc_dapm_add_path(&card->dapm, source, sink,
                NULL, NULL);
        }

        /* connect BE DAI capture if widgets are valid */
        if (codec_dai->capture_widget && cpu_dai->capture_widget) {
            source = codec_dai->capture_widget;
            sink = cpu_dai->capture_widget;
            dev_dbg(rtd->dev, "connected DAI link %s:%s -> %s:%s\n",
                codec_dai->component->name, source->name,
                cpu_dai->component->name, sink->name);

            snd_soc_dapm_add_path(&card->dapm, source, sink,
                NULL, NULL);
        }
    }
}

复制代码

3.10如果存在card->controls,调用snd_soc_add_card_controls()创建kcontrol,并添加kcontrol到card->snd_card->controls链表中。

3.11如果存在card->dapm_routes,调用snd_soc_dapm_add_routes()创建dapm path,并将path加到card->paths链表中。

3.12调用snd_soc_dapm_new_widgets,遍历card->widgets链表,对带有control的widget(mixer/mux/pga)创建kcontrol,并添加到card->snd_card->controls链表中。读取widget 当前寄存器的值,初始化widget的power状态,将widget加到card->dapm_dirty链表中,最后调用dapm_power_widget() 统一处理card->dapm_dirty链表里所有widget的power状态的变化。

复制代码

/**
 * snd_soc_dapm_new_widgets - add new dapm widgets
 * @card: card to be checked for new dapm widgets
 *
 * Checks the codec for any new dapm widgets and creates them if found.
 *
 * Returns 0 for success.
 */
int snd_soc_dapm_new_widgets(struct snd_soc_card *card)
{
    struct snd_soc_dapm_widget *w;
    unsigned int val;

    mutex_lock_nested(&card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);

    list_for_each_entry(w, &card->widgets, list)
    {
        if (w->new)
            continue;

        if (w->num_kcontrols) {
            w->kcontrols = kzalloc(w->num_kcontrols *
                        sizeof(struct snd_kcontrol *),
                        GFP_KERNEL);
            if (!w->kcontrols) {
                mutex_unlock(&card->dapm_mutex);
                return -ENOMEM;
            }
        }

        switch(w->id) {
        case snd_soc_dapm_switch:
        case snd_soc_dapm_mixer:
        case snd_soc_dapm_mixer_named_ctl:
            dapm_new_mixer(w);
            break;
        case snd_soc_dapm_mux:
        case snd_soc_dapm_demux:
            dapm_new_mux(w);
            break;
        case snd_soc_dapm_pga:
        case snd_soc_dapm_out_drv:
            dapm_new_pga(w);
            break;
        case snd_soc_dapm_dai_link:
            dapm_new_dai_link(w);
            break;
        default:
            break;
        }

        /* Read the initial power state from the device */
        if (w->reg >= 0) {
            soc_dapm_read(w->dapm, w->reg, &val);
            val = val >> w->shift;
            val &= w->mask;
            if (val == w->on_val)
                w->power = 1;
        }

        w->new = 1;

        dapm_mark_dirty(w, "new widget");
        dapm_debugfs_add_widget(w);
    }

    dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);
    mutex_unlock(&card->dapm_mutex);
    return 0;
}

复制代码

3.13调用snd_card_register()注册card->snd_card.

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页