Skip to content

Latest commit

 

History

History
615 lines (495 loc) · 33.8 KB

File metadata and controls

615 lines (495 loc) · 33.8 KB

六、ALSA SoC 框架——深入研究机器类驱动

在开始我们的 ALSA SoC 框架系列时,我们注意到平台和编解码器类驱动都不打算单独工作。 ASOC 架构的设计使得平台和编解码器类驱动必须绑定在一起才能构建音频设备。 此绑定可以从所谓的机器驱动或从设备树内完成,每种绑定都是特定于机器的。 不用说,机器驱动的目标是特定的系统,它可能会从一块板换到另一块板。 在本章中,我们将重点介绍 ASOC 机器类驱动的阴暗面,并讨论我们需要编写机器类驱动时可能遇到的具体情况。

在本章中,我们将介绍 Linux ASOC 驱动的体系结构和实现。 本章将分为不同的部分,具体如下:

  • 机器类驱动简介
  • 机器布线注意事项
  • 计时和格式化注意事项
  • 声卡注册
  • 利用简单卡片机驱动

技术要求

本章需要以下内容:

。 机器类驱动简介

编解码器和平台驱动不能单独工作。 机器驱动负责将它们绑定在一起,以完成音频信息处理。 机器驱动类充当描述其他组件驱动并将其捆绑在一起以形成 ALSA 声卡设备的粘合剂。 它管理任何特定于机器的控制和机器级别的音频事件(例如在播放开始时打开放大器)。 机器驱动描述并绑定 CPU数字音频接口(DAIS)和编解码器驱动,以创建 DAI 链路和 ALSA 声卡。 机器驱动通过链接第 5 章,ALSA SoC Framework-利用编解码器和平台类驱动中描述的每个模块(CPU 和编解码器)公开的 DAI 来连接编解码器驱动。 它定义struct snd_soc_dai_link结构并实例化声卡struct snd_soc_card

平台和编解码器驱动通常是可重用的,但机器驱动不是可重用的,因为它们具有大多数时间不可重用的特定硬件功能。 所谓硬件特性是指 DAI 之间的链接;通过 GPIO 打开放大器;通过 GPIO 检测插件;使用 MCLK/外部 OSC 等时钟作为 I2 的参考时钟源;编解码器模块等。 一般而言,机器司机的职责包括以下内容:

  • 使用适当的 CPU 和编解码器 DAI 填充struct snd_soc_dai_link结构
  • 物理编解码器时钟设置(如果有)和编解码器初始化主/从配置(如果有)
  • 定义 DAPM 小部件以通过物理编解码器内部进行路由,并根据需要完成 DAPM 路径
  • 根据需要将运行时采样频率传播到各个编解码器驱动器

总而言之,我们有以下流程:

  1. 编解码器驱动注册组件驱动、DAI 驱动及其操作功能。
  2. 平台驱动注册组件驱动、PCM 驱动、CPU DAI 驱动及其操作功能,并根据需要设置回放和捕获操作。
  3. 机器层在编解码器和 CPU 之间创建 DAI 链路,并注册声卡和 PCM 设备。

现在我们已经看到了机器类驱动的开发流程,让我们从第一步开始,它包括填充代林 k。

DAI 链接

DAI 链路是 CPU 和编解码器 DAI 之间链路的逻辑表示。 它在内核中使用struct snd_soc_dai_link表示,定义如下:

struct snd_soc_dai_link {
    const char *name;
    const char *stream_name;
    const char *cpu_name;
    struct device_node *cpu_of_node;
    const char *cpu_dai_name;
    const char *codec_name;
    struct device_node *codec_of_node;
    const char *codec_dai_name;
    struct snd_soc_dai_link_component *codecs;
    unsigned int num_codecs;
    const char *platform_name;
    struct device_node *platform_of_node;
    int id;
    const struct snd_soc_pcm_stream *params;
    unsigned int num_params;
    unsigned int dai_fmt;
    enum snd_soc_dpcm_trigger trigger[2];
  /* codec/machine specific init - e.g. add machine controls */
    int (*init)(struct snd_soc_pcm_runtime *rtd);
    /* machine stream operations */
    const struct snd_soc_ops *ops;
    /* For unidirectional dai links */
    unsigned int playback_only:1;
    unsigned int capture_only:1;
    /* Keep DAI active over suspend */
    unsigned int ignore_suspend:1;
[...]
    /* DPCM capture and Playback support */
    unsigned int dpcm_capture:1;
    unsigned int dpcm_playback:1;
    struct list_head list; /* DAI link list of the soc card */
};

