程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Rockchip RK3399 - ASoC 声卡之Control设备&kcontrol

----------------------------------------------------------------------------------------------------------------------------

开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux   :6.3
----------------------------------------------------------------------------------------------------------------------------

Control是音频驱动中用来表示用户可操作的音频参数或功能的抽象。它可以是音量控制、混音控制(Mixer)、开关控制(Mux)等。Control 提供了一个统一的接口,使用户能够通过音频设备驱动程序来管理和调整音频参数。

ALSA CORE已经实现了Control中间层,在include/sound/control.h中定义了所有的Control API.,如果你要为你的Codec实现自己的控件,请在代码中包含该头文件。

需要注意的是:这里说的Control设备中的Control表示的是控制的意思;而后文提到的controls/control/kcontrol表示的是控件的意思,其主要实现控制声卡的音量,混音等一系列控制,可以理解为switch,

一、Control设备

1.1 创建Control设备

Control设备和PCM设备一样,都属于声卡下的逻辑设备。用户空间的应用程序通过alsa-lib访问该Control设备,读取或控制控件的控制状态,从而达到控制音频Codec进行各种Mixer等控制操作。

Control设备的创建过程大体上和PCM设备的创建过程相同。对于创建 PCM设备只需要在驱动初始化时主动调用snd_pcm_new函数创建,而Control设备则用snd_ctl_create创建。不过由于 snd_card_create函数中已经会调用 snd_ctl_create函数创建Control设备,故我们无需显示地创建Control设备,只要建立声卡,Control设备则被自动地创建。

我们来看一下snd_ctl_create到底做了什么。函数定义在sound/core/control.c;

/*
 * create control core:
 * called from init.c
 */
int snd_ctl_create(struct snd_card *card)       // 传入声卡设备
{
        static struct snd_device_ops ops = {     // 声卡Control设备操作集
                .dev_free = snd_ctl_dev_free,
                .dev_register = snd_ctl_dev_register,
                .dev_disconnect = snd_ctl_dev_disconnect,
        };
        int err;

        if (snd_BUG_ON(!card))
                return -ENXIO;
        if (snd_BUG_ON(card->number < 0 || card->number >= SNDRV_CARDS))  // 声卡设备编号无效
                return -ENXIO;

        snd_device_initialize(&card->ctl_dev, card);               // 初始化card->ctl_dev控制设备,设置class为sound_class,parent为card->card_dev
        dev_set_name(&card->ctl_dev, "controlC%d", card->number);  // 为控制设备分配一个名词 controlC%d

        err = snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);  // 创建一个新的snd_device实例,并将添加到声卡设备的devices链表中
        if (err < 0)
                put_device(&card->ctl_dev);
        return err;
}

这个函数主要只做了两件事情:

  • 调用snd_device_initialize初始化声卡的控制设备,也就是card->ctrl_dev;这里设置控制设备的parent为声卡设备card->card_dev,class为sound_class;
  • 调用snd_device_new为控制设备分配一个snd_device实例,并添加到声卡设备的逻辑设备链表devices中;
1.1.1 snd_device_initialize

snd_device_initialize函数定义在sound/core/init.c,用于初始化struct device 结构体的各种成员变量并分配合适的资源;

/**
 * snd_device_initialize - Initialize struct device for sound devices
 * @dev: device to initialize
 * @card: card to assign, optional
 */
void snd_device_initialize(struct device *dev, struct snd_card *card)
{
        device_initialize(dev);
        if (card)
                dev->parent = &card->card_dev;
        dev->class = sound_class;
        dev->release = default_release;
}
1.1.2 snd_ctl_dev_register

在注册声卡设备card时会遍历声卡设备的逻辑设备链表devices,并调用声卡逻辑设备操作集中的dev_register函数,对于Control设备也就是snd_ctl_dev_register函数;

我们最后来看一下Control设备的操作集:

static struct snd_device_ops ops = {    
        .dev_free = snd_ctl_dev_free,
        .dev_register = snd_ctl_dev_register,
        .dev_disconnect = snd_ctl_dev_disconnect,
};

这些回调函数都是定义在sound/core/control.c,以snd_ctl_dev_register函数为例,该回调函数建立了和用户空间应用程序(alsa-lib)通信所用的设备文件节点:/dev/snd/controlC%d;

/*
 * registration of the control device
 */
static int snd_ctl_dev_register(struct snd_device *device)
{
        struct snd_card *card = device->device_data;

        return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,       // 注册Control设备
                                   &snd_ctl_f_ops, card, &card->ctl_dev);
}

dev_free、dev_disconnnet我们不是特别关心,忽略就即可:

/*
 * disconnection of the control device
 */
static int snd_ctl_dev_disconnect(struct snd_device *device)
{
        struct snd_card *card = device->device_data;
        struct snd_ctl_file *ctl;

        read_lock(&card->ctl_files_rwlock);     // 读写自旋锁
        list_for_each_entry(ctl, &card->ctl_files, list) {  // 遍历ctl_files链表
                wake_up(&ctl->change_sleep);
                kill_fasync(&ctl->fasync, SIGIO, POLL_ERR);
        }
        read_unlock(&card->ctl_files_rwlock);  // 释放锁

        return snd_unregister_device(&card->ctl_dev);  // 卸载声卡逻辑设备
}

/*
 * free all controls
 */
static int snd_ctl_dev_free(struct snd_device *device)
{
        struct snd_card *card = device->device_data;
        struct snd_kcontrol *control;

        down_write(&card->controls_rwsem);     // 获取读写信号量
        while (!list_empty(&card->controls)) {  // 遍历控制链表
                control = snd_kcontrol(card->controls.next);
                snd_ctl_remove(card, control); 
        }
        up_write(&card->controls_rwsem);   // 释放读写信号量
        put_device(&card->ctl_dev);
        return 0;
}
View Code

二、kcontrol

kcontrol其实是一种控件,其主要实现控制声卡的音量,混音等一系列控制,可以理解为switch。

kcontrol对应的数据结构是snd_kcontrol_new,这些snd_kcontrol_new结构会在声卡的初始化阶段,通过snd_soc_add_component_controls函数注册到系统中,用户空间就可以通过amixer或alsamixer等工具查看和设定这些控件的状态。

实际上除了snd_kcontrol_new结构外,还有一个snd_kcontrol结构,snd_kcontrol_new更像是kcontrol的模板,而snd_kcontrol才是真正的kcontrol。

控件的创建步骤如下:
(1)定义snd_kcontrol_new数组;

(2)通过snd_soc_add_component_controls根据snd_kcontrol_new数组创建并添加多个kcontrol到声卡card的controls链表;

2.1 数据结构

2.1.1 snd_kcontrol_new 

struct snd_kcontrol_new定义在include/sound/control.h文件中:

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 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;
};

其中:

  • iface:控件的类型,alsa定义了几种类型(SNDDRV_CTL_ELEM_IFACE_XXX),常用的类型是 MIXER,当然也可以定义属于全局的 CARD 类型,也可以定义属于某类设备的类型,例如 HWDEP,PCMRAWMIDI,TIMER 等,这时需要在 device 和 subdevice 字段中指出声卡的设备逻辑编号;
  • name :控件的名字,从ALSA 0.9.x开始,控件的名字是变得比较重要,因为控件的作用是按名字来归类的。ALSA 已经预定义了一些控件的名字,我们在后面的章节中会详细讨论;
  • index:控件的在该声卡中的编号。如果声卡中有不止一个codec,每个codec中有相同名字的控件,这时我们可以通过index来区分这些控件。当index 为 0 时,则可以忽略这种区分策略;
  • access:控件的访问类型。每一个 bit 代表一种访问类型,这些访问类型可以多个“或”运算组合在一起;
  • private_value:根据不同的控件类型有不同的意义,比如对于普通的控件,private_value字段可以用来定义该控件所对应的寄存器的地址以及对应的控制位在寄存器中的位置信息;
  • tlv:控件元数据;
  • get:用于获取控件当前的状态值;
  • put:用于设置控件的状态值;
2.1.2 关系图

为了更加清晰的了解struct snd_kcontrol_new、struct snd_kcontrol 等数据结构的关系,我们绘制了如下关系图:

2.1.3 控件的名字

控件的名字需要遵循一些标准,通常可以分成3部分来定义控件的名字:源–方向–功能。

  • 源:可以理解为该控件的输入端,alsa已经预定义了一些常用的源,例如:Master,PCM,CD,Line、I2S、Headphone、Speaker、Mic等等;
  • 方向:代表该控件的数据流向,例如:Playback,Capture,Bypass,Bypass Capture 等等,也可以不定义方向,这时表示该控件是双向的( playback和capture);
  • 功能:根据控件的功能,可以是以下字符串:Switch,Volume,Route等等;

也有一些命名上的特例,比如Tone Control - Switch、Tone Control - Bass等,目前官方已经不推荐使用了。

更多内容可以参考官方指导手册:Standard ALSA Control Names

2.1.3 访问标志

access字段是一个bitmask,它保存了该控件的访问类型。默认的访问类型是:SNDDRV_CTL_ELEM_ACCESS_READWRITE,表明该控件支持读和写操作。如果access字段没有定义(.access==0),此时也认为是 READWRITE 类型。

