Rockchip RK3399 - ASoC Codec驱动基础
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
在Rockchip RK3399 - ALSA子系统我们介绍了ALSA子系统的软件架构,同时介绍了ALSA CORE核心数据结构和相关API。本节我们将会介绍ASoC软件体系中音频三大驱动模块(codec、platform 和machine)中的codec。
Codec driver它不应包含任何特定于目标平台或设备的代码。所有特定于平台和设备的代码应分别添加到平台和机器驱动程序中,codec driver提供了配置编解码器、FM、MODEM、BT或外部DSP,以提供音频捕获和播放功能。
每个codec driver必须提供以下功能:
- codec dai和pcm的配置信息;通过struct snd_soc_dai_driver描述,包括dai的能力描述和操作接口;
- codec的控制接口:其控制接口一般是I2C或SPI。控制接口用于读写codec的寄存器。在struct snd_soc_component_driver结构体中,有大量字段描述codec的控制接口,比如read、write等;
- Mixer和其它音频控件;
- codec的音频操作:通过结构体struct snd_soc_dai_ops描述;
- DAPM描述信息;
- DAPM事件处理程序;
可选地,codec driver还可以提供:
- DAC数字静音控制;
一、核心数据结构
描述codec的最主要的几个数据结构分别是:snd_soc_dai,snd_soc_dai_driver、snd_soc_component、snd_soc_component_driver,其中:
- snd_soc_dai:描述codec端的dai;
- snd_soc_dai_driver:与snd_soc_dai对应的驱动数据结构就是snd_soc_dai_driver;
- snd_soc_component:ASoC使用同一的数据结构来描述codec设备和platform设备,该数据结构就是snd_soc_component;也就是说,不管是platform、codec,核心层现在都统一用struct snd_soc_component来管理了, 一个component对应一个模块;
- snd_soc_component_driver:与snd_soc_component对应的驱动数据结构就是snd_soc_component_driver;
每个codec driver都必须有一个struct snd_soc_dai_driver结构体,用于定义其dai和pcm的能力和操作。该结构体被导出,以便machine driver可以将其注册到ASoC CORE中。
关于snd_soc_dai,snd_soc_dai_driver数据结构具体参考:Rockchip RK3399 - ASoC Machine驱动基础 dai相关内容。
1.1 struct snd_soc_component
ASoC使用snd_soc_component来描述codec设备,定义在include/sound/soc-component.h:
struct snd_soc_component { const char *name; int id; const char *name_prefix; struct device *dev; struct snd_soc_card *card; unsigned int active; unsigned int suspended:1; /* is in suspend PM state */ struct list_head list; struct list_head card_aux_list; /* for auxiliary bound components */ struct list_head card_list; const struct snd_soc_component_driver *driver; struct list_head dai_list; int num_dai; struct regmap *regmap; int val_bytes; struct mutex io_mutex; /* attached dynamic objects */ struct list_head dobj_list; /* * DO NOT use any of the fields below in drivers, they are temporary and * are going to be removed again soon. If you use them in driver code * the driver will be marked as BROKEN when these fields are removed. */ /* Don't use these, use snd_soc_component_get_dapm() */ struct snd_soc_dapm_context dapm; /* machine specific init */ int (*init)(struct snd_soc_component *component); /* function mark */ void *mark_module; struct snd_pcm_substream *mark_open; struct snd_pcm_substream *mark_hw_params; struct snd_pcm_substream *mark_trigger; struct snd_compr_stream *mark_compr_open; void *mark_pm; struct dentry *debugfs_root; const char *debugfs_prefix; };
其中:
- name:名称,音频数据链路可以通过这个字段到全局链表component_list中查找与之匹配的component;
- dapm:dapm域;
- dev:一般为codec设备对应的struct device;
- regmap:regmap实例,regmap一种通用的接口来操作底层硬件寄存器;
- name_prefix:名称前缀,如果使用设备树的话,取自dev设备节点的属性"sound-name-prefix";
- card:ASoC声卡设备;
- dai_list:保存dai的链表;链表中的数据元素类型为struct snd_soc_dai;
- num_dai:dai_list链表的长度;
- driver:component driver;
- list:用于构建链表节点,将当前节点添加到全局链表component_list;
- card_list:用于构建链表节点,将当前节点添加到声卡的component_dev_list链表;
1.2 struct snd_soc_component_driver
ASoC使用snd_soc_component_driver来描述codec driver,定义在include/sound/soc-component.h:
struct snd_soc_component_driver { const char *name; /* Default control and setup, added after probe() is run */ const struct snd_kcontrol_new *controls; unsigned int num_controls; const struct snd_soc_dapm_widget *dapm_widgets; unsigned int num_dapm_widgets; const struct snd_soc_dapm_route *dapm_routes; unsigned int num_dapm_routes; int (*probe)(struct snd_soc_component *component); void (*remove)(struct snd_soc_component *component); int (*suspend)(struct snd_soc_component *component); int (*resume)(struct snd_soc_component *component); unsigned int (*read)(struct snd_soc_component *component, unsigned int reg); int (*write)(struct snd_soc_component *component, unsigned int reg, unsigned int val); /* pcm creation and destruction */ int (*pcm_construct)(struct snd_soc_component *component, struct snd_soc_pcm_runtime *rtd); void (*pcm_destruct)(struct snd_soc_component *component, struct snd_pcm *pcm); /* component wide operations */ int (*set_sysclk)(struct snd_soc_component *component, int clk_id, int source, unsigned int freq, int dir); int (*set_pll)(struct snd_soc_component *component, int pll_id, int source, unsigned int freq_in, unsigned int freq_out); int (*set_jack)(struct snd_soc_component *component, struct snd_soc_jack *jack, void *data); int (*get_jack_type)(struct snd_soc_component *component); /* DT */ int (*of_xlate_dai_name)(struct snd_soc_component *component, const struct of_phandle_args *args, const char **dai_name); int (*of_xlate_dai_id)(struct snd_soc_component *comment, struct device_node *endpoint); void (*seq_notifier)(struct snd_soc_component *component, enum snd_soc_dapm_type type, int subseq); int (*stream_event)(struct snd_soc_component *component, int event); int (*set_bias_level)(struct snd_soc_component *component, enum snd_soc_bias_level level); int (*open)(struct snd_soc_component *component, struct snd_pcm_substream *substream); int (*close)(struct snd_soc_component *component, struct snd_pcm_substream *substream); int (*ioctl)(struct snd_soc_component *component, struct snd_pcm_substream *substream, unsigned int cmd, void *arg); int (*hw_params)(struct snd_soc_component *component, struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params); int (*hw_free)(struct snd_soc_component *component, struct snd_pcm_substream *substream); int (*prepare)(struct snd_soc_component *component, struct snd_pcm_substream *substream); int (*trigger)(struct snd_soc_component *component, struct snd_pcm_substream *substream, int cmd); int (*sync_stop)(struct snd_soc_component *component, struct snd_pcm_substream *substream); snd_pcm_uframes_t (*pointer)(struct snd_soc_component *component, struct snd_pcm_substream *substream); int (*get_time_info)(struct snd_soc_component *component, struct snd_pcm_substream *substream, struct timespec64 *system_ts, struct timespec64 *audio_ts, struct snd_pcm_audio_tstamp_config *audio_tstamp_config, struct snd_pcm_audio_tstamp_report *audio_tstamp_report); int (*copy_user)(struct snd_soc_component *component, struct snd_pcm_substream *substream, int channel, unsigned long pos, void __user *buf, unsigned long bytes); struct page *(*page)(struct snd_soc_component *component, struct snd_pcm_substream *substream, unsigned long offset); int (*mmap)(struct snd_soc_component *component, struct snd_pcm_substream *substream, struct vm_area_struct *vma); int (*ack)(struct snd_soc_component *component, struct snd_pcm_substream *substream); snd_pcm_sframes_t (*delay)(struct snd_soc_component *component, struct snd_pcm_substream *substream); const struct snd_compress_ops *compress_ops; /* probe ordering - for components with runtime dependencies */ int probe_order; int remove_order; /* * signal if the module handling the component should not be removed * if a pcm is open. Setting this would prevent the module * refcount being incremented in probe() but allow it be incremented * when a pcm is opened and decremented when it is closed. */ unsigned int module_get_upon_open:1; /* bits */ unsigned int idle_bias_on:1; unsigned int suspend_bias_off:1; unsigned int use_pmdown_time:1; /* care pmdown_time at stop */ /* * Indicates that the component does not care about the endianness of * PCM audio data and the core will ensure that both LE and BE variants * of each used format are present. Typically this is because the * component sits behind a bus that abstracts away the endian of the * original data, ie. one for which the transmission endian is defined * (I2S/SLIMbus/SoundWire), or the concept of endian doesn't exist (PDM, * analogue). */ unsigned int endianness:1; unsigned int legacy_dai_naming:1; /* this component uses topology and ignore machine driver FEs */ const char *ignore_machine; const char *topology_name_prefix; int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd, struct snd_pcm_hw_params *params); bool use_dai_pcm_id; /* use DAI link PCM ID as PCM device number */ int be_pcm_base; /* base device ID for all BE PCMs */ unsigned int start_dma_last; #ifdef CONFIG_DEBUG_FS const char *debugfs_prefix; #endif };
其中:
- controls:指向动态分配得到的数组,每个元素都是一个struct snd_kcontrol;可以用来保存component的kcontrol;
- num_controls:controls指向的数组的长度;
- dapm_widgets:指向动态分配得到的数组,每个元素都是一个struct snd_soc_dapm_widget ;
- num_dapm_widgets:dapm_widgets指向的数组的长度;
- dapm_routes:指向动态分配得到的数组,每个元素都是一个struct snd_soc_dapm_route;
- num_dapm_routes:dapm_routes指向的数组的长度;
- probe:可选的conmponent探测回调函数;注册声卡时回调;
- remove:可选的conmponent卸载回调函数;
- probe_order:probe函数执行顺序;
- remove_order:remove函数执行顺序;
- read:用于读取寄存器的值,如果使用了regamp进行寄存器读写就不用配置该函数了;
- write:用于向寄存器写入值,如果使用了regamp进行寄存器读写就不用配置该函数了;
1.3 数据结构关系图
为了更加形象的表示snd_soc_dai,snd_soc_dai_driver、snd_soc_component、snd_soc_component_driver之间的关系,我们绘制了如下关系框图:
二、注册component
devm_snd_soc_register_component是带有资源管理的component注册函数,该函数会动态申请一个component,并将其添加到全局链表component_list中,同时会为每个dai driver动态分配一个dai,并建立dai与component、dai与dai driver的关系。
在Machine驱动中匹配codec,实际上就是根据音频数据链路snd_soc_dai_link codec指定的name去遍历component_list找到匹配的component, 然后再根据dai_name从component->dai_list中获取到匹配的codec dai。
函数定义在sound/soc/soc-devres.c:
/** * devm_snd_soc_register_component - resource managed component registration * @dev: Device used to manage component * @cmpnt_drv: Component driver * @dai_drv: DAI driver * @num_dai: Number of DAIs to register * * Register a component with automatic unregistration when the device is * unregistered. */ 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; // 动态分配内存,指向struct *snd_soc_component_driver,在设备释放时自动调用devm_component_release函数 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; }
此函数主要通过devres_alloc函数完成了devm_component_release的注册,并申请了资源,然后通过snd_soc_register_component完成了snd_soc_component_driver和snd_soc_dai_driver的注册。
snd_soc_register_component定义在sound/soc/soc-core.c,此处来看一下snd_soc_register_component函数,此函数中主要通过devm_kzalloc函数来完成了struct snd_soc_component数据结构的申请,然后通过snd_soc_component_initialize函数来完成component的初始化,最后通过snd_soc_add_component函数将component添加到全局链表component_list中,具体代码实现如下:
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 snd_soc_component *component; int ret; component = devm_kzalloc(dev, sizeof(*component), GFP_KERNEL); // 动态分配struct snd_soc_component if (!component) return -ENOMEM; ret = snd_soc_component_initialize(component, component_driver, dev); // 初始化成员name、dev、driver if (ret < 0) return ret; return snd_soc_add_component(component, dai_drv, num_dai); // 完成snd_soc_dai_driver的注册,并将component来添加到全局链表component_list中 }
2.1 snd_soc_component_initialize
再来看一下snd_soc_component_initialize函数,此函数通过INIT_LIST_HEAD这宏完成了四个list的初始化,有component->dai_list、component->dobj_list、component->card_list和component->list,然后通过fmt_single_name函数来完成component->id的component组件的匹配工作,函数定义在sound/soc/soc-core.c:
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); // 设置为dev设备的名称,如果设备名称为%s.%d格式,将解析最后的数字存放到component->id中 if (!component->name) { dev_err(dev, "ASoC: Failed to allocate name\n"); return -ENOMEM; } component->dev = dev; // 设置dev component->driver = driver; // 设置driver #ifdef CONFIG_DEBUG_FS if (!component->debugfs_prefix) component->debugfs_prefix = driver->debugfs_prefix; #endif return 0; }
这里面fmt_single_name函数就是确定codec的名字,这个名字很重要,在没有使用设备树的情景下,在Machine驱动中音频数据链路可以通过这个字段到全局链表component_list中查找与之匹配的component;
/* * Simplify DAI link configuration by removing ".-1" from device names * and sanitizing names. */ static char *fmt_single_name(struct device *dev, int *id) // 以rt5651设备为例 { const char *devname = dev_name(dev); // 获取设备名称:1-001a char *found, *name; unsigned int id1, id2; if (devname == NULL) return NULL; name = devm_kstrdup(dev, devname, GFP_KERNEL); // dump if (!name) return NULL; /* are we a "%s.%d" name (platform and SPI components) */ found = strstr(name, dev->driver->name); // 查找dev->driver->name("rt5651")在name第一次出现的地址 if (found) { // 未找到 /* get ID */ if (sscanf(&found[strlen(dev->driver->name)], ".%d", id) == 1) { // 解析出id /* discard ID from name if ID == -1 */ if (*id == -1) found[strlen(dev->driver->name)] = '\0'; } /* I2C component devices are named "bus-addr" */ } else if (sscanf(name, "%x-%x", &id1, &id2) == 2) { // id1="1" id2="001a" /* 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); // name设置为rt5651.1-001a } else { *id = 0; } return name; }
比如我们在设备树中配置的设备节点rt5651,该设备节点会被转换为platform_device,platform_device->dev的名称为1-001a;经过fmt_single_name函数处理返回"rt5651.1-001a";
&i2c1 { status = "okay"; i2c-scl-rising-time-ns = <300>; i2c-scl-falling-time-ns = <15>; rt5651: rt5651@1a { #sound-dai-cells = <0>; compatible = "rockchip,rt5651"; reg = <0x1a>; clocks = <&cru SCLK_I2S_8CH_OUT>; clock-names = "mclk"; status = "okay"; }; };
2.2 snd_soc_add_component
snd_soc_add_component函数主要流程如下:
- 创建num_dai个dai,并为每个dai设置dai driver;
- 然后将dai加入到component的dai_list链表中;
- 初始化component成员regmap,用于对I2C/SPI设备底层硬件寄存器进行读写;
- 最后再将component添加到全局链表component_list中;
函数定义在sound/soc/soc-core.c:
int snd_soc_add_component(struct snd_soc_component *component, struct snd_soc_dai_driver *dai_drv, int num_dai) { int ret; int i; mutex_lock(&client_mutex); if (component->driver->endianness) { for (i = 0; i < num_dai; i++) { convert_endianness_formats(&dai_drv[i].playback); convert_endianness_formats(&dai_drv[i].capture); } } ret = snd_soc_register_dais(component, dai_drv, num_dai); // 注册dai driver列表(内部会创建num_dai个dai,每个dai对应一个dai driver) if (ret < 0) { dev_err(component->dev, "ASoC: Failed to register DAIs: %d\n", ret); goto err_cleanup; } if (!component->driver->write && !component->driver->read) { // 如果没有定义寄存器读、写函数 if (!component->regmap) // 为空 component->regmap = dev_get_regmap(component->dev, // 获取设备的regmap,并赋值给component成员regmap NULL); if (component->regmap) // 走这里 snd_soc_component_setup_regmap(component); // 从regmap中获取寄存器值位宽,赋值给component->val_bytes } /* see for_each_component */ list_add(&component->list, &component_list); // 将当前component添加到全局链表component_list err_cleanup: if (ret < 0) snd_soc_del_component_unlocked(component); mutex_unlock(&client_mutex); if (ret == 0) snd_soc_try_rebind_card(); return ret; }
2.2.1 snd_soc_register_dais
snd_soc_register_dais函数定义在sound/soc/soc-core.c,内部循环调用snd_soc_register_dai依次注册每个dai driver;
/** * snd_soc_register_dais - Register a DAI with the ASoC core * * @component: The component the DAIs are registered for * @dai_drv: DAI driver to use for the DAIs * @count: Number of DAIs */ static int snd_soc_register_dais(struct snd_soc_component *component, struct snd_soc_dai_driver *dai_drv, // dai driver size_t count) // dai driver数量 { struct snd_soc_dai *dai; unsigned int i; int ret; for (i = 0; i < count; i++) { dai = snd_soc_register_dai(component, dai_drv + i, count == 1 && // 注册每个dai driver component->driver->legacy_dai_naming); if (dai == NULL) { ret = -ENOMEM; goto err; } } return 0; err: snd_soc_unregister_dais(component); return ret; }
2.2.2 snd_soc_register_dai
snd_soc_register_dai函数如下:
/** * snd_soc_register_dai - Register a DAI dynamically & create its widgets * * @component: The component the DAIs are registered for * @dai_drv: DAI driver to use for the DAI * @legacy_dai_naming: if %true, use legacy single-name format; * if %false, use multiple-name format; * * Topology can use this API to register DAIs when probing a component. * These DAIs's widgets will be freed in the card cleanup and the DAIs * will be freed in the component cleanup. */ struct snd_soc_dai *snd_soc_register_dai(struct snd_soc_component *component, struct snd_soc_dai_driver *dai_drv, bool legacy_dai_naming) { struct device *dev = component->dev; struct snd_soc_dai *dai; dev_dbg(dev, "ASoC: dynamically register DAI %s\n", dev_name(dev)); lockdep_assert_held(&client_mutex); dai = devm_kzalloc(dev, sizeof(*dai), GFP_KERNEL); // 动态分配分配struct snd_soc_dai if (dai == NULL) return NULL; /* * Back in the old days when we still had component-less DAIs, * instead of having a static name, component-less DAIs would * inherit the name of the parent device so it is possible to * register multiple instances of the DAI. We still need to keep * the same naming style even though those DAIs are not * component-less anymore. */ if (legacy_dai_naming && // 设置dai名称 (dai_drv->id == 0 || dai_drv->name == NULL)) { dai->name = fmt_single_name(dev, &dai->id); // 设置为dev设备的名称 } else { dai->name = fmt_multiple_name(dev, dai_drv); // 设置为dai_drv->name,即dai driver的名称 if (dai_drv->id) dai->id = dai_drv->id; else dai->id = component->num_dai; } if (!dai->name) return NULL; dai->component = component; // 初始化dai成员 dai->dev = dev; dai->driver = dai_drv; /* see for_each_component_dais */ list_add_tail(&dai->list, &component->dai_list); // 将当前dai添加到component的dai_list链表中 component->num_dai++; // 计数 dev_dbg(dev, "ASoC: Registered DAI '%s'\n", dai->name); return dai; }
具体流程如下:
- 首先分配struct snd_soc_dai;
- 设置dai->name参数;在machine驱动注册过程中,将会用snd_soc_dai_link中codecs的dai_name和这里的name做匹配;
- 初始化dai成员component、dev、driver;
- 将dai加入到component的dai_list链表中;
参考文章
[2] Linux ALSA声卡驱动之七:ASoC架构中的Codec