【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” */