欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux-Audio Codec

程序员文章站 2022-07-14 20:44:52
...

0、简要介绍

处理器要处理外界的声音需要将外界的声音(模拟信号)转换成二进制数据(数字信号),这个过程涉及到了一个模拟信号到数字信号的转换过程,完成这个功能的就是ADC芯片,

同样,如果处理器需要对外输出声音,那就就需要将数字信号装换成模拟信号,完成这个功能的就是DAC芯片。

将这两者合起来我们就称之为音频编解码芯片,也就是Audio Codec。其工作流程是:外界的声音(模拟信号)通过麦克风进入Audio Codec中,经由ADC模块将模拟信号转换成数字信号后通过IIS接口送给SOC,SOC对这些数字信号加工后再通过IIS接口传输给Audio Codec,由SOC传来的数字信号中经过DAC模块转换成模拟信号送到耳机或者喇叭放出声音。(另外Audio Codec的控制器是通过IIC进行配置的)

Linux-Audio Codec

 

采样:数字音频系统需要将声波波形信号通过adc转换成计算机支持的二进制,这一过程叫做音频采样,采样就是把连续的模拟信号转换成离散的数字信号 。

量化:采样后的值还需要通过量化,也就是将连续的值近似未某个范围内有限多个离散值的处理过程。

编码:计算机的世界里,所有数值都是用二进制表示的,因而我们还需要把量化值进行二进制编码并进行编码压缩。

Linux-Audio Codec

 

PCM:俗称脉冲编码调制,是将模拟信号数字化的一种经典方式,得到了非常广泛的应用,比如数字音频在计算机、DVD以及数字电话等系统中的标准格式采用的就是PCM,它的基本原理就是我们上面的几个流程。即对原始模拟信号进行抽样、量化和编码,从而产生PCM流 。

采样率:奈奎斯特定律,采样频率如果是声音频率最高值的两倍,则可以还原出原始声音。(是否可以根据声音频率改变采样率?)人耳所能辨识的声音范围是20-20KHz,所以人们一般都是选用44.1KHz、48KHz或者更高的频率作为采样速率,采样率越高,声音还原越真实,相应产生的文件也就越大。

采样深度:量化是将连续值近似为某个范围内有限多个离散值的处理过程,那么这个范围的宽度以及可用离散值的数量会直接影响到音频采样的准确性,这就是采样深度的意义。

Linux-Audio Codec

 

如上图是一个采用4位深度进行量化得到的PCM,因为4bit最多只能表达16个数值(0-15),所以图中最终量化后的数值依次为7、9、11、12、13、14、15等,这样的结果是相对粗糙的,存在一定程度的失真,当位深越大,所能表达的数值范围越广,上图中纵坐标的划分也就越细致,从而使得量化的值越接近原始数据。

 

声道:一个声道,简单来说就代表了一种独立的音频信号,所以双声道理论上就是两种独立音频信号的混合。具体而言,如果我们在录制声音时在不同空间位置放置两套采集设备,那就可以录制两个声音的音频数据了。后期对采集到的声音进行回放时,通过与录制时相同数量的外放扬声器来分别播放各声道的音频,就可以尽可能的还原出录制现场的真实声音了。

数字音频格式

不压缩的格式:PCM数据就是采样后得到的未经压缩的数据,PCM数据在Windows和Mac系统上通常分别以wav和aiff后缀进行存储,可想而知,这样的文件大小是比较客观的。

无损压缩格式:这种压缩的前提是不破坏音频信息,也就是说后期可以完整还原出原始数据。同时它一定程度上可以减小文件体积。比如FLAC、APE、WV、m4a等

有损压缩格式:无损压缩技术能减小的文件体积相对有限,因而在满足一定音质要求下,可以进行有损压缩。其中最为人熟知的就是mp3格式,另外还有iTunes上使用的AAC,这些格式通常可以指定压缩的比率,比率越大,文件体积越小,但效果也越差。

常见音频格式

WAV格式,是微软公司开发的一种声音文件格式,也叫波形声音文件,是最早的数字音频格式,被Windows平台及其应用程序广泛支持,压缩率低(无压缩)。

MP3格式, 全称是MPEG-1 Audio Layes 3 。在1992年合并到MPEG规范中,Mp3能够以高音质,低采样率对数字音频文件进行压缩,应用最普遍。

WMA格式,Windows Media Audio 是微软在互联网音频,视频领域的力作,WMA格式是以减少数据流量但保持音质的方法来达到更高的压缩率目的的,其压缩率一般可以达到1:18.此外,WMA还可以通过DRM 保护版权。

1、采样频率

采样的过程就是将通常的模拟音频信号的电信号转换成二进制码0和1的过程,这些0 和 1便构成了数字音频文件。下图中正弦曲线代表了原始音频曲线,方格代表采样后得到的结果。二者越吻合说明采样结果越好。

 

采样频率就是每秒钟采样的次数,我们通常说的44.1kHz采样频率就是每秒钟采样44100次,如上图,当采样频率越高的时候,数字信号越能反应模拟信号的曲线图,转换的精度越高。

2、Linux 下的音频框架

linux平台下提供了两种主要的音频驱动框架:OSS以及ALA。

OSS(open sound system)

早期linux版本采用的是oss框架,它也是unix即类unix系统中广泛使用的一种音频体系,oss既可以指oss接口本身,也可以用来表示接口的实现,但是oss框架的源码不是开源的,这也是linux内核最终放弃oss的一个原因。另外,oss的某些方面也遭到人们的质疑,比如:对新音频特性的支持不足,缺乏对最新内核的支持等。但是,oss作为unix下统一音频操作的早期实现,本身算的上是很成功的,它符合一切皆是文件的设计理念。而且作为一种体系框架,其更多的只是规定了应用程序与操作系统音频驱动间的交互,因而各个系统可以根据实际需求进行定制开发,其用到了如下所示的设备节点:

Linux-Audio Codec

 

ALSA(Advanced Linux Sound Architecture)

ALSA是linux社区为了取代OSS而提出的一种框架,是一个源代码完全开放的系统,ALSA在kernel 2.5版本中被正式引入后,OSS就逐步被排除在内核之外了,当然OSS本身还是在不断维护的,只是不再为内核所采用而已。

ALSA相对于OSS提供了更多,也更为复杂的API接口,因而开发难度相对来说也加大了一些,为此,ALSA专门提供了一个供开发者使用的工具库,以帮助它们更好的使用ALSA的API,根据官方文档的介绍,ALSA具有如下特性:

1、高效支持大多数类型的Audio interface

2、高度模块化的声音驱动

3、SMP及线程安全设计

4、在用户空间提供了alsa-lib 来简化应用程序的编写

5、与OSS API保持兼容,这样可以保证老的OSS程序在系统中正确的运行

2.1 ALSA 在linux内核的目录框架:

core ALSA驱动的中间层,它是整个ALSA驱动的核心部分

oss 包含模拟旧的OSS架构的PCM和Mixer模块(RK代码中未编译)

seq 有关音时序器的驱动代码(RK代码中未编译)

sound.c ALSA驱动的入口 (完成了snd_fops的注册,其中只有一个成员.open = snd_open(这个snd_open是个引子,会调用到snd_minors[]去调用新的file_oprations.另外在sound.c的入口函数中还完成了register_chrdev,对应的class_create在sound_core.c中完成)

control.c 通过snd_ctl_dev_register注册了一个sound contorl逻辑单元,其设备的名字是controlC%i,并完成了一个opration填充到了snd_minors[]中,给sound.c的入口函数中的snd_open调用。

pcm.c 通过snd_pcm_dev_register创建了一个sound pcm逻辑单元,其设备的名字是pcmC%iD%i(p/c),并完成了一个opration填充到了snd_minors[]中,给sound.c的入口函数中的snd_open调用。

drivers 与CPU、BUS架构无关的公用代码

aloop.c

soc 针对system-on-chip体系的中间层代码

generic simple card framework 通用方式创建声卡

simple-card.c rk的代码在这里完成了struct snd_soc_card snd_card的创建(asoc架构)

rockchip

rockchip_i2s.c CPU的i2s接口驱动

codecs

rk817_codec.c codec驱动

last.c

sound_core.c

完成了class_create,跟sound.c中的register_chrdev对应。

soun_firmware.c

2.2 ALSA 的代码框架:

主要的实现分成三个部分:mahine、platform、codec

mahine:单板相关、表明platform是哪个、cpu DAI接口是哪个、表明codec是哪个、codec的DAI接口是哪个、dma是哪个

platform:CPU DAI(初始化iis模块)、DMA(数据传输)

codec:Codec DAI 、 控制接口

2.2.1 Machine

核心是注册一个 struct snd_soc_card结构体。指定了platform、CPU DAI、codec、codec DAI、dma等。

这部分实现在soc_core.c中完成。

代码调用路径:

snd_soc_init--->
platform_driver_register(&soc_driver)--->
.probe		= soc_probe,--->
struct snd_soc_card *card = platform_get_drvdata(pdev);  --->
snd_soc_register_card(card);

注意:在rk1808这个单板中上述过程没有被调用到。这一过程在simple-card.c中完成,路径如下:

module_platform_driver(asoc_simple_card)--------->
.probe = asoc_simple_card_probe ---------->
ret = devm_snd_soc_register_card(&pdev->dev, &priv->snd_card)---------->
ret = snd_soc_register_card(card);

2.2.2 platform

这部分包括两个部分cpu dai 以及dma,这个驱动在上面的machine部分被struct snd_soc_card指定(name),通过这个name可以找到cpu dai 以及 dma的具体实现。

		simple-audio-card,cpu {
			sound-dai = <&i2s1>;
		};
		simple-audio-card,codec {
			sound-dai = <&rk809_codec>;
		};

通过DTS的信息可以看出,cpu dai 用的是i2s1接口:

	i2s1: aaa@qq.com {
		compatible = "rockchip,rk1808-i2s", "rockchip,rk3066-i2s";
		reg = <0x0 0xff7f0000 0x0 0x1000>;
		interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>;
		clocks = <&cru SCLK_I2S1_2CH>, <&cru HCLK_I2S1_2CH>;
		clock-names = "i2s_clk", "i2s_hclk";
		dmas = <&dmac 18>, <&dmac 19>;
		dma-names = "tx", "rx";
		resets = <&cru SRST_I2S1>, <&cru SRST_I2S1_H>;
		reset-names = "reset-m", "reset-h";
		pinctrl-names = "default";
		pinctrl-0 = <&i2s1_2ch_sclk
			     &i2s1_2ch_lrck
			     &i2s1_2ch_sdi
			     &i2s1_2ch_sdo>;
		status = "disabled";
	};

再从i2s1节点看,i2s1中用于接收的数据的dma用的是dmac 19,用于发送数据的dma是dmac 18。我们通过i2s1节点的compatible属性:“rk1808-i2s”可以定位到i2s1的驱动。同理可以定位到dma的驱动程序。

2.2.2.1 cpu dai驱动(i2s1驱动)

位置:sound\soc\rockchip\rockchip_i2s.c

作用:配置iis控制器

调用关系:

module_platform_driver(rockchip_i2s_driver)------>
.of_match_table = of_match_ptr(rockchip_i2s_match)---------->
{ .compatible = "rockchip,rk1808-i2s", }------->
.probe = rockchip_i2s_probe   

2.2.2.2 cpu dma驱动(dmac18 、dmac19 驱动)

位置:driver\dma\pl330.c

作用:驱动dma控制器

调用关系:

module_amba_driver(pl330_driver)-------> 
.probe = pl330_probe (匹配过程有点不对劲,后续深入分析)

2.2.3 codec

同样的,根据struct snd_soc_card结构体中的信息,以及DTS中的节点信息,我们可以找到codec相关驱动代码:

		rk809_codec: codec {
			#sound-dai-cells = <0>;
			compatible = "rockchip,rk809-codec", "rockchip,rk817-codec";
			clocks = <&cru SCLK_I2S1_2CH_OUT>;
			clock-names = "mclk";
			pinctrl-names = "default";
			pinctrl-0 = <&i2s1_2ch_mclk>;
			hp-volume = <20>;
			spk-volume = <3>;
			status = "okay";

驱动位置:sound\soc\codecs\rk817_codec.c

作用: codec芯片驱动

调用关系:

module_platform_driver(rk817_codec_driver)---------->
.probe = rk817_platform_probe  ---------->
snd_soc_register_codec(&pdev->dev, &soc_codec_dev_rk817,  rk817_dai

再codec芯片驱动中调用snd_soc_register_codec注册了一个codec,其中包含了soc_codec_dev_rk817以及rk817_dai,它们分别对应这codec中的控制接口以及DAI。

2.2.3.1 codec中的控制接口

static struct snd_soc_codec_driver soc_codec_dev_rk817 = {
	.probe = rk817_probe,
	.remove = rk817_remove,
	.get_regmap = rk817_get_regmap,
	.suspend = rk817_suspend,
	.resume = rk817_resume,
};

 

2.2.3.2 codec dai(i2s接口驱动)

static struct snd_soc_dai_driver rk817_dai[] = {
	{
		.name = "rk817-hifi",
		.id = RK817_HIFI,
		.playback = {
			.stream_name = "HiFi Playback",
			.channels_min = 2,
			.channels_max = 8,
			.rates = RK817_PLAYBACK_RATES,
			.formats = RK817_FORMATS,
		},
		.capture = {
			.stream_name = "HiFi Capture",
			.channels_min = 2,
			.channels_max = 8,
			.rates = RK817_CAPTURE_RATES,
			.formats = RK817_FORMATS,
		},
		.ops = &rk817_dai_ops,
	},
	{
		.name = "rk817-voice",
		.id = RK817_VOICE,
		.playback = {
			.stream_name = "Voice Playback",
			.channels_min = 1,
			.channels_max = 2,
			.rates = RK817_PLAYBACK_RATES,
			.formats = RK817_FORMATS,
		},
		.capture = {
			.stream_name = "Voice Capture",
			.channels_min = 2,
			.channels_max = 8,
			.rates = RK817_CAPTURE_RATES,
			.formats = RK817_FORMATS,
		},
		.ops = &rk817_dai_ops,
	},

};

2.2.4 ALSA驱动框架总结

整个ALSA驱动主要由CPU DAI驱动、CPU DMA驱动、CODEC DAI驱动、CODEC 控制接口驱动组成,最后综合到一起组成声卡结构体进行注册,下面分析一下上述驱动都完成了什么事情,综合起来又完成了什么事情。

3、驱动分析

3.1 Platform(CPU DAI驱动、DMA驱动)

如上面所说整个音频传输的大概流程是先将音频数据从内存通过DMA方式传输到CPU侧的dai接口,然后通过cpu的dai接口(通过iis总线)将数据送达到Codec中,数据将会在codec侧做解码操作,最终输出到耳机或者音响中。

在platform侧主要的功能是:音频数据管理,通过dma进行内存到iis buffer的音频数据搬运。以及通过iis总线将cpu侧的音频数据传输到codec侧,下面首先分析iis驱动。

static int rockchip_i2s_probe(struct platform_device *pdev)  //IIS驱动的probe函数
    i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL); //给结构体rk_i2s_dev 分配内存空间
    i2s->dev = &pdev->dev;
    i2s->grf = syscon_regmap_lookup_by_phandle(node, "rockchip,grf");
    i2s->reset_m = devm_reset_control_get(&pdev->dev, "reset-m");
	i2s->reset_h = devm_reset_control_get(&pdev->dev, "reset-h");
    i2s->hclk = devm_clk_get(&pdev->dev, "i2s_hclk");   //完成结构体的填充
    ret = clk_prepare_enable(i2s->hclk);  //iis的时钟使能
    i2s->mclk = devm_clk_get(&pdev->dev, "i2s_clk"); //获取iis的mclk
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //物理地址获取
    regs = devm_ioremap_resource(&pdev->dev, res);//映射为虚拟地址
    i2s->playback_dma_data.addr = res->start + I2S_TXDR; //播放声音时的dma地址
    i2s->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;//播放声音时dma地址的宽度
    i2s->playback_dma_data.maxburst = 8; //每次传输数据的数量,以src_addr_width 为单位
    i2s->capture_dma_data.addr = res->start + I2S_RXDR; //录音时的DMA地址
	i2s->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
	i2s->capture_dma_data.maxburst = 8;
    soc_dai = devm_kzalloc(&pdev->dev,sizeof(*soc_dai), GFP_KERNEL);  //准备填充snd_soc_dai_driver
    memcpy(soc_dai, &rockchip_i2s_dai, sizeof(*soc_dai)); //直接拿准备好数据填充进去
    ret = devm_snd_soc_register_component(&pdev->dev,&rockchip_i2s_component,soc_dai, 1);//注册一个component组件
        ret = snd_soc_register_component(dev, cmpnt_drv, dai_drv, num_dai);
            ret = snd_soc_component_initialize(cmpnt, cmpnt_drv, dev); //初始化component
                component->name = fmt_single_name(dev, &component->id);//填充name
                。。。
                INIT_LIST_HEAD(&component->dai_list); //初始化链表头omponent->dai_list
            snd_soc_register_dais(cmpnt, dai_drv, num_dai, true); //注册了一个dai
                list_add(&dai->list, &component->dai_list);
            snd_soc_component_add(cmpnt);
                snd_soc_component_add_unlocked(component);
                    list_add(&component->list, &component_list);  //加入component_list链表
    ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);   //申请DMA通道
            ret = snd_dmaengine_pcm_register(dev, config, flags);
                 ret = dmaengine_pcm_request_chan_of(pcm, dev, config);
                     chan = dma_request_slave_channel_reason(dev, name);//根据name去dts中找资源,申请对应的DMA通道
                         return of_dma_request_slave_channel(dev->of_node, name);
                 ret = snd_soc_add_platform(dev, &pcm->platform,&dmaengine_pcm_platform);//注册platform到ASoC Core

关键点1分析:

devm_snd_soc_register_component(&pdev->dev,&rockchip_i2s_component,soc_dai, 1);

该函数传入两个关键参数:struct snd_soc_component_driver *cmpnt_drv

struct snd_soc_dai_driver *dai_drv

struct snd_soc_component_driver *cmpnt_drv:

static const struct snd_soc_component_driver rockchip_i2s_component = {
	.name = DRV_NAME,
};

struct snd_soc_dai_driver *dai_drv

static struct snd_soc_dai_driver rockchip_i2s_dai = {
	.probe = rockchip_i2s_dai_probe,
	.playback = {   //回放能力描述信息,如所支持的声道数目、采样率、音频格式
		.stream_name = "Playback",
		.channels_min = 2,
		.channels_max = 8,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = (SNDRV_PCM_FMTBIT_S8 |
			    SNDRV_PCM_FMTBIT_S16_LE |
			    SNDRV_PCM_FMTBIT_S20_3LE |
			    SNDRV_PCM_FMTBIT_S24_LE |
			    SNDRV_PCM_FMTBIT_S32_LE),
	},
	.capture = {    //录制能力描述信息,如所支持的声道数、采样率、音频格式
		.stream_name = "Capture",
		.channels_min = 2,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = (SNDRV_PCM_FMTBIT_S8 |
			    SNDRV_PCM_FMTBIT_S16_LE |
			    SNDRV_PCM_FMTBIT_S20_3LE |
			    SNDRV_PCM_FMTBIT_S24_LE |
			    SNDRV_PCM_FMTBIT_S32_LE),
	},
	.ops = &rockchip_i2s_dai_ops, //对dai的操作函数集,定义了dai的时钟配置、格式配置、硬件参数配置等
};

1、component 实例化:

在devm_snd_soc_register_component函数中可以看到首先定义了一个struct snd_soc_component *cmpnt,然后通过snd_soc_component_initialize函数去实例化这个结构体(主要依靠一开始传入的struct snd_soc_component_driver *cmpnt_drv去填充snd_soc_component 结构体)经过snd_soc_component_initialize函数的初始化,可以得到一个填充完成的snd_soc_component 结构体,其中的dai_list链表头也被初始化了。

2、dai实例化:

在devm_snd_soc_register_component函数中第二步调用了snd_soc_register_dais函数去向asoc 中注册一个dai,传入的参数:cmpnt(上一步初始化完成的snd_soc_component 结构体)、dai_drv(struct snd_soc_dai_driver)、num_dai,ture。

在snd_soc_register_dai中首先定义了一个struct snd_soc_dai *dai。然后通过一个for循环(有几个dai就循环几次,注册几个dai)然后就开始填充dai结构体了,首先是填充它的name,然后将dai->compontnt 指向刚才初始化完成的component。最后调用了list_add将dai中的list挂载到了上一步初始化的链表&component->dai_list中去了。

3、挂载到总的链表中:

在devm_snd_soc_register_component函数中第三步调用snd_soc_component_add

这个函数其实就做了一件事,就是将componet中的list加入了一个全局的链表component-list当中,这样我们在遍历这个全局链表的过程中就可以通过这个链接来访问component结构体了,再通过上一步中dai中的list和component->dai_list的关系就可以进一步访问dai结构体了。

关键点2分析:

上面说了,在platform中需要完成音频数据管理和音频数据的dma搬运,其中就涉及到了dma相关的操作,在iis驱动的probe函数中就调用了devm_snd_dmaengine_pcm_register 来实现这一个过程。它使用了PCM DMA的架构来实现DMA通道的申请。详细的过程如下分析:

devm_snd_dmaengine_pcm_register 调用了snd_dmaengine_pcm_register,该函数首先分配了一个基于dmaengine的pcm结构体的内存,然后将刚申请到的pcm作为参数传入dmaengine_pcm_request_chan_of函数去完成dma通道的申请,一个DMA控制器有多个channel,每两个作为一组可以作为输入也可以作为输出,但是它们的控制代码都是一样的,并且在DMA的注册过程中,已经使DMA处于可用的状态了,这里直接申请一个channel作为PCM传输的DMA通道(DMA和PCM使绑定起来的)。

关键结构体:

struct dmaengine_pcm {
	struct dma_chan *chan[SNDRV_PCM_STREAM_LAST + 1];
	const struct snd_dmaengine_pcm_config *config;
	struct snd_soc_platform platform;
	unsigned int flags;
};
struct snd_soc_platform {
	struct device *dev;
	const struct snd_soc_platform_driver *driver;

	struct list_head list;

	struct snd_soc_component component;
};

DMA通道的申请:

dmaengine_pcm_request_chan_of(pcm, dev, config);传入的参数有三个:pcm(需要用到这个DMA通道的PCM设备)、dev(父设备)、config(platform的配置参数)

首先会根据PCM结构体中的flag来解析playback和capture是否公用DMA,如过公用,则将channel的名字设置为rx-tx,否则先处理tx后处理rx。

		if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX)
			name = "rx-tx";
		else
			name = dmaengine_pcm_dma_channel_names[i];

第二步调用dma_request_slave_channel_reason(dev, name);去申请一个chan,传入参数中name就是上一步中确定的name= “rx-tx” 或“rx”或“tx”,该函数进一步调用of_dma_request_slave_channel(dev->of_node, name);它将会根据这个name去设备树中匹配对应的dma通道;(注意这一步是运行在一个for循环中,将会根据需要获取多个dma channel。)

i2s1: aaa@qq.com {
		compatible = "rockchip,rk1808-i2s", "rockchip,rk3066-i2s";
		reg = <0x0 0xff7f0000 0x0 0x1000>;
		interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>;
		clocks = <&cru SCLK_I2S1_2CH>, <&cru HCLK_I2S1_2CH>;
		clock-names = "i2s_clk", "i2s_hclk";
		dmas = <&dmac 18>, <&dmac 19>;
		dma-names = "tx", "rx";
		resets = <&cru SRST_I2S1>, <&cru SRST_I2S1_H>;
		reset-names = "reset-m", "reset-h";
		pinctrl-names = "default";
		pinctrl-0 = <&i2s1_2ch_sclk
			     &i2s1_2ch_lrck
			     &i2s1_2ch_sdi
			     &i2s1_2ch_sdo>;
		status = "disabled";
	};

第三步,将刚申请到的dma赋值给pcm,PCM和DMA绑定完成,PCM结构体的成员进一步完善。

pcm->chan[i] = chan;

PCM结构体中的platform成员:

devm_snd_dmaengine_pcm_register 的最后调用了ret = snd_soc_add_platform,该函数传入了一个重要的参数&dmaengine_pcm_platform,它就是dmaengine pcm平台的驱动,它的.ops成员就是该驱动的调用方法。

static const struct snd_soc_platform_driver dmaengine_pcm_platform = {
	.component_driver = {
		.probe_order = SND_SOC_COMP_ORDER_LATE,
	},
	.ops		= &dmaengine_pcm_ops,
	.pcm_new	= dmaengine_pcm_new,
};
static const struct snd_pcm_ops dmaengine_pcm_ops = {
	.open		= dmaengine_pcm_open,
	.close		= snd_dmaengine_pcm_close,
	.ioctl		= snd_pcm_lib_ioctl,
	.hw_params	= dmaengine_pcm_hw_params,
	.hw_free	= snd_pcm_lib_free_pages,
	.trigger	= snd_dmaengine_pcm_trigger,
	.pointer	= dmaengine_pcm_pointer,
};

 

1、component 实例化:

snd_soc_add_platform该函数的功能就是添加一个platform到ASOC core中,这个platform是pcm中的platform。platform中有componet这个成员,所以第一步调用 snd_soc_component_initialize来完成这个component的初始化(跟关键点1分析中实例化component类似,这里主要通过刚介绍的重要参数&dmaengine_pcm_platform 中的.component_driver成员来完成component的填充的)

2、挂载component中的链表到总的component链表中:

在初始化完成component后,调用了snd_soc_component_add_unlocked(&platform->component);将platform中的component挂载到了全局component链表当中。当后面有代码在遍历这个全局的component链表时,不仅能访问最开始注册的dai,也可访问到这里实现的platform,进而访问到刚实现的pcm结构体,以及获取其中的dma通道等信息。

3、挂载platform到全局platform链表

snd_soc_add_platform的最后调用了list_add 将platform的list节点挂载到了全局platform链表中去。

综上:cpu侧的IIS驱动的probe函数中,主要实现了一个DAI结构体和component结构的实例化,以及通过链表将这两个结构体关联起来了,最后将component中的链表挂载到一个全局的链表当中,这样我们在其他地方去遍历这个全局的链表时就可以访问dai结构体了。最后还通过调用devm_snd_dmaengine_pcm_register去注册了一个基于dmaengine的PCM设备,在这个设备中完成了从设备树中获取dma通道的申请,以及一个platform的实现,实现过程中也实例化了一个component结构,并将这个component中的链表也挂载到了一个全局的链表当中。总而言之,这个probe就是完成了结构体DAI、结构体PCM的填充实例化以及对ASOC提供了一个可见可查找的入口(component list)

3.2 Codec(Codec DAI驱动、控制器驱动)

Codec的作用可以归结为4种 ,分别是:

1、对PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号

2、对Mic、Linein或者其他输入源的模拟信号进行A/D转换,把模拟的声音信号转变成CPU能处理的数字信号

3、对音频通路进行控制,比如播放音乐、收听调频收音机又或者是接听电话等,这些不同的场景音频信号在codec内的流通线路都是不一样的。

4、对音频信号做出相应的处理,例如音量控制、功率放大等。

如上面Audio Codec代码框架种所说的那样,rk1808这个平台的外接codec代码位于:sound\soc\codecs\rk817_codec.c,根据入口函数找到codec驱动的probe函数对其进行分析。

static struct platform_driver rk817_codec_driver = {
	.driver = {
		   .name = "rk817-codec",
		   .of_match_table = rk817_codec_dt_ids,
		   },
	.probe = rk817_platform_probe,
	.remove = rk817_platform_remove,
	.shutdown = rk817_platform_shutdown,
};
static int rk817_platform_probe(struct platform_device *pdev)
    struct rk808 *rk817 = dev_get_drvdata(pdev->dev.parent);//填充了rk817(用什么IIC(IIC 0)、中断、寄存器等信息)
    rk817_codec_data = devm_kzalloc(&pdev->dev,sizeof(struct rk817_codec_priv),GFP_KERNEL);//分配内存
    ret = rk817_codec_parse_dt_property(&pdev->dev, rk817_codec_data);//解析设备树,填充rk817_codec_data
    rk817_codec_data->regmap = devm_regmap_init_i2c(rk817->i2c,&rk817_codec_regmap_config);//内核的regmap机制,封装了IIC的操作
    rk817_codec_data->mclk = devm_clk_get(&pdev->dev, "mclk");  //mclk获取
    ret = snd_soc_register_codec(&pdev->dev, &soc_codec_dev_rk817,rk817_dai, ARRAY_SIZE(rk817_dai)); //注册codec
        codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);//分配codec的内存
        ret = snd_soc_component_initialize(&codec->component,&codec_drv->component_driver, dev);//初始化了component
       codec->component.controls = codec_drv->controls;   //填充component
       。。。
       dapm = snd_soc_codec_get_dapm(codec); //动态音频电源管理
       fixup_codec_formats(&dai_drv[i].playback); //设置DAI字节序
       fixup_codec_formats(&dai_drv[i].capture); //设置DAI字节序
       ret = snd_soc_register_dais(&codec->component, dai_drv, num_dai, false); //注册一个dai设备
           list_add(&dai->list, &component->dai_list);
       list_for_each_entry(dai, &codec->component.dai_list, list)
           dai->codec = codec;    
       snd_soc_component_add_unlocked(&codec->component);
       list_add(&codec->list, &codec_list); 

关键点1分析:

devm_regmap_init_i2c

内核3.1后引入了一套新的API机制:regmap。它主要为IIC、SPI、IRO等操作提供统一接口,提高代码可重用性,减少重复逻辑。

比如,之前我们要操作IIC设备的寄存器,那么就要调用i2c_transfer接口,要操作spi设备的寄存器,就要调用spi_write/spi_read等接口。但是如果我们将它们都抽象成了regmap结构,那么只要调用regmap_read/regmap_write就可以了。regmap的使用一般分为三个步骤:

1、初始化regmap_config结构体

struct regmap_config {
	const char *name; // regmap的可选名称,设备有多个的时候有用
	int reg_bits;  // 寄存器地址的位数(必选项)
	int reg_stride; //寄存器地址对齐
	int pad_bits; //寄存器和值之间的填充位数
	int val_bits; //寄存器值得位数(必选项)
	bool (*writeable_reg)(struct device *dev, unsigned int reg);//寄存器是否可写
	bool (*readable_reg)(struct device *dev, unsigned int reg);//寄存器是否可读
	bool (*volatile_reg)(struct device *dev, unsigned int reg);//寄存器值是否可存
	bool (*precious_reg)(struct device *dev, unsigned int reg); //????
	regmap_lock lock;  //锁
	regmap_unlock unlock;  //解锁
	void *lock_arg; //作为锁和解锁得参数传递
	int (*reg_read)(void *context, unsigned int reg, unsigned int *val); //读取寄存器中的值
	int (*reg_write)(void *context, unsigned int reg, unsigned int val); //向寄存器写入某个值

	bool fast_io; //跟锁相关

	unsigned int max_register; //寄存器地址的最大值
	const struct regmap_access_table *wr_table;//指定写访问的有效范围
	const struct regmap_access_table *rd_table; //指定读访问的有效范围
	const struct regmap_access_table *volatile_table;//指定易失性寄存器的范围
	const struct regmap_access_table *precious_table; //指定重要寄存器的范围
	const struct reg_default *reg_defaults; //寄存器的复位值
	unsigned int num_reg_defaults; //reg_defaults中的元素数
	enum regcache_type cache_type; //regmap的类型
	const void *reg_defaults_raw; //打开寄存器的复位值
	unsigned int num_reg_defaults_raw;//reg_defaults_raw的元素数目
	u8 read_flag_mask;  //读操作时设置掩码
	u8 write_flag_mask; //写操作时设置掩码

	bool use_single_rw;  //将批量读写操作转换为一系列的单词读写操作(用于不支持批量读写操作的设备)
	bool can_multi_write; //支持批量写入的多重写入模式

	enum regmap_endian reg_format_endian; //格式化寄存器地址的字节序
	enum regmap_endian val_format_endian; //格式化寄存器地址的字节序

	const struct regmap_range_cfg *ranges; //虚拟地址范围的配置条目的数组
	unsigned int num_ranges; //范围配置条目的数量
};

rk1808中使用的regmap_config:

static const struct regmap_config rk817_codec_regmap_config = {
	.name = "rk817-codec",
	.reg_bits = 8,   //寄存器地址位数(必选项)
	.val_bits = 8,   //寄存器值位数(必选项)
	.reg_stride = 1,    //寄存器地址对齐
	.max_register = 0x4f,  //最大寄存器地址
	.cache_type = REGCACHE_NONE,  //regmap类型
	.volatile_reg = rk817_volatile_register, //寄存器地址是否可存
	.writeable_reg = rk817_codec_register,  //判断寄存器是否可写
	.readable_reg = rk817_codec_register,  //判断寄存器是否可读
	.reg_defaults = rk817_reg_defaults, //寄存器的默认地址
	.num_reg_defaults = ARRAY_SIZE(rk817_reg_defaults), //reg_defaults 的数量
};

2、regmap的创建

rk这边调用的是devm_regmap_init_i2c来初始化regmap,加上devm是为了在卸载时内核会自动管理资源,这个函数最终调用了regmap_init来初始化regmap。

3、regmap的使用

读写寄存器调用函数:

regmap_updata_bits() //先读回来再对相应位进行操作

regmap_read() //读寄存器

regmap_wirte() //写寄存器

关键点2分析:

snd_soc_component_initialize

1、component 实例化:

snd_soc_register_codec该函数的功能就是添加一个codec到ASOC core中,这个codec中有componet这个成员,所以第一步调用 snd_soc_component_initialize来完成这个component的初始化(跟之前分析中实例化component类似)

2、挂载component中的链表到总的component链表中:

在初始化完成component后,调用了snd_soc_component_add_unlocked(&platform->component);将codec中的component挂载到了全局component链表当中。当后面有代码在遍历这个全局的component链表时,不仅能访问最开始注册的dai,也可访问到这里实现的codec。

关键点3分析:

snd_soc_codec_get_dapm

DAPM是Dynamic Audio Power Management的缩写,直译过来就是动态音频电源管理的意思,DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态下。DAMP对用户空间的应用来说使透明的,所有与电源相关的开关都在ASOC Core中完成,用户空间的应用程序无需对代码做出修改,也无需重新编译,DAPM根据当前**的音频流(playback、cpature)和声卡中的mixer等的配置来决定哪些音频控件的电源开官被打开或者关闭。(这里不做深入分析了,关于dapm是一个很庞大的体系,后面会单独分析)

关键点4分析:

snd_soc_register_dais

codec中也包含有一个dai,所以在对codec的注册中也需要注册一个dai。这个过程跟之前注册dai的过程一致。都是获取name,然后有几个dai就创建几个dai的实例。最后将dai中的链表挂载全局链表中方便machine去管理。

总结:

总的来说codec的probe函数最终要的调用时snd_soc_register_codec。完成一个codec的创建。在这之前呢,由于codec芯片需要通过iic进行配置,所有调用了devm_regmap_init_i2c来完成一个regmap的初始化,后续可以通过regmap_write接口对codec寄存器进行操作。

在snd_sco_register_codec中主要实现了component的支持,将注册的codec向machine提供了管理的接口,同时也通过component将codec与新注册的dai关联起来。

综合codec以及platform中实现的几个结构体(dai、pcm、codec)当中都包括了component。

3.3 Machine

上面我们已经分析了ASOC架构中的paltform以及codec,但是单独的paltform以及codec是无法完成音频播放、录音等功能的,还需要ASOC架构中最重要的部分:machine

根据前面的分析我们可以知道,machine相关的代码在:simple-card.c。

static const struct of_device_id asoc_simple_of_match[] = {
	{ .compatible = "simple-audio-card", },
	{},
};
MODULE_DEVICE_TABLE(of, asoc_simple_of_match);

static struct platform_driver asoc_simple_card = {
	.driver = {
		.name = "asoc-simple-card",
		.pm = &snd_soc_pm_ops,
		.of_match_table = asoc_simple_of_match,
	},
	.probe = asoc_simple_card_probe,
	.remove = asoc_simple_card_remove,
};

module_platform_driver(asoc_simple_card);
rk809_sound: rk809-sound {    //machine 匹配层的驱动节点
		status = "disabled";    //默认关闭
		compatible = "simple-audio-card";  //simple-card framework 框架
		simple-audio-card,format = "i2s";
		simple-audio-card,name = "rockchip,rk809-codec";
		simple-audio-card,mclk-fs = <256>;  //主控给编解码芯片用的时钟
		simple-audio-card,widgets =   //指定相关组将
			"Microphone", "Mic Jack",  //mic
			"Headphone", "Headphone Jack";//耳机
		simple-audio-card,routing =  //音频路径,如mic输入,耳机输出走哪些通路
			"Mic Jack", "MICBIAS1",
			"IN1P", "Mic Jack",
			"Headphone Jack", "HPOL",
			"Headphone Jack", "HPOR";
		simple-audio-card,cpu {
			sound-dai = <&i2s1>;  //指定cpu介入音频编解码的dai(数字化接口)
		};
		simple-audio-card,codec {
			sound-dai = <&rk809_codec>; //指定编解码音频介入cpu的dai
		};
	};

代码中的{ .compatible = "simple-audio-card", }, 和DTS中的 compatible = "simple-audio-card";匹配,会直接调用.probe = asoc_simple_card_probe。

asoc_simple_card_probe
    num_links = 1; //获取dai links的数目
    priv = devm_kzalloc(dev,sizeof(*priv) + sizeof(*dai_link) * num_links,GFP_KERNEL);
    priv->snd_card.owner = THIS_MODULE; //初始化snd_soc_card结构体
    。。。
    ret = asoc_simple_card_parse_of(np, priv);//从设备树中解析信息
        snd_soc_of_parse_card_name(&priv->snd_card, "simple-audio-card,name");//接入simple-card框架的名字
        ret = snd_soc_of_parse_audio_simple_widgets(&priv->snd_card,"simple-audio-card,widgets");//解析设备树中的音频组件
        ret = snd_soc_of_parse_audio_routing(&priv->snd_card,"simple-audio-card,routing");//解析设备数中音频路径
       ret = of_property_read_u32(node, "simple-audio-card,mclk-fs", &val);//解析设备书中主控给codec的时钟频率
       ret = asoc_simple_card_dai_link_of(node, priv, 0, true);//填充了dai_link
   snd_soc_card_set_drvdata(&priv->snd_card, priv);    
   ret = devm_snd_soc_register_card(&pdev->dev, &priv->snd_card);  //正式注册一个card
   ret = snd_soc_register_card(card); 
       ret = snd_soc_init_multicodec(card, link);  
       snd_soc_initialize_card_lists(card);    
           INIT_LIST_HEAD(&card->codec_dev_list);
           INIT_LIST_HEAD(&card->widgets);
           INIT_LIST_HEAD(&card->paths);
           INIT_LIST_HEAD(&card->dapm_list);
       INIT_LIST_HEAD(&card->dapm_dirty);
       INIT_LIST_HEAD(&card->dobj_list);
       ret = snd_soc_instantiate_card(card);    
           ret = soc_bind_dai_link(card, i); //绑定cpu_dai \codec_dai 
           ret = soc_bind_aux_dev(card, i);
           ret = snd_soc_init_codec_cache(codec);
           ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,card->owner, 0, &card->snd_card);
           ret = card->probe(card);
           ret = soc_probe_link_components(card, i, order);//执行cpu dai probe、codecs probe以及platform probe
           ret = soc_probe_link_dais(card, i, order);
           ret = snd_card_register(card->snd_card); //对pcm和control进行注 册

关键点1分析

asoc_simple_card_parse_of

在machine中我们都需要一个特殊的结构体dai_links来完成machine、codec、platform的联系,这个结构体的内容就是通过这里的解析设备树后得到,在asoc_simple_card_probe中调用了asoc_simple_card_parse_of完成了这项工作,而真正执行任务的函数是:asoc_simple_card_dai_link_of

1、获取设备节点

在设备树中,cpu和codec分别对应着两个节点:

	simple-audio-card,cpu {
			sound-dai = <&i2s1>;  //指定cpu介入音频编解码的dai(数字化接口)
		};
    simple-audio-card,codec {
			sound-dai = <&rk809_codec>; //指定编解码音频介入cpu的dai
		};

在asoc_simple_card_dai_link_of 中首先通过节点的名字找到对应的子节点cpu、codec

2、获取format

 of_fmt_table[] = {
		{ "i2s",	SND_SOC_DAIFMT_I2S },
		{ "right_j",	SND_SOC_DAIFMT_RIGHT_J },
		{ "left_j",	SND_SOC_DAIFMT_LEFT_J },
		{ "dsp_a",	SND_SOC_DAIFMT_DSP_A },
		{ "dsp_b",	SND_SOC_DAIFMT_DSP_B },
		{ "ac97",	SND_SOC_DAIFMT_AC97 },
		{ "pdm",	SND_SOC_DAIFMT_PDM},
		{ "msb",	SND_SOC_DAIFMT_MSB },
		{ "lsb",	SND_SOC_DAIFMT_LSB },
	};

根据of-fmt-table和dts中相关节点做匹配可以确定format的值。

3、获取cpu-dai的name

调用asoc_simple_card_sub_parse_of传入cpu相关的参数获取cpu-dai的name,填充到dai-link结构中。

4、获取codec-dai的name

同上,asoc_simple_card_sub_parse_of传入codec相关的参数可以获取codec-dai的name,填充到dai-link的结构体中。

5、设置simple-card提供的钩子函数

dai_link->ops = &asoc_simple_card_ops; dai_link->init = asoc_simple_card_dai_init;

关键点2分析:

asoc_simple_card_dai_init

关键点3分析:

snd_soc_register_card

在填充完成dai-link结构体后,开始向ASOC注册card。

1、为card->dai_link中的codecs申请内存

调用snd_soc_init_multicodec(card, link)去出初始化了一个codec。目的未明???

2、做了一些判断保证了codec、platform、cpu的正确性

3、初始化了很多list

snd_soc_initialize_card_lists

通过调用snd_soc_initialize_card_lists初始化了card中的很多list:codec_dev_list、widgets、paths、dapm_list

4、实例化card

snd_soc_instantiate_card

在snd-soc-register-card中调用了snd_soc_instantiate_card函数,完成了一个card的实例化,这个函数是最重要的函数。

关键点4分析:

snd_soc_instantiate_card

该函数是整个machine中最重要完成任务最多的函数,其主要功能如下:

1、绑定DAI

soc_bind_dai_link(card, i)

这个函数调用了snd_soc_find_dai从component_list的dai_list中找到cpu-dai以及codec_dai分别赋值给rtd->cpu_dai 和rtd->codec_dai。在platform_list全局链表中找到platform赋值给rtd->platform。经过这一过程绑定cpu_dai、codec_dais、platform到rtd这个snd_soc_pcm_runtime结构体中,至此,snd_soc_pcm_runtime:(card->rtd)中保存了本Machine中使用的Codec,DAI和Platform驱动的信息。

2、绑定AUX

soc_bind_aux_dev(card, i)

同上,绑定aux到card->aux_comp_list。

3、为codec注册cache

snd_soc_init_codec_cache

通过list_for_each_entry函数遍历codec_list链表,对整个链表中的每一个codec(未初始化cache)调用snd_soc_init_codec_cache去初始化它的cache。

4、调用标准的ALSA函数去创建一个声卡实例

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)是ALSA提供的一个创建以及初始化card实例的函数,它需要6个参数。其含义分别是:

parent:父设备 ----------------------------- card->dev

idx:crad的下标(第几个card) ----------------------------- -1

xid:card的标识(ASCII字符串)---------------------------- null

module:用于锁定的*模块 ------------------------------ card->owner

extra_size: 在声卡结构之后分配的额外大小 ------------- 0

card_ret:储存创建的card实例的指针 ----------------------- &card->snd_card

该函数一开始就调用了kzalloc分配了一个sizeof(*card)+extra_size大小的空间给card,然后就通过传入的xid和idx参数确定card中的id成员以及number成员。

接着就是一堆链表的初始化了。调用了device_initialize对card的设备进行了初始化。接着调用了snd_ctl_creat对控制接口进行了初始化(其实也是调用了device_initialize),就是创建了控制接口的设备,对文件系统提供了支持等,函数的最后调用了 snd_info_card_create完成了card对prco文件系统的支持,建立了proc文件中的info节点。

5、调用各个子部件的probe函数

通过调用soc_probe_link_components实现了CPU侧的所有component以及CODEC侧所有的component的probe函数的调用执行,也实现了platform的probe函数的执行。

通过调用soc_probe_link_dais实现了CODEC DAI以及Platform DAI的probe函数的调用 执行。

6、注册声卡

snd_card_register

ALSA架构的最后就是为了注册一个声卡设备。

首先,调用了device_add函数创建sysfs设备,声卡的class将会出现在/sys/class/sound下面;然后将现在创建的声卡实例保存在snd_cards这个全局数组当中,数组下标为card->number;然后通过snd_device_register_all 遍历挂载在该声卡下的所有逻辑设备,回调各snd_device的ops->dev-register。这里会调用到snd_pcm_dev_register。

7、pcm的注册

snd_pcm_dev_register

在snd_card_register中回调了snd_pcm_dev_register,这里面对于一个pcm设备,都会生成两个设备文件,一个用于playback(pcmCxDxp,通常系统中只有一个声卡和一个pcm即pcmC0D0p),一个用于capture(pcmCxDxc,通常系统中只有一个声卡和一个pcm即pcmC0D0c)。在注册这两个文件的时候是通过snd_register_device完成的,这个函数中有个关键点就是将file_operation结构体放入了一个全局数组snd_minors中,而当我们在应用层操作ALSA提供的文件接口时,首先调用的时ALSA的open函数,然后再这个open函数中就会调用到这个全局数组snd_minors,通过这个全局数据,得到正真能干事情的file_operation执行open操作。

总结:machine这一块先通过dts解析资源填充了dai-link结构体,然后通过这个dai-link把相应的codec、dai和platform实例赋值到了card->rtd[]中,再添加到card->rtd_list链表中,而后遍历了全局的codec->list,为每个符合要求的codec申请了空间。并调用了alsa提供的标准接口创建和初始化了一个声卡实例。调用了这个card中的probe函数(如果存在),接着就是开始扫描链表,首先扫描card->list链表,调用了cpu dai、codec dai、platform的component中的probe函数,而后又调用了 各个子部件的probe函数。还通过soc new pcm函数创建了pcm逻辑设备。最终调用了alsa提供的标准接口对声卡进行了注册。