重要音符

完整的snd_soc_dai_link数据结构定义可以在https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc.h#L880中找到。

该链接是从机器驱动内设置的。 它应该指定cpu_daicodec_dai和使用的平台。 一旦设置好,DAI 链路就会馈送到struct snd_soc_cardstruct snd_soc_card表示声卡。 下面的列表描述了结构中的元素:

  • name:这是任意选择的。 它可以是任何东西。
  • codec_dai_name:这必须与编解码器芯片驱动中的snd_soc_dai_driver.name字段匹配。 编解码器可以有一个或多个 DAI。 请参阅编解码器驱动以标识 DAI 名称。
  • cpu_dai_name:这必须与 CPU DAI 驱动中的snd_soc_dai_driver.name字段匹配。
  • stream_name:这是此链接的流名称。
  • init:这是 DAI 链接初始化回调。 它通常用于添加 DAI 链接特定的小部件或其他类型的一次性设置。
  • dai_fmt:应使用支持的格式和时钟配置进行设置,这对于 CPU 和编解码器 DAI 驱动都应该是一致的。 稍后将介绍该字段的可能位标志。
  • ops:此字段为struct snd_soc_ops类型。 应设置 DAI 链路的机器级 PCM 操作:startuphw_paramspreparetriggerhw_freeshutdown。 此字段将在稍后详细介绍。
  • codec_name:如果设置,这应该是编解码器驱动的名称,例如platform_driver.driver.namei2c_driver.driver.name
  • codec_of_node:与编解码器关联的设备树节点。
  • cpu_name:如果设置,这应该是 CPU DAI 驱动 CPU 的名称。
  • cpu_of_node:这是与 CPU DAI 关联的设备树节点。
  • platform_nameplatform_of_node:这是对提供 DMA 功能的平台节点的名称或 dt 节点引用。
  • 在诸如 SPDIF 的单向链路的情况下使用playback_onlycapture_only。 如果这是仅输出链接(仅播放),则playback_onlycapture_only必须分别设置为truefalse。 对于仅限输入的链接,应该使用相反的值。

在大多数情况下,.cpu_of_node.platform_of_node是相同的,因为 CPU DAI 驱动和 DMA PCM 驱动是由同一设备实现的。 也就是说,您必须按名称或按of_node指定链接的编解码器,但不能同时指定两者。 您必须对 CPU 和平台执行相同的操作。 但是,必须至少指定 CPU DAI 名称或 CPU 设备名称/节点中的一个。 这可以总结如下:

if (link->platform_name && link->platform_of_node)
    ==> Error
if (link->cpu_name && link->cpu_of_node)
    ==> Eror
if (!link->cpu_dai_name && !(link->cpu_name ||                              link->cpu_of_node))
    ==> Error

这里有一个关键点值得注意。 如何引用 DAI 链路中的平台或 CPU 节点? 我们稍后会回答这个问题。 让我们首先考虑以下两个设备节点。 第一个(ssi1)是 i.MX6 SoC 的 SSIcpu-dai节点。 第二个节点(sgtl5000)表示 SGTL5000 编解码器芯片:

ssi1: ssi@2028000 {
    #sound-dai-cells = <0>;
    compatible = "fsl,imx6q-ssi", "fsl,imx51-ssi";
    reg = <0x02028000 0x4000>;
    interrupts = <0 46 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6QDL_CLK_SSI1_IPG>,
             <&clks IMX6QDL_CLK_SSI1>;
    clock-names = "ipg", "baud";
    dmas = <&sdma 37 1 0>, <&sdma 38 1 0>;
    dma-names = "rx", "tx";
    fsl,fifo-depth = <15>;
    status = "disabled";
};
&i2c0{
    sgtl5000: codec@0a {
        compatible = "fsl,sgtl5000";
        #sound-dai-cells = <0>;
        reg = <0x0a>;
        clocks = <&audio_clock>;
        VDDA-supply = <&reg_3p3v>;
        VDDIO-supply = <&reg_3p3v>;
        VDDD-supply = <&reg_1p5v>;
    };
};

