Linux音頻子系統(七)ASoC架構中的Machine

前面一節的內容我們提到,ASoC被分爲Machine、Platform和Codec三大部分,並且介紹了其軟硬件框架和基本的數據流,其中的Machine驅動負責Platform和Codec之間的耦合以及部分和設備或板子特定的代碼,本章的主要內容是

  1. Machine如何驅動負責處理機器特有的一些控件和音頻事件
  2. Machine驅動如何把Platform和Codec結合在一起完成整個設備的音頻處理工作。

ASoC的一切都從Machine驅動開始,包括聲卡的註冊,綁定Platform和Codec驅動等等,下面就讓我們從Machine驅動開始討論吧。

1. 硬件設計

我們以mini2440開發板爲例,其對於音頻的設計硬件設計框圖如下,對於系統中用到的基本知識,例如I2S/L3的硬件管腳定義和描述,基本的協議本章不做介紹。
在這裏插入圖片描述

  • 當需要發出聲音信號的時候,數據從內存通過系統總線進入soc的I2S模塊,I2S模塊再把數據發送到UDA1341,然後通過揚聲器輸出
  • 當需要接受聲音信號的時候,數據從外界通過聲音採集設備,進入UDA1341,然後I2S模塊,最後通過系統總線傳輸到內存

2. 註冊Platform Device

ASoC把聲卡註冊爲Platform Device,我們以裝配有UDA1341的一款Samsung的開發板SMDK爲例子做說明,UDA1341是多功能Codec芯片
代碼的位於:/sound/soc/samsung/s3c24xx_uda134x.c,我們關注模塊的初始化函數:

static int s3c24xx_uda134x_probe(struct platform_device *pdev)
{
	int ret;

	printk(KERN_INFO "S3C24XX_UDA134X SoC Audio driver\n");

	s3c24xx_uda134x_l3_pins = pdev->dev.platform_data;
	if (s3c24xx_uda134x_l3_pins == NULL) {
		printk(KERN_ERR "S3C24XX_UDA134X SoC Audio: "
		       "unable to find platform data\n");
		return -ENODEV;
	}
	s3c24xx_uda134x.power = s3c24xx_uda134x_l3_pins->power;
	s3c24xx_uda134x.model = s3c24xx_uda134x_l3_pins->model;

	if (s3c24xx_uda134x_setup_pin(s3c24xx_uda134x_l3_pins->l3_data,
				      "data") < 0)
		return -EBUSY;
	if (s3c24xx_uda134x_setup_pin(s3c24xx_uda134x_l3_pins->l3_clk,
				      "clk") < 0) {
		gpio_free(s3c24xx_uda134x_l3_pins->l3_data);
		return -EBUSY;
	}
	if (s3c24xx_uda134x_setup_pin(s3c24xx_uda134x_l3_pins->l3_mode,
				      "mode") < 0) {
		gpio_free(s3c24xx_uda134x_l3_pins->l3_data);
		gpio_free(s3c24xx_uda134x_l3_pins->l3_clk);
		return -EBUSY;
	}

	s3c24xx_uda134x_snd_device = platform_device_alloc("soc-audio", -1);
	if (!s3c24xx_uda134x_snd_device) {
		printk(KERN_ERR "S3C24XX_UDA134X SoC Audio: "
		       "Unable to register\n");
		return -ENOMEM;
	}

	platform_set_drvdata(s3c24xx_uda134x_snd_device,
			     &snd_soc_s3c24xx_uda134x);
	platform_device_add_data(s3c24xx_uda134x_snd_device, &s3c24xx_uda134x, sizeof(s3c24xx_uda134x));
	ret = platform_device_add(s3c24xx_uda134x_snd_device);
	if (ret) {
		printk(KERN_ERR "S3C24XX_UDA134X SoC Audio: Unable to add\n");
		platform_device_put(s3c24xx_uda134x_snd_device);
	}

	return ret;
}

