ASOC全解析(二)codec驱动解析与实践

【ASOC全解析(二)】codec驱动解析与实践

  • 一、Codec的作用
  • 二、Codec驱动程序内容
  • 三、从零写一个虚拟音频外设驱动程序
    • 举例如何写结构体snd_soc_component_driver与结构体snd_soc_dai_driver
      • 结构体snd_soc_component_driver
      • 结构体snd_soc_dai_driver
      • 结构体snd_soc_component_driver与结构体snd_soc_dai_driver的区别
  • 四、完整的codec驱动代码示例

/*****************************************************************************************************************/

声明: 本博客内容均由https://blog.csdn.net/weixin_47702410原创,转载or引用请注明出处,谢谢!

创作不易,如果文章对你有帮助,麻烦点赞 收藏支持~感谢

/*****************************************************************************************************************/

一、Codec的作用

本文的codec指的是ASOC架构下的一部分,它是ASOC三大组成部分的codec驱动程序,主要是各种音频编解码IC的驱动程序。

ASOC之所以提出codec的目的主要是为了解决编解码器驱动程序与底层 SoC CPU 紧密耦合的问题。在提出这个框架后,编解码驱动程序(codec驱动)便可与具体的平台分离开,成为独立的一部分。这十分有利于开发产品的厂商去整合音频IC与主控IC,他们只需要在machine端将两者连接起来便可将音频IC整合到主控IC的平台上使用了!

二、Codec驱动程序内容

Codec驱动程序可以包含下面七种内容:

  • Codec DAI和PCM配置:必须能够配置codec的数字音频接口(DAI)和PCM(脉冲编码调制)音频数据的参数。每个编解码器驱动程序必须有一个 struct snd_soc_dai_driver 来定义其 DAI 和 PCM 功能和操作

  • Codec控制IO:使用RegMap API进行寄存器访问。RegMap API提供了一种简化的方式来读写codec的寄存器。

  • 混音器和音频控制:提供音频路径的混合和音量控制等功能。

  • Codec音频操作:实现codec的基本音频操作,如初始化、启动、停止等。

  • DAPM描述:定义Dynamic Audio Power Management(动态音频功率管理)的小部件、路径和控制点,以优化功率消耗。

  • DAPM事件处理器:处理DAPM系统中的事件,如音频流的启动和停止,以及功率状态的变化。

  • DAC数字静音控制:如果需要,可以提供数字到模拟转换器(DAC)的静音控制功能。

本文分析Codec DAI和PCM配置、Codec音频操作的一些API函数是如何构成的。关于这两个API函数,一般使用如下的函数进行注册:

int snd_soc_register_component(struct device *dev,
            const struct snd_soc_component_driver *component_driver,
            struct snd_soc_dai_driver *dai_drv,
            int num_dai)

有些codec驱动程序可能用的是devm_snd_soc_register_component函数去调用,但是其实devm_snd_soc_register_component函数也是调用snd_soc_register_component函数去完成的相关操作。devm_snd_soc_register_component函数源码如下所示:

//file path:/sound/soc/soc-devres.c
int devm_snd_soc_register_component(struct device *dev,
             const struct snd_soc_component_driver *cmpnt_drv,
             struct snd_soc_dai_driver *dai_drv, int num_dai)
{
    const struct snd_soc_component_driver **ptr;
    int ret;

    ptr = devres_alloc(devm_component_release, sizeof(*ptr), GFP_KERNEL);
    if (!ptr)
        return -ENOMEM;

    ret = snd_soc_register_component(dev, cmpnt_drv, dai_drv, num_dai);
    if (ret == 0) {
        *ptr = cmpnt_drv;
        devres_add(dev, ptr);
    } else {
        devres_free(ptr);
    }

    return ret;
}

三、从零写一个虚拟音频外设驱动程序

首先分析一下注册声卡驱动所用的API,了解注册声卡需要提供哪些函数或者结构体:

注册声卡的函数原型如下:

int snd_soc_register_component(struct device *dev,
            const struct snd_soc_component_driver *component_driver,
            struct snd_soc_dai_driver *dai_drv,
            int num_dai)