重要音符

在 SSI 节点中,您可以看到dma-names = "rx", "tx";属性,它是 pcmdmaengine 框架请求的预期 DMA 通道名称。 这也可能表示 CPU DAI 和平台 PCM 由同一节点表示。

我们将考虑一个将 i.MX6 SoC 连接到 SGTL5000 音频编解码器的系统。 机器驱动通常通过引用这些节点(实际上是它们的phandle)来获取 CPU 或编解码器设备树节点作为其属性。 这样,您就可以只使用其中一个OF帮助器(如of_parse_phandle())来获取这些节点上的引用。 以下是通过OF节点引用编解码器和平台的机器节点示例:

sound {
    compatible = "fsl,imx51-babbage-sgtl5000",
                 "fsl,imx-audio-sgtl5000";
    model = "imx51-babbage-sgtl5000";
    ssi-controller = <&ssi1>;
    audio-codec = <&sgtl5000>;
    [...]
};

在前面的机器节点中,编解码器和 CPUE 通过audio-codecssi-controller属性通过引用(它们的phandle)传递。 只要机器驱动是您编写的,这些属性名称就不是标准化的(例如,如果您使用simple-card机器驱动,它需要一些预定义的名称,则不是这样)。 在机器驱动中,您将看到如下所示:

static int imx_sgtl5000_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct device_node *ssi_np, *codec_np;
    struct imx_sgtl5000_data *data = NULL;
    int int_port, ext_port; int ret;
[...]
    ssi_np = of_parse_phandle(pdev->dev.of_node,                               "ssi-controller", 0);
    codec_np = of_parse_phandle(pdev->dev.of_node,                                 "audio-codec", 0);
    if (!ssi_np || !codec_np) {
        dev_err(&pdev->dev, "phandle missing or invalid\n");
        ret = -EINVAL;
        goto fail;
    }
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data) {
        ret = -ENOMEM;
       goto fail;
    }
    data->dai.name = "HiFi";
    data->dai.stream_name = "HiFi";
    data->dai.codec_dai_name = "sgtl5000";
    data->dai.codec_of_node = codec_np;
    data->dai.cpu_of_node = ssi_np;
    data->dai.platform_of_node = ssi_np;
    data->dai.init = &imx_sgtl5000_dai_init;
    data->card.dev = &pdev->dev;
    [...]
};

前面的摘录使用of_parse_phandle()获取节点引用。 这是imx_sgtl5000机器的摘录,在内核源代码中是sound/soc/fsl/imx-sgtl5000.c。 现在我们已经熟悉了 DAI 链路的处理方式,我们可以继续从机器驱动内部进行音频路由,以便定义音频数据应该遵循的路径。

机器布线注意事项

机器驱动可以从编解码器中更改(或者应该说附加)定义的路由。 例如,它具有编解码器引脚必须使用 d 的最终决定权。

编解码器引脚

编解码器引脚用于连接到主板连接器。 可用编解码器引脚在编解码器驱动中使用SND_SOC_DAPM_INPUTSND_SOC_DAPM_OUTPUT宏定义。 可以使用编解码器驱动中的grep命令搜索这些宏,以便找到可用的 PIN。

例如,sgtl5000编解码器驱动定义了以下输出和输入:

static const struct snd_soc_dapm_widget sgtl5000_dapm_widgets[] = {
    SND_SOC_DAPM_INPUT("LINE_IN"),
    SND_SOC_DAPM_INPUT("MIC_IN"),
    SND_SOC_DAPM_OUTPUT("HP_OUT"),
    SND_SOC_DAPM_OUTPUT("LINE_OUT"),
    SND_SOC_DAPM_SUPPLY("Mic Bias", SGTL5000_CHIP_MIC_CTRL, 8,                         0,
                        mic_bias_event,
                        SND_SOC_DAPM_POST_PMU |                         SND_SOC_DAPM_PRE_PMD),
[...]
};