由此可見,模塊初始化時,這個函數主要做了三件事情:

  1. 初始化一些配置信息,比較重要的L3接口的引腳定義
  2. 分配一個名爲soc-audio的平臺設備,有平臺設備,必定有平臺驅動,以soc-audio搜索,在Soc-core.c函數裏面有對應的平臺驅動,這個後面再做介紹
  3. 同時把smdk設到platform_device結構的dev.drvdata字段中,這裏引出了第一個數據結構snd_soc_card的實例smdk,同時也將uda134x_platform_data結構也添加到platform_device結構中

下面來看看第一個數據結構snd_soc_card,他的定義如下:

static struct snd_soc_ops s3c24xx_uda134x_ops = {
	.startup = s3c24xx_uda134x_startup,
	.shutdown = s3c24xx_uda134x_shutdown,
	.hw_params = s3c24xx_uda134x_hw_params,
};

static struct snd_soc_dai_link s3c24xx_uda134x_dai_link = {
	.name = "UDA134X",
	.stream_name = "UDA134X",
	.codec_name = "uda134x-codec",						//根據codec_name知道用哪一個編解碼芯片
	.codec_dai_name = "uda134x-hifi",					//codec_dai_name表示codec芯片裏的哪一個接口,有些編解碼芯片有多個接口
	.cpu_dai_name = "s3c24xx-iis",						//cpu_dai_name表示2440那一側的dai接口(IIs接口),
	.ops = &s3c24xx_uda134x_ops,
	.platform_name	= "samsung-audio",					//platform_name表示DMA 
};

static struct snd_soc_card snd_soc_s3c24xx_uda134x = {
	.name = "S3C24XX_UDA134X",
	.owner = THIS_MODULE,
	.dai_link = &s3c24xx_uda134x_dai_link,
	.num_links = 1,
};

通過snd_soc_card結構,又引出了Machine驅動的另外兩個個數據結構:

  • snd_soc_dai_link(實例:s3c24xx_uda134x_dai_link)【指定了Platform、Codec、codec_dai、cpu_dai的名字】
  • snd_soc_ops(實例:s3c24xx_uda134x_ops)【硬件的操作】

其中,snd_soc_dai_link中,指定了Platform、Codec、codec_dai、cpu_dai的名字,稍後Machine驅動將會利用這些名字去匹配已經在系統中註冊的platform,codec,dai,這些註冊的部件都是在另外相應的Platform驅動和Codec驅動的代碼文件中定義的,這樣看來,Machine驅動的設備初始化代碼無非就是選擇合適Platform和Codec以及dai,用他們填充以上幾個數據結構,然後註冊Platform設備即可。當然還要實現連接Platform和Codec的dai_link對應的ops實現,本例就是smdk_ops。

3. 註冊Platform Driver

按照Linux的設備模型,有platform_device,就一定會有platform_driver,搜索“soc-audio”得到ASoC的platform_driver在以下文件中定義:sound/soc/soc-core.c。

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

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

我們看到platform_driver的name字段爲soc-audio,正好與platform_device中的名字相同,按照Linux的設備模型,platform總線會匹配這兩個名字相同的device和driver,同時會觸發soc_probe的調用,它正是整個ASoC驅動初始化的入口。

static int soc_probe(struct platform_device *pdev)
{
	struct snd_soc_card *card = platform_get_drvdata(pdev);

	/*
	 * no card, so machine driver should be registering card
	 * we should not be here in that case so ret error
	 */
	if (!card)
		return -EINVAL;

	dev_warn(&pdev->dev,
		 "ASoC: machine %s should use snd_soc_register_card()\n",
		 card->name);

	/* Bodge while we unpick instantiation */
	card->dev = &pdev->dev;

	return snd_soc_register_card(card);
}

soc_probe函數本身很簡單,它先從platform_device參數中取出snd_soc_card,然後調用snd_soc_register_card,下面來看看這個函數主要做了些什麼?

	card->rtd = devm_kzalloc(card->dev,
				 sizeof(struct snd_soc_pcm_runtime) *
				 (card->num_links + card->num_aux_devs),
				 GFP_KERNEL);
	if (card->rtd == NULL)
		return -ENOMEM;
	card->num_rtd = 0;
	card->rtd_aux = &card->rtd[card->num_links];

	for (i = 0; i < card->num_links; i++) {
		card->rtd[i].card = card;
		card->rtd[i].dai_link = &card->dai_link[i];
		card->rtd[i].codec_dais = devm_kzalloc(card->dev,
					sizeof(struct snd_soc_dai *) *
					(card->rtd[i].dai_link->num_codecs),
					GFP_KERNEL);
		if (card->rtd[i].codec_dais == NULL)
			return -ENOMEM;
	}