这里解释一下这个函数的参数的含义:

  • struct device *dev: 这个参数是一个指向device结构体的指针,代表了要注册的音频组件的设备。

  • const struct snd_soc_component_driver *component_driver: 这个参数是一个指向snd_soc_component_driver结构体的指针,它定义了组件驱动的操作和行为。这个结构体通常包含了一系列的回调函数,比如初始化(init)、读写寄存器(read/write)、挂起(suspend)和恢复(resume)等,以及可能包含的组件特有的控制元素和调试信息。

  • struct snd_soc_dai_driver *dai_drv: 这个参数是一个指向snd_soc_dai_driver结构体的指针,它代表了数字音频接口(DAI)的驱动程序。DAI是SoC音频组件的一部分,负责处理数字音频流。dai_drv结构体包含了DAI的配置信息和操作函数,如启动(startup)、停止(shutdown)、设置格式(set_fmt)等。

  • int num_dai: 这个参数表示数字音频接口的数量。

结构体struct snd_soc_component_driver的原型可以见源码:/include/sound/soc-component.h

结构体struct snd_soc_dai_driver的原型可以见源码:/include/sound/soc-dai.h

举例如何写结构体snd_soc_component_driver与结构体snd_soc_dai_driver

结构体snd_soc_component_driver

结构体的内容比较多,但是一般情况下,我们只需要完成probe、remove函数,外加controls和DAPM相关定义便可。如下是一个例子:

static const struct snd_soc_component_driver my_soc_component_driver = {
    .name = my_codec_name,
    .probe = my_codec_probe,            //probe和remove
    .remove = my_codec_remove,
    .controls = my_snd_controls,       //controls相关
    .num_controls = ARRAY_SIZE(my_snd_controls),
    .dapm_widgets = my_dapm_widgets,   //DAPM相关
    .num_dapm_widgets = ARRAY_SIZE(my_dapm_widgets),
    .dapm_routes = my_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(my_dapm_routes),
};

注意

  • .name赋值时的右值需要提供字符串,一般情况下我们都使用宏定义,然后将宏写入ops当中
  • .probe & .remove赋值时的右值应该是函数的名字,需要事先定义好函数
  • .controls & .dapm_widgets & .dapm_routes应该对应到不同结构体的数组
  • .num_controls & .num_dapm_widgets & .num_dapm_routes是.controls & .dapm_widgets & .dapm_routes结构体数组的数量,在赋值时右值是unsigned int类型的变量或者数字,一般用ARRAY_SIZE计算出其具体的数值。

结构体snd_soc_dai_driver

这个结构体是对应到DAI相关操作的结构体,主要是涉及到数据流这块的,一般定义如下:

static const struct snd_soc_dai_ops my_dai_ops = {
    .startup        = my_startup,
    .hw_params      = my_hw_params,
    .prepare        = my_prepare,
    .trigger        = my_trigger,
    .shutdown       = my_shutdown,
};

static struct snd_soc_dai_driver my_dai_driver[] = {
    {
        .id = my_ID,
        .name = "my-snd-codec",
        .playback = {
            .stream_name = "MY Playback",
            .channels_min = 1,
            .channels_max = 2,
            .rates = SNDRV_PCM_RATE_8000_48000 |
                 SNDRV_PCM_RATE_96000 |
                 SNDRV_PCM_RATE_192000,
            .formats = my_FORMATS,
        },
        .capture = {
            .stream_name = "MY Capture",
            .channels_min = 1,
            .channels_max = 2,
            .rates = SNDRV_PCM_RATE_8000 |
                 SNDRV_PCM_RATE_16000 |
                 SNDRV_PCM_RATE_32000 |
                 SNDRV_PCM_RATE_48000 |
                 SNDRV_PCM_RATE_96000 |
                 SNDRV_PCM_RATE_192000,
            .formats = my_FORMATS,
        },
        .ops = &my_codec_dai_ops,
    },
    //...
};

上面是一个示例,值得注意的是snd_soc_dai_ops中可定义的不止上面这些ops,也可能包含更多的ops或者更少的ops,要看codec厂商如何设计使用IC。

结构体snd_soc_component_driver与结构体snd_soc_dai_driver的区别

在ASoC (ALSA System on Chip) 框架中,snd_soc_component_driver 和 snd_soc_dai_driver 是两个核心的结构体,它们分别代表了codec组件的不同方面。