在接下来的几节中,我们将看到这些管脚是如何连接到电路板上的。

板卡接头

电路板连接器在机器驱动器中寄存器struct snd_soc_cardstruct snd_soc_dapm_widget部分定义。 大多数情况下,这些电路板连接器是虚拟的。 它们只是用编解码器引脚连接的逻辑贴纸(这次是真实的)。 下面列出了由imx-sgtl5000机器驱动sound/soc/fsl/imx-sgtl5000.c(其文档为Documentation/devicetree/bindings/sound/imx-audio- sgtl5000.txt)定义的连接器,该驱动到目前为止已作为示例给出:

static const struct snd_soc_dapm_widget imx_sgtl5000_dapm_widgets[] = { 
    SND_SOC_DAPM_MIC("Mic Jack", NULL),
    SND_SOC_DAPM_LINE("Line In Jack", NULL),
    SND_SOC_DAPM_HP("Headphone Jack", NULL),
    SND_SOC_DAPM_SPK("Line Out Jack", NULL),
    SND_SOC_DAPM_SPK("Ext Spk", NULL),
};

下一节将把该连接器连接到编解码器引脚。

机器工艺路线

最终的机器路由可以是静态的(即从机器驱动本身填充),也可以从设备树中填充。 此外,机器驱动器可以通过使用SND_SOC_DAPM_SUPPLYSND_SOC_DAPM_REGULATOR_SUPPLY连接到已在编解码器驱动器中定义的电源微件来可选地扩展编解码器功率图并成为音频子系统的音频功率图。

设备树路由

让我们以我们机器的节点为例,它将一个 i.MX6 SoC 连接到一个 sgtl5000 编解码器(此摘录可以在机器文档中找到):

sound {
    compatible = "fsl,imx51-babbage-sgtl5000",
                 "fsl,imx-audio-sgtl5000";
    model = "imx51-babbage-sgtl5000";
    ssi-controller = <&ssi1>;
    audio-codec = <&sgtl5000>;
    audio-routing = "MIC_IN", "Mic Jack",
                    "Mic Jack", "Mic Bias",
                    "Headphone Jack", "HP_OUT";
[...]
};

来自设备树的路由要求以某种格式给出音频映射。 也就是说,条目被解析为字符串对,第一对是连接的接收器,第二对是连接的源。 大多数时间,这些连接被具体化为编解码器引脚和板连接器映射。 源和接收器的有效名称取决于硬件绑定,如下所示:

  • 编解码器:这应该是定义了其名称在这里使用的管脚。
  • 机器:这应该是定义了这里使用的名称的连接器或插孔。

在前面的摘录中,你注意到了什么? 我们可以看到MIC_INHP_OUT"Mic Bias",它们是编解码器管脚(来自编解码器驱动),以及"Mic Jack""Headphone Jack",它们已经在机器驱动中定义为板连接器。

为了使用 DT 中定义的路线,机器驱动必须调用snd_soc_of_parse_audio_routing(),其原型如下:

int snd_soc_of_parse_card_name(struct snd_soc_card *card,
                               const char *prop);

在前面的原型中,card表示要为其解析路由的声卡,prop是包含设备树节点中的路由的属性的名称。 此函数在成功时返回0,在出错时返回负错误代码。

静态路由

静态路由包括从机器驱动定义 DAPM 路由映射,并将其直接分配给声卡,如下所示:

static const struct snd_soc_dapm_widget rk_dapm_widgets[] = {
    SND_SOC_DAPM_HP("Headphone", NULL),
    SND_SOC_DAPM_MIC("Headset Mic", NULL),
    SND_SOC_DAPM_MIC("Int Mic", NULL),
    SND_SOC_DAPM_SPK("Speaker", NULL),
};
/* Connection to the codec pin */
static const struct snd_soc_dapm_route rk_audio_map[] = {
    {"IN34", NULL, "Headset Mic"},
    {"Headset Mic", NULL, "MICBIAS"},
    {"DMICL", NULL, "Int Mic"},
    {"Headphone", NULL, "HPL"},
    {"Headphone", NULL, "HPR"},
    {"Speaker", NULL, "SPKL"},
    {"Speaker", NULL, "SPKR"},
};
static struct snd_soc_card snd_soc_card_rk = {
    .name = "ROCKCHIP-I2S",
    .owner = THIS_MODULE,
[...]
    .dapm_widgets = rk_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(rk_dapm_widgets),
    .dapm_routes = rk_audio_map,
    .num_dapm_routes = ARRAY_SIZE(rk_audio_map),
    .controls = rk_mc_controls,
    .num_controls = ARRAY_SIZE(rk_mc_controls),
};

前面的片段摘自sound/soc/rockchip/rockchip_rt5645.c。 通过这种方式使用它,就不需要使用snd_soc_of_parse_audio_routing()。 然而,使用这种方法的一个缺点是,在不重新编译内核的情况下,LE 不可能更改路由。 接下来,我们将了解计时和格式化注意事项。

计时和格式化注意事项

在深入研究这一节之前,让我们花一些时间在snd_soc_dai_link->ops字段上。 此字段的类型为struct snd_soc_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);
};

此结构中的这些回调字段应该会提醒您在类型为struct snd_soc_dai_opssnd_soc_dai_driver->ops字段中定义的回调字段。 在 DAI 链路内,这些回调表示 DAI 链路的计算机级别 PCM 操作,而在struct snd_soc_dai_driver中,它们要么是特定于 DAI 的编解码器,要么是特定于 CPU 的 DAI。

当 PCM 子流打开时(当有人打开捕获/播放设备时),ALSA 调用startup(),而在设置音频流时调用hw_params()。 机器驱动器可以从这两个回调内配置 DAI 链路数据格式。 hw_params()提供了接收流参数(通道计数格式采样率等)的优点。

CPU DAI 和编解码器之间的数据格式配置应一致。 ASOC 核心提供助手功能来更改这些配置。 这些建议如下:

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)

在前面的助手列表中,snd_soc_dai_set_fmt为时钟主从关系、音频格式和信号反转等设置 DAI 格式;snd_soc_dai_set_pll配置时钟 PLL;snd_soc_dai_set_sysclk配置时钟源;snd_soc_dai_set_clkdiv配置时钟分频器。 这些帮助器中的每一个都将在底层 DAI 的驱动操作中调用适当的回调。 例如,使用 CPU DAI 调用snd_soc_dai_set_fmt()将调用此 CPU DAI 的dai->driver->ops->set_fmt回调。

以下是可以分配给 DAIS 或dai_link.format字段的格式/标志的实际列表:

  • Format: Configured through snd_soc_dai_set_fmt():

    A)时钟主/从

    A)SND_SOC_DAIFMT_CBM_CFM:CPU 是位时钟和帧同步的从机。 这也意味着编解码器是两者的主控。

    B)SND_SOC_DAIFMT_CBS_CFS。 CPU 是位时钟和帧同步的主机。 这也意味着编解码器是两者的从属。

    C)SND_SOC_DAIFMT_CBM_CFS。 CPU 是位时钟的从机,帧同步的主机。 这也意味着编解码器是前者的主编解码器和后者的从属编解码器。

    B)音频格式

    A)SND_SOC_DAIFMT_DSP_A:帧同步为 1 位时钟宽、1 位延迟。

    B)SND_SOC_DAIFMT_DSP_B:帧同步为 1 位时钟宽度,0 位延迟。 此格式可用于 TDM 协议。

    C)SND_SOC_DAIFMT_I2S:帧同步为 1 个音频字宽、1 位延迟、I2S 模式。

    D)SND_SOC_DAIFMT_RIGHT_J:右对齐模式。

    E)SND_SOC_DAIFMT_LEFT_J:左对齐模式。

    F)SND_SOC_DAIFMT_DSP_A:帧同步为 1 位时钟宽、1 位延迟。

    G)SND_SOC_DAIFMT_AC97:AC97 模式。

    H)SND_SOC_DAIFMT_PDM:脉冲密度调制。

    I)SND_SOC_DAIFMT_DSP_B:帧同步为 1 位时钟宽、1 位延迟。

    C)信号反转

    A)SND_SOC_DAIFMT_NB_NF:正常位时钟,正常帧同步。 CPU 发送器在位时钟的下降沿移出数据,接收器在上升沿采样数据。 CPU 帧同步发生器在帧同步的上升沿开始帧。 对于 CPU 侧的 I2S,建议使用此参数。

    B)SND_SOC_DAIFMT_NB_IF:正常位时钟,反相帧同步。 CPU 发送器在位时钟的下降沿移出数据,接收器在上升沿采样数据。 CPU 帧同步发生器在帧同步的下降沿开始帧。

    C)SND_SOC_DAIFMT_IB_NF:反相位时钟,正常帧同步。 CPU 发送器在位时钟的上升沿移出数据,接收器在下降沿采样数据。 CPU 帧同步发生器在帧同步的上升沿开始帧。

    D)SND_SOC_DAIFMT_IB_IF:反相位时钟,反相帧同步。 CPU 发送器在位时钟的上升沿移出数据,接收器在下降沿采样数据。 CPU 帧同步发生器在帧同步的下降沿开始帧。 此配置可用于 PCM 模式(例如蓝牙或基于调制解调器的音频芯片)。

  • Clock source: Configured through snd_soc_dai_set_sysclk(). The following are the direction parameters letting ALSA know which clock is used:

    A)SND_SOC_CLOCK_IN:表示 sysclock 使用内部时钟。

    B)SND_SOC_CLOCK_OUT:表示 sysclock 使用外部时钟。

  • 时钟分频器:通过snd_soc_dai_set_clkdiv()配置。

前面的标志是可以在dai_link->dai_fmt字段中设置或从机器驱动内分配给编解码器或 CPU DAI 的可能值。 以下是典型的hw_param()实施:

static int foo_hw_params(struct snd_pcm_substream *substream,
                          struct snd_pcm_hw_params *params)
{
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
    struct snd_soc_dai *codec_dai = rtd->codec_dai;
    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
    unsigned int pll_out = 24000000;
    int ret = 0;
    /* set the cpu DAI configuration */
    ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S |
                              SND_SOC_DAIFMT_NB_NF |                               SND_SOC_DAIFMT_CBM_CFM);
    if (ret < 0)
        return ret;
    /* set codec DAI configuration */
    ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S |
                              SND_SOC_DAIFMT_NB_NF |                               SND_SOC_DAIFMT_CBM_CFM);
    if (ret < 0)
        return ret;
    /* set the codec PLL */
    ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, 0,
                          pll_out, params_rate(params) * 256);
    if (ret < 0)
        return ret;
    /* set the codec system clock */
    ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1,
                  params_rate(params) * 256, SND_SOC_CLOCK_IN);
    if (ret < 0)
        return ret;
    return 0;
}

在前面的foo_hw_params()函数的实现中,我们可以看到编解码器和平台 DAI 是如何使用格式和时钟设置进行配置的。 现在我们进入机器驱动实现的最后一步,它包括注册声卡,声卡是在系统上执行音频操作的设备。

声卡注册

声卡在内核中表示为struct snd_soc_card的实例,定义如下:

struct snd_soc_card {
    const char *name;
    struct module *owner;
    [...]
    /* 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);
    [...]
    /* CPU <--> Codec DAI links	*/
    struct snd_soc_dai_link *dai_link;
    int num_links;
    const struct snd_kcontrol_new *controls;
    int num_controls;
    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;
[...]
};

出于可读性考虑,仅列出了相关字段,完整定义可在https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc.h#L1010中找到。 话虽如此,下面的列表描述了我们列出的字段:

  • name是声卡的名称。
  • owner是此声卡的模块所有者。
  • dai_link是组成此声卡的 DAI 链接数组,num_links指定数组中的条目数。
  • controls是包含由机器驱动静态定义和设置的控件的数组,num_controls指定数组中的条目数。
  • dapm_widgets是包含由机器驱动静态定义和设置的 DAPM 小部件的数组,num_dapm_widgets指定数组中的条目数。
  • damp_routes是包含由机器驱动静态定义和设置的 DAPM 路由的数组,num_dapm_routes指定数组中的条目数。
  • of_dapm_widgets表示从 DT(通过snd_soc_of_parse_audio_simple_widgets())馈送的 DAPM 小部件,num_of_dapm_widgets是小部件条目的实际数量。
  • of_dapm_routes表示从 DT(通过snd_soc_of_parse_audio_routing())馈送的 DAPM 路由,num_of_dapm_routes是路由条目的实际数量。

设置好声音卡结构后,机器可以使用devm_snd_soc_register_card()方法进行注册,其原型如下:

int devm_snd_soc_register_card(struct device *dev,
                               struct snd_soc_card *card);

在前面的原型中,dev表示用于管理卡的底层设备,card是先前设置的实际声卡数据结构。 此函数在成功时返回0。 但是,当调用此函数时,将探测每个组件驱动和 DAI 驱动。 因此,将为 CPU 和编解码器调用component_driver->probe()dai_driver->probe()方法。 此外,将为每个成功探测的 DAI 链路创建新的 PCM 设备。

以下摘录(摘自使用 MAX90809 编解码器的主板的 RockChip 机器 ASOC 驱动,在内核源代码的sound/soc/rockchip/rockchip_max98090.c中实现)将显示通过 DAI 链路配置创建声卡的整个过程,从小部件到路由。 让我们首先为这台机器定义一个小部件和控件,以及用于配置 CPU 和编解码器 DAI 的回调:

static const struct snd_soc_dapm_widget rk_dapm_widgets[] = { 
    [...]
};
static const struct snd_soc_dapm_route rk_audio_map[] = {
    [...]
};
static const struct snd_kcontrol_new rk_mc_controls[] = {
    SOC_DAPM_PIN_SWITCH("Headphone"),
    SOC_DAPM_PIN_SWITCH("Headset Mic"),
    SOC_DAPM_PIN_SWITCH("Int Mic"),
    SOC_DAPM_PIN_SWITCH("Speaker"),
};
static const struct snd_soc_ops rk_aif1_ops = {
    .hw_params = rk_aif1_hw_params,
};
static struct snd_soc_dai_link rk_dailink = {
    .name = "max98090",
    .stream_name = "Audio",
    .codec_dai_name = "HiFi",
    .ops = &rk_aif1_ops,
    /* set max98090 as slave */
    .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
                 SND_SOC_DAIFMT_CBS_CFS,
};

在前面的摘录中,可以在原始代码实现文件中看到rk_aif1_hw_params。 现在是用于构建声卡的数据结构,定义如下:

static struct snd_soc_card snd_soc_card_rk = {
    .name = "ROCKCHIP-I2S",
    .owner = THIS_MODULE,
    .dai_link = &rk_dailink,
    .num_links = 1,
    .dapm_widgets = rk_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(rk_dapm_widgets),
    .dapm_routes = rk_audio_map,
    .num_dapm_routes = ARRAY_SIZE(rk_audio_map),
    .controls = rk_mc_controls,
    .num_controls = ARRAY_SIZE(rk_mc_controls),
};

此声卡最终在驱动probe方法中创建,如下所示:

static int snd_rk_mc_probe(struct platform_device *pdev)
{
    int ret = 0;
    struct snd_soc_card *card = &snd_soc_card_rk;
    struct device_node *np = pdev->dev.of_node;
[...]
    card->dev = &pdev->dev;
    /* Assign codec, cpu and platform node */
    rk_dailink.codec_of_node = of_parse_phandle(np,
                                  "rockchip,audio-codec", 0);
    rk_dailink.cpu_of_node = of_parse_phandle(np,
                                "rockchip,i2s-controller", 0);
    rk_dailink.platform_of_node = rk_dailink.cpu_of_node;
[...]
    ret = snd_soc_of_parse_card_name(card, "rockchip,model");
    ret = devm_snd_soc_register_card(&pdev->dev, card);
[...]
}

同样,前面的三个代码块摘自sound/soc/rockchip/rockchip_max98090.c。 到目前为止,我们已经了解了机器驱动的主要用途,即将编解码器驱动和 CPU 驱动绑定在一起,并定义音频路径。 话虽如此,在某些情况下,我们可能需要更少的代码。 这种情况涉及板,其中 CPU 和编解码器在绑定到一起之前都不需要特殊的黑客攻击。 在本例中,ASOC 框架提供了简单卡机器驱动,将在下一节中介绍。

利用简单卡片机驱动

在情况下,您的主板不需要来自编解码器或 CPU DAI 的任何黑客攻击。 ASOC 内核提供了simple-audio机器驱动,该驱动可用于描述 DT 中的整个声卡。 以下是这样一个节点的摘录:

sound {
    compatible ="simple-audio-card";
    simple-audio-card,name ="VF610-Tower-Sound-Card";
    simple-audio-card,format ="left_j";
    simple-audio-card,bitclock-master = <&dailink0_master>;
    simple-audio-card,frame-master = <&dailink0_master>;
    simple-audio-card,widgets ="Microphone","Microphone Jack",
                               "Headphone","Headphone Jack",                              
                               "Speaker","External Speaker";
    simple-audio-card,routing = "MIC_IN","Microphone Jack",
                                "Headphone Jack","HP_OUT",
                                "External Speaker","LINE_OUT";
    simple-audio-card,cpu {
        sound-dai = <&sh_fsi20>;
    };
    dailink0_master: simple-audio-card,codec {
        sound-dai = <&ak4648>;
        clocks = <&osc>;
    };
};

这在Documentation/devicetree/bindings/sound/simple-card.txt中有详细说明。 在前面的摘录中,我们可以看到机器小部件和路由图被指定,因为我们 ll 被引用为编解码器和 CPU 节点。 既然我们已经熟悉了简单卡片机器驱动,我们就可以利用它,并尽量不编写我们自己的机器驱动。 话虽如此,但在某些情况下,编解码器设备无法分离,这会改变机器的写入方式。 这样的音频设备被称为无编解码器和卡,我们将在下一节讨论它们。

无编解码器声卡

可能会有从外部系统采样数字音频数据的情况,例如当使用 SPDIF 接口时,因此数据被预格式化。 在这种情况下,声卡注册是相同的,但 ASOC 核心需要注意这种特殊情况。

对于输出,DAI 链接对象的.capture_only字段应该是false,而.playback_only应该是true。 反之亦然,应该对输入进行反转。 此外,机器驱动器必须将 DAI 链接的codec_dai_namecodec_name分别设置为"snd-soc-dummy-dai""snd-soc-dummy"。 例如,imx-spdif机器驱动(sound/soc/fsl/imx-spdif.c)就是这种情况,它包含以下摘录:

data->dai.name = "S/PDIF PCM";
data->dai.stream_name = "S/PDIF PCM";
data->dai.codecs->dai_name = "snd-soc-dummy-dai";
data->dai.codecs->name = "snd-soc-dummy";
data->dai.cpus->of_node = spdif_np;
data->dai.platforms->of_node = spdif_np;
data->dai.playback_only = true;
data->dai.capture_only = true;
if (of_property_read_bool(np, "spdif-out"))
    data->dai.capture_only = false;
if (of_property_read_bool(np, "spdif-in"))
    data->dai.playback_only = false;
if (data->dai.playback_only && data->dai.capture_only) {
    dev_err(&pdev->dev, "no enabled S/PDIF DAI link\n");
    goto end;
}

您可以在Documentation/devicetree/bindings/sound/imx-audio-spdif.txt中找到该驱动的绑定文档。 在机器类驱动研究的最后,完成了整个 ASOC 类驱动的开发。 在这个机器类驱动中,除了在代码中绑定 CPU 和 Codec,以及提供设置回调之外,我们还看到了如何通过使用 Simple-Card MACHINE 驱动并在设备树中实现其余部分来避免编写代码。

摘要

在本章中,我们介绍了 ASOC 机器类驱动的体系结构,它代表了本 ASOC 系列的最后一个元素。 我们不仅了解了如何绑定平台和子设备驱动,还了解了如何定义音频数据的路由。

在下一章中,我们将介绍另一个 Linux 媒体子系统,即 V4L2,它用于处理视频设备。