如果是一个只读控件,access应该设置为:SNDDRV_CTL_ELEM_ACCESS_READ,这时,我们不必定义put回调函数。类似地,如果是只写控件,access应该设置为:SNDDRV_CTL_ELEM_ACCESS_WRITE,这时,我们不必定义get回调函数。

如果控件的值会频繁地改变(例如:电平表),我们可以使用VOLATILE类型,这意味着该控件会在没有通知的情况下改变,应用程序应该定时地查询该控件的值。

2.1.5 回调函数

(1)info回调函数

info回调函数用于获取控件的详细信息。它的主要工作就是填充通过参数传入的snd_ctl_elem_info数据结构,以下例子是一个具有单个元素的boolean型控件的info回调:

/**
 * snd_ctl_boolean_mono_info - Helper function for a standard boolean info
 * callback with a mono channel
 * @kcontrol: the kcontrol instance
 * @uinfo: info to store
 *
 * This is a function that can be used as info callback for a standard
 * boolean control with a single mono channel.
 *
 * Return: Zero (always successful)
 */
int snd_ctl_boolean_mono_info(struct snd_kcontrol *kcontrol,
                              struct snd_ctl_elem_info *uinfo)
{
        uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
        uinfo->count = 1;
        uinfo->value.integer.min = 0;
        uinfo->value.integer.max = 1;
        return 0;
}

其中:

  • type :指出该控件的值类型,值类型可以是BOOLEAN,INTEGER,ENUMERATED,BYTES,IEC958和INTEGER64之一;
  • count:指出了该控件中包含有多少个元素单元,比如,立体声的音量控件左右两个声道的音量值,它的count字段等于2;
  • value: 是一个联合体(union),value的内容和控件的类型有关;其中,boolean和integer类型是相同的;

ENUMERATED类型有些特殊。它的value需要设定一个字符串和字符串的索引,请看以下例子:

/**
 * snd_ctl_enum_info - fills the info structure for an enumerated control
 * @info: the structure to be filled
 * @channels: the number of the control's channels; often one
 * @items: the number of control values; also the size of @names
 * @names: an array containing the names of all control values
 *
 * Sets all required fields in @info to their appropriate values.
 * If the control's accessibility is not the default (readable and writable),
 * the caller has to fill @info->access.
 *
 * Return: Zero (always successful)
 */
int snd_ctl_enum_info(struct snd_ctl_elem_info *info, unsigned int channels,
                      unsigned int items, const char *const names[])
{
        info->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
        info->count = channels;
        info->value.enumerated.items = items;
        if (!items)
                return 0;
        if (info->value.enumerated.item >= items)
                info->value.enumerated.item = items - 1;
        WARN(strlen(names[info->value.enumerated.item]) >= sizeof(info->value.enumerated.name),
             "ALSA: too long item name '%s'\n",
             names[info->value.enumerated.item]);
        strscpy(info->value.enumerated.name,
                names[info->value.enumerated.item],
                sizeof(info->value.enumerated.name));
        return 0;
}

alsa已经为我们实现了一些通用的info回调函数,例如:snd_ctl_boolean_mono_info,snd_ctl_boolean_stereo_info、snd_ctl_enum_info等等。

(2)get回调函数

get回调函数用于读取控件的当前值,并返回给用户空间的应用程序;

static int snd_myctl_get(struct snd_kcontrol *kcontrol,
    struct snd_ctl_elem_value *ucontrol)
{
    struct mychip *chip = snd_kcontrol_chip(kcontrol);
    ucontrol->value.integer.value[0] = get_some_value(chip);
    return 0;
}

value字段的赋值依赖于控件的类型(如同info回调)。很多声卡的驱动利用它存储硬件寄存器的地址、bit-shift和bit-mask,这时private_value字段可以按以下例子进行设置:

private_value = reg | (shift << 16) | (mask << 24);

然后,get回调函数可以这样实现:

static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol,
    struct snd_ctl_elem_value *ucontrol)

{
    int reg = kcontrol->private_value & 0xff;
    int shift = (kcontrol->private_value >> 16) & 0xff;
    int mask = (kcontrol->private_value >> 24) & 0xff;
    ....

    //根据以上的值读取相应寄存器的值并填入value中
}

如果控件的count字段大于1,表示控件有多个元素单元,get回调函数也应该为value填充多个数值。

(3)put回调函数

put回调函数用于把应用程序的控制值设置到控件中。

