音频2-ALSA/ASOC音频驱动框架

计划分成下面8章来详细展开,后面再根据实际情况做调整。

  • 1.基础知识(硬件,音频相关概念)
  • 2.ALSA/ASOC音频驱动框架
  • 3.codec 驱动dapm 相关(kcontrol、widget、route),以及hal层设置通路
  • 4.android 音频框架( 重点在AudioFlinger 和 AudioPolicyService服务 )
  • 5.音频通路的选择(AudioTrack播放选择设备的过程,以及设备切换过程)
  • 6.播放 录音音频流的传输过程
  • 7.audio hal介绍
  • 8.一些常见问题解决思路

以上内容基于tinyalsa展开,尽量剔除厂商的差异性。



ALSA/ASOC框架

本节讲解了ALSA和ASOC的框架流程,并介绍了声卡的注册过程。

一.ALSA

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
  1. 顶层
    在sound.c中会去注册主设备号为116的字符设备,但不是具体的硬件操作file_operations 结构体,而是打开的时候根据次设备号找到具体的操作file_operation结构体

先看顶层的操作逻辑

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);
}
  1. 声卡的注册流程
    上文说了顶层是标准的结构,证明不涉及硬件差异性, 实际硬件声卡驱动,硬件差异性是在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 pcmC0D0ppcmC0D0c的设备的过程,以及将实际的file_operations结构体放在snd_minors[]数组中的过程,供snd_open函数使用。

一.ASOC

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

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是将TinyALSA转换为ALSA SoC驱动程序的示例代码: 1. 定义硬件描述符 ``` static struct snd_soc_dai_driver my_dai = { .name = "my_dai", .playback = { .stream_name = "Playback", .channels_min = 1, .channels_max = 2, .rates = SNDRV_PCM_RATE_8000_48000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, .capture = { .stream_name = "Capture", .channels_min = 1, .channels_max = 2, .rates = SNDRV_PCM_RATE_8000_48000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, }; ``` 2. 注册SoC驱动程序 ``` static int my_probe(struct platform_device *pdev) { int ret; ret = snd_soc_register_dai(&pdev->dev, &my_dai); if (ret) { dev_err(&pdev->dev, "Failed to register DAI: %d\n", ret); return ret; } return 0; } static int my_remove(struct platform_device *pdev) { snd_soc_unregister_dai(&pdev->dev); return 0; } static const struct of_device_id my_of_match[] = { { .compatible = "my,codec" }, {}, }; MODULE_DEVICE_TABLE(of, my_of_match); static struct platform_driver my_driver = { .driver = { .name = "my-driver", .owner = THIS_MODULE, .of_match_table = my_of_match, }, .probe = my_probe, .remove = my_remove, }; module_platform_driver(my_driver); ``` 3. 在SoC DAI中实现PCM操作 ``` static int my_dai_playback_trigger(struct snd_pcm_substream *substream, int cmd) { struct snd_soc_pcm_runtime *rtd = substream->private_data; struct snd_soc_dai *codec_dai = rtd->codec_dai; struct snd_soc_dai *cpu_dai = rtd->cpu_dai; switch (cmd) { case SNDRV_PCM_TRIGGER_START: // 实现开始播放的操作 break; case SNDRV_PCM_TRIGGER_STOP: // 实现停止播放的操作 break; } return 0; } static int my_dai_playback_prepare(struct snd_pcm_substream *substream) { return 0; } static struct snd_soc_ops my_dai_ops = { .trigger = my_dai_playback_trigger, .prepare = my_dai_playback_prepare, }; static struct snd_soc_dai_driver my_dai = { .name = "my_dai", .playback = { .stream_name = "Playback", .channels_min = 1, .channels_max = 2, .rates = SNDRV_PCM_RATE_8000_48000, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, .ops = &my_dai_ops, }; ``` 以上是将TinyALSA转换为ALSA SoC驱动程序的示例代码,其中包括定义硬件描述符、注册SoC驱动程序、在SoC DAI中实现PCM操作等步骤。开发者可以根据自己的需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值