音频设备接口包括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体系中,从内核调用到驱动的全过程。