ALSA ASOC Kcontrol
一、结构体 snd_kcontrol_new
定义位于:include/sound/control.h
在前面"声卡之 Control 设备"一节中已经有介绍 Control 设备如何创建。通常,一个 kcontrol 代表着一个 mixer(混音器),或者是一个 mux(多路开关),又或者是一个音量控制器等等。从之前介绍中我们知道,定义一个 kcontrol 主要就是定义一个 snd_kcontrol_new 结构,为了方便讨论,这里再次给出它的定义:
struct snd_kcontrol_new {
snd_ctl_elem_iface_t iface; /* interface identifier */
unsigned int device; /* device/client number */
unsigned int subdevice; /* subdevice (substream) number */
const unsigned char *name; /* ASCII name of item */
unsigned int index; /* index of item */
unsigned int access; /* access rights */
unsigned int count; /* count of same elements */
snd_kcontrol_info_t *info;
snd_kcontrol_get_t *get;
snd_kcontrol_put_t *put;
union {
snd_kcontrol_tlv_rw_t *c;
const unsigned int *p;
} tlv;
unsigned long private_value;
};
我们知道,对于每个控件,我们需要定义一个和它对应的 snd_kcontrol_new 机构,这些 snd_kcontrol_new 结构会在声卡初始化阶段通过 snd_soc_add_card_controls() 函数(详细查看代码)
注册到系统中,用户空间就可以通过 amixer 或 alsamixer 等工具查看和设定这些控件的状态。
snd_kcontrol_new 结构中,几个主要的字段是 get,put,private_value 等。get 回调函数用于获取该控件当前的状态值,而 put 回调函数则用于设置控件的状态值,而 private_value 字段则根据不同的控件类型有不同的意义,比如普通控件,private_value 字段可以用来定义该控件所对应的寄存器地址以及对应的控制位在寄存器中的位置信息。值得庆幸的是,ASoC 系统已经为我们准备了大量的宏定义,用于定义常用的控件,这些宏定义位于 include/sound/soc.h
中。
二、ASoC 系统定义的 kcontrol 宏
# Note:ASoC Kcontrol 控件中 put 回调函数的定义仅仅是利用用户参数设置在实际的 register 中。
下面分别讨论下如何用这些预设的宏定义来定义一些常用的控件:
2.1 SOC_SINGLE
SOC_SINGLE 应该算是最简单的控件了,这种控件只有一个控制量,比如一个开关,又或者是一个数值变量(如 Codec 中的某个频率,FIFO 大小等),该宏定义如下:
#define SOC_SINGLE(xname, reg, shift, max, invert) \
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
.info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
.put = snd_soc_put_volsw, \
.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
该宏定义参数分别如下:
参数 | 含义 |
---|---|
xname | 控件的名字 |
reg | 控件对应的寄存器 |
shift | 控件位在寄存器中的位移 |
max | 控件可设置的最大值 |
invert | 设定值是否逻辑取反 |
private_value | 定义如下 : |
如定义所示,private_value 字段使用了一个新的宏定义:SOC_SINGLE_VALUE
,定义如下:
#define SOC_DOUBLE_VALUE(xreg, shift_left, shift_right, xmax, xinvert, xautodisable) \
((unsigned long)&(struct soc_mixer_control) \
{
.reg = xreg, .rreg = xreg, .shift = shift_left, \
.rshift = shift_right, .max = xmax, .platform_max = xmax, \
.invert = xinvert, .autodisable = xautodisable})
#define SOC_SINGLE_VALUE(xreg, xshift, xmax, xinvert, xautodisable) \
SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert, xautodisable)
如定义所示,这里实际上是定义了一个 soc_mixer_control
结构,然后把该结构的地址赋给了 private_value 字段,其定义如下:
/* mixer control */
struct soc_mixer_control {
int min, max, platform_max;
int reg, rreg;
unsigned int shift, rshift;
unsigned int sign_bit;
unsigned int invert:1;
unsigned int autodisable:1;
struct snd_soc_dobj dobj;
};
从其定义可以看出 soc_mixer_control 是控件特征的真正描述者,它确定了该控件对应寄存器的地址,位移值,最大值和是否逻辑取反等特性,控件的 put/get 回到函数则会借助该结构来访问实际的寄存器。例如可以看看这 get 回调函数的定义:位于 sound/soc/soc-ops.c
/**
* snd_soc_get_volsw - single mixer get callback
* @kcontrol: mixer control
* @ucontrol: control element information
*
* Callback to get the value of a single mixer control, or a double mixer
* control that spans 2 registers.
*
* Returns 0 for success.
*/
int snd_soc_get_volsw(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
//通过 kcontrol->private_value 获取到 soc_mixer_control
struct soc_mixer_control *mc =
(struct soc_mixer_control *)kcontrol->private_value;
unsigned int reg = mc->reg;
unsigned int reg2 = mc->rreg;
unsigned int shift = mc->shift;
unsigned int rshift = mc->rshift;
int max = mc->max;
int min = mc->min;
int sign_bit = mc->sign_bit;
unsigned int mask = (1 << fls(max)) - 1;
unsigned int invert = mc->invert;
int val;
int ret;
if (sign_bit)
mask = BIT(sign_bit + 1) - 1;
ret = snd_soc_read_signed(component, reg, mask, shift, sign_bit, &val);
if (ret)
return ret;
ucontrol->value.integer.value[0] = val - min;
if (invert)
ucontrol->value.integer.value[0] =
max - ucontrol->value.integer.value[0];
if (snd_soc_volsw_is_stereo(mc)) {
if (reg == reg2)
ret = snd_soc_read_signed(component, reg, mask, rshift,
sign_bit, &val);
else
ret = snd_soc_read_signed(component, reg2, mask, shift,
sign_bit, &val);
if (ret)
return ret;
ucontrol->value.integer.value[1] = val - min;
if (invert)
ucontrol->value.integer.value[1] =
max - ucontrol->value.integer.value[1];
}
return 0;
}
从上述代码一目了然,从 private_value 字段取出 soc_mixer_kcontrol 结构,利用该结构的信息访问对应的寄存器,返回相应的值。
2.2 SOC_SINGLE_TLV
该宏是 SOC_SINGLE 的一种扩展,主要用于定义那些有增益控制的控件,例如音量控制器,EQ均衡器等等,定义如下:
#define SOC_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array) \
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |\
SNDRV_CTL_ELEM_ACCESS_READWRITE,\
.tlv.p = (tlv_array), \
.info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
.put = snd_soc_put_volsw, \
.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
从它的定义可以看出,用于设置寄存器信息的 private_value 字段的定义和 SOC_SINGLE 是一样的,甚至 put、get 回到函数也是使用同一套,唯一不同的是增加了一个 tlv_arry 参数
,并把它赋值给了 tlv.p 字段
。用户空间可以通过声卡的 control 设备发起以下两种 ioctl 来访问 tlv 字段所指向的数组:
#define SNDRV_CTL_ELEM_ACCESS_TLV_READ (1<<4) /* TLV read is possible */
#define SNDRV_CTL_ELEM_ACCESS_TLV_WRITE (1<<5) /* TLV write is possible */
#define SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE (SNDRV_CTL_ELEM_ACCESS_TLV_READ|SNDRV_CTL_ELEM_ACCESS_TLV_WRITE)
#define SNDRV_CTL_ELEM_ACCESS_TLV_COMMAND (1<<6) /* TLV command is possible */
通常,tlv_arry 用来描述寄存器的设定值与它代表的实际意义之间的映射关系,最常用的就是用于音量控件时,设定值与对应的 dB 值之间的映射关系,如下以 wm8960.c 为例:
DECLARE_TLV_DB_SCALE 用于定义一个 dB 值映射的 tlv_array,上述的例子标明,该 tlv 的类型是 SNDRV_CTL_TLV_DB_SCALE,寄存器的最小值为 -15dB,寄存器每增加一个单位值,对应的 dB 数值是增加 3dB(0.01dB* 300),而由接下来的 SOC_SINGLE_TLV 定义可以看出,我们定义了一个 boost 控件,寄存器地址都是 WM8960_MIXER1,控制位是第 4 bit,最大值是 7,即对应的范围为 min:0(-15dB),max:7(6dB).
2.3 SOC_DOUBLE
与 SOC_SINGLE 相对应,区别是 SOC_SINGLE 只控制一个变量,而 SOC_DOUBLE 则可以同时控制一个寄存器中控制两个相似的变量,最常用的就是用于一些立体声的控件,我们需要同时对左右声道进行控制,因为多了一个声道,参数也就相应地多了一个 shift 位移值,其定义如下:
#define SOC_DOUBLE(xname, reg, shift_left, shift_right, max, invert) \
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = (xname),\
.info = snd_soc_info_volsw, .get = snd_soc_get_volsw, \
.put = snd_soc_put_volsw, \
.private_value = SOC_DOUBLE_VALUE(reg, shift_left, shift_right, \
max, invert, 0) }
SOC_DOUBLE_R:与 SOC_DOUBLE 类似,对于左右声道的控制寄存器不一样的情况,使用 SOC_DOUBLE_R 来定义,参数中需要指定两个寄存器地址,定义如下:
#define SOC_DOUBLE_R(xname, reg_left, reg_right, xshift, xmax, xinvert) \
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = (xname), \
.info = snd_soc_info_volsw, \
.get = snd_soc_get_volsw, .put = snd_soc_put_volsw, \
.private_value = SOC_DOUBLE_R_VALUE(reg_left, reg_right, xshift, \
xmax, xinvert) }
SOC_DOUBLE_TLV:与 SOC_SINGLE_TLV 对应的立体声版本,通常用于立体声音量控件的定义。
SOC_DOUBLE_R_TLV:左右声道有独立寄存器控制的 SOC_DOUBLE_TLV 版本。
2.4 Mixer 控件
用于音频通道的路由控制,由多个输入和一个输出组成,多个输入可以自由地混合在一起,形成混合后的输出:
对于 Mixer 控件,我们可以认为是多个简单控件的组合,通常,我们会为 mixer 的每个输入端都单独定义一个简单控件来控制该路输入的开启和关闭,反映在代码上,就是定义一个 soc_kcontrol_new 数组
,以 wm8960.c 举例:
以上这个 mixer 使用寄存器 WM8960_LOUTMIX 的第 8 位,WM8960_LOUTMIX 的第 7 位,WM8960_BYPASS1 的第7 位来分别控制 3 个输入端的开启和关闭。
2.5 Mux 控件
与 mixer 控件类似,也是多个输入端和一个输出端的组合控件,与 mixer 控件不同的是,mux 控件的多个输入端同时只能有一个被选中。因此,mux 控件所对应的寄存器,通常可以设定一段连续的数值,每个不同的数值对应不同的输入端被打开,与上述的 mixer 控件不同,ASoC 用 soc_enum
结构来描述 mux 控件的寄存器信息,定义如下:
/* enumerated kcontrol */
struct soc_enum {
int reg;
unsigned char shift_l;
unsigned char shift_r;
unsigned int items;
unsigned int mask;
const char * const *texts;
const unsigned int *values;
unsigned int autodisable:1;
struct snd_soc_dobj dobj;
};
两个寄存器地址和位移字段:reg,shift_l,shift_r,用于描述左右声道的控制寄存器信息。字符串数组指针 texts 用于描述每个输入端对应的名字,values 字段则指向一个数组,该数组定义了寄存器可以选择的值,每个值对应一个输入端,如果 value 是一组连续的值,通常我们可以忽略 values 参数。
其中,soc_enum 结构需要使用辅助宏 SOC_ENUM_SINGLE
来定义,该宏声明如下:
#define SOC_ENUM_DOUBLE(xreg, xshift_l, xshift_r, xitems, xtexts) \
{
.reg = xreg, .shift_l = xshift_l, .shift_r = xshift_r, \
.items = xitems, .texts = xtexts, \
.mask = xitems ? roundup_pow_of_two(xitems) - 1 : 0}
#define SOC_ENUM_SINGLE(xreg, xshift, xitems, xtexts) \
SOC_ENUM_DOUBLE(xreg, xshift, xshift, xitems, xtexts)
上述宏仅仅是填充 soc_enum 变量,但是对于定义好的 soc_enum 结构变量的话,我们还需要定义 Mux 控件的 snd_kcontrol_new
,需要使用 SOC_ENUM
宏,定义如下:
#define SOC_ENUM(xname, xenum) \
{
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,\
.info = snd_soc_info_enum_double, \
.get = snd_soc_get_enum_double, .put = snd_soc_put_enum_double, \
.private_value = (unsigned long)&xenum }
由定义看出,会使用 private_value 字段记录 soc_enum 结构变量,不过几个回调函数变了,我们看看 get 回调
对应的 snd_soc_get_enum_double 函数:
/**
* snd_soc_get_enum_double - enumerated double mixer get callback
* @kcontrol: mixer control
* @ucontrol: control element information
*
* Callback to get the value of a double enumerated mixer.
*
* Returns 0 for success.
*/
int snd_soc_get_enum_double(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;
unsigned int val, item;
unsigned int reg_val;
int ret;
ret = snd_soc_component_read(component, e->reg, ®_val);
if (ret)
return ret;
val = (reg_val >> e->shift_l) & e->mask;
item = snd_soc_enum_val_to_item(e, val);
ucontrol->value.enumerated.item[0] = item;
if (e->shift_l != e->shift_r) {
val = (reg_val >> e->shift_r) & e->mask;
item = snd_soc_enum_val_to_item(e, val);
ucontrol->value.enumerated.item[1] = item;
}
return 0;
}
从上述代码一目了然,从 private_value 字段取出 soc_enum 结构,利用该结构的信息访问对应的寄存器,返回相应的 item.
下面我们先看看如何定义一个 mux控件:
① 定义 mux 控件每个输入端对应的字符串 texts 和 values 数组,以下的例子因为 values 是连续的,所以不用定义:
② 利用 ASoC 提供的辅助宏定义 soc_enum 结构变量,用于描述寄存器:
③ 利用 ASoC 提供的辅助宏,定义 snd_kcontrol_new 结构,该结构最后用于注册 mux 控件:
以上几步定义了一个叫 ADC Data Output Select 的 mux 控件,该控件具有四个输入选择,分别是代表选择的是 LR/LL/RR/RL,用寄存器 WM8960_ADDCTL1 控制。
以下是另外几个常用于定义 mux 控件的宏:
- SOC_VALUE_ENUM_SINGLE 用于定义带 values 字段的 soc_enum 宏;
- SOC_VALUE_ENUM_DOUBLE SOC_VALUE_ENUM_SINGLE 对应的立体声版本。
2.6 其他控件
其实,除了以上介绍的几种常用控件,ASoC 还为我们提供了另外一些控件定义辅助宏,详细参考:include/sound/soc.h
.
这里列举几个:需要自己定义 get 和 put 回调时,可以使用以下这些带 EXT 的版本:
- SOC_SINGLE_EXT
- SOC_DOUBLE_EXT
- SOC_SINGLE_EXT_TLV
- SOC_DOUBLE_EXT_TLV
- SOC_DOUBLE_R_EXT_TLV
- SOC_ENUM_EXT