static int snd_myctl_put(struct snd_kcontrol *kcontrol,
    struct snd_ctl_elem_value *ucontrol)
{
    struct mychip *chip = snd_kcontrol_chip(kcontrol);
    int changed = 0;
    if (chip->current_value !=
        ucontrol->value.integer.value[0]) {
        change_current_value(chip,
        ucontrol->value.integer.value[0]);
        changed = 1;
    }
    return changed;
}

如上述例子所示,当控件的值被改变时,put回调必须要返回1,如果值没有被改变,则返回0。如果发生了错误,则返回一个负数的错误号。

和get回调一样,当控件的count 大于1时,put回调也要处理多个控件中的元素值。

2.2 辅助宏定义

ASoc层已经为我们准备了大量的宏定义,用于定义常用的控件,这些宏定义位于include/sound/soc.h中。下面我们分别讨论一下如何用这些预设的宏定义来定义一些常用的控件。

2.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字段:SOC_SINGLE_VALUE,我们看看它的定义:

#define SOC_SINGLE_VALUE(xreg, xshift, xmax, xinvert, xautodisable) \
        SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert, xautodisable)

#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, \
        .invert = xinvert, .autodisable = xautodisable})

这里实际上是定义了一个soc_mixer_control结构,然后把该结构的地址赋值给了private_value字段,soc_mixer_control结构是这样的:

/* 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;
#ifdef CONFIG_SND_SOC_TOPOLOGY
        struct snd_soc_dobj dobj;
#endif
};

看来soc_mixer_control是控件特征的真正描述者,它确定了该控件对应寄存器的地址,位移值,最大值和是否逻辑取反等特性。

控件的put回调函数和get回调函数需要借助该结构来访问实际的寄存器。我们看看这get回调函数snd_soc_get_volsw的定义, 定义位于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);
        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;
}
View Code

上述代码一目了然,从private_value字段取出soc_mixer_control结构,利用该结构的信息,访问对应的寄存器,返回相应的值。

2.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_array参数,并把它赋值给了tlv.p字段。用户空间可以通过对声卡的Control设备发起以下两种ioctl来访问tlv字段所指向的数组:

SNDRV_CTL_IOCTL_TLV_READ
SNDRV_CTL_IOCTL_TLV_WRITE
SNDRV_CTL_IOCTL_TLV_COMMAND

通常,tlv_array用来描述寄存器的设定值与它所代表的实际意义之间的映射关系,最常用的就是用于音量控件时,设定值与对应的dB值之间的映射关系,请看以下例子:

static const DECLARE_TLV_DB_SCALE(mixin_boost_tlv, 0, 900, 0);

static const struct snd_kcontrol_new wm1811_snd_controls[] = {
SOC_SINGLE_TLV("MIXINL IN1LP Boost Volume", WM8994_INPUT_MIXER_1, 7, 1, 0,
               mixin_boost_tlv),
SOC_SINGLE_TLV("MIXINL IN1RP Boost Volume", WM8994_INPUT_MIXER_1, 8, 1, 0,
               mixin_boost_tlv),
};

DECLARE_TLV_DB_SCALE用于定义一个dB值映射的tlv_array,上述的例子表明,该tlv的类型是SNDRV_CTL_TLVT_DB_SCALE,寄存器的最小值对应是0dB,寄存器每增加一个单位值,对应的dB数增加是9dB(0.01dB*900),而由接下来的两组SOC_SINGLE_TLV定义可以看出,我们定义了两个boost控件,寄存器的地址都是WM8994_INPUT_MIXER_1,控制位分别是第7bit和第8bit,最大值是1,所以,该控件只能设定两个数值0和1,对应的dB值就是0dB和9dB。

2.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来定义,参数中需要指定两个寄存器地址。

SOC_DOUBLE_TLV : 与SOC_SINGLE_TLV对应的立体声版本,通常用于立体声音量控件的定义。

SOC_DOUBLE_R_TLV: 左右声道有独立寄存器控制的SOC_DOUBLE_TLV版本。

2.2.4 Mixer控件

用于音频通道的路由控制,由多个输入和一个输出组成,多个输入可以自由地混合在一起,形成混合后的输出:如手机同时打电话,又播放音乐,需要将两路数据混合后再输出到Speaker,就需要用到Mixer;

对于Mixer控件,我们可以认为是多个简单控件的组合,通常,我们会为Mixer的每个输入端都单独定义一个简单控件来控制该路输入的开启和关闭,反应在代码上,就是定义一个soc_kcontrol_new数组:

static const struct snd_kcontrol_new wm8960_lin_boost[] = {
    SOC_SINGLE("LINPUT2 Switch", WM8960_LINPATH, 6, 1, 0),
    SOC_SINGLE("LINPUT3 Switch", WM8960_LINPATH, 7, 1, 0),
    SOC_SINGLE("LINPUT1 Switch", WM8960_LINPATH, 8, 1, 0),
};

以上这个Mixer使用寄存器WM8960_LINPATH的第6,7、8位来分别控制3个输入端的开启和关闭。

2.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;
#ifdef CONFIG_SND_SOC_TOPOLOGY
        struct snd_soc_dobj dobj;
#endif
};

其中:

  • reg,reg2,shift_l,shift_r:两个寄存器地址和位移字段,用于描述左右声道的控制寄存器信息;
  • texts:字符串数组指针用于描述每个输入端对应的名字;
  • values:则指向一个数组,该数组定义了寄存器可以选择的值,每个值对应一个输入端,如果values是一组连续的值,通常我们可以忽略values参数。

下面我们先看看如何定义一个Mux控件:第一步,定义字符串和values数组,以下的例子因为values是连续的,所以不用定义:

static const char *drc_path_text[] = {
       "ADC",
       "DAC"
};

第二步,利用ASoc提供的辅助宏定义soc_enum结构,用于描述寄存器:

static const struct soc_enum drc_path =
        SOC_ENUM_SINGLE(WM8993_DRC_CONTROL_1, 14, 2, drc_path_text);

第三步,利用ASoc提供的辅助宏,定义soc_kcontrol_new结构,该结构最后用于注册该Mux控件:

static const struct snd_kcontrol_new wm8993_snd_controls[] = {
  SOC_DOUBLE_TLV(......),
  ......
  SOC_ENUM("DRC Path", drc_path),
  ......
}

以上几步定义了一个叫DRC PATH的Mux控件,该控件具有两个输入选择,分别是来自ADC和DAC,用寄存器WM8993_DRC_CONTROL_1控制。其中,soc_enum结构使用了辅助宏SOC_ENUM_SINGLE来定义,该宏的声明如下:

#define SOC_ENUM_DOUBLE(xreg, xshift_l, xshift_r, xmax, xtexts) \
{       .reg = xreg, .shift_l = xshift_l, .shift_r = xshift_r, \
        .max = xmax, .texts = xtexts, \
        .mask = xmax ? roundup_pow_of_two(xmax) - 1 : 0}
#define SOC_ENUM_SINGLE(xreg, xshift, xmax, xtexts) \
        SOC_ENUM_DOUBLE(xreg, xshift, xshift, xmax, xtexts)

定义soc_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;

        reg_val = snd_soc_component_read(component, e->reg);
        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;
}

通过info回调函数则可以获取某个输入端所对应的名字,其实就是从soc_enum结构的texts数组中获得:

/**
 * snd_soc_info_enum_double - enumerated double mixer info callback
 * @kcontrol: mixer control
 * @uinfo: control element information
 *
 * Callback to provide information about a double enumerated
 * mixer control.
 *
 * Returns 0 for success.
 */
