codec和platform分别对各自的设备构成和驱动进行描述,在ASoC中,需要将两者结合起来构成音频设备,machine class实现对如何将codec和platform结合构成音频设备进行描述,将不同部分(codec class和platform class)连接起来构成ALSA sound card device。其负责machine-specific controls和machine-level audio events的处理(比如在开始播放后打开放大器)。machine class drvier中涉及两个重要的结构体:strcut snd_soc_dai_link
,用于描述CPU DAI和codec DAI之间的连接,strcut snd_soc_card
用于表示最终组成的声卡,并将其注册到ALSA中。
platform和codec driver的代码是可以复用的,但是machine driver不是,具体的硬件特征是不可复用的,这主要是指codec DAI和platform DAI之间的连接。比如,通过GPIO打开放大器、通过GPIO检测音频设备插入、使用外部的OSC作为I2S总线的主时钟等等
1. Machine driver简介
machine driver负责的功能主要有:
- 根据开发板的硬件配置,填充
strcut snd_soc_dai_link
中对应的CPU和codec DAI;
- codec的时钟配置和codec的初始化;
- 补充DAPM widgets,从而完成DAPM链路的连接;
- 设定不同codec的具体工作参数,比如采样频率;
machine driver的开发从widgets、route的定义、dailink的配置到最后的声卡注册。
2. DAI Link以及Machine Class初始化
DAI Link用于表示CPU DAI和 codec DAI之间连接的逻辑关系,将codec和platform的DAI接口对应起来实现音频信号的完整传输。
2.1 snd_soc_dai_link数据含义
在内核中,使用strcut snd_soc_dai_link
表示。其定义如下
位于“include/sound/soc.h”中
struct snd_soc_dai_link {
const char *name;
const char *stream_name;
struct snd_soc_dai_link_component *cpus;
unsigned int num_cpus;
struct snd_soc_dai_link_component *codecs;
unsigned int num_codecs;
struct snd_soc_dai_link_component *platforms;
unsigned int num_platforms;
int id;
const struct snd_soc_pcm_stream *params;
unsigned int num_params;
unsigned int dai_fmt;
enum snd_soc_dpcm_trigger trigger[2];
int (*init)(struct snd_soc_pcm_runtime *rtd);
void (*exit)(struct snd_soc_pcm_runtime *rtd);
int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_hw_params *params);
const struct snd_soc_ops *ops;
const struct snd_soc_compr_ops *compr_ops;
unsigned int nonatomic:1;
unsigned int playback_only:1;
unsigned int capture_only:1;
unsigned int ignore_suspend:1;
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
unsigned int no_pcm:1;
unsigned int dynamic:1;
unsigned int dpcm_capture:1;
unsigned int dpcm_playback:1;
unsigned int dpcm_merged_format:1;
unsigned int dpcm_merged_chan:1;
unsigned int dpcm_merged_rate:1;
unsigned int ignore_pmdown_time:1;
unsigned int ignore:1;
#ifdef CONFIG_SND_SOC_TOPOLOGY
struct snd_soc_dobj dobj;
#endif
ANDROID_KABI_RESERVE(1);
};
上述数据结构中指定了cpu、codec相关的设备树节点信息、cpu和codec的驱动信息,从而与开发板的实际硬件连接相对应,实现开发板的音频输出功能。
在一般情况下,开发板都有指定CPU DAI(比如I2S控制器)和codec DAI(比如nau8825),DAI link应指定cpu_dai、codec_dai和对应的platform设备。machine driver将dai_link设定完成后,将strcut snd_soc_dai_link
变量传递给数据类型为strcut snd_soc_card
的声卡变量。
strcut snd_soc_dai_link
中的struct snd_soc_dai_link_component
成员cpus、codecs和platforms中存储与设备树相关的信息,用于寻找相应的设备配置。
位于“include/sound/soc.h”中
struct snd_soc_dai_link_component {
const char *name;
struct device_node *of_node;
const char *dai_name;
};
2.2 dai_link数据填充
在设备树文件中对相关的codec、cpu的设备节点定义如下
声卡设备节点
nau8822_sound: nau8822-sound {
status = "okay";
compatible = "rockchip,multicodecs-card";
rockchip,card-name = "rockchip-nau8822";
hp-det-gpio = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
io-channels = <&saradc 3>;
io-channel-names = "adc-detect";
keyup-threshold-microvolt = <1800000>;
poll-interval = <100>;
rockchip,format = "i2s";
rockchip,mclk-fs = <256>;
rockchip,cpu = <&i2s0_8ch>;
rockchip,codec = <&nau8822>;
rockchip,audio-routing =
"Headphone", "RHP",
"Headphone", "LHP",
"Speaker", "LSPK",
"Speaker", "RSPK",
"RMICP", "Main Mic",
"RMICN", "Main Mic",
"RMICP", "Headset Mic",
"RMICN", "Headset Mic",
"LMICP", "Main Mic",
"LMICN", "Main Mic",
"LMICP", "Headset Mic",
"LMICN", "Headset Mic";
pinctrl-names = "default";
pinctrl-0 = <&hp_det>;
play-pause-key {
label = "playpause";
linux,code = <KEY_PLAYPAUSE>;
press-threshold-microvolt = <2000>;
poll-interval = <1000>;
};
};
codec设备节点
&i2c3 {
status = "okay";
nau8822: nau8822@1a {
status = "okay";
#sound-dai-cells = <0>;
compatible = "nuvoton,nau8822";
reg = <0x1a>;
clocks = <&mclkout_i2s0>;
clock-names = "mclk";
assigned-clocks = <&mclkout_i2s0>;
assigned-clock-rates = <12288000>;
pinctrl-names = "default";
pinctrl-0 = <&i2s0_mclk>;
};
};
platform设备节点
位于rk3588s.dts中
i2s0_8ch: i2s@fe470000 {
compatible = "rockchip,rk3588-i2s-tdm";
reg = <0x0 0xfe470000 0x0 0x1000>;
interrupts = <GIC_SPI 180 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru MCLK_I2S0_8CH_TX>, <&cru MCLK_I2S0_8CH_RX>, <&cru HCLK_I2S0_8CH>;
clock-names = "mclk_tx", "mclk_rx", "hclk";
assigned-clocks = <&cru CLK_I2S0_8CH_TX_SRC>, <&cru CLK_I2S0_8CH_RX_SRC>;
assigned-clock-parents = <&cru PLL_AUPLL>, <&cru PLL_AUPLL>;
dmas = <&dmac0 0>, <&dmac0 1>;
dma-names = "tx", "rx";
power-domains = <&power RK3588_PD_AUDIO>;
resets = <&cru SRST_M_I2S0_8CH_TX>, <&cru SRST_M_I2S0_8CH_RX>;
reset-names = "tx-m", "rx-m";
rockchip,clk-trcm = <1>;
i2s-lrck-gpio = <&gpio1 RK_PC5 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default", "idle", "clk";
pinctrl-0 = <&i2s0_sdi0
&i2s0_sdi1
&i2s0_sdi2
&i2s0_sdi3
&i2s0_sdo0
&i2s0_sdo1>;
pinctrl-1 = <&i2s0_idle>;
pinctrl-2 = <&i2s0_lrck
&i2s0_sclk>;
#sound-dai-cells = <0>;
status = "disabled";
};
通过调用of_parse_phandle
设备树帮助函数,获取对应的设备在设备树中的phandle。可以填充
snd_soc_dai_link_component
中的of_node
成员。
elf2位于“sound/soc/rockchip/multicodecs.c”中,通过调用of_parse_phandle
函数,从而获得对应的device tree中的设备属性节点。
static int rk_multicodecs_probe(struct platform_device *pdev)
{
link->codecs = codecs;
link->num_codecs = idx;
idx = 0;
for (i = 0; i < count; i++) {
node = of_parse_phandle(np, "rockchip,codec", i);
if (!node)
return -ENODEV;
if (!of_device_is_available(node))
continue;
ret = of_parse_phandle_with_fixed_args(np, "rockchip,codec",
0, i, &args);
if (ret)
return ret;
codecs[idx].of_node = node;
ret = snd_soc_get_dai_name(&args, &codecs[idx].dai_name);
if (ret)
return ret;
idx++;
}
rk_multicodecs_parse_daifmt(np, codecs[0].of_node, mc_data, prefix);
link->cpus->of_node = of_parse_phandle(np, "rockchip,cpu", 0);
if (!link->cpus->of_node)
return -ENODEV;
link->platforms->of_node = link->cpus->of_node;
}
3 Machine routing 设计
Machine driver中结合Codec class driver和Platform class driver中对DAPM的route设置,实现完整音频链路的描述。
3.1 Codec Pin
Codec Pin表示Codec连接到开发板上的耳机、喇叭等设备的引脚。这些引脚属于音频链路的末端,这些引脚在codec driver中使用SND_SOC_DAPM_INPUT
和SND_SOC_DAPM_OUTPUT
宏来进行定义。elf2开发板对应的nau8822的引脚定义如下。
位于“sound/soc/codecs/nau8822.c”中
static const struct snd_soc_dapm_widget nau8822_dapm_widgets[] = {
SND_SOC_DAPM_DAC("Left DAC", "Left HiFi Playback",
NAU8822_REG_POWER_MANAGEMENT_3, 0, 0),
SND_SOC_DAPM_DAC("Right DAC", "Right HiFi Playback",
NAU8822_REG_POWER_MANAGEMENT_3, 1, 0),
SND_SOC_DAPM_ADC("Left ADC", "Left HiFi Capture",
NAU8822_REG_POWER_MANAGEMENT_2, 0, 0),
SND_SOC_DAPM_ADC("Right ADC", "Right HiFi Capture",
NAU8822_REG_POWER_MANAGEMENT_2, 1, 0),
SOC_MIXER_ARRAY("Left Output Mixer",
NAU8822_REG_POWER_MANAGEMENT_3, 2, 0, nau8822_left_out_mixer),
SOC_MIXER_ARRAY("Right Output Mixer",
NAU8822_REG_POWER_MANAGEMENT_3, 3, 0, nau8822_right_out_mixer),
SOC_MIXER_ARRAY("AUX1 Output Mixer",
NAU8822_REG_POWER_MANAGEMENT_1, 7, 0, nau8822_auxout1_mixer),
SOC_MIXER_ARRAY("AUX2 Output Mixer",
NAU8822_REG_POWER_MANAGEMENT_1, 6, 0, nau8822_auxout2_mixer),
SOC_MIXER_ARRAY("Left Input Mixer",
NAU8822_REG_POWER_MANAGEMENT_2,
2, 0, nau8822_left_input_mixer),
SOC_MIXER_ARRAY("Right Input Mixer",
NAU8822_REG_POWER_MANAGEMENT_2,
3, 0, nau8822_right_input_mixer),
SND_SOC_DAPM_PGA("Left Boost Mixer",
NAU8822_REG_POWER_MANAGEMENT_2, 4, 0, NULL, 0),
SND_SOC_DAPM_PGA("Right Boost Mixer",
NAU8822_REG_POWER_MANAGEMENT_2, 5, 0, NULL, 0),
SND_SOC_DAPM_PGA("Left Capture PGA",
NAU8822_REG_LEFT_INP_PGA_CONTROL, 6, 1, NULL, 0),
SND_SOC_DAPM_PGA("Right Capture PGA",
NAU8822_REG_RIGHT_INP_PGA_CONTROL, 6, 1, NULL, 0),
SND_SOC_DAPM_PGA("Left Headphone Out",
NAU8822_REG_POWER_MANAGEMENT_2, 7, 0, NULL, 0),
SND_SOC_DAPM_PGA("Right Headphone Out",
NAU8822_REG_POWER_MANAGEMENT_2, 8, 0, NULL, 0),
SND_SOC_DAPM_PGA("Left Speaker Out",
NAU8822_REG_POWER_MANAGEMENT_3, 6, 0, NULL, 0),
SND_SOC_DAPM_PGA("Right Speaker Out",
NAU8822_REG_POWER_MANAGEMENT_3, 5, 0, NULL, 0),
SND_SOC_DAPM_PGA("AUX1 Out",
NAU8822_REG_POWER_MANAGEMENT_3, 8, 0, NULL, 0),
SND_SOC_DAPM_PGA("AUX2 Out",
NAU8822_REG_POWER_MANAGEMENT_3, 7, 0, NULL, 0),
SND_SOC_DAPM_SUPPLY("PLL",
NAU8822_REG_POWER_MANAGEMENT_1, 5, 0, NULL, 0),
SND_SOC_DAPM_SWITCH("Digital Loopback", SND_SOC_NOPM, 0, 0,
&nau8822_loopback),
SND_SOC_DAPM_INPUT("LMICN"),
SND_SOC_DAPM_INPUT("LMICP"),
SND_SOC_DAPM_INPUT("RMICN"),
SND_SOC_DAPM_INPUT("RMICP"),
SND_SOC_DAPM_INPUT("LAUX"),
SND_SOC_DAPM_INPUT("RAUX"),
SND_SOC_DAPM_INPUT("L2"),
SND_SOC_DAPM_INPUT("R2"),
SND_SOC_DAPM_OUTPUT("LHP"),
SND_SOC_DAPM_OUTPUT("RHP"),
SND_SOC_DAPM_OUTPUT("LSPK"),
SND_SOC_DAPM_OUTPUT("RSPK"),
SND_SOC_DAPM_OUTPUT("AUXOUT1"),
SND_SOC_DAPM_OUTPUT("AUXOUT2"),
};
3.2 Board Connectors
用于注册的声卡strcut snd_soc_card
中的struct snd_soc_dapm_widget
中定义相关的board connectors。这些引脚为板卡上的MIC、Speaker、LineIn的引脚,在现实中,这些引脚一般与codec上的引脚相连,由于machine class是针对开发板进行描述,所以在machine中定义这些引脚,完成整个音频链路。对于elf2开发板,相关的定义如下
位于“sound/soc/rockchip/rockchip_multicodecs.c”
static const struct snd_soc_dapm_widget mc_dapm_widgets[] = {
SND_SOC_DAPM_HP("Headphone", NULL),
SND_SOC_DAPM_SPK("Speaker", NULL),
SND_SOC_DAPM_MIC("Main Mic", NULL),
SND_SOC_DAPM_MIC("Headset Mic", NULL),
SND_SOC_DAPM_SUPPLY("Speaker Power",
SND_SOC_NOPM, 0, 0,
mc_spk_event,
SND_SOC_DAPM_POST_PMU |
SND_SOC_DAPM_PRE_PMD),
SND_SOC_DAPM_SUPPLY("Headphone Power",
SND_SOC_NOPM, 0, 0,
mc_hp_event,
SND_SOC_DAPM_POST_PMU |
SND_SOC_DAPM_PRE_PMD),
};
3.3 Machine routing
对codec和machine的相关引脚完成定义后。通过调用snd_soc_of_parse_audio_routing()
,可以将Device Tree中声卡的"audio-route"属性解析为音频子系统的audio power map,存储在struct snd_soc_dapm_route
中,elf2中的调用为。
位于“sound/soc/rockchip/rockchip_multicodecs.c”
snd_soc_of_parse_audio_routing(card, "rockchip,audio-routing");
4 Clocking and formatting 设计,时钟和音频格式设置
struct snd_soc_ops
是ASoC中定义回调函数的接口,在DAI-Link的结构体struct snd_soc_dai_link
中,这些回调函数表示machine-level的PCM操作,而在struct snd_soc_dai_driver
中表示codec-dai或cpu-dai的相关操作。
位于“include/sound/soc.h”中
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);
};
其中startup
在打开playback或capture设备时调用,hw_params()
在建立音频流时调用。Machine driver可以在两者调用时配置DAI link的数据格式。hw_params()
可以接收音频流的相关参数(比如,channel count、format、sample rate等),因此更适合进行配置。
CPU DAI和codec DAI间的数据格式配置应当保持一致,ASoC Core提供了相关的辅助函数用于修改相关的配置。这些函数原型为:
int snd_soc_dai_set_fmt(struct snd_soc_dai *dai,unsigned int fmt);
int snd_soc_dai_set_pll(struct snd_soc_dai *dai, int pll_id, int source,unsigned int freq_in, unsigned int freq_out);
int snd_soc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id,unsigned int freq, int dir);
int snd_soc_dai_set_clkdiv(struct snd_soc_dai *dai,int div_id, int div);
以上函数会根据输入参数中的dai的值不同,从而调用codec或者platform中对应的回调函数,比如
static int rk_multicodecs_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
struct snd_soc_dai *codec_dai;
struct multicodecs_data *mc_data = snd_soc_card_get_drvdata(rtd->card);
unsigned int mclk;
int ret, i;
mclk = params_rate(params) * mc_data->mclk_fs;
for_each_rtd_codec_dais(rtd, i, codec_dai) {
ret = snd_soc_dai_set_sysclk(codec_dai, substream->stream, mclk,
SND_SOC_CLOCK_IN);
if (ret && ret != -ENOTSUPP) {
pr_err("Set codec_dai sysclk failed: %d\n", ret);
goto out;
}
}
ret = snd_soc_dai_set_sysclk(cpu_dai, substream->stream, mclk,
SND_SOC_CLOCK_OUT);
if (ret && ret != -ENOTSUPP) {
pr_err("Set cpu_dai sysclk failed: %d\n", ret);
goto out;
}
return 0;
out:
return ret;
}
5 声卡的注册
在完成struct snd_soc_card
中相关成员的赋值后,调用devm_snd_soc_register_card()
将声卡注册到ALSA中,该调用在machine class driver的.probe
函数中,在machine class driver所在的模块加载时完成声卡注册,为后续的音频应用提供服务。
static int rk_multicodecs_probe(struct platform_device *pdev)
{
struct snd_soc_card *card;
snd_soc_card_set_drvdata(card, mc_data);
platform_set_drvdata(pdev, card);
ret = devm_snd_soc_register_card(&pdev->dev, card);
if (ret) {
dev_err(&pdev->dev, "Failed to register card: %d\n", ret);
return ret;
}
irq = gpiod_to_irq(mc_data->hp_det_gpio);
if (irq >= 0)
queue_delayed_work(system_power_efficient_wq,
&mc_data->handler, msecs_to_jiffies(50));
return ret;
}