snd_soc_component_driver: 这个结构体代表了codec组件的控制部分。它包含了一系列的回调函数和操作,用于管理codec的音频控制、DAPM (Dynamic Audio Power Management) 配置和其他codec特定的功能。例如,它可以包含音量控制、静音开关、EQ设置等的回调函数。此外,它还可以包含用于初始化和关闭codec的回调函数。

  • snd_soc_component_driver 结构体的定义可能包含以下字段(不是完整列表):

    .probe 和 .remove:当codec设备被绑定或解绑时调用的函数。

    .controls、.dapm_widgets 和 .dapm_routes:定义codec的控制元素、DAPM小部件和音频路径。

    .idle_bias_off:指示codec在空闲时是否关闭偏置电压。

    .suspend 和 .resume:在系统挂起和恢复时调用的函数。

    snd_soc_dai_driver: 这个结构体代表了codec的DAI (Digital Audio Interface) 部分,它定义了codec与其他音频组件(如处理器或其他数字音频设备)之间的接口。这包括支持的音频格式、时钟配置、数据传输模式等。

  • snd_soc_dai_driver 结构体的定义可能包含以下字段(不是完整列表):

    .playback 和 .capture:定义了播放和录音的能力,如支持的格式、速率、通道数等。

    .ops:包含了操作DAI的回调函数,如启动、停止、设置格式等。

    .symmetric_rates:指示是否使用对称的采样率进行播放和录音。

这两个结构体之间的主要区别在于它们的职责范围。snd_soc_component_driver 更关注于codec的控制和管理,而 snd_soc_dai_driver 更关注于定义codec的数字音频接口。

当你注册一个codec时,你需要提供这两个结构体,因为ASoC框架需要知道如何控制codec(通过snd_soc_component_driver)以及如何通过DAI与codec进行数字音频通信(通过snd_soc_dai_driver)。这样,ASoC框架就可以正确地将codec集成到整个音频系统中,并确保音频数据可以正确地在系统的不同部分之间传输。

四、完整的codec驱动代码示例

如下所示,提供一份完整的codec驱动代码的基本框架:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/soc.h>

