DSP

ALSA 驱动框架和驱动开发 (一)

2019-07-13 19:53发布

音频设备接口包括PCM IIS AC97三大类 两种音频驱动框架: ALSA 和 OSS OSS包含DSP和MIXER字符设备接口,完全使用文件操作 ALSA以CARD和组件(PCM,mixer等)为主线,在用户空间的变成中不适用文件接口,而是使用alsalib,而下文要介绍的没有使用ALSAlib,而是使用了OSS lib       接口芯片为PCM系列 Linux 2.6.26 ARM9 AT91   首先,说明ALSA的driver部分 一,基本方法:        先说明整个ALSA的体系,如下图所示,   直接提供给用户空间操作的文件操作方法是由oss层提供的,包括pcm_oss.c和mixer_oss.c,如果编写一个pcm的驱动话,是在ALSA_driver层,包括的文件为at91_pcmXXX.c和pcmXXX.c,驱动编写的主要思想是,通过probe将一个新的snd_soc_codec *codec结构填充,然后调用snd_soc_new_pcms注册一系列的pcm接口,最后调用snd_soc_register_card 注册声卡   (假设走的是先注册驱动,在注册设备的流程,二者谁先谁后都一样,事实上先注册的确实是驱动) a)      soc-core.c 首先注册了一个平台驱动, static struct platform_driver soc_driver = { .driver           = {         .name            = "soc-audio", }, .probe           = soc_probe, .remove        = soc_remove, .suspend       = soc_suspend, .resume         = soc_resume, }; 如果在没有设备名为“soc-audio”的设备时,则不调用.Probe b)     at91_pcmXXX.c 同样,在epayment_snd_init中,先申请一个SSC设备(申请涉及到atmel_ssc.o模块),接着注册了一个platform的设备,起名字为“soc-audio”,该平台设备的driver_data为epayment_snd_devdata指针,该私有数据指针为struct  snd_soc_device的指针,使能SSC时钟。 此时,由于platform总线下的设备和驱动名字匹配,则调用driver的probe方法。 c)      .probe=soc_probe Struct snd_soc_device包含四个结构, 分别是 Struct snd_soc_machine Struct snd_soc_platform Struct snd_soc_codec_device Struct snd_soc_codec_drvdata Probe中调用的顺序为, Soc_core(probe)     àcpu  àplatform  àcodec Machine probe null Cpu_dai probe null Codec_dev->probe pcmXXX probe(pcmXXX.c) Platform probe null 最后初始化一个工作,在进行close(shutdown)操作时调用,该工作是保证在结束播放后,延时一定时间,保证资源全部释放之后,在进行最终结束操作。 而对于PcmXXX.probe, 该函数的主要工作就是分配一个新的snd_soc_codec *codec结构,然后通过它注册PCM接口和卡设备 具体来说,PcmXXX_init(),codec的各个参量的初始化及其注册 Codec->private_data=pcmXXXpri, Codec->dai=&pcmXXX_dai Codec->num_dai=1 接着调用snd_soc_new_pcms注册一系列的pcm接口 调用snd_soc_register_card 注册声卡 上述两个操作都是soc-core的操作   d)     对于ALSA的open/write/read/ioctl(用于配置参数和prepare),如果驱动调用相关操作时,会大致根据  àcpu  àplatform  àcodec的顺序进行相关方法的调用,具体的如下,CPU(machine)包括cpu接口和codec接口。   以上就是简单的借用platform driver完成的pcm设备驱动程序框架。   二.Atmel_ssc.o  首先在板级程序中通过调用at91_add_device_ssc添加了SSC设备, 其中 static struct platform_device at91sam9260_ssc_device = {      .name = "ssc",      .id = 0,      .dev   = {          .dma_mask       = &ssc_dmamask,          .coherent_dma_mask   = DMA_BIT_MASK(32),      },      .resource = ssc_resources,      .num_resources  = ARRAY_SIZE(ssc_resources), }; 其中resource包括irq和iomem。 在atmel_ssc driver中,name均为ssc,于是调用driver的probe函数。 Probe函数中,申请ssc_device结构指针ssc,填充ssc,包括iomem,irq,clk,pdev等,最后将ssc添加到ssc_list中。 spin_lock(&user_lock); list_add_tail(&ssc->list, &ssc_list); spin_unlock(&user_lock); 这样就完成了driver的注册,此时ssc_list不为空,查看ssc_request函数知道,申请ssc设备就是遍历ssc_list,然后查找设备号是否一致,如果一致且设备无人使用,则返回该结构的指针,完成设备的申请过程,并且使能SSC时钟。   三.SOC-CORE a)      snd_soc_new_pcms 对应于controlC0设备和PCMC0D0P设备 该函数首先创建一个新的声卡卡结构(调用snd_card_new),然后根据num_links,分别调用soc_new_pcm创建新的pcm接口          ---àsnd_card_new  将controlC0设备添加到card->devices中                      具体就是, codec->card = snd_card_new(idx, xid, codec->owner, 0); (这里使用codec->owner计数的原因是,一个card可以对应多个pcm流,但是一个card不能被多个pcm流占用计数,所以编码器的owner即为card的owner)                                      ---àsnd_ctl_create 创建一个新的卡设备,name为CONTROL,定义相应的卡设备操作,这里指CONTROL设备的snd_device_ops,        static struct snd_device_ops ops = {               .dev_free = snd_ctl_dev_free,               .dev_register =    snd_ctl_dev_register,               .dev_disconnect = snd_ctl_dev_disconnect,        };    snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops) 然后将该卡结构(通过dev->list)添加到card->devices队列中(实际注册这些设备时通过遍历card->devices,然后调用各自的dev_register方法,对自身注册)                                      ---àsnd_info_card_create,创建一个卡的proc 文件          ---àsoc_new_pcm  将PCMC0D0P设备添加到card->devices中                      同样的,ret = soc_new_pcm(socdev, &machine->dai_link[i], i);                             这里有两个新数据结构                      一个是struct snd_soc_pcm_runtime *rtd(runtime data )                             Struct snd_soc_pcm_runtime {        Struct snd_soc_dai_link *dai;        Struct snd_soc_device *socdev; }                      一个是struct snd_pcm *pcm                             Pcm->private_data=rtd;                                                                   ---àsnd_pcm_new(pcm.c) 同snd_ctl_create,一样的生成一个PCM device                             具体涉及如下:                                    static struct snd_device_ops ops = {                                           .dev_free = snd_pcm_dev_free,                                           .dev_register =    snd_pcm_dev_register,                                           .dev_disconnect = snd_pcm_dev_disconnect,                                    }; snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)) 然后将该卡结构(通过dev->list)添加到card->devices队列中(实际注册这些设备时通过遍历card->devices,然后调用各自的dev_register方法,对自身注册)                                      ---à snd_pcm_set_ops(….., &soc_pcm_ops)                       Soc_pcm_ops为pcm相关操作的指针                                      ---àsocdev->platform->pcm_new                                           具体的函数为at91_pcm_new 该函数主要为DMA传输设置DAM MASK 和提前分配DMA buffer,包括playbakc和capture的                                          ---à最后,pcm->private_free=socdev->platform->pcm_free   b)      snd_soc_register_card                               ---àsnd_card_register(codec->card)                                             ----àsnd_device_register_all(card) 这里将card->devices中的device分别注册,调用各自的device_ops中的.dev_register进行注册,实际上只有两个设备,control和pcm.   一个是control设备controlC0, Control调用的是自身的snd_ctl_dev_register 注册control设备,Control设备有其相应的设备操作方法,PCM设备也有其相应的设备操作方法。                一个是pcm设备PCMC0D0P, PCM调用的是自身的snd_pcm_dev_register 设置PCM设备的名字,然后针对不同的设备用途,是playback流还是capture流,分别注册不同的操作方法,我们这里只有一个playback,所以只注册了playback的方法,cidx=0,函数的调用如下,               1).snd_register_device_for_dev     这里真正的调用device_create,通过pcm->device这一嵌套的device注册了设备(PCM)               2).snd_add_device_sysfs_file 添加设备信息到sysfs中               3).snd_pcm_timer_init 该部分创建了timer设备,具体见pcm_timer               4)   list_for_each_entry(notify, &snd_pcm_notify_list, list)               notify->n_register(pcm); 正是在这里注册了/dev/dsp , /dev/audio,snd_pcm_notify_list的由来,具体看PCM_OSS.O模块                                             ----choose_default_id(card)                                             ----àinit_info_for_card(card) proc文件的初始化                                                                                     ----à snd_mixer_oss_notify_callback 由于定义了 #ifdefined(CONFIG_SND_MIXER_OSS)||defined(CONFIG_SND_MIXER_OSS_MODULE) 所以mixer_oss模块被调用 在这里注册了mixer这一设备。具体见mixer_oss模块                                                                                                          ---àsnd_soc_dapm_sys_add(socdev->dev) 添加电源管理   三.PCM_OSS模块      在dev/目录下,可以看到6个与audio有关的设备,包括 mixer,dsp,audio,controlC0,pcmC0D0P,timer 从上面的图可以直观的说明它们的关系,对dsp设备文件的操作(OSS层),最终是调用pcm.o提供的方法,同样,对mixer设备文件的操作最终调用的是control.o提供的方法。 在alsa_pcm_oss_init中,先初始化dsp_map[i]全为0,adsp_map[i]全为1(default设置),检查合理,然后调用snd_pcm_notify(&snd_pcm_oss_notify, 0) 在snd_pcm_notify 函数中, 将snd_pcm_oss_notify加入到snd_pcm_notify_list中,即        list_add_tail(¬ify->list, &snd_pcm_notify_list); 然后遍历snd_pcm_devices,找出链表中的已经添加的PCM,然后调用notify中的.n_register进行注册,也即     list_for_each_entry(pcm, &snd_pcm_devices, list) notify->n_register(pcm); 在init中是没有PCM接口的,所以为空,只有在添加了PCM接口后,才会有一个,那就是上面soc_core中所说的。 在snd_soc_register_card中,最后创建/dev/dsp和/dev/audio时,是通过遍历notify list,然后调用对应的n_register完成的。 list_for_each_entry(notify, &snd_pcm_notify_list, list) notify->n_register(pcm); 在链表snd_pcm_notify_list中,只有一个notify,就是上面注册了的snd_pcm_oss_notify 而snd_pcm_oss_notify的.n_register实际调用的就是 Snd_pcm_oss_register_minor(struct snd_pcm *pcm) 具体的, Snd_pcm_oss_register_minor---à Register_oss_dsp(pcm,0)----àsnd_register_oss_device 通过snd_register_oss_device,建立了最上层的文件操作接口,该文件就是/dev/dsp文件和/dev/audio文件 snd_register_oss_device的具体调用为 register_sound_special_device(f_ops, 3, carddev); dsp    register_sound_special_device(f_ops, 4, carddev); audio 最终调用 sound_insert_unit(&chains[chain], fops, -1, unit, max_unit,                              name, S_IRUSR | S_IWUSR, dev); sound_insert_unit 调用两个, r = __sound_insert_unit(s, list, fops, index, low, top)插入到chains链表中和device_create创建设备和文件 最后adsp_map[0]!=pcm->device,前者为1,跳过最后snd_pcm_oss_proc_init(pcm),为proc oss init 以上就是建立设备文件/dev/dsp,/dev/audio,并将它们链入Chains的过程。        四.mixer_OSS模块   在初始化的过程中将snd_mixer_oss_notify_callback这一函数指针赋值为snd_mixer_oss_notify_handler,但是由于初始化过程中,snd_cards为空,所以未建立mixer设备文件,直到在最终的初始化后建立了MIXER设备        在soc_core中提到最后会通过调用snd_mixer_oss_notify_callback创建mixer设备文件。 具体的,就是在该snd_mixer_oss_notify_handler中,调用如下, ----àsnd_register_oss_device 将MIXER注册。 ----àsnd_mixer_oss_build     建立oss mixer element,该函数又调用. ----àsnd_mixer_oss_build_input 该函数主要完成struct snd_mixer_oss_slot *rslot的填充,包括put_volume,get_volume,get_recsrc,put_recsrc等.        ----àsnd_mixer_oss_proc_init  初始化oss mixer的proc文件.        Mixer_oss的相关操作是建立在得到mixer这一个kcontrol结构指针的基础上的,因为PCM没有相应的control寄存器,所以具体的mixer_oss的相关操作没有实现(control的相关操作亦没有实现)        五.Pcm_timer.c和 timer.c        a) Timer接口  为支持声音的同步事件提供访问声卡上的定时器。        Snd_pcm_timer_init函数,首先通过snd_timer_new创建一个timer设备(这里也定义了设备的相关方法),并且将其添加到Chains链表中,然后通过snd_device_register将Chains链表中的timer设备找到,并调用它自身的.dev_register将自身注册。注册之后,就能在/dev/目下看到timer设备了。        以上基本与前面提到的dsp/audio和mixer 的注册一致。        同时,在init函数中,做了对timer的填充和一些赋值操作,包括        Timer->hw=snd_pcm_timer,        Subtream->timer=timer        Timer->private_data=substream,        Timer->private_free=snd_pcm_timer_free        而snd_pcm_timer_free的定义如下,        static struct snd_timer_hardware snd_pcm_timer = {        .flags =  SNDRV_TIMER_HW_AUTO | SNDRV_TIMER_HW_SLAVE,        .resolution = 0,        .ticks =  1,        .c_resolution =    snd_pcm_timer_resolution,        .start =   snd_pcm_timer_start,        .stop =          snd_pcm_timer_stop, }; 这些参数在snd_timer_notify中被用到.          b) Timer.c中snd_timer_notify在Write操作时的.post_action中被调用到,        首先被调用的是        Timer->hw.c_resolution,即snd_pcm_timer_resolution,该函数的到runtime的resolution        接下来就是        list_for_each_entry(ti, &timer->active_list_head, active_list) {               if (ti->ccallback)                      ti->ccallback(ti, event, tstamp, resolution);               list_for_each_entry(ts, &ti->slave_active_head, active_list)                      if (ts->ccallback)                             ts->ccallback(ts, event, tstamp, resolution);        }        实际中,我们编写的PCMXXX驱动的定时器链表timer->active_list_head为空,所以不执行就直接返回了。        由于timer接口是为支持声音的同步事件提供访问声卡上的定时器,所以不提供也能正常使用。        六.Control.c 控制接口control对于许多开关(switch)和调节器(slider)而言应用相当广泛,它能从用户空间被存取。control的最主要用途是mixer,所有的mixer 元素基于control 内核API 实现,在ALSA 中,control 用snd_kcontrol结构体描述。 创建一个control,调用snd_ctl_add()和snd_ctl_new1()这两个函数来完成,步骤为 snd_ctl_new1()函数用于创建一个snd_kcontrol 并返回其指针,snd_ctl_add()函数用于将创建的snd_kcontrol 添加到对应的card 中。 如果需要创建control,可以在pcmXXX.c中的pcm_probe中进行实现不过由于pcm没有支持的control寄存器,所以probe中也没有进行此操作。        七.电源管理         Pcm驱动中也没有提供电源管理的control结构,例如在WM8731中就有相应的设置,这是由芯片决定的,         wm8731_add_controls(codec)  添加control至card->controls链表中,然后遍历链表将这些control接口全部注册。         wm8731_add_widgets(codec)  添加dapm control           至于设备的suspend和resume,pcmXXX虽然有此函数,不过可以不用实现,因为连接的串口处于suspend时,pcm也就没有数据处理的过程,这种从设备的形式的suspend和resume就可以不用实现。          Suspend过程包括两个方面,          At91-ssc.c中的suspend和resume(cpu) 当ssc处于suspend的状态时,保存此时SSC相关寄存器的值,待resume被调用时恢复。这些寄存器包括,SR,CMR,RCMR,RFMR,TCMR,TFMR等。          At91-pcm.c中的suspend和resume(platform),          至于平台的suspend,设计的就是PDC。在suspend时,禁止DMA传输,同时将PDC相关寄存器,包括XPR,XCR,XNPR,XNCR保存,待resume时恢复,进行继续的传输过程。   八.ALSA中的链表结构        在ALSA中设计到很多的链表结构,理解这些链表能更好的理解ALSA a)       card->devices card->devices链表的建立方便了card相关设备的注册过程和设备的管理。通过这个链表,在注册设备的过程中,可以先将设备(包括设备编号,设备相应的操作指针等)添加进链表中,然后再遍历链表,各自的设备调用本身的注册函数将自身注册,完成card相关所有设备的注册过程。 b)      snd_pcm_devices 该链表结构则是为了将已经存在了的PCM接口链接到该链表上,方便pcm的管理 c)       snd_pcm_notify_list 此链表是为pcm注册的通用方法,如果只注册了一个snd_pcm_oss_notify ,则在遍历snd_pcm_devices时,查找到的pcm device均使用该notify的.n_register 进行PCM的注册。 d)      card->controls cadr->controls链表和card->devices链表类似,只不顾一个负责管设备,一个负责管控制接口,基本操作类似,不过PCM中没有相关寄存器,所以未应用. e)       snd_control_ioctls 与control有关的链表,如果在control中snd_ctl_add则是将control添加到此链表中。 f)       timer->active_list_head 与timer有关的链表        九.驱动中各个结构体和各个模块的关系        a) soc_core所用到的各个结构体之间的关联图,可以说是体系中的CORE层。如下,     从上图中看,soc_core中多数函数以soc_device指针为函数参量的原因也很显然。 而遵循soc_core的调用关系,即cpu----àplatform----àcodec,在上述结构总很好的体现。 还有一个重要的结构就是runtime,该结构代表的是DAI runtime的信息。   b)上面的结构图是以snd_soc_device为主线的,即以设备驱动的创建过程分析所得。而下面的这个结构图,是以snd_pcm_substream为主线,主要是分析在open,hw_params,Write,read等操作中由substream得到所需结构的过程。   下一篇博客将介绍前面谈到的ALSA体系中,从内核调用到驱动的全过程。