int snd_soc_info_enum_double(struct snd_kcontrol *kcontrol,
        struct snd_ctl_elem_info *uinfo)
{
        struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;

        return snd_ctl_enum_info(uinfo, e->shift_l == e->shift_r ? 1 : 2,
                                 e->items, e->texts);
}

以下是另外几个常用于定义Mux控件的宏:

  • SOC_VALUE_ENUM_SINGLE :用于定义带values字段的soc_enum结构;
  • SOC_VALUE_ENUM_DOUBLE   :SOC_VALUE_ENUM_SINGLE的立体声版本;
2.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。

2.3 核心API

2.3.1 snd_soc_cnew

当把以上讨论的内容都准备好了以后,我们就可以通过snd_soc_cnew函数创建我们自己的kcontrol 了,snd_soc_cnew位于 sound/soc/soc-core.c,由ASoC层提供;

/**
 * snd_soc_cnew - create new control
 * @_template: control template
 * @data: control private data
 * @long_name: control long name
 * @prefix: control name prefix
 *
 * Create a new mixer control from a template control.
 *
 * Returns 0 for success, else error.
 */
struct snd_kcontrol *snd_soc_cnew(const struct snd_kcontrol_new *_template, // kcontrol模板
                                  void *data, const char *long_name,
                                  const char *prefix)
{
        struct snd_kcontrol_new template;
        struct snd_kcontrol *kcontrol;
        char *name = NULL;

        memcpy(&template, _template, sizeof(template));
        template.index = 0;

        if (!long_name)
                long_name = template.name;

        if (prefix) {
                name = kasprintf(GFP_KERNEL, "%s %s", prefix, long_name);
                if (!name)
                        return NULL;

                template.name = name;
        } else {
                template.name = long_name;
        }

        kcontrol = snd_ctl_new1(&template, data);  // 重点

        kfree(name);

        return kcontrol;
}

