Linux ALSA声卡驱动之七:ASoC架构中的Codec

1. Codec简介

在移动设备中,Codec的作用可以归结为4种,分别是:

对PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号对Mic、Linein或者其他输入源的模拟信号进行A/D转换,把模拟的声音信号转变CPU能够处理的数字信号对音频通路进行控制,比如播放音乐,收听调频收音机,又或者接听电话时,音频信号在codec内的流通路线是不一样的对音频信号做出相应的处理,例如音量控制,功率放大,EQ控制等等

ASoC对Codec的这些功能都定义好了一些列相应的接口,以方便地对Codec进行控制。ASoC对Codec驱动的一个基本要求是:驱动程序的代码必须要做到平台无关性,以方便同一个Codec的代码不经修改即可用在不同的平台上。以下的讨论基于wolfson的Codec芯片WM8994,kernel的版本3.3.x。

/*****************************************************************************************************//*****************************************************************************************************/

2. ASoC中对Codec的数据抽象描述Codec的最主要的几个数据结构分别是:snd_soc_codec,snd_soc_codec_driver,snd_soc_dai,snd_soc_dai_driver,其中的snd_soc_dai和snd_soc_dai_driver在ASoC的Platform驱动中也会使用到,Platform和Codec的DAI通过snd_soc_dai_link结构,在Machine驱动中进行绑定连接。下面我们先看看这几个结构的定义,这里我只贴出我要关注的字段,详细的定义请参照:/include/sound/soc.h。snd_soc_codec:[html]view plaincopy

    /*SoCAudioCodecdevice*/structsnd_soc_codec{constchar*name;/*Codec的名字*/structdevice*dev;/*指向Codec设备的指针*/conststructsnd_soc_codec_driver*driver;/*指向该codec的驱动的指针*/structsnd_soc_card*card;/*指向Machine驱动的card实例*/intnum_dai;/*该Codec数字接口的个数,目前越来越多的Codec带有多个I2S或者是PCM接口*/int(*volatile_register)(…);/*用于判定某一寄存器是否是volatile*/int(*readable_register)(…);/*用于判定某一寄存器是否可读*/int(*writable_register)(…);/*用于判定某一寄存器是否可写*//*runtime*/……/*codecIO*/void*control_data;/*该指针指向的结构用于对codec的控制,通常和read,write字段联合使用*/enumsnd_soc_control_typecontrol_type;/*可以是SND_SOC_SPI,SND_SOC_I2C,SND_SOC_REGMAP中的一种*/unsignedint(*read)(structsnd_soc_codec*,unsignedint);/*读取Codec寄存器的函数*/int(*write)(structsnd_soc_codec*,unsignedint,unsignedint);/*写入Codec寄存器的函数*//*dapm*/structsnd_soc_dapm_contextdapm;/*用于DAPM控件*/};

snd_soc_codec_driver:[html]view plaincopy

    /*codecdriver*/structsnd_soc_codec_driver{/*driverops*/int(*probe)(structsnd_soc_codec*);/*codec驱动的probe函数,由snd_soc_instantiate_card回调*/int(*remove)(structsnd_soc_codec*);int(*suspend)(structsnd_soc_codec*);/*电源管理*/int(*resume)(structsnd_soc_codec*);/*电源管理*//*Defaultcontrolandsetup,addedafterprobe()isrun*/conststructsnd_kcontrol_new*controls;/*音频控件指针*/conststructsnd_soc_dapm_widget*dapm_widgets;/*dapm部件指针*/conststructsnd_soc_dapm_route*dapm_routes;/*dapm路由指针*//*codecwideoperations*/int(*set_sysclk)(…);/*时钟配置函数*/int(*set_pll)(…);/*锁相环配置函数*//*codecIO*/unsignedint(*read)(…);/*读取codec寄存器函数*/int(*write)(…);/*写入codec寄存器函数*/int(*volatile_register)(…);/*用于判定某一寄存器是否是volatile*/int(*readable_register)(…);/*用于判定某一寄存器是否可读*/int(*writable_register)(…);/*用于判定某一寄存器是否可写*//*codecbiaslevel*/int(*set_bias_level)(…);/*偏置电压配置函数*/};

snd_soc_dai:[html]view plaincopy

    /**DigitalAudioInterfaceruntimedata.**HoldsruntimedataforaDAI.*/structsnd_soc_dai{constchar*name;/*dai的名字*/structdevice*dev;/*设备指针*//*driverops*/structsnd_soc_dai_driver*driver;/*指向dai驱动结构的指针*//*DAIruntimeinfo*/unsignedintcapture_active:1;/*streamisinuse*/unsignedintplayback_active:1;/*streamisinuse*//*DAIDMAdata*/void*playback_dma_data;/*用于管理playbackdma*/void*capture_dma_data;/*用于管理capturedma*//*parentplatform/codec*/union{structsnd_soc_platform*platform;/*如果是cpudai,指向所绑定的平台*/structsnd_soc_codec*codec;/*如果是codecdai指向所绑定的codec*/};structsnd_soc_card*card;/*指向Machine驱动中的crad实例*/};

