计划分成下面8章来详细展开,后面再根据实际情况做调整。
以上内容基于tinyalsa展开,尽量剔除厂商的差异性。
本节讲解了ALSA和ASOC的框架流程,并介绍了声卡的注册过程。
ALSA是Advanced Linux Sound Architecture
的缩写,高级Linux声音架构的简称,它在Linux操作系统上提供了音频和MIDI(Musical Instrument Digital Interface
,音乐设备数字化接口)的支持.
一般是在pc机上使用,使用PCI接口
声卡节点如图:
既然我们说ALSA
是标准,那通过什么来规范标准呢,就是通过file_operations
结构体,把每个设备的操作方式确定下来,应用就可以使用标准的方式来访问操作。
相关file_operations 结构体
顶层:sound.c snd_fops /sound/core/sound.c
下层:control节点 snd_ctl_f_ops /sound/core/control.c
pcmC0D0c snd_pcm_f_ops[1] /sound/core/pcm_native.c
pcmC0D0p snd_pcm_f_ops[0] /sound/core/pcm_native.c
先看顶层的操作逻辑
static const struct file_operations snd_fops =
{
.owner = THIS_MODULE,
.open = snd_open,
.llseek = noop_llseek,
};
register_chrdev(116, "alsa", &snd_fops) //注册主设备号为116的
//snd_open 函数 通过次设备号找到具体的file_operation结构体的open函数
static int snd_open(struct inode *inode, struct file *file)
{
unsigned int minor = iminor(inode);
struct snd_minor *mptr = NULL;
mptr = snd_minors[minor];
file->f_op = fops_get(mptr->f_ops);
file->f_op->open(inode, file);
}
snd_card
,那是怎么跟实际设备的file_operations
结构体结合起来的呢?就是我们下面分析的。asla框架 实际声卡的驱动的流程 需要下面三个流程
struct snd_card *card;
snd_card_create(index, id, THIS_MODULE, 0, &card);
snd_ctl_create(card); //注册control 节点
snd_pcm_new() //注册playback captrue 节点
snd_card_register(card); //注册声卡驱动
a.注册control
节点
control
节点是必须要有的, 在上文中的主设备是116的类下去创建设备,会在/dev/snd
下生成control
节点,并把操作control
节点的file_operations
结构体放到snd_minors[contro节点的次设备号]
。
//1.分析snd_ctl_create init.c 文件中
snd_ctl_create(card); //注册control节点
static struct snd_device_ops ops = {
.dev_free = snd_ctl_dev_free,
.dev_register = snd_ctl_dev_register, //当声卡调用 snd_card_register(cand) 就会调用这个函数
.dev_disconnect = snd_ctl_dev_disconnect,
};
snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);
snd_ctl_dev_register
sprintf(name, "controlC%i", cardnum);
//snd_ctl_f_ops 是control节点的具体操作file_operations结构体
snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,&snd_ctl_f_ops, card, name)
snd_register_device_for_dev(file_operations *f_ops)
preg->f_ops = f_ops;
snd_minors[minor] = preg; //把具体的结构体放到 snd_minors 结构体中,让上文中的sound.c 使用
preg->dev = device_create(MKDEV(major, minor)); //在类下创建设备
b.注册playback
captrue
节点
//2.分析 snd_pcm_new
snd_pcm_new
static struct snd_device_ops ops = {
.dev_free = snd_pcm_dev_free,
.dev_register = snd_pcm_dev_register, //当声卡调用 snd_card_register(cand) 就会调用这个函数
.dev_disconnect = snd_pcm_dev_disconnect,
};
snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)
snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)
snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)
snd_pcm_dev_register
sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
//snd_pcm_f_ops 是pcmC0D0*的实际操作file_operations结构体
snd_register_device_for_dev(&snd_pcm_f_ops[cidx]);
snd_minors[minor] = preg; //把具体的结构体放到 snd_minors 结构体中,让上文中的sound.c 使用
preg->dev = device_create(MKDEV(major, minor));//在类下创建设备
b.注册声卡驱动
snd_pcm_dev_register
sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
//snd_pcm_f_ops 是pcmC0D0*的实际操作file_operations结构体
snd_register_device_for_dev(&snd_pcm_f_ops[cidx]);
snd_minors[minor] = preg; //把具体的结构体放到 snd_minors 结构体中,让上文中的sound.c 使用
preg->dev = device_create(MKDEV(major, minor));//在类下创建设备
上述的过程,讲解了 在类下创建control
、pcmC0D0p
、pcmC0D0c
的设备的过程,以及将实际的file_operations
结构体放在snd_minors[]
数组中的过程,供snd_open
函数使用。
ASOC是建立在标准ALSA驱动层上,为了更好地支持嵌入式处理器和移动设备中的音频Codec的一套软件体系。ASoC不能单独存在,只是建立在标准ALSA驱动上,必须和标准的ALSA驱动框架相结合才能工作。
这里有个疑问,为什么有ALSA
驱动框架,还需要ASOC
框架。以下是结合网上一些资料和自己的看法总结。
为什么要出现ASOC?
1.Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码的重复,例如,仅是wm8731
的驱动,当时Linux中有分别针对4个平台的驱动代码
2.音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码进行重新对音频路径进行配置。
3.当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持通过改变过取样频率和偏置电流来达到省电的目的。
接下来我们以rk 平台来展开说明。
ASOC
嵌入式设备的音频系统可以被划分为板载硬件(Machine
)、Soc(Platform
)、Codec
三大部分,如下图所示:
Machine :是指某一款机器,可以是某款设备,某款开发板,又或者是某款智能手机,由此可以看出Machine几乎是不可重用的,每个Machine上的硬件实现可能都不一样,CPU不一样,Codec不一样,音频的输入、输出设备也不一样,Machine为CPU、Codec、输入输出设备提供了一个载体。
Platform : 一般是指某一个SoC平台,比如pxaxxx,s3cxxxx,omapxxx等等,与音频相关通常包含该SoC中的时钟、DMA、I2S、PCM等等,只要指定了SoC,那么我们可以认为它会有一个对应的Platform,它只与SoC相关,与Machine无关,这样我们就可以把Platform抽象出来,使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实际上,把Platform认为是某个SoC更好理解。
Codec : 字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、A/D、MixerPA(功放),通常包含多种输入(Mic、Line-in、I2S、PCM)和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样,是可重用的部件,同一个Codec可以被不同的Machine使用。嵌入式Codec通常通过I2C对内部的寄存器进行控制。
下面以一个最简单的Asoc 驱动程序来讲解这三部分是怎么构成的?
结合图片来分析比较清楚
接下来以es8323芯片作为说明来展开:
Machine driver
sound/soc/rockchip/rk_es8323.c
其中的Machine驱动负责Platform和Codec之间的耦合以及部分和设备或板子特定的代码采样率时钟配置
Platform driver
sound/soc/rockchip/rk30_i2s.c
I2S 控制器驱动 采样率时钟DMA 等配置
Codec driver
sound/soc/codecs/es8323.c
codec 的寄存器通路的配置
codec部分:
1.构造 snd_soc_codec_driver 结构体
2.构造 snd_soc_dai_driver 结构体
3.通过 snd_soc_register_codec 注册
codec 部分有两个比较重要的结构体
snd_soc_codec_driver
描述了使用哪一款codec芯片
snd_soc_dai_driver
描述了使用codec 芯片的哪一种dai接口,dai 接口的参数等
//snd_soc_codec_driver提供了读写Codec寄存器的函数
struct snd_soc_codec_driver soc_codec_dev_es8323 {
.read = es8323_read_reg_cache,
.write = es8323_write,
}
//snd_soc_dai_driver 用于codec的IIS 表示使用哪一个IIS接口,配置能传输哪种格式数据,并提供了设置参数的函数
static struct snd_soc_dai_ops es8323_ops = {
.hw_params = es8323_pcm_hw_params,
};
static struct snd_soc_dai_driver es8323_dai = {
.name = "ES8323 HiFi",
.playback = {.stream_name = "Playback",},
.capture = {.stream_name = "Capture",},
.ops = &es8323_ops,
};
snd_soc_register_codec(&i2c->dev,&soc_codec_dev_es8323, &es8323_dai, 1);
codec 部分 通过snd_soc_register_codec
将这两个重要的函数分别注册到内核中的链表dai_list
,codec_list
。我们系统中的源码可能有很多codec 的驱动程序,所以链表中会有很多其他的项。
platform部分:
1.构造 snd_soc_dai_driver 结构体
2.通过 snd_soc_register_dai 注册
3.构造 snd_soc_platform_driver 结构体
4.通过 snd_soc_register_platform 注册
platfrom部分也有两个关键的结构体
snd_soc_platform_driver
描述了使用哪一个平台,涉及到数据传输dma相关
snd_soc_dai_driver
描述了使用平台上的哪一个dai 接口,dai接口的参数
//snd_soc_dai_driver 包括主控soc端的dai接口的name 参数 设置参数/启动传输的函数
struct snd_soc_dai_driver *dai;
//下面是设置默认参数 采样率 位数 声道数等和设置参数的和启动传输函数
dai->playback.rates = SNDRV_PCM_RATE_44100;
dai->playback.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE;
dai->capture.channels_min = 2;
dai->capture.rates = SNDRV_PCM_RATE_44100
dai->capture.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE;
dai->ops = &rockchip_i2s_dai_ops;
static struct snd_soc_dai_ops rockchip_i2s_dai_ops = {
.trigger = rockchip_i2s_trigger,//启动传输的函数
.hw_params = rockchip_i2s_hw_params,//设置参数的函数
};
snd_soc_register_dai(&pdev->dev, dai);
通过snd_soc_register_dai
接口注册到内核中的dai_list
链表中。
//snd_soc_platform_driver 负责管理音频数据,把音频数据通过dma或其他操作传送至cpu dai
static struct snd_pcm_ops s3c2440_dma_ops = {
.open = s3c2440_dma_open,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = s3c2440_dma_hw_params,
.trigger = s3c2440_dma_trigger,
...
};
static struct snd_soc_platform_driver s3c2440_dma_platform = {
.ops = &s3c2440_dma_ops,
.pcm_new = s3c2440_dma_new,
.pcm_free = s3c2440_dma_free,
};
snd_soc_register_platform(&pdev->dev, &s3c2440_dma_platform);
通过snd_soc_register_platform
把结构体snd_soc_platform_driver
注册到内核的链表platform_list
中。内核中可能会存在多个platform的代码,导致链表中就会有多个节点。
经过了上面的讲诉,我们知道内核中可能存在多个codec驱动,多个平台的代码。那我们到底这个板子用的是什么型号的codec芯片,使用了codec 的哪一个dai接口 就由我们接下来的machine 部分决定。也就是确定了使用dai_list
platform_list
codec_list
中的哪一个节点。
machine部分:
1.构造 snd_soc_card 结构体,包含 snd_soc_dai_link结构体
2.通过 snd_soc_register_card 注册
static struct snd_soc_ops rk29_ops = {
.hw_params = rk29_hw_params,
};
static struct snd_soc_dai_link rk29_dai = {
.name = "ES8316",
.stream_name = "ES8316 PCM",
.codec_name = "ES8316.v01a", //确定codec芯片 确定codec_list中哪个soc_snd_codec_driver
.platform_name = "rockchip-audio", //确定使用哪个平台来传输数据 确定platform_list中哪个soc_snd_platform_driver
.cpu_dai_name = "rk29_i2s.0",//确定使用平台中哪一个dai接口 确定dai_list中哪个soc_snd_dai_driver
.codec_dai_name = "ES8316 HiFi", //确定使用codec芯片中的哪一个dai接口 确定dai_list中哪个soc_snd_dai_driver
.init = rk29_es8316_init,
.ops = &rk29_ops,
};
static struct snd_soc_card rockchip_es8316_snd_card = {
.name = "RK_ES8316",
.dai_link = &rk29_dai,
.num_links = 1,
};
struct snd_soc_card *card = &rockchip_es8316_snd_card;
snd_soc_register_card(card);
把信息放到结构体snd_soc_card
中,再通过snd_soc_register_card
对上面的节点进行绑定。
snd_soc_register_card(card);
snd_soc_instantiate_card(card);
//绑定dai
soc_bind_dai_link(card, i);
//接下去就是ALSA相关内内容了
snd_card_create(index, id, THIS_MODULE, 0, &card);
snd_pcm_new()
snd_card_register(card);
涉及到一个重点的函数soc_bind_dai_link
在这个函数里面,会根据machine的配置信息找到对应的链表中的节点进行绑定。
到这里,我们可以总结出,ALSA框架是没有平台,板级等差异性的,是一套标准化的东西。我们通过ASOC框架,把硬件的差异性给确定出来,确定好之后再调用ASLA的逻辑。另外也把相对pc 差异化的东西独立出来。再细看ASOC 框架,也是一个把差异化的东西分离的思想,codec 部分,因为只跟codec相关不同的平台codec芯片的驱动能做到大致的兼容。platform部分,由于跟平台相关,不同板子也能做到兼容。所以在前期调通阶段,首要的工作就是在machine部分。在声卡节点生成之后才进行其他调试。
上文如有错误之处,希望大家指正–by mj