最终调用ALSA层的snd_ctl_new1,函数位于sound/soc/control.c文件。snd_ctl_new1会分配一个kcontrol,并把ncontrol模板中相应的值复制到该kcontrol中,所以,在定义ncontrol时,通常我们可以加上 __devinitdata 前缀;

/**
 * snd_ctl_new1 - create a control instance from the template
 * @ncontrol: the initialization record
 * @private_data: the private data to set
 *
 * Allocates a new struct snd_kcontrol instance and initialize from the given
 * template.  When the access field of ncontrol is 0, it's assumed as
 * READWRITE access. When the count field is 0, it's assumes as one.
 *
 * Return: The pointer of the newly generated instance, or %NULL on failure.
 */
struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,
                                  void *private_data)
{
        struct snd_kcontrol *kctl;
        unsigned int count;
        unsigned int access;
        int err;

        if (snd_BUG_ON(!ncontrol || !ncontrol->info))
                return NULL;

        count = ncontrol->count;
        if (count == 0)
                count = 1;

        access = ncontrol->access;
        if (access == 0)     // 设置默认访问类型
                access = SNDRV_CTL_ELEM_ACCESS_READWRITE;
        access &= (SNDRV_CTL_ELEM_ACCESS_READWRITE |
                   SNDRV_CTL_ELEM_ACCESS_VOLATILE |
                   SNDRV_CTL_ELEM_ACCESS_INACTIVE |
                   SNDRV_CTL_ELEM_ACCESS_TLV_READWRITE |
                   SNDRV_CTL_ELEM_ACCESS_TLV_COMMAND |
                   SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK |
                   SNDRV_CTL_ELEM_ACCESS_LED_MASK |
                   SNDRV_CTL_ELEM_ACCESS_SKIP_CHECK);

        err = snd_ctl_new(&kctl, count, access, NULL);  // 创建一个新的snd_kcontrol,并通过kctl返回;
        if (err < 0)
                return NULL;

        /* The 'numid' member is decided when calling snd_ctl_add(). */
        kctl->id.iface = ncontrol->iface;
        kctl->id.device = ncontrol->device;
        kctl->id.subdevice = ncontrol->subdevice;
        if (ncontrol->name) {
                strscpy(kctl->id.name, ncontrol->name, sizeof(kctl->id.name));
                if (strcmp(ncontrol->name, kctl->id.name) != 0)
                        pr_warn("ALSA: Control name '%s' truncated to '%s'\n",
                                ncontrol->name, kctl->id.name);
        }
        kctl->id.index = ncontrol->index;

        kctl->info = ncontrol->info;

        kctl->info = ncontrol->info;
        kctl->get = ncontrol->get;
        kctl->put = ncontrol->put;
        kctl->tlv.p = ncontrol->tlv.p;

        kctl->private_value = ncontrol->private_value;
        kctl->private_data = private_data;

        return kctl;
}

snd_ctl_new函数用于申请一个kcontrol,并通过kctl返回;

/**
 * snd_ctl_new - create a new control instance with some elements
 * @kctl: the pointer to store new control instance
 * @count: the number of elements in this control
 * @access: the default access flags for elements in this control
 * @file: given when locking these elements
 *
 * Allocates a memory object for a new control instance. The instance has
 * elements as many as the given number (@count). Each element has given
 * access permissions (@access). Each element is locked when @file is given.
 *
 * Return: 0 on success, error code on failure
 */
static int snd_ctl_new(struct snd_kcontrol **kctl, unsigned int count,
                       unsigned int access, struct snd_ctl_file *file)
{
        unsigned int idx;

        if (count == 0 || count > MAX_CONTROL_COUNT)
                return -EINVAL;

        *kctl = kzalloc(struct_size(*kctl, vd, count), GFP_KERNEL);  // 动态分配内存,数据结构类型为struct snd_kcontrol ,同时额外为成员vd分配内存
        if (!*kctl)
                return -ENOMEM;

        for (idx = 0; idx < count; idx++) {  // 初始化成员vd,struct snd_kcontrol_volatile数组,长度为count
                (*kctl)->vd[idx].access = access;   // 设置访问标志
                (*kctl)->vd[idx].owner = file;
        }
        (*kctl)->count = count;  // 设置数量

        return 0;
}
2.3.2 snd_ctl_add

当控件创建完毕,我们就可以调用snd_ctl_add函数将kcontrol添加到声卡card的controls链表中,函数位于sound/soc/control.c文件;