snd_soc_dai_driver:[html]view plaincopy

    /**DigitalAudioInterfaceDriver.**DescribestheDigitalAudioInterfaceintermsofitsALSA,DAIandAC97*operationsandcapabilities.Codecandplatformdriverswillregisterthis*structureforeveryDAItheyhave.**Thisstructurecoverstheclocking,formatingandALSAoperationsforeach*interface.*/structsnd_soc_dai_driver{/*DAIdescription*/constchar*name;/*dai驱动名字*//*DAIdrivercallbacks*/int(*probe)(structsnd_soc_dai*dai);/*dai驱动的probe函数,由snd_soc_instantiate_card回调*/int(*remove)(structsnd_soc_dai*dai);int(*suspend)(structsnd_soc_dai*dai);/*电源管理*/int(*resume)(structsnd_soc_dai*dai);/*ops*/conststructsnd_soc_dai_ops*ops;/*指向本dai的snd_soc_dai_ops结构*//*DAIcapabilities*/structsnd_soc_pcm_streamcapture;/*描述capture的能力*/structsnd_soc_pcm_streamplayback;/*描述playback的能力*/};

snd_soc_dai_ops用于实现该dai的控制盒参数配置:[html]view plaincopy

    structsnd_soc_dai_ops{/**DAIclockingconfiguration,alloptional.*Calledbysoc_carddrivers,normallyintheirhw_params.*/int(*set_sysclk)(…);int(*set_pll)(…);int(*set_clkdiv)(…);/**DAIformatconfiguration*Calledbysoc_carddrivers,normallyintheirhw_params.*/int(*set_fmt)(…);int(*set_tdm_slot)(…);int(*set_channel_map)(…);int(*set_tristate)(…);/**DAIdigitalmute-optional.*Calledbysoc-coretominimiseanypops.*/int(*digital_mute)(…);/**ALSAPCMaudiooperations-alloptional.*Calledbysoc-coreduringaudioPCMoperations.*/int(*startup)(…);void(*shutdown)(…);int(*hw_params)(…);int(*hw_free)(…);int(*prepare)(…);int(*trigger)(…);/**ForhardwarebasedFIFOcauseddelayreporting.*Optional.*/snd_pcm_sframes_t(*delay)(…);};

3. Codec的注册因为Codec驱动的代码要做到平台无关性,要使得Machine驱动能够使用该Codec,Codec驱动的首要任务就是确定snd_soc_codec和snd_soc_dai的实例,并把它们注册到系统中,注册后的codec和dai才能为Machine驱动所用。以WM8994为例,对应的代码位置:/sound/soc/codecs/wm8994.c,模块的入口函数注册了一个platform driver:[html]view plaincopy

    staticstructplatform_driverwm8994_codec_driver={.driver={.name="wm8994-codec",.owner=THIS_MODULE,},.probe=wm8994_probe,.remove=__devexit_p(wm8994_remove),};module_platform_driver(wm8994_codec_driver);

有platform driver,必定会有相应的platform device,这个platform device的来源后面再说,显然,platform driver注册后,probe回调将会被调用,这里是wm8994_probe函数:[html]view plaincopy

    staticint__devinitwm8994_probe(structplatform_device*pdev){returnsnd_soc_register_codec(&pdev->dev,&soc_codec_dev_wm8994,wm8994_dai,ARRAY_SIZE(wm8994_dai));}

其中,soc_codec_dev_wm8994和wm8994_dai的定义如下(代码中定义了3个dai,这里只列出第一个):[html]view plaincopy

    staticstructsnd_soc_codec_driversoc_codec_dev_wm8994={.probe=wm8994_codec_probe,.remove=wm8994_codec_remove,.suspend=wm8994_suspend,.resume=wm8994_resume,.set_bias_level=wm8994_set_bias_level,.reg_cache_size=WM8994_MAX_REGISTER,.volatile_register=wm8994_soc_volatile,};

[html]view plaincopy

    staticstructsnd_soc_dai_driverwm8994_dai[]={{.name="wm8994-aif1",.id=1,.playback={.stream_name="AIF1Playback",.channels_min=1,.channels_max=2,.rates=WM8994_RATES,.formats=WM8994_FORMATS,},.capture={.stream_name="AIF1Capture",.channels_min=1,.channels_max=2,.rates=WM8994_RATES,.formats=WM8994_FORMATS,},.ops=&wm8994_aif1_dai_ops,},……}

可见,Codec驱动的第一个步骤就是定义snd_soc_codec_driver和snd_soc_dai_driver的实例,然后调用snd_soc_register_codec函数对Codec进行注册。进入snd_soc_register_codec函数看看:首先,它申请了一个snd_soc_codec结构的实例:[html]view plaincopy

    codec=kzalloc(sizeof(structsnd_soc_codec),GFP_KERNEL);

确定codec的名字,这个名字很重要,Machine驱动定义的snd_soc_dai_link中会指定每个link的codec和dai的名字,进行匹配绑定时就是通过和这里的名字比较,从而找到该Codec的![html]view plaincopy

    /*createCODECcomponentname*/codec->name=fmt_single_name(dev,&codec->id);

然后初始化它的各个字段,多数字段的值来自上面定义的snd_soc_codec_driver的实例soc_codec_dev_wm8994:[html]view plaincopy

    codec->write=codec_drv->write;codec->read=codec_drv->read;codec->volatile_register=codec_drv->volatile_register;codec->readable_register=codec_drv->readable_register;codec->writable_register=codec_drv->writable_register;codec->dapm.bias_level=SND_SOC_BIAS_OFF;codec->dapm.dev=dev;codec->dapm.codec=codec;codec->dapm.seq_notifier=codec_drv->seq_notifier;codec->dapm.stream_event=codec_drv->stream_event;codec->dev=dev;codec->driver=codec_drv;codec->num_dai=num_dai;

在做了一些寄存器缓存的初始化和配置工作后,通过snd_soc_register_dais函数对本Codec的dai进行注册:[html]view plaincopy

    /*registeranyDAIs*/if(num_dai){ret=snd_soc_register_dais(dev,dai_drv,num_dai);if(ret<0)gotofail;}

最后,它把codec实例链接到全局链表codec_list中,并且调用snd_soc_instantiate_cards是函数触发Machine驱动进行一次匹配绑定操作:[html]view plaincopy

    list_add(&codec->list,&codec_list);snd_soc_instantiate_cards();

上面的snd_soc_register_dais函数其实也是和snd_soc_register_codec类似,显示为每个snd_soc_dai实例分配内存,确定dai的名字,用snd_soc_dai_driver实例的字段对它进行必要初始化,最后把该dai链接到全局链表dai_list中,和Codec一样,最后也会调用snd_soc_instantiate_cards函数触发一次匹配绑定的操作。 图3.1 dai的注册关于snd_soc_instantiate_cards函数,请参阅另一篇博文:Linux音频驱动之六:ASoC架构中的Machine。4. mfd设备

前面已经提到,codec驱动把自己注册为一个platform driver,那对应的platform device在哪里定义?答案是在以下代码文件中:/drivers/mfd/wm8994-core.c。

WM8994本身具备多种功能,除了codec外,它还有作为LDO和GPIO使用,这几种功能共享一些IO和中断资源,linux为这种设备提供了一套标准的实现方法:mfd设备。其基本思想是为这些功能的公共部分实现一个父设备,以便共享某些系统资源和功能,然后每个子功能实现为它的子设备,这样既共享了资源和代码,又能实现合理的设备层次结构,主要利用到的API就是:mfd_add_devices(),mfd_remove_devices(),mfd_cell_enable(),mfd_cell_disable(),mfd_clone_cell()。

回到wm8994-core.c中,因为WM8994使用I2C进行内部寄存器的存取,它首先注册了一个I2C驱动:

[html]view plaincopy

    staticstructi2c_driverwm8994_i2c_driver={.driver={.name="wm8994",.owner=THIS_MODULE,.pm=&wm8994_pm_ops,.of_match_table=wm8994_of_match,},.probe=wm8994_i2c_probe,.remove=wm8994_i2c_remove,.id_table=wm8994_i2c_id,};staticint__initwm8994_i2c_init(void){intret;ret=i2c_add_driver(&wm8994_i2c_driver);if(ret!=0)pr_err("Failedtoregisterwm8994I2Cdriver:%d\n",ret);returnret;}module_init(wm8994_i2c_init);

进入wm8994_i2c_probe()函数,它先申请了一个wm8994结构的变量,该变量被作为这个I2C设备的driver_data使用,上面已经讲过,codec作为它的子设备,将会取出并使用这个driver_data。接下来,本函数利用regmap_init_i2c()初始化并获得一个regmap结构,该结构主要用于后续基于regmap机制的寄存器I/O,关于regmap我们留在后面再讲。最后,通过wm8994_device_init()来添加mfd子设备:[html]view plaincopy

    staticintwm8994_i2c_probe(structi2c_client*i2c,conststructi2c_device_id*id){structwm8994*wm8994;intret;wm8994=devm_kzalloc(&i2c->dev,sizeof(structwm8994),GFP_KERNEL);i2c_set_clientdata(i2c,wm8994);wm8994->dev=&i2c->dev;wm8994->irq=i2c->irq;wm8994->type=id->driver_data;wm8994->regmap=regmap_init_i2c(i2c,&wm8994_base_regmap_config);returnwm8994_device_init(wm8994,i2c->irq);}

继续进入wm8994_device_init()函数,它首先为两个LDO添加mfd子设备:[html]view plaincopy

    /*Addtheon-chipregulatorsfirstforbootstrapping*/ret=mfd_add_devices(wm8994->dev,-1,wm8994_regulator_devs,ARRAY_SIZE(wm8994_regulator_devs),NULL,0);

因为WM1811,WM8994,WM8958三个芯片功能类似,因此这三个芯片都使用了WM8994的代码,所以wm8994_device_init()接下来根据不同的芯片型号做了一些初始化动作,这部分的代码就不贴了。接着,从platform_data中获得部分配置信息:[html]view plaincopy

    if(pdata){wm8994->irq_base=pdata->irq_base;wm8994->gpio_base=pdata->gpio_base;/*GPIOconfigurationisonlyappliedifit’snon-zero*/……}

最后,初始化irq,然后添加codec子设备和gpio子设备:[html]view plaincopy

    wm8994_irq_init(wm8994);ret=mfd_add_devices(wm8994->dev,-1,wm8994_devs,ARRAY_SIZE(wm8994_devs),NULL,0);

经过以上这些处理后,作为父设备的I2C设备已经准备就绪,它的下面挂着4个子设备:ldo-0,ldo-1,codec,gpio。其中,codec子设备的加入,它将会和前面所讲codec的platform driver匹配,触发probe回调完成下面所说的codec驱动的初始化工作。5. Codec初始化Machine驱动的初始化,codec和dai的注册,都会调用snd_soc_instantiate_cards()进行一次声卡和codec,dai,platform的匹配绑定过程,这里所说的绑定,正如Machine驱动一文中所描述,就是通过3个全局链表,按名字进行匹配,把匹配的codec,dai和platform实例赋值给声卡每对dai的snd_soc_pcm_runtime变量中。一旦绑定成功,将会使得codec和dai驱动的probe回调被调用,codec的初始化工作就在该回调中完成。对于WM8994,该回调就是wm8994_codec_probe函数:

图5.1 wm8994_codec_probe

取出父设备的driver_data,其实就是上一节的wm8994结构变量,取出其中的regmap字段,复制到codec的control_data字段中;申请一个wm8994_priv私有数据结构,并把它设为codec设备的driver_data;通过snd_soc_codec_set_cache_io初始化regmap io,完成这一步后,就可以使用API:snd_soc_read(),snd_soc_write()对codec的寄存器进行读写了;把父设备的driver_data(struct wm8994)和platform_data保存到私有结构wm8994_priv中;因为要同时支持3个芯片型号,这里要根据芯片的型号做一些特定的初始化工作;申请必要的几个中断;设置合适的偏置电平;通过snd_soc_update_bits修改某些寄存器;根据父设备的platform_data,完成特定于平台的初始化配置;添加必要的control,dapm部件进而dapm路由信息;

至此,codec驱动的初始化完成。

5. regmap-io我们知道,要想对codec进行控制,通常都是通过读写它的内部寄存器完成的,读写的接口通常是I2C或者是SPI接口,不过每个codec芯片寄存器的比特位组成都有所不同,寄存器地址的比特位也有所不同。例如WM8753的寄存器地址是7bits,数据是9bits,WM8993的寄存器地址是8bits,数据也是16bits,而WM8994的寄存器地址是16bits,数据也是16bits。在kernel3.1版本,内核引入了一套regmap机制和相关的API,这样就可以用统一的操作来实现对这些多样的寄存器的控制。regmap使用起来也相对简单:为codec定义一个regmap_config结构实例,指定codec寄存器的地址和数据位等信息;根据codec的控制总线类型,调用以下其中一个函数,得到一个指向regmap结构的指针:struct regmap *regmap_init_i2c(struct i2c_client *i2c,const struct regmap_config *config);struct regmap *regmap_init_spi(struct spi_device *dev,const struct regmap_config *config);把获得的regmap结构指针赋值给codec->control_data;调用soc-io的api:snd_soc_codec_set_cache_io使得soc-io和regmap进行关联;完成以上步骤后,codec驱动就可以使用诸如snd_soc_read、snd_soc_write、snd_soc_update_bits等API对codec的寄存器进行读写了。“人无完人金无足赤”,只要是人就不会是完美的,

Linux ALSA声卡驱动之七:ASoC架构中的Codec

相关文章:

你感兴趣的文章:

标签云: