此文档仅作为开发随笔记录文档,可作为正式文档做材料参考,但不做正式文档。
Written bywolfgang huang([email protected])
此类文档仅记录Android4.1.2+Kernel2.6.37+OMAP3730平台ALSA开发及内核要点,备注好资料应用,以供后续开发人员快速入手,也可作为科普类资料,供其他相关人员学习。
ALSA System on Chip(ASOC)
本系列文档的目标是使读者从安卓HAL一直走到硬件层的处理流程,使Kernel的ALSA构架内部细节透露出来。但是由于鄙人水平有限,难免有错误理解,如有错误,请联系,立即改正。
下面我们细述Linux音频子系统中的ASOC,对于整体的框架作图如下。

我们接下来分块的对ASOC的子模块在内核中如何启动,注册等运作流程。
在linux设备模型下,device与相应的driver相匹配,从而支持linux透过drvier对于其绑定的device做到控制管理。故使对应的设备要在linux下进行良好的工作,则必须正确加载device和driver。
Platform模块:
对于platform的设备启动流程如下:
omap_init_audio->platform_device_register(&omap_pcm);
将omap_pcm的设备注册到platform总线的设备链表上。
对于platform的驱动,则有:
snd_omap_pcm_init->platform_driver_register(&omap_pcm_driver);
将omap_pcm_driver注册到platform总线的驱动链表上。
由linux驱动模型,当注册driver或device时,都会到其隶属总线的对应链表上去查找匹配的驱动或设备(linux更相信设备)。这样匹配后调用对应的driver->probe,则调用到我们的omap_pcm_probe,其调用snd_soc_register_platform,注册omap_soc_platform到ASOC的platform_list中,且由于device中对于其id设置为-1,则platform的名称设置为dev->driver->name。
Note:linux驱动模型下,platform总线绑定的依据有三种,of设备树的dtb方式,还有id_table的方式,最后就是默认的name比对方式。对于其他总线,诸如SPI用modalias比对等方式,此处不做过多细述。
Cpu_dai模块:
对于cpu_dai启动流程如下:
omap_init_audio->platform_device_register(&omap_mcbsp1);
将omap_mcbsp的设备注册到platform总线的设备链表上。
对于cpu_dai的驱动,则有:
snd_omap_mcbsp_init->platform_driver_register(&asoc_mcbsp_driver);
将asoc_mcbsp_driver注册到platform总线的驱动链表上。
由上面分析,我们知道驱动模型绑定后,根据名称都为omap-mcbsp-dai匹配后,调用到其驱动的probe接口,即asoc_mcbsp_probe。其调用snd_soc_register_dai,将omap_mcbsp_dai注册到ASOC的dai_list链表上。
Codec模块和Codec_dai模块:
在我们使用的OMAP平台上,codec使用的集成的TPS65951,其设备加载过程较为复杂,下面进行细述。
其设备的加载流程:
devkit8000_init->devkit8000_i2c_init->omap_register_i2c_bus(..devkit8000_i2c1_boardinfo)。
tps65951,使用的是twl4030兼容的驱动。
twl_init->i2c_add_driver(&twl_driver)<throughid_table(与上面的boardinfo匹配)>
->twl_probe->add_children->add_child<has_codec成立的条件下>
->platform_device_add()<name=”twl4030-audio”>。
将tps65951中的音频声卡codec设备加入到platform总线的设备链表。
但是此处并没有完成codec资源的解析,其又调用一次匹配后,进行资源的分块解析。
该过程为:
twl4030_codec_init->platform_driver_register(&twl4030_codec_driver);
与上面注册的codec设备匹配后,运行其驱动的probe,即twl4030_codec_probe,
其完成对于codec时钟的设置后,调用mfd_add_devices将获得资源重组到新的platform_device中,并将其加入到platform总线上,其名称为twl4030_codec。此时才是最终的tps65951设备声卡加入的真正设备资源。
对于其真正的设备资源,对应的驱动有:
twl4030_modinit-> platform_driver_register(&twl4030_codec_driver);<name=”twl4030_codec”>。
则匹配后调用其驱动的probe,即twl4030_codec_probe(与上面的有区别)。其调用snd_soc_register_codec,注册了对应的
则根据驱动模型绑定后,我们知道运行其驱动的probe,即twl4030_codec_probe,其调用snd_soc_register_codec,注册soc_codec_dev_twl4030到ASOC的codec_list,并将对应的twl4030_dai注册到ASOC的dai_list中,twl4030_dai有hifi和voice两种模式,即codec支持这两种音频数据流。其最终根据ASOC的配置环境snd_soc_dai_link,选用对应的配置模式。
ASOC(Machine):
最后对应ASOC整体的框架注册过程:
对应其设备注册流程如下:
omap3beagle_soc_init-> platform_device_add(omap3beagle_snd_device);<name=”soc-audio”>
对应的驱动注册则有:
snd_soc_init-> platform_driver_register(&soc_driver);根据驱动模型绑定后,则会调用到soc_probe函数。其获取ASOC定义的snd_soc_card配置数据snd_soc_omap3beagle。
snd_soc_card中的成员num_link定义有几个snd_soc_pcm_runtime,对于该处为1,成员dai_link,则定义了该snd_card对应的配置环境数据。
其结构定义如下:
-
struct snd_soc_dai_link {
-
/* config - must be set by machine driver */
-
const char *name; /* Codec name */
-
const char *stream_name; /* Stream name */
-
const char *codec_name; /* for multi-codec */
-
const char *platform_name; /* for multi-platform */
-
const char *cpu_dai_name;
-
const char *codec_dai_name;
-
-
/* Keep DAI active over suspend */
-
unsigned int ignore_suspend:1;
-
-
/* Symmetry requirements */
-
unsigned int symmetric_rates:1;
-
-
/* codec/machine specific init - e.g. add machine controls */
-
int (*init)(struct snd_soc_pcm_runtime *rtd);
-
-
/* machine stream operations */
-
struct snd_soc_ops *ops;
-
};
根据注释,在观察我们的具体配置:
-
static struct snd_soc_dai_link omap3beagle_dai = {
-
.name = "TWL4030",
-
.stream_name = "TWL4030", //runtime名称
-
.cpu_dai_name = "omap-mcbsp-dai.1", //cpu_dai<I2S>
-
.platform_name = "omap-pcm-audio", //platform
-
.codec_dai_name = "twl4030-hifi", //codec_dai,选择高保真音频数据流格式
-
.codec_name = "twl4030-codec", //tps65951
-
.ops = &omap3beagle_ops,
-
};
soc_probe获取对应的snd_soc_card配置数据后,调用snd_soc_register_card。
-
static int snd_soc_register_card(struct snd_soc_card *card)
-
{
-
int i;
-
-
if (!card->name || !card->dev)
-
return -EINVAL;
-
-
card->rtd = kzalloc(sizeof(struct snd_soc_pcm_runtime) * card->num_links,
-
GFP_KERNEL);
-
if (card->rtd == NULL)
-
return -ENOMEM;
-
-
for (i = 0; i < card->num_links; i++)
-
card->rtd[i].dai_link = &card->dai_link[i];
-
-
INIT_LIST_HEAD(&card->list);
-
card->instantiated = 0;
-
mutex_init(&card->mutex);
-
-
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;
-
}
根据配置数据的num_links,分配对应的snd_soc_pcm_runtime,并绑定各自snd_soc_pcm_runtime与dai_link,接着将加入的snd_soc_card加入到card_list,ASOC最多支持八个snd_soc_card。最后调用snd_soc_instantiate_cards。该函数在其他的注册接口也有调用,但是其只有在dai_link中的组件全部都组建完成才能继续往下走。故在此处综述。
snd_soc_instantiate_cards:遍历card_list,依次调用snd_soc_instantiate_card。
-
static void snd_soc_instantiate_card(struct snd_soc_card *card)
-
{
-
struct platform_device *pdev = to_platform_device(card->dev);
-
int ret, i;
-
-
mutex_lock(&card->mutex);
-
-
if (card->instantiated) {
-
mutex_unlock(&card->mutex);
-
return;
-
}
-
-
/* bind DAIs */
-
for (i = 0; i < card->num_links; i++)
-
soc_bind_dai_link(card, i);
-
-
/* bind completed ? */
-
if (card->num_rtd != card->num_links) {
-
mutex_unlock(&card->mutex);
-
return;
-
}
-
-
/*
-
* #define SNDRV_DEFAULT_IDX1 (-1)
-
* #define SNDRV_DEFAULT_STR1 NULL
-
*/
-
-
/* card bind complete so register a sound card */
-
ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
-
card->owner, 0, &card->snd_card);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: can't create sound card for card %s\n",
-
card->name);
-
mutex_unlock(&card->mutex);
-
return;
-
}
-
card->snd_card->dev = card->dev;
-
-
#ifdef CONFIG_PM
-
/* deferred resume work */
-
INIT_WORK(&card->deferred_resume_work, soc_resume_deferred);
-
#endif
-
-
/* initialise the sound card only once */
-
if (card->probe) {
-
ret = card->probe(pdev);
-
if (ret < 0)
-
goto card_probe_error;
-
}
-
-
for (i = 0; i < card->num_links; i++) {
-
ret = soc_probe_dai_link(card, i);
-
if (ret < 0) {
-
pr_err("asoc: failed to instantiate card %s: %d\n",
-
card->name, ret);
-
goto probe_dai_err;
-
}
-
}
-
-
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->name);
-
-
ret = snd_card_register(card->snd_card);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to register soundcard for %s\n", card->name);
-
goto probe_dai_err;
-
}
-
-
#ifdef CONFIG_SND_SOC_AC97_BUS
-
/* register any AC97 codecs */
-
for (i = 0; i < card->num_rtd; i++) {
-
ret = soc_register_ac97_dai_link(&card->rtd[i]);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to register AC97 %s\n", card->name);
-
while (--i >= 0)
-
soc_unregister_ac97_dai_link(&card->rtd[i]);
-
goto probe_dai_err;
-
}
-
}
-
#endif
-
-
card->instantiated = 1;
-
mutex_unlock(&card->mutex);
-
return;
-
-
probe_dai_err:
-
for (i = 0; i < card->num_links; i++)
-
soc_remove_dai_link(card, i);
-
-
card_probe_error:
-
if (card->remove)
-
card->remove(pdev);
-
-
snd_card_free(card->snd_card);
-
-
mutex_unlock(&card->mutex);
-
}
我们把该函数拆分为四段函数进行分析,分别为soc_bind_dai_link,snd_card_create,soc_probe_dai_link及snd_card_register。AC97我们在此略过,一般嵌入式设备都使用I2S。
-
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_codec *codec;
-
struct snd_soc_platform *platform;
-
struct snd_soc_dai *codec_dai, *cpu_dai;
-
-
if (rtd->complete)
-
return 1;
-
dev_dbg(card->dev, "binding %s at idx %d\n", dai_link->name, num);
-
-
/* do we already have the CPU DAI for this link ? */
-
if (rtd->cpu_dai) {
-
goto find_codec;
-
}
-
/* no, then find CPU DAI from registered DAIs*/
-
list_for_each_entry(cpu_dai, &dai_list, list) {
-
//依次遍历,查找是否有dai_link按名称指定的cpu_dai
-
if (!strcmp(cpu_dai->name, dai_link->cpu_dai_name)) {
-
-
if (!try_module_get(cpu_dai->dev->driver->owner))
-
return -ENODEV;
-
-
rtd->cpu_dai = cpu_dai;
-
goto find_codec;
-
}
-
}
-
dev_dbg(card->dev, "CPU DAI %s not registered\n",
-
dai_link->cpu_dai_name);
-
-
find_codec:
-
/* do we already have the CODEC for this link ? */
-
if (rtd->codec) {
-
goto find_platform;
-
}
-
-
/* no, then find CODEC from registered CODECs*/
-
list_for_each_entry(codec, &codec_list, list) {
-
//依次遍历,查找是否有dai_link按名称指定的codec
-
if (!strcmp(codec->name, dai_link->codec_name)) {
-
rtd->codec = codec;
-
-
if (!try_module_get(codec->dev->driver->owner))
-
return -ENODEV;
-
-
/* CODEC found, so find CODEC DAI from registered DAIs from this CODEC*/
-
list_for_each_entry(codec_dai, &dai_list, list) {
-
//依次遍历,查找是否有dai_link按名称指定的codec_dai
-
if (codec->dev == codec_dai->dev &&
-
!strcmp(codec_dai->name, dai_link->codec_dai_name)) {
-
rtd->codec_dai = codec_dai;
-
goto find_platform;
-
}
-
}
-
dev_dbg(card->dev, "CODEC DAI %s not registered\n",
-
dai_link->codec_dai_name);
-
-
goto find_platform;
-
}
-
}
-
dev_dbg(card->dev, "CODEC %s not registered\n",
-
dai_link->codec_name);
-
-
find_platform:
-
/* do we already have the CODEC DAI for this link ? */
-
if (rtd->platform) {
-
goto out;
-
}
-
/* no, then find CPU DAI from registered DAIs*/
-
list_for_each_entry(platform, &platform_list, list) {
-
//依次遍历,查找是否有dai_link按名称指定的platform
-
if (!strcmp(platform->name, dai_link->platform_name)) {
-
-
if (!try_module_get(platform->dev->driver->owner))
-
return -ENODEV;
-
-
rtd->platform = platform;
-
goto out;
-
}
-
}
-
-
dev_dbg(card->dev, "platform %s not registered\n",
-
dai_link->platform_name);
-
return 0;
-
-
out:
-
/* mark rtd as complete if we found all 4 of our client devices */
-
if (rtd->codec && rtd->codec_dai && rtd->platform && rtd->cpu_dai) {
-
rtd->complete = 1;
-
card->num_rtd++;
-
}
-
return 1;
-
}
其完成的工作单调而统一,就是依次比对dai_link指定的四大模块,缺一不可,且对应于多个num_links的还需要一一绑定。找到对应的dai_link指定的模块后,就将runtime与其对应模块进行绑定。
-
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);
-
……
-
/* 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) {
-
snd_printk(KERN_ERR "unable to register control minors\n");
-
goto __error;
-
}
-
err = snd_info_card_create(card);
-
if (err < 0) {
-
snd_printk(KERN_ERR "unable to create card info\n");
-
goto __error_ctl;
-
}
-
……
-
return err;
-
}
其分配对应的snd_card,并分配对应的id,且ASOC最大支持八个snd_card。之后调用snd_ctl_create->snd_device_new(card, SNDRV_DEV_CONTROL,…);注册该snd_card绑定的mixer控制设备节点,controlCx。且其目前只是创建,在后面snd_card_register的时候注册到用户空间设备节点。该函数调用snd_info_card_create,创建/proc/asound/cardX,向用户透露信息。
-
static int soc_probe_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_codec *codec = rtd->codec;
-
struct snd_soc_platform *platform = rtd->platform;
-
struct snd_soc_dai *codec_dai = rtd->codec_dai, *cpu_dai = rtd->cpu_dai;
-
int ret;
-
-
dev_dbg(card->dev, "probe %s dai link %d\n", card->name, num);
-
-
/* config components */
-
codec_dai->codec = codec;
-
codec->card = card;
-
cpu_dai->platform = platform;
-
rtd->card = card;
-
rtd->dev.parent = card->dev;
-
codec_dai->card = card;
-
cpu_dai->card = card;
-
-
/* set default power off timeout */
-
rtd->pmdown_time = pmdown_time;
-
-
/* probe the cpu_dai */
-
if (!cpu_dai->probed) {
-
if (cpu_dai->driver->probe) {
-
ret = cpu_dai->driver->probe(cpu_dai);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to probe CPU DAI %s\n",
-
cpu_dai->name);
-
return ret;
-
}
-
}
-
cpu_dai->probed = 1;
-
/* mark cpu_dai as probed and add to card cpu_dai list */
-
list_add(&cpu_dai->card_list, &card->dai_dev_list);
-
}
-
-
/* probe the CODEC */
-
if (!codec->probed) {
-
if (codec->driver->probe) {
-
ret = codec->driver->probe(codec);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to probe CODEC %s\n",
-
codec->name);
-
return ret;
-
}
-
}
-
-
soc_init_codec_debugfs(codec);
-
-
/* mark codec as probed and add to card codec list */
-
codec->probed = 1;
-
list_add(&codec->card_list, &card->codec_dev_list);
-
}
-
-
/* probe the platform */
-
if (!platform->probed) {
-
if (platform->driver->probe) {
-
ret = platform->driver->probe(platform);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to probe platform %s\n",
-
platform->name);
-
return ret;
-
}
-
}
-
/* mark platform as probed and add to card platform list */
-
platform->probed = 1;
-
list_add(&platform->card_list, &card->platform_dev_list);
-
}
-
-
/* probe the CODEC DAI */
-
if (!codec_dai->probed) {
-
if (codec_dai->driver->probe) {
-
ret = codec_dai->driver->probe(codec_dai);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to probe CODEC DAI %s\n",
-
codec_dai->name);
-
return ret;
-
}
-
}
-
-
/* mark cpu_dai as probed and add to card cpu_dai list */
-
codec_dai->probed = 1;
-
list_add(&codec_dai->card_list, &card->dai_dev_list);
-
}
-
-
/* DAPM dai link stream work */
-
INIT_DELAYED_WORK(&rtd->delayed_work, close_delayed_work);
-
-
/* now that all clients have probed, initialise the DAI link */
-
if (dai_link->init) {
-
ret = dai_link->init(rtd);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to init %s\n", dai_link->stream_name);
-
return ret;
-
}
-
}
-
-
/* Make sure all DAPM widgets are instantiated */
-
snd_soc_dapm_new_widgets(codec);
-
snd_soc_dapm_sync(codec);
-
-
/* register the rtd device */
-
rtd->dev.release = rtd_release;
-
rtd->dev.init_name = dai_link->name;
-
ret = device_register(&rtd->dev); /* TWL4030 in the /sys/devices/platform/soc-audio */
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: failed to register DAI runtime device %d\n", ret);
-
return ret;
-
}
-
-
rtd->dev_registered = 1;
-
ret = device_create_file(&rtd->dev, &dev_attr_pmdown_time);
-
if (ret < 0)
-
printk(KERN_WARNING "asoc: failed to add pmdown_time sysfs\n");
-
-
/* add DAPM sysfs entries for this codec */
-
ret = snd_soc_dapm_sys_add(&rtd->dev);
-
if (ret < 0)
-
printk(KERN_WARNING "asoc: failed to add codec dapm sysfs entries\n");
-
-
/* add codec sysfs entries */
-
ret = device_create_file(&rtd->dev, &dev_attr_codec_reg);
-
if (ret < 0)
-
printk(KERN_WARNING "asoc: failed to add codec sysfs files\n");
-
-
/* create the pcm */
-
ret = soc_new_pcm(rtd, num);
-
if (ret < 0) {
-
printk(KERN_ERR "asoc: can't create pcm %s\n", dai_link->stream_name);
-
return ret;
-
}
-
-
/* add platform data for AC97 devices */
-
if (rtd->codec_dai->driver->ac97_control)
-
snd_ac97_dev_add_pdata(codec->ac97, rtd->cpu_dai->ac97_pdata);
-
-
return 0;
-
}
该函数对四个模块依次调用probe接口,这些probe函数不进行细述,主要是对于声卡的初始化,并把声卡内部的控件加入到系统snd_kcontrol中,其他模块未做主要事宜或者没有probe接口。接着初始化close_delayed_work工作队列,其在关闭接口时,用于延时调用,防止pop刺耳噪音。接下来对于DAPM的初始化和同步。开始DAPM将加入的codec->dapm_widget进行分类型初始化,并绑定相关的电源操作接口,最后以snd_kcontrol提供用户控制。调用snd_soc_dapm_sync,对加入的控件分类加到上电和下电的链表中,并根据情况对不同的链表上下电处理。之后就是相关sysfs属性文件的注册,注册到sysfs文件系统中。最后调用soc_new_pcm,根据对应配置的codec_dai的packback和capture创建对应的pcm设备空间,并完成其初始化,绑定对应的操作接口,但是其也要在最后snd_card_register的时候才能注册到sysfs和devtmpfs中。
snd_card_register->snd_device_register_all:完成controlCx,pcmCxDx(p/c)的到sysfs和devtmpfs文件系统的注册。<timer在alsa_timer_init -> snd_register_device 完成,不过我们不关注>
通过上述分析,我们知道了音频子系统,用户操作的接口在内核中是经过了怎样的流程才注册到用户空间。对于具体操作这些设备节点,又会是怎样的流程我们在后续的文档进行分析。对于访问具体的控件,调整路径,电源控制,都留在后续的文档一一分析。
下面我们以图表的形式分析这些设备节点的注册过程。

