audio Asoc 驱动架构详解
说明:文章可能有点乱,是平时调试记录的,但是比较详细。
Linux内核版本:4.19.172
Hardware Driver:音频硬件设备驱动,由三大部分组成,分别是 Machine、Platform、Codec。
ASoC音频驱动由三部分构成:platform,codec,machine。
machine 是包含 platform 与 codec 这两者之间通过i2s 连接控制,
platform (cpu_dai) <----->codec_dai
Platform:Platform驱动程序包括音频DMA引擎驱动程序,数字音频接口(DAI)驱动程序(例如I2S,AC97,PCM)以及该平台的任何音频DSP驱动程序。
cpu dai:在嵌入式系统里面通常指CPU的I2S、PCM总线控制器,负责将音频数据从I2S tx FIFO搬运到CODEC(回放的情形,录制则方向相反)。cpu_dai通过snd_soc_register_dai()来注册。注:DAI是Digital Audio Interface的缩写,分为cpu_dai和codec_dai,这两者通过I2S/PCM/pdm总线连接;
AIF是Audio Interface的缩写,一般分为I2S和PCM接口。
2 pcm dma:负责将dmabuffer中的音频数据搬运到I2S tx FIFO,这部分的逻辑比较复杂,以下几篇会对它详细阐述。音频dma驱动通过snd_soc_register_platform()来注册。值得留意的是:某些情形下是不需要dma操作的,比如Modem和CODEC直连,因为Modem本身已经把数据送到PCM FIFO了,这时只需启动codec_dai接收数据即可;该情形下,Machine驱动dai_link中需要指定.platform_name = “snd-soc-dummy”, 这是虚拟出来的platform驱动,实现见sound/soc/soc-utils.c。
Codec:Codec驱动程序独立于平台,包含音频控件,音频接口功能,编解码器DAPM定义和编解码器IO功能。如果需要,该类可扩展至BT,FM和MODEM IC。Codec类驱动程序应该是可以在任何体系结构和机器上运行的通用代码。
对于Playback来说,userspace送过来的PCM数据是经过抽样量化出来的数字信号,在codec经过DAC转换成模拟信号送到外放耳机输出,这样我们就可以听到声音了。Codec字面意思是编解码器,但芯片里面的功能部件很多,常见的有AIF、DAC、ADC、Mixer、PGA、Line-in、Line-out,有些高端的codec芯片还有EQ、DSP、SRC、DRC、AGC、Echo canceller、Noise suppression等部件。
Machine:Machine 驱动程序充当描述和绑定其他组件驱动程序以形成ALSA“声卡设备”的粘合剂。它可以处理任何机器特定的控制和机器级音频事件(例如,在播放开始时打开放大器)。
Machine 可以理解为对开发板的抽象,开发板可能包括多个声卡,对应Machine部分包含多个link。
Machine 指某一款机器,它把cpu_dai、codec_dai、modem_dai各个音频接口通过定义dai_link链结起来,然后注册snd_soc_card。和上面两个不一样,Platform和CODEC驱动一般是可以重用的,而Machine有它特定的硬件特性,几乎是不可重用的。所谓的硬件特性指:DAIs之间的链结;通过某个GPIO打开Amplifier;通过某个GPIO检测耳机插拔;使用某个时钟如MCLK/External OSC作为I2S、CODEC模块的基准时钟源等等。
dai_link:machine驱动中定义的音频数据链路,它指定用到的cpu_dai、codec_dai、platform、codec分别是什么。
PCM数据流
对于回放(Playback)的情形,PCM数据流向大致如下:
User Space---[copy_from_user()]--->DMA Buffer(kernel space)---DMA-->i2s TX FIFO--i2s-->CODEC--DAC->PGA/Mixer-->SPK/HP/Earp
对于Linux来说,由于分为 user space 和kernel space,而且两者之间不能随便互相访问。因此用户如果播放音频,则需要调用copy_from_user()将用户数据从user space拷贝到kernel space (DMA Buffer)。
DMA 负责将DMA Buffer中的音频数据搬运到I2S TX FIFO。
通过I2S总线,将音频数据传送到Codec。
Codec内部经过DAC转换,将模拟信号传到扬声器SPK(头戴式耳机HP、耳塞式耳机Earp)
对于录音(Capture)的情形,PCM数据流向是方向如下:
User Space<---[copy_from_user()]--DMA Buffer(kernel space)<---DMA--i2s RX FIFO<--i2s--CODEC<--ADC--MIC
platform驱动: platform驱动为cpu部分的控制代码,其抽象出两个结构体snd_soc_dai_driver和snd_soc_platform_driver;
codec驱动: codec驱动是编解码器部分的控制代码,其抽象出两个结构体分别为snd_soc_dai_driver和snd_soc_codec_driver;
machine驱动: machine驱动是控制管理platform和codec之间的连接匹配,管理控件(controls)、部件(widgets)以及routes,其抽象的结构体为snd_soc_card。
ASoC音频驱动注册流程:
注册PCM DMA 调用snd_register_platform(),注册struct snd_soc_platform_driver,配置pcm_new()、snd_pcm_ops.
|
注册CPU_dai 调用snd_soc_register_component(),注册struct snd_soc_dai_driver
|
注册 codec和codec_dai 调用snd_soc_register_codec(),注册struct snd_soc_dai_driver 和 struct snd_soc_codec_driver
|
注册声卡 调用snd_soc_register_card(),注册struct snd_soc_card. 初始化dai_link下的cpu_daipcm dma codeccodec_dai,穿创建pcm逻辑设备。
Platform驱动程序包括音频DMA引擎驱动程序(PCM DMA),数字音频接口(CPU DAI)驱动程序(例如I2S,AC97,PCM)以及该平台的任何音频DSP驱动程序。其中常用的是CPU DAI和PCM DMA驱动。
CPU DAI:在嵌入式系统里面通常指CPU的I2S、PCM总线控制器。对于playback,负责将音频数据从I2S TX FIFO搬运到CODEC(Capture则方向相反)。cpu_dai通过snd_soc_register_dai()来注册。
PCM DMA:对于playback,负责将dma buffer中的音频数据搬运到I2S TX FIFO(Capture则方向相反)。音频dma驱动通过snd_soc_register_platform()来注册。
CPU DAI
* Digital Audio Interface Driver.
*
* Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97
* operations and capabilities. Codec and platform drivers will register this
* structure for every DAI they have.
*
* This structure covers the clocking, formating and ALSA operations for each
* interface.
*/
struct snd_soc_dai_driver {
/* DAI description */
const char *name;
unsigned int id;
unsigned int base;
struct snd_soc_dobj dobj;
/* DAI driver callbacks */
int (*probe)(struct snd_soc_dai *dai);
int (*remove)(struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai);
int (*resume)(struct snd_soc_dai *dai);
/* compress dai */
int (*compress_new)(struct snd_soc_pcm_runtime *rtd, int num);
/* Optional Callback used at pcm creation*/
int (*pcm_new)(struct snd_soc_pcm_runtime *rtd,
struct snd_soc_dai *dai);
/* DAI is also used for the control bus */
bool bus_control;
/* ops */
const struct snd_soc_dai_ops *ops;
const struct snd_soc_cdai_ops *cops;
/* DAI capabilities */
struct snd_soc_pcm_stream capture; // 重要的数据结构 录音流
struct snd_soc_pcm_stream playback; // 播放流
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;
};
name:cpu_dai的名称标识,machine中的dai_link通过cpu_dai_name来匹配cpu_dai;
probe:cpu_dai的probe函数,由snd_soc_instantiate_card()回调;
playback:回放数据流性能描述信息,如所支持的声道数、采样率、音频格式;
capture:录制数据流性能描述信息,如所支持声道数、采样率、音频格式;
ops:指向cpu_dai的操作函数集,这些函数集非常重要,它定义了DAI的时钟配置、格式配置、数字静音、PCM音频接口、FIFO延迟报告等回调。
snd_soc_register_dai()
严格来说,注册cpu_dai是通过snd_soc_register_component()->snd_soc_register_dai()。
当注册多个cpu_dai时,通过snd_soc_register_component()->snd_soc_register_dais()
PCM DMA
2.2.1. 数据结构struct snd_soc_platform_driver
ASoC音频驱动中,用struct snd_soc_platform_driver结构体来描述 PCM DMA。
控件(controls)、部件(widgets)、路由(routes):
controls 为控件,其抽象的结构体为struct snd_kcontrol_new,可以为单独的一个控件,控制声卡某一功能,如声卡的播放声音,也可以用于连接两个部件(widget)形成一条通路(route),controls的注册函数snd_soc_add_codec_controls();
widgets 为部件,其抽象的结构体为struct snd_soc_dapm_widget,个人感觉部件就是声卡内部的节点的抽象,也是对某些controls做了一层封装,如录音输入引脚、输出引脚以及中间控制多路声音混合的混音器,widgets注册函数snd_soc_dapm_new_controls;
route 表示路由,其抽象的结构体为struct snd_soc_dapm_route。两个widget中间通过controls连接形成一条route(貌似controls可以为空),routes注册函数snd_soc_dapm_add_routes();
snd_pcm_ops
open:打开pcm逻辑设备时,会回调该函数,用于为runtime设定硬件约束,为runtime的private_data申请一个私有结构,保存dma资源如通道号、传输单元、缓冲区信息、IO设备信息等。
close:close函数,和open操作相反;
ioctl:这个可以不用自己写,使用内核的snd_pcm_lib_ioctl()函数;
hw_params:设置pcm硬件参数时(cmd:SNDRV_PCM_IOCTL_HW_PARAMS),会回调该函数,一般用于初始化dma资源,包括通道号、传输单元、缓冲区信息、IO设备信息等。
prepare:当数据已准备好时(cmd:SNDRV_PCM_IOCTL_PREPARE),会回调该函数告知dma数据已就绪。
trigger:pcm数据传送开始、停止、暂停、恢复时,会回调该函数启动或停止dma传输(补充:当上层第一次调用pcm_write()时,触发trigger启动dma传输;当上层调用pcm_stop()或pcm_drop()时,触发trigger停止dma传输)。trigger函数里面的操作必须是原子的,不能有可能引起睡眠的操作,并且应尽量简单。
pointer:该回调函数返回传输数据的当前位置。当dma每完成一次传输后,都会调用该函数获得传输数据的当前位置,这样pcm native可根据它来计算dma buffer指针位置及可用空间。该函数也是原子的。
注册PCM DMA:snd_soc_register_platform()
音频dma驱动通过snd_soc_register_platform()来注册。(和snd_soc_register_dai()函数非常相似)
DMA Buffer Allocation
2.2.1小节中数次提及dma buffer,即dma数据缓冲区,用于保存上层拷贝过来和麦克风录到的音频数据。
使用struct snd_dma_buffer结构体来描述dma buffer
struct snd_dma_buffer {
struct snd_dma_device dev; /* device type */
unsigned char *area; /* virtual pointer */
dma_addr_t addr; /* physical address */
size_t bytes; /* buffer size in bytes */
void *private_data; /* private for allocator; don't touch */
};
dma buffer的分配,一般发生在pcm_dma驱动(struct snd_soc_platform_driver)初始化阶段(probe)或pcm逻辑设备创建阶段(pcm_new)。probe()和pcm_new()回调函数
对于Playback来说,user space送过来的PCM数据是经过抽样量化出来的数字信号,在codec经过DAC转换成模拟信号送到外放耳机输出,这样我们就可以听到声音了。
Codec字面意思是编解码器,但芯片里面的功能部件很多,常见的有AIF、DAC、ADC、Mixer、PGA、Line-in、Line-out,有些高端的codec芯片还有EQ、DSP、SRC、DRC、AGC、Echo canceller、Noise suppression等部件。
本文重点关注codec_daiI驱动和codec驱动。
Codec指音频codec共有的部分,包括codec初始化函数、控制接口、寄存器缓存、控件列表、dapm部件列表、音频路由列表、偏置电压设置函数等描述信息.
Codec_dai指codec上的数字音频接口DAI驱动描述,各个接口的描述信息不一定都是一致的,所以各个音频接口都有它自身的驱动描述,包括音频接口的初始化函数、操作函数集、能力描述等。
注:DAPM,Dynamic Audio Power Management,动态音频电源管理,为移动Linux设备设计,使得音频系统任何时候都工作在最低功耗状态。
控件(controls)、部件(widgets)、路由(routes):
controls 为控件,其抽象的结构体为struct snd_kcontrol_new,可以为单独的一个控件,控制声卡某一功能,如声卡的播放声音,也可以用于连接两个部件(widget)形成一条通路(route),controls的注册函数snd_soc_add_codec_controls();
widgets 为部件,其抽象的结构体为struct snd_soc_dapm_widget,个人感觉部件就是声卡内部的节点的抽象,也是对某些controls做了一层封装,如录音输入引脚、输出引脚以及中间控制多路声音混合的混音器,widgets注册函数snd_soc_dapm_new_controls;
route 表示路由,其抽象的结构体为struct snd_soc_dapm_route。两个widget中间通过controls连接形成一条route(貌似controls可以为空),routes注册函数snd_soc_dapm_add_routes();
注册Codec
调用kzalloc()创建一个snd_soc_codec实例codec;
调用fmt_single_name()创建codec name和id;
初始化snd_soc_dai实例codec相关参数,包括读写相关的操作函数(实际上是由codec_drv来打理);还包括dev和driver指针的链接;
调用kmemdup()分配 CODEC register cache;
如果有需要,设定default volatile_register、readable_register、writable_register回调函数;
调用fixup_codec_formats()设置数据的格式(big and little endian);
调用list_add()将snd_soc_codec实例codec插入到codec_list链表,Machine驱动初始化时会遍历该链表,以找到dai_link声明的codec并绑定。
调用snd_soc_register_dais()注册codec_dai,下一小节我们详细分析该函数。
Codec DAI
2.2.1. 数据结构struct snd_soc_dai_driver
通过struct snd_soc_dai_driver结构体来定义一个codec_dai。struct snd_soc_dai_driver结构体如下:
*
* Digital Audio Interface Driver.
*
* Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97
* operations and capabilities. Codec and platform drivers will register this
* structure for every DAI they have.
*
* This structure covers the clocking, formating and ALSA operations for each
* interface.
*/
struct snd_soc_dai_driver {
/* DAI description */
const char *name;
unsigned int id;
unsigned int base;
struct snd_soc_dobj dobj;
/* DAI driver callbacks */
int (*probe)(struct snd_soc_dai *dai);
int (*remove)(struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai);
int (*resume)(struct snd_soc_dai *dai);
/* compress dai */
int (*compress_new)(struct snd_soc_pcm_runtime *rtd, int num);
/* Optional Callback used at pcm creation*/
int (*pcm_new)(struct snd_soc_pcm_runtime *rtd,
struct snd_soc_dai *dai);
/* DAI is also used for the control bus */
bool bus_control;
/* ops */
const struct snd_soc_dai_ops *ops;
const struct snd_soc_cdai_ops *cops;
/* DAI capabilities */
struct snd_soc_pcm_stream capture;
struct snd_soc_pcm_stream playback;
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;
};
snd_soc_dai_ops //name:codec_dai的名称标识,machine中的dai_link通过codec_dai_name来匹配codec_dai;
probe:codec_dai的probe函数,由snd_soc_instantiate_card()回调;
playback:回放数据流性能描述信息,如所支持的声道数、采样率、音频格式;
capture:录制数据流性能描述信息,如所支持声道数、采样率、音频格式;
ops:指向codec_dai的操作函数集,这些函数集非常重要,它定义了DAI的时钟配置、格式配置、数字静音、PCM音频接口、FIFO延迟报告等回调。struct snd_soc_dai_ops 结构体注释非常详细,这里不再赘述。
struct snd_soc_dai_ops {
/*
* DAI clocking configuration, all optional.
* Called by soc_card drivers, normally in their hw_params.
*/
int (*set_sysclk)(struct snd_soc_dai *dai,
int clk_id, unsigned int freq, int dir);
int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
unsigned int freq_in, unsigned int freq_out);
int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);
int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio);
/*
* DAI format configuration
* Called by soc_card drivers, normally in their hw_params.
*/
int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);
int (*xlate_tdm_slot_mask)(unsigned int slots,
unsigned int *tx_mask, unsigned int *rx_mask);
int (*set_tdm_slot)(struct snd_soc_dai *dai,
unsigned int tx_mask, unsigned int rx_mask,
int slots, int slot_width);
int (*set_channel_map)(struct snd_soc_dai *dai,
unsigned int tx_num, unsigned int *tx_slot,
unsigned int rx_num, unsigned int *rx_slot);
int (*get_channel_map)(struct snd_soc_dai *dai,
unsigned int *tx_num, unsigned int *tx_slot,
unsigned int *rx_num, unsigned int *rx_slot);
int (*set_tristate)(struct snd_soc_dai *dai, int tristate);
int (*set_sdw_stream)(struct snd_soc_dai *dai,
void *stream, int direction);
/*
* DAI digital mute - optional.
* Called by soc-core to minimise any pops.
*/
int (*digital_mute)(struct snd_soc_dai *dai, int mute);
int (*mute_stream)(struct snd_soc_dai *dai, int mute, int stream);
/*
* ALSA PCM audio operations - all optional.
* Called by soc-core during audio PCM operations.
*/
int (*startup)(struct snd_pcm_substream *,
struct snd_soc_dai *);
void (*shutdown)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*hw_params)(struct snd_pcm_substream *,
struct snd_pcm_hw_params *, struct snd_soc_dai *);
int (*hw_free)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*prepare)(struct snd_pcm_substream *,
struct snd_soc_dai *);
/*
* NOTE: Commands passed to the trigger function are not necessarily
* compatible with the current state of the dai. For example this
* sequence of commands is possible: START STOP STOP.
* So do not unconditionally use refcounting functions in the trigger
* function, e.g. clk_enable/disable.
*/
int (*trigger)(struct snd_pcm_substream *, int,
struct snd_soc_dai *);
int (*bespoke_trigger)(struct snd_pcm_substream *, int,
struct snd_soc_dai *);
/*
* For hardware based FIFO caused delay reporting.
* Optional.
*/
snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,
struct snd_soc_dai *);
};
/* SoC PCM stream information */
struct snd_soc_pcm_stream {
const char *stream_name;
u64 formats; /* SNDRV_PCM_FMTBIT_* */
unsigned int rates; /* SNDRV_PCM_RATE_* */
unsigned int rate_min; /* min rate */
unsigned int rate_max; /* max rate */
unsigned int channels_min; /* min channels */
unsigned int channels_max; /* max channels */
unsigned int sig_bits; /* number of bits of content */
const char *aif_name; /* DAPM AIF widget name */
};
/* SoC audio ops */
struct snd_soc_ops {
int (*startup)(struct snd_pcm_substream *);
void (*shutdown)(struct snd_pcm_substream *);
int (*hw_params)(struct snd_pcm_substream *, struct snd_pcm_hw_params *);
int (*hw_free)(struct snd_pcm_substream *);
int (*prepare)(struct snd_pcm_substream *);
int (*trigger)(struct snd_pcm_substream *, int);
};
Machine
Linux 音频驱动(二) ASoC音频驱动之Platform驱动和Linux 音频驱动(三) ASoC音频驱动之Codec驱动分别介绍了platform驱动、codec驱动,但仅有platform驱动、codec驱动是不能工作的,需要一个角色把codec、codec_dai、cpu_dai、platform给链结起来才能构成一个完整的音频回路,这个角色就由machine承担了。
Machine 可以理解为对开发板的抽象,开发板可能包括多个声卡,对应machine部分包含多个link。
Machine 驱动控制管理platform和codec之间的连接匹配,其抽象的结构体为struct snd_soc_dai_link。
struct snd_soc_dai_link {
/* config - must be set by machine driver */
const char *name; /* Codec name */
const char *stream_name; /* Stream name */
/*
* You MAY specify the link's CPU-side device, either by device name,
* or by DT/OF node, but not both. If this information is omitted,
* the CPU-side DAI is matched using .cpu_dai_name only, which hence
* must be globally unique. These fields are currently typically used
* only for codec to codec links, or systems using device tree.
*/
const char *cpu_name;
struct device_node *cpu_of_node;
/*
* You MAY specify the DAI name of the CPU DAI. If this information is
* omitted, the CPU-side DAI is matched using .cpu_name/.cpu_of_node
* only, which only works well when that device exposes a single DAI.
*/
const char *cpu_dai_name;
/*
* You MUST specify the link's codec, either by device name, or by
* DT/OF node, but not both.
*/
const char *codec_name;
struct device_node *codec_of_node;
/* You MUST specify the DAI name within the codec */
const char *codec_dai_name;
struct snd_soc_dai_link_component *codecs;
unsigned int num_codecs;
/*
* You MAY specify the link's platform/PCM/DMA driver, either by
* device name, or by DT/OF node, but not both. Some forms of link
* do not need a platform.
*/
const char *platform_name;
struct device_node *platform_of_node;
int id; /* optional ID for machine driver link identification */
const struct snd_soc_pcm_stream *params;
unsigned int num_params;
unsigned int dai_fmt; /* format to set on init */
enum snd_soc_dpcm_trigger trigger[2]; /* trigger type for DPCM */
/* codec/machine specific init - e.g. add machine controls */
int (*init)(struct snd_soc_pcm_runtime *rtd);
/* optional hw_params re-writing for BE and FE sync */
int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_hw_params *params);
/* machine stream operations */
const struct snd_soc_ops *ops;
const struct snd_soc_compr_ops *compr_ops;
/* Mark this pcm with non atomic ops */
bool nonatomic;
/* For unidirectional dai links */
unsigned int playback_only:1;
unsigned int capture_only:1;
/* Keep DAI active over suspend */
unsigned int ignore_suspend:1;
/* Symmetry requirements */
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
/* Do not create a PCM for this DAI link (Backend link) */
unsigned int no_pcm:1;
/* This DAI link can route to other DAI links at runtime (Frontend)*/
unsigned int dynamic:1;
/* This DAI link can be reconfigured at runtime (Backend) */
unsigned int dynamic_be:1;
/*
* This DAI can support no host IO (no pcm data is
* copied to from host)
*/
unsigned int no_host_mode:2;
/* DPCM capture and Playback support */
unsigned int dpcm_capture:1;
unsigned int dpcm_playback:1;
/* DPCM used FE & BE merged format */
unsigned int dpcm_merged_format:1;
/* DPCM used FE & BE merged channel */
unsigned int dpcm_merged_chan:1;
................
codec_name:音频链路需要绑定的codec名称标识,soc-core中会遍历codec_list,找到同名的codec并绑定;
platform_name:音频链路需要绑定的platform名称标识,soc-core中会遍历platform_list,找到同名的platform并绑定;
cpu_dai_name:音频链路需要绑定的cpu_dai名称标识,soc-core中会遍历dai_list,找到同名的dai并绑定;
codec_dai_name:音频链路需要绑定的codec_dai名称标识,soc-core中会遍历dai_list,找到同名的dai并绑定;
ops:machine数据流操作函数集。重点留意hw_params回调,一般来说这个回调是要实现的,用于配置codec、cpu_dai的时钟、格式。
struct snd_soc_card
/* SoC card */
struct snd_soc_card {
const char *name;
......
bool instantiated;
int (*probe)(struct snd_soc_card *card);
......
/* CPU <--> Codec DAI links */
struct snd_soc_dai_link *dai_link;
int num_links;
struct snd_soc_pcm_runtime *rtd;
int num_rtd;
......
const struct snd_kcontrol_new *controls;
int num_controls;
......
};
instantiated:用于标记声卡是否已经初始化完毕,在snd_soc_instantiate_card()函数最后会置1;
probe:声卡的probe函数,由snd_soc_instantiate_card()回调;
dai_link:声卡音频回路集合指针,即machine driver指针;
rtd:非常重要的数据指针,整个ASoC都以snd_soc_pcm_runtime为桥梁来操作,可以这么理解:每一个音频物理链路对应一个dai_link,而每个dai_link都有着自身的设备私有数据,这些私有数据保存在snd_soc_pcm_runtime中;
controls:声卡控件指针;
注册声卡 snd_soc_register_card()
在我们学习音频驱动时,多次碰到了platform。但是,每次提platform时都要结合上下文,才能确定其真正指代什么。要注意区分:
1. 当我们谈ASoC驱动包含platform driver、codec driver、machine driver时,此时说的platform特指某款SoC平台,如exynos、omap、qcom、mtk等等;
2. 当我们谈machine driver时,即snd_soc_dai_link时,此时说的platform特指PCM DMA,即snd_soc_platform_driver;
3. 当我们谈注册声卡时,声卡被抽象为挂在platform bus上的platform device。因此,才会有创建名字为 “soc-audio” 的标准Linux平台设备(platform_device),注册标准的Linux平台驱动(platform_driver),此时说的platform特指虚拟的platform bus。
PCM
ASoC音频驱动中Codec、Platform、Machine驱动的组成部分及其注册过程,这三者都是物理设备相关的,大家应该对音频物理链路有了一定的认知。接着分析音频驱动的中间层,由于这些并不是真正的物理设备,故我们称之为逻辑设备。
PCM逻辑设备,我们又习惯称之为PCM中间层或pcm native,起着承上启下的作用:
1.往上是与用户态接口的交互,实现音频数据在用户态和内核态之间的拷贝,即user space <-> kernel space;
2.往下是触发codec、platform、machine的操作函数,实现音频数据在dma_buffer<-> cpu_dai <-> codec之间的传输。
创建PCM逻辑设备 snd_soc_instantiate_card 该函数往下执行。 此处有逻辑图,有空去理解
snd_register_device_for_dev
snd_register_device_for_dev()创建pcmCxDxp、(目前内核不是该函数来创建 sound_insert_unit)pcmCxDxc设备节点的过程如下:
static int sound_insert_unit(struct sound_unit **list, const struct file_operations *fops, int index, int low, int top, const char *name, umode_t mode, struct device *dev)
{
struct sound_unit *s = kmalloc(sizeof(*s), GFP_KERNEL);
int r;
if (!s)
return -ENOMEM;
spin_lock(&sound_loader_lock);
retry:
r = __sound_insert_unit(s, list, fops, index, low, top);
spin_unlock(&sound_loader_lock);
if (r < 0)
goto fail;
else if (r < SOUND_STEP)
sprintf(s->name, "sound/%s", name);
else
sprintf(s->name, "sound/%s%d", name, r / SOUND_STEP);
..............................
device_create(sound_class, dev, MKDEV(SOUND_MAJOR, s->unit_minor),
NULL, "%s", s->name+6);
return s->unit_minor;
fail:
kfree(s);
return r;
}
首先,分配并初始化一个snd_minor结构中的各字段;
type:SNDRV_DEVICE_TYPE_PCM_PLAYBACK/SNDRV_DEVICE_TYPE_PCM_CAPTURE
card:card的编号
device:pcm实例的编号,大多数情况为0
f_ops:pcmCxDxp、pcmCxDxc设备节点的文件操作函数集,为snd_pcm_f_ops[*]
private_data:指向该snd_pcm的实例对象
根据type,card和pcm的编号,确定数组的索引值minor,minor也作为pcm设备的次设备号;
把该snd_minor结构的地址放入全局数组snd_minors[minor]中;
最后,调用device_create创建设备节点。
注1:C0D0代表的是Card 0 Device 0,即声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则。
注2:snd_minors[]非常重要,用于保存声卡下某个逻辑设备的上下文信息,它在逻辑设备建立阶段被填充,在逻辑设备被使用时就可以从该结构体中得到相应的信息。
注3:sound_class 是sysfs文件系统中的声卡类,在init_soundcore()中被创建。
rk66_gnrc:/dev/snd # ls -l
total 0
crw-rw---- 1 system audio 116, 4 2017-08-04 09:00 controlC0
crw-rw---- 1 system audio 116, 6 2017-08-04 09:00 controlC1
crw-rw---- 1 system audio 116, 3 2017-08-04 09:00 pcmC0D0c
crw-rw---- 1 system audio 116, 2 2017-08-04 09:00 pcmC0D0p
crw-rw---- 1 system audio 116, 5 2017-08-04 09:00 pcmC1D0c
crw-rw---- 1 system audio 116, 1 2017-08-04 09:00 seq
crw-rw---- 1 system audio 116, 33 2017-08-04 09:00 timer
rk66_gnrc:/proc/asound # cat devices
1: : sequencer
2: [ 0- 0]: digital audio playback
3: [ 0- 0]: digital audio capture
4: [ 0] : control
5: [ 1- 0]: digital audio capture
6: [ 1] : control
33: : timer
可以看到这些设备节点的Major=116,Minor则与系统逻辑设备信息文件/proc/asound/devices所列的对应起来,都是字符设备。上层可以通过open/close/read/write/ioctl等系统调用来操作声卡设备,和其他字符设备类似。但是,一般情况下我们使用已封装好的用户接口库如tinyalsa、alsa-lib。
下文将基于tinyslsa详细介绍。Tinyalsa提供了一套完整的pcm逻辑设备用户接口,比如pcm_open()、pcm_start()、pcm_prepare()、pcm_read()、pcm_write()。
PCM逻辑设备文件操作函数集:snd_pcm_f_ops[]
PCM逻辑设备文件操作函数集对于Playback和Capture是分开定义的,该操作函数集如下:
const struct file_operations snd_pcm_f_ops[2] = {
{
.owner = THIS_MODULE,
.write = snd_pcm_write,//用于单通道音频信号写
.write_iter = snd_pcm_writev,
.open = snd_pcm_playback_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_poll,
.unlocked_ioctl = snd_pcm_ioctl,//用于多通道音频信号写
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
},
{
.owner = THIS_MODULE,
.read = snd_pcm_read,//用于单通道音频信号读
.read_iter = snd_pcm_readv,
.open = snd_pcm_capture_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_poll,
.unlocked_ioctl = snd_pcm_ioctl,//用于多通道音频信号读
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
}
};
样本长度(sample):样本是记录音频数据最基本的单位,常见的有8位和16位。
通道数(channel):该参数为1表示单声道,2则是立体声。
桢(frame):桢记录了一个声音单元,其长度为样本长度与通道数的乘积。
采样率(rate):每秒钟采样次数,该次数是针对桢而言。
周期(period):音频设备一次处理所需要的桢数,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。
交错模式(interleaved):是一种音频数据的记录方式,在交错模式下,数据以连续桢的形式存放,即首先记录完桢1的左声道样本和右声道样本(假设为立体声格式),再开始桢2的记录。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,我们只需要使用交错模式就可以了。
period(周期):硬件中中断间的间隔时间。它表示输入延时。
声卡接口中有一个指针来指示声卡硬件缓存区中当前的读写位置。只要接口在运行,这个指针将循环地指向缓存区中的某个位置。
frame size = sizeof(one sample) * nChannels
alsa中配置的缓存(buffer)和周期(size)大小在runtime中是以帧(frames)形式存储的。
period_bytes = frames_to_bytes(runtime, runtime->period_size);
bytes_to_frames()
声音缓存和数据传输
每个声卡都有一个硬件缓存区来保存记录下来的样本。当缓存区足够满时,声卡将产生一个中断。内核声卡驱动然后使用直接内存(DMA)访问通道将样本传送到内存中的应用程序缓存区。类似地,对于回放,任何应用程序使用DMA将自己的缓存区数据传送到声卡的硬件缓存区中。
这样硬件缓存区是环缓存。也就是说当数据到达缓存区末尾时将重新回到缓存区的起始位置。ALSA维护一个指针来指向硬件缓存以及应用程序缓存区中数据操作的当前位置。从内核外部看,我们只对应用程序的缓存区感兴趣,所以本文只讨论应用程序缓存区。
应用程序缓存区的大小可以通过ALSA库函数调用来控制。缓存区可以很大,一次传输操作可能会导致不可接受的延迟,我们把它称为延时(latency)。为了解决这个问题,ALSA将缓存区拆分成一系列周期(period)(OSS/Free中叫片断fragments).ALSA以period为单元来传送数据。
一个周期(period)存储一些帧(frames)。每一帧包含时间上一个点所抓取的样本。对于立体声设备,一个帧会包含两个信道上的样本。分解过程:一个缓存区分解成周期,然后是帧,然后是样本。左右信道信息被交替地存储在一个帧内。这称为交错 (interleaved)模式。在非交错模式中,一个信道的所有样本数据存储在另外一个信道的数据之后。
Over and Under Run
当一个声卡活动时,数据总是连续地在硬件缓存区和应用程序缓存区间传输。但是也有例外。在录音例子中,如果应用程序读取数据不够快,循环缓存区将会被新的数据覆盖。这种数据的丢失被称为over run.在回放例子中,如果应用程序写入数据到缓存区中的速度不够快,缓存区将会"饿死"。这样的错误被称为"under run"。在ALSA文档中,有时将这两种情形统称为"XRUN"。适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。
snd_pcm_read() // interleaved 是否是交错模式取决与该参数
|--->snd_pcm_lib_read(substream, buf, count);
|--->__snd_pcm_lib_xfer(substream, (void __force *)buf, //(交错模式) runtime->access != SNDRV_PCM_ACCESS_RW_INTERLEAVED //判断存储模式
true, frames, false);///* the common loop for read/write data */
|--->pcm_sanity_check(substream);
|--->transfer = substream->ops->copy_kernel; / transfer = (pcm_transfer_f)substream->ops->copy_user;
|--->snd_pcm_start(substream);
|--->snd_pcm_action(&snd_pcm_action_start, substream,
SNDRV_PCM_STATE_RUNNING);
|--->snd_pcm_stream_linked(substream)
|--->snd_pcm_action_group(ops, substream, state, 1);//该函数是处理链接流的核心
|--->ops->pre_action(s, state); = snd_pcm_pre_start(struct snd_pcm_substream *substream, int state) // 回调函数
|--->ops->do_action(s, state); = snd_pcm_do_start(struct snd_pcm_substream *substream, int state)
|--->substream->runtime->trigger_master
|--->substream->ops->trigger(substream, SNDRV_PCM_TRIGGER_START);
|--->soc_pcm_trigger;>>>>soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)中赋值
|--->codec_dai->driver->ops->trigger(substream,cmd, codec_dai); //codec 驱动中赋值
|--->component->driver->ops->trigger(substream, cmd);
|--->cpu_dai->driver->ops->trigger(substream, cmd, cpu_dai); // codec 驱动中赋值
snd_soc_dai_driver ->snd_soc_dai_ops->int (*trigger)(struct snd_pcm_substream *, int,struct snd_soc_dai *);
|--->rtd->dai_link->ops->trigger(substream, cmd);
|--->snd_pcm_avail(substream);
|--->pcm_lib_apply_appl_ptr(substream, appl_ptr);
snd_pcm_write()//
|--->snd_pcm_lib_write(substream, buf, count);
|--->__snd_pcm_lib_xfer(substream, (void __force *)buf, true, frames, false);
|--->snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,
void *data, bool interleaved,
snd_pcm_uframes_t size, bool in_kernel)
..........与snd_pcm_read()类似
Open PCM逻辑设备
User Space:应用程序通过pcm_open()直接呼叫系统调用open()打开pcmCxDxp或pcmCxDxc ./external/tinyalsa/pcm.c,
struct pcm *pcm_open(unsigned int card, unsigned int device,
unsigned int flags, struct pcm_config *config)
{
struct pcm *pcm;
struct snd_pcm_info info;
struct snd_pcm_hw_params params;
struct snd_pcm_sw_params sparams;
char fn[256];
int rc;
if (!config) {
return &bad_pcm; /* TODO: could support default config here */
}
pcm = calloc(1, sizeof(struct pcm));
if (!pcm)
return &bad_pcm; /* TODO: could support default config here */
pcm->config = *config;
snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
flags & PCM_IN ? 'c' : 'p');
pcm->flags = flags;
pcm->fd = open(fn, O_RDWR|O_NONBLOCK);
..............
}
Kernel Space:应用层呼叫的系统调用open()到内核空间就是调用PCM逻辑设备的snd_pcm_f_ops.open文件操作函数
sound/core/pcm_native.c
static int snd_pcm_playback_open(struct inode *inode, struct file *file)
{
struct snd_pcm *pcm;
int err = nonseekable_open(inode, file);
if (err < 0)
return err;
pcm = snd_lookup_minor_data(iminor(inode),
SNDRV_DEVICE_TYPE_PCM_PLAYBACK);
err = snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_PLAYBACK);
if (pcm)
snd_card_unref(pcm->card);
return err;
}
static int snd_pcm_capture_open(struct inode *inode, struct file *file)
{
struct snd_pcm *pcm;
int err = nonseekable_open(inode, file);
if (err < 0)
return err;
pcm = snd_lookup_minor_data(iminor(inode),
SNDRV_DEVICE_TYPE_PCM_CAPTURE);
err = snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_CAPTURE);
if (pcm)
snd_card_unref(pcm->card);
return err;
}
static int snd_pcm_playback_open(struct inode *inode, struct file *file)
{
| snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_PLAYBACK);
| snd_pcm_open_file(file, pcm, stream);
| snd_pcm_open_substream(pcm, stream, file, &substream);
| substream->ops->open(substream); //即soc_pcm_open(), 在soc_new_pcm()函数中配置的
|--> cpu_dai->driver->ops->startup()
|--> platform->driver->ops->open()
|--> codec_dai->driver->ops->startup()
|--> rtd->dai_link->ops->startup()
}
snd_pcm_capture_open 类似
Write/Read PCM逻辑设备
在我的源码包里,tinyalsa write PCM逻辑设备是通过 ioctl() 函数完成的,即应用程序将需要播放的音频数据通过pcm_write() --> ioctl() 传递到内核。
User Space:应用程序write PCM逻辑设备是通过pcm_write() --> ioctl() 来完成的
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
struct snd_xferi x;
if (pcm->flags & PCM_IN)
return -EINVAL;
x.buf = (void*)data;
x.frames = count / (pcm->config.channels *
pcm_format_to_bits(pcm->config.format) / 8);
........................
}
if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)) {
pcm->prepared = 0;
.......................
}
Kernel Space:应用层呼叫的系统调用ioctl()到内核空间就是调用PCM逻辑设备的snd_pcm_f_ops.ioctl文件操作函数
.unlocked_ioctl = snd_pcm_ioctl,
static long snd_pcm_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct snd_pcm_file *pcm_file;
pcm_file = file->private_data;
if (((cmd >> 8) & 0xff) != 'A')
return -ENOTTY;
return snd_pcm_common_ioctl(file, pcm_file->substream, cmd,
(void __user *)arg);
}
cmd 记录:
在驱动程序里, ioctl() 函数上传送的变量 cmd 是应用程序用于区别设备驱动程序请求处理内容的值。cmd除了可区别数字外,还包含有助于处理的几种相应信息。 cmd的大小为 32位,共分 4 个域:
bit31~bit30 2位为 “区别读写” 区,作用是区分是读取命令还是写入命令。
bit29~bit15 14位为 "数据大小" 区,表示 ioctl() 中的 arg 变量传送的内存大小。
bit20~bit08 8位为 “魔数"(也称为"幻数")区,这个值用以与其它设备驱动程序的 ioctl 命令进行区别。
bit07~bit00 8位为 "区别序号" 区,是区分命令的命令顺序序号。
像 命令码中的 “区分读写区” 里的值可能是 _IOC_NONE (0值)表示无数据传输,_IOC_READ (读), _IOC_WRITE (写) , _IOC_READ|_IOC_WRITE (双向)。
内核定义了 _IO() , _IOR() , IOW() 和 _IOWR() 这 4 个宏来辅助生成上面的 cmd 。下面分析 _IO() 的实现,其它的类似:
这几个宏的使用格式为:
· _IO (魔数, 基数);
· _IOR (魔数, 基数, 变量型)
· _IOW (魔数, 基数, 变量型)
· _IOWR (魔数, 基数,变量型 )
魔数 (magic number)
魔数范围为 0~255 。通常,用英文字符 "A" ~ "Z" 或者 "a" ~ "z" 来表示。设备驱动程序从传递进来的命令获取魔数,然后与自身处理的魔数想比较,如果相同则处理,不同则不处理。魔数是拒绝误使用的初步辅助状态。设备驱动程序可以通过 _IOC_TYPE (cmd) 来获取魔数。不同的设备驱动程序最好设置不同的魔数,但并不是要求绝对,也是可以使用其他设备驱动程序已用过的魔数。
基(序列号)数
基数用于区别各种命令。通常,从 0开始递增,相同设备驱动程序上可以重复使用该值。例如,读取和写入命令中使用了相同的基数,设备驱动程序也能分辨出来,原因在于设备驱动程序区分命令时 使用 switch ,且直接使用命令变量 cmd值。创建命令的宏生成的值由多个域组合而成,所以即使是相同的基数,也会判断为不同的命令。设备驱动程序想要从命令中获取该基数,就使用下面的宏:
_IOC_NR (cmd)
通常,switch 中的 case 值使用的是命令的本身。
变量型
变量型使用 arg 变量指定传送的数据大小,但是不直接代入输入,而是代入变量或者是变量的类型,原因是在使用宏创建命令,已经包含了 sizeof() 编译命令
#define SNDRV_PCM_IOCTL_PVERSION _IOR('A', 0x00, int)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
// #define _IOC_NRBITS 8
#define _IOC_TYPEBITS 8
#define _IOC_NRSHIFT 0
#ifndef _IOC_SIZEBITS
# define _IOC_SIZEBITS 14
#endif
#ifndef _IOC_DIRBITS
# define _IOC_DIRBITS 2
#endif
#define _IOC_TYPECHECK(t) (sizeof(t))
# define _IOC_NONE 0U //无操作
# define _IOC_WRITE 1U //写
# define _IOC_READ 2U //读
#define _IOC(dir,type,nr,size)
(((dir) << _IOC_DIRSHIFT) | #define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)
((type) << _IOC_TYPESHIFT) | #define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
((nr) << _IOC_NRSHIFT) | #define _IOC_NRSHIFT 0
((size) << _IOC_SIZESHIFT)) #define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC(dir,type,nr,size)
(((dir) << 30) | _IOC_DIRSHIFT = 30
((type) << 8) | // _IOC_TYPESHIFT = 8
((nr) << 0) | _IOC_NRSHIFT = 0
((size) << 16)) // _IOC_SIZESHIFT =16
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
_IOR('A', 0x00, int)
_IOC(2U,('A'),(0x00),(sizeof(int)))
#define SNDRV_PCM_IOCTL_PVERSION _IOR('A', 0x00, int) =
#define _IOC(dir,type,nr,size)
(((2U) << 30) |
(('A') << 8) |
((0x00) << 0) |
((sizeof(int)) << 16))
SNDRV_PCM_IOCTL_PVERSION = 80044100 //32位
//以前内核的入口在snd_pcm_playback_ioctl ,而目前入口在snd_pcm_ioctl 自己去跟进
snd_pcm_ioctlsnd_pcm_ioctl(struct file *file, unsigned int cmd,unsigned long arg)
static long snd_pcm_playback_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
|--> //./kernel-3.10/sound/core/pcm_native.c, line 2624
| snd_pcm_playback_ioctl1(file, pcm_file->substream, cmd, (void __user *)arg);
|--> //./kernel-3.10/sound/core/pcm_lib.c, line 2101
| snd_pcm_lib_write(substream, xferi.buf, xferi.frames);
|--> //./kernel-3.10/sound/core/pcm_lib.c, line 1985
| snd_pcm_lib_write1(pcm, stream, file, &substream);
|--> //./kernel-3.10/sound/core/pcm_lib.c, line 1962
| transfer(substream); //即snd_pcm_lib_write_transfer(). snd_pcm_lib_write1()函数的最后一个入参
|--> hwbuf = runtime->dma_area + frames_to_bytes(runtime, hwoff);
| //内存和DMA Buffer之间的数据传递, 循环搬送直到播放完毕
|--> copy_from_user(hwbuf, buf, frames_to_bytes(runtime, frames))
|--> //./kernel-3.10/sound/core/pcm_native.c, line 904
| snd_pcm_start() //启动DMA传输(只是在开始时,调用一次)
|--> //./kernel-3.10/sound/core/pcm_native.c, line 785
| snd_pcm_action(&snd_pcm_action_start, substream, SNDRV_PCM_STATE_RUNNING);
|--> //./kernel-3.10/sound/core/pcm_native.c, line 716 or line 765
| snd_pcm_action_group()/snd_pcm_action_single()
|--> //ops为snd_pcm_action()函数第一个入参
| res = ops->do_action(substream, state); //snd_pcm_action_start.do_action(), snd_pcm_do_start()
|--> //./kernel-3.10/sound/core/soc-pcm.c, line 609
| substream->ops->trigger(); //即soc_pcm_trigger(), 在soc_new_pcm()函数中配置的
|--> codec_dai->driver->ops->trigger()
|--> platform->driver->ops->trigger()
|--> cpu_dai->driver->ops->trigger()
}
static int snd_pcm_action(const struct action_ops *ops,
struct snd_pcm_substream *substream,
int state)
snd_pcm_action()的第一个入参很重要,write PCM逻辑设备时,snd_pcm_action()函数的第一个入参为struct action_ops snd_pcm_action_start。
细心读者可能已经注意到,PCM逻辑设备提供了snd_pcm_f_ops[0].write()文件操作函数,那为什么应用程序不是通过系统调用 write()来写音频数据给内核呢 ?
这个问题,我自己的思考:
比较一下PCM逻辑设备的write()和ioctl(),可以发现snd_pcm_write()函数是snd_pcm_playback_ioctl()函数的简化版。
对于单声道音频数据,应用程序使用系统调用write()就可以满足需求。
对于多声道音频数据,应用程序要使用系统调用ioctl()。在多声道时,snd_pcm_playback_ioctl()会调用snd_pcm_lib_writev()。
关于read PCM逻辑设备,应用程序也是通过系统调用 ioctl() 完成的,到内核空间后调用snd_pcm_capture_ioctl()。code flow 和 write非常相似.
Close PCM逻辑设备
User Space:应用程序通过pcm_close() --> pcm_stop() --> ioctl()关闭pcmCxDxp或pcmCxDxc
int pcm_close(struct pcm *pcm)
{
if (pcm == &bad_pcm)
return 0;
pcm_hw_munmap_status(pcm);
if (pcm->flags & PCM_MMAP) {
pcm_stop(pcm);
munmap(pcm->mmap_buffer, pcm_frames_to_bytes(pcm, pcm->buffer_size));
}
............
}
Kernel Space:应用层呼叫的系统调用ioctl()到内核空间就是调用PCM逻辑设备的snd_pcm_f_ops.ioctl文件操作函数
.unlocked_ioctl = snd_pcm_ioctl,//用于多通道音频信号读
//该函数见上文分析
static long snd_pcm_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct snd_pcm_file *pcm_file;
pcm_file = file->private_data;
if (((cmd >> 8) & 0xff) != 'A')
return -ENOTTY;
return snd_pcm_common_ioctl(file, pcm_file->substream, cmd,
(void __user *)arg);
}
//以前内核的入口在snd_pcm_playback_ioctl ,而目前入口在snd_pcm_ioctl 自己去跟进
static long snd_pcm_playback_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
| snd_pcm_playback_ioctl1(file, pcm, SNDRV_PCM_STREAM_PLAYBACK);
| snd_pcm_common_ioctl1(file, substream, cmd, arg);
| snd_pcm_drop(substream);
| snd_pcm_stop(substream, SNDRV_PCM_STATE_SETUP);
| snd_pcm_action(&snd_pcm_action_stop, substream, state);
| snd_pcm_action_group()/snd_pcm_action_single()
|--> //ops为snd_pcm_action()函数第一个入参
| res = ops->do_action(substream, state); //snd_pcm_action_stop.do_action(), snd_pcm_do_stop()
| substream->ops->trigger(); //即soc_pcm_trigger(), 在soc_new_pcm()函数中配置的
|--> codec_dai->driver->ops->trigger()
|--> platform->driver->ops->trigger()
|--> cpu_dai->driver->ops->trigger()
}
snd_pcm_action()的第一个入参很重要,close PCM逻辑设备时,snd_pcm_action()函数的第一个入参为struct action_ops snd_pcm_action_stop。
注:请注意区分write/close PCM逻辑设备时snd_pcm_action()的第一个入参。
ALSA音频驱动之PCM Write数据传递过程
本文,我们将以回放(Playback,播放音频)为例,讲解PCM Data是如何从用户空间到内核空间,最后传递到Codec。
流程: [User Space]---copy_from_user()-->[DMA Buffer(kernel space)]----DMA--->[i2s TX FIFO]---i2s---->[Codec]---DAC->PGA/Mixer---> [SPK/HP/Earp]
对于Linux来说,由于分为 user space 和kernel space,而且两者之间不能随便互相访问。因此用户如果播放音频,则需要调用copy_from_user()将用户数据从user space拷贝到kernel space (DMA Buffer)。
DMA 负责将DMA Buffer中的音频数据搬运到I2S TX FIFO。
通过I2S总线,将音频数据传送到Codec。
Codec内部经过DAC转换,将模拟信号传到扬声器SPK(头戴式耳机HP、耳塞式耳机Earp)。
PCM Data Flow
User Space
用户空间应用程序使用的是 tinyalsa提供的接口write PCM Data,即播放音频文件。
Write PCM逻辑设备是通过 ioctl() 函数完成的,即应用程序将需要播放的音频数据通过pcm_write() --> ioctl() 传递到内核。
pcm_write() 上文以经有该函数的介绍粘贴。
音频数据中的几个重要概念:
Format:样本长度(采样精度 or 采样深度),音频数据最基本的单位,常见的有 8 位和 16 位;
Channel:声道数,分为单声道 mono 和立体声stereo;
Frame:帧,构成一个完整的声音单元,Frame = Format * Channel;
Rate:又称 sample rate:采样率,即每秒的采样次数,针对帧而言;
Period size:周期,每次硬件中断处理音频数据的帧数,对于音频设备的数据读写,以此为单位;
Buffer size:数据缓冲区大小,这里指runtime 的 buffer size,而不是结构体 snd_pcm_hardware 中定义的buffer_bytes_max;一般来说 buffer_size = period_size * period_count,period_count 相当于处理完一个 buffer 数据所需的硬件中断次数。
为了通过系统调用ioctl()传递音频数据,定义了struct snd_xferi x,x.buf指向本次要播放的音频数据,x.frames表示本次音频数据总共有多少帧(frame)。
Kernel Space
通过系统调用ioctl()传递数据到内核,在内核空间是PCM逻辑设备对应的snd_pcm_f_ops[0].unlocked_ioctl()。上文以经提及过了。
snd_pcm_ioctl()
--->snd_pcm_common_ioctl(file, pcm_file->substream, cmd, (void __user *)arg);
--->snd_pcm_lib_write(struct snd_pcm_substream *substream,const void __user *buf, snd_pcm_uframes_t frames)
--->/* the common loop for read/write data */
snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,void *data, bool interleaved,snd_pcm_uframes_t size, bool in_kernel)
.................
PCM write时的数据传递: (该数据传递是内核3.10 的版本,如需分析,需要从snd_pcm_common_ioctl() 函数分析)
应用程序调用tinyalsa提供的接口pcm_write()-->ioctl()将需要回放的音频数据指针和帧数传递给内核。
内核在snd_pcm_lib_write_transfer()函数中使用copy_from_user()将音频数据从user space拷贝到kernel space,即从应用程序的buffer拷贝到DMA buffer。
内核在snd_pcm_start()中启动DMA传输,将音频数据从DMA buffer拷贝到I2S TX FIFO。(实质上是通过pcm_dma的trigger函数来做的。)
#define SNDRV_CTL_ELEM_IFACE_MIXER ((__force snd_ctl_elem_iface_t) 2) /* virtual mixer device */
#define SOC_ENUM_EXT(xname, xenum, xhandler_get, xhandler_put)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_enum_double,
.get = xhandler_get, .put = xhandler_put,
.private_value = (unsigned long)&xenum }
ALSA声卡驱动中的DAPM详解之一:kcontrol
DAPM是Dynamic Audio Power Management的缩写,直译过来就是动态音频电源管理的意思,DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态下。DAPM对用户空间的应用程序来说是透明的,所有与电源相关的开关都在ASoc core中完成。用户空间的应用程序无需对代码做出修改,也无需重新编译,DAPM根据当前激活的音频流(playback/capture)和声卡中的mixer等的配置来决定那些音频控件的电源开关被打开或关闭。
snd_kcontrol_new结构
在正式讨论DAPM之前,我们需要先搞清楚ASoc中的一个重要的概念:kcontrol,不熟悉的读者需要浏览一下我之前的文章:Linux ALSA声卡驱动之四:Control设备的创建。通常,一个kcontrol代表着一个mixer(混音器),或者是一个mux(多路开关),又或者是一个音量控制器等等
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;
};
nd_kcontrol_new结构会在声卡的初始化阶段,通过snd_soc_add_codec_controls函数注册到系统中,用户空间就可以通过amixer或alsamixer等工具查看和设定这些控件的状态。
snd_kcontrol_new结构中,几个主要的字段是get,put,private_value,get回调函数用于获取该控件当前的状态值,而put回调函数则用于设置控件的状态值,而private_value字段则根据不同的控件类型有不同的意义,比如对于普通的控件,private_value字段可以用来定义该控件所对应的寄存器的地址以及对应的控制位在寄存器中的位置信息。值得庆幸的是,ASoc系统已经为我们准备了大量的宏定义,用于定义常用的控件,这些宏定义位于include/sound/soc.h中
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) }
Mixer控件
Mixer控件用于音频通道的路由控制,由多个输入和一个输出组成,多个输入可以自由地混合在一起,形成混合后的输出:
对于Mixer控件,我们可以认为是多个简单控件的组合,通常,我们会为mixer的每个输入端都单独定义一个简单控件来控制该路输入的开启和关闭,反应在代码上,就是定义一个soc_kcontrol_new数组:
static const struct snd_kcontrol_new left_speaker_mixer[] = {
SOC_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
SOC_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
SOC_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
SOC_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
};
以上这个mixer使用寄存器WM8993_SPEAKER_MIXER的第3,5,6,7位来分别控制4个输入端的开启和关闭。
Mux控件
mux控件与mixer控件类似,也是多个输入端和一个输出端的组合控件,与mixer控件不同的是,mux控件的多个输入端同时只能有一个被选中。因此,mux控件所对应的寄存器,通常可以设定一段连续的数值,每个不同的数值对应不同的输入端被打开,与上述的mixer控件不同,ASoc用soc_enum结构来描述mux控件的寄存器信息:
/* enumerated kcontrol */
struct soc_enum {
unsigned short reg;
unsigned short reg2;
unsigned char shift_l;
unsigned char shift_r;
unsigned int max;
unsigned int mask;
const char * const *texts;
const unsigned int *values;
};
两个寄存器地址和位移字段:reg,reg2,shift_l,shift_r,用于描述左右声道的控制寄存器信息。字符串数组指针用于描述每个输入端对应的名字,value字段则指向一个数组,该数组定义了寄存器可以选择的值,每个值对应一个输入端,如果value是一组连续的值,通常我们可以忽略values参数。
widget-具备路径和电源管理信息的kcontrol
利用kcontrol,我们可以完成对音频系统中的mixer,mux,音量控制,音效控制,以及各种开关量的控制,通过对各种kcontrol的控制,使得音频硬件能够按照我们预想的结果进行工作。同时我们可以看到,kcontrol还是有以下几点不足:
只能描述自身,无法描述各个kcontrol之间的连接关系;
没有相应的电源管理机制;
没有相应的时间处理机制来响应播放、停止、上电、下电等音频事件;
为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序;
当一个音频路径不再有效时,不能自动关闭该路径上的所有的kcontrol;
DAPM的基本单元:widget
目前kcontrol的一些不足,而DAPM框架为了解决这些问题,引入了widget这一概念,所谓widget,其实可以理解为是kcontrol的进一步升级和封装,她同样是指音频系统中的某个部件,比如mixer,mux,输入输出引脚,电源供应器等等,甚至,我们可以定义虚拟的widget,例如playback stream widget。widget把kcontrol和动态电源管理进行了有机的结合,同时还具备音频路径的连结功能,一个widget可以与它相邻的widget有某种动态的连结关系。在DAPM框架中,widget用结构体snd_soc_dapm_widget来描述
/* dapm widget */
struct snd_soc_dapm_widget {
enum snd_soc_dapm_type id; //该widget的类型值,比如snd_soc_dapm_output,snd_soc_dapm_mixer等等。
const char *name; /* widget name */ 该widget的名字
const char *sname; /* stream name */ 代表该widget所在stream的名字,比如对于snd_soc_dapm_dai_in类型的widget,会使用该字段
struct list_head list; // 所有注册到系统中的widget都会通过该list,链接到代表声卡的snd_soc_card结构的widgets链表头字段中。
struct snd_soc_dapm_context *dapm; //snd_soc_dapm_context结构指针,ASoc把系统划分为多个dapm域,每个widget属于某个dapm域,同一个域代表着同样的偏置电压供电策略,比如,同一个codec中的widget通常位于同一个dapm域,而平台上的widget可能又会位于另外一个platform域中。
void *priv; /* widget specific data */有些widget可能需要一些专有的数据,可以使用该字段来保存,像snd_soc_dapm_dai_in类型的widget,会使用该字段来记住与之相关联的snd_soc_dai结构指针。
struct regulator *regulator; /* attached regulator */对于snd_soc_dapm_regulator_supply类型的widget,该字段指向与之相关的regulator结构指针。
struct pinctrl *pinctrl; /* attached pinctrl */
const struct snd_soc_pcm_stream *params; /* params for dai links */目前对于snd_soc_dapm_dai_link类型的widget,指向该dai的配置信息的snd_soc_pcm_stream结构。
unsigned int num_params; /* number of params for dai links */
unsigned int params_select; /* currently selected param for dai link */
/* dapm control */
int reg; /* negative reg = no direct dapm */reg shift mask 这3个字段用来控制该widget的电源状态,
分别对应控制信息所在的寄存器地址,位移值和屏蔽值。
unsigned char shift; /* bits to shift */
unsigned int mask; /* non-shifted mask */
unsigned int on_val; /* on state value */
unsigned int off_val; /* off state value */
unsigned char power:1; /* block power status */
unsigned char active:1; /* active stream on DAC, ADC's */
unsigned char connected:1; /* connected codec pin */
unsigned char new:1; /* cnew complete */
unsigned char force:1; /* force state */
unsigned char ignore_suspend:1; /* kept enabled over suspend */
unsigned char new_power:1; /* power from this run */
unsigned char power_checked:1; /* power checked this run */
unsigned char is_supply:1; /* Widget is a supply type widget */
unsigned char is_ep:2; /* Widget is a endpoint type widget */
int subseq; /* sort within widget type */
int (*power_check)(struct snd_soc_dapm_widget *w);
/* external events */
unsigned short event_flags; /* flags to specify event types */
int (*event)(struct snd_soc_dapm_widget*, struct snd_kcontrol *, int);
/* kcontrols that relate to this widget */
int num_kcontrols;
const struct snd_kcontrol_new *kcontrol_news;
struct snd_kcontrol **kcontrols;
struct snd_soc_dobj dobj;
/* widget input and output edges */
struct list_head edges[2];
/* used during DAPM updates */
struct list_head work_list;
struct list_head power_list;
struct list_head dirty;
int endpoints[2];
struct clk *clk;
};
value on_val off_val 电源状态的当前只,开启时和关闭时所对应的值。
power invert 用于指示该widget当前是否处于上电状态,invert则用于表明power字段是否需要逻辑反转。
active connected 分别表示该widget是否处于激活状态和连接状态,当和相邻的widget有连接关系时,connected位会被置1,否则置0。
new 我们定义好的widget(snd_soc_dapm_widget结构),在注册到声卡中时需要进行实例化,该字段用来表示该widget是否已经被实例化。
ext 表示该widget当前是否有外部连接,比如连接mic,耳机,喇叭等等。
force 该位被设置后,将会不管widget当前的状态,强制更新至新的电源状态。
ignore_suspend new_power power_checked 这些电源管理相关的字段。
subseq 该widget目前在上电或下电队列中的排序编号,为了防止在上下电的过程中出现pop-pop声,DAPM会给每个widget分配合理的上下电顺序。
*power_check 用于检查该widget是否应该上电或下电的回调函数指针。
event_flags 该字段是一个位或字段,每个位代表该widget会关注某个DAPM事件通知。只有被关注的通知事件会被发送到widget的事件处理回调函数中。
*event DAPM事件处理回调函数指针。
num_kcontrols *kcontrol_news **kcontrols 这3个字段用来描述与该widget所包含的kcontrol控件,例如一个mixer控件或者是一个mux控件。
sources sinks 两个链表字段,两个widget如果有连接关系,会通过一个snd_soc_dapm_path结构进行连接,sources链表用于链接所有的输入path,sinks链表用于链接所有的输出path。
power_list 每次更新整个dapm的电源状态时,会根据一定的算法扫描所有的widget,然后把需要变更电源状态的widget利用该字段链接到一个上电或下电的链表中,扫描完毕后,dapm系统会遍历这两个链表执行相应的上电或下电操作。
dirty 链表字段,widget的状态变更后,dapm系统会利用该字段,把该widget加入到一个dirty链表中,稍后会对dirty链表进行扫描,以执行整个路径的更新。
inputs 该widget的所有有效路径中,连接到输入端的路径数量。
outputs 该widget的所有有效路径中,连接到输出端的路径数量。
*clk 对于snd_soc_dapm_clock_supply类型的widget,指向相关联的clk结构指针。
widget的种类
在DAPM框架中,把各种不同的widget划分为不同的种类,snd_soc_dapm_widget结构中的id字段用来表示该widget的种类,可选的种类都定义在一个枚举中:
/* dapm widget types */
enum snd_soc_dapm_type {
snd_soc_dapm_input = 0, /* input pin */
snd_soc_dapm_output, /* output pin */
snd_soc_dapm_mux, /* selects 1 analog signal from many inputs */
snd_soc_dapm_demux, /* connects the input to one of multiple outputs */
snd_soc_dapm_mixer, /* mixes several analog signals together */
snd_soc_dapm_mixer_named_ctl, /* mixer with named controls */
snd_soc_dapm_pga, /* programmable gain/attenuation (volume) */
snd_soc_dapm_out_drv, /* output driver */
snd_soc_dapm_adc, /* analog to digital converter */
snd_soc_dapm_dac, /* digital to analog converter */
snd_soc_dapm_micbias, /* microphone bias (power) - DEPRECATED: use snd_soc_dapm_supply */
snd_soc_dapm_mic, /* microphone */
snd_soc_dapm_hp, /* headphones */
snd_soc_dapm_spk, /* speaker */
snd_soc_dapm_line, /* line input/output */
snd_soc_dapm_switch, /* analog switch */
snd_soc_dapm_vmid, /* codec bias/vmid - to minimise pops */
snd_soc_dapm_pre, /* machine specific pre widget - exec first */
snd_soc_dapm_post, /* machine specific post widget - exec last */
snd_soc_dapm_supply, /* power/clock supply */
snd_soc_dapm_pinctrl, /* pinctrl */
snd_soc_dapm_regulator_supply, /* external regulator */
snd_soc_dapm_clock_supply, /* external clock */
snd_soc_dapm_aif_in, /* audio interface input */
snd_soc_dapm_aif_out, /* audio interface output */
snd_soc_dapm_siggen, /* signal generator */
snd_soc_dapm_sink,
snd_soc_dapm_dai_in, /* link to DAI structure */
snd_soc_dapm_dai_out,
snd_soc_dapm_dai_link, /* link between two DAI structures */
snd_soc_dapm_kcontrol, /* Auto-disabled kcontrol */
snd_soc_dapm_buffer, /* DSP/CODEC internal buffer */
snd_soc_dapm_scheduler, /* DSP/CODEC internal scheduler */
snd_soc_dapm_effect, /* DSP/CODEC effect component */
snd_soc_dapm_src, /* DSP/CODEC SRC component */
snd_soc_dapm_asrc, /* DSP/CODEC ASRC component */
snd_soc_dapm_encoder, /* FW/SW audio encoder component */
snd_soc_dapm_decoder, /* FW/SW audio decoder component */
};
snd_soc_dapm_input 该widget对应一个输入引脚。
snd_soc_dapm_output 该widget对应一个输出引脚。
snd_soc_dapm_mux 该widget对应一个mux控件。
snd_soc_dapm_virt_mux 该widget对应一个虚拟的mux控件。
snd_soc_dapm_value_mux 该widget对应一个value类型的mux控件。
snd_soc_dapm_mixer 该widget对应一个mixer控件。
snd_soc_dapm_mixer_named_ctl 该widget对应一个mixer控件,但是对应的kcontrol的名字不会加入widget的名字作为前缀。
snd_soc_dapm_pga 该widget对应一个pga控件(可编程增益控件)。
snd_soc_dapm_out_drv 该widget对应一个输出驱动控件
snd_soc_dapm_adc 该widget对应一个ADC
snd_soc_dapm_dac 该widget对应一个DAC
snd_soc_dapm_micbias 该widget对应一个麦克风偏置电压控件
snd_soc_dapm_mic 该widget对应一个麦克风。
snd_soc_dapm_hp 该widget对应一个耳机。
snd_soc_dapm_spk 该widget对应一个扬声器。
snd_soc_dapm_line 该widget对应一个线路输入。
snd_soc_dapm_switch 该widget对应一个模拟开关。
snd_soc_dapm_vmid 该widget对应一个codec的vmid偏置电压。
snd_soc_dapm_pre machine级别的专用widget,会先于其它widget执行检查操作。
snd_soc_dapm_post machine级别的专用widget,会后于其它widget执行检查操作。
snd_soc_dapm_supply 对应一个电源或是时钟源。
snd_soc_dapm_regulator_supply 对应一个外部regulator稳压器。
snd_soc_dapm_clock_supply 对应一个外部时钟源。
snd_soc_dapm_aif_in 对应一个数字音频输入接口,比如I2S接口的输入端。
snd_soc_dapm_aif_out 对应一个数字音频输出接口,比如I2S接口的输出端。
snd_soc_dapm_siggen 对应一个信号发生器。
snd_soc_dapm_dai_in 对应一个platform或codec域的输入DAI结构。
snd_soc_dapm_dai_out 对应一个platform或codec域的输出DAI结构。
snd_soc_dapm_dai_link 用于链接一对输入/输出DAI结构。
//部件
widget之间的连接器:path
之前已经提到,一个widget是有输入和输出的,而且widget之间是可以动态地进行连接的,那它们是用什么来连接两个widget的呢?DAPM为我们提出了path这一概念,path相当于电路中的一根跳线,它把一个widget的输出端和另一个widget的输入端连接在一起,path用snd_soc_dapm_path结构来描述:
/* dapm audio path between two widgets */
struct snd_soc_dapm_path {
const char *name;
/*
* source (input) and sink (output) widgets
* The union is for convience, since it is a lot nicer to type
* p->source, rather than p->node[SND_SOC_DAPM_DIR_IN]
*/
union {
struct {
struct snd_soc_dapm_widget *f;
struct snd_soc_dapm_widget *sink;
};
struct snd_soc_dapm_widget *node[2];
};
/* status */
u32 connect:1; /* source and sink widgets are connected */
u32 walking:1; /* path is in the process of being walked */
u32 weak:1; /* path ignored for power management */
u32 is_supply:1; /* At least one of the connected widgets is a supply */
int (*connected)(struct snd_soc_dapm_widget *source,
struct snd_soc_dapm_widget *sink);
struct list_head list_node[2];
struct list_head list_kcontrol;
struct list_head list;
};
num snd_soc_dapm_direction {
SND_SOC_DAPM_DIR_IN,
SND_SOC_DAPM_DIR_OUT
};
当widget之间发生连接关系时,snd_soc_dapm_path作为连接者,它的source字段会指向该连接的起始端widget,而它的sink字段会指向该连接的到达端widget,还记得前面snd_soc_dapm_widget结构中的两个链表头字段:sources和sinks么?widget的输入端和输出端可能连接着多个path,所有输入端的snd_soc_dapm_path结构通过list_sink字段挂在widget的souces链表中,同样,所有输出端的snd_soc_dapm_path结构通过list_source字段挂在widget的sinks链表中。这里可能大家会被搞得晕呼呼的,一会source,一会sink,不要紧,只要记住,连接的路径是这样的:起始端widget的输出-->path的输入-->path的输出-->到达端widget输入。
另外,snd_soc_dapm_path结构的list字段用于把所有的path注册到声卡中,其实就是挂在snd_soc_card结构的paths链表头字段中。如果你要自己定义方法来检查path的当前连接状态,你可以提供自己的connected回调函数指针。
connect,walked,walking,weak是几个辅助字段,用于帮助所有path的遍历。
widget的连接关系:route 路由
一个路径的连接至少包含以下几个元素:起始端widget,跳线path,到达端widget,在DAPM中,用snd_soc_dapm_route结构来描述这样一个连接关系:
/*
* DAPM audio route definition.
*
* Defines an audio route originating at source via control and finishing
* at sink.
*/
struct snd_soc_dapm_route {
const char *sink;
const char *control;
const char *source;
/* Note: currently only supported for links where source is a supply */
int (*connected)(struct snd_soc_dapm_widget *source,
struct snd_soc_dapm_widget *sink);
};
sink指向到达端widget的名字字符串,source指向起始端widget的名字字符串,control指向负责控制该连接所对应的kcontrol名字字符串,connected回调则定义了上一节所提到的自定义连接检查回调函数。该结构的意义很明显就是:source通过一个kcontrol,和sink连接在一起,现在是否处于连接状态,请调用connected回调函数检查。
这里直接使用名字字符串来描述连接关系,所有定义好的route,最后都要注册到dapm系统中,dapm会根据这些名字找出相应的widget,并动态地生成所需要的snd_soc_dapm_path结构,正确地处理各个链表和指针的关系,实现两个widget之间的连接,具体的连接代码分析,我们留到以后的章节中讨论。
如何定义各种widget
snd_soc_dapm_widget,snd_soc_dapm_path,snd_soc_dapm_route。其中snd_soc_dapm_path无需我们自己定义,它会在注册snd_soc_dapm_route时动态地生成,但是对于系统中的widget和route,我们是需要自己进行定义的,另外,widget所包含的kcontrol与普通的kcontrol有所不同,它们的定义方法与标准的kcontrol也有所不同。本节的内容我将会介绍如何使用DAPM系统提供的一些辅助宏定义来定义各种类型的widget和它所用到的kcontrol。
定义widget
和普通的kcontrol一样,DAPM框架为我们提供了大量的辅助宏用来定义各种各样的widget控件,这些宏定义根据widget的类型,按照它们的电源所在的域,被分为了几个域,他们分别是:
codec域 比如VREF和VMID等提供参考电压的widget,这些widget通常在codec的probe/remove回调中进行控制,当然,在工作中如果没有音频流时,也可以适当地进行控制它们的开启与关闭。
platform域 位于该域上的widget通常是针对平台或板子的一些需要物理连接的输入/输出接口,例如耳机、扬声器、麦克风,因为这些接口在每块板子上都可能不一样,所以通常它们是在machine驱动中进行定义和控制,并且也可以由用户空间的应用程序通过某种方式来控制它们的打开和关闭。
音频路径域 一般是指codec内部的mixer、mux等控制音频路径的widget,这些widget可以根据用户空间的设定连接关系,自动设定他们的电源状态。
音频数据流域 是指那些需要处理音频数据流的widget,例如ADC、DAC等等。
codec域widget的定义
目前,DAPM框架只提供了定义一个codec域widget的辅助宏:
/* codec domain */
#define SND_SOC_DAPM_VMID(wname)
{ .id = snd_soc_dapm_vmid, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0}
platform域widget的定义
DAPM框架为我们提供了多种platform域widget的辅助定义宏:
/* platform domain */
#define SND_SOC_DAPM_SIGGEN(wname)
{ .id = snd_soc_dapm_siggen, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM }
#define SND_SOC_DAPM_SINK(wname)
{ .id = snd_soc_dapm_sink, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM }
#define SND_SOC_DAPM_INPUT(wname)
{ .id = snd_soc_dapm_input, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM }
#define SND_SOC_DAPM_OUTPUT(wname)
{ .id = snd_soc_dapm_output, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM }
#define SND_SOC_DAPM_MIC(wname, wevent)
{ .id = snd_soc_dapm_mic, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent,
.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD}
#define SND_SOC_DAPM_HP(wname, wevent)
{ .id = snd_soc_dapm_hp, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent,
.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}
#define SND_SOC_DAPM_SPK(wname, wevent)
{ .id = snd_soc_dapm_spk, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent,
.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}
#define SND_SOC_DAPM_LINE(wname, wevent)
{ .id = snd_soc_dapm_line, .name = wname, .kcontrol_news = NULL,
.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent,
.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}
#define SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert)
.reg = wreg, .mask = 1, .shift = wshift,
.on_val = winvert ? 0 : 1, .off_val = winvert ? 1 : 0
以上这些widget分别对应信号发生器,输入引脚,输出引脚,麦克风,耳机,扬声器,线路输入接口。其中的reg字段被设置为SND_SOC_NOPM(-1),表明这些widget是没有寄存器控制位来控制widget的电源状态的。麦克风,耳机,扬声器,线路输入接口这几种widget,还可以定义一个dapm事件回调函数wevent,从event_flags字段的设置可以看出,他们只会响应SND_SOC_DAPM_POST_PMU(上电后)和SND_SOC_DAPM_PMD(下电前)事件,这几个widget通常会在machine驱动中定义,而SND_SOC_DAPM_INPUT和SND_SOC_DAPM_OUTPUT则用来定义codec芯片的输出输入脚,通常在codec驱动中定义,最后,在machine驱动中增加相应的route,把麦克风和耳机等widget与相应的codec输入输出引脚的widget连接起来。
音频路径(path)域widget的定义
这种widget通常是对普通kcontrols控件的再封装,增加音频路径和电源管理功能,所以这种widget会包含一个或多个kcontrol,普通kcontrol的定义方法我们在 ALSA声卡驱动中的DAPM详解之一:kcontrol中已经介绍过,不过这些被包含的kcontrol不能使用这种方法定义,它们需要使用dapm框架提供的定义宏来定义,详细的讨论我们后面有介绍。这里先列出这些widget的定义宏:
/* path domain */
#define SND_SOC_DAPM_PGA(wname, wreg, wshift, winvert,
wcontrols, wncontrols)
{ .id = snd_soc_dapm_pga, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = wncontrols}
#define SND_SOC_DAPM_OUT_DRV(wname, wreg, wshift, winvert,
wcontrols, wncontrols)
{ .id = snd_soc_dapm_out_drv, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = wncontrols}
#define SND_SOC_DAPM_MIXER(wname, wreg, wshift, winvert,
wcontrols, wncontrols)
{ .id = snd_soc_dapm_mixer, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = wncontrols}
#define SND_SOC_DAPM_MIXER_NAMED_CTL(wname, wreg, wshift, winvert,
wcontrols, wncontrols)
{ .id = snd_soc_dapm_mixer_named_ctl, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = wncontrols}
/* DEPRECATED: use SND_SOC_DAPM_SUPPLY */
#define SND_SOC_DAPM_MICBIAS(wname, wreg, wshift, winvert)
{ .id = snd_soc_dapm_micbias, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = NULL, .num_kcontrols = 0}
#define SND_SOC_DAPM_SWITCH(wname, wreg, wshift, winvert, wcontrols)
{ .id = snd_soc_dapm_switch, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = 1}
#define SND_SOC_DAPM_MUX(wname, wreg, wshift, winvert, wcontrols)
{ .id = snd_soc_dapm_mux, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = 1}
#define SND_SOC_DAPM_DEMUX(wname, wreg, wshift, winvert, wcontrols)
{ .id = snd_soc_dapm_demux, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.kcontrol_news = wcontrols, .num_kcontrols = 1}
可以看出,这些widget的reg和shift字段是需要赋值的,说明这些widget是有相应的电源控制寄存器的,DAPM框架在扫描和更新音频路径时,会利用这些寄存器来控制widget的电源状态,使得它们的供电状态是按需分配的,需要的时候(在有效的音频路径上)上电,不需要的时候(不再有效的音频路径上)下电。这些widget需要完成和之前介绍的mixer、mux等控件同样的功能,实际上,这是通过它们包含的kcontrol控件来完成的,这些kcontrol我们需要在定义widget前先定义好,然后通过wcontrols和num_kcontrols参数传递给这些辅助定义宏。
音频数据流(stream)域widget的定义
这些widget主要包含音频输入/输出接口,ADC/DAC等等:
/* stream domain */
#define SND_SOC_DAPM_AIF_IN(wname, stname, wslot, wreg, wshift, winvert)
{ .id = snd_soc_dapm_aif_in, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), }
#define SND_SOC_DAPM_AIF_IN_E(wname, stname, wslot, wreg, wshift, winvert,
wevent, wflags)
{ .id = snd_soc_dapm_aif_in, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.event = wevent, .event_flags = wflags }
#define SND_SOC_DAPM_AIF_OUT(wname, stname, wslot, wreg, wshift, winvert)
{ .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), }
#define SND_SOC_DAPM_AIF_OUT_E(wname, stname, wslot, wreg, wshift, winvert,
wevent, wflags)
{ .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.event = wevent, .event_flags = wflags }
#define SND_SOC_DAPM_DAC(wname, stname, wreg, wshift, winvert)
{ .id = snd_soc_dapm_dac, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert) }
#define SND_SOC_DAPM_DAC_E(wname, stname, wreg, wshift, winvert,
wevent, wflags)
{ .id = snd_soc_dapm_dac, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.event = wevent, .event_flags = wflags}
#define SND_SOC_DAPM_ADC(wname, stname, wreg, wshift, winvert)
{ .id = snd_soc_dapm_adc, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), }
#define SND_SOC_DAPM_ADC_E(wname, stname, wreg, wshift, winvert,
wevent, wflags)
{ .id = snd_soc_dapm_adc, .name = wname, .sname = stname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.event = wevent, .event_flags = wflags}
#define SND_SOC_DAPM_CLOCK_SUPPLY(wname)
{ .id = snd_soc_dapm_clock_supply, .name = wname,
.reg = SND_SOC_NOPM, .event = dapm_clock_event,
.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD }
每个codec有多个dai,而cpu(通常就是指某个soc cpu芯片)也会有多个dai,dai注册时,dapm系统会为每个dai创建一个snd_soc_dapm_dai_in或snd_soc_dapm_dai_out类型的widget,通常,这两种widget会和codec中具有相同的stream name的widget进行连接。另外一种情况,当系统中具有多个音频处理器(比如多个codec)时,他们之间可能会通过某两个dai进行连接,当machine驱动确认有这种配置时(通过判断dai_links结构中的param字段),会为他们建立一个dai link把他们绑定在一起,因为有连接关系,两个音频处理器之间的widget的电源状态就可以互相传递。
/* generic widgets */
#define SND_SOC_DAPM_REG(wid, wname, wreg, wshift, wmask, won_val, woff_val)
{ .id = wid, .name = wname, .kcontrol_news = NULL, .num_kcontrols = 0,
.reg = wreg, .shift = wshift, .mask = wmask,
.on_val = won_val, .off_val = woff_val, }
#define SND_SOC_DAPM_SUPPLY(wname, wreg, wshift, winvert, wevent, wflags)
{ .id = snd_soc_dapm_supply, .name = wname,
SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert),
.event = wevent, .event_flags = wflags}
#define SND_SOC_DAPM_REGULATOR_SUPPLY(wname, wdelay, wflags)
{ .id = snd_soc_dapm_regulator_supply, .name = wname,
.reg = SND_SOC_NOPM, .shift = wdelay, .event = dapm_regulator_event,
.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD,
.on_val = wflags}
#define SND_SOC_DAPM_PINCTRL(wname, active, sleep)
{ .id = snd_soc_dapm_pinctrl, .name = wname,
.priv = (&(struct snd_soc_dapm_pinctrl_priv)
{ .active_state = active, .sleep_state = sleep,}),
.reg = SND_SOC_NOPM, .event = dapm_pinctrl_event,
.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD }
定义dapm kcontrol
对于音频路径上的mixer或mux类型的widget,它们包含了若干个kcontrol,这些被包含的kcontrol实际上就是我们之前讨论的mixer和mux等,dapm利用这些kcontrol完成音频路径的控制。不过,对于widget来说,它的任务还不止这些,dapm还要动态地管理这些音频路径的连结关系,以便可以根据这些连接关系来控制这些widget的电源状态,如果按照普通的方法定义这些kcontrol,是无法达到这个目的的,因此,dapm为我们提供了另外一套定义宏,由它们完成这些被widget包含的kcontrol的定义。
static const struct snd_kcontrol_new left_speaker_mixer[] = {
SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
SOC_DAPM_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
};
/* dapm kcontrol types */
#define SOC_DAPM_DOUBLE(xname, reg, lshift, rshift, max, invert)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_volsw,
.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw,
.private_value = SOC_DOUBLE_VALUE(reg, lshift, rshift, max, invert, 0) }
#define SOC_DAPM_DOUBLE_R(xname, lreg, rreg, shift, max, invert)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_volsw,
.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw,
.private_value = SOC_DOUBLE_R_VALUE(lreg, rreg, shift, max, invert) }
#define SOC_DAPM_SINGLE(xname, reg, shift, max, invert)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_volsw,
.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw,
.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
#define SOC_DAPM_SINGLE_AUTODISABLE(xname, reg, shift, max, invert)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_volsw,
.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw,
.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 1) }
#define SOC_DAPM_SINGLE_VIRT(xname, max)
SOC_DAPM_SINGLE(xname, SND_SOC_NOPM, 0, max, 0)
#define SOC_DAPM_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_volsw,
.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | SNDRV_CTL_ELEM_ACCESS_READWRITE,
.tlv.p = (tlv_array),
.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw,
.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
#define SOC_DAPM_SINGLE_TLV_AUTODISABLE(xname, reg, shift, max, invert, tlv_array)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_volsw,
.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | SNDRV_CTL_ELEM_ACCESS_READWRITE,
.tlv.p = (tlv_array),
.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw,
.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 1) }
#define SOC_DAPM_SINGLE_TLV_VIRT(xname, max, tlv_array)
SOC_DAPM_SINGLE(xname, SND_SOC_NOPM, 0, max, 0, tlv_array)
#define SOC_DAPM_ENUM(xname, xenum)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_enum_double,
.get = snd_soc_dapm_get_enum_double,
.put = snd_soc_dapm_put_enum_double,
.private_value = (unsigned long)&xenum }
#define SOC_DAPM_ENUM_EXT(xname, xenum, xget, xput)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,
.info = snd_soc_info_enum_double,
.get = xget,
.put = xput,
.private_value = (unsigned long)&xenum }
#define SOC_DAPM_PIN_SWITCH(xname)
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname " Switch",
.info = snd_soc_dapm_info_pin_switch,
.get = snd_soc_dapm_get_pin_switch,
.put = snd_soc_dapm_put_pin_switch,
.private_value = (unsigned long)xname }
可以看出,SOC_DAPM_SINGLE对应与普通控件的SOC_SINGLE,SOC_DAPM_SINGLE_TLV对应SOC_SINGLE_TLV......,相比普通的kcontrol控件,dapm的kcontrol控件只是把info,get,put回调函数换掉了。dapm kcontrol的put回调函数不仅仅会更新控件本身的状态,他还会把这种变化传递到相邻的dapm kcontrol,相邻的dapm kcontrol又会传递这个变化到他自己相邻的dapm kcontrol,知道音频路径的末端,通过这种机制,只要改变其中一个widget的连接状态,与之相关的所有widget都会被扫描并测试一下自身是否还在有效的音频路径中,从而可以动态地改变自身的电源状态,这就是dapm的精髓所在。
建立widget和route
第一步,利用辅助宏定义widget所需要的dapm kcontrol:
static const struct snd_kcontrol_new left_speaker_mixer[] = {
SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
SOC_DAPM_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
};
static const struct snd_kcontrol_new right_speaker_mixer[] = {
SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
SOC_DAPM_SINGLE("IN1RP Switch", WM8993_SPEAKER_MIXER, 4, 1, 0),
SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 2, 1, 0),
SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 0, 1, 0),
};
static const char *aif_text[] = {
"Left", "Right"
};
static const struct soc_enum aifinl_enum =
SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 15, 2, aif_text);
static const struct snd_kcontrol_new aifinl_mux =
SOC_DAPM_ENUM("AIFINL Mux", aifinl_enum);
static const struct soc_enum aifinr_enum =
SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 14, 2, aif_text);
static const struct snd_kcontrol_new aifinr_mux =
SOC_DAPM_ENUM("AIFINR Mux", aifinr_enum);
以上,我们定义了wm8993中左右声道的speaker mixer控件:left_speaker_mixer和right_speaker_mixer,同时还为左右声道各定义了一个叫做AIFINL Mux和AIFINR Mux的输入选择mux控件。
第二步,定义真正的widget,包含第一步定义好的dapm控件:
static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {
......
SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),
......
SND_SOC_DAPM_MUX("DACL Mux", SND_SOC_NOPM, 0, 0, &aifinl_mux),
SND_SOC_DAPM_MUX("DACR Mux", SND_SOC_NOPM, 0, 0, &aifinr_mux),
SND_SOC_DAPM_MIXER("SPKL", WM8993_POWER_MANAGEMENT_3, 8, 0,
left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer)),
SND_SOC_DAPM_MIXER("SPKR", WM8993_POWER_MANAGEMENT_3, 9, 0,
right_speaker_mixer, ARRAY_SIZE(right_speaker_mixer)),
......
};
这一步,为左右声道各自定义了一个mux widget:DACL Mux和DACR Mux,实际的多路选择由dapm kcontrol:aifinl_mux和aifinr_mux,来完成,因为传入了SND_SOC_NOPM参数,这两个widget不具备电源属性,但是mux的切换会影响到与之相连的其它具备电源属性的电源状态。我们还为左右声道的扬声器各自定义了一个mixer widget:SPKL和SPKR,具体的mixer控制由上一步定义的left_speaker_mixer和right_speaker_mixer来完成,两个widget具备电源属性,所以,当这两个widget在一条有效的音频路径上时,dapm框架可以通过寄存器WM8993_POWER_MANAGEMENT_3的第8位和第9位控制它的电源状态。
第三步,定义这些widget的连接路径:
static const struct snd_soc_dapm_route routes[] = {
......
{ "DACL Mux", "Left", "AIFINL" },
{ "DACL Mux", "Right", "AIFINR" },
{ "DACR Mux", "Left", "AIFINL" },
{ "DACR Mux", "Right", "AIFINR" },
......
{ "SPKL", "DAC Switch", "DACL" },
{ "SPKL", NULL, "CLK_SYS" },
{ "SPKR", "DAC Switch", "DACR" },
{ "SPKR", NULL, "CLK_SYS" },
};
通过第一步的定义,我们知道DACL Mux和DACR Mux有两个输入引脚,分别是
Left
Right
而SPKL和SPKR有四个输入选择引脚,分别是:
Input Switch
IN1LP Switch/IN1RP Switch
Output Switch
DAC Switch
所以,很显然,上面的路径定义的意思就是:
AIFINL连接到DACL Mux的Left输入脚
AIFINR连接到DACL Mux的Right输入脚
AIFINL连接到DACR Mux的Left输入脚
AIFINR连接到DACR Mux的Right输入脚
DACL连接到SPKL的DAC Switch输入脚
DACR连接到SPKR的DAC Switch输入脚
第四步,在codec驱动的probe回调中注册这些widget和路径:
static int wm8993_probe(struct snd_soc_codec *codec)
{
......
snd_soc_dapm_new_controls(dapm, wm8993_dapm_widgets,
ARRAY_SIZE(wm8993_dapm_widgets));
......
snd_soc_dapm_add_routes(dapm, routes, ARRAY_SIZE(routes));
......
}
在machine驱动中,我们可以用同样的方式定义和注册板子特有的widget和路径信息
如何注册widget
如何连接两个widget
一个widget的状态裱画如何传递到整个音频路径中
dapm context
dapm context,直译过来的意思是dapm上下文,这个好像不好理解,其实我们可以这么理解:dapm把整个音频系统,按照功能和偏置电压级别,划分为若干个电源域,每个域包含各自的widget,每个域中的所有widget通常都处于同一个偏置电压级别上,而一个电源域就是一个dapm context,通常会有以下几种dapm context:
属于codec中的widget位于一个dapm context中
属于platform的widget位于一个dapm context中
属于整个声卡的widget位于一个dapm context中
对于音频系统的硬件来说,通常要提供合适的偏置电压才能正常地工作,有了dapm context这种组织方式,我们可以方便地对同一组widget进行统一的偏置电压管理,ASoc用snd_soc_dapm_context结构来表示一个dapm context:
/* DAPM context */
struct snd_soc_dapm_context {
enum snd_soc_bias_level bias_level;
unsigned int idle_bias_off:1; /* Use BIAS_OFF instead of STANDBY */
/* Go to BIAS_OFF in suspend if the DAPM context is idle */
unsigned int suspend_bias_off:1;
void (*seq_notifier)(struct snd_soc_dapm_context *,
enum snd_soc_dapm_type, int);
struct device *dev; /* from parent - for debug */
struct snd_soc_component *component; /* parent component */
struct snd_soc_card *card; /* parent card */
/* used during DAPM updates */
enum snd_soc_bias_level target_bias_level;
struct list_head list;
int (*stream_event)(struct snd_soc_dapm_context *dapm, int event);
int (*set_bias_level)(struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
struct snd_soc_dapm_wcache path_sink_cache;
struct snd_soc_dapm_wcache path_source_cache;
#ifdef CONFIG_DEBUG_FS
struct dentry *debugfs_dapm;
#endif
};
snd_soc_bias_level的取值范围是以下几种:
SND_SOC_BIAS_OFF
SND_SOC_BIAS_STANDBY
SND_SOC_BIAS_PREPARE
SND_SOC_BIAS_ON
snd_soc_dapm_context被内嵌到代表codec、platform、card、dai的结构体中:
struct snd_soc_codec {
......
/* dapm */
struct snd_soc_dapm_context dapm;
......
};
struct snd_soc_platform {
......
/* dapm */
struct snd_soc_dapm_context dapm;
......
};
struct snd_soc_card {
......
/* dapm */
struct snd_soc_dapm_context dapm;
......
};
:
struct snd_soc_dai {
......
/* dapm */
struct snd_soc_dapm_widget *playback_widget;
struct snd_soc_dapm_widget *capture_widget;
struct snd_soc_dapm_context dapm;
......
};
代表widget结构snd_soc_dapm_widget中,有一个snd_soc_dapm_context结构指针,指向所属的codec、platform、card、或dai的dapm结构。同时,所有的dapm结构,通过它的list字段,链接到代表声卡的snd_soc_card结构的dapm_list链表头字段。
我们已经知道,一个widget用snd_soc_dapm_widget结构体来描述,通常,我们会根据音频硬件的组成,分别在声卡的codec驱动、platform驱动和machine驱动中定义一组widget,这些widget用数组进行组织,我们一般会使用dapm框架提供的大量的辅助宏来定义这些widget数组,辅助宏的说明请参考前一偏文章:ALSA声卡驱动中的DAPM详解之三:如何定义各种widget。
codec驱动中注册 我们知道,我们会通过ASoc提供的api函数snd_soc_register_codec来注册一个codec驱动,该函数的第二个参数是一个snd_soc_component_driver结构指针,这个snd_soc_component_driver结构需要我们在codec驱动中显式地进行定义,其中有几个与dapm框架有关的字段:
/* component interface */
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;
...............
};
我们只要把我们定义好的snd_soc_dapm_widget结构数组的地址和widget的数量赋值到dapm_widgets和num_dapm_widgets字段即可,这样,经过devm_snd_soc_register_card注册codec后,在machine驱动匹配上该codec时,系统会判断这两个字段是否被赋值,如果有,它会调佣dapm框架提供的api来创建和注册widget,注意这里我说还要创建这个词,你可能比较奇怪,既然代表widget的snd_soc_dapm_widget结构数组已经在codec驱动中定义好了,为什么还要在创建?事实上,我们在codec驱动中定义的widget数组只是作为一个模板,dapm框架会根据该模板重新申请内存并初始化各个widget。
static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {
......
SND_SOC_DAPM_SUPPLY("VMID", SND_SOC_NOPM, 0, 0, NULL, 0),
SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),
......
};
static struct snd_soc_component_driver soc_codec_dev_wm8993 = {
.probe = codec_xxx_probe,
......
.dapm_widgets = &wm8993_dapm_widgets[0],
.num_dapm_widgets = ARRAY_SIZE(wm8993_dapm_widgets),
......
};
static int codec_wm8993_i2c_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
......
ret = devm_snd_soc_register_card(&i2c->dev,
&soc_codec_dev_wm8993, &wm8993_dai, 1);
......
}
上面这种注册方法有个缺点,有时候我们为了代码的清晰,可能会根据功能把不同的widget定义成多个数组,但是snd_soc_codec_driver中只有一个dapm_widgets字段,无法设定多个widget数组,这时候,我们需要主动在codec的probe回调中调用dapm框架提供的api来创建这些widget:
static int wm8993_probe(struct snd_soc_codec *codec)
{
......
snd_soc_dapm_new_controls(dapm, wm8993_dapm_widgets,
ARRAY_SIZE(wm8993_dapm_widgets));
......
}
实际上,对于第一种方法,snd_soc_register_codec内部其实也是调用snd_soc_dapm_new_controls来完成的。后面会有关于这个函数的详细分析。
platform驱动中注册 和codec驱动一样,我们会通过ASoc提供的api函数snd_soc_register_platform来注册一个platform驱动,该函数的第二个参数是一个snd_soc_platform_driver结构指针,snd_soc_platform_driver结构中同样也包含了与dapm相关的字段:
/* component interface */
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;
..................
};
要注册platform级别的widget,和codec驱动一样,只要把定义好的widget数组赋值给dapm_widgets和num_dapm_widgets字段即可,snd_soc_register_platform函数注册paltform后,当machine驱动匹配上该platform时,系统会自动完成创建和注册的工作。同理,我们也可以在platform驱动的probe回调函数中主动使用snd_soc_dapm_new_controls来完成widget的创建工作。具体的代码和codec驱动是类似的,这里就不贴了。
machine驱动中注册 有些widget可能不是位于codec中,例如一个独立的耳机放大器,或者是喇叭功放等,这种widget通常需要在machine驱动中注册,通常他们的dapm context也从属于声卡(snd_soc_card)域。做法依然和codec驱动类似,通过代表声卡的snd_soc_card结构中的几个dapm字段完成:
/* SoC card */
struct snd_soc_card {
const char *name;
const char *long_name;
const char *driver_name;
char dmi_longname[80];
char topology_shortname[32];
struct device *dev;
struct snd_card *snd_card;
struct module *owner;
struct mutex mutex;
struct mutex dapm_mutex;
struct mutex dapm_power_mutex;
bool instantiated;
bool topology_shortname_created;
int (*probe)(struct snd_soc_card *card);
int (*late_probe)(struct snd_soc_card *card);
int (*remove)(struct snd_soc_card *card);
/* the pre and post PM functions are used to do any PM work before and
* after the codec and DAI's do any PM work. */
int (*suspend_pre)(struct snd_soc_card *card);
int (*suspend_post)(struct snd_soc_card *card);
int (*resume_pre)(struct snd_soc_card *card);
int (*resume_post)(struct snd_soc_card *card);
/* callbacks */
int (*set_bias_level)(struct snd_soc_card *,
struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
int (*set_bias_level_post)(struct snd_soc_card *,
struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
int (*add_dai_link)(struct snd_soc_card *,
struct snd_soc_dai_link *link);
void (*remove_dai_link)(struct snd_soc_card *,
struct snd_soc_dai_link *link);
long pmdown_time;
/* CPU <--> Codec DAI links */
struct snd_soc_dai_link *dai_link; /* predefined links only */
int num_links; /* predefined links only */
struct list_head dai_link_list; /* all links */
int num_dai_links;
struct list_head rtd_list;
int num_rtd;
/* optional codec specific configuration */
struct snd_soc_codec_conf *codec_conf;
int num_configs;
/*
* optional auxiliary devices such as amplifiers or codecs with DAI
* link unused
*/
struct snd_soc_aux_dev *aux_dev;
int num_aux_devs;
struct list_head aux_comp_list;
const struct snd_kcontrol_new *controls;
int num_controls;
/*
* Card-specific routes and widgets.
* Note: of_dapm_xxx for Device Tree; Otherwise for driver build-in.
*/
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;
const struct snd_soc_dapm_widget *of_dapm_widgets;
int num_of_dapm_widgets;
const struct snd_soc_dapm_route *of_dapm_routes;
int num_of_dapm_routes;
bool fully_routed;
struct work_struct deferred_resume_work;
/* lists of probed devices belonging to this card */
struct list_head component_dev_list;
struct list_head widgets;
struct list_head paths;
struct list_head dapm_list;
struct list_head dapm_dirty;
/* attached dynamic objects */
struct list_head dobj_list;
/* Generic DAPM context for the card */
struct snd_soc_dapm_context dapm;
struct snd_soc_dapm_stats dapm_stats;
struct snd_soc_dapm_update *update;
#ifdef CONFIG_DEBUG_FS
struct dentry *debugfs_card_root;
struct dentry *debugfs_pop_time;
#endif
u32 pop_time;
void *drvdata;
};
只要把定义好的widget数组和数量赋值给dapm_widgets指针和num_dapm_widgets即可,注册声卡使用的api:snd_soc_register_card(),也会通过snd_soc_dapm_new_controls来完成widget的创建工作。
注册音频路径
系统中注册的各种widget需要互相连接在一起才能协调工作,连接关系通过snd_soc_dapm_route结构来定义,关于如何用snd_soc_dapm_route结构来定义路径信息,请参考: ALSA声卡驱动中的DAPM详解之三:如何定义各种widget中的"建立widget和route"一节的内容。通常,所有的路径信息会用一个snd_soc_dapm_route结构数组来定义。和widget一样,路径信息也分别存在与codec驱动,machine驱动和platform驱动中,我们一样有两种方式来注册音频路径信息:
通过snd_soc_component_driver(原 snd_soc_codec_driver/snd_soc_platform_driver) /snd_soc_card结构中的dapm_routes和num_dapm_routes字段;
在codec、platform的的probe回调中主动注册音频路径,machine驱动中则通过snd_soc_dai_link结构的init回调函数来注册音频路径;
两种方法最终都是通过调用snd_soc_dapm_add_routes函数来完成音频路径的注册工作的。以下的代码片段是omap的pandora板子的machine驱动,使用第二种方法注册路径信息:
static const struct snd_soc_dapm_widget omap3pandora_in_dapm_widgets[] = {
SND_SOC_DAPM_MIC("Mic (internal)", NULL),
SND_SOC_DAPM_MIC("Mic (external)", NULL),
SND_SOC_DAPM_LINE("Line In", NULL),
};
static const struct snd_soc_dapm_route omap3pandora_out_map[] = {
{"PCM DAC", NULL, "APLL Enable"},
{"Headphone Amplifier", NULL, "PCM DAC"},
{"Line Out", NULL, "PCM DAC"},
{"Headphone Jack", NULL, "Headphone Amplifier"},
};
static const struct snd_soc_dapm_route omap3pandora_in_map[] = {
{"AUXL", NULL, "Line In"},
{"AUXR", NULL, "Line In"},
{"MAINMIC", NULL, "Mic (internal)"},
{"Mic (internal)", NULL, "Mic Bias 1"},
{"SUBMIC", NULL, "Mic (external)"},
{"Mic (external)", NULL, "Mic Bias 2"},
};
static int omap3pandora_out_init(struct snd_soc_pcm_runtime *rtd)
{
struct snd_soc_codec *codec = rtd->codec;
struct snd_soc_dapm_context *dapm = &codec->dapm;
int ret;
/* All TWL4030 output pins are floating */
snd_soc_dapm_nc_pin(dapm, "EARPIECE");
......
//注册kcontrol控件
ret = snd_soc_dapm_new_controls(dapm, omap3pandora_out_dapm_widgets,
ARRAY_SIZE(omap3pandora_out_dapm_widgets));
if (ret < 0)
return ret;
//注册machine的音频路径
return snd_soc_dapm_add_routes(dapm, omap3pandora_out_map,
ARRAY_SIZE(omap3pandora_out_map));
}
static int omap3pandora_in_init(struct snd_soc_pcm_runtime *rtd)
{
struct snd_soc_codec *codec = rtd->codec;
struct snd_soc_dapm_context *dapm = &codec->dapm;
int ret;
/* Not comnnected */
snd_soc_dapm_nc_pin(dapm, "HSMIC");
......
//注册kcontrol控件
ret = snd_soc_dapm_new_controls(dapm, omap3pandora_in_dapm_widgets,
ARRAY_SIZE(omap3pandora_in_dapm_widgets));
if (ret < 0)
return ret;
//注册machine音频路径
return snd_soc_dapm_add_routes(dapm, omap3pandora_in_map,
ARRAY_SIZE(omap3pandora_in_map));
}
/* Digital audio interface glue - connects codec <--> CPU */
static struct snd_soc_dai_link omap3pandora_dai[] = {
{
.name = "PCM1773",
......
.init = omap3pandora_out_init,
}, {
.name = "TWL4030",
.stream_name = "Line/Mic In",
......
.init = omap3pandora_in_init,
}
};
dai widget
上面几节的内容介绍了codec、platform以及machine级别的widget和route的注册方法,在dapm框架中,还有另外一种widget,它代表了一个dai(数字音频接口),关于dai的描述,请参考:Linux ALSA声卡驱动之七:ASoC架构中的Codec。dai按所在的位置,又分为cpu dai和codec dai,在硬件上,通常一个cpu dai会连接一个codec dai,而在machine驱动中,我们要在snd_soc_card结构中指定一个叫做snd_soc_dai_link的结构,该结构定义了声卡使用哪一个cpu dai和codec dai进行连接。在Asoc中,一个dai用snd_soc_dai结构来表述,其中有几个字段和dapm框架有关:
/*
* Digital Audio Interface runtime data.
*
* Holds runtime data for a DAI.
*/
struct snd_soc_dai {
const char *name;
int id;
struct device *dev;
/* driver ops */
struct snd_soc_dai_driver *driver;
/* DAI runtime info */
unsigned int capture_active; /* stream usage count */
unsigned int playback_active; /* stream usage count */
unsigned int probed:1;
unsigned int active;
struct snd_soc_dapm_widget *playback_widget;
struct snd_soc_dapm_widget *capture_widget;
/* DAI DMA data */
void *playback_dma_data;
void *capture_dma_data;
/* Symmetry data - only valid if symmetry is being enforced */
unsigned int rate;
unsigned int channels;
unsigned int sample_bits;
/* parent platform/codec */
struct snd_soc_component *component;
/* CODEC TDM slot masks and params (for fixup) */
unsigned int tx_mask;
unsigned int rx_mask;
struct list_head list;
};
dai由codec驱动和平台代码中的iis或pcm接口驱动注册,machine驱动负责找到 snd_soc_dai_link中指定的一对cpu/codec dai,并把它们进行绑定。不管是cpu dai还是codec dai,通常会同时传输播放和录音的音频流的能力,所以我们可以看到,snd_soc_dai中有两个widget指针,分别代表播放流和录音流。
codec dai widget
首先,codec驱动在注册codec时,会传入该codec所支持的dai个数和记录dai信息的snd_soc_dai_driver结构指针:
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);
int devm_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);
static struct snd_soc_dai_driver wm8993_dai = {
.name = "wm8993-hifi",
.playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2,
.rates = WM8993_RATES,
.formats = WM8993_FORMATS,
.sig_bits = 24,
},
.capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2,
.rates = WM8993_RATES,
.formats = WM8993_FORMATS,
.sig_bits = 24,
},
.ops = &wm8993_ops,
.symmetric_rates = 1,
};
static int wm8993_i2c_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
......
ret = snd_soc_register_component(&i2c->dev,
&soc_codec_dev_wm8993, &wm8993_dai, 1);
......
}
这回使得ASoc把codec的dai注册到系统中,并把这些dai都挂在全局链表变量dai_list中,然后,在codec被machine驱动匹配后,soc_probe_component函数会被调用,他会通过全局链表变量dai_list查找所有属于该codec的dai,调用snd_soc_dapm_new_dai_widgets函数来生成该dai的播放流widget和录音流widget:
static int soc_probe_component(struct snd_soc_card *card,
struct snd_soc_component *component)
{
struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(component);
struct snd_soc_dai *dai;
int ret;
if (!strcmp(component->name, "snd-soc-dummy"))
return 0;
if (component->card) {
if (component->card != card) {
dev_err(component->dev,
"Trying to bind component to card "%s" but is already bound to card "%s"n",
card->name, component->card->name);
return -ENODEV;
}
return 0;
}
if (!try_module_get(component->dev->driver->owner))
return -ENODEV;
component->card = card;
dapm->card = card;
soc_set_name_prefix(card, component);
soc_init_component_debugfs(component);
if (component->driver->dapm_widgets) {
ret = snd_soc_dapm_new_controls(dapm,
component->driver->dapm_widgets,
component->driver->num_dapm_widgets);
if (ret != 0) {
dev_err(component->dev,
"Failed to create new controls %dn", ret);
goto err_probe;
}
}
list_for_each_entry(dai, &component->dai_list, list) {
ret = snd_soc_dapm_new_dai_widgets(dapm, dai);
if (ret != 0) {
dev_err(component->dev,
"Failed to create DAI widgets %dn", ret);
goto err_probe;
}
}
if (component->driver->probe) {
ret = component->driver->probe(component);
if (ret < 0) {
dev_err(component->dev,
"ASoC: failed to probe component %dn", ret);
goto err_probe;
}
WARN(dapm->idle_bias_off &&
dapm->bias_level != SND_SOC_BIAS_OFF,
"codec %s can not start from non-off bias with idle_bias_off==1n",
component->name);
}
/* machine specific init */
if (component->init) {
ret = component->init(component);
if (ret < 0) {
dev_err(component->dev,
"Failed to do machine specific init %dn", ret);
goto err_probe;
}
}
if (component->driver->controls)
snd_soc_add_component_controls(component,
component->driver->controls,
component->driver->num_controls);
if (component->driver->dapm_routes)
snd_soc_dapm_add_routes(dapm,
component->driver->dapm_routes,
component->driver->num_dapm_routes);
list_add(&dapm->list, &card->dapm_list);
list_add(&component->card_list, &card->component_dev_list);
return 0;
err_probe:
soc_cleanup_component_debugfs(component);
component->card = NULL;
module_put(component->dev->driver->owner);
return ret;
}
snd_soc_dapm_new_dai_widgets的代码:
int snd_soc_dapm_new_dai_widgets(struct snd_soc_dapm_context *dapm,
struct snd_soc_dai *dai)
{
struct snd_soc_dapm_widget template;
struct snd_soc_dapm_widget *w;
WARN_ON(dapm->dev != dai->dev);
memset(&template, 0, sizeof(template));
template.reg = SND_SOC_NOPM;
if (dai->driver->playback.stream_name) { // 创建播放 dai widget
template.id = snd_soc_dapm_dai_in;
template.name = dai->driver->playback.stream_name;
template.sname = dai->driver->playback.stream_name;
dev_dbg(dai->dev, "ASoC: adding %s widgetn",
template.name);
w = snd_soc_dapm_new_control_unlocked(dapm, &template);
if (IS_ERR(w)) {
int ret = PTR_ERR(w);
/* Do not nag about probe deferrals */
if (ret != -EPROBE_DEFER)
dev_err(dapm->dev,
"ASoC: Failed to create %s widget (%d)n",
dai->driver->playback.stream_name, ret);
return ret;
}
if (!w) {
dev_err(dapm->dev, "ASoC: Failed to create %s widgetn",
dai->driver->playback.stream_name);
return -ENOMEM;
}
w->priv = dai;
dai->playback_widget = w;
}
if (dai->driver->capture.stream_name) { // 创建录音 dai widget
template.id = snd_soc_dapm_dai_out;
template.name = dai->driver->capture.stream_name;
template.sname = dai->driver->capture.stream_name;
dev_dbg(dai->dev, "ASoC: adding %s widgetn",
template.name);
w = snd_soc_dapm_new_control_unlocked(dapm, &template);
if (IS_ERR(w)) {
int ret = PTR_ERR(w);
/* Do not nag about probe deferrals */
if (ret != -EPROBE_DEFER)
dev_err(dapm->dev,
"ASoC: Failed to create %s widget (%d)n",
dai->driver->playback.stream_name, ret);
return ret;
}
if (!w) {
dev_err(dapm->dev, "ASoC: Failed to create %s widgetn",
dai->driver->capture.stream_name);
return -ENOMEM;
}
w->priv = dai;
dai->capture_widget = w;
}
return 0;
}
分别为Playback和Capture创建了一个widget,widget的priv字段指向了该dai,这样通过widget就可以找到相应的dai,并且widget的名字就是 snd_soc_dai_driver结构的stream_name。
端点widget
一条完整的dapm音频路径,必然有起点和终点,我们把位于这些起点和终点的widget称之为端点widget。以下这些类型的widget可以成为端点widget:
codec的输入输出引脚:
snd_soc_dapm_output
snd_soc_dapm_input
外接的音频设备:
snd_soc_dapm_hp
snd_soc_dapm_spk
snd_soc_dapm_line
音频流(stream domain):
snd_soc_dapm_adc
snd_soc_dapm_dac
snd_soc_dapm_aif_out
snd_soc_dapm_aif_in
snd_soc_dapm_dai_out
snd_soc_dapm_dai_in
电源、时钟和其它:
snd_soc_dapm_supply
snd_soc_dapm_regulator_supply
snd_soc_dapm_clock_supply
snd_soc_dapm_kcontrol
建立widget之间的连接关系
前面我们主要着重于codec、platform、machine驱动程序中如何使用和建立dapm所需要的widget,route,这些是音频驱动开发人员必须要了解的内容,经过前几章的介绍,我们应该知道如何在alsa音频驱动的3大部分(codec、platform、machine)中,按照所使用的音频硬件结构,定义出相应的widget,kcontrol,以及必要的音频路径,而在本章中,我们将会深入dapm的核心部分,看看各个widget之间是如何建立连接关系,形成一条完整的音频路径。
前面我们已经简单地介绍过,驱动程序需要使用以下api函数创建widget:
int snd_soc_dapm_new_controls(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_widget *widget,
int num);
实际上,这个函数只是创建widget的第一步,它为每个widget分配内存,初始化必要的字段,然后把这些widget挂在代表声卡的snd_soc_card的widgets链表字段中。要使widget之间具备连接能力,我们还需要第二个函数:
/* dapm path setup */
int snd_soc_dapm_new_widgets(struct snd_soc_card *card);
这个函数会根据widget的信息,创建widget所需要的dapm kcontrol,这些dapm kcontol的状态变化,代表着音频路径的变化,从而影响着各个widget的电源状态。看到函数的名称可能会迷惑一下, 实际上, snd_soc_dapm_new_controls的作用更多地是创建widget,而 snd_soc_dapm_new_widget的作用则更多地是创建widget所包含的kcontrol,所以 在我看来,这两个函数名称应该换过来叫更好
创建widget:snd_soc_dapm_new_controls
snd_soc_dapm_new_controls函数完成widget的创建工作,并把这些创建好的widget注册在声卡的widgets链表中
/**
* snd_soc_dapm_new_controls - create new dapm controls
* @dapm: DAPM context
* @widget: widget array
* @num: number of widgets
*
* Creates new DAPM controls based upon the templates.
*
* Returns 0 for success else error.
*/
int snd_soc_dapm_new_controls(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_widget *widget,
int num)
{
struct snd_soc_dapm_widget *w;
int i;
int ret = 0;
mutex_lock_nested(&dapm->card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);
for (i = 0; i < num; i++) {
w = snd_soc_dapm_new_control_unlocked(dapm, widget);
if (IS_ERR(w)) {
ret = PTR_ERR(w);
/* Do not nag about probe deferrals */
if (ret == -EPROBE_DEFER)
break;
dev_err(dapm->dev,
"ASoC: Failed to create DAPM control %s (%d)n",
widget->name, ret);
break;
}
if (!w) {
dev_err(dapm->dev,
"ASoC: Failed to create DAPM control %sn",
widget->name);
ret = -ENOMEM;
break;
}
widget++;
}
mutex_unlock(&dapm->card->dapm_mutex);
return ret;
}
struct snd_soc_dapm_widget *
snd_soc_dapm_new_control(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_widget *widget);
struct snd_soc_dapm_widget *
snd_soc_dapm_new_control_unlocked(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_widget *widget);
该函数只是简单的一个循环,为传入的widget模板数组依次调用snd_soc_dapm_new_control函数,实际的工作由snd_soc_dapm_new_control完成,继续进入该函数,看看它做了那些工作。
我们之前已经说过,驱动中定义的snd_soc_dapm_widget数组,只是作为一个模板,所以,snd_soc_dapm_new_control_unlocked 所做的第一件事,就是为该widget重新分配内存,并把模板的内容拷贝过来
struct snd_soc_dapm_widget *
snd_soc_dapm_new_control_unlocked(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_widget *widget)
{
enum snd_soc_dapm_direction dir;
struct snd_soc_dapm_widget *w;
const char *prefix;
int ret;
if ((w = dapm_cnew_widget(widget)) == NULL) //dapm_cnew_widget完成内存申请和拷贝模板的动作
return NULL;
switch (w->id) {
case snd_soc_dapm_regulator_supply:
w->regulator = devm_regulator_get(dapm->dev, w->name);
if (IS_ERR(w->regulator)) {
ret = PTR_ERR(w->regulator);
if (ret == -EPROBE_DEFER)
return ERR_PTR(ret);
dev_err(dapm->dev, "ASoC: Failed to request %s: %dn",
w->name, ret);
return NULL;
}
if (w->on_val & SND_SOC_DAPM_REGULATOR_BYPASS) {
ret = regulator_allow_bypass(w->regulator, true);
if (ret != 0)
dev_warn(w->dapm->dev,
"ASoC: Failed to bypass %s: %dn",
w->name, ret);
}
break;
case snd_soc_dapm_pinctrl:
w->pinctrl = devm_pinctrl_get(dapm->dev);
if (IS_ERR(w->pinctrl)) {
ret = PTR_ERR(w->pinctrl);
.................
}
对于snd_soc_dapm_regulator_supply类型的widget,根据widget的名称获取对应的regulator结构,对于snd_soc_dapm_clock_supply类型的widget,根据widget的名称,获取对应的clock结构。接下来,根据需要,在widget的名称前加入必要的前缀:
if (dapm->codec && dapm->codec->name_prefix)
w->name = kasprintf(GFP_KERNEL, "%s %s",
dapm->codec->name_prefix, widget->name);
else
w->name = kasprintf(GFP_KERNEL, "%s", widget->name);
然后,为不同类型的widget设置合适的power_check电源状态回调函数,widget类型和对应的power_check回调函数设置如下表所示:
widget的power_check回调函数 (具体分析代码)
当音频路径发生变化时,power_check回调会被调用,用于检查该widget的电源状态是否需要更新。power_check设置完成后,需要设置widget所属的codec、platform和dapm context,几个用于音频路径的链表也需要初始化,然后,把该widget加入到声卡的widgets链表中:
w->dapm = dapm;
w->codec = dapm->codec;
w->platform = dapm->platform;
INIT_LIST_HEAD(&w->sources);
INIT_LIST_HEAD(&w->sinks);
INIT_LIST_HEAD(&w->list);
INIT_LIST_HEAD(&w->dirty);
list_add(&w->list, &dapm->card->widgets);
几个链表的作用如下:
sources 用于链接所有连接到该widget输入端的snd_soc_path结构
sinks 用于链接所有连接到该widget输出端的snd_soc_path结构
list 用于链接到声卡的widgets链表
dirty 用于链接到声卡的dapm_dirty链表
最后,把widget设置为connect状态:
/* machine layer set ups unconnected pins and insertions */
w->connected = 1;
return w;
}
connected字段代表着引脚的连接状态, 目前, 只有以下这些widget使用connected字段:
snd_soc_dapm_output
snd_soc_dapm_input
snd_soc_dapm_hp
snd_soc_dapm_spk
snd_soc_dapm_line
snd_soc_dapm_vmid
snd_soc_dapm_mic
snd_soc_dapm_siggen
驱动程序可以使用以下这些api来设置引脚的连接状态:
snd_soc_dapm_enable_pin
snd_soc_dapm_force_enable_pin
snd_soc_dapm_disable_pin
snd_soc_dapm_nc_pin
到此,widget已经被正确地创建并初始化,而且被挂在声卡的widgets链表中,以后我们就可以通过声卡的widgets链表来遍历所有的widget,再次强调一下snd_soc_dapm_new_controls函数所完成的主要功能:
//snd_soc_dapm_new_control_unlocked
为widget分配内存,并拷贝参数中传入的在驱动中定义好的模板
设置power_check回调函数
把widget挂在声卡的widgets链表中
为widget建立dapm kcontrol
定义一个widget,我们需要指定两个很重要的内容:一个是用于控制widget的电源状态的reg/shift等寄存器信息,另一个是用于控制音频路径切换的dapm kcontrol信息,这些dapm kcontrol有它们自己的reg/shift寄存器信息用于切换widget的路径连接方式。前一节的内容中,我们只是创建了widget的实例,并把它们注册到声卡的widgts链表中,但是到目前为止,包含在widget中的dapm kcontrol并没有建立起来,dapm框架在声卡的初始化阶段,等所有的widget(包括machine、platform、codec)都创建好之后,通过snd_soc_dapm_new_widgets函数,创建widget内包含的dapm kcontrol,并初始化widget的初始电源状态和音频路径的初始连接状态。我们看看声卡的初始化函数,都有那些初始化与dapm有关:
static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
struct snd_soc_pcm_runtime *rtd;
struct snd_soc_dai_link *dai_link;
int ret, i, order;
................
card->dapm.bias_level = SND_SOC_BIAS_OFF;
card->dapm.dev = card->dev;
card->dapm.card = card;
list_add(&card->dapm.list, &card->dapm_list);
#ifdef CONFIG_DEBUG_FS
snd_soc_dapm_debugfs_init(&card->dapm, card->debugfs_card_root);
#endif
#ifdef CONFIG_PM_SLEEP
/* deferred resume work */
INIT_WORK(&card->deferred_resume_work, soc_resume_deferred);
#endif
if (card->dapm_widgets) /* 创建machine级别的widget */
snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,
card->num_dapm_widgets);
if (card->of_dapm_widgets)
snd_soc_dapm_new_controls(&card->dapm, card->of_dapm_widgets,
card->num_of_dapm_widgets);
/* initialise the sound card only once */
if (card->probe) {
ret = card->probe(card);
if (ret < 0)
goto card_probe_error;
}
................
snd_soc_dapm_link_dai_widgets(card); /* 连接dai widget */
snd_soc_dapm_connect_dai_link_widgets(card);
if (card->controls) /* 建立machine级别的普通kcontrol控件 */
snd_soc_add_card_controls(card, card->controls, card->num_controls);
if (card->dapm_routes) /* 注册machine级别的路径连接信息 */
snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,
card->num_dapm_routes);
if (card->of_dapm_routes)
snd_soc_dapm_add_routes(&card->dapm, card->of_dapm_routes,
card->num_of_dapm_routes);
..............
snd_soc_dapm_new_widgets(card);/*初始化widget包含的dapm kcontrol、电源状态和连接状态*/
..........
}
正如我添加的注释中所示,在完成machine级别的widget和route处理之后,调用的snd_soc_dapm_new_widgets函数,来为所有已经注册的widget初始化他们所包含的dapm kcontrol,并初始化widget的电源状态和路径连接状态。下面我们看看snd_soc_dapm_new_widgets函数的工作过程。
该函数通过声卡的widgets链表,遍历所有已经注册了的widget,其中的new字段用于判断该widget是否已经执行过snd_soc_dapm_new_widgets函数,如果num_kcontrols字段有数值,表明该widget包含有若干个dapm kcontrol,那么就需要为这些kcontrol分配一个指针数组,并把数组的首地址赋值给widget的kcontrols字段,该数组存放着指向这些kcontrol的指针,当然现在这些都是空指针,因为实际的kcontrol现在还没有被创建:
int snd_soc_dapm_new_widgets(struct snd_soc_card *card)
{
struct snd_soc_dapm_widget *w;
unsigned int val;
mutex_lock_nested(&card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);
list_for_each_entry(w, &card->widgets, list)
{
if (w->new)
continue;
if (w->num_kcontrols) {
w->kcontrols = kcalloc(w->num_kcontrols,
sizeof(struct snd_kcontrol *),
GFP_KERNEL);
if (!w->kcontrols) {
mutex_unlock(&card->dapm_mutex);
return -ENOMEM;
}
}
//接着,对几种能影响音频路径的widget,创建并初始化它们所包含的dapm kcontrol:
switch(w->id) {
case snd_soc_dapm_switch:
case snd_soc_dapm_mixer:
case snd_soc_dapm_mixer_named_ctl:
dapm_new_mixer(w);
break;
case snd_soc_dapm_mux:
case snd_soc_dapm_demux:
dapm_new_mux(w);
break;
case snd_soc_dapm_pga:
case snd_soc_dapm_out_drv:
dapm_new_pga(w);
break;
case snd_soc_dapm_dai_link:
dapm_new_dai_link(w);
break;
default:
break;
}
/* Read the initial power state from the device */
if (w->reg >= 0) {//根据widget寄存器的当前值,初始化widget的电源状态,并设置到power字段中
soc_dapm_read(w->dapm, w->reg, &val);
val = val >> w->shift;
val &= w->mask;
if (val == w->on_val)
w->power = 1;
}
w->new = 1;//设置new字段,表明该widget已经初始化完成,我们还要吧该widget加入到声卡的dapm_dirty链表中,表明该widget的状态发生了变化,稍后在合适的时刻,dapm框架会扫描dapm_dirty 链表,统一处理所有已经变化的widget。为什么要统一处理?因为dapm要控制各种widget的上下电顺序,同时也是为了减少寄存器的读写次数(多个widget可能使用同一个寄存器):
dapm_mark_dirty(w, "new widget");
dapm_debugfs_add_widget(w);
}
dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);//通过dapm_power_widgets函数,统一处理所有位于dapm_dirty链表上的widget的状态改变
mutex_unlock(&card->dapm_mutex);
return 0;
}
需要用到的创建函数分别是:
dapm_new_mixer() 对于mixer类型,用该函数创建dapm kcontrol;
dapm_new_mux() 对于mux类型,用该函数创建dapm kcontrol;
dapm_new_pga() 对于pga类型,用该函数创建dapm kcontrol;
dapm mixer kcontrol
上一节中,我们提到,对于mixer类型的dapm kcontrol,我们会使用dapm_new_mixer来完成具体的创建工作,先看代码后分析:
/* create new dapm mixer control */
static int dapm_new_mixer(struct snd_soc_dapm_widget *w)
{
int i, ret;
struct snd_soc_dapm_path *path;
struct dapm_kcontrol_data *data;
/* add kcontrol */
(1) for (i = 0; i < w->num_kcontrols; i++) {
/* match name */
(2) snd_soc_dapm_widget_for_each_source_path(w, path) {
/* mixer/mux paths name must match control name */
(3) if (path->name != (char *)w->kcontrol_news[i].name)
continue;
(4) if (!w->kcontrols[i]) {
(5) ret = dapm_create_or_share_kcontrol(w, i);
if (ret < 0)
return ret;
}
(6) dapm_kcontrol_add_path(w->kcontrols[i], path);
data = snd_kcontrol_chip(w->kcontrols[i]);
if (data->widget)
snd_soc_dapm_add_path(data->widget->dapm,
data->widget,
path->source,
NULL, NULL);
}
}
return 0;
}
(1) 因为一个mixer是由多个kcontrol组成的,每个kcontrol控制着mixer的一个输入端的开启和关闭,所以,该函数会根据kcontrol的数量做循环,逐个建立对应的kcontrol。
(2)(3) 之前多次提到,widget之间使用snd_soc_path进行连接,widget的sources链表保存着所有和输入端连接的snd_soc_path结构,所以我们可以用kcontrol模板中指定的名字来匹配对应的snd_soc_path结构。
(4) 因为一个输入脚可能会连接多个输入源,所以可能在上一个输入源的path关联时已经创建了这个kcontrol,所以这里判断kcontrols指针数组中对应索引中的指针值,如果已经赋值,说明kcontrol已经在之前创建好了,所以我们只要简单地 把连接该输入端的path加入到kcontrol的path_list链表中,并且 增加一个虚拟的影子widget,该影子widget连接和输入端对应的源widget,因为使用了kcontrol本身的reg/shift等寄存器信息,所以实际上控制的是该kcontrol的开和关,这个影子widget只有在kcontrol的autodisable字段被设置的情况下才会被创建,该特性使得source的关闭时,与之连接的mixer的输入端也可以自动关闭,这个特性通过dapm_kcontrol_add_path来实现这一点:
static void dapm_kcontrol_add_path(const struct snd_kcontrol *kcontrol,
struct snd_soc_dapm_path *path)
{
struct dapm_kcontrol_data *data = snd_kcontrol_chip(kcontrol);
/* 把kcontrol连接的path加入到paths链表中 */
/* paths链表所在的dapm_kcontrol_data结构会保存在kcontrol的private_data字段中 */
list_add_tail(&path->list_kcontrol, &data->paths);
}
(5) 如果kcontrol之前没有被创建,则通过dapm_create_or_share_mixmux_kcontrol创建这个输入端的kcontrol,同理,kcontrol对应的影子widget也会通过dapm_kcontrol_add_path判断是否需要创建。
dapm mux kcontrol
因为一个widget最多只会包含一个mux类型的damp kcontrol,所以他的创建方法稍有不同,dapm框架使用dapm_new_mux函数来创建mux类型的dapm kcontrol:
/* create new dapm mux control */
static int dapm_new_mux(struct snd_soc_dapm_widget *w)
{
struct snd_soc_dapm_context *dapm = w->dapm;
enum snd_soc_dapm_direction dir;
struct snd_soc_dapm_path *path;
const char *type;
int ret;
switch (w->id) {
case snd_soc_dapm_mux:
dir = SND_SOC_DAPM_DIR_OUT;
type = "mux";
break;
case snd_soc_dapm_demux:
dir = SND_SOC_DAPM_DIR_IN;
type = "demux";
break;
default:
return -EINVAL;
}
(1) if (w->num_kcontrols != 1) {
dev_err(dapm->dev,
"ASoC: %s %s has incorrect number of controlsn", type,
w->name);
return -EINVAL;
}
if (list_empty(&w->edges[dir])) {
dev_err(dapm->dev, "ASoC: %s %s has no pathsn", type, w->name);
return -EINVAL;
}
(2) ret = dapm_create_or_share_kcontrol(w, 0);
if (ret < 0)
return ret;
(3) snd_soc_dapm_widget_for_each_path(w, dir, path) {
if (path->name)
dapm_kcontrol_add_path(w->kcontrols[0], path);
}
return 0;
}
(1) 对于mux类型的widget,因为只会有一个kcontrol,所以在这里做一下判断。
(2) 同样地,和mixer类型一样,也使用dapm_create_or_share_mixmux_kcontrol来创建这个kcontrol。
(3) 对每个输入端所连接的path都加入dapm_kcontrol_data结构的paths链表中,并且创建一个影子widget,用于支持autodisable特性。
dapm_create_or_share_kcontrol
上面所说的mixer类型和mux类型的widget,在创建他们所包含的dapm kcontrol时,最后其实都是使用了dapm_create_or_share_kcontrol函数来完成创建工作的,所以在这里我们有必要分析一下这个函数的工作原理。这个函数中有很大一部分代码实在处理kcontrol的名字是否要加入codec的前缀,我们会忽略这部分的代码,感兴趣的读者可以自己查看内核的代码,路径在:sound/soc/soc-dapm.c中,简化后的代码如下
/*
* Determine if a kcontrol is shared. If it is, look it up. If it isn't,
* create it. Either way, add the widget into the control's widget list
*/
static int dapm_create_or_share_kcontrol(struct snd_soc_dapm_widget *w,
int kci)
{
struct snd_soc_dapm_context *dapm = w->dapm;
struct snd_card *card = dapm->card->snd_card;
const char *prefix;
size_t prefix_len;
int shared;
struct snd_kcontrol *kcontrol;
bool wname_in_long_name, kcname_in_long_name;
char *long_name = NULL;
const char *name;
int ret = 0;
prefix = soc_dapm_prefix(dapm);
if (prefix)
prefix_len = strlen(prefix) + 1;
else
prefix_len = 0;
(1) shared = dapm_is_shared_kcontrol(dapm, w, &w->kcontrol_news[kci],
&kcontrol);
(2) if (!kcontrol) {
if (shared) {
wname_in_long_name = false;
kcname_in_long_name = true;
} else {
switch (w->id) {
case snd_soc_dapm_switch:
case snd_soc_dapm_mixer:
case snd_soc_dapm_pga:
case snd_soc_dapm_out_drv:
wname_in_long_name = true;
kcname_in_long_name = true;
break;
case snd_soc_dapm_mixer_named_ctl:
wname_in_long_name = false;
kcname_in_long_name = true;
break;
case snd_soc_dapm_demux:
case snd_soc_dapm_mux:
wname_in_long_name = true;
kcname_in_long_name = false;
break;
default:
return -EINVAL;
}
}
if (wname_in_long_name && kcname_in_long_name) {
/*
* The control will get a prefix from the control
* creation process but we're also using the same
* prefix for widgets so cut the prefix off the
* front of the widget name.
*/
long_name = kasprintf(GFP_KERNEL, "%s %s",
w->name + prefix_len,
w->kcontrol_news[kci].name);
if (long_name == NULL)
return -ENOMEM;
name = long_name;
} else if (wname_in_long_name) {
long_name = NULL;
name = w->name + prefix_len;
} else {
long_name = NULL;
name = w->kcontrol_news[kci].name;
}
(3) kcontrol = snd_soc_cnew(&w->kcontrol_news[kci], NULL, name,
prefix);
if (!kcontrol) {
ret = -ENOMEM;
goto exit_free;
}
kcontrol->private_free = dapm_kcontrol_free;
(4) ret = dapm_kcontrol_data_alloc(w, kcontrol, name);
if (ret) {
snd_ctl_free_one(kcontrol);
goto exit_free;
}
(5) ret = snd_ctl_add(card, kcontrol);
if (ret < 0) {
dev_err(dapm->dev,
"ASoC: failed to add widget %s dapm kcontrol %s: %dn",
w->name, name, ret);
goto exit_free;
}
}
(6) ret = dapm_kcontrol_add_widget(kcontrol, w);
if (ret == 0)
(7) w->kcontrols[kci] = kcontrol;
exit_free:
kfree(long_name);
return ret;
}
(1) 为了节省内存,通过kcontrol名字的匹配查找,如果这个kcontrol已经在其他widget中已经创建好了,那我们不再创建,dapm_is_shared_kcontrol的参数kcontrol会返回已经创建好的kcontrol的指针。
(2) 如果kcontrol指针被赋值,说明在(1)中查找到了其他widget中同名的kcontrol,我们不用再次创建,只要共享该kcontrol即可。
(3) 标准的kcontrol创建函数,请参看: Linux ALSA声卡驱动之四:Control设备的创建中的“创建control“一节的内容。
(4) 如果widget支持autodisable特性,创建与该kcontrol所对应的影子widget,该影子widget的类型是:snd_soc_dapm_kcontrol。
(5) 标准的kcontrol创建函数,请参看: Linux ALSA声卡驱动之四:Control设备的创建中的“创建control“一节的内容。
(6) 把所有共享该kcontrol的影子widget(snd_soc_dapm_kcontrol),加入到kcontrol的private_data字段所指向的dapm_kcontrol_data结构中。
(7) 把创建好的kcontrol指针赋值到widget的kcontrols数组中。
需要注意的是,如果kcontol支持autodisable特性,一旦kcontrol由于source的关闭而被自动关闭,则用户空间只能操作该kcontrol的cache值,只有该kcontrol再次打开时,该cache值才会被真正地更新到寄存器中。
现在。我们总结一下,创建一个widget所包含的kcontrol所做的工作:
循环每一个输入端,为每个输入端依次执行下面的一系列操作
为每个输入端创建一个kcontrol,能共享的则直接使用创建好的kcontrol
kcontrol的private_data字段保存着这些共享widget的信息
如果支持autodisable特性,每个输入端还要额外地创建一个虚拟的snd_soc_dapm_kcontrol类型的影子widget,该影子widget也记录在private_data字段中
创建好的kcontrol会依次存放在widget的kcontrols数组中,供路径的控制和匹配之用。
为widget建立连接关系
如果widget之间没有连接关系,dapm就无法实现动态的电源管理工作,正是widget之间有了连结关系,这些连接关系形成了一条所谓的完成的音频路径,dapm可以顺着这条路径,统一控制路径上所有widget的电源状态,前面我们已经知道,widget之间是使用snd_soc_path结构进行连接的,驱动要做的是定义一个snd_soc_route结构数组,该数组的每个条目描述了目的widget的和源widget的名称,以及控制这个连接的kcontrol的名称,最终,驱动程序使用api函数snd_soc_dapm_add_routes来注册这些连接信息,接下来我们就是要分析该函数的具体实现方式
/**
* snd_soc_dapm_add_routes - Add routes between DAPM widgets
* @dapm: DAPM context
* @route: audio routes
* @num: number of routes
*
* Connects 2 dapm widgets together via a named audio path. The sink is
* the widget receiving the audio signal, whilst the source is the sender
* of the audio signal.
*
* Returns 0 for success else error. On error all resources can be freed
* with a call to snd_soc_card_free().
*/
int snd_soc_dapm_add_routes(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_route *route, int num)
{
int i, r, ret = 0;
mutex_lock_nested(&dapm->card->dapm_mutex, SND_SOC_DAPM_CLASS_RUNTIME);
for (i = 0; i < num; i++) {
r = snd_soc_dapm_add_route(dapm, route);
if (r < 0) {
dev_err(dapm->dev, "ASoC: Failed to add route %s -> %s -> %sn",
route->source,
route->control ? route->control : "direct",
route->sink);
ret = r;
}
route++;
}
mutex_unlock(&dapm->card->dapm_mutex);
return ret;
}
该函数只是一个循环,依次对参数传入的数组调用snd_soc_dapm_add_route,主要的工作由snd_soc_dapm_add_route完成。我们进入snd_soc_dapm_add_route函数看看:
static int snd_soc_dapm_add_route(struct snd_soc_dapm_context *dapm,
const struct snd_soc_dapm_route *route)
{
struct snd_soc_dapm_widget *wsource = NULL, *wsink = NULL, *w;
struct snd_soc_dapm_widget *wtsource = NULL, *wtsink = NULL;
const char *sink;
const char *source;
char prefixed_sink[80];
char prefixed_source[80];
const char *prefix;
int ret;
prefix = soc_dapm_prefix(dapm);
if (prefix) {
snprintf(prefixed_sink, sizeof(prefixed_sink), "%s %s",
prefix, route->sink);
sink = prefixed_sink;
snprintf(prefixed_source, sizeof(prefixed_source), "%s %s",
prefix, route->source);
source = prefixed_source;
} else {
sink = route->sink;
source = route->source;
}
wsource = dapm_wcache_lookup(&dapm->path_source_cache, source);
wsink = dapm_wcache_lookup(&dapm->path_sink_cache, sink);
if (wsink && wsource)
goto skip_search;
/*
* find src and dest widgets over all widgets but favor a widget from
* current DAPM context
*/
list_for_each_entry(w, &dapm->card->widgets, list) {
if (!wsink && !(strcmp(w->name, sink))) {
wtsink = w;
if (w->dapm == dapm) {
wsink = w;
if (wsource)
break;
}
continue;
}
if (!wsource && !(strcmp(w->name, source))) {
wtsource = w;
if (w->dapm == dapm) {
wsource = w;
if (wsink)
break;
}
}
}
/* use widget from another DAPM context if not found from this */
if (!wsink)
wsink = wtsink;
if (!wsource)
wsource = wtsource;
if (wsource == NULL) {
dev_err(dapm->dev, "ASoC: no source widget found for %sn",
route->source);
return -ENODEV;
}
if (wsink == NULL) {
dev_err(dapm->dev, "ASoC: no sink widget found for %sn",
route->sink);
return -ENODEV;
}
skip_search:
dapm_wcache_update(&dapm->path_sink_cache, wsink);
dapm_wcache_update(&dapm->path_source_cache, wsource);
ret = snd_soc_dapm_add_path(dapm, wsource, wsink, route->control, //使用来增加一条连接信息:
route->connected);
if (ret)
goto err;
return 0;
err:
dev_warn(dapm->dev, "ASoC: no dapm match for %s --> %s --> %sn",
source, route->control, sink);
return ret;
}
list_for_each_entry 宏定义分析
/*
/**
* list_for_each_entry - iterate over list of given type
* @pos: the type * to use as a loop cursor.
* @head: the head for your list.
* @member: the name of the list_head within the struct.
*/
#define list_for_each_entry(pos, head, member)
for (pos = list_first_entry(head, typeof(*pos), member);
&pos->member != (head);
pos = list_next_entry(pos, member))
*/
/**
* list_first_entry - get the first element from a list
* @ptr: the list head to take the element from.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_head within the struct.
*
* Note, that list is expected to be not empty.
*/
#define list_first_entry(ptr, type, member)
list_entry((ptr)->next, type, member)
list_for_each_entry(w, &dapm->card->widgets, list)
#define list_for_each_entry(w, &dapm->card->widgets, list)
for (w = list_first_entry(&dapm->card->widgets, typeof(*w), list);
&w->list != (&dapm->card->widgets);
w = list_next_entry(w, list))
#define list_for_each_entry(w, &dapm->card->widgets, list)
for (w = container_of((&dapm->card->widgets)->next, snd_soc_dapm_widget, list);
&w->list != (&dapm->card->widgets);
w = list_next_entry(w, list))
*/
#define list_entry(ptr, type, member)
container_of(ptr, type, member)
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({
void *__mptr = (void *)(ptr);
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&
!__same_type(*(ptr), void),
"pointer type mismatch in container_of()");
((type *)(__mptr - offsetof(type, member))); })
container_of((&dapm->card->widgets)->next, snd_soc_dapm_widget, list);
#define container_of(ptr, type, member) ({
void *__mptr = (void *)(ptr);
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&
!__same_type(*(ptr), void),
"pointer type mismatch in container_of()");
((type *)(__mptr - offsetof(type, member))); })
/**
* BUILD_BUG_ON_MSG - break compile if a condition is true & emit supplied
* error message.
* @condition: the condition which the compiler should know is false.
*
* See BUILD_BUG_ON for description.
*/
#define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)
snd_soc_dapm_add_path函数是整个调用链条中的关键,我们来分析一下:
static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
struct snd_soc_dapm_widget *wsource, struct snd_soc_dapm_widget *wsink,
const char *control,
int (*connected)(struct snd_soc_dapm_widget *source,
struct snd_soc_dapm_widget *sink))
{
struct snd_soc_dapm_widget *widgets[2];
enum snd_soc_dapm_direction dir;
struct snd_soc_dapm_path *path;
int ret;
if (wsink->is_supply && !wsource->is_supply) {
dev_err(dapm->dev,
"Connecting non-supply widget to supply widget is not supported (%s -> %s)n",
wsource->name, wsink->name);
return -EINVAL;
}
if (connected && !wsource->is_supply) {
dev_err(dapm->dev,
"connected() callback only supported for supply widgets (%s -> %s)n",
wsource->name, wsink->name);
return -EINVAL;
}
if (wsource->is_supply && control) {
dev_err(dapm->dev,
"Conditional paths are not supported for supply widgets (%s -> [%s] -> %s)n",
wsource->name, control, wsink->name);
return -EINVAL;
}
ret = snd_soc_dapm_check_dynamic_path(dapm, wsource, wsink, control);
if (ret)
return ret;
path = kzalloc(sizeof(struct snd_soc_dapm_path), GFP_KERNEL);
if (!path)
return -ENOMEM;
path->node[SND_SOC_DAPM_DIR_IN] = wsource;
path->node[SND_SOC_DAPM_DIR_OUT] = wsink;
widgets[SND_SOC_DAPM_DIR_IN] = wsource;
widgets[SND_SOC_DAPM_DIR_OUT] = wsink;
path->connected = connected;
INIT_LIST_HEAD(&path->list);
INIT_LIST_HEAD(&path->list_kcontrol);
if (wsource->is_supply || wsink->is_supply)
path->is_supply = 1;
/* connect static paths */
if (control == NULL) {
path->connect = 1;
} else {
switch (wsource->id) {
case snd_soc_dapm_demux:
ret = dapm_connect_mux(dapm, path, control, wsource);
if (ret)
goto err;
break;
default:
break;
}
switch (wsink->id) {
case snd_soc_dapm_mux:
ret = dapm_connect_mux(dapm, path, control, wsink);
if (ret != 0)
goto err;
break;
case snd_soc_dapm_switch:
case snd_soc_dapm_mixer:
case snd_soc_dapm_mixer_named_ctl:
ret = dapm_connect_mixer(dapm, path, control);
if (ret != 0)
goto err;
break;
default:
break;
}
}
list_add(&path->list, &dapm->card->paths);
snd_soc_dapm_for_each_direction(dir)
list_add(&path->list_node[dir], &widgets[dir]->edges[dir]);
snd_soc_dapm_for_each_direction(dir) {
dapm_update_widget_flags(widgets[dir]);
dapm_mark_dirty(widgets[dir], "Route added");
}
if (dapm->card->instantiated && path->connect)
dapm_path_invalidate(path);
return 0;
err:
kfree(path);
return ret;
}
函数的一开始,首先为这个连接分配了一个snd_soc_path结构,path的source和sink字段分别指向源widget和目的widget,connected字段保存connected回调函数,初始化几个snd_soc_path结构中的几个链表。
/* connect static paths */
if (control == NULL) {
path->connect = 1;
} else {
switch (wsource->id) {
case snd_soc_dapm_demux:
ret = dapm_connect_mux(dapm, path, control, wsource);
if (ret)
goto err;
break;
default:
break;
}
switch (wsink->id) {
case snd_soc_dapm_mux:
ret = dapm_connect_mux(dapm, path, control, wsink);
if (ret != 0)
goto err;
break;
case snd_soc_dapm_switch:
case snd_soc_dapm_mixer:
case snd_soc_dapm_mixer_named_ctl:
ret = dapm_connect_mixer(dapm, path, control);
if (ret != 0)
goto err;
break;
default:
break;
}
}
按照目的widget来判断,如果属于以上这些类型,直接把它们连接在一起即可,目的widget如果是mixer和mux类型,分别用dapm_connect_mixer和dapm_connect_mux函数完成连接工作,这两个函数我们后面再讲。
/* widget has no PM register bit */
#define SND_SOC_NOPM -1
/* connect mux widget to its interconnecting audio paths */
static int dapm_connect_mux(struct snd_soc_dapm_context *dapm,
struct snd_soc_dapm_path *path, const char *control_name,
struct snd_soc_dapm_widget *w)
{
const struct snd_kcontrol_new *kcontrol = &w->kcontrol_news[0];
struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;
unsigned int val, item;
int i;
if (e->reg != SND_SOC_NOPM) {
soc_dapm_read(dapm, e->reg, &val);
val = (val >> e->shift_l) & e->mask;
item = snd_soc_enum_val_to_item(e, val);
} else {
/* since a virtual mux has no backing registers to
* decide which path to connect, it will try to match
* with the first enumeration. This is to ensure
* that the default mux choice (the first) will be
* correctly powered up during initialization.
*/
item = 0;
}
i = match_string(e->texts, e->items, control_name);
if (i < 0)
return -ENODEV;
path->name = e->texts[i];
path->connect = (i == item);
return 0;
}
声卡创建并初始化好了所需的widget,各个widget也通过path连接在了一起,接下来,dapm等待用户的指令,一旦某个dapm kcontrol被用户空间改变,利用这些连接关系,dapm会重新创建音频路径,脱离音频路径的widget会被下电,加入音频路径的widget会被上电,所有的上下电动作都会自动完成,用户空间的应用程序无需关注这些变化,它只管按需要改变某个dapm kcontrol即可。
设计dapm的主要目的之一,就是希望声卡上的各种部件的电源按需分配,需要的就上电,不需要的就下电,使得整个音频系统总是处于最小的耗电状态,最主要的就是,这一切对用户空间的应用程序是透明的,也就是说,用户空间的应用程序无需关心那个部件何时需要电源,它只要按需要设定好音频路径,播放音频数据,暂停或停止,dapm框架会根据音频路径,完美地对各种部件的电源进行控制,而且精确地按某种顺序进行,防止上下电过程中产生不必要的pop-pop声。
统计widget连接至端点widget的路径个数
端点widget位于音频路径的起始端或者末端,所以通常它们就是指codec的输入输出引脚所对应的widget,或者是外部器件对应的widget,这些widget的类型有以下这些
codec的输入输出引脚 snd_soc_dapm_output
snd_soc_dapm_input
外接的音频设备 snd_soc_dapm_hp
snd_soc_dapm_spk
snd_soc_dapm_line
snd_soc_dapm_mic
音频流(stream domain) snd_soc_dapm_adc
snd_soc_dapm_dac
snd_soc_dapm_aif_out
snd_soc_dapm_aif_in
snd_soc_dapm_dai_out
snd_soc_dapm_dai_in
电源、时钟 snd_soc_dapm_supply
snd_soc_dapm_regulator_supply
snd_soc_dapm_clock_supply
影子widget snd_soc_dapm_kcontrol
dapm要给一个widget上电的其中一个前提条件是:这个widget位于一条完整的音频路径上,而一条完整的音频路径的两头,必须是输入/输出引脚,或者是一个外部音频设备,又或者是一个处于激活状态的音频流widget,也就是上表中的前三项,上表中的后两项,它们可以位于路径的末端,但不是构成完成音频路径的必要条件,我们只用它来判断扫描一条路径的结束条件。dapm提供了两个内部函数,用来统计一个widget连接到输出引脚、输入引脚、激活的音频流widget的有效路径个数
is_connected_output_ep 返回连接至输出引脚或激活状态的输出音频流的路径数量
is_connected_input_ep 返回连接至输入引脚或激活状态的输入音频流的路径数量
dapm_dirty链表
在代表声卡的snd_soc_card结构中,有一个链表字段:dapm_dirty,所有状态发生了改变的widget,dapm不会立刻处理它的电源状态,而是需要先挂在该链表下面,等待后续的进一步处理:或者是上电,或者是下电。dapm为我们提供了一个api函数来完成这个动作:
void dapm_mark_dirty(struct snd_soc_dapm_widget *w, const char *reason)
{
if (!dapm_dirty_widget(w)) {
dev_vdbg(w->dapm->dev, "Marking %s dirty due to %sn",
w->name, reason);
list_add_tail(&w->dirty, &w->dapm->card->dapm_dirty);
}
}
power_check回调函数
建立widget之间的连接关系中,我们知道,在创建widget的时候,widget的power_check回调函数会根据widget的类型,设置不同的回调函数。当widget的状态改变后,dapm会遍历dapm_dirty链表,并通过power_check回调函数,决定该widget是否需要上电。大多数的widget的power_check回调被设置为:dapm_generic_check_power:
/* Generic check to see if a widget should be powered. */
static int dapm_generic_check_power(struct snd_soc_dapm_widget *w)
{
int in, out;
DAPM_UPDATE_STAT(w, power_checks);
in = is_connected_input_ep(w, NULL, NULL);
out = is_connected_output_ep(w, NULL, NULL);
return out != 0 && in != 0;
}
很简单,分别用is_connected_output_ep和is_connected_input_ep得到该widget是否有同时连接到一个输入端和一个输出端,如果是,返回1来表示该widget需要上电。
对于snd_soc_dapm_dai_out和snd_soc_dapm_dai_in类型,power_check回调是dapm_generic_check_power
widget的上电和下电顺序
在扫描dapm_dirty链表时,dapm使用两个链表来分别保存需要上电和需要下电的widget:
up_list 保存需要上电的widget
down_list 保存需要下电的widget
dapm内部使用dapm_seq_insert函数把一个widget加入到上述两个链表中的其中一个:
/* Insert a widget in order into a DAPM power sequence. */
static void dapm_seq_insert(struct snd_soc_dapm_widget *new_widget,
struct list_head *list,
bool power_up)
{
struct snd_soc_dapm_widget *w;
list_for_each_entry(w, list, power_list)
if (dapm_seq_compare(new_widget, w, power_up) < 0) {
list_add_tail(&new_widget->power_list, &w->power_list);
return;
}
list_add_tail(&new_widget->power_list, list);
}
上述函数会按照一定的顺序把widget加入到链表中,从而保证正确的上下电顺序:
上电顺序
static int dapm_up_seq[] = {
[snd_soc_dapm_pre] = 0,
[snd_soc_dapm_supply] = 1,
[snd_soc_dapm_regulator_supply] = 1,
[snd_soc_dapm_clock_supply] = 1,
[snd_soc_dapm_micbias] = 2,
[snd_soc_dapm_dai_link] = 2,
[snd_soc_dapm_dai_in] = 3,
[snd_soc_dapm_dai_out] = 3,
[snd_soc_dapm_aif_in] = 3,
[snd_soc_dapm_aif_out] = 3,
[snd_soc_dapm_mic] = 4,
[snd_soc_dapm_mux] = 5,
[snd_soc_dapm_virt_mux] = 5,
[snd_soc_dapm_value_mux] = 5,
[snd_soc_dapm_dac] = 6,
[snd_soc_dapm_switch] = 7,
[snd_soc_dapm_mixer] = 7,
[snd_soc_dapm_mixer_named_ctl] = 7,
[snd_soc_dapm_pga] = 8,
[snd_soc_dapm_adc] = 9,
[snd_soc_dapm_out_drv] = 10,
[snd_soc_dapm_hp] = 10,
[snd_soc_dapm_spk] = 10,
[snd_soc_dapm_line] = 10,
[snd_soc_dapm_kcontrol] = 11,
[snd_soc_dapm_post] = 12,
};
下电顺序
static int dapm_down_seq[] = {
[snd_soc_dapm_pre] = 0,
[snd_soc_dapm_kcontrol] = 1,
[snd_soc_dapm_adc] = 2,
[snd_soc_dapm_hp] = 3,
[snd_soc_dapm_spk] = 3,
[snd_soc_dapm_line] = 3,
[snd_soc_dapm_out_drv] = 3,
[snd_soc_dapm_pga] = 4,
[snd_soc_dapm_switch] = 5,
[snd_soc_dapm_mixer_named_ctl] = 5,
[snd_soc_dapm_mixer] = 5,
[snd_soc_dapm_dac] = 6,
[snd_soc_dapm_mic] = 7,
[snd_soc_dapm_micbias] = 8,
[snd_soc_dapm_mux] = 9,
[snd_soc_dapm_virt_mux] = 9,
[snd_soc_dapm_value_mux] = 9,
[snd_soc_dapm_aif_in] = 10,
[snd_soc_dapm_aif_out] = 10,
[snd_soc_dapm_dai_in] = 10,
[snd_soc_dapm_dai_out] = 10,
[snd_soc_dapm_dai_link] = 11,
[snd_soc_dapm_clock_supply] = 12,
[snd_soc_dapm_regulator_supply] = 12,
[snd_soc_dapm_supply] = 12,
[snd_soc_dapm_post] = 13,
};
widget的上下电过程
dapm_power_widgets
当一个widget的状态改变后,该widget会被加入dapm_dirty链表,然后通过dapm_power_widgets函数来改变整个音频路径上的电源状态,
/*
* Scan each dapm widget for complete audio path.
* A complete path is a route that has valid endpoints i.e.:-
*
* o DAC to output pin.
* o Input pin to ADC.
* o Input pin to Output pin (bypass, sidetone)
* o DAC to ADC (loopback).
*/
static int dapm_power_widgets(struct snd_soc_card *card, int event)
{
struct snd_soc_dapm_widget *w;
struct snd_soc_dapm_context *d;
LIST_HEAD(up_list);
LIST_HEAD(down_list);
ASYNC_DOMAIN_EXCLUSIVE(async_domain);
enum snd_soc_bias_level bias;
lockdep_assert_held(&card->dapm_mutex);
trace_snd_soc_dapm_start(card);
mutex_lock(&card->dapm_power_mutex);
list_for_each_entry(d, &card->dapm_list, list) {
if (dapm_idle_bias_off(d))
d->target_bias_level = SND_SOC_BIAS_OFF;
else
d->target_bias_level = SND_SOC_BIAS_STANDBY;
}
dapm_reset(card);
/* Check which widgets we need to power and store them in
* lists indicating if they should be powered up or down. We
* only check widgets that have been flagged as dirty but note
* that new widgets may be added to the dirty list while we
* iterate.
*/
list_for_each_entry(w, &card->dapm_dirty, dirty) {
dapm_power_one_widget(w, &up_list, &down_list);
}
list_for_each_entry(w, &card->widgets, list) {
switch (w->id) {
case snd_soc_dapm_pre:
case snd_soc_dapm_post:
/* These widgets always need to be powered */
break;
default:
list_del_init(&w->dirty);
break;
}
if (w->new_power) {
d = w->dapm;
/* Supplies and micbiases only bring the
* context up to STANDBY as unless something
* else is active and passing audio they
* generally don't require full power. Signal
* generators are virtual pins and have no
* power impact themselves.
*/
switch (w->id) {
case snd_soc_dapm_siggen:
case snd_soc_dapm_vmid:
break;
case snd_soc_dapm_supply:
case snd_soc_dapm_regulator_supply:
case snd_soc_dapm_pinctrl:
case snd_soc_dapm_clock_supply:
case snd_soc_dapm_micbias:
if (d->target_bias_level < SND_SOC_BIAS_STANDBY)
d->target_bias_level = SND_SOC_BIAS_STANDBY;
break;
default:
d->target_bias_level = SND_SOC_BIAS_ON;
break;
}
}
}
/* Force all contexts in the card to the same bias state if
* they're not ground referenced.
*/
bias = SND_SOC_BIAS_OFF;
list_for_each_entry(d, &card->dapm_list, list)
if (d->target_bias_level > bias)
bias = d->target_bias_level;
list_for_each_entry(d, &card->dapm_list, list)
if (!dapm_idle_bias_off(d))
d->target_bias_level = bias;
trace_snd_soc_dapm_walk_done(card);
/* Run card bias changes at first */
dapm_pre_sequence_async(&card->dapm, 0);
/* Run other bias changes in parallel */
list_for_each_entry(d, &card->dapm_list, list) {
if (d != &card->dapm)
async_schedule_domain(dapm_pre_sequence_async, d,
&async_domain);
}
async_synchronize_full_domain(&async_domain);
list_for_each_entry(w, &down_list, power_list) {
dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMD);
}
list_for_each_entry(w, &up_list, power_list) {
dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMU);
}
/* Power down widgets first; try to avoid amplifying pops. */
dapm_seq_run(card, &down_list, event, false);
dapm_widget_update(card);
/* Now power up. */
dapm_seq_run(card, &up_list, event, true);
/* Run all the bias changes in parallel */
list_for_each_entry(d, &card->dapm_list, list) {
if (d != &card->dapm)
async_schedule_domain(dapm_post_sequence_async, d,
&async_domain);
}
async_synchronize_full_domain(&async_domain);
/* Run card bias changes at last */
dapm_post_sequence_async(&card->dapm, 0);
/* do we need to notify any clients that DAPM event is complete */
list_for_each_entry(d, &card->dapm_list, list) {
if (d->stream_event)
d->stream_event(d, event);
}
pop_dbg(card->dev, card->pop_time,
"DAPM sequencing finished, waiting %dmsn", card->pop_time);
pop_wait(card->pop_time);
mutex_unlock(&card->dapm_power_mutex);
trace_snd_soc_dapm_done(card);
return 0;
}
可见,该函数通过遍历dapm_dirty链表,对每个链表中的widget调用dapm_power_one_widget,dapm_power_one_widget函数除了处理自身的状态改变外,还把自身的变化传递到和它相连的邻居widget中,结果就是,所有需要上电的widget会被放在up_list链表中,而所有需要下电的widget会被放在down_list链表中,这个函数我们稍后再讨论。
遍历down_list链表,向其中的widget发出SND_SOC_DAPM_WILL_PMD事件,感兴趣该事件的widget的event回调会被调用。
遍历up_list链表,向其中的widget发出SND_SOC_DAPM_WILL_PMU事件,感兴趣该事件的widget的event回调会被调用。
通过dapm_seq_run函数,处理down_list中的widget,使它们按定义好的顺序依次下电。
通过dapm_widget_update函数,切换触发该次状态变化的widget的kcontrol中的寄存器值,对应的结果就是:改变音频路径。
通过dapm_seq_run函数,处理up_list中的widget,使它们按定义好的顺序依次上电。
对每个dapm context发出状态改变回调。
适当的延时,防止pop-pop声。
dapm_power_one_widget
static void dapm_power_one_widget(struct snd_soc_dapm_widget *w,
struct list_head *up_list,
struct list_head *down_list)
{
int power;
switch (w->id) {
case snd_soc_dapm_pre:
dapm_seq_insert(w, down_list, false);
break;
case snd_soc_dapm_post:
dapm_seq_insert(w, up_list, true);
break;
default:
power = dapm_widget_power_check(w);
dapm_widget_set_power(w, power, up_list, down_list);
break;
}
}
dapm_power_widgets的第一步,就是遍历dapm_dirty链表,对每个链表中的widget调用dapm_power_one_widget,把需要上电和需要下电的widget分别加入到up_list和down_list链表中,同时,他还会把受到影响的邻居widget再次加入到dapm_dirty链表的末尾,通过这个动作,声卡中所以受到影响的widget都会被“感染”,依次被加到dapm_dirty链表,然后依次被执行dapm_power_one_widget函数。
通过dapm_widget_power_check,调用widget的power_check回调函数,获得该widget新的电源状态。
调用dapm_widget_set_power,“感染”与之相连的邻居widget。
遍历source widget,通过dapm_widget_set_peer_power函数,把处于连接状态的source widget加入dapm_dirty链表中。
遍历sink widget,通过dapm_widget_set_peer_power函数,把处于连接状态的sink widget加入dapm_dirty链表中。
根据第一步得到的新的电源状态,把widget加入到up_list或down_list链表中。
可见,通过该函数,一个widget的状态改变,邻居widget会受到“感染”而被加入到dapm_dirty链表的末尾,所以扫描到链表的末尾时,邻居widget也会执行同样的操作,从而“感染”邻居的邻居,直到没有新的widget被加入dapm_dirty链表为止,这时,所有受到影响的widget都被加入到up_list或down_li链表中,等待后续的上下电操作。
dapm_seq_run
static void dapm_seq_run(struct snd_soc_card *card,
struct list_head *list, int event, bool power_up)
{
struct snd_soc_dapm_widget *w, *n;
struct snd_soc_dapm_context *d;
LIST_HEAD(pending);
int cur_sort = -1;
int cur_subseq = -1;
int cur_reg = SND_SOC_NOPM;
struct snd_soc_dapm_context *cur_dapm = NULL;
int ret, i;
int *sort;
if (power_up)
sort = dapm_up_seq;
else
sort = dapm_down_seq;
list_for_each_entry_safe(w, n, list, power_list) {
ret = 0;
/* Do we need to apply any queued changes? */
if (sort[w->id] != cur_sort || w->reg != cur_reg ||
w->dapm != cur_dapm || w->subseq != cur_subseq) {
if (cur_dapm && !list_empty(&pending))
dapm_seq_run_coalesced(card, &pending);
if (cur_dapm && cur_dapm->seq_notifier) {
for (i = 0; i < ARRAY_SIZE(dapm_up_seq); i++)
if (sort[i] == cur_sort)
cur_dapm->seq_notifier(cur_dapm,
i,
cur_subseq);
}
if (cur_dapm && w->dapm != cur_dapm)
soc_dapm_async_complete(cur_dapm);
INIT_LIST_HEAD(&pending);
cur_sort = -1;
cur_subseq = INT_MIN;
cur_reg = SND_SOC_NOPM;
cur_dapm = NULL;
}
switch (w->id) {
case snd_soc_dapm_pre:
if (!w->event)
list_for_each_entry_safe_continue(w, n, list,
power_list);
if (event == SND_SOC_DAPM_STREAM_START)
ret = w->event(w,
NULL, SND_SOC_DAPM_PRE_PMU);
else if (event == SND_SOC_DAPM_STREAM_STOP)
ret = w->event(w,
NULL, SND_SOC_DAPM_PRE_PMD);
break;
case snd_soc_dapm_post:
if (!w->event)
list_for_each_entry_safe_continue(w, n, list,
power_list);
if (event == SND_SOC_DAPM_STREAM_START)
ret = w->event(w,
NULL, SND_SOC_DAPM_POST_PMU);
else if (event == SND_SOC_DAPM_STREAM_STOP)
ret = w->event(w,
NULL, SND_SOC_DAPM_POST_PMD);
break;
default:
/* Queue it up for application */
cur_sort = sort[w->id];
cur_subseq = w->subseq;
cur_reg = w->reg;
cur_dapm = w->dapm;
list_move(&w->power_list, &pending);
break;
}
if (ret < 0)
dev_err(w->dapm->dev,
"ASoC: Failed to apply widget power: %dn", ret);
}
if (cur_dapm && !list_empty(&pending))
dapm_seq_run_coalesced(card, &pending);
if (cur_dapm && cur_dapm->seq_notifier) {
for (i = 0; i < ARRAY_SIZE(dapm_up_seq); i++)
if (sort[i] == cur_sort)
cur_dapm->seq_notifier(cur_dapm,
i, cur_subseq);
}
list_for_each_entry(d, &card->dapm_list, list) {
soc_dapm_async_complete(d);
}
}
当所有需要上电或下电的widget都被加入到dapm_dirty链表后,接着会通过dapm_seq_run处理down_list链表上的widget,把该链表上的widget按顺序下电,然后通过dapm_widget_update更新widget中的kcontrol(这个kcontrol通常就是触发本次状态改变的触发源),接着又通过apm_seq_run处理up_list链表上的widget,把该链表上的widget按顺序上电。最终的上电或下电操作需要通过codec的寄存器来实现,因为定义widget时,如果这是一个带电源控制的widget,我们必须提供reg/shift等字段的设置值,如果该widget无需寄存器控制电源状态,则reg字段必须赋值为:
SND_SOC_NOPM (该宏定义的实际值是-1)
具体实现上,dapm框架使用了一点技巧:如果位于同一个上下电顺序的几个widget使用了同一个寄存器地址(一个寄存器可能使用不同的位来控制不同的widget的电源状态),dapm_seq_run通过dapm_seq_run_coalesced函数合并这几个widget的变更,然后只需要把合并后的值一次写入寄存器即可。