爲snd_soc_pcm_runtime數組申請內存,每一個dai_link對應snd_soc_pcm_runtime數組的一個單元,然後把snd_soc_card中的dai_link配置複製到相應的snd_soc_pcm_runtime中,之後進入一系列的初始化,到核心的接口函數snd_soc_instantiate_card,進入該函數之後,馬上調用了soc_bind_dai_link

static int soc_bind_dai_link(struct snd_soc_card *card, int num)
{
	struct snd_soc_dai_link *dai_link = &card->dai_link[num];
	struct snd_soc_pcm_runtime *rtd = &card->rtd[num];
	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 = rtd->codec_dais;
	struct snd_soc_platform *platform;
	const char *platform_name;
	int i;

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

	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);
		return -EPROBE_DEFER;
	}

	rtd->num_codecs = dai_link->num_codecs;

	/* Find CODEC from registered CODECs */
	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);
			return -EPROBE_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);
		return -EPROBE_DEFER;
	}

	card->num_rtd++;

	return 0;
}

SoC定義了三個全局的鏈表頭變量: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驅動的信息。
那麼,這個鏈表的元素從哪裏來的呢?這些元素的名字則是由設備的名字(device->name)和對應的驅動的名字(device->driver->name)組成。這些元素的名字則是由設備的名字(device->name)和對應的驅動的名字(device->driver->name)組成,這個在上一章的框圖中已經介紹了。
之後通過調用snd_card_new來創建一個snd_card結構體

	/* 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;
	}

下面來看看 snd_card_new

int snd_card_new(struct device *parent, int idx, const char *xid,
		    struct module *module, int extra_size,
		    struct snd_card **card_ret)
{
	struct snd_card *card;
	int err;

	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 (extra_size > 0)
		card->private_data = (char *)card + sizeof(struct snd_card);
	if (xid)
		strlcpy(card->id, xid, sizeof(card->id));
	err = 0;
	mutex_lock(&snd_card_mutex);
	if (idx < 0) /* first check the matching module-name slot */
		idx = get_slot_from_bitmask(idx, module_slot_match, module);
	if (idx < 0) /* if not matched, assign an empty slot */
		idx = get_slot_from_bitmask(idx, check_empty_slot, module);
	if (idx < 0)
		err = -ENODEV;
	else if (idx < snd_ecards_limit) {
		if (test_bit(idx, snd_cards_lock))
			err = -EBUSY;	/* invalid */
	} else if (idx >= SNDRV_CARDS)
		err = -ENODEV;
	if (err < 0) {
		mutex_unlock(&snd_card_mutex);
		dev_err(parent, "cannot find the slot for index %d (range 0-%i), error: %d\n",
			 idx, snd_ecards_limit - 1, err);
		kfree(card);
		return err;
	}
	set_bit(idx, snd_cards_lock);		/* lock it */
	if (idx >= snd_ecards_limit)
		snd_ecards_limit = idx + 1; /* increase the limit */
	mutex_unlock(&snd_card_mutex);
	card->dev = parent;
	card->number = idx;
	card->module = module;
	INIT_LIST_HEAD(&card->devices);
	init_rwsem(&card->controls_rwsem);
	rwlock_init(&card->ctl_files_rwlock);
	mutex_init(&card->user_ctl_lock);
	INIT_LIST_HEAD(&card->controls);
	INIT_LIST_HEAD(&card->ctl_files);
	spin_lock_init(&card->files_lock);
	INIT_LIST_HEAD(&card->files_list);
#ifdef CONFIG_PM
	mutex_init(&card->power_lock);
	init_waitqueue_head(&card->power_sleep);
#endif

	device_initialize(&card->card_dev);
	card->card_dev.parent = parent;
	card->card_dev.class = sound_class;
	card->card_dev.release = release_card_device;
	card->card_dev.groups = card_dev_attr_groups;
	err = kobject_set_name(&card->card_dev.kobj, "card%d", idx);
	if (err < 0)
		goto __error;

	/* 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);
	if (err < 0) {
		dev_err(parent, "unable to register control minors\n");
		goto __error;
	}
	err = snd_info_card_create(card);
	if (err < 0) {
		dev_err(parent, "unable to create card info\n");
		goto __error_ctl;
	}
	*card_ret = card;
	return 0;

      __error_ctl:
	snd_device_free_all(card);
      __error:
	put_device(&card->card_dev);
  	return err;
}
  • 根據extra_size參數的大小分配內存,該內存區可以作爲芯片的專有數據使用
  • 拷貝聲卡的ID字符串,如果傳入的聲卡編號爲-1,自動分配一個索引編號,初始化snd_card結構
  • 建立邏輯設備snd_ctl_create:Control
  • snd_info_card_create建立proc文件中的info節點:通常就是/proc/asound/card0
    接着依次調用各個子結構的probe函數,首先會調用card->probe
	/* 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++) {
		for (i = 0; i < card->num_links; i++) {
			ret = soc_probe_link_components(card, i, order);
			if (ret < 0) {
				dev_err(card->dev,
					"ASoC: failed to instantiate card %d\n",
					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++) {
		for (i = 0; i < card->num_links; i++) {
			ret = soc_probe_link_dais(card, i, order);
			if (ret < 0) {
				dev_err(card->dev,
					"ASoC: failed to instantiate card %d\n",
					ret);
				goto probe_dai_err;
			}
		}
	}

	for (i = 0; i < card->num_aux_devs; i++) {
		ret = soc_probe_aux_dev(card, i);
		if (ret < 0) {
			dev_err(card->dev,
				"ASoC: failed to add auxiliary devices %d\n",
				ret);
			goto probe_aux_dev_err;
		}
	}

在soc_probe_link_dais調用了codec,dai和platform驅動的probe函數外,在最後還調用了soc_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);

	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;
}
  1. 該函數首先初始化snd_soc_runtime中的snd_pcm_ops字段,也就是rtd->ops中的部分成員,例如open,close,hw_params等,
  2. 調用標準alsa驅動中的創建pcm的函數snd_pcm_new()創建聲卡的pcm實例,pcm的private_data字段設置爲該runtime變量rtd,然後用platform驅動中的snd_pcm_ops替換部分pcm中的snd_pcm_ops字段,
  3. 調用platform驅動的pcm_new回調,該回調實現該platform下的dma內存申請和dma初始化等相關工作。到這裏,聲卡和他的pcm實例創建完成。

回到snd_soc_instantiate_card函數,完成snd_card和snd_pcm的創建後,接着對dapm和dai支持的格式做出一些初始化合設置工作後,調用了 card->late_probe(card)進行一些最後的初始化合設置工作,最後則是調用標準alsa驅動的聲卡註冊函數對聲卡進行註冊。 那麼我們對該函數做一些總結:

  1. 把相應的codec,dai和platform實例賦值到card->rtd[]中,再添加到card->rtd_list鏈表中,rtd也就是一個snd_soc_pcm_runtime結構體。
  2. 添加card->dai_link+i->list到card->dai_link_list
  3. 遍歷這個全局codec_list結構體,爲codec申請空間
  4. 標準的alsa函數創建聲卡實例
  5. 如果有的話,調用card的probe:card->probe(card),當然,我們這裏snd_soc_s3c24xx_uda134x這snd_soc_card結構體是沒設置有probe的
  6. 掃描card->rtd_list鏈表,調用cpu_dai、codec_dais、platform的component->probe。
  7. 掃描card->rtd_list鏈表 ,調用各個子結構(cpu_dai、codec_dais、platform)的probe函數,還通過soc_new_pcm函數創建了pcm邏輯設備。
  8. 對dapm和dai支持的格式做出一些初始化合設置工作後,調用了 card->late_probe(card)進行一些最後的初始化合設置工作
  9. 調用標準alsa驅動的聲卡註冊函數對聲卡進行註冊

至此,整個Machine驅動的初始化已經完成,通過各個子結構的probe調用,實際上,也完成了部分Platfrom驅動和Codec驅動的初始化工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章