一 声卡简介
一个音频系统由硬件和驱动这些硬件的软件组成。
在一台设备上,如果硬件完整,且驱动硬件的软件全部初始化成功后,ALSA系统就会注册一个声卡,通过这个声卡,应用程序可以控制硬件设备的链路联通,并向声卡写入数据或读取数据来完成播放与录音的音频功能。
在这里说的声卡不是配电脑时说的硬件声卡,而是Linux内核中的一个软件概念的声卡。
二 声卡目录下设备节点的作用
在注册声卡时,ALSA core会在dev/snd/下创建声卡的设备节点,在下面这张图中,可以很明显的看到有三种设备节点,分别是control设备,pcm设备,以及一个timer节点
PCM设备就是应用层直接操作的节点,应用层的tinyalsa库就是直接对这个节点进行读写来实现播放和录音的。
想知道tinyalsa怎么将数据写入PCM节点,可以看external\tinyalsa\pcm.c这个文件,在安卓的Audio HAL中,播放音乐时,就是通过调用pcm_write来将数据写下去。
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
struct snd_xferi x;
if (pcm->flags & PCM_IN)
return -EINVAL;
x.buf = (void*)data;
x.frames = count / (pcm->config.channels *
pcm_format_to_bits(pcm->config.format) / 8);
for (;;) {
if (!pcm->running) {
int prepare_error = pcm_prepare(pcm);
if (prepare_error)
return prepare_error;
if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x))
return oops(pcm, errno, "cannot write initial data");
pcm->running = 1;
return 0;
}
if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)) { //在这里写入数据
pcm->prepared = 0;
pcm->running = 0;
if (errno == EPIPE) {
/* we failed to make our window -- try to restart if we are
* allowed to do so. Otherwise, simply allow the EPIPE error to
* propagate up to the app level */
pcm->underruns++;
if (pcm->flags & PCM_NORESTART)
return -EPIPE;
continue;
}
return oops(pcm, errno, "cannot write stream data");
}
return 0;
}
}
control设备用于控制声卡的内部通路开关。为了节省功耗,在移动设备上音频通路不是一直接通的,只有在需要用到时,通路才会接通,
例如下面这个例子
当需要把音频输出到Speaker时,就需要通过操作control节点,来吧I2S0的RX1接到MUX1,而当需要输出到耳机时,则可以把RX1接到MUX2。
至于timer设备,目前我暂时没有用过。
Machine驱动
Machine驱动里面描述了当前设备上的各个组件的连接关系,从platform到codec再到Speaker,Machine驱动也是一个单独的驱动,能单独的注册到ALSA中。
ALSA core会在驱动程序注册最后一步尝试去初始化声卡,这里的驱动程序是指所有的音频驱动程序,包括platform,codec,Machine。
注册声卡时,会根据Machine驱动中定义的组件连接关系,去查找所需要的组件,如果此时音频组件没有全部注册完成,则声卡会初始化失败,并返回错误码 -517,这个错误码在Linux内核的含义是需要重试,当最后的组件注册完成时,ALSA会再次尝试初始化声卡。
Machine驱动描述了platform和codec之间的连接关系,通过连接关系,来找到对应的接口。
一条简单的音频链路如图所示,CPU DAI与Codec DAI相连接,进行数据传输。
在Machine驱动中,通过 struct snd_soc_dai_link 结构体来定义连接关系,DAI全称是Digital Audio Interface,以三星的s5pv4418开发板举例,定义一个简单dai_link如下所示
static struct snd_soc_dai_link es8316_dai_link = {
.name = "ASOC-ES8316",
.stream_name = "es8316 HiFi",
.cpu_dai_name = "c0055000.i2s", /* nx_snd_i2s_driver name */
.platform_name = "nexell-pcm", /* nx_snd_pcm_driver name */
.codec_dai_name = "ES8316 HiFi", /* es8316_dai's name */
.codec_name = "ES8316.0-0011", /* es8316_i2c_driver name
+ '.' + bus + '-'
+ address(7bit) */
.ops = &es8316_ops,
.symmetric_rates = 1,
.init = es8316_dai_init,
};
在这个定义中,可以很清楚的知道这条链路的源头DAI是c0055000.i2s,连接到codec的ES8316 HiFi这个接口上。
在dai_link描述了完整的链路信息后,就可以用这个dai_link来注册声卡,为此,需要创建一个声卡实例。
static struct snd_soc_card es8316_card = {
.name = "I2S-ES8316", /* proc/asound/cards */
.owner = THIS_MODULE,
.dai_link = &es8316_dai_link,
.num_links = 1,
.suspend_pre = &es8316_suspend_pre,
.resume_pre = &es8316_resume_pre,
.resume_post = &es8316_resume_post,
.dapm_widgets = es8316_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(es8316_dapm_widgets),
.dapm_routes = es8316_audio_map,
.num_dapm_routes = ARRAY_SIZE(es8316_audio_map),
};
暂时只需要知道dai_link这个成员的含义。
有了声卡实例之后,可以用snd_soc_register_card来注册声卡,这个函数只需要传入一个声卡实例,这里省略了大量对平台的gpio,memory,clock进行初始化的代码,只对ALSA注册声卡进行分析
int snd_soc_register_card(struct snd_soc_card *card)
{
int i, j, ret;
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 = snd_soc_init_multicodec(card, link);
if (ret) {
dev_err(card->dev, "ASoC: failed to init multicodec\n");
return ret;
}
for (j = 0; j < link->num_codecs; j++) { //有很多种定义dai_link的形式,这个循环内是对dai_link的数据进行检查以及标准化
/*
* Codec must be specified by 1 of name or OF node,
* not both or neither.
*/
if (!!link->codecs[j].name ==
!!link->codecs[j].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[j].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;
}
}
dev_set_drvdata(card->dev, card);
snd_soc_initialize_card_lists(card); //对声卡结构体内的几个链表头进行初始化
card->rtd = devm_kzalloc(card->dev, //根据 dai_link 创建rtd
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;
}
for (i = 0; i < card->num_aux_devs; i++)
card->rtd_aux[i].card = card;
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 */
for (i = 0; i < card->num_rtd; i++) {
struct snd_soc_pcm_runtime *rtd = &card->rtd[i];
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;
}
EXPORT_SYMBOL_GPL(snd_soc_register_card);
比较核心的是在snd_soc_instantiate_card,在这里才是真正的对声卡进行初始化,会依次调用dai_link上所有组建的初始化函数,来对系统中的所有组件进行初始化,这个函数非常复杂,需要在看动platform和codec驱动,以及dapm之后回来看才能看懂。