/**
 * snd_ctl_add - add the control instance to the card
 * @card: the card instance
 * @kcontrol: the control instance to add
 *
 * Adds the control instance created via snd_ctl_new() or
 * snd_ctl_new1() to the given card. Assigns also an unique
 * numid used for fast search.
 *
 * It frees automatically the control which cannot be added.
 *
 * Return: Zero if successful, or a negative error code on failure.
 *
 */
int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol)
{
        return snd_ctl_add_replace(card, kcontrol, CTL_ADD_EXCLUSIVE);
}

snd_ctl_add_replace函数实现如下:

static int snd_ctl_add_replace(struct snd_card *card,
                               struct snd_kcontrol *kcontrol,
                               enum snd_ctl_add_mode mode)  // CTL_ADD_EXCLUSIVE
{
        int err = -EINVAL;

        if (! kcontrol)
                return err;
        if (snd_BUG_ON(!card || !kcontrol->info))
                goto error;

        down_write(&card->controls_rwsem);   // 获取读写信号量,用于并发操作controls链表
        err = __snd_ctl_add_replace(card, kcontrol, mode);
        up_write(&card->controls_rwsem); // 释放信号量
        if (err < 0)
                goto error;
        return 0;

 error:
        snd_ctl_free_one(kcontrol);
        return err;
}

__snd_ctl_add_replace函数实现如下:

/* add/replace a new kcontrol object; call with card->controls_rwsem locked */
static int __snd_ctl_add_replace(struct snd_card *card,
                                 struct snd_kcontrol *kcontrol,
                                 enum snd_ctl_add_mode mode)  // CTL_ADD_EXCLUSIVE
{
        struct snd_ctl_elem_id id;
        unsigned int idx;
        struct snd_kcontrol *old;
        int err;

        id = kcontrol->id;
        if (id.index > UINT_MAX - kcontrol->count)
                return -EINVAL;

        old = snd_ctl_find_id(card, &id);  // 根据id查找card->controls链表中的匹配项(实际上就是变量card->controls成员的id)
        if (!old) {     // 未找到
                if (mode == CTL_REPLACE)
                        return -EINVAL;
        } else {  // 如果已经存在,直接返回错误信息
                if (mode == CTL_ADD_EXCLUSIVE) {
                        dev_err(card->dev,
                                "control %i:%i:%i:%s:%i is already present\n",
                                id.iface, id.device, id.subdevice, id.name,
                                id.index);
                        return -EBUSY;
                }

                err = snd_ctl_remove(card, old);  // 移除
                if (err < 0)
                        return err;
        }

        if (snd_ctl_find_hole(card, kcontrol->count) < 0)
                return -ENOMEM;

        list_add_tail(&kcontrol->list, &card->controls); // 将kcontrl->list链表节点添加到card->controls链表中
        card->controls_count += kcontrol->count;   // 计数
        kcontrol->id.numid = card->last_numid + 1;  // 分配一个编号 
        card->last_numid += kcontrol->count;

        add_hash_entries(card, kcontrol);

        for (idx = 0; idx < kcontrol->count; idx++)
                snd_ctl_notify_one(card, SNDRV_CTL_EVENT_MASK_ADD, kcontrol, idx);

        return 0;
}
2.3.3 snd_soc_add_component_controls

ASoC CORE对snd_ctl_new1、snd_ctl_add进行了包装类,提供了snd_soc_add_component_controls函数根据kcontrol模板数组创建并添加多个kcontrol到声卡card的controls链表,函数定义在sound/soc/soc-core.c:

/**
 * snd_soc_add_component_controls - Add an array of controls to a component.
 *
 * @component: Component to add controls to
 * @controls: Array of controls to add
 * @num_controls: Number of elements in the array
 *
 * Return: 0 for success, else error.
 */
int snd_soc_add_component_controls(struct snd_soc_component *component,
        const struct snd_kcontrol_new *controls, unsigned int num_controls)
{
        struct snd_card *card = component->card->snd_card;

        return snd_soc_add_controls(card, component->dev, controls,
                        num_controls, component->name_prefix, component);
} 

具体是通过snd_soc_add_controls函数完成的:

static int snd_soc_add_controls(struct snd_card *card, struct device *dev,
        const struct snd_kcontrol_new *controls, int num_controls,
        const char *prefix, void *data)
{
        int i;

        for (i = 0; i < num_controls; i++) {  // 注册每一个snd_kcontrol_new
                const struct snd_kcontrol_new *control = &controls[i];
                int err = snd_ctl_add(card, snd_soc_cnew(control, data, // 这里实际上就是调用了snd_soc_cnew创建snd_kcontrol实例
                                                         control->name, prefix));
                if (err < 0) {
                        dev_err(dev, "ASoC: Failed to add %s: %d\n",
                                control->name, err);
                        return err;
                }
        }

        return 0;
}

三、Control设备文件

当我们移植外音频驱动之后,我们是可以在/dev/snd目录下看到pcm设备文件的,比如在NanoPC-T4开发板移植了ALC5651声卡驱动之后;

root@rk3399:~# ll /dev/snd
drwxr-xr-x  2 root root       60 Aug 24 21:31 by-path/
crw-rw----  1 root audio 116,  4 Aug 24 21:31 controlC0
crw-rw----  1 root audio 116,  3 Aug 24 21:31 pcmC0D0c
crw-rw----  1 root audio 116,  2 Aug 24 21:31 pcmC0D0p
crw-rw----  1 root audio 116,  1 Aug 24 21:31 seq
crw-rw----  1 root audio 116, 33 Aug 24 21:31 timer

可以看到这些字符设备的主设备号都是116,其中:

  • controlC0:用于声卡的控制,例如通道选择,混音,麦克控制,音量加减,开关等;

  • pcmC0D0c:用于录音的pcm设备;

  • pcmC0D0p:用于播放的pcm设备;

  • seq:音序器;

  • timer:定时器;

C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表playback。

3.1 应用层打开Control设备

当我们在应用成层打开Control设备,比如dev/snd/controlC0。对于字符设备,当我们进行open操作时,实际上会执行字符设备的文件操作集的open函数,也就是snd_ctl_f_ops中的open函数,即snd_ctl_open;

/*
 *  INIT PART
 */

static const struct file_operations snd_ctl_f_ops =
{
        .owner =        THIS_MODULE,
        .read =         snd_ctl_read,
        .open =         snd_ctl_open,
        .release =      snd_ctl_release,
        .llseek =       no_llseek,
        .poll =         snd_ctl_poll,
        .unlocked_ioctl =       snd_ctl_ioctl,
        .compat_ioctl = snd_ctl_ioctl_compat,
        .fasync =       snd_ctl_fasync,
};

这里我们大致看一下snd_ctl_open函数做了什么:

static int snd_ctl_open(struct inode *inode, struct file *file)
{
        unsigned long flags;
        struct snd_card *card;
        struct snd_ctl_file *ctl;
        int i, err;

        err = stream_open(inode, file);
        if (err < 0)
                return err;

        card = snd_lookup_minor_data(iminor(inode), SNDRV_DEVICE_TYPE_CONTROL); // 获取声卡设备
        if (!card) {
                err = -ENODEV;
                goto __error1;
        }
        err = snd_card_file_add(card, file); // 添加file到card文件列表files_list
        if (err < 0) {
                err = -ENODEV;
                goto __error1;
        }
        if (!try_module_get(card->module)) {
                err = -EFAULT;
                goto __error2;
        }
        ctl = kzalloc(sizeof(*ctl), GFP_KERNEL);   // 动态申请内存,数据结构类型为struct snd_ctl_file
        if (ctl == NULL) {
                err = -ENOMEM;
                goto __error;
        }
        INIT_LIST_HEAD(&ctl->events);             // 初始化链表节点
        init_waitqueue_head(&ctl->change_sleep);  // 初始化等待队列头
        spin_lock_init(&ctl->read_lock);          // 初始化自旋锁  
        ctl->card = card;                         // 设置声卡设备     
        for (i = 0; i < SND_CTL_SUBDEV_ITEMS; i++)
                ctl->preferred_subdevice[i] = -1;
        ctl->pid = get_pid(task_pid(current));    // 获取进程id
        file->private_data = ctl;
        write_lock_irqsave(&card->ctl_files_rwlock, flags);
        list_add_tail(&ctl->list, &card->ctl_files);   // 将ctl->list链表节点添加到card->ctl_files链表
        write_unlock_irqrestore(&card->ctl_files_rwlock, flags);
        snd_card_unref(card);
        return 0;

      __error:
        module_put(card->module);
      __error2:
        snd_card_file_remove(card, file);
      __error1:
        if (card)
                snd_card_unref(card);
        return err;
}

参考文章

[1] RK3399 探索之旅 / Audio 驱动层速读

[2] Linux ALSA音频驱动之一:框架概述

[3] Linux音频子系统

[4] http://www.alsa-project.org/

[5] Linux ALSA驱动之二:声卡的创建流程

[6] Linux内核中的链表——struct list_head

[7] Linux ALSA 音频系统:物理链路篇

[8] Sound Subsystem Documentation

[9] ALSA声卡驱动中的DAPM详解之一:kcontrol

posted @ 2023-06-17 21:04  大奥特曼打小怪兽  阅读(805)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步