static int my_codec_probe(struct snd_soc_component *codec)
{
    //int ret;
    printk("-----%s----
",__func__);

    return 0;
}

static void my_codec_remove(struct snd_soc_component *codec)
{
    printk("-%s,line:%d
",__func__,__LINE__);
}

static struct snd_soc_component_driver soc_my_codec_drv = {
    .probe = my_codec_probe,
    .remove = my_codec_remove,
};

static int my_codec_startup(struct snd_pcm_substream *substream,
                struct snd_soc_dai *dai) {
    printk("-%s,line:%d
",__func__,__LINE__);

    return 0;
}

static int my_codec_hw_params(struct snd_pcm_substream *substream,
                struct snd_pcm_hw_params *params,
                struct snd_soc_dai *dai)
{
    printk("-%s,line:%d
",__func__,__LINE__);

    return 0;
}

static void my_codec_shutdown(struct snd_pcm_substream *substream,
                struct snd_soc_dai *dai) {
    printk("-%s,line:%d
",__func__,__LINE__);  
        
}

static int my_codec_trigger(struct snd_pcm_substream *substream,
                int cmd, struct snd_soc_dai *dai)
{

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
            printk("-%s: playback start
",__func__);
        } else {
            printk("-%s: catpure start
",__func__);
        }
        break;
    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
            printk("-%s:playback stop
",__func__);
        } else {
            printk("-%s:catpure stop
",__func__);
        }

        break;
    default:
        return -EINVAL;
    }
    return 0;
}

static int my_codec_prepare(struct snd_pcm_substream *substream,
                struct snd_soc_dai *dai) {
    printk("-%s,line:%d
",__func__,__LINE__);
    return 0;
}

static const struct snd_soc_dai_ops my_codec_dai_ops = {
    .startup        = my_codec_startup,
    .hw_params      = my_codec_hw_params,
    .prepare        = my_codec_prepare,
    .trigger        = my_codec_trigger,
    .shutdown       = my_codec_shutdown,
};

static struct snd_soc_dai_driver my_codec_dai[] = {
    {
        .name   = "my_codec_dai",
        .playback = {
            .stream_name = "Playback",
            .channels_min = 1,
            .channels_max = 2,
            .rates  = SNDRV_PCM_RATE_8000_192000
                | SNDRV_PCM_RATE_KNOT,
            .formats = SNDRV_PCM_FMTBIT_S16_LE
                | SNDRV_PCM_FMTBIT_S24_LE,
        },
        .capture = {
            .stream_name = "Capture",
            .channels_min = 1,
            .channels_max = 2,
            .rates = SNDRV_PCM_RATE_8000_48000
                | SNDRV_PCM_RATE_KNOT,
            .formats = SNDRV_PCM_FMTBIT_S16_LE
                | SNDRV_PCM_FMTBIT_S24_LE,
        },
        .ops = &my_codec_dai_ops,
    },
};

static int codec_probe(struct platform_device *pdev) {
    int ret = 0;
    
    printk("-%s,line:%d
",__func__,__LINE__);
    
    ret = snd_soc_register_component(&pdev->dev, &soc_my_codec_drv,
                my_codec_dai, ARRAY_SIZE(my_codec_dai));
    if (ret < 0) {
        dev_err(&pdev->dev, "register codec failed
");
        return -1;
    }
    
    return ret;
}

static int codec_remove(struct platform_device *pdev){
    printk("-%s,line:%d
",__func__,__LINE__);

    return 0;
}

static void codec_pdev_release(struct device *dev)
{
    printk("-%s,line:%d
",__func__,__LINE__);
}

static struct platform_device codec_pdev = {
    .name           = "my_codec",
    .dev.release    = codec_pdev_release,
};

static struct platform_driver codec_pdrv = {
    .probe      = codec_probe,
    .remove     = codec_remove,
    .driver     = {
        .name   = "my_codec",
    },
};

static int __init codec_init(void)
{
    int ret;

    ret = platform_device_register(&codec_pdev);
    if (ret)
        return ret;

    ret = platform_driver_register(&codec_pdrv);
    if (ret)
        platform_device_unregister(&codec_pdev);

    return ret;
}

static void __exit codec_exit(void)
{
    platform_driver_unregister(&codec_pdrv);
    platform_device_unregister(&codec_pdev);
}

module_init(codec_init);
module_exit(codec_exit);
MODULE_LICENSE("GPL");

在codec驱动中我们只需要提供两个信息给machine层:

    .codec_dai_name = "my_codec_dai",
    .codec_name = "my_codec.0",

细心的读者可能会发现“.codec_name”的右值不是"my_codec"而是“my_codec.0",这个原因是因为在snd_soc_register_component函数中的下面的语句决定的:

//snd_soc_register_component函数  调用 snd_soc_component_initialize函数
int snd_soc_component_initialize(struct snd_soc_component *component,
                 const struct snd_soc_component_driver *driver,
                 struct device *dev)
{
    INIT_LIST_HEAD(&component->dai_list);
    INIT_LIST_HEAD(&component->dobj_list);
    INIT_LIST_HEAD(&component->card_list);
    INIT_LIST_HEAD(&component->list);
    mutex_init(&component->io_mutex);

    component->name = fmt_single_name(dev, &component->id);
    if (!component->name) {
        dev_err(dev, "ASoC: Failed to allocate name
");
        return -ENOMEM;
    }

    component->dev      = dev;
    component->driver   = driver;

    return 0;
}

// codec_name其实就是component->name,是由fmt_single_name函数决定
static char *fmt_single_name(struct device *dev, int *id)
{
    const char *devname = dev_name(dev);
    char *found, *name;
    int id1, id2;

    if (devname == NULL)
        return NULL;

    name = devm_kstrdup(dev, devname, GFP_KERNEL);
    if (!name)
        return NULL;

    /* are we a "%s.%d" name (platform and SPI components) */
    found = strstr(name, dev->driver->name);
    if (found) {
        /* get ID */
        if (sscanf(&found[strlen(dev->driver->name)], ".%d", id) == 1) {

            /* discard ID from name if ID == -1 */
            if (*id == -1)
                found[strlen(dev->driver->name)] = '';
        }

    /* I2C component devices are named "bus-addr" */
    } else if (sscanf(name, "%x-%x", &id1, &id2) == 2) {

        /* create unique ID number from I2C addr and bus */
        *id = ((id1 & 0xffff) << 16) + id2;

        devm_kfree(dev, name);

        /* sanitize component name for DAI link creation */
        name = devm_kasprintf(dev, GFP_KERNEL, "%s.%s", dev->driver->name, devname);
    } else {
        *id = 0;
    }

    return name;
}
 
/*   
fmt_single_name函数分析:
1、先获取设备的名称:const char *devname = dev_name(dev);  我们的设备名称应该是"my_codec"
2、解析设备ID,会有两种情况,一种是非I2C设备,一种是I2C设备。
3、如果是非I2C则codec_name名字是"%s.%d"(%d为ID);如果是I2C设备,则名字是"%s.bus-addr"(bus-addr为I2C的bus和addr)
4、我们在驱动注册的时候没有指定ID的数值是多少,这个ID应该默认为0.也就是我们fmt_single_name函数实际上返回的是“my_codec.0”
*/