平台:ubuntu 16.04,kernel版本是4.15.0, 理论任何平台都可以,甚至是android,只要能编译通过。
需要完成的功能:一个codec里面可能有多个录音通道,如果要打开某一通道录音,需要怎么做?
目的:就像做数学题一样,看一遍答案,以为自己看懂了,就会了,非也,真到自己去做时,不一定能做出来。那就在自己的驱动里实现一遍。
本文只追求应用,不讲原理。想了解细节可以看官方文档或者看https://blog.youkuaiyun.com/droidphone/category_1118446.html。
widget是干嘛用的
widget也可以控制寄存器。那问题来了,有了kcontrol,为什么还要有widget?
widget是kcontrol的plus,可以理解为是kcontrol的进一步升级和封装,它同样是指音频系统中的某个部件,比如mixer,mux,输入输出引脚,电源供应器等等。
kcontrol还有以下几点不足:
- 只能描述自身,无法描述各个kcontrol之间的连接关系;
- 没有相应的电源管理机制;
- 没有相应的时间处理机制来响应播放、停止、上电、下电等音频事件;
- 为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序;
- 当一个音频路径不再有效时,不能自动关闭该路径上的所有的kcontrol;
所以,widget诞生了。
像地图一样的codec
以下是全志A33 内部codec的模块图
像不像一幅地图。
当录音从mic1输入,经过各个mixer/mux,就像经过各个关卡一样,最终到达ADCL,模拟信号转换成数字信号。
条条大道通罗马,通往罗马的道路千万条,但是我们只需要走其中一条,没必要全部都走一遍;同样的,能到达ADCL的信号,可以是mic1、mic2或者其他的,但是当我只需要mic1输入时,其他路就不需要使能。需要被使能的这一条路,称之为complete path。
目的就是为了省电。
构造两条complete path
为更好理解dapm,在自己的虚拟声卡驱动里构造两条complete path。
先看看路线图,如下:
路线一:mic1 --> kcontrol(一个开关) --> mixer --> ADCL, 就是个录音
路线二:DACL --> HPOUTL --> speaker(功放), 就是个播放
下面看看代码实现
步骤一:定义widget
static const struct snd_kcontrol_new mic1_input_mixer[] = {
SOC_DAPM_SINGLE("MIC1 Boost Switch", VCODEC_ADCL_REG, 0, 1, 0),
};
static const struct snd_soc_dapm_widget vcodec_dapm_widgets[] = {
SND_SOC_DAPM_INPUT("MIC1"),
SND_SOC_DAPM_MIXER("ADCL Input", SND_SOC_NOPM, 0, 0,
mic1_input_mixer,
ARRAY_SIZE(mic1_input_mixer)),
SND_SOC_DAPM_AIF_OUT_E("ADCL", "Capture", 0, VCODEC_ADCL_REG,
8, 0,
vcodec_capture_event,
SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_POST_PMD),
SND_SOC_DAPM_AIF_IN_E("DACL", "Playback", 0, VCODEC_DAC_REG,
0, 0,
vcodec_playback_event,
SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD),
SND_SOC_DAPM_OUTPUT("HPOUTL"),
SND_SOC_DAPM_SPK("HpSpeaker", vcodec_hpspeaker_event),
};
这是个数组,成员是snd_soc_dapm_widget,通过各种宏定义来填充不同类型的widget;
-
SND_SOC_DAPM_INPUT:该widget对应一个输入引脚;一条完整的dapm音频路径,必然有起点和终点,我们把位于这些起点和终点的widget称之为端点widget。明显
mic1
是起点,ADCL
是终点; -
SND_SOC_DAPM_MIXER:该widget对应一个mixer控件,看一下宏定义:
#define SND_SOC_DAPM_MIXER(wname, wreg, wshift, winvert, \ wcontrols, wncontrols)\ { .id = snd_soc_dapm_mixer, .name = wname, \ SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ .kcontrol_news = wcontrols, .num_kcontrols = wncontrols}
参数说明:
- id:表示该widget的类型,snd_soc_dapm_mixer就是一个mixer控件;
- wname:该widget名称;
- wreg:该widget对应的寄存器控制位,用于控制widget的电源状态的,没有就是设置成SND_SOC_NOPM;
- winvert:设定值是否逻辑取反;
- wcontrols:kcontrol,表示开关,一个mixer可能有多路输入,一个kcontrol表示一个开关,就是上图的
m
; - wncontrols:kcontrol个数;
-
SND_SOC_DAPM_AIF_OUT_E:对应一个数字音频输出接口,看一下宏定义:
#define SND_SOC_DAPM_AIF_OUT_E(wname, stname, wslot, wreg, wshift, winvert, \ wevent, wflags) \ { .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname, \ SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ .event = wevent, .event_flags = wflags }
stname必须要和codec dai_driver 的stream_name一样;
心细如发的你肯定发现了这个宏后面带了“_E”后缀,相对没带“_E”后缀的多了wevent和wflags参数,wevent用于保存该widget的事件回调函数,同时,wflags用于保存该widget需要关心的dapm事件种类,只有wflags中相应的事件位被设置了的事件才会发到event回调函数中进行处理。dapm有哪些事件种类呢?
事件类型 说明 SND_SOC_DAPM_PRE_PMU widget要上电前发出的事件 SND_SOC_DAPM_POST_PMU widget要上电后发出的事件 SND_SOC_DAPM_PRE_PMD widget要下电前发出的事件 SND_SOC_DAPM_POST_PMD widget要下电后发出的事件 SND_SOC_DAPM_PRE_REG 音频路径设置之前发出的事件 SND_SOC_DAPM_POST_REG 音频路径设置之后发出的事件 SND_SOC_DAPM_WILL_PMU 在处理up_list链表之前发出的事件 SND_SOC_DAPM_WILL_PMD 在处理down_list链表之前发出的事件 SND_SOC_DAPM_PRE_POST_PMD SND_SOC_DAPM_PRE_PMD和
SND_SOC_DAPM_POST_PMD的合并 -
SND_SOC_DAPM_OUTPUT:该widget对应一个输出引脚;
-
SND_SOC_DAPM_SPK:该widget对应一个扬声器;
步骤二:注册widgets
snd_soc_dapm_new_controls(dapm, vcodec_dapm_widgets,
ARRAY_SIZE(vcodec_dapm_widgets));
widgets最终会加入到card的widgets链表
步骤三:设置route
static const struct snd_soc_dapm_route vcodec_dapm_routes[] = {
/* Mic input route */
{"ADCL Input", "MIC1 Boost Switch", "MIC1"},
{"ADCL", NULL, "ADCL Input"},
/* Headphone output route */
{"HPOUTL", NULL, "DACL"},
{"HpSpeaker", NULL, "HPOUTL"},
};
一个widget是有输入和输出的,widget之间是可以动态地进行连接的,连接两个widget的“线”就是path,它把一个widget的输出端和另一个widget的输入端连接在一起;
那route是干嘛用的?可以理解为,route就是用来生成path的,route指定了输出端的widget和输入端的widget,然后生成连接两widget的path;
比如上面的数组,“ADCL Input”
是目的widget,"MIC1 Boost Switch"
是两个widget之间的开关(kcontrol),没有就是NULL,"MIC1"
就是起始widget;
步骤四:注册routes
snd_soc_dapm_add_routes(dapm, vcodec_dapm_routes,
ARRAY_SIZE(vcodec_dapm_routes));
snd_soc_dapm_add_routes()函数所做的事情,简单点来说就是,先找到route指定的目的widget、起始widget和开关(kcontrol),生成path,然后将path添加到card的paths链表。
怎么用
dapm要给一个widget上电的其中一个前提条件是:这个widget位于一条完整的音频路径上,而一条完整的音频路径的两头,必须是输入/输出引脚,或者是一个外部音频设备,又或者是一个处于激活状态的音频流widget;
以刚定义的widget和route为例,"MIC1"是输入端点widget, "ADCL"是输出端点widget,那么
"MIC1" --> "ADCL Input" --> "ADCL"
就是一条complete path
(完整的音频路径),如果这条complete path
的开关(“MIC1 Boost Switch”)被应用使能了,在打开录音时,这条complete path
上的所有widget都会被上电;
同理"DACL" --> "HPOUTL" --> "HpSpeaker"
是一条complete path
,因为这条complete path
没有开关,所以在打开播放的时候,这条complete path
上的所有widget都会被上电,当然了,widget设置的event函数也会在此时调用。反之,结束播放的时候,这条complete path
上的所有widget都会被下电,widget设置的event函数也会被调用;
可以看看这篇文章:ALSA声卡驱动中的DAPM详解之六:精髓所在,牵一发而动全身
怎么知道什么时候给一条complete path
上电/下电,当然是去扫描一下了,
以下几种情况可以触发dapm发起一次扫描操作:
- 声卡初始化阶段;
- 用户空间修改了kcontrol的配置值;
- pcm的打开或关闭;
- 驱动改变widget并把它加入到dapm_dirty链表。
测试
在ubuntu下测试需要另外安装驱动,如下:
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/core/snd-compress.ko
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/core/snd-pcm-dmaengine.ko
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/soc/snd-soc-core.ko
为什么是/lib/modules/4.15.0-112-generic/kernel/sound/core/
这个目录,请看前几篇文章。
然后安装我们的驱动
sudo insmod vplatform.ko
sudo insmod vcodec.ko
sudo insmod vmachine.ko
如果你是在开发板上测试,就不需要这么麻烦,直接安装驱动
安装完驱动之后,就可以看到自己的声卡,如下:
vbox@vbox-pc:~$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: I82801AAICH [Intel 82801AA-ICH], device 0: Intel ICH [Intel 82801AA-ICH]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: mycodec [my-codec], device 0: MY-CODEC vcodec_dai-0 []
Subdevices: 1/1
Subdevice #0: subdevice #0
可见我们注册的是card 1,ubuntu本身有自己的声卡,再看看自己注册的controls
vbox@vbox-pc:~$ amixer -D hw:1 contents
numid=2,iface=MIXER,name='ADCL Input MIC1 Boost Switch'
; type=BOOLEAN,access=rw------,values=1
: values=on
numid=1,iface=MIXER,name='DAC volume'
; type=INTEGER,access=rw---R--,values=2,min=0,max=255,step=0
: values=50,60
| dBscale-min=-119.25dB,step=0.75dB,mute=0
诶,没看到注册widget,是注册失败了吗?并没有
可以看一下/sys/kernel/debug/asoc/my-codec/codec:vcodec.0/dapm
目录
vbox@vbox-pc:~$ sudo ls -l /sys/kernel/debug/asoc/my-codec/codec\:vcodec\.0/dapm
total 0
-r--r--r-- 1 root root 0 11月 15 18:58 ADCL
-r--r--r-- 1 root root 0 11月 15 18:58 ADCL Input
-r--r--r-- 1 root root 0 11月 15 18:58 bias_level
-r--r--r-- 1 root root 0 11月 15 18:58 Capture
-r--r--r-- 1 root root 0 11月 15 18:58 DACL
-r--r--r-- 1 root root 0 11月 15 18:58 HPOUTL
-r--r--r-- 1 root root 0 11月 15 18:58 HpSpeaker
-r--r--r-- 1 root root 0 11月 15 18:58 MIC1
-r--r--r-- 1 root root 0 11月 15 18:58 Playback
ps:ubuntu下要root权限才能看到
可以去打开一下’ADCL Input MIC1 Boost Switch’
vbox@vbox-pc:~$ amixer -D hw:1 cset numid=2 on
numid=2,iface=MIXER,name='ADCL Input MIC1 Boost Switch'
; type=BOOLEAN,access=rw------,values=1
: values=on
你会发现,这个kcontrol的名字变了,定义时是"MIC1 Boost Switch"
,现在变成了"ADCL Input MIC1 Boost Switch"
;
如果此时正在录音,那么对应的complete path
上的所有widget都会被上电,widget的event函数也会被调用;
看看以下log,会不会清楚一些
[ 2294.464437] vplat_pcm_open,line:235
[ 2294.464441] -vcodec_startup,line:193
[ 2294.464630] my_card_hw_params,rate:48000
[ 2294.464632] -vcodec_hw_params,line:205
[ 2294.464633] playback period size : 32768
[ 2294.464667] -vcodec_prepare,line:247
[ 2294.464697] -vcodec_playback_event,SND_SOC_DAPM_PRE_PMU
[ 2294.464700] -vcodec_reg_read,line:163,reg_data[2] = 0x0
[ 2294.464701] -vcodec_reg_write,line:171,reg=0x2,val=0x1
[ 2294.464703] -vcodec_hpspeaker_event,SND_SOC_DAPM_POST_PMU
[ 2294.466124] vplat_pcm_open,line:235
[ 2294.466127] -vcodec_startup,line:193
[ 2294.466251] my_card_hw_params,rate:48000
[ 2294.466253] -vcodec_hw_params,line:205
[ 2294.466254] capture period size : 32768
[ 2294.466289] -vcodec_prepare,line:247
[ 2294.466314] -vcodec_reg_read,line:163,reg_data[1] = 0x1
[ 2294.466315] -vcodec_reg_write,line:171,reg=0x1,val=0x101
[ 2294.466317] -vcodec_capture_event,SND_SOC_DAPM_POST_PMU
[ 2294.466336] -vcodec_prepare,line:247
[ 2294.466355] -vcodec_prepare,line:247
[ 2294.473240] -vcodec_trigger: catpure start
[ 2294.473243] capture running...
[ 2294.492381] -vcodec_trigger: playback start
[ 2294.492383] playback running...
[ 2304.140557] -vcodec_trigger:playback stop
[ 2304.140559] playback stop...
[ 2304.358123] -vcodec_shutdown,line:212
[ 2304.358126] vplat_pcm_close,line:248
[ 2304.358150] -vcodec_hpspeaker_event,SND_SOC_DAPM_PRE_PMD
[ 2304.358153] -vcodec_reg_read,line:163,reg_data[2] = 0x1
[ 2304.358155] -vcodec_reg_write,line:171,reg=0x2,val=0x0
[ 2304.358156] -vcodec_playback_event,SND_SOC_DAPM_POST_PMD
[ 2304.358178] -vcodec_trigger:catpure stop
[ 2304.358179] capture stop...
[ 2304.358235] -vcodec_shutdown,line:212
[ 2304.358236] vplat_pcm_close,line:248
[ 2304.358250] -vcodec_reg_read,line:163,reg_data[1] = 0x101
[ 2304.358252] -vcodec_reg_write,line:171,reg=0x1,val=0x1
[ 2304.358253] -vcodec_capture_event,SND_SOC_DAPM_POST_PMD
代码
代码位置:https://gitcode.com/u014056414/myalsa
后续会增加其他的功能,完成本文的提交是: 9.加widget, 模拟codec的两条complete path
初学者可以按照此提交学习,